diff --git a/.coveragerc b/.coveragerc index 693959684f1..35c47de4160 100644 --- a/.coveragerc +++ b/.coveragerc @@ -166,7 +166,6 @@ omit = homeassistant/components/dsmr_reader/* homeassistant/components/dte_energy_bridge/sensor.py homeassistant/components/dublin_bus_transport/sensor.py - homeassistant/components/duke_energy/sensor.py homeassistant/components/dunehd/media_player.py homeassistant/components/dwd_weather_warnings/sensor.py homeassistant/components/dweet/* @@ -248,7 +247,6 @@ omit = homeassistant/components/fritzbox/* homeassistant/components/fritzbox_callmonitor/sensor.py homeassistant/components/fritzbox_netmonitor/sensor.py - homeassistant/components/fritzdect/switch.py homeassistant/components/fronius/sensor.py homeassistant/components/frontier_silicon/media_player.py homeassistant/components/futurenow/light.py @@ -387,7 +385,6 @@ omit = homeassistant/components/linode/* homeassistant/components/linux_battery/sensor.py homeassistant/components/lirc/* - homeassistant/components/liveboxplaytv/media_player.py homeassistant/components/llamalab_automate/notify.py homeassistant/components/lockitron/lock.py homeassistant/components/logi_circle/__init__.py @@ -412,9 +409,15 @@ omit = homeassistant/components/mcp23017/* homeassistant/components/media_extractor/* homeassistant/components/mediaroom/media_player.py + homeassistant/components/melcloud/__init__.py + homeassistant/components/melcloud/climate.py + homeassistant/components/melcloud/sensor.py homeassistant/components/message_bird/notify.py homeassistant/components/met/weather.py - homeassistant/components/meteo_france/* + homeassistant/components/meteo_france/__init__.py + homeassistant/components/meteo_france/const.py + homeassistant/components/meteo_france/sensor.py + homeassistant/components/meteo_france/weather.py homeassistant/components/meteoalarm/* homeassistant/components/metoffice/sensor.py homeassistant/components/metoffice/weather.py @@ -424,6 +427,10 @@ omit = homeassistant/components/mikrotik/device_tracker.py homeassistant/components/mill/climate.py homeassistant/components/mill/const.py + homeassistant/components/minecraft_server/__init__.py + homeassistant/components/minecraft_server/binary_sensor.py + homeassistant/components/minecraft_server/const.py + homeassistant/components/minecraft_server/sensor.py homeassistant/components/minio/* homeassistant/components/mitemp_bt/sensor.py homeassistant/components/mjpeg/camera.py @@ -603,6 +610,7 @@ omit = homeassistant/components/russound_rnet/media_player.py homeassistant/components/sabnzbd/* homeassistant/components/saj/sensor.py + homeassistant/components/salt/device_tracker.py homeassistant/components/satel_integra/* homeassistant/components/scrape/sensor.py homeassistant/components/scsgate/* @@ -622,8 +630,6 @@ omit = homeassistant/components/shodan/sensor.py homeassistant/components/sht31/sensor.py homeassistant/components/sigfox/sensor.py - homeassistant/components/signal_messenger/__init__.py - homeassistant/components/signal_messenger/notify.py homeassistant/components/simplepush/notify.py homeassistant/components/simplisafe/__init__.py homeassistant/components/simplisafe/alarm_control_panel.py @@ -750,7 +756,6 @@ omit = homeassistant/components/twentemilieu/sensor.py homeassistant/components/twilio_call/notify.py homeassistant/components/twilio_sms/notify.py - homeassistant/components/twitch/sensor.py homeassistant/components/twitter/notify.py homeassistant/components/ubee/device_tracker.py homeassistant/components/ubus/device_tracker.py @@ -781,10 +786,10 @@ omit = homeassistant/components/vesync/switch.py homeassistant/components/viaggiatreno/sensor.py homeassistant/components/vicare/* + homeassistant/components/vilfo/__init__.py + homeassistant/components/vilfo/sensor.py + homeassistant/components/vilfo/const.py homeassistant/components/vivotek/camera.py - homeassistant/components/vizio/__init__.py - homeassistant/components/vizio/const.py - homeassistant/components/vizio/media_player.py homeassistant/components/vlc/media_player.py homeassistant/components/vlc_telnet/media_player.py homeassistant/components/volkszaehler/sensor.py diff --git a/.github/stale.yml b/.github/stale.yml index 44cd95e1f5d..e75d791a57c 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -52,4 +52,14 @@ markComment: > limitPerRun: 30 # Limit to only `issues` or `pulls` -only: issues +# only: issues + +# Handle pull requests a little bit faster and with an adjusted comment. +pulls: + daysUntilStale: 30 + markComment: > + There hasn't been any activity on this pull request recently. This pull + request has been automatically marked as stale because of that and will + be closed if no further activity occurs within 7 days. + + Thank you for your contributions. diff --git a/.pre-commit-config-all.yaml b/.pre-commit-config-all.yaml deleted file mode 100644 index a6b882e617b..00000000000 --- a/.pre-commit-config-all.yaml +++ /dev/null @@ -1,59 +0,0 @@ -# This configuration includes the full set of hooks we use. In -# addition to the defaults (see .pre-commit-config.yaml), this -# includes hooks that require our development and test dependencies -# installed and the virtualenv containing them active by the time -# pre-commit runs to produce correct results. -# -# If this is not a problem for your workflow, using this config is -# recommended, install it with -# pre-commit install --config .pre-commit-config-all.yaml -# Otherwise, see the default .pre-commit-config.yaml for a lighter one. - -repos: -- repo: https://github.com/psf/black - rev: 19.10b0 - hooks: - - id: black - args: - - --safe - - --quiet - files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$ -- repo: https://github.com/PyCQA/flake8 - rev: 3.7.9 - hooks: - - id: flake8 - additional_dependencies: - - flake8-docstrings==1.5.0 - - pydocstyle==5.0.2 - files: ^(homeassistant|script|tests)/.+\.py$ -- repo: https://github.com/PyCQA/bandit - rev: 1.6.2 - hooks: - - id: bandit - args: - - --quiet - - --format=custom - - --configfile=tests/bandit.yaml - files: ^(homeassistant|script|tests)/.+\.py$ -- repo: https://github.com/pre-commit/mirrors-isort - rev: v4.3.21 - hooks: - - id: isort -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.4.0 - hooks: - - id: check-json -# Using a local "system" mypy instead of the mypy hook, because its -# results depend on what is installed. And the mypy hook runs in a -# virtualenv of its own, meaning we'd need to install and maintain -# 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/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1f27e82b6d9..a340aa7ae67 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,10 +1,3 @@ -# This configuration includes the default, minimal set of hooks to be -# run on all commits. It requires no specific setup and one can just -# start using pre-commit with it. -# -# See .pre-commit-config-all.yaml for a more complete one that comes -# with a better coverage at the cost of some specific setup needed. - repos: - repo: https://github.com/psf/black rev: 19.10b0 @@ -14,6 +7,15 @@ repos: - --safe - --quiet files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$ +- repo: https://github.com/codespell-project/codespell + rev: v1.16.0 + hooks: + - id: codespell + args: + - --ignore-words-list=hass,alot,datas,dof,dur,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing + - --skip="./.*,*.json" + - --quiet-level=2 + exclude_types: [json] - repo: https://gitlab.com/pycqa/flake8 rev: 3.7.9 hooks: @@ -39,3 +41,16 @@ repos: rev: v2.4.0 hooks: - id: check-json +- repo: local + hooks: + # Run mypy through our wrapper script in order to get the possible + # pyenv and/or virtualenv activated; it may not have been e.g. if + # committing from a GUI tool that was not launched from an activated + # shell. + - id: mypy + name: mypy + entry: script/run-in-env.sh mypy + language: script + types: [python] + require_serial: true + files: ^homeassistant/.+\.py$ diff --git a/CODEOWNERS b/CODEOWNERS index 6983d13fc8b..a8057197827 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -35,6 +35,7 @@ homeassistant/components/arest/* @fabaff homeassistant/components/asuswrt/* @kennedyshead homeassistant/components/aten_pe/* @mtdcr homeassistant/components/atome/* @baqs +homeassistant/components/august/* @bdraco homeassistant/components/aurora_abb_powerone/* @davet2001 homeassistant/components/auth/* @home-assistant/core homeassistant/components/automatic/* @armills @@ -83,6 +84,7 @@ homeassistant/components/discogs/* @thibmaek homeassistant/components/doorbird/* @oblogic7 homeassistant/components/dsmr_reader/* @depl0y homeassistant/components/dweet/* @fabaff +homeassistant/components/dynalite/* @ziv1234 homeassistant/components/dyson/* @etheralm homeassistant/components/ecobee/* @marthoc homeassistant/components/ecovacs/* @OverloadUT @@ -117,6 +119,7 @@ homeassistant/components/freebox/* @snoof85 homeassistant/components/fronius/* @nielstron homeassistant/components/frontend/* @home-assistant/frontend homeassistant/components/garmin_connect/* @cyberjunky +homeassistant/components/gdacs/* @exxamalte homeassistant/components/gearbest/* @HerrHofrat homeassistant/components/geniushub/* @zxdavb homeassistant/components/geo_rss_events/* @exxamalte @@ -184,14 +187,13 @@ homeassistant/components/kef/* @basnijholt homeassistant/components/keyboard_remote/* @bendavid homeassistant/components/knx/* @Julius2342 homeassistant/components/kodi/* @armills -homeassistant/components/konnected/* @heythisisnate +homeassistant/components/konnected/* @heythisisnate @kit-klein homeassistant/components/lametric/* @robbiet480 homeassistant/components/launch_library/* @ludeeus homeassistant/components/lcn/* @alengwenus homeassistant/components/life360/* @pnbruckner homeassistant/components/linky/* @Quentame homeassistant/components/linux_battery/* @fabaff -homeassistant/components/liveboxplaytv/* @pschmitt homeassistant/components/local_ip/* @issacg homeassistant/components/logger/* @home-assistant/core homeassistant/components/logi_circle/* @evanjd @@ -204,14 +206,16 @@ homeassistant/components/mastodon/* @fabaff homeassistant/components/matrix/* @tinloaf homeassistant/components/mcp23017/* @jardiamj homeassistant/components/mediaroom/* @dgomes +homeassistant/components/melcloud/* @vilppuvuorinen homeassistant/components/melissa/* @kennedyshead homeassistant/components/met/* @danielhiversen -homeassistant/components/meteo_france/* @victorcerutti @oncleben31 +homeassistant/components/meteo_france/* @victorcerutti @oncleben31 @Quentame homeassistant/components/meteoalarm/* @rolfberkenbosch homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel homeassistant/components/mikrotik/* @engrbm87 homeassistant/components/mill/* @danielhiversen homeassistant/components/min_max/* @fabaff +homeassistant/components/minecraft_server/* @elmurato homeassistant/components/minio/* @tkislan homeassistant/components/mobile_app/* @robbiet480 homeassistant/components/modbus/* @adamchengtkc @@ -275,7 +279,7 @@ homeassistant/components/quantum_gateway/* @cisasteelersfan homeassistant/components/qwikswitch/* @kellerza homeassistant/components/rainbird/* @konikvranik homeassistant/components/raincloud/* @vanstinator -homeassistant/components/rainforest_eagle/* @gtdiehl +homeassistant/components/rainforest_eagle/* @gtdiehl @jcalbert homeassistant/components/rainmachine/* @bachya homeassistant/components/random/* @fabaff homeassistant/components/repetier/* @MTrab @@ -285,6 +289,7 @@ homeassistant/components/rmvtransport/* @cgtobi homeassistant/components/roomba/* @pschmitt homeassistant/components/safe_mode/* @home-assistant/core homeassistant/components/saj/* @fredericvl +homeassistant/components/salt/* @bjornorri homeassistant/components/samsungtv/* @escoand homeassistant/components/scene/* @home-assistant/core homeassistant/components/scrape/* @fabaff @@ -354,6 +359,7 @@ homeassistant/components/time_date/* @fabaff homeassistant/components/tmb/* @alemuro homeassistant/components/todoist/* @boralyl homeassistant/components/toon/* @frenck +homeassistant/components/totalconnect/* @austinmroczek homeassistant/components/tplink/* @rytilahti homeassistant/components/traccar/* @ludeeus homeassistant/components/tradfri/* @ggravlingen @@ -379,6 +385,7 @@ homeassistant/components/versasense/* @flamm3blemuff1n homeassistant/components/version/* @fabaff homeassistant/components/vesync/* @markperdue @webdjoe homeassistant/components/vicare/* @oischinger +homeassistant/components/vilfo/* @ManneW homeassistant/components/vivotek/* @HarlemSquirrel homeassistant/components/vizio/* @raman325 homeassistant/components/vlc_telnet/* @rodripf diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 546b63950fe..4c6a353d775 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -43,7 +43,11 @@ stages: . venv/bin/activate pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - pre-commit install-hooks --config .pre-commit-config-all.yaml + pre-commit install-hooks + - script: | + . venv/bin/activate + pre-commit run codespell --all-files + displayName: 'Run codespell' - script: | . venv/bin/activate pre-commit run flake8 --all-files @@ -94,7 +98,7 @@ stages: . venv/bin/activate pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - pre-commit install-hooks --config .pre-commit-config-all.yaml + pre-commit install-hooks - script: | . venv/bin/activate pre-commit run black --all-files --show-diff-on-failure @@ -190,8 +194,8 @@ stages: . venv/bin/activate pip install -e . -r requirements_test.txt -c homeassistant/package_constraints.txt - pre-commit install-hooks --config .pre-commit-config-all.yaml + pre-commit install-hooks - script: | . venv/bin/activate - pre-commit run --config .pre-commit-config-all.yaml mypy --all-files + pre-commit run mypy --all-files displayName: 'Run mypy' diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 135057f2ae4..c98f12dfac6 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -163,7 +163,7 @@ stages: git commit -am "Bump Home Assistant $version" git push - displayName: 'Update version files' + displayName: "Update version files" - job: 'ReleaseDocker' pool: vmImage: 'ubuntu-latest' diff --git a/.codecov.yml b/codecov.yml similarity index 92% rename from .codecov.yml rename to codecov.yml index be739b61809..1455c20749a 100644 --- a/.codecov.yml +++ b/codecov.yml @@ -13,4 +13,7 @@ coverage: url: "secret:TgWDUM4Jw0w7wMJxuxNF/yhSOHglIo1fGwInJnRLEVPy2P2aLimkoK1mtKCowH5TFw+baUXVXT3eAqefbdvIuM8BjRR4aRji95C6CYyD0QHy4N8i7nn1SQkWDPpS8IthYTg07rUDF7s5guurkKv2RrgoCdnnqjAMSzHoExMOF7xUmblMdhBTWJgBpWEhASJy85w/xxjlsE1xoTkzeJu9Q67pTXtRcn+5kb5/vIzPSYg=" comment: require_changes: yes - branches: master + layout: reach + branches: + - master + - !dev diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 9b3cf49fa22..710b4af1cd8 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -301,7 +301,7 @@ class AuthManager: async def async_deactivate_user(self, user: models.User) -> None: """Deactivate a user.""" if user.is_owner: - raise ValueError("Unable to deactive the owner") + raise ValueError("Unable to deactivate the owner") await self._store.async_deactivate_user(user) async def async_remove_credentials(self, credentials: models.Credentials) -> None: diff --git a/homeassistant/auth/mfa_modules/__init__.py b/homeassistant/auth/mfa_modules/__init__.py index fd9e61b9d17..c2ec2260cf2 100644 --- a/homeassistant/auth/mfa_modules/__init__.py +++ b/homeassistant/auth/mfa_modules/__init__.py @@ -1,4 +1,4 @@ -"""Plugable auth modules for Home Assistant.""" +"""Pluggable auth modules for Home Assistant.""" import importlib import logging import types diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index 46cc634bcae..8da81a44a61 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -317,7 +317,7 @@ class NotifySetupFlow(SetupFlow): async def async_step_setup( self, user_input: Optional[Dict[str, str]] = None ) -> Dict[str, Any]: - """Verify user can recevie one-time password.""" + """Verify user can receive one-time password.""" errors: Dict[str, str] = {} hass = self._auth_module.hass diff --git a/homeassistant/auth/models.py b/homeassistant/auth/models.py index 08f2f375b41..8b4e6355700 100644 --- a/homeassistant/auth/models.py +++ b/homeassistant/auth/models.py @@ -31,22 +31,28 @@ class User: """A user.""" name = attr.ib(type=Optional[str]) - perm_lookup = attr.ib(type=perm_mdl.PermissionLookup, cmp=False) + perm_lookup = attr.ib(type=perm_mdl.PermissionLookup, eq=False, order=False) id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex) is_owner = attr.ib(type=bool, default=False) is_active = attr.ib(type=bool, default=False) system_generated = attr.ib(type=bool, default=False) - groups = attr.ib(type=List[Group], factory=list, cmp=False) + groups = attr.ib(type=List[Group], factory=list, eq=False, order=False) # List of credentials of a user. - credentials = attr.ib(type=List["Credentials"], factory=list, cmp=False) + credentials = attr.ib(type=List["Credentials"], factory=list, eq=False, order=False) # Tokens associated with a user. - refresh_tokens = attr.ib(type=Dict[str, "RefreshToken"], factory=dict, cmp=False) + refresh_tokens = attr.ib( + type=Dict[str, "RefreshToken"], factory=dict, eq=False, order=False + ) _permissions = attr.ib( - type=Optional[perm_mdl.PolicyPermissions], init=False, cmp=False, default=None + type=Optional[perm_mdl.PolicyPermissions], + init=False, + eq=False, + order=False, + default=None, ) @property diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 3d8523bf9ac..7d4155257db 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -1,23 +1,26 @@ """Provide methods to bootstrap a Home Assistant instance.""" import asyncio +import contextlib import logging import logging.handlers import os import sys -from time import time +from time import monotonic from typing import Any, Dict, Optional, Set +from async_timeout import timeout import voluptuous as vol from homeassistant import config as conf_util, config_entries, core, loader from homeassistant.components import http from homeassistant.const import ( EVENT_HOMEASSISTANT_CLOSE, + EVENT_HOMEASSISTANT_STOP, REQUIRED_NEXT_PYTHON_DATE, REQUIRED_NEXT_PYTHON_VER, ) from homeassistant.exceptions import HomeAssistantError -from homeassistant.setup import async_setup_component +from homeassistant.setup import DATA_SETUP, async_setup_component from homeassistant.util.logging import AsyncHandler from homeassistant.util.package import async_get_user_site, is_virtual_env from homeassistant.util.yaml import clear_secret_cache @@ -71,6 +74,7 @@ async def async_setup_hass( _LOGGER.info("Config directory: %s", config_dir) config_dict = None + basic_setup_success = False if not safe_mode: await hass.async_add_executor_job(conf_util.process_ha_config_upgrade, hass) @@ -79,19 +83,45 @@ async def async_setup_hass( config_dict = await conf_util.async_hass_config_yaml(hass) except HomeAssistantError as err: _LOGGER.error( - "Failed to parse configuration.yaml: %s. Falling back to safe mode", - err, + "Failed to parse configuration.yaml: %s. Activating safe mode", err, ) else: if not is_virtual_env(): await async_mount_local_lib_path(config_dir) - await async_from_config_dict(config_dict, hass) + basic_setup_success = ( + await async_from_config_dict(config_dict, hass) is not None + ) finally: clear_secret_cache() - if safe_mode or config_dict is None: + if config_dict is None: + safe_mode = True + + elif not basic_setup_success: + _LOGGER.warning("Unable to set up core integrations. Activating safe mode") + safe_mode = True + + elif ( + "frontend" in hass.data.get(DATA_SETUP, {}) + and "frontend" not in hass.config.components + ): + _LOGGER.warning("Detected that frontend did not load. Activating safe mode") + # Ask integrations to shut down. It's messy but we can't + # do a clean stop without knowing what is broken + hass.async_track_tasks() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP, {}) + with contextlib.suppress(asyncio.TimeoutError): + async with timeout(10): + await hass.async_block_till_done() + + safe_mode = True + hass = core.HomeAssistant() + hass.config.config_dir = config_dir + + if safe_mode: _LOGGER.info("Starting in safe mode") + hass.config.safe_mode = True http_conf = (await http.async_get_last_config(hass)) or {} @@ -110,7 +140,26 @@ async def async_from_config_dict( Dynamically loads required components and its dependencies. This method is a coroutine. """ - start = time() + start = monotonic() + + hass.config_entries = config_entries.ConfigEntries(hass, config) + await hass.config_entries.async_initialize() + + # Set up core. + _LOGGER.debug("Setting up %s", CORE_INTEGRATIONS) + + if not all( + await asyncio.gather( + *( + async_setup_component(hass, domain, config) + for domain in CORE_INTEGRATIONS + ) + ) + ): + _LOGGER.error("Home Assistant core failed to initialize. ") + return None + + _LOGGER.debug("Home Assistant core initialized") core_config = config.get(core.DOMAIN, {}) @@ -126,12 +175,9 @@ async def async_from_config_dict( ) return None - hass.config_entries = config_entries.ConfigEntries(hass, config) - await hass.config_entries.async_initialize() - await _async_set_up_integrations(hass, config) - stop = time() + stop = monotonic() _LOGGER.info("Home Assistant initialized in %.2fs", stop - start) if REQUIRED_NEXT_PYTHON_DATE and sys.version_info[:3] < REQUIRED_NEXT_PYTHON_VER: @@ -193,7 +239,7 @@ def async_enable_logging( pass # If the above initialization failed for any reason, setup the default - # formatting. If the above succeeds, this wil result in a no-op. + # formatting. If the above succeeds, this will result in a no-op. logging.basicConfig(format=fmt, datefmt=datefmt, level=logging.INFO) # Suppress overly verbose logs from libraries that aren't helpful @@ -264,7 +310,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 - if "safe_mode" not in config: + if not hass.config.safe_mode: domains.update(hass.config_entries.async_domains()) # Make sure the Hass.io component is loaded @@ -296,25 +342,6 @@ async def _async_set_up_integrations( return_exceptions=True, ) - # Set up core. - _LOGGER.debug("Setting up %s", CORE_INTEGRATIONS) - - if not all( - await asyncio.gather( - *( - async_setup_component(hass, domain, config) - for domain in CORE_INTEGRATIONS - ) - ) - ): - _LOGGER.error( - "Home Assistant core failed to initialize. " - "Further initialization aborted" - ) - return - - _LOGGER.debug("Home Assistant core initialized") - # Finish resolving domains for dep_domains in await resolved_domains_task: # Result is either a set or an exception. We ignore exceptions diff --git a/homeassistant/components/abode/.translations/es-419.json b/homeassistant/components/abode/.translations/es-419.json new file mode 100644 index 00000000000..f2def50d063 --- /dev/null +++ b/homeassistant/components/abode/.translations/es-419.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": "Complete su informaci\u00f3n de inicio de sesi\u00f3n de Abode" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/hu.json b/homeassistant/components/abode/.translations/hu.json new file mode 100644 index 00000000000..385334c8549 --- /dev/null +++ b/homeassistant/components/abode/.translations/hu.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Csak egyetlen Abode konfigur\u00e1ci\u00f3 enged\u00e9lyezett." + }, + "error": { + "connection_error": "Nem lehet csatlakozni az Abode-hez.", + "identifier_exists": "Fi\u00f3k m\u00e1r regisztr\u00e1lva van", + "invalid_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u0151 adatok" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Email c\u00edm" + }, + "title": "T\u00f6ltse ki az Abode bejelentkez\u00e9si adatait" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/pl.json b/homeassistant/components/abode/.translations/pl.json index c3f3b8f2c88..d086aaca395 100644 --- a/homeassistant/components/abode/.translations/pl.json +++ b/homeassistant/components/abode/.translations/pl.json @@ -5,7 +5,7 @@ }, "error": { "connection_error": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z Abode.", - "identifier_exists": "Konto zosta\u0142o ju\u017c zarejestrowane", + "identifier_exists": "Konto jest ju\u017c zarejestrowane.", "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce" }, "step": { diff --git a/homeassistant/components/abode/.translations/sv.json b/homeassistant/components/abode/.translations/sv.json new file mode 100644 index 00000000000..9a59e4c2007 --- /dev/null +++ b/homeassistant/components/abode/.translations/sv.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Endast en enda konfiguration av Abode \u00e4r till\u00e5ten." + }, + "error": { + "connection_error": "Det gick inte att ansluta till Abode.", + "identifier_exists": "Kontot \u00e4r redan registrerat.", + "invalid_credentials": "Ogiltiga autentiseringsuppgifter." + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "username": "E-postadress" + }, + "title": "Fyll i din inloggningsinformation f\u00f6r Abode" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/switch.py b/homeassistant/components/abode/switch.py index bbe3f01f488..d6773e10ca1 100644 --- a/homeassistant/components/abode/switch.py +++ b/homeassistant/components/abode/switch.py @@ -11,6 +11,8 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) +DEVICE_TYPES = [CONST.TYPE_SWITCH, CONST.TYPE_VALVE] + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Abode switch devices.""" @@ -18,8 +20,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [] - for device in data.abode.get_devices(generic_type=CONST.TYPE_SWITCH): - entities.append(AbodeSwitch(data, device)) + for device_type in DEVICE_TYPES: + for device in data.abode.get_devices(generic_type=device_type): + entities.append(AbodeSwitch(data, device)) for automation in data.abode.get_automations(generic_type=CONST.TYPE_AUTOMATION): entities.append( diff --git a/homeassistant/components/adguard/.translations/es-419.json b/homeassistant/components/adguard/.translations/es-419.json index ed8e0c3a358..eb3274f19b6 100644 --- a/homeassistant/components/adguard/.translations/es-419.json +++ b/homeassistant/components/adguard/.translations/es-419.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "Esta integraci\u00f3n requiere AdGuard Home {minimal_version} o superior, tiene {current_version}. Actualice su complemento Hass.io AdGuard Home.", + "adguard_home_outdated": "Esta integraci\u00f3n requiere AdGuard Home {minimal_version} o superior, tiene {current_version}.", "existing_instance_updated": "Se actualiz\u00f3 la configuraci\u00f3n existente.", "single_instance_allowed": "Solo se permite una \u00fanica configuraci\u00f3n de AdGuard Home." }, diff --git a/homeassistant/components/adguard/.translations/sv.json b/homeassistant/components/adguard/.translations/sv.json index 22bd81e3e97..519ecef52db 100644 --- a/homeassistant/components/adguard/.translations/sv.json +++ b/homeassistant/components/adguard/.translations/sv.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "Den h\u00e4r integrationen kr\u00e4ver AdGuard Home {minimal_version} eller senare, du har {current_version}. Uppdatera ditt Hass.io AdGuard Home-till\u00e4gg.", + "adguard_home_outdated": "Den h\u00e4r integrationen kr\u00e4ver AdGuard Home {minimal_version} eller senare, du har {current_version}.", "existing_instance_updated": "Uppdaterade existerande konfiguration.", "single_instance_allowed": "Endast en enda konfiguration av AdGuard Home \u00e4r till\u00e5ten." }, diff --git a/homeassistant/components/airly/.translations/es-419.json b/homeassistant/components/airly/.translations/es-419.json new file mode 100644 index 00000000000..74924493863 --- /dev/null +++ b/homeassistant/components/airly/.translations/es-419.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "La clave API no es correcta.", + "name_exists": "El nombre ya existe.", + "wrong_location": "No hay estaciones de medici\u00f3n Airly en esta \u00e1rea." + }, + "step": { + "user": { + "data": { + "api_key": "Clave API de Airly", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nombre de la integraci\u00f3n" + }, + "description": "Configure la integraci\u00f3n de la calidad del aire de Airly. Para generar la clave API, vaya a https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/.translations/hu.json b/homeassistant/components/airly/.translations/hu.json new file mode 100644 index 00000000000..30898c61abb --- /dev/null +++ b/homeassistant/components/airly/.translations/hu.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Ezen koordin\u00e1t\u00e1k Airly integr\u00e1ci\u00f3ja m\u00e1r konfigur\u00e1lva van." + }, + "error": { + "auth": "Az API kulcs nem megfelel\u0151.", + "name_exists": "A n\u00e9v m\u00e1r l\u00e9tezik", + "wrong_location": "Ezen a ter\u00fcleten nincs Airly m\u00e9r\u0151\u00e1llom\u00e1s." + }, + "step": { + "user": { + "data": { + "api_key": "Airly API kulcs", + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g", + "name": "Az integr\u00e1ci\u00f3 neve" + }, + "description": "Az Airly leveg\u0151min\u0151s\u00e9gi integr\u00e1ci\u00f3j\u00e1nak be\u00e1ll\u00edt\u00e1sa. Api-kulcs l\u00e9trehoz\u00e1s\u00e1hoz nyissa meg a k\u00f6vetkez\u0151 weboldalt: https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/.translations/nl.json b/homeassistant/components/airly/.translations/nl.json index 232d5d54d85..a9c6865ad91 100644 --- a/homeassistant/components/airly/.translations/nl.json +++ b/homeassistant/components/airly/.translations/nl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Airly-integratie voor deze co\u00f6rdinaten is al geconfigureerd." + }, "error": { "auth": "API-sleutel is niet correct.", "name_exists": "Naam bestaat al.", diff --git a/homeassistant/components/airly/.translations/sl.json b/homeassistant/components/airly/.translations/sl.json index 08f57d88bcb..f8ca4e5b6d5 100644 --- a/homeassistant/components/airly/.translations/sl.json +++ b/homeassistant/components/airly/.translations/sl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Airly integracija za te koordinate je \u017ee nastavljen." + }, "error": { "auth": "Klju\u010d API ni pravilen.", "name_exists": "Ime \u017ee obstaja", diff --git a/homeassistant/components/airly/.translations/sv.json b/homeassistant/components/airly/.translations/sv.json new file mode 100644 index 00000000000..5b81b4625a2 --- /dev/null +++ b/homeassistant/components/airly/.translations/sv.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Airly-integrationen f\u00f6r dessa koordinater \u00e4r redan konfigurerad." + }, + "error": { + "auth": "API-nyckeln \u00e4r inte korrekt.", + "name_exists": "Namnet finns redan.", + "wrong_location": "Inga Airly m\u00e4tstationer i detta omr\u00e5de." + }, + "step": { + "user": { + "data": { + "api_key": "Airly API-nyckel", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Integrationens namn" + }, + "description": "Konfigurera integration av luftkvalitet. F\u00f6r att skapa API-nyckel, g\u00e5 till https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/sv.json b/homeassistant/components/alarm_control_panel/.translations/sv.json new file mode 100644 index 00000000000..65e4433f5a3 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/sv.json @@ -0,0 +1,18 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Larma {entity_name} borta", + "arm_home": "Larma {entity_name} hemma", + "arm_night": "Larma {entity_name} natt", + "disarm": "Avlarma {entity_name}", + "trigger": "Utl\u00f6sare {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} larmad borta", + "armed_home": "{entity_name} larmad hemma", + "armed_night": "{entity_name} larmad natt", + "disarmed": "{entity_name} bortkopplad", + "triggered": "{entity_name} utl\u00f6st" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarmdecoder/binary_sensor.py b/homeassistant/components/alarmdecoder/binary_sensor.py index dc3f16b7d22..13a7913e190 100644 --- a/homeassistant/components/alarmdecoder/binary_sensor.py +++ b/homeassistant/components/alarmdecoder/binary_sensor.py @@ -138,7 +138,7 @@ class AlarmDecoderBinarySensor(BinarySensorDevice): def _restore_callback(self, zone): """Update the zone's state, if needed.""" - if zone is None or int(zone) == self._zone_number: + if zone is None or (int(zone) == self._zone_number and not self._loop): self._state = 0 self.schedule_update_ha_state() diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index eb1474aed7e..94cf41d530b 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1,5 +1,6 @@ """Alexa capabilities.""" import logging +import math from homeassistant.components import ( cover, @@ -645,6 +646,43 @@ class AlexaSpeaker(AlexaCapability): """Return the Alexa API name of this interface.""" return "Alexa.Speaker" + def properties_supported(self): + """Return what properties this entity supports.""" + properties = [{"name": "volume"}] + + supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if supported & media_player.SUPPORT_VOLUME_MUTE: + properties.append({"name": "muted"}) + + return properties + + 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 == "volume": + current_level = self.entity.attributes.get( + media_player.ATTR_MEDIA_VOLUME_LEVEL + ) + try: + current = math.floor(int(current_level * 100)) + except ZeroDivisionError: + current = 0 + return current + + if name == "muted": + return bool( + self.entity.attributes.get(media_player.ATTR_MEDIA_VOLUME_MUTED) + ) + + return None + class AlexaStepSpeaker(AlexaCapability): """Implements Alexa.StepSpeaker. @@ -711,6 +749,13 @@ class AlexaInputController(AlexaCapability): source_list = self.entity.attributes.get( media_player.ATTR_INPUT_SOURCE_LIST, [] ) + input_list = AlexaInputController.get_valid_inputs(source_list) + + return input_list + + @staticmethod + def get_valid_inputs(source_list): + """Return list of supported inputs.""" input_list = [] for source in source_list: formatted_source = ( diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 254cec44553..b10f11e2bbc 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -508,12 +508,7 @@ class MediaPlayerCapabilities(AlexaEntity): supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & media_player.const.SUPPORT_VOLUME_SET: yield AlexaSpeaker(self.entity) - - step_volume_features = ( - media_player.const.SUPPORT_VOLUME_MUTE - | media_player.const.SUPPORT_VOLUME_STEP - ) - if supported & step_volume_features: + elif supported & media_player.const.SUPPORT_VOLUME_STEP: yield AlexaStepSpeaker(self.entity) playback_features = ( @@ -531,7 +526,13 @@ class MediaPlayerCapabilities(AlexaEntity): yield AlexaSeekController(self.entity) if supported & media_player.SUPPORT_SELECT_SOURCE: - yield AlexaInputController(self.entity) + inputs = AlexaInputController.get_valid_inputs( + self.entity.attributes.get( + media_player.const.ATTR_INPUT_SOURCE_LIST, [] + ) + ) + if len(inputs) > 0: + yield AlexaInputController(self.entity) if supported & media_player.const.SUPPORT_PLAY_MEDIA: yield AlexaChannelController(self.entity) diff --git a/homeassistant/components/alexa/messages.py b/homeassistant/components/alexa/messages.py index cb78f269f8f..4dd154ea11f 100644 --- a/homeassistant/components/alexa/messages.py +++ b/homeassistant/components/alexa/messages.py @@ -43,7 +43,7 @@ class AlexaDirective: Behavior when self.has_endpoint is False is undefined. Will raise AlexaInvalidEndpointError if the endpoint in the request is - malformed or nonexistant. + malformed or nonexistent. """ _endpoint_id = self._directive[API_ENDPOINT]["endpointId"] self.entity_id = _endpoint_id.replace("#", ".") diff --git a/homeassistant/components/almond/.translations/hu.json b/homeassistant/components/almond/.translations/hu.json new file mode 100644 index 00000000000..2331e57c6eb --- /dev/null +++ b/homeassistant/components/almond/.translations/hu.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_setup": "Csak egy Almond fi\u00f3kot konfigur\u00e1lhat.", + "cannot_connect": "Nem lehet csatlakozni az Almond szerverhez.", + "missing_configuration": "K\u00e9rj\u00fck, ellen\u0151rizze az Almond be\u00e1ll\u00edt\u00e1s\u00e1nak dokument\u00e1ci\u00f3j\u00e1t." + }, + "step": { + "hassio_confirm": { + "description": "Be szeretn\u00e9 \u00e1ll\u00edtani a Home Assistant alkalmaz\u00e1st az Almondhoz val\u00f3 csatlakoz\u00e1shoz, amelyet a Hass.io kieg\u00e9sz\u00edt\u0151 biztos\u00edt: {addon} ?", + "title": "Almond a Hass.io kieg\u00e9sz\u00edt\u0151n kereszt\u00fcl" + }, + "pick_implementation": { + "title": "V\u00e1lassza ki a hiteles\u00edt\u00e9si m\u00f3dszert" + } + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/nl.json b/homeassistant/components/almond/.translations/nl.json index d77fe69f7fa..939a9a904ad 100644 --- a/homeassistant/components/almond/.translations/nl.json +++ b/homeassistant/components/almond/.translations/nl.json @@ -6,6 +6,10 @@ "missing_configuration": "Raadpleeg de documentatie over het instellen van Almond." }, "step": { + "hassio_confirm": { + "description": "Wilt u Home Assistant configureren om verbinding te maken met Almond die wordt aangeboden door de hass.io add-on {addon} ?", + "title": "Almond via Hass.io add-on" + }, "pick_implementation": { "title": "Kies de authenticatie methode" } diff --git a/homeassistant/components/almond/.translations/sl.json b/homeassistant/components/almond/.translations/sl.json index 086190590ac..4a593cc5605 100644 --- a/homeassistant/components/almond/.translations/sl.json +++ b/homeassistant/components/almond/.translations/sl.json @@ -6,6 +6,10 @@ "missing_configuration": "Prosimo, preverite dokumentacijo o tem, kako nastaviti Almond." }, "step": { + "hassio_confirm": { + "description": "Ali \u017eelite konfigurirati Home Assistant za povezavo z Almondom, ki ga ponuja dodatek Hass.io: {addon} ?", + "title": "Almond prek dodatka Hass.io" + }, "pick_implementation": { "title": "Izberite na\u010din preverjanja pristnosti" } diff --git a/homeassistant/components/almond/.translations/sv.json b/homeassistant/components/almond/.translations/sv.json index 61af3a04e47..d2630b95c02 100644 --- a/homeassistant/components/almond/.translations/sv.json +++ b/homeassistant/components/almond/.translations/sv.json @@ -1,9 +1,19 @@ { "config": { + "abort": { + "already_setup": "Du kan bara konfigurera ett Almond-konto.", + "cannot_connect": "Det g\u00e5r inte att ansluta till Almond-servern.", + "missing_configuration": "Kontrollera dokumentationen f\u00f6r hur du st\u00e4ller in Almond." + }, "step": { "hassio_confirm": { + "description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till Almond som tillhandah\u00e5lls av Hass.io-till\u00e4gget: {addon} ?", "title": "Almond via Hass.io-till\u00e4gget" + }, + "pick_implementation": { + "title": "V\u00e4lj autentiseringsmetod" } - } + }, + "title": "Almond" } } \ No newline at end of file diff --git a/homeassistant/components/alpha_vantage/manifest.json b/homeassistant/components/alpha_vantage/manifest.json index 33348c9d7b3..c7220d8e059 100644 --- a/homeassistant/components/alpha_vantage/manifest.json +++ b/homeassistant/components/alpha_vantage/manifest.json @@ -2,7 +2,7 @@ "domain": "alpha_vantage", "name": "Alpha Vantage", "documentation": "https://www.home-assistant.io/integrations/alpha_vantage", - "requirements": ["alpha_vantage==2.1.2"], + "requirements": ["alpha_vantage==2.1.3"], "dependencies": [], "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/ambient_station/.translations/ca.json b/homeassistant/components/ambient_station/.translations/ca.json index d3c451f3e3f..280a90354b0 100644 --- a/homeassistant/components/ambient_station/.translations/ca.json +++ b/homeassistant/components/ambient_station/.translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Aquesta clau d'aplicaci\u00f3 ja est\u00e0 en \u00fas." + }, "error": { "identifier_exists": "Clau d'aplicaci\u00f3 i/o clau API ja registrada", "invalid_key": "Clau API i/o clau d'aplicaci\u00f3 inv\u00e0lida/es", diff --git a/homeassistant/components/ambient_station/.translations/de.json b/homeassistant/components/ambient_station/.translations/de.json index 1431efbf167..451a2e70e68 100644 --- a/homeassistant/components/ambient_station/.translations/de.json +++ b/homeassistant/components/ambient_station/.translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Dieser App-Schl\u00fcssel wird bereits verwendet." + }, "error": { "identifier_exists": "Anwendungsschl\u00fcssel und / oder API-Schl\u00fcssel bereits registriert", "invalid_key": "Ung\u00fcltiger API Key und / oder Anwendungsschl\u00fcssel", diff --git a/homeassistant/components/ambient_station/.translations/en.json b/homeassistant/components/ambient_station/.translations/en.json index 5bd643da55c..8b8e71d5316 100644 --- a/homeassistant/components/ambient_station/.translations/en.json +++ b/homeassistant/components/ambient_station/.translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "This app key is already in use." + }, "error": { "identifier_exists": "Application Key and/or API Key already registered", "invalid_key": "Invalid API Key and/or Application Key", diff --git a/homeassistant/components/ambient_station/.translations/ko.json b/homeassistant/components/ambient_station/.translations/ko.json index eb9209a6c37..3379411678b 100644 --- a/homeassistant/components/ambient_station/.translations/ko.json +++ b/homeassistant/components/ambient_station/.translations/ko.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\uc774 \uc571 \ud0a4\ub294 \uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4." + }, "error": { "identifier_exists": "\uc560\ud50c\ub9ac\ucf00\uc774\uc158 \ud0a4 \ud639\uc740 API \ud0a4\uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "invalid_key": "\uc560\ud50c\ub9ac\ucf00\uc774\uc158 \ud0a4 \ud639\uc740 API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", diff --git a/homeassistant/components/ambient_station/.translations/no.json b/homeassistant/components/ambient_station/.translations/no.json index 0b9d377718b..4a089eba4c0 100644 --- a/homeassistant/components/ambient_station/.translations/no.json +++ b/homeassistant/components/ambient_station/.translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Denne app n\u00f8kkelen er allerede i bruk." + }, "error": { "identifier_exists": "Programn\u00f8kkel og/eller API-n\u00f8kkel er allerede registrert", "invalid_key": "Ugyldig API-n\u00f8kkel og/eller programn\u00f8kkel", diff --git a/homeassistant/components/ambient_station/.translations/pl.json b/homeassistant/components/ambient_station/.translations/pl.json index 6ebd0848a63..5da886f05cd 100644 --- a/homeassistant/components/ambient_station/.translations/pl.json +++ b/homeassistant/components/ambient_station/.translations/pl.json @@ -1,7 +1,10 @@ { "config": { + "abort": { + "already_configured": "Ten klucz aplikacji jest ju\u017c w u\u017cyciu." + }, "error": { - "identifier_exists": "Klucz aplikacji i/lub klucz API ju\u017c jest zarejestrowany", + "identifier_exists": "Klucz aplikacji i/lub klucz API ju\u017c jest zarejestrowany.", "invalid_key": "Nieprawid\u0142owy klucz API i/lub klucz aplikacji", "no_devices": "Nie znaleziono urz\u0105dze\u0144 na koncie" }, diff --git a/homeassistant/components/ambient_station/.translations/zh-Hant.json b/homeassistant/components/ambient_station/.translations/zh-Hant.json index 6c7c88a8045..6de1579f6ff 100644 --- a/homeassistant/components/ambient_station/.translations/zh-Hant.json +++ b/homeassistant/components/ambient_station/.translations/zh-Hant.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u6b64\u61c9\u7528\u7a0b\u5f0f\u5bc6\u9470\u5df2\u88ab\u4f7f\u7528\u3002" + }, "error": { "identifier_exists": "API \u5bc6\u9470\u53ca/\u6216\u61c9\u7528\u5bc6\u9470\u5df2\u8a3b\u518a", "invalid_key": "API \u5bc6\u9470\u53ca/\u6216\u61c9\u7528\u5bc6\u9470\u7121\u6548", diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index c61e15dfeb5..0bbb7a760fe 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -378,7 +378,7 @@ class AmbientStation: if data != self.stations[mac_address][ATTR_LAST_DATA]: _LOGGER.debug("New data received: %s", data) self.stations[mac_address][ATTR_LAST_DATA] = data - async_dispatcher_send(self._hass, TOPIC_UPDATE) + async_dispatcher_send(self._hass, TOPIC_UPDATE.format(mac_address)) _LOGGER.debug("Resetting watchdog") self._watchdog_listener() @@ -518,7 +518,7 @@ class AmbientWeatherEntity(Entity): self.async_schedule_update_ha_state(True) self._async_unsub_dispatcher_connect = async_dispatcher_connect( - self.hass, TOPIC_UPDATE, update + self.hass, TOPIC_UPDATE.format(self._mac_address), update ) async def async_will_remove_from_hass(self): diff --git a/homeassistant/components/ambient_station/const.py b/homeassistant/components/ambient_station/const.py index 21a6e514b30..4f94e1cfe88 100644 --- a/homeassistant/components/ambient_station/const.py +++ b/homeassistant/components/ambient_station/const.py @@ -8,7 +8,7 @@ CONF_APP_KEY = "app_key" DATA_CLIENT = "data_client" -TOPIC_UPDATE = "update" +TOPIC_UPDATE = "ambient_station_data_update_{0}" TYPE_BINARY_SENSOR = "binary_sensor" TYPE_SENSOR = "sensor" diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index f7814939e3a..5578d350e22 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -23,6 +23,7 @@ from homeassistant.const import ( CONF_SENSORS, CONF_USERNAME, ENTITY_MATCH_ALL, + ENTITY_MATCH_NONE, HTTP_BASIC_AUTHENTICATION, ) from homeassistant.exceptions import Unauthorized, UnknownUser @@ -34,7 +35,15 @@ from homeassistant.helpers.service import async_extract_entity_ids from .binary_sensor import BINARY_SENSORS from .camera import CAMERA_SERVICES, STREAM_SOURCE_LIST -from .const import CAMERAS, DATA_AMCREST, DEVICES, DOMAIN, SERVICE_UPDATE +from .const import ( + CAMERAS, + COMM_RETRIES, + COMM_TIMEOUT, + DATA_AMCREST, + DEVICES, + DOMAIN, + SERVICE_UPDATE, +) from .helpers import service_signal from .sensor import SENSORS @@ -110,38 +119,56 @@ class AmcrestChecker(Http): self._wrap_name = name self._wrap_errors = 0 self._wrap_lock = threading.Lock() + self._wrap_login_err = False self._unsub_recheck = None super().__init__( - host, port, user, password, retries_connection=1, timeout_protocol=3.05 + host, + port, + user, + password, + retries_connection=COMM_RETRIES, + timeout_protocol=COMM_TIMEOUT, ) @property def available(self): """Return if camera's API is responding.""" - return self._wrap_errors <= MAX_ERRORS + return self._wrap_errors <= MAX_ERRORS and not self._wrap_login_err + + def _start_recovery(self): + dispatcher_send(self._hass, service_signal(SERVICE_UPDATE, self._wrap_name)) + self._unsub_recheck = track_time_interval( + self._hass, self._wrap_test_online, RECHECK_INTERVAL + ) def command(self, cmd, retries=None, timeout_cmd=None, stream=False): """amcrest.Http.command wrapper to catch errors.""" try: ret = super().command(cmd, retries, timeout_cmd, stream) + except LoginError as ex: + with self._wrap_lock: + was_online = self.available + was_login_err = self._wrap_login_err + self._wrap_login_err = True + if not was_login_err: + _LOGGER.error("%s camera offline: Login error: %s", self._wrap_name, ex) + if was_online: + self._start_recovery() + raise except AmcrestError: with self._wrap_lock: was_online = self.available - self._wrap_errors += 1 - _LOGGER.debug("%s camera errs: %i", self._wrap_name, self._wrap_errors) + errs = self._wrap_errors = self._wrap_errors + 1 offline = not self.available - if offline and was_online: + _LOGGER.debug("%s camera errs: %i", self._wrap_name, errs) + if was_online and offline: _LOGGER.error("%s camera offline: Too many errors", self._wrap_name) - dispatcher_send( - self._hass, service_signal(SERVICE_UPDATE, self._wrap_name) - ) - self._unsub_recheck = track_time_interval( - self._hass, self._wrap_test_online, RECHECK_INTERVAL - ) + self._start_recovery() raise with self._wrap_lock: was_offline = not self.available self._wrap_errors = 0 + self._wrap_login_err = False if was_offline: self._unsub_recheck() self._unsub_recheck = None @@ -151,6 +178,7 @@ class AmcrestChecker(Http): def _wrap_test_online(self, now): """Test if camera is back online.""" + _LOGGER.debug("Testing if %s back online", self._wrap_name) try: self.current_time except AmcrestError: @@ -166,14 +194,9 @@ def setup(hass, config): username = device[CONF_USERNAME] password = device[CONF_PASSWORD] - try: - api = AmcrestChecker( - hass, name, device[CONF_HOST], device[CONF_PORT], username, password - ) - - except LoginError as ex: - _LOGGER.error("Login error for %s camera: %s", name, ex) - continue + api = AmcrestChecker( + hass, name, device[CONF_HOST], device[CONF_PORT], username, password + ) ffmpeg_arguments = device[CONF_FFMPEG_ARGUMENTS] resolution = RESOLUTION_LIST[device[CONF_RESOLUTION]] @@ -236,6 +259,9 @@ def setup(hass, config): if have_permission(user, entity_id) ] + if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_NONE: + return [] + call_ids = await async_extract_entity_ids(hass, call) entity_ids = [] for entity_id in hass.data[DATA_AMCREST][CAMERAS]: diff --git a/homeassistant/components/amcrest/binary_sensor.py b/homeassistant/components/amcrest/binary_sensor.py index ac16f0664aa..a99901f54a3 100644 --- a/homeassistant/components/amcrest/binary_sensor.py +++ b/homeassistant/components/amcrest/binary_sensor.py @@ -1,4 +1,4 @@ -"""Suppoort for Amcrest IP camera binary sensors.""" +"""Support for Amcrest IP camera binary sensors.""" from datetime import timedelta import logging diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index e9e1e2b5f84..0e64d4fefc9 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -1,11 +1,11 @@ """Support for Amcrest IP cameras.""" import asyncio from datetime import timedelta +from functools import partial import logging from amcrest import AmcrestError from haffmpeg.camera import CameraMjpeg -from urllib3.exceptions import HTTPError import voluptuous as vol from homeassistant.components.camera import ( @@ -26,9 +26,11 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( CAMERA_WEB_SESSION_TIMEOUT, CAMERAS, + COMM_TIMEOUT, DATA_AMCREST, DEVICES, SERVICE_UPDATE, + SNAPSHOT_TIMEOUT, ) from .helpers import log_update_error, service_signal @@ -90,6 +92,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([AmcrestCam(name, device, hass.data[DATA_FFMPEG])], True) +class CannotSnapshot(Exception): + """Conditions are not valid for taking a snapshot.""" + + class AmcrestCam(Camera): """An implementation of an Amcrest IP camera.""" @@ -112,28 +118,58 @@ class AmcrestCam(Camera): self._motion_recording_enabled = None self._color_bw = None self._rtsp_url = None - self._snapshot_lock = asyncio.Lock() + self._snapshot_task = None self._unsub_dispatcher = [] self._update_succeeded = False - async def async_camera_image(self): - """Return a still image response from the camera.""" + def _check_snapshot_ok(self): available = self.available if not available or not self.is_on: _LOGGER.warning( - "Attempt to take snaphot when %s camera is %s", + "Attempt to take snapshot when %s camera is %s", self.name, "offline" if not available else "off", ) + raise CannotSnapshot + + async def _async_get_image(self): + try: + # Send the request to snap a picture and return raw jpg data + # Snapshot command needs a much longer read timeout than other commands. + return await self.hass.async_add_executor_job( + partial( + self._api.snapshot, + timeout=(COMM_TIMEOUT, SNAPSHOT_TIMEOUT), + stream=False, + ) + ) + except AmcrestError as error: + log_update_error(_LOGGER, "get image from", self.name, "camera", error) + return None + finally: + self._snapshot_task = None + + async def async_camera_image(self): + """Return a still image response from the camera.""" + _LOGGER.debug("Take snapshot from %s", self._name) + try: + # Amcrest cameras only support one snapshot command at a time. + # Hence need to wait if a previous snapshot has not yet finished. + # Also need to check that camera is online and turned on before each wait + # and before initiating shapshot. + while self._snapshot_task: + self._check_snapshot_ok() + _LOGGER.debug("Waiting for previous snapshot from %s ...", self._name) + await self._snapshot_task + self._check_snapshot_ok() + # Run snapshot command in separate Task that can't be cancelled so + # 1) it's not possible to send another snapshot command while camera is + # still working on a previous one, and + # 2) someone will be around to catch any exceptions. + self._snapshot_task = self.hass.async_create_task(self._async_get_image()) + return await asyncio.shield(self._snapshot_task) + except CannotSnapshot: return None - async with self._snapshot_lock: - try: - # Send the request to snap a picture and return raw jpg data - response = await self.hass.async_add_executor_job(self._api.snapshot) - return response.data - except (AmcrestError, HTTPError) as error: - log_update_error(_LOGGER, "get image from", self.name, "camera", error) - return None async def handle_async_mjpeg_stream(self, request): """Return an MJPEG stream.""" diff --git a/homeassistant/components/amcrest/const.py b/homeassistant/components/amcrest/const.py index 98d613634b5..38ff8a8894e 100644 --- a/homeassistant/components/amcrest/const.py +++ b/homeassistant/components/amcrest/const.py @@ -6,6 +6,9 @@ DEVICES = "devices" BINARY_SENSOR_SCAN_INTERVAL_SECS = 5 CAMERA_WEB_SESSION_TIMEOUT = 10 +COMM_RETRIES = 1 +COMM_TIMEOUT = 6.05 SENSOR_SCAN_INTERVAL_SECS = 10 +SNAPSHOT_TIMEOUT = 20 SERVICE_UPDATE = "update" diff --git a/homeassistant/components/amcrest/manifest.json b/homeassistant/components/amcrest/manifest.json index 8b2d72effa6..38e19e4ec26 100644 --- a/homeassistant/components/amcrest/manifest.json +++ b/homeassistant/components/amcrest/manifest.json @@ -2,7 +2,7 @@ "domain": "amcrest", "name": "Amcrest", "documentation": "https://www.home-assistant.io/integrations/amcrest", - "requirements": ["amcrest==1.5.3"], + "requirements": ["amcrest==1.5.6"], "dependencies": ["ffmpeg"], "codeowners": ["@pnbruckner"] } diff --git a/homeassistant/components/amcrest/sensor.py b/homeassistant/components/amcrest/sensor.py index be03b3bedff..04436cd95ab 100644 --- a/homeassistant/components/amcrest/sensor.py +++ b/homeassistant/components/amcrest/sensor.py @@ -1,4 +1,4 @@ -"""Suppoort for Amcrest IP camera sensors.""" +"""Support for Amcrest IP camera sensors.""" from datetime import timedelta import logging diff --git a/homeassistant/components/apcupsd/manifest.json b/homeassistant/components/apcupsd/manifest.json index a8a5506fc0a..5908523e6d8 100644 --- a/homeassistant/components/apcupsd/manifest.json +++ b/homeassistant/components/apcupsd/manifest.json @@ -1,6 +1,6 @@ { "domain": "apcupsd", - "name": "APCUPSd", + "name": "apcupsd", "documentation": "https://www.home-assistant.io/integrations/apcupsd", "requirements": ["apcaccess==0.0.13"], "dependencies": [], diff --git a/homeassistant/components/apns/notify.py b/homeassistant/components/apns/notify.py index febe344a9c4..3cd43ee36ae 100644 --- a/homeassistant/components/apns/notify.py +++ b/homeassistant/components/apns/notify.py @@ -177,7 +177,7 @@ class ApnsNotificationService(BaseNotificationService): def device_state_changed_listener(self, entity_id, from_s, to_s): """ - Listen for sate change. + Listen for state change. Track device state change if a device has a tracking id specified. """ diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index 2f37d941ac8..8ca42beab61 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/apple_tv", "requirements": ["pyatv==0.3.13"], "dependencies": ["configurator"], + "after_dependencies": ["discovery"], "codeowners": [] } diff --git a/homeassistant/components/apprise/manifest.json b/homeassistant/components/apprise/manifest.json index 1f41d5a24e2..0895c2af1f9 100644 --- a/homeassistant/components/apprise/manifest.json +++ b/homeassistant/components/apprise/manifest.json @@ -2,7 +2,7 @@ "domain": "apprise", "name": "Apprise", "documentation": "https://www.home-assistant.io/integrations/apprise", - "requirements": ["apprise==0.8.3"], + "requirements": ["apprise==0.8.4"], "dependencies": [], "codeowners": ["@caronc"] } diff --git a/homeassistant/components/arcam_fmj/.translations/sv.json b/homeassistant/components/arcam_fmj/.translations/sv.json new file mode 100644 index 00000000000..b0ad4660d0f --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/sv.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/arlo/alarm_control_panel.py b/homeassistant/components/arlo/alarm_control_panel.py index 838f319abc1..49a1bced577 100644 --- a/homeassistant/components/arlo/alarm_control_panel.py +++ b/homeassistant/components/arlo/alarm_control_panel.py @@ -110,19 +110,19 @@ class ArloBaseStation(AlarmControlPanel): else: self._state = None - async def async_alarm_disarm(self, code=None): + def alarm_disarm(self, code=None): """Send disarm command.""" self._base_station.mode = DISARMED - async def async_alarm_arm_away(self, code=None): + def alarm_arm_away(self, code=None): """Send arm away command. Uses custom mode.""" self._base_station.mode = self._away_mode_name - async def async_alarm_arm_home(self, code=None): + def alarm_arm_home(self, code=None): """Send arm home command. Uses custom mode.""" self._base_station.mode = self._home_mode_name - async def async_alarm_arm_night(self, code=None): + def alarm_arm_night(self, code=None): """Send arm night command. Uses custom mode.""" self._base_station.mode = self._night_mode_name diff --git a/homeassistant/components/arlo/camera.py b/homeassistant/components/arlo/camera.py index 958c383765a..22b1bfbe810 100644 --- a/homeassistant/components/arlo/camera.py +++ b/homeassistant/components/arlo/camera.py @@ -78,8 +78,10 @@ class ArloCam(Camera): async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" + video = await self.hass.async_add_executor_job( + getattr, self._camera, "last_video" + ) - video = self._camera.last_video if not video: error_msg = "Video not found for {0}. Is it older than {1} days?".format( self.name, self._camera.min_days_vdo_cache diff --git a/homeassistant/components/asuswrt/__init__.py b/homeassistant/components/asuswrt/__init__.py index 64d2d7c7a4b..897258c6299 100644 --- a/homeassistant/components/asuswrt/__init__.py +++ b/homeassistant/components/asuswrt/__init__.py @@ -70,7 +70,7 @@ async def async_setup(hass, config): await api.connection.async_connect() if not api.is_connected: - _LOGGER.error("Unable to setup asuswrt component") + _LOGGER.error("Unable to setup component") return False hass.data[DATA_ASUSWRT] = api diff --git a/homeassistant/components/asuswrt/manifest.json b/homeassistant/components/asuswrt/manifest.json index 02999ada68b..416144b450c 100644 --- a/homeassistant/components/asuswrt/manifest.json +++ b/homeassistant/components/asuswrt/manifest.json @@ -1,6 +1,6 @@ { "domain": "asuswrt", - "name": "Asuswrt", + "name": "ASUSWRT", "documentation": "https://www.home-assistant.io/integrations/asuswrt", "requirements": ["aioasuswrt==1.1.22"], "dependencies": [], diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index b5ce8539f44..50100d3625d 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -1,6 +1,7 @@ """Asuswrt status sensors.""" import logging +from homeassistant.const import DATA_GIGABYTES, DATA_RATE_MEGABITS_PER_SECOND from homeassistant.helpers.entity import Entity from . import DATA_ASUSWRT @@ -61,7 +62,7 @@ class AsuswrtRXSensor(AsuswrtSensor): """Representation of a asuswrt download speed sensor.""" _name = "Asuswrt Download Speed" - _unit = "Mbit/s" + _unit = DATA_RATE_MEGABITS_PER_SECOND @property def unit_of_measurement(self): @@ -79,7 +80,7 @@ class AsuswrtTXSensor(AsuswrtSensor): """Representation of a asuswrt upload speed sensor.""" _name = "Asuswrt Upload Speed" - _unit = "Mbit/s" + _unit = DATA_RATE_MEGABITS_PER_SECOND @property def unit_of_measurement(self): @@ -97,7 +98,7 @@ class AsuswrtTotalRXSensor(AsuswrtSensor): """Representation of a asuswrt total download sensor.""" _name = "Asuswrt Download" - _unit = "Gigabyte" + _unit = DATA_GIGABYTES @property def unit_of_measurement(self): @@ -115,7 +116,7 @@ class AsuswrtTotalTXSensor(AsuswrtSensor): """Representation of a asuswrt total upload sensor.""" _name = "Asuswrt Upload" - _unit = "Gigabyte" + _unit = DATA_GIGABYTES @property def unit_of_measurement(self): diff --git a/homeassistant/components/august/.translations/de.json b/homeassistant/components/august/.translations/de.json new file mode 100644 index 00000000000..dd3b2ea9f44 --- /dev/null +++ b/homeassistant/components/august/.translations/de.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Konto ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "login_method": "Anmeldemethode", + "password": "Passwort", + "timeout": "Zeit\u00fcberschreitung (Sekunden)", + "username": "Benutzername" + }, + "title": "Richten Sie ein August-Konto ein" + }, + "validation": { + "data": { + "code": "Verifizierungs-Code" + }, + "description": "Bitte \u00fcberpr\u00fcfen Sie Ihre {login_method} ({username}) und geben Sie den Best\u00e4tigungscode ein", + "title": "Zwei-Faktor-Authentifizierung" + } + }, + "title": "August" + } +} \ No newline at end of file diff --git a/homeassistant/components/august/.translations/en.json b/homeassistant/components/august/.translations/en.json new file mode 100644 index 00000000000..32c628f0b0d --- /dev/null +++ b/homeassistant/components/august/.translations/en.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Account is already configured" + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "login_method": "Login Method", + "password": "Password", + "timeout": "Timeout (seconds)", + "username": "Username" + }, + "description": "If the Login Method is 'email', Username is the email address. If the Login Method is 'phone', Username is the phone number in the format '+NNNNNNNNN'.", + "title": "Setup an August account" + }, + "validation": { + "data": { + "code": "Verification code" + }, + "description": "Please check your {login_method} ({username}) and enter the verification code below", + "title": "Two factor authentication" + } + }, + "title": "August" + } +} \ No newline at end of file diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index a52df5e361c..67e177d11d9 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -1,8 +1,10 @@ """Support for August devices.""" +import asyncio from datetime import timedelta +from functools import partial import logging -from august.api import Api +from august.api import Api, AugustApiHTTPError from august.authenticator import AuthenticationState, Authenticator, ValidationResult from requests import RequestException, Session import voluptuous as vol @@ -13,9 +15,10 @@ from homeassistant.const import ( CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle +from homeassistant.util import Throttle, dt _LOGGER = logging.getLogger(__name__) @@ -42,9 +45,22 @@ DEFAULT_ENTITY_NAMESPACE = "august" # avoid hitting rate limits MIN_TIME_BETWEEN_LOCK_DETAIL_UPDATES = timedelta(seconds=1800) -DEFAULT_SCAN_INTERVAL = timedelta(seconds=10) +# Limit locks status check to 900 seconds now that +# we get the state from the lock and unlock api calls +# and the lock and unlock activities are now captured +MIN_TIME_BETWEEN_LOCK_STATUS_UPDATES = timedelta(seconds=900) + +# Doorbells need to update more frequently than locks +# since we get an image from the doorbell api +MIN_TIME_BETWEEN_DOORBELL_STATUS_UPDATES = timedelta(seconds=20) + +# Activity needs to be checked more frequently as the +# doorbell motion and rings are included here MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) +DEFAULT_SCAN_INTERVAL = timedelta(seconds=10) + + LOGIN_METHODS = ["phone", "email"] CONFIG_SCHEMA = vol.Schema( @@ -65,7 +81,7 @@ CONFIG_SCHEMA = vol.Schema( AUGUST_COMPONENTS = ["camera", "binary_sensor", "lock"] -def request_configuration(hass, config, api, authenticator): +def request_configuration(hass, config, api, authenticator, token_refresh_lock): """Request configuration steps from the user.""" configurator = hass.components.configurator @@ -79,7 +95,7 @@ def request_configuration(hass, config, api, authenticator): _CONFIGURING[DOMAIN], "Invalid verification code" ) elif result == ValidationResult.VALIDATED: - setup_august(hass, config, api, authenticator) + setup_august(hass, config, api, authenticator, token_refresh_lock) if DOMAIN not in _CONFIGURING: authenticator.send_verification_code() @@ -100,7 +116,7 @@ def request_configuration(hass, config, api, authenticator): ) -def setup_august(hass, config, api, authenticator): +def setup_august(hass, config, api, authenticator, token_refresh_lock): """Set up the August component.""" authentication = None @@ -123,7 +139,9 @@ def setup_august(hass, config, api, authenticator): if DOMAIN in _CONFIGURING: hass.components.configurator.request_done(_CONFIGURING.pop(DOMAIN)) - hass.data[DATA_AUGUST] = AugustData(hass, api, authentication.access_token) + hass.data[DATA_AUGUST] = AugustData( + hass, api, authentication, authenticator, token_refresh_lock + ) for component in AUGUST_COMPONENTS: discovery.load_platform(hass, component, DOMAIN, {}, config) @@ -133,13 +151,13 @@ def setup_august(hass, config, api, authenticator): _LOGGER.error("Invalid password provided") return False if state == AuthenticationState.REQUIRES_VALIDATION: - request_configuration(hass, config, api, authenticator) + request_configuration(hass, config, api, authenticator, token_refresh_lock) return True return False -def setup(hass, config): +async def async_setup(hass, config): """Set up the August component.""" conf = config[DOMAIN] @@ -171,20 +189,28 @@ def setup(hass, config): _LOGGER.debug("August HTTP session closed.") - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, close_http_session) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_http_session) _LOGGER.debug("Registered for Home Assistant stop event") - return setup_august(hass, config, api, authenticator) + token_refresh_lock = asyncio.Lock() + + return await hass.async_add_executor_job( + setup_august, hass, config, api, authenticator, token_refresh_lock + ) class AugustData: """August data object.""" - def __init__(self, hass, api, access_token): + def __init__(self, hass, api, authentication, authenticator, token_refresh_lock): """Init August data object.""" self._hass = hass self._api = api - self._access_token = access_token + self._authenticator = authenticator + self._access_token = authentication.access_token + self._access_token_expires = authentication.access_token_expires + + self._token_refresh_lock = token_refresh_lock self._doorbells = self._api.get_doorbells(self._access_token) or [] self._locks = self._api.get_operable_locks(self._access_token) or [] self._house_ids = set() @@ -192,11 +218,20 @@ class AugustData: self._house_ids.add(device.house_id) self._doorbell_detail_by_id = {} + self._door_last_state_update_time_utc_by_id = {} + self._lock_last_status_update_time_utc_by_id = {} self._lock_status_by_id = {} self._lock_detail_by_id = {} self._door_state_by_id = {} self._activities_by_id = {} + # We check the locks right away so we can + # remove inoperative ones + self._update_locks_status() + self._update_locks_detail() + + self._filter_inoperative_locks() + @property def house_ids(self): """Return a list of house_ids.""" @@ -212,24 +247,48 @@ class AugustData: """Return a list of locks.""" return self._locks - def get_device_activities(self, device_id, *activity_types): + async def _async_refresh_access_token_if_needed(self): + """Refresh the august access token if needed.""" + if self._authenticator.should_refresh(): + async with self._token_refresh_lock: + await self._hass.async_add_executor_job(self._refresh_access_token) + + def _refresh_access_token(self): + refreshed_authentication = self._authenticator.refresh_access_token(force=False) + _LOGGER.info( + "Refreshed august access token. The old token expired at %s, and the new token expires at %s", + self._access_token_expires, + refreshed_authentication.access_token_expires, + ) + self._access_token = refreshed_authentication.access_token + self._access_token_expires = refreshed_authentication.access_token_expires + + async def async_get_device_activities(self, device_id, *activity_types): """Return a list of activities.""" - _LOGGER.debug("Getting device activities") - self._update_device_activities() + _LOGGER.debug("Getting device activities for %s", device_id) + await self._async_update_device_activities() activities = self._activities_by_id.get(device_id, []) if activity_types: return [a for a in activities if a.activity_type in activity_types] return activities - def get_latest_device_activity(self, device_id, *activity_types): + async def async_get_latest_device_activity(self, device_id, *activity_types): """Return latest activity.""" - activities = self.get_device_activities(device_id, *activity_types) + activities = await self.async_get_device_activities(device_id, *activity_types) return next(iter(activities or []), None) @Throttle(MIN_TIME_BETWEEN_UPDATES) - def _update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT): + async def _async_update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT): """Update data object with latest from August API.""" + + # This is the only place we refresh the api token + await self._async_refresh_access_token_if_needed() + return await self._hass.async_add_executor_job( + partial(self._update_device_activities, limit=ACTIVITY_FETCH_LIMIT) + ) + + def _update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT): _LOGGER.debug("Start retrieving device activities") for house_id in self.house_ids: _LOGGER.debug("Updating device activity for house id %s", house_id) @@ -243,14 +302,18 @@ class AugustData: self._activities_by_id[device_id] = [ a for a in activities if a.device_id == device_id ] + _LOGGER.debug("Completed retrieving device activities") - def get_doorbell_detail(self, doorbell_id): + async def async_get_doorbell_detail(self, doorbell_id): """Return doorbell detail.""" - self._update_doorbells() + await self._async_update_doorbells() return self._doorbell_detail_by_id.get(doorbell_id) - @Throttle(MIN_TIME_BETWEEN_UPDATES) + @Throttle(MIN_TIME_BETWEEN_DOORBELL_STATUS_UPDATES) + async def _async_update_doorbells(self): + await self._hass.async_add_executor_job(self._update_doorbells) + def _update_doorbells(self): detail_by_id = {} @@ -275,38 +338,79 @@ class AugustData: _LOGGER.debug("Completed retrieving doorbell details") self._doorbell_detail_by_id = detail_by_id - def get_lock_status(self, lock_id): + def update_door_state(self, lock_id, door_state, update_start_time_utc): + """Set the door status and last status update time. + + This is called when newer activity is detected on the activity feed + in order to keep the internal data in sync + """ + self._door_state_by_id[lock_id] = door_state + self._door_last_state_update_time_utc_by_id[lock_id] = update_start_time_utc + return True + + def update_lock_status(self, lock_id, lock_status, update_start_time_utc): + """Set the lock status and last status update time. + + This is used when the lock, unlock apis are called + or newer activity is detected on the activity feed + in order to keep the internal data in sync + """ + self._lock_status_by_id[lock_id] = lock_status + self._lock_last_status_update_time_utc_by_id[lock_id] = update_start_time_utc + return True + + def lock_has_doorsense(self, lock_id): + """Determine if a lock has doorsense installed and can tell when the door is open or closed.""" + # We do not update here since this is not expected + # to change until restart + if self._lock_detail_by_id[lock_id] is None: + return False + return self._lock_detail_by_id[lock_id].doorsense + + async def async_get_lock_status(self, lock_id): """Return status if the door is locked or unlocked. This is status for the lock itself. """ - self._update_locks() + await self._async_update_locks() return self._lock_status_by_id.get(lock_id) - def get_lock_detail(self, lock_id): + async def async_get_lock_detail(self, lock_id): """Return lock detail.""" - self._update_locks() + await self._async_update_locks() return self._lock_detail_by_id.get(lock_id) - def get_door_state(self, lock_id): + def get_lock_name(self, device_id): + """Return lock name as August has it stored.""" + for lock in self._locks: + if lock.device_id == device_id: + return lock.device_name + + async def async_get_door_state(self, lock_id): """Return status if the door is open or closed. This is the status from the door sensor. """ - self._update_locks_status() + await self._async_update_locks_status() return self._door_state_by_id.get(lock_id) - def _update_locks(self): - self._update_locks_status() - self._update_locks_detail() + async def _async_update_locks(self): + await self._async_update_locks_status() + await self._async_update_locks_detail() + + @Throttle(MIN_TIME_BETWEEN_LOCK_STATUS_UPDATES) + async def _async_update_locks_status(self): + await self._hass.async_add_executor_job(self._update_locks_status) - @Throttle(MIN_TIME_BETWEEN_UPDATES) def _update_locks_status(self): status_by_id = {} state_by_id = {} + lock_last_status_update_by_id = {} + door_last_state_update_by_id = {} _LOGGER.debug("Start retrieving lock and door status") for lock in self._locks: + update_start_time_utc = dt.utcnow() _LOGGER.debug("Updating lock and door status for %s", lock.device_name) try: ( @@ -315,6 +419,13 @@ class AugustData: ) = self._api.get_lock_status( self._access_token, lock.device_id, door_status=True ) + # Since there is a a race condition between calling the + # lock and activity apis, we set the last update time + # BEFORE making the api call since we will compare this + # to activity later we want activity to win over stale lock/door + # state. + lock_last_status_update_by_id[lock.device_id] = update_start_time_utc + door_last_state_update_by_id[lock.device_id] = update_start_time_utc except RequestException as ex: _LOGGER.error( "Request error trying to retrieve lock and door status for %s. %s", @@ -331,8 +442,33 @@ class AugustData: _LOGGER.debug("Completed retrieving lock and door status") self._lock_status_by_id = status_by_id self._door_state_by_id = state_by_id + self._door_last_state_update_time_utc_by_id = door_last_state_update_by_id + self._lock_last_status_update_time_utc_by_id = lock_last_status_update_by_id + + def get_last_lock_status_update_time_utc(self, lock_id): + """Return the last time that a lock status update was seen from the august API.""" + # Since the activity api is called more frequently than + # the lock api it is possible that the lock has not + # been updated yet + if lock_id not in self._lock_last_status_update_time_utc_by_id: + return dt.utc_from_timestamp(0) + + return self._lock_last_status_update_time_utc_by_id[lock_id] + + def get_last_door_state_update_time_utc(self, lock_id): + """Return the last time that a door status update was seen from the august API.""" + # Since the activity api is called more frequently than + # the lock api it is possible that the door has not + # been updated yet + if lock_id not in self._door_last_state_update_time_utc_by_id: + return dt.utc_from_timestamp(0) + + return self._door_last_state_update_time_utc_by_id[lock_id] @Throttle(MIN_TIME_BETWEEN_LOCK_DETAIL_UPDATES) + async def _async_update_locks_detail(self): + await self._hass.async_add_executor_job(self._update_locks_detail) + def _update_locks_detail(self): detail_by_id = {} @@ -358,8 +494,60 @@ class AugustData: def lock(self, device_id): """Lock the device.""" - return self._api.lock(self._access_token, device_id) + return _call_api_operation_that_requires_bridge( + self.get_lock_name(device_id), + "lock", + self._api.lock, + self._access_token, + device_id, + ) def unlock(self, device_id): """Unlock the device.""" - return self._api.unlock(self._access_token, device_id) + return _call_api_operation_that_requires_bridge( + self.get_lock_name(device_id), + "unlock", + self._api.unlock, + self._access_token, + device_id, + ) + + def _filter_inoperative_locks(self): + # Remove non-operative locks as there must + # be a bridge (August Connect) for them to + # be usable + operative_locks = [] + for lock in self._locks: + lock_detail = self._lock_detail_by_id.get(lock.device_id) + if lock_detail is None: + _LOGGER.info( + "The lock %s could not be setup because the system could not fetch details about the lock.", + lock.device_name, + ) + elif lock_detail.bridge is None: + _LOGGER.info( + "The lock %s could not be setup because it does not have a bridge (Connect).", + lock.device_name, + ) + elif not lock_detail.bridge.operative: + _LOGGER.info( + "The lock %s could not be setup because the bridge (Connect) is not operative.", + lock.device_name, + ) + else: + operative_locks.append(lock) + + self._locks = operative_locks + + +def _call_api_operation_that_requires_bridge( + device_name, operation_name, func, *args, **kwargs +): + """Call an API that requires the bridge to be online.""" + ret = None + try: + ret = func(*args, **kwargs) + except AugustApiHTTPError as err: + raise HomeAssistantError(device_name + ": " + str(err)) + + return ret diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index f840d3db532..aed1995d592 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -2,84 +2,92 @@ from datetime import datetime, timedelta import logging -from august.activity import ActivityType +from august.activity import ACTIVITY_ACTION_STATES, ActivityType from august.lock import LockDoorStatus from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.util import dt from . import DATA_AUGUST _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=10) +SCAN_INTERVAL = timedelta(seconds=5) -def _retrieve_door_state(data, lock): +async def _async_retrieve_door_state(data, lock): """Get the latest state of the DoorSense sensor.""" - return data.get_door_state(lock.device_id) + return await data.async_get_door_state(lock.device_id) -def _retrieve_online_state(data, doorbell): +async def _async_retrieve_online_state(data, doorbell): """Get the latest state of the sensor.""" - detail = data.get_doorbell_detail(doorbell.device_id) + detail = await data.async_get_doorbell_detail(doorbell.device_id) if detail is None: return None return detail.is_online -def _retrieve_motion_state(data, doorbell): +async def _async_retrieve_motion_state(data, doorbell): - return _activity_time_based_state( + return await _async_activity_time_based_state( data, doorbell, [ActivityType.DOORBELL_MOTION, ActivityType.DOORBELL_DING] ) -def _retrieve_ding_state(data, doorbell): +async def _async_retrieve_ding_state(data, doorbell): - return _activity_time_based_state(data, doorbell, [ActivityType.DOORBELL_DING]) + return await _async_activity_time_based_state( + data, doorbell, [ActivityType.DOORBELL_DING] + ) -def _activity_time_based_state(data, doorbell, activity_types): +async def _async_activity_time_based_state(data, doorbell, activity_types): """Get the latest state of the sensor.""" - latest = data.get_latest_device_activity(doorbell.device_id, *activity_types) + latest = await data.async_get_latest_device_activity( + doorbell.device_id, *activity_types + ) if latest is not None: start = latest.activity_start_time - end = latest.activity_end_time + timedelta(seconds=30) + end = latest.activity_end_time + timedelta(seconds=45) return start <= datetime.now() <= end return None -# Sensor types: Name, device_class, state_provider -SENSOR_TYPES_DOOR = {"door_open": ["Open", "door", _retrieve_door_state]} +SENSOR_NAME = 0 +SENSOR_DEVICE_CLASS = 1 +SENSOR_STATE_PROVIDER = 2 + +# sensor_type: [name, device_class, async_state_provider] +SENSOR_TYPES_DOOR = {"door_open": ["Open", "door", _async_retrieve_door_state]} SENSOR_TYPES_DOORBELL = { - "doorbell_ding": ["Ding", "occupancy", _retrieve_ding_state], - "doorbell_motion": ["Motion", "motion", _retrieve_motion_state], - "doorbell_online": ["Online", "connectivity", _retrieve_online_state], + "doorbell_ding": ["Ding", "occupancy", _async_retrieve_ding_state], + "doorbell_motion": ["Motion", "motion", _async_retrieve_motion_state], + "doorbell_online": ["Online", "connectivity", _async_retrieve_online_state], } -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 August binary sensors.""" data = hass.data[DATA_AUGUST] devices = [] for door in data.locks: for sensor_type in SENSOR_TYPES_DOOR: - state_provider = SENSOR_TYPES_DOOR[sensor_type][2] - if state_provider(data, door) is LockDoorStatus.UNKNOWN: + if not data.lock_has_doorsense(door.device_id): _LOGGER.debug( "Not adding sensor class %s for lock %s ", - SENSOR_TYPES_DOOR[sensor_type][1], + SENSOR_TYPES_DOOR[sensor_type][SENSOR_DEVICE_CLASS], door.device_name, ) continue _LOGGER.debug( "Adding sensor class %s for %s", - SENSOR_TYPES_DOOR[sensor_type][1], + SENSOR_TYPES_DOOR[sensor_type][SENSOR_DEVICE_CLASS], door.device_name, ) devices.append(AugustDoorBinarySensor(data, sensor_type, door)) @@ -88,12 +96,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for sensor_type in SENSOR_TYPES_DOORBELL: _LOGGER.debug( "Adding doorbell sensor class %s for %s", - SENSOR_TYPES_DOORBELL[sensor_type][1], + SENSOR_TYPES_DOORBELL[sensor_type][SENSOR_DEVICE_CLASS], doorbell.device_name, ) devices.append(AugustDoorbellBinarySensor(data, sensor_type, doorbell)) - add_entities(devices, True) + async_add_entities(devices, True) class AugustDoorBinarySensor(BinarySensorDevice): @@ -120,28 +128,79 @@ class AugustDoorBinarySensor(BinarySensorDevice): @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" - return SENSOR_TYPES_DOOR[self._sensor_type][1] + return SENSOR_TYPES_DOOR[self._sensor_type][SENSOR_DEVICE_CLASS] @property def name(self): """Return the name of the binary sensor.""" return "{} {}".format( - self._door.device_name, SENSOR_TYPES_DOOR[self._sensor_type][0] + self._door.device_name, SENSOR_TYPES_DOOR[self._sensor_type][SENSOR_NAME] ) - def update(self): - """Get the latest state of the sensor.""" - state_provider = SENSOR_TYPES_DOOR[self._sensor_type][2] - self._state = state_provider(self._data, self._door) - self._available = self._state is not None + async def async_update(self): + """Get the latest state of the sensor and update activity.""" + async_state_provider = SENSOR_TYPES_DOOR[self._sensor_type][ + SENSOR_STATE_PROVIDER + ] + lock_door_state = await async_state_provider(self._data, self._door) + self._available = ( + lock_door_state is not None and lock_door_state != LockDoorStatus.UNKNOWN + ) + self._state = lock_door_state == LockDoorStatus.OPEN - self._state = self._state == LockDoorStatus.OPEN + door_activity = await self._data.async_get_latest_device_activity( + self._door.device_id, ActivityType.DOOR_OPERATION + ) + + if door_activity is not None: + self._sync_door_activity(door_activity) + + def _update_door_state(self, door_state, update_start_time): + new_state = door_state == LockDoorStatus.OPEN + if self._state != new_state: + self._state = new_state + self._data.update_door_state( + self._door.device_id, door_state, update_start_time + ) + + def _sync_door_activity(self, door_activity): + """Check the activity for the latest door open/close activity (events). + + We use this to determine the door state in between calls to the lock + api as we update it more frequently + """ + last_door_state_update_time_utc = self._data.get_last_door_state_update_time_utc( + self._door.device_id + ) + activity_end_time_utc = dt.as_utc(door_activity.activity_end_time) + + if activity_end_time_utc > last_door_state_update_time_utc: + _LOGGER.debug( + "The activity log has new events for %s: [action=%s] [activity_end_time_utc=%s] > [last_door_state_update_time_utc=%s]", + self.name, + door_activity.action, + activity_end_time_utc, + last_door_state_update_time_utc, + ) + activity_start_time_utc = dt.as_utc(door_activity.activity_start_time) + if door_activity.action in ACTIVITY_ACTION_STATES: + self._update_door_state( + ACTIVITY_ACTION_STATES[door_activity.action], + activity_start_time_utc, + ) + else: + _LOGGER.info( + "Unhandled door activity action %s for %s", + door_activity.action, + self.name, + ) @property def unique_id(self) -> str: """Get the unique of the door open binary sensor.""" return "{:s}_{:s}".format( - self._door.device_id, SENSOR_TYPES_DOOR[self._sensor_type][0].lower() + self._door.device_id, + SENSOR_TYPES_DOOR[self._sensor_type][SENSOR_NAME].lower(), ) @@ -169,25 +228,31 @@ class AugustDoorbellBinarySensor(BinarySensorDevice): @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" - return SENSOR_TYPES_DOORBELL[self._sensor_type][1] + return SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_DEVICE_CLASS] @property def name(self): """Return the name of the binary sensor.""" return "{} {}".format( - self._doorbell.device_name, SENSOR_TYPES_DOORBELL[self._sensor_type][0] + self._doorbell.device_name, + SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_NAME], ) - def update(self): + async def async_update(self): """Get the latest state of the sensor.""" - state_provider = SENSOR_TYPES_DOORBELL[self._sensor_type][2] - self._state = state_provider(self._data, self._doorbell) - self._available = self._doorbell.is_online + async_state_provider = SENSOR_TYPES_DOORBELL[self._sensor_type][ + SENSOR_STATE_PROVIDER + ] + self._state = await async_state_provider(self._data, self._doorbell) + # The doorbell will go into standby mode when there is no motion + # for a short while. It will wake by itself when needed so we need + # to consider is available or we will not report motion or dings + self._available = self._doorbell.is_online or self._doorbell.status == "standby" @property def unique_id(self) -> str: """Get the unique id of the doorbell sensor.""" return "{:s}_{:s}".format( self._doorbell.device_id, - SENSOR_TYPES_DOORBELL[self._sensor_type][0].lower(), + SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_NAME].lower(), ) diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py index 885ee444c6b..5426d9574dc 100644 --- a/homeassistant/components/august/camera.py +++ b/homeassistant/components/august/camera.py @@ -10,7 +10,7 @@ from . import DATA_AUGUST, DEFAULT_TIMEOUT SCAN_INTERVAL = timedelta(seconds=10) -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 August cameras.""" data = hass.data[DATA_AUGUST] devices = [] @@ -18,14 +18,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for doorbell in data.doorbells: devices.append(AugustCamera(data, doorbell, DEFAULT_TIMEOUT)) - add_entities(devices, True) + async_add_entities(devices, True) class AugustCamera(Camera): - """An implementation of a Canary security camera.""" + """An implementation of a August security camera.""" def __init__(self, data, doorbell, timeout): - """Initialize a Canary security camera.""" + """Initialize a August security camera.""" super().__init__() self._data = data self._doorbell = doorbell @@ -58,18 +58,23 @@ class AugustCamera(Camera): """Return the camera model.""" return "Doorbell" - def camera_image(self): + async def async_camera_image(self): """Return bytes of camera image.""" - latest = self._data.get_doorbell_detail(self._doorbell.device_id) + latest = await self._data.async_get_doorbell_detail(self._doorbell.device_id) if self._image_url is not latest.image_url: self._image_url = latest.image_url - self._image_content = requests.get( - self._image_url, timeout=self._timeout - ).content + self._image_content = await self.hass.async_add_executor_job( + self._camera_image + ) return self._image_content + def _camera_image(self): + """Return bytes of camera image via http get.""" + # Move this to py-august: see issue#32048 + return requests.get(self._image_url, timeout=self._timeout).content + @property def unique_id(self) -> str: """Get the unique id of the camera.""" diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index d336e21653b..9d5df1192a7 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -2,11 +2,12 @@ from datetime import timedelta import logging -from august.activity import ActivityType +from august.activity import ACTIVITY_ACTION_STATES, ActivityType from august.lock import LockStatus from homeassistant.components.lock import LockDevice from homeassistant.const import ATTR_BATTERY_LEVEL +from homeassistant.util import dt from . import DATA_AUGUST @@ -15,7 +16,7 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=10) -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 August locks.""" data = hass.data[DATA_AUGUST] devices = [] @@ -24,7 +25,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.debug("Adding lock for %s", lock.device_name) devices.append(AugustLock(data, lock)) - add_entities(devices, True) + async_add_entities(devices, True) class AugustLock(LockDevice): @@ -39,27 +40,77 @@ class AugustLock(LockDevice): self._changed_by = None self._available = False - def lock(self, **kwargs): + async def async_lock(self, **kwargs): """Lock the device.""" - self._data.lock(self._lock.device_id) + update_start_time_utc = dt.utcnow() + lock_status = await self.hass.async_add_executor_job( + self._data.lock, self._lock.device_id + ) + self._update_lock_status(lock_status, update_start_time_utc) - def unlock(self, **kwargs): + async def async_unlock(self, **kwargs): """Unlock the device.""" - self._data.unlock(self._lock.device_id) + update_start_time_utc = dt.utcnow() + lock_status = await self.hass.async_add_executor_job( + self._data.unlock, self._lock.device_id + ) + self._update_lock_status(lock_status, update_start_time_utc) - def update(self): - """Get the latest state of the sensor.""" - self._lock_status = self._data.get_lock_status(self._lock.device_id) - self._available = self._lock_status is not None + def _update_lock_status(self, lock_status, update_start_time_utc): + if self._lock_status != lock_status: + self._lock_status = lock_status + self._data.update_lock_status( + self._lock.device_id, lock_status, update_start_time_utc + ) + self.schedule_update_ha_state() - self._lock_detail = self._data.get_lock_detail(self._lock.device_id) + async def async_update(self): + """Get the latest state of the sensor and update activity.""" + self._lock_status = await self._data.async_get_lock_status(self._lock.device_id) + self._available = ( + self._lock_status is not None and self._lock_status != LockStatus.UNKNOWN + ) + self._lock_detail = await self._data.async_get_lock_detail(self._lock.device_id) - activity = self._data.get_latest_device_activity( + lock_activity = await self._data.async_get_latest_device_activity( self._lock.device_id, ActivityType.LOCK_OPERATION ) - if activity is not None: - self._changed_by = activity.operated_by + if lock_activity is not None: + self._changed_by = lock_activity.operated_by + self._sync_lock_activity(lock_activity) + + def _sync_lock_activity(self, lock_activity): + """Check the activity for the latest lock/unlock activity (events). + + We use this to determine the lock state in between calls to the lock + api as we update it more frequently + """ + last_lock_status_update_time_utc = self._data.get_last_lock_status_update_time_utc( + self._lock.device_id + ) + activity_end_time_utc = dt.as_utc(lock_activity.activity_end_time) + + if activity_end_time_utc > last_lock_status_update_time_utc: + _LOGGER.debug( + "The activity log has new events for %s: [action=%s] [activity_end_time_utc=%s] > [last_lock_status_update_time_utc=%s]", + self.name, + lock_activity.action, + activity_end_time_utc, + last_lock_status_update_time_utc, + ) + activity_start_time_utc = dt.as_utc(lock_activity.activity_start_time) + if lock_activity.action in ACTIVITY_ACTION_STATES: + self._update_lock_status( + ACTIVITY_ACTION_STATES[lock_activity.action], + activity_start_time_utc, + ) + else: + _LOGGER.info( + "Unhandled lock activity action %s for %s", + lock_activity.action, + self.name, + ) @property def name(self): diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index bacd7346ca7..7afa742f3ca 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["py-august==0.8.1"], + "requirements": ["py-august==0.14.0"], "dependencies": ["configurator"], - "codeowners": [] + "codeowners": ["@bdraco"] } diff --git a/homeassistant/components/automation/mqtt.py b/homeassistant/components/automation/mqtt.py index fb0073c78d5..046cbba2873 100644 --- a/homeassistant/components/automation/mqtt.py +++ b/homeassistant/components/automation/mqtt.py @@ -11,8 +11,10 @@ import homeassistant.helpers.config_validation as cv # mypy: allow-untyped-defs CONF_ENCODING = "encoding" +CONF_QOS = "qos" CONF_TOPIC = "topic" DEFAULT_ENCODING = "utf-8" +DEFAULT_QOS = 0 TRIGGER_SCHEMA = vol.Schema( { @@ -20,6 +22,9 @@ TRIGGER_SCHEMA = vol.Schema( vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_PAYLOAD): cv.string, vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string, + vol.Optional(CONF_QOS, default=DEFAULT_QOS): vol.All( + vol.Coerce(int), vol.In([0, 1, 2]) + ), } ) @@ -29,6 +34,7 @@ async def async_attach_trigger(hass, config, action, automation_info): topic = config[CONF_TOPIC] payload = config.get(CONF_PAYLOAD) encoding = config[CONF_ENCODING] or None + qos = config[CONF_QOS] @callback def mqtt_automation_listener(mqttmsg): @@ -49,6 +55,6 @@ async def async_attach_trigger(hass, config, action, automation_info): hass.async_run_job(action, {"trigger": data}) remove = await mqtt.async_subscribe( - hass, topic, mqtt_automation_listener, encoding=encoding + hass, topic, mqtt_automation_listener, encoding=encoding, qos=qos ) return remove diff --git a/homeassistant/components/axis/.translations/ca.json b/homeassistant/components/axis/.translations/ca.json index ecf7b552bba..58f5c0e4ad2 100644 --- a/homeassistant/components/axis/.translations/ca.json +++ b/homeassistant/components/axis/.translations/ca.json @@ -9,7 +9,7 @@ }, "error": { "already_configured": "El dispositiu ja est\u00e0 configurat", - "already_in_progress": "El flux de dades pel dispositiu ja est\u00e0 en curs.", + "already_in_progress": "El flux de dades de configuraci\u00f3 pel dispositiu ja est\u00e0 en curs.", "device_unavailable": "El dispositiu no est\u00e0 disponible", "faulty_credentials": "Credencials d'usuari incorrectes" }, diff --git a/homeassistant/components/axis/.translations/en.json b/homeassistant/components/axis/.translations/en.json index abc1e2f17ec..1f00800422c 100644 --- a/homeassistant/components/axis/.translations/en.json +++ b/homeassistant/components/axis/.translations/en.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Device is already configured", - "bad_config_file": "Bad data from config file", + "bad_config_file": "Bad data from configuration file", "link_local_address": "Link local addresses are not supported", "not_axis_device": "Discovered device not an Axis device", "updated_configuration": "Updated device configuration with new host address" diff --git a/homeassistant/components/axis/.translations/hu.json b/homeassistant/components/axis/.translations/hu.json index 41dd3c00d2b..4f05087cad8 100644 --- a/homeassistant/components/axis/.translations/hu.json +++ b/homeassistant/components/axis/.translations/hu.json @@ -1,10 +1,14 @@ { "config": { + "abort": { + "updated_configuration": "Friss\u00edtett eszk\u00f6zkonfigur\u00e1ci\u00f3 \u00faj \u00e1llom\u00e1sc\u00edmmel" + }, "error": { "already_configured": "Az eszk\u00f6zt m\u00e1r konfigur\u00e1ltuk", "device_unavailable": "Az eszk\u00f6z nem \u00e9rhet\u0151 el", "faulty_credentials": "Rossz felhaszn\u00e1l\u00f3i hiteles\u00edt\u0151 adatok" }, + "flow_title": "Axis eszk\u00f6z: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/.translations/ko.json b/homeassistant/components/axis/.translations/ko.json index e471ae3ea7a..3f1aa97f266 100644 --- a/homeassistant/components/axis/.translations/ko.json +++ b/homeassistant/components/axis/.translations/ko.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "bad_config_file": "\uad6c\uc131 \ud30c\uc77c\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "bad_config_file": "\uad6c\uc131 \ud30c\uc77c\uc5d0 \uc798\ubabb\ub41c \ub370\uc774\ud130\uac00 \uc788\uc2b5\ub2c8\ub2e4", "link_local_address": "\ub85c\uceec \uc8fc\uc18c \uc5f0\uacb0\uc740 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", "not_axis_device": "\ubc1c\uacac\ub41c \uae30\uae30\ub294 Axis \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4", "updated_configuration": "\uc0c8\ub85c\uc6b4 \ud638\uc2a4\ud2b8 \uc8fc\uc18c\ub85c \uc5c5\ub370\uc774\ud2b8\ub41c \uae30\uae30 \uad6c\uc131" diff --git a/homeassistant/components/axis/.translations/nl.json b/homeassistant/components/axis/.translations/nl.json index 10fc8c02d66..b512690e2a3 100644 --- a/homeassistant/components/axis/.translations/nl.json +++ b/homeassistant/components/axis/.translations/nl.json @@ -4,7 +4,8 @@ "already_configured": "Apparaat is al geconfigureerd", "bad_config_file": "Slechte gegevens van het configuratiebestand", "link_local_address": "Link-lokale adressen worden niet ondersteund", - "not_axis_device": "Ontdekte apparaat, is geen Axis-apparaat" + "not_axis_device": "Ontdekte apparaat, is geen Axis-apparaat", + "updated_configuration": "Bijgewerkte apparaatconfiguratie met nieuw hostadres" }, "error": { "already_configured": "Apparaat is al geconfigureerd", diff --git a/homeassistant/components/axis/.translations/no.json b/homeassistant/components/axis/.translations/no.json index 60db56146fa..32e1cc2fd40 100644 --- a/homeassistant/components/axis/.translations/no.json +++ b/homeassistant/components/axis/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", - "bad_config_file": "D\u00e5rlig data fra konfigurasjonsfilen", + "bad_config_file": "D\u00e5rlige data fra konfigurasjonsfilen", "link_local_address": "Linking av lokale adresser st\u00f8ttes ikke", "not_axis_device": "Oppdaget enhet ikke en Axis enhet", "updated_configuration": "Oppdatert enhetskonfigurasjonen med ny vertsadresse" diff --git a/homeassistant/components/axis/.translations/pl.json b/homeassistant/components/axis/.translations/pl.json index d5deb327a75..9f7bb55147d 100644 --- a/homeassistant/components/axis/.translations/pl.json +++ b/homeassistant/components/axis/.translations/pl.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", "bad_config_file": "B\u0142\u0119dne dane z pliku konfiguracyjnego", "link_local_address": "Po\u0142\u0105czenie lokalnego adresu nie jest obs\u0142ugiwane", "not_axis_device": "Wykryte urz\u0105dzenie nie jest urz\u0105dzeniem Axis", "updated_configuration": "Zaktualizowano konfiguracj\u0119 urz\u0105dzenia o nowy adres hosta" }, "error": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", - "already_in_progress": "Konfigurowanie urz\u0105dzenia jest ju\u017c w toku.", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "already_in_progress": "Konfiguracja urz\u0105dzenia jest ju\u017c w toku.", "device_unavailable": "Urz\u0105dzenie jest niedost\u0119pne", "faulty_credentials": "B\u0142\u0119dne dane uwierzytelniaj\u0105ce" }, diff --git a/homeassistant/components/axis/.translations/sv.json b/homeassistant/components/axis/.translations/sv.json index a38ef2ef745..59761c7202f 100644 --- a/homeassistant/components/axis/.translations/sv.json +++ b/homeassistant/components/axis/.translations/sv.json @@ -2,9 +2,10 @@ "config": { "abort": { "already_configured": "Enheten \u00e4r redan konfigurerad", - "bad_config_file": "Felaktig data fr\u00e5n config fil", + "bad_config_file": "Felaktig data fr\u00e5n konfigurationsfilen", "link_local_address": "Link local addresses are not supported", - "not_axis_device": "Uppt\u00e4ckte enhet som inte \u00e4r en Axis enhet" + "not_axis_device": "Uppt\u00e4ckte enhet som inte \u00e4r en Axis enhet", + "updated_configuration": "Uppdaterad enhetskonfiguration med ny v\u00e4rdadress" }, "error": { "already_configured": "Enheten \u00e4r redan konfigurerad", @@ -12,6 +13,7 @@ "device_unavailable": "Enheten \u00e4r inte tillg\u00e4nglig", "faulty_credentials": "Felaktiga anv\u00e4ndaruppgifter" }, + "flow_title": "Axisenhet: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/.translations/zh-Hant.json b/homeassistant/components/axis/.translations/zh-Hant.json index 751a7544202..ac552afe583 100644 --- a/homeassistant/components/axis/.translations/zh-Hant.json +++ b/homeassistant/components/axis/.translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "bad_config_file": "\u8a2d\u5b9a\u6a94\u6848\u8cc7\u6599\u7121\u6548", + "bad_config_file": "\u8a2d\u5b9a\u6a94\u6848\u8cc7\u6599\u7121\u6548\u932f\u8aa4", "link_local_address": "\u4e0d\u652f\u63f4\u9023\u7d50\u672c\u5730\u7aef\u4f4d\u5740", "not_axis_device": "\u6240\u767c\u73fe\u7684\u8a2d\u5099\u4e26\u975e Axis \u8a2d\u5099", "updated_configuration": "\u4f7f\u7528\u65b0\u4e3b\u6a5f\u7aef\u4f4d\u5740\u66f4\u65b0\u88dd\u7f6e\u8a2d\u5b9a" diff --git a/homeassistant/components/axis/__init__.py b/homeassistant/components/axis/__init__.py index 5c928aa9f31..5294e30ed6f 100644 --- a/homeassistant/components/axis/__init__.py +++ b/homeassistant/components/axis/__init__.py @@ -1,15 +1,23 @@ """Support for Axis devices.""" +import logging + from homeassistant.const import ( CONF_DEVICE, + CONF_HOST, CONF_MAC, + CONF_PASSWORD, + CONF_PORT, CONF_TRIGGER_TIME, + CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) from .const import CONF_CAMERA, CONF_EVENTS, DEFAULT_TRIGGER_TIME, DOMAIN from .device import AxisNetworkDevice, get_device +LOGGER = logging.getLogger(__name__) + async def async_setup(hass, config): """Old way to set up Axis devices.""" @@ -35,7 +43,7 @@ async def async_setup_entry(hass, config_entry): config_entry, unique_id=device.api.vapix.params.system_serialnumber ) - hass.data[DOMAIN][device.serial] = device + hass.data[DOMAIN][config_entry.unique_id] = device await device.async_update_device_registry() @@ -52,7 +60,13 @@ async def async_unload_entry(hass, config_entry): async def async_populate_options(hass, config_entry): """Populate default options for device.""" - device = await get_device(hass, config_entry.data[CONF_DEVICE]) + device = await get_device( + hass, + host=config_entry.data[CONF_HOST], + port=config_entry.data[CONF_PORT], + username=config_entry.data[CONF_USERNAME], + password=config_entry.data[CONF_PASSWORD], + ) supported_formats = device.vapix.params.image_format camera = bool(supported_formats) @@ -64,3 +78,18 @@ async def async_populate_options(hass, config_entry): } hass.config_entries.async_update_entry(config_entry, options=options) + + +async def async_migrate_entry(hass, config_entry): + """Migrate old entry.""" + LOGGER.debug("Migrating from version %s", config_entry.version) + + # Flatten configuration but keep old data if user rollbacks HASS + if config_entry.version == 1: + config_entry.data = {**config_entry.data, **config_entry.data[CONF_DEVICE]} + + config_entry.version = 2 + + LOGGER.info("Migration to version %s successful", config_entry.version) + + return True diff --git a/homeassistant/components/axis/camera.py b/homeassistant/components/axis/camera.py index 6b82c938a99..51d6b6805cc 100644 --- a/homeassistant/components/axis/camera.py +++ b/homeassistant/components/axis/camera.py @@ -9,7 +9,6 @@ from homeassistant.components.mjpeg.camera import ( ) from homeassistant.const import ( CONF_AUTHENTICATION, - CONF_DEVICE, CONF_HOST, CONF_NAME, CONF_PASSWORD, @@ -35,15 +34,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config = { CONF_NAME: config_entry.data[CONF_NAME], - CONF_USERNAME: config_entry.data[CONF_DEVICE][CONF_USERNAME], - CONF_PASSWORD: config_entry.data[CONF_DEVICE][CONF_PASSWORD], + CONF_USERNAME: config_entry.data[CONF_USERNAME], + CONF_PASSWORD: config_entry.data[CONF_PASSWORD], CONF_MJPEG_URL: AXIS_VIDEO.format( - config_entry.data[CONF_DEVICE][CONF_HOST], - config_entry.data[CONF_DEVICE][CONF_PORT], + config_entry.data[CONF_HOST], config_entry.data[CONF_PORT], ), CONF_STILL_IMAGE_URL: AXIS_IMAGE.format( - config_entry.data[CONF_DEVICE][CONF_HOST], - config_entry.data[CONF_DEVICE][CONF_PORT], + config_entry.data[CONF_HOST], config_entry.data[CONF_PORT], ), CONF_AUTHENTICATION: HTTP_DIGEST_AUTHENTICATION, } @@ -76,14 +73,14 @@ class AxisCamera(AxisEntityBase, MjpegCamera): async def stream_source(self): """Return the stream source.""" return AXIS_STREAM.format( - self.device.config_entry.data[CONF_DEVICE][CONF_USERNAME], - self.device.config_entry.data[CONF_DEVICE][CONF_PASSWORD], + self.device.config_entry.data[CONF_USERNAME], + self.device.config_entry.data[CONF_PASSWORD], self.device.host, ) def _new_address(self): """Set new device address for video stream.""" - port = self.device.config_entry.data[CONF_DEVICE][CONF_PORT] + port = self.device.config_entry.data[CONF_PORT] self._mjpeg_url = AXIS_VIDEO.format(self.device.host, port) self._still_image_url = AXIS_IMAGE.format(self.device.host, port) diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 88c1cab98c1..29658c19c5b 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -4,7 +4,6 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import ( - CONF_DEVICE, CONF_HOST, CONF_MAC, CONF_NAME, @@ -33,16 +32,12 @@ DEFAULT_PORT = 80 class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Axis config flow.""" - VERSION = 1 + VERSION = 2 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH def __init__(self): """Initialize the Axis config flow.""" self.device_config = {} - self.model = None - self.name = None - self.serial_number = None - self.discovery_schema = {} self.import_schema = {} @@ -55,24 +50,32 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: try: + device = await get_device( + self.hass, + host=user_input[CONF_HOST], + port=user_input[CONF_PORT], + username=user_input[CONF_USERNAME], + password=user_input[CONF_PASSWORD], + ) + + serial_number = device.vapix.params.system_serialnumber + await self.async_set_unique_id(serial_number) + + self._abort_if_unique_id_configured( + updates={ + CONF_HOST: user_input[CONF_HOST], + CONF_PORT: user_input[CONF_PORT], + } + ) + self.device_config = { CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT], CONF_USERNAME: user_input[CONF_USERNAME], CONF_PASSWORD: user_input[CONF_PASSWORD], + CONF_MAC: serial_number, + CONF_MODEL: device.vapix.params.prodnbr, } - device = await get_device(self.hass, self.device_config) - - self.serial_number = device.vapix.params.system_serialnumber - config_entry = await self.async_set_unique_id(self.serial_number) - if config_entry: - return self._update_entry( - config_entry, - host=user_input[CONF_HOST], - port=user_input[CONF_PORT], - ) - - self.model = device.vapix.params.prodnbr return await self._create_entry() @@ -101,41 +104,23 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): Generate a name to be used as a prefix for device entities. """ + model = self.device_config[CONF_MODEL] same_model = [ entry.data[CONF_NAME] for entry in self.hass.config_entries.async_entries(DOMAIN) - if entry.data[CONF_MODEL] == self.model + if entry.data[CONF_MODEL] == model ] - self.name = f"{self.model}" + name = model for idx in range(len(same_model) + 1): - self.name = f"{self.model} {idx}" - if self.name not in same_model: + name = f"{model} {idx}" + if name not in same_model: break - data = { - CONF_DEVICE: self.device_config, - CONF_NAME: self.name, - CONF_MAC: self.serial_number, - CONF_MODEL: self.model, - } + self.device_config[CONF_NAME] = name - title = f"{self.model} - {self.serial_number}" - return self.async_create_entry(title=title, data=data) - - def _update_entry(self, entry, host, port): - """Update existing entry.""" - if ( - entry.data[CONF_DEVICE][CONF_HOST] == host - and entry.data[CONF_DEVICE][CONF_PORT] == port - ): - return self.async_abort(reason="already_configured") - - entry.data[CONF_DEVICE][CONF_HOST] = host - entry.data[CONF_DEVICE][CONF_PORT] = port - - self.hass.config_entries.async_update_entry(entry) - return self.async_abort(reason="updated_configuration") + title = f"{model} - {self.device_config[CONF_MAC]}" + return self.async_create_entry(title=title, data=self.device_config) async def async_step_zeroconf(self, discovery_info): """Prepare configuration for a discovered Axis device.""" @@ -147,18 +132,19 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if discovery_info[CONF_HOST].startswith("169.254"): return self.async_abort(reason="link_local_address") - config_entry = await self.async_set_unique_id(serial_number) - if config_entry: - return self._update_entry( - config_entry, - host=discovery_info[CONF_HOST], - port=discovery_info[CONF_PORT], - ) + await self.async_set_unique_id(serial_number) + + self._abort_if_unique_id_configured( + updates={ + CONF_HOST: discovery_info[CONF_HOST], + CONF_PORT: discovery_info[CONF_PORT], + } + ) # 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], + CONF_NAME: discovery_info["hostname"][:-7], + CONF_HOST: discovery_info[CONF_HOST], } self.discovery_schema = { diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index 85ad59268df..a204136e018 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -7,9 +7,7 @@ import axis from axis.streammanager import SIGNAL_PLAYING from homeassistant.const import ( - CONF_DEVICE, CONF_HOST, - CONF_MAC, CONF_NAME, CONF_PASSWORD, CONF_PORT, @@ -42,7 +40,7 @@ class AxisNetworkDevice: @property def host(self): """Return the host of this device.""" - return self.config_entry.data[CONF_DEVICE][CONF_HOST] + return self.config_entry.data[CONF_HOST] @property def model(self): @@ -75,7 +73,13 @@ class AxisNetworkDevice: async def async_setup(self): """Set up the device.""" try: - self.api = await get_device(self.hass, self.config_entry.data[CONF_DEVICE]) + self.api = await get_device( + self.hass, + host=self.config_entry.data[CONF_HOST], + port=self.config_entry.data[CONF_PORT], + username=self.config_entry.data[CONF_USERNAME], + password=self.config_entry.data[CONF_PASSWORD], + ) except CannotConnect: raise ConfigEntryNotReady @@ -126,7 +130,7 @@ class AxisNetworkDevice: This is a static method because a class method (bound method), can not be used with weak references. """ - device = hass.data[DOMAIN][entry.data[CONF_MAC]] + device = hass.data[DOMAIN][entry.unique_id] device.api.config.host = device.host async_dispatcher_send(hass, device.event_new_address) @@ -197,15 +201,15 @@ class AxisNetworkDevice: return True -async def get_device(hass, config): +async def get_device(hass, host, port, username, password): """Create a Axis device.""" device = axis.AxisDevice( loop=hass.loop, - host=config[CONF_HOST], - username=config[CONF_USERNAME], - password=config[CONF_PASSWORD], - port=config[CONF_PORT], + host=host, + port=port, + username=username, + password=password, web_proto="http", ) @@ -224,13 +228,11 @@ async def get_device(hass, config): return device except axis.Unauthorized: - LOGGER.warning( - "Connected to device at %s but not registered.", config[CONF_HOST] - ) + LOGGER.warning("Connected to device at %s but not registered.", host) raise AuthenticationRequired except (asyncio.TimeoutError, axis.RequestError): - LOGGER.error("Error connecting to the Axis device at %s", config[CONF_HOST]) + LOGGER.error("Error connecting to the Axis device at %s", host) raise CannotConnect except axis.AxisException: diff --git a/homeassistant/components/axis/strings.json b/homeassistant/components/axis/strings.json index 7facd7060ad..04a9f9e388a 100644 --- a/homeassistant/components/axis/strings.json +++ b/homeassistant/components/axis/strings.json @@ -1,30 +1,29 @@ { - "config": { - "title": "Axis device", - "flow_title": "Axis device: {name} ({host})", - "step": { - "user": { - "title": "Set up Axis device", - "data": { - "host": "Host", - "username": "Username", - "password": "Password", - "port": "Port" - } - } - }, - "error": { - "already_configured": "Device is already configured", - "already_in_progress": "Config flow for device is already in progress.", - "device_unavailable": "Device is not available", - "faulty_credentials": "Bad user credentials" - }, - "abort": { - "already_configured": "Device is already configured", - "bad_config_file": "Bad data from config file", - "link_local_address": "Link local addresses are not supported", - "not_axis_device": "Discovered device not an Axis device", - "updated_configuration": "Updated device configuration with new host address" + "config": { + "title": "Axis device", + "flow_title": "Axis device: {name} ({host})", + "step": { + "user": { + "title": "Set up Axis device", + "data": { + "host": "Host", + "username": "Username", + "password": "Password", + "port": "Port" } + } + }, + "error": { + "already_configured": "Device is already configured", + "already_in_progress": "Config flow for device is already in progress.", + "device_unavailable": "Device is not available", + "faulty_credentials": "Bad user credentials" + }, + "abort": { + "already_configured": "Device is already configured", + "bad_config_file": "Bad data from configuration file", + "link_local_address": "Link local addresses are not supported", + "not_axis_device": "Discovered device not an Axis device" } + } } diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py index f5e5865f6f0..259066d4561 100644 --- a/homeassistant/components/bbox/sensor.py +++ b/homeassistant/components/bbox/sensor.py @@ -11,6 +11,7 @@ from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_MONITORED_VARIABLES, CONF_NAME, + DATA_RATE_MEGABITS_PER_SECOND, DEVICE_CLASS_TIMESTAMP, ) import homeassistant.helpers.config_validation as cv @@ -20,8 +21,6 @@ from homeassistant.util.dt import utcnow _LOGGER = logging.getLogger(__name__) -BANDWIDTH_MEGABITS_SECONDS = "Mb/s" - ATTRIBUTION = "Powered by Bouygues Telecom" DEFAULT_NAME = "Bbox" @@ -32,22 +31,22 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) SENSOR_TYPES = { "down_max_bandwidth": [ "Maximum Download Bandwidth", - BANDWIDTH_MEGABITS_SECONDS, + DATA_RATE_MEGABITS_PER_SECOND, "mdi:download", ], "up_max_bandwidth": [ "Maximum Upload Bandwidth", - BANDWIDTH_MEGABITS_SECONDS, + DATA_RATE_MEGABITS_PER_SECOND, "mdi:upload", ], "current_down_bandwidth": [ "Currently Used Download Bandwidth", - BANDWIDTH_MEGABITS_SECONDS, + DATA_RATE_MEGABITS_PER_SECOND, "mdi:download", ], "current_up_bandwidth": [ "Currently Used Upload Bandwidth", - BANDWIDTH_MEGABITS_SECONDS, + DATA_RATE_MEGABITS_PER_SECOND, "mdi:upload", ], "uptime": ["Uptime", None, "mdi:clock"], diff --git a/homeassistant/components/binary_sensor/.translations/es-419.json b/homeassistant/components/binary_sensor/.translations/es-419.json index f1c20e5346b..e727e18775a 100644 --- a/homeassistant/components/binary_sensor/.translations/es-419.json +++ b/homeassistant/components/binary_sensor/.translations/es-419.json @@ -28,6 +28,12 @@ "is_not_occupied": "{entity_name} no est\u00e1 ocupado", "is_not_open": "{entity_name} est\u00e1 cerrado", "is_not_plugged_in": "{entity_name} est\u00e1 desconectado", + "is_not_unsafe": "{entity_name} es seguro", + "is_occupied": "{entity_name} est\u00e1 ocupado", + "is_off": "{entity_name} est\u00e1 apagado", + "is_on": "{entity_name} est\u00e1 encendido", + "is_open": "{entity_name} est\u00e1 abierto", + "is_plugged_in": "{entity_name} est\u00e1 enchufado", "is_powered": "{entity_name} est\u00e1 encendido", "is_present": "{entity_name} est\u00e1 presente", "is_problem": "{entity_name} est\u00e1 detectando un problema", @@ -45,6 +51,7 @@ "hot": "{entity_name} se calent\u00f3", "light": "{entity_name} comenz\u00f3 a detectar luz", "locked": "{entity_name} bloqueado", + "moist": "{entity_name} se humedeci\u00f3", "moist\u00a7": "{entity_name} se humedeci\u00f3", "motion": "{entity_name} comenz\u00f3 a detectar movimiento", "moving": "{entity_name} comenz\u00f3 a moverse", @@ -59,7 +66,22 @@ "not_cold": "{entity_name} no se enfri\u00f3", "not_connected": "{entity_name} desconectado", "not_hot": "{entity_name} no se calent\u00f3", - "not_locked": "{entity_name} desbloqueado" + "not_locked": "{entity_name} desbloqueado", + "not_moist": "{entity_name} se sec\u00f3", + "not_moving": "{entity_name} dej\u00f3 de moverse", + "not_opened": "{entity_name} cerrado", + "not_plugged_in": "{entity_name} desconectado", + "not_present": "{entity_name} no presente", + "not_unsafe": "{entity_name} se volvi\u00f3 seguro", + "occupied": "{entity_name} se ocup\u00f3", + "opened": "{entity_name} abierto", + "plugged_in": "{entity_name} enchufado", + "present": "{entity_name} presente", + "problem": "{entity_name} comenz\u00f3 a detectar problemas", + "smoke": "{entity_name} comenz\u00f3 a detectar humo", + "sound": "{entity_name} comenz\u00f3 a detectar sonido", + "turned_off": "{entity_name} apagado", + "turned_on": "{entity_name} encendido" } } } \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/sv.json b/homeassistant/components/binary_sensor/.translations/sv.json new file mode 100644 index 00000000000..5df2ce17c92 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/sv.json @@ -0,0 +1,94 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name}-batteriet \u00e4r l\u00e5gt", + "is_cold": "{entity_name} \u00e4r kall", + "is_connected": "{entity_name} \u00e4r ansluten", + "is_gas": "{entity_name} detekterar gas", + "is_hot": "{entity_name} \u00e4r varm", + "is_light": "{entity_name} uppt\u00e4cker ljus", + "is_locked": "{entity_name} \u00e4r l\u00e5st", + "is_moist": "{entity_name} \u00e4r fuktig", + "is_motion": "{entity_name} detekterar r\u00f6relse", + "is_moving": "{entity_name} r\u00f6r sig", + "is_no_gas": "{entity_name} uppt\u00e4cker inte gas", + "is_no_light": "{entity_name} uppt\u00e4cker inte ljus", + "is_no_motion": "{entity_name} detekterar inte r\u00f6relse", + "is_no_problem": "{entity_name} uppt\u00e4cker inte problem", + "is_no_smoke": "{entity_name} detekterar inte r\u00f6k", + "is_no_sound": "{entity_name} uppt\u00e4cker inte ljud", + "is_no_vibration": "{entity_name} uppt\u00e4cker inte vibrationer", + "is_not_bat_low": "{entity_name} batteri \u00e4r normalt", + "is_not_cold": "{entity_name} \u00e4r inte kall", + "is_not_connected": "{entity_name} \u00e4r fr\u00e5nkopplad", + "is_not_hot": "{entity_name} \u00e4r inte varm", + "is_not_locked": "{entity_name} \u00e4r ol\u00e5st", + "is_not_moist": "{entity_name} \u00e4r torr", + "is_not_moving": "{entity_name} r\u00f6r sig inte", + "is_not_occupied": "{entity_name} \u00e4r inte upptagen", + "is_not_open": "{entity_name} \u00e4r st\u00e4ngd", + "is_not_plugged_in": "{entity_name} \u00e4r urkopplad", + "is_not_powered": "{entity_name} \u00e4r inte str\u00f6mf\u00f6rd", + "is_not_present": "{entity_name} finns inte", + "is_not_unsafe": "{entity_name} \u00e4r s\u00e4ker", + "is_occupied": "{entity_name} \u00e4r upptagen", + "is_off": "{entity_name} \u00e4r avst\u00e4ngd", + "is_on": "{entity_name} \u00e4r p\u00e5", + "is_open": "{entity_name} \u00e4r \u00f6ppen", + "is_plugged_in": "{entity_name} \u00e4r ansluten", + "is_powered": "{entity_name} \u00e4r str\u00f6mf\u00f6rd", + "is_present": "{entity_name} \u00e4r n\u00e4rvarande", + "is_problem": "{entity_name} uppt\u00e4cker problem", + "is_smoke": "{entity_name} detekterar r\u00f6k", + "is_sound": "{entity_name} uppt\u00e4cker ljud", + "is_unsafe": "{entity_name} \u00e4r os\u00e4ker", + "is_vibration": "{entity_name} uppt\u00e4cker vibrationer" + }, + "trigger_type": { + "bat_low": "{entity_name} batteri l\u00e5gt", + "closed": "{entity_name} st\u00e4ngd", + "cold": "{entity_name} blev kall", + "connected": "{entity_name} ansluten", + "gas": "{entity_name} b\u00f6rjade detektera gas", + "hot": "{entity_name} blev varm", + "light": "{entity_name} b\u00f6rjade uppt\u00e4cka ljus", + "locked": "{entity_name} l\u00e5st", + "moist": "{entity_name} blev fuktig", + "moist\u00a7": "{entity_name} blev fuktig", + "motion": "{entity_name} b\u00f6rjade detektera r\u00f6relse", + "moving": "{entity_name} b\u00f6rjade r\u00f6ra sig", + "no_gas": "{entity_name} slutade uppt\u00e4cka gas", + "no_light": "{entity_name} slutade uppt\u00e4cka ljus", + "no_motion": "{entity_name} slutade uppt\u00e4cka r\u00f6relse", + "no_problem": "{entity_name} slutade uppt\u00e4cka problem", + "no_smoke": "{entity_name} slutade detektera r\u00f6k", + "no_sound": "{entity_name} slutade uppt\u00e4cka ljud", + "no_vibration": "{entity_name} slutade uppt\u00e4cka vibrationer", + "not_bat_low": "{entity_name} batteri normalt", + "not_cold": "{entity_name} blev inte kall", + "not_connected": "{entity_name} fr\u00e5nkopplad", + "not_hot": "{entity_name} blev inte varm", + "not_locked": "{entity_name} ol\u00e5st", + "not_moist": "{entity_name} blev torr", + "not_moving": "{entity_name} slutade r\u00f6ra sig", + "not_occupied": "{entity_name} blev inte upptagen", + "not_opened": "{entity_name} st\u00e4ngd", + "not_plugged_in": "{entity_name} urkopplad", + "not_powered": "{entity_name} inte str\u00f6mf\u00f6rd", + "not_present": "{entity_name} inte n\u00e4rvarande", + "not_unsafe": "{entity_name} blev s\u00e4ker", + "occupied": "{entity_name} blev upptagen", + "opened": "{entity_name} \u00f6ppnades", + "plugged_in": "{entity_name} ansluten", + "powered": "{entity_name} str\u00f6mf\u00f6rd", + "present": "{entity_name} n\u00e4rvarande", + "problem": "{entity_name} b\u00f6rjade uppt\u00e4cka problem", + "smoke": "{entity_name} b\u00f6rjade detektera r\u00f6k", + "sound": "{entity_name} b\u00f6rjade uppt\u00e4cka ljud", + "turned_off": "{entity_name} st\u00e4ngdes av", + "turned_on": "{entity_name} slogs p\u00e5", + "unsafe": "{entity_name} blev os\u00e4ker", + "vibration": "{entity_name} b\u00f6rjade detektera vibrationer" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py index 63f84b657c1..cb98ec90b5d 100644 --- a/homeassistant/components/binary_sensor/device_condition.py +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -1,4 +1,4 @@ -"""Implemenet device conditions for binary sensor.""" +"""Implement device conditions for binary sensor.""" from typing import Dict, List import voluptuous as vol diff --git a/homeassistant/components/bitcoin/sensor.py b/homeassistant/components/bitcoin/sensor.py index bc8394d51a5..6a8651be6dc 100644 --- a/homeassistant/components/bitcoin/sensor.py +++ b/homeassistant/components/bitcoin/sensor.py @@ -1,4 +1,4 @@ -"""Bitcoin information service that uses blockchain.info.""" +"""Bitcoin information service that uses blockchain.com.""" from datetime import timedelta import logging @@ -12,7 +12,7 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -ATTRIBUTION = "Data provided by blockchain.info" +ATTRIBUTION = "Data provided by blockchain.com" DEFAULT_CURRENCY = "USD" @@ -168,7 +168,7 @@ class BitcoinData: self.ticker = None def update(self): - """Get the latest data from blockchain.info.""" + """Get the latest data from blockchain.com.""" self.stats = statistics.get() self.ticker = exchangerates.get_ticker() diff --git a/homeassistant/components/blockchain/manifest.json b/homeassistant/components/blockchain/manifest.json index f57e91a9262..324abf792df 100644 --- a/homeassistant/components/blockchain/manifest.json +++ b/homeassistant/components/blockchain/manifest.json @@ -1,6 +1,6 @@ { "domain": "blockchain", - "name": "Blockchain.info", + "name": "Blockchain.com", "documentation": "https://www.home-assistant.io/integrations/blockchain", "requirements": ["python-blockchain-api==0.0.2"], "dependencies": [], diff --git a/homeassistant/components/blockchain/sensor.py b/homeassistant/components/blockchain/sensor.py index 6d17484bdd7..acf86957957 100644 --- a/homeassistant/components/blockchain/sensor.py +++ b/homeassistant/components/blockchain/sensor.py @@ -1,4 +1,4 @@ -"""Support for Blockchain.info sensors.""" +"""Support for Blockchain.com sensors.""" from datetime import timedelta import logging @@ -12,7 +12,7 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -ATTRIBUTION = "Data provided by blockchain.info" +ATTRIBUTION = "Data provided by blockchain.com" CONF_ADDRESSES = "addresses" @@ -31,7 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Blockchain.info sensors.""" + """Set up the Blockchain.com sensors.""" addresses = config.get(CONF_ADDRESSES) name = config.get(CONF_NAME) @@ -45,7 +45,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class BlockchainSensor(Entity): - """Representation of a Blockchain.info sensor.""" + """Representation of a Blockchain.com sensor.""" def __init__(self, name, addresses): """Initialize the sensor.""" diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 04ba21555d4..db5c65eab8b 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -421,7 +421,7 @@ class BluesoundPlayer(MediaPlayerDevice): # sync_status. We will force an update if the player is # grouped this isn't a foolproof solution. A better # solution would be to fetch sync_status more often when - # the device is playing. This would solve alot of + # the device is playing. This would solve a lot of # problems. This change will be done when the # communication is moved to a separate library await self.force_update_sync_status() diff --git a/homeassistant/components/bme680/sensor.py b/homeassistant/components/bme680/sensor.py index 65c87890242..43430f724bb 100644 --- a/homeassistant/components/bme680/sensor.py +++ b/homeassistant/components/bme680/sensor.py @@ -1,7 +1,7 @@ """Support for BME680 Sensor over SMBus.""" import logging import threading -from time import sleep, time +from time import monotonic, sleep import bme680 # pylint: disable=import-error from smbus import SMBus # pylint: disable=import-error @@ -240,15 +240,15 @@ class BME680Handler: # Pause to allow initial data read for device validation. sleep(1) - start_time = time() - curr_time = time() + start_time = monotonic() + curr_time = monotonic() burn_in_data = [] _LOGGER.info( "Beginning %d second gas sensor burn in for Air Quality", burn_in_time ) while curr_time - start_time < burn_in_time: - curr_time = time() + curr_time = monotonic() if self._sensor.get_sensor_data() and self._sensor.data.heat_stable: gas_resistance = self._sensor.data.gas_resistance burn_in_data.append(gas_resistance) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 9e507a212b9..6b6311c6b0d 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -2,7 +2,7 @@ "domain": "bmw_connected_drive", "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", - "requirements": ["bimmer_connected==0.7.0"], + "requirements": ["bimmer_connected==0.7.1"], "dependencies": [], "codeowners": ["@gerard33"] } diff --git a/homeassistant/components/bom/sensor.py b/homeassistant/components/bom/sensor.py index 7d951968cb2..bd57e20edaa 100644 --- a/homeassistant/components/bom/sensor.py +++ b/homeassistant/components/bom/sensor.py @@ -112,7 +112,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if station is not None: if zone_id and wmo_id: _LOGGER.warning( - "Using config %s, not %s and %s for BOM sensor", + "Using configuration %s, not %s and %s for BOM sensor", CONF_STATION, CONF_ZONE_ID, CONF_WMO_ID, @@ -281,7 +281,7 @@ def _get_bom_stations(): """Return {CONF_STATION: (lat, lon)} for all stations, for auto-config. This function does several MB of internet requests, so please use the - caching version to minimise latency and hit-count. + caching version to minimize latency and hit-count. """ latlon = {} with io.BytesIO() as file_obj: diff --git a/homeassistant/components/braviatv/manifest.json b/homeassistant/components/braviatv/manifest.json index 1cb3efdd2cc..8bfa48b9195 100644 --- a/homeassistant/components/braviatv/manifest.json +++ b/homeassistant/components/braviatv/manifest.json @@ -2,7 +2,7 @@ "domain": "braviatv", "name": "Sony Bravia TV", "documentation": "https://www.home-assistant.io/integrations/braviatv", - "requirements": ["braviarc-homeassistant==0.3.7.dev0", "getmac==0.8.1"], + "requirements": ["bravia-tv==1.0", "getmac==0.8.1"], "dependencies": ["configurator"], "codeowners": ["@robbiet480"] } diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index ef0640c8e87..67feb8bfc48 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -2,7 +2,7 @@ import ipaddress import logging -from braviarc.braviarc import BraviaRC +from bravia_tv import BraviaRC from getmac import get_mac_address import voluptuous as vol diff --git a/homeassistant/components/broadlink/__init__.py b/homeassistant/components/broadlink/__init__.py index dd7c02b82ad..be6aa266491 100644 --- a/homeassistant/components/broadlink/__init__.py +++ b/homeassistant/components/broadlink/__init__.py @@ -75,18 +75,20 @@ def async_setup_service(hass, host, device): async def _learn_command(call): """Learn a packet from remote.""" + device = hass.data[DOMAIN][call.data[CONF_HOST]] - try: - auth = await hass.async_add_executor_job(device.auth) - except socket.timeout: - _LOGGER.error("Failed to connect to device, timeout") - return - if not auth: - _LOGGER.error("Failed to connect to device") - return - - await hass.async_add_executor_job(device.enter_learning) + for retry in range(DEFAULT_RETRY): + try: + await hass.async_add_executor_job(device.enter_learning) + break + except (socket.timeout, ValueError): + try: + await hass.async_add_executor_job(device.auth) + except socket.timeout: + if retry == DEFAULT_RETRY - 1: + _LOGGER.error("Failed to enter learning mode") + return _LOGGER.info("Press the key you want Home Assistant to learn") start_time = utcnow() diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py index 0b9d10b1e74..96698e5b02f 100644 --- a/homeassistant/components/broadlink/remote.py +++ b/homeassistant/components/broadlink/remote.py @@ -270,7 +270,7 @@ class BroadlinkRemote(RemoteDevice): async def _async_learn_code(self, command, device, toggle, timeout): """Learn a code from a remote. - Capture an aditional code for toggle commands. + Capture an additional code for toggle commands. """ try: if not toggle: diff --git a/homeassistant/components/brother/.translations/es-419.json b/homeassistant/components/brother/.translations/es-419.json new file mode 100644 index 00000000000..49b77b829b5 --- /dev/null +++ b/homeassistant/components/brother/.translations/es-419.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Impresora Brother" + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/.translations/fr.json b/homeassistant/components/brother/.translations/fr.json index 99d49cc3bd8..788d0c74003 100644 --- a/homeassistant/components/brother/.translations/fr.json +++ b/homeassistant/components/brother/.translations/fr.json @@ -9,6 +9,7 @@ "snmp_error": "Serveur SNMP d\u00e9sactiv\u00e9 ou imprimante non prise en charge.", "wrong_host": "Nom d'h\u00f4te ou adresse IP invalide." }, + "flow_title": "Imprimante Brother: {model} {serial_number}", "step": { "user": { "data": { @@ -21,7 +22,9 @@ "zeroconf_confirm": { "data": { "type": "Type d'imprimante" - } + }, + "description": "Voulez-vous ajouter l'imprimante Brother {model} avec le num\u00e9ro de s\u00e9rie `{serial_number}` \u00e0 Home Assistant ?", + "title": "Imprimante Brother d\u00e9couverte" } }, "title": "Imprimante Brother" diff --git a/homeassistant/components/brother/.translations/hu.json b/homeassistant/components/brother/.translations/hu.json index a0e83450b37..1907d65f289 100644 --- a/homeassistant/components/brother/.translations/hu.json +++ b/homeassistant/components/brother/.translations/hu.json @@ -1,7 +1,24 @@ { "config": { + "abort": { + "already_configured": "Ez a nyomtat\u00f3 m\u00e1r konfigur\u00e1lva van.", + "unsupported_model": "Ez a nyomtat\u00f3modell nem t\u00e1mogatott." + }, + "error": { + "connection_error": "Csatlakoz\u00e1si hiba.", + "snmp_error": "Az SNMP szerver ki van kapcsolva, vagy a nyomtat\u00f3 nem t\u00e1mogatott.", + "wrong_host": "\u00c9rv\u00e9nytelen \u00e1llom\u00e1sn\u00e9v vagy IP-c\u00edm." + }, "flow_title": "Brother nyomtat\u00f3: {model} {serial_number}", "step": { + "user": { + "data": { + "host": "Nyomtat\u00f3 \u00e1llom\u00e1sneve vagy IP-c\u00edme", + "type": "A nyomtat\u00f3 t\u00edpusa" + }, + "description": "A Brother nyomtat\u00f3 integr\u00e1ci\u00f3j\u00e1nak be\u00e1ll\u00edt\u00e1sa. Ha probl\u00e9m\u00e1id vannak a konfigur\u00e1ci\u00f3val, l\u00e1togass el a k\u00f6vetkez\u0151 oldalra: https://www.home-assistant.io/integrations/brother", + "title": "Brother nyomtat\u00f3" + }, "zeroconf_confirm": { "data": { "type": "A nyomtat\u00f3 t\u00edpusa" diff --git a/homeassistant/components/brother/.translations/nl.json b/homeassistant/components/brother/.translations/nl.json index ed7d3980f47..c72aab46801 100644 --- a/homeassistant/components/brother/.translations/nl.json +++ b/homeassistant/components/brother/.translations/nl.json @@ -1,18 +1,32 @@ { "config": { "abort": { + "already_configured": "Deze printer is al geconfigureerd.", "unsupported_model": "Dit printermodel wordt niet ondersteund." }, "error": { "connection_error": "Verbindingsfout.", + "snmp_error": "SNMP-server uitgeschakeld of printer wordt niet ondersteund.", "wrong_host": "Ongeldige hostnaam of IP-adres." }, + "flow_title": "Brother Printer: {model} {serial_number}", "step": { "user": { "data": { - "host": "Printerhostnaam of IP-adres" - } + "host": "Printerhostnaam of IP-adres", + "type": "Type printer" + }, + "description": "Zet Brother printerintegratie op. Als u problemen heeft met de configuratie ga dan naar: https://www.home-assistant.io/integrations/brother", + "title": "Brother Printer" + }, + "zeroconf_confirm": { + "data": { + "type": "Type printer" + }, + "description": "Wilt u het Brother Printer {model} met serienummer {serial_number}' toevoegen aan Home Assistant?", + "title": "Ontdekte Brother Printer" } - } + }, + "title": "Brother Printer" } } \ No newline at end of file diff --git a/homeassistant/components/brother/.translations/sl.json b/homeassistant/components/brother/.translations/sl.json index 99caf69a86f..d22f128ffbe 100644 --- a/homeassistant/components/brother/.translations/sl.json +++ b/homeassistant/components/brother/.translations/sl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Ta tiskalnik je \u017ee konfiguriran.", "unsupported_model": "Ta model tiskalnika ni podprt." }, "error": { @@ -8,6 +9,7 @@ "snmp_error": "Stre\u017enik SNMP je izklopljen ali tiskalnik ni podprt.", "wrong_host": "Neveljavno ime gostitelja ali IP naslov." }, + "flow_title": "Tiskalnik Brother: {model} {serial_number}", "step": { "user": { "data": { @@ -16,6 +18,13 @@ }, "description": "Nastavite integracijo tiskalnika Brother. \u010ce imate te\u017eave s konfiguracijo, pojdite na: https://www.home-assistant.io/integrations/brother", "title": "Brother Tiskalnik" + }, + "zeroconf_confirm": { + "data": { + "type": "Vrsta tiskalnika" + }, + "description": "Ali \u017eelite dodati Brother tiskalnik {model} s serijsko \u0161tevilko ' {serial_number} ' v Home Assistant?", + "title": "Odkriti Brother tiskalniki" } }, "title": "Brother Tiskalnik" diff --git a/homeassistant/components/brother/.translations/sv.json b/homeassistant/components/brother/.translations/sv.json index 8661c3278bc..774863d4f08 100644 --- a/homeassistant/components/brother/.translations/sv.json +++ b/homeassistant/components/brother/.translations/sv.json @@ -1,11 +1,32 @@ { "config": { + "abort": { + "already_configured": "Den h\u00e4r skrivaren \u00e4r redan konfigurerad.", + "unsupported_model": "Den h\u00e4r skrivarmodellen st\u00f6ds inte." + }, + "error": { + "connection_error": "Anslutningsfel.", + "snmp_error": "SNMP-servern har st\u00e4ngts av eller s\u00e5 st\u00f6ds inte skrivaren.", + "wrong_host": "Ogiltigt v\u00e4rdnamn eller IP-adress." + }, + "flow_title": "Brother-skrivare: {model} {serial_number}", "step": { + "user": { + "data": { + "host": "Skrivarens v\u00e4rdnamn eller IP-adress", + "type": "Typ av skrivare" + }, + "description": "St\u00e4ll in Brother-skrivarintegration. Om du har problem med konfigurationen g\u00e5r du till: https://www.home-assistant.io/integrations/brother", + "title": "Brother-skrivare" + }, "zeroconf_confirm": { "data": { "type": "Typ av skrivare" - } + }, + "description": "Vill du l\u00e4gga till Brother-skrivaren {model} med serienumret {serial_number} i Home Assistant?", + "title": "Uppt\u00e4ckte Brother-skrivare" } - } + }, + "title": "Brother-skrivare" } } \ No newline at end of file diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index e63fb9b0d7c..51e6c3284ff 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/brother", "dependencies": [], "codeowners": ["@bieniu"], - "requirements": ["brother==0.1.4"], + "requirements": ["brother==0.1.6"], "zeroconf": ["_printer._tcp.local."], "config_flow": true } diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index ad9dac1f727..7cdf69a0c33 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -193,27 +193,50 @@ class WebDavCalendarData: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data.""" + start_of_today = dt.start_of_local_day() + start_of_tomorrow = dt.start_of_local_day() + timedelta(days=1) + # We have to retrieve the results for the whole day as the server # won't return events that have already started - results = self.calendar.date_search( - dt.start_of_local_day(), dt.start_of_local_day() + timedelta(days=1) - ) + results = self.calendar.date_search(start_of_today, start_of_tomorrow) + + # Create new events for each recurrence of an event that happens today. + # For recurring events, some servers return the original event with recurrence rules + # and they would not be properly parsed using their original start/end dates. + new_events = [] + for event in results: + vevent = event.instance.vevent + for start_dt in vevent.getrruleset() or []: + _start_of_today = start_of_today + _start_of_tomorrow = start_of_tomorrow + if self.is_all_day(vevent): + start_dt = start_dt.date() + _start_of_today = _start_of_today.date() + _start_of_tomorrow = _start_of_tomorrow.date() + if _start_of_today <= start_dt < _start_of_tomorrow: + new_event = event.copy() + new_vevent = new_event.instance.vevent + if hasattr(new_vevent, "dtend"): + dur = new_vevent.dtend.value - new_vevent.dtstart.value + new_vevent.dtend.value = start_dt + dur + new_vevent.dtstart.value = start_dt + new_events.append(new_event) + elif _start_of_tomorrow <= start_dt: + break + vevents = [event.instance.vevent for event in results + new_events] # dtstart can be a date or datetime depending if the event lasts a # whole day. Convert everything to datetime to be able to sort it - results.sort(key=lambda x: self.to_datetime(x.instance.vevent.dtstart.value)) + vevents.sort(key=lambda x: self.to_datetime(x.dtstart.value)) vevent = next( ( - event.instance.vevent - for event in results + vevent + for vevent in vevents if ( - self.is_matching(event.instance.vevent, self.search) - and ( - not self.is_all_day(event.instance.vevent) - or self.include_all_day - ) - and not self.is_over(event.instance.vevent) + self.is_matching(vevent, self.search) + and (not self.is_all_day(vevent) or self.include_all_day) + and not self.is_over(vevent) ) ), None, @@ -223,7 +246,7 @@ class WebDavCalendarData: if vevent is None: _LOGGER.debug( "No matching event found in the %d results for %s", - len(results), + len(vevents), self.calendar.name, ) self.event = None diff --git a/homeassistant/components/caldav/manifest.json b/homeassistant/components/caldav/manifest.json index 2f48fb5fc27..85dc005a6a8 100644 --- a/homeassistant/components/caldav/manifest.json +++ b/homeassistant/components/caldav/manifest.json @@ -1,6 +1,6 @@ { "domain": "caldav", - "name": "CalDav", + "name": "CalDAV", "documentation": "https://www.home-assistant.io/integrations/caldav", "requirements": ["caldav==0.6.1"], "dependencies": [], diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index b02874780e5..647e54556c4 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -377,7 +377,7 @@ class Camera(Entity): async def handle_async_mjpeg_stream(self, request): """Serve an HTTP MJPEG stream from the camera. - This method can be overridden by camera plaforms to proxy + This method can be overridden by camera platforms to proxy a direct stream from the camera. """ return await self.handle_async_still_stream(request, self.frame_interval) diff --git a/homeassistant/components/cert_expiry/.translations/es-419.json b/homeassistant/components/cert_expiry/.translations/es-419.json index 392dbf35f5a..e350faffcb3 100644 --- a/homeassistant/components/cert_expiry/.translations/es-419.json +++ b/homeassistant/components/cert_expiry/.translations/es-419.json @@ -1,5 +1,26 @@ { "config": { + "abort": { + "host_port_exists": "Esta combinaci\u00f3n de host y puerto ya est\u00e1 configurada" + }, + "error": { + "certificate_error": "El certificado no pudo ser validado", + "certificate_fetch_failed": "No se puede recuperar el certificado de esta combinaci\u00f3n de host y puerto", + "connection_timeout": "Tiempo de espera al conectarse a este host", + "host_port_exists": "Esta combinaci\u00f3n de host y puerto ya est\u00e1 configurada", + "resolve_failed": "Este host no puede resolverse", + "wrong_host": "El certificado no coincide con el nombre de host" + }, + "step": { + "user": { + "data": { + "host": "El nombre de host del certificado", + "name": "El nombre del certificado", + "port": "El puerto del certificado" + }, + "title": "Definir el certificado para probar" + } + }, "title": "Expiraci\u00f3n del certificado" } } \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/pl.json b/homeassistant/components/cert_expiry/.translations/pl.json index 671cbfcd1ff..2e50a9f8cbc 100644 --- a/homeassistant/components/cert_expiry/.translations/pl.json +++ b/homeassistant/components/cert_expiry/.translations/pl.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "host_port_exists": "Ta kombinacja hosta i portu jest ju\u017c skonfigurowana" + "host_port_exists": "Ten host z tym portem jest ju\u017c skonfigurowany." }, "error": { "certificate_error": "Nie mo\u017cna zweryfikowa\u0107 certyfikatu", "certificate_fetch_failed": "Nie mo\u017cna pobra\u0107 certyfikatu z tej kombinacji hosta i portu", "connection_timeout": "Przekroczono limit czasu po\u0142\u0105czenia z hostem.", - "host_port_exists": "Ta kombinacja hosta i portu jest ju\u017c skonfigurowana", + "host_port_exists": "Ten host z tym portem jest ju\u017c skonfigurowany.", "resolve_failed": "Tego hosta nie mo\u017cna rozwi\u0105za\u0107", "wrong_host": "Certyfikat nie pasuje do nazwy hosta" }, diff --git a/homeassistant/components/cert_expiry/.translations/sv.json b/homeassistant/components/cert_expiry/.translations/sv.json new file mode 100644 index 00000000000..bdccf51b2cd --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/sv.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "host_port_exists": "Denna v\u00e4rd- och portkombination \u00e4r redan konfigurerad" + }, + "error": { + "certificate_error": "Certifikatet kunde inte valideras", + "certificate_fetch_failed": "Kan inte h\u00e4mta certifikat fr\u00e5n denna v\u00e4rd- och portkombination", + "connection_timeout": "Timeout vid anslutning till den h\u00e4r v\u00e4rden", + "host_port_exists": "Denna v\u00e4rd- och portkombination \u00e4r redan konfigurerad", + "resolve_failed": "Denna v\u00e4rd kan inte resolveras", + "wrong_host": "Certifikatet matchar inte v\u00e4rdnamnet" + }, + "step": { + "user": { + "data": { + "host": "Certifikatets v\u00e4rdnamn", + "name": "Certifikatets namn", + "port": "Certifikatets port" + }, + "title": "Definiera certifikatet som ska testas" + } + }, + "title": "Certifikatets utg\u00e5ng" + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/config_flow.py b/homeassistant/components/cert_expiry/config_flow.py index 14532aea65f..f3bd2f07d63 100644 --- a/homeassistant/components/cert_expiry/config_flow.py +++ b/homeassistant/components/cert_expiry/config_flow.py @@ -69,7 +69,7 @@ class CertexpiryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return False async def async_step_user(self, user_input=None): - """Step when user intializes a integration.""" + """Step when user initializes a integration.""" self._errors = {} if user_input is not None: # set some defaults in case we need to return to the form diff --git a/homeassistant/components/climate/.translations/es-419.json b/homeassistant/components/climate/.translations/es-419.json new file mode 100644 index 00000000000..f3b861b9195 --- /dev/null +++ b/homeassistant/components/climate/.translations/es-419.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "Cambiar el modo HVAC en {entity_name}" + }, + "condition_type": { + "is_hvac_mode": "{entity_name} est\u00e1 configurado en un modo HVAC espec\u00edfico" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/.translations/hu.json b/homeassistant/components/climate/.translations/hu.json new file mode 100644 index 00000000000..38d6cc68822 --- /dev/null +++ b/homeassistant/components/climate/.translations/hu.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "F\u0171t\u00e9s- \u00e9s l\u00e9gtechnikai (HVAC) \u00fczemm\u00f3d m\u00f3dos\u00edt\u00e1sa a k\u00f6vetkez\u0151n: {entity_name}", + "set_preset_mode": "A(z) {entity_name} be\u00e1ll\u00edt\u00e1s\u00e1nak v\u00e1lt\u00e1sa" + }, + "condition_type": { + "is_hvac_mode": "{entity_name} speci\u00e1lis f\u0171t\u00e9s, szell\u0151z\u00e9s \u00e9s l\u00e9gkondicion\u00e1l\u00e1s (HVAC) \u00fczemm\u00f3dra van be\u00e1ll\u00edtva", + "is_preset_mode": "A(z) {entity_name} el\u0151re be\u00e1ll\u00edtott m\u00f3dja van kiv\u00e1lasztva" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} m\u00e9rt p\u00e1ratartalma megv\u00e1ltozott", + "current_temperature_changed": "{entity_name} m\u00e9rt h\u0151m\u00e9rs\u00e9klete megv\u00e1ltozott", + "hvac_mode_changed": "{entity_name} f\u0171t\u00e9s, szell\u0151z\u00e9s \u00e9s l\u00e9gkondicion\u00e1l\u00e1s (HVAC) \u00fczemm\u00f3d megv\u00e1ltozott" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/.translations/sv.json b/homeassistant/components/climate/.translations/sv.json new file mode 100644 index 00000000000..51fe0540549 --- /dev/null +++ b/homeassistant/components/climate/.translations/sv.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "\u00c4ndra HVAC-l\u00e4ge p\u00e5 {entity_name}", + "set_preset_mode": "\u00c4ndra f\u00f6rinst\u00e4llning p\u00e5 {entity_name}" + }, + "condition_type": { + "is_hvac_mode": "{entity_name} \u00e4r inst\u00e4lld p\u00e5 ett specifikt HVAC-l\u00e4ge", + "is_preset_mode": "{entity_name} \u00e4r inst\u00e4lld p\u00e5 ett specifikt f\u00f6rinst\u00e4llt l\u00e4ge" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} uppm\u00e4tt fuktighet har \u00e4ndrats", + "current_temperature_changed": "{entity_name} uppm\u00e4tt temperatur har \u00e4ndrats", + "hvac_mode_changed": "{entity_name} HVAC-l\u00e4ge har \u00e4ndrats" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/services.yaml b/homeassistant/components/climate/services.yaml index 34e89d57346..815df57f342 100644 --- a/homeassistant/components/climate/services.yaml +++ b/homeassistant/components/climate/services.yaml @@ -30,7 +30,7 @@ set_temperature: description: New target temperature for HVAC. example: 25 target_temp_high: - description: New target high tempereature for HVAC. + description: New target high temperature for HVAC. example: 26 target_temp_low: description: New target low temperature for HVAC. diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 22df26cce4e..c69a3ed5739 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -8,6 +8,7 @@ from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES from homeassistant.components import websocket_api from homeassistant.components.http import HomeAssistantView from homeassistant.exceptions import Unauthorized +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.data_entry_flow import ( FlowManagerIndexView, FlowManagerResourceView, @@ -45,7 +46,9 @@ def _prepare_json(result): if schema is None: data["data_schema"] = [] else: - data["data_schema"] = voluptuous_serialize.convert(schema) + data["data_schema"] = voluptuous_serialize.convert( + schema, custom_serializer=cv.custom_serializer + ) return data diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 458a9dd3ecb..a7993017116 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -68,6 +68,7 @@ async def websocket_get_entity(hass, connection, msg): vol.Required("entity_id"): cv.entity_id, # If passed in, we update value. Passing None will remove old value. vol.Optional("name"): vol.Any(str, None), + vol.Optional("icon"): vol.Any(str, None), vol.Optional("new_entity_id"): str, # We only allow setting disabled_by user via API. vol.Optional("disabled_by"): vol.Any("user", None), @@ -88,11 +89,9 @@ async def websocket_update_entity(hass, connection, msg): changes = {} - if "name" in msg: - changes["name"] = msg["name"] - - if "disabled_by" in msg: - changes["disabled_by"] = msg["disabled_by"] + for key in ("name", "icon", "disabled_by"): + if key in msg: + changes[key] = msg[key] if "new_entity_id" in msg and msg["new_entity_id"] != msg["entity_id"]: changes["new_entity_id"] = msg["new_entity_id"] @@ -151,5 +150,8 @@ def _entry_dict(entry): "disabled_by": entry.disabled_by, "entity_id": entry.entity_id, "name": entry.name, + "icon": entry.icon, "platform": entry.platform, + "original_name": entry.original_name, + "original_icon": entry.original_icon, } diff --git a/homeassistant/components/config/manifest.json b/homeassistant/components/config/manifest.json index 809db4ffecc..5d5db4b0741 100644 --- a/homeassistant/components/config/manifest.json +++ b/homeassistant/components/config/manifest.json @@ -1,6 +1,6 @@ { "domain": "config", - "name": "Config", + "name": "Configuration", "documentation": "https://www.home-assistant.io/integrations/config", "requirements": [], "dependencies": ["http"], diff --git a/homeassistant/components/configurator/__init__.py b/homeassistant/components/configurator/__init__.py index 78333d96355..4d79b13355c 100644 --- a/homeassistant/components/configurator/__init__.py +++ b/homeassistant/components/configurator/__init__.py @@ -209,7 +209,7 @@ class Configurator: entity_id = self._requests.pop(request_id)[0] # If we remove the state right away, it will not be included with - # the result fo the service call (current design limitation). + # the result of the service call (current design limitation). # Instead, we will set it to configured to give as feedback but delete # it shortly after so that it is deleted when the client updates. self.hass.states.async_set(entity_id, STATE_CONFIGURED) diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index 158a365981b..91031c141dd 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -131,9 +131,22 @@ class ConversationProcessView(http.HomeAssistantView): """Send a request for processing.""" hass = request.app["hass"] - intent_result = await _async_converse( - hass, data["text"], data.get("conversation_id"), self.context(request) - ) + try: + intent_result = await _async_converse( + hass, data["text"], data.get("conversation_id"), self.context(request) + ) + except intent.IntentError as err: + _LOGGER.error("Error handling intent: %s", err) + return self.json( + { + "success": False, + "error": { + "code": str(err.__class__.__name__).lower(), + "message": str(err), + }, + }, + status_code=500, + ) return self.json(intent_result) diff --git a/homeassistant/components/coolmaster/.translations/es-419.json b/homeassistant/components/coolmaster/.translations/es-419.json new file mode 100644 index 00000000000..2bcdecb2aec --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/es-419.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "off": "Puede ser apagado" + }, + "title": "Configure los detalles de su conexi\u00f3n CoolMasterNet." + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/.translations/hu.json b/homeassistant/components/coolmaster/.translations/hu.json new file mode 100644 index 00000000000..cbf055e2fba --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/hu.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hoszt" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/.translations/sv.json b/homeassistant/components/coolmaster/.translations/sv.json new file mode 100644 index 00000000000..89e2ab32863 --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/sv.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "connection_error": "Det gick inte att ansluta till CoolMasterNet-instansen. Kontrollera din v\u00e4rd.", + "no_units": "Det gick inte att hitta n\u00e5gra HVAC-enheter i CoolMasterNet-v\u00e4rden." + }, + "step": { + "user": { + "data": { + "cool": "St\u00f6d svalt l\u00e4ge", + "dry": "St\u00f6d torrl\u00e4ge", + "fan_only": "St\u00f6d endast fl\u00e4ktl\u00e4ge", + "heat": "St\u00f6d v\u00e4rmel\u00e4ge", + "heat_cool": "St\u00f6d automatiskt v\u00e4rme/kyl-l\u00e4ge", + "host": "V\u00e4rd", + "off": "Kan st\u00e4ngas av" + }, + "title": "St\u00e4ll in dina CoolMasterNet-anslutningsdetaljer." + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/config_flow.py b/homeassistant/components/coolmaster/config_flow.py index e9cef562647..c267b283118 100644 --- a/homeassistant/components/coolmaster/config_flow.py +++ b/homeassistant/components/coolmaster/config_flow.py @@ -26,6 +26,7 @@ class CoolmasterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + @core.callback def _async_get_entry(self, data): supported_modes = [ key for (key, value) in data.items() if key in AVAILABLE_MODES and value diff --git a/homeassistant/components/cover/.translations/sv.json b/homeassistant/components/cover/.translations/sv.json new file mode 100644 index 00000000000..906768d3eb3 --- /dev/null +++ b/homeassistant/components/cover/.translations/sv.json @@ -0,0 +1,20 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} \u00e4r st\u00e4ngd", + "is_closing": "{entity_name} st\u00e4ngs", + "is_open": "{entity_name} \u00e4r \u00f6ppen", + "is_opening": "{entity_name} \u00f6ppnas", + "is_position": "Aktuell position f\u00f6r {entity_name} \u00e4r", + "is_tilt_position": "Aktuell {entity_name} lutningsposition \u00e4r" + }, + "trigger_type": { + "closed": "{entity_name} st\u00e4ngd", + "closing": "{entity_name} st\u00e4nger", + "opened": "{entity_name} \u00f6ppnades", + "opening": "{entity_name} \u00f6ppnas", + "position": "{entity_name} position \u00e4ndras", + "tilt_position": "{entity_name} lutningsposition \u00e4ndras" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/.translations/pl.json b/homeassistant/components/daikin/.translations/pl.json index 5d5448a93db..3caea70c4de 100644 --- a/homeassistant/components/daikin/.translations/pl.json +++ b/homeassistant/components/daikin/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", "device_fail": "Nieoczekiwany b\u0142\u0105d tworzenia urz\u0105dzenia.", "device_timeout": "Przekroczono limit czasu \u0142\u0105czenia z urz\u0105dzeniem." }, diff --git a/homeassistant/components/danfoss_air/sensor.py b/homeassistant/components/danfoss_air/sensor.py index ea0002d0ac3..247eb955154 100644 --- a/homeassistant/components/danfoss_air/sensor.py +++ b/homeassistant/components/danfoss_air/sensor.py @@ -48,10 +48,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ["Danfoss Air Remaining Filter", "%", ReadCommand.filterPercent, None], ["Danfoss Air Humidity", "%", ReadCommand.humidity, DEVICE_CLASS_HUMIDITY], ["Danfoss Air Fan Step", "%", ReadCommand.fan_step, None], - ["Dandoss Air Exhaust Fan Speed", "RPM", ReadCommand.exhaust_fan_speed, None], - ["Dandoss Air Supply Fan Speed", "RPM", ReadCommand.supply_fan_speed, None], + ["Danfoss Air Exhaust Fan Speed", "RPM", ReadCommand.exhaust_fan_speed, None], + ["Danfoss Air Supply Fan Speed", "RPM", ReadCommand.supply_fan_speed, None], [ - "Dandoss Air Dial Battery", + "Danfoss Air Dial Battery", "%", ReadCommand.battery_percent, DEVICE_CLASS_BATTERY, diff --git a/homeassistant/components/deconz/.translations/ca.json b/homeassistant/components/deconz/.translations/ca.json index 8a9ae15a7c1..e690d597dce 100644 --- a/homeassistant/components/deconz/.translations/ca.json +++ b/homeassistant/components/deconz/.translations/ca.json @@ -66,16 +66,16 @@ }, "trigger_type": { "remote_awakened": "Dispositiu despertat", - "remote_button_double_press": "Bot\u00f3 \"{subtype}\" clicat dues vegades consecutives", - "remote_button_long_press": "Bot\u00f3 \"{subtype}\" premut continuament", + "remote_button_double_press": "Bot\u00f3 \"{subtype}\" clicat dues vegades", + "remote_button_long_press": "Bot\u00f3 \"{subtype}\" premut cont\u00ednuament", "remote_button_long_release": "Bot\u00f3 \"{subtype}\" alliberat despr\u00e9s d'una estona premut", - "remote_button_quadruple_press": "Bot\u00f3 \"{subtype}\" clicat quatre vegades consecutives", - "remote_button_quintuple_press": "Bot\u00f3 \"{subtype}\" clicat cinc vegades consecutives", + "remote_button_quadruple_press": "Bot\u00f3 \"{subtype}\" clicat quatre vegades", + "remote_button_quintuple_press": "Bot\u00f3 \"{subtype}\" clicat cinc vegades", "remote_button_rotated": "Bot\u00f3 \"{subtype}\" girat", "remote_button_rotation_stopped": "La rotaci\u00f3 del bot\u00f3 \"{subtype}\" s'ha aturat", "remote_button_short_press": "Bot\u00f3 \"{subtype}\" premut", "remote_button_short_release": "Bot\u00f3 \"{subtype}\" alliberat", - "remote_button_triple_press": "Bot\u00f3 \"{subtype}\" clicat tres vegades consecutives", + "remote_button_triple_press": "Bot\u00f3 \"{subtype}\" clicat tres vegades", "remote_double_tap": "Dispositiu \"{subtype}\" tocat dues vegades", "remote_double_tap_any_side": "Dispositiu tocat dues vegades a alguna cara", "remote_falling": "Dispositiu en caiguda lliure", @@ -108,7 +108,8 @@ "allow_clip_sensor": "Permet sensors deCONZ CLIP", "allow_deconz_groups": "Permet grups de llums deCONZ" }, - "description": "Configura la visibilitat dels tipus dels dispositius deCONZ" + "description": "Configura la visibilitat dels tipus dels dispositius deCONZ", + "title": "Opcions de deCONZ" } } } diff --git a/homeassistant/components/deconz/.translations/da.json b/homeassistant/components/deconz/.translations/da.json index ed1f0b06e64..d1af7e1f4ba 100644 --- a/homeassistant/components/deconz/.translations/da.json +++ b/homeassistant/components/deconz/.translations/da.json @@ -108,7 +108,8 @@ "allow_clip_sensor": "Tillad deCONZ CLIP-sensorer", "allow_deconz_groups": "Tillad deCONZ-lysgrupper" }, - "description": "Konfigurer synligheden af deCONZ-enhedstyper" + "description": "Konfigurer synligheden af deCONZ-enhedstyper", + "title": "deCONZ-indstillinger" } } } diff --git a/homeassistant/components/deconz/.translations/de.json b/homeassistant/components/deconz/.translations/de.json index 479e645173b..c3ad3cd24c8 100644 --- a/homeassistant/components/deconz/.translations/de.json +++ b/homeassistant/components/deconz/.translations/de.json @@ -108,7 +108,8 @@ "allow_clip_sensor": "deCONZ CLIP-Sensoren zulassen", "allow_deconz_groups": "deCONZ-Lichtgruppen zulassen" }, - "description": "Sichtbarkeit der deCONZ-Ger\u00e4tetypen konfigurieren" + "description": "Sichtbarkeit der deCONZ-Ger\u00e4tetypen konfigurieren", + "title": "deCONZ-Optionen" } } } diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index b3d9e00bfe6..756636ad90a 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -108,7 +108,8 @@ "allow_clip_sensor": "Allow deCONZ CLIP sensors", "allow_deconz_groups": "Allow deCONZ light groups" }, - "description": "Configure visibility of deCONZ device types" + "description": "Configure visibility of deCONZ device types", + "title": "deCONZ options" } } } diff --git a/homeassistant/components/deconz/.translations/es.json b/homeassistant/components/deconz/.translations/es.json index 6f5513d9729..cfff05b1e02 100644 --- a/homeassistant/components/deconz/.translations/es.json +++ b/homeassistant/components/deconz/.translations/es.json @@ -108,7 +108,8 @@ "allow_clip_sensor": "Permitir sensores deCONZ CLIP", "allow_deconz_groups": "Permitir grupos de luz deCONZ" }, - "description": "Configurar la visibilidad de los tipos de dispositivos deCONZ" + "description": "Configurar la visibilidad de los tipos de dispositivos deCONZ", + "title": "Opciones deCONZ" } } } diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json index c900bdab6ab..214c887cc34 100644 --- a/homeassistant/components/deconz/.translations/fr.json +++ b/homeassistant/components/deconz/.translations/fr.json @@ -77,6 +77,7 @@ "remote_button_short_release": "Bouton \"{subtype}\" rel\u00e2ch\u00e9", "remote_button_triple_press": "Bouton \"{subtype}\" triple cliqu\u00e9", "remote_double_tap": "Appareil \"{subtype}\" tapot\u00e9 deux fois", + "remote_double_tap_any_side": "Appareil double tap\u00e9 de n\u2019importe quel c\u00f4t\u00e9", "remote_falling": "Appareil en chute libre", "remote_flip_180_degrees": "Dispositif retourn\u00e9 \u00e0 180 degr\u00e9s", "remote_flip_90_degrees": "Dispositif retourn\u00e9 \u00e0 90 degr\u00e9s", diff --git a/homeassistant/components/deconz/.translations/hu.json b/homeassistant/components/deconz/.translations/hu.json index f162130680c..c5bf9718127 100644 --- a/homeassistant/components/deconz/.translations/hu.json +++ b/homeassistant/components/deconz/.translations/hu.json @@ -2,13 +2,23 @@ "config": { "abort": { "already_configured": "A bridge m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "Az \u00e1tj\u00e1r\u00f3 konfigur\u00e1ci\u00f3s folyamata m\u00e1r folyamatban van.", "no_bridges": "Nem tal\u00e1ltam deCONZ bridget", - "one_instance_only": "Ez a komponens csak egy deCONZ egys\u00e9get t\u00e1mogat" + "not_deconz_bridge": "Nem egy deCONZ \u00e1tj\u00e1r\u00f3", + "one_instance_only": "Ez a komponens csak egy deCONZ egys\u00e9get t\u00e1mogat", + "updated_instance": "A deCONZ-p\u00e9ld\u00e1ny \u00faj \u00e1llom\u00e1sc\u00edmmel friss\u00edtve" }, "error": { "no_key": "API kulcs lek\u00e9r\u00e9se nem siker\u00fclt" }, + "flow_title": "deCONZ Zigbee \u00e1tj\u00e1r\u00f3 ({host})", "step": { + "hassio_confirm": { + "data": { + "allow_clip_sensor": "Virtu\u00e1lis \u00e9rz\u00e9kel\u0151k import\u00e1l\u00e1s\u00e1nak enged\u00e9lyez\u00e9se" + }, + "title": "deCONZ Zigbee \u00e1tj\u00e1r\u00f3 a Hass.io kieg\u00e9sz\u00edt\u0151 seg\u00edts\u00e9g\u00e9vel" + }, "init": { "data": { "host": "Hoszt", @@ -32,12 +42,72 @@ }, "device_automation": { "trigger_subtype": { - "close": "Bez\u00e1r\u00e1s" + "both_buttons": "Mindk\u00e9t gomb", + "button_1": "Els\u0151 gomb", + "button_2": "M\u00e1sodik gomb", + "button_3": "Harmadik gomb", + "button_4": "Negyedik gomb", + "close": "Bez\u00e1r\u00e1s", + "dim_down": "S\u00f6t\u00e9t\u00edt", + "dim_up": "Vil\u00e1gos\u00edt", + "left": "Balra", + "open": "Nyitva", + "right": "Jobbra", + "side_1": "1. oldal", + "side_2": "2. oldal", + "side_3": "3. oldal", + "side_4": "4. oldal", + "side_5": "5. oldal", + "side_6": "6. oldal", + "turn_off": "Kikapcsolva", + "turn_on": "Bekapcsolva" }, "trigger_type": { + "remote_awakened": "A k\u00e9sz\u00fcl\u00e9k fel\u00e9bredt", + "remote_button_double_press": "\" {subtype} \" gombra k\u00e9tszer kattintottak", + "remote_button_long_press": "A \" {subtype} \" gomb folyamatosan lenyomva", + "remote_button_long_release": "A \" {subtype} \" gomb hossz\u00fa megnyom\u00e1s ut\u00e1n elengedve", + "remote_button_quadruple_press": "\" {subtype} \" gombra n\u00e9gyszer kattintottak", + "remote_button_quintuple_press": "\" {subtype} \" gombra \u00f6tsz\u00f6r kattintottak", + "remote_button_rotated": "A gomb elforgatva: \" {subtype} \"", + "remote_button_rotation_stopped": "A (z) \" {subtype} \" gomb forg\u00e1sa le\u00e1llt", + "remote_button_short_press": "\" {subtype} \" gomb lenyomva", + "remote_button_short_release": "\"{alt\u00edpus}\" gomb elengedve", + "remote_button_triple_press": "\" {subtype} \" gombra h\u00e1romszor kattintottak", + "remote_double_tap": "Az \" {subtype} \" eszk\u00f6z dupla kattint\u00e1sa", "remote_double_tap_any_side": "A k\u00e9sz\u00fcl\u00e9k b\u00e1rmelyik oldal\u00e1n dupl\u00e1n koppint.", + "remote_falling": "K\u00e9sz\u00fcl\u00e9k szabades\u00e9sben", "remote_flip_180_degrees": "180 fokkal megd\u00f6nt\u00f6tt eszk\u00f6z", - "remote_flip_90_degrees": "90 fokkal megd\u00f6nt\u00f6tt eszk\u00f6z" + "remote_flip_90_degrees": "90 fokkal megd\u00f6nt\u00f6tt eszk\u00f6z", + "remote_gyro_activated": "A k\u00e9sz\u00fcl\u00e9k meg lett r\u00e1zva", + "remote_moved": "Az eszk\u00f6z a \" {subtype} \"-lal felfel\u00e9 mozgatva", + "remote_moved_any_side": "A k\u00e9sz\u00fcl\u00e9k valamelyik oldal\u00e1val felfel\u00e9 mozogott", + "remote_rotate_from_side_1": "Az eszk\u00f6z a \"1. oldalr\u00f3l\" a \" {subtype} \" -ra fordult", + "remote_rotate_from_side_2": "Az eszk\u00f6z a \"2. oldalr\u00f3l\" a \" {subtype} \" -ra fordult", + "remote_rotate_from_side_3": "Az eszk\u00f6z a \"3. oldalr\u00f3l\" a \" {subtype} \" -ra fordult", + "remote_rotate_from_side_4": "Az eszk\u00f6z a \"4. oldalr\u00f3l\" a \" {subtype} \" -ra fordult", + "remote_rotate_from_side_5": "Az eszk\u00f6z a \"5. oldalr\u00f3l\" a \" {subtype} \" -ra fordult", + "remote_rotate_from_side_6": "Az eszk\u00f6z a \"6. oldalr\u00f3l\" a \" {subtype} \" -ra fordult", + "remote_turned_clockwise": "A k\u00e9sz\u00fcl\u00e9k az \u00f3ramutat\u00f3 j\u00e1r\u00e1s\u00e1val megegyez\u0151en fordult", + "remote_turned_counter_clockwise": "A k\u00e9sz\u00fcl\u00e9k az \u00f3ramutat\u00f3 j\u00e1r\u00e1s\u00e1val ellent\u00e9tes ir\u00e1nyban fordult" + } + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "Enged\u00e9lyezze a deCONZ CLIP \u00e9rz\u00e9kel\u0151ket", + "allow_deconz_groups": "DeCONZ f\u00e9nycsoportok enged\u00e9lyez\u00e9se" + }, + "description": "A deCONZ eszk\u00f6zt\u00edpusok l\u00e1that\u00f3s\u00e1g\u00e1nak konfigur\u00e1l\u00e1sa" + }, + "deconz_devices": { + "data": { + "allow_clip_sensor": "Enged\u00e9lyezze a deCONZ CLIP \u00e9rz\u00e9kel\u0151ket", + "allow_deconz_groups": "DeCONZ f\u00e9nycsoportok enged\u00e9lyez\u00e9se" + }, + "description": "A deCONZ eszk\u00f6zt\u00edpusok l\u00e1that\u00f3s\u00e1g\u00e1nak konfigur\u00e1l\u00e1sa" + } } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json index 980409d6987..f6223cec6c1 100644 --- a/homeassistant/components/deconz/.translations/it.json +++ b/homeassistant/components/deconz/.translations/it.json @@ -108,7 +108,8 @@ "allow_clip_sensor": "Consentire sensori CLIP deCONZ", "allow_deconz_groups": "Consentire gruppi luce deCONZ" }, - "description": "Configurare la visibilit\u00e0 dei tipi di dispositivi deCONZ" + "description": "Configurare la visibilit\u00e0 dei tipi di dispositivi deCONZ", + "title": "Opzioni deCONZ" } } } diff --git a/homeassistant/components/deconz/.translations/ko.json b/homeassistant/components/deconz/.translations/ko.json index d526d706a8b..1b72545bc09 100644 --- a/homeassistant/components/deconz/.translations/ko.json +++ b/homeassistant/components/deconz/.translations/ko.json @@ -108,7 +108,8 @@ "allow_clip_sensor": "deCONZ CLIP \uc13c\uc11c \ud5c8\uc6a9", "allow_deconz_groups": "deCONZ \ub77c\uc774\ud2b8 \uadf8\ub8f9 \ud5c8\uc6a9" }, - "description": "deCONZ \uae30\uae30 \uc720\ud615\uc758 \ud45c\uc2dc \uc5ec\ubd80 \uad6c\uc131" + "description": "deCONZ \uae30\uae30 \uc720\ud615\uc758 \ud45c\uc2dc \uc5ec\ubd80 \uad6c\uc131", + "title": "deCONZ \uc635\uc158" } } } diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json index 4b04cfa03ce..42fd840524f 100644 --- a/homeassistant/components/deconz/.translations/lb.json +++ b/homeassistant/components/deconz/.translations/lb.json @@ -108,7 +108,8 @@ "allow_clip_sensor": "deCONZ Clip Sensoren erlaben", "allow_deconz_groups": "deCONZ Luucht Gruppen erlaben" }, - "description": "Visibilit\u00e9it vun deCONZ Apparater konfigur\u00e9ieren" + "description": "Visibilit\u00e9it vun deCONZ Apparater konfigur\u00e9ieren", + "title": "deCONZ Optiounen" } } } diff --git a/homeassistant/components/deconz/.translations/nl.json b/homeassistant/components/deconz/.translations/nl.json index c0ee391b0c7..585c09c5339 100644 --- a/homeassistant/components/deconz/.translations/nl.json +++ b/homeassistant/components/deconz/.translations/nl.json @@ -77,15 +77,21 @@ "remote_button_short_release": "\"{subtype}\" knop losgelaten", "remote_button_triple_press": "\" {subtype} \" knop driemaal geklikt", "remote_double_tap": "Apparaat \"{subtype}\" dubbel getikt", + "remote_double_tap_any_side": "Apparaat dubbel getikt aan elke kant", "remote_falling": "Apparaat in vrije val", + "remote_flip_180_degrees": "Apparaat 180 graden omgedraaid", + "remote_flip_90_degrees": "Apparaat 90 graden omgedraaid", "remote_gyro_activated": "Apparaat geschud", "remote_moved": "Apparaat verplaatst met \"{subtype}\" omhoog", + "remote_moved_any_side": "Apparaat gedraaid met elke kant naar boven", "remote_rotate_from_side_1": "Apparaat gedraaid van \"zijde 1\" naar \"{subtype}\"\".", "remote_rotate_from_side_2": "Apparaat gedraaid van \"zijde 2\" naar \"{subtype}\"\".", "remote_rotate_from_side_3": "Apparaat gedraaid van \"zijde 3\" naar \" {subtype} \"", "remote_rotate_from_side_4": "Apparaat gedraaid van \"zijde 4\" naar \" {subtype} \"", "remote_rotate_from_side_5": "Apparaat gedraaid van \"zijde 5\" naar \" {subtype} \"", - "remote_rotate_from_side_6": "Apparaat gedraaid van \"zijde 6\" naar \" {subtype} \"" + "remote_rotate_from_side_6": "Apparaat gedraaid van \"zijde 6\" naar \" {subtype} \"", + "remote_turned_clockwise": "Apparaat met de klok mee gedraaid", + "remote_turned_counter_clockwise": "Apparaat tegen de klok in gedraaid" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json index d6133542c64..3387c993ae0 100644 --- a/homeassistant/components/deconz/.translations/no.json +++ b/homeassistant/components/deconz/.translations/no.json @@ -108,7 +108,8 @@ "allow_clip_sensor": "Tillat deCONZ CLIP-sensorer", "allow_deconz_groups": "Tillat deCONZ lys grupper" }, - "description": "Konfigurere synlighet av deCONZ enhetstyper" + "description": "Konfigurere synlighet av deCONZ enhetstyper", + "title": "deCONZ alternativer" } } } diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json index df85e7b8d1d..d12e633bf23 100644 --- a/homeassistant/components/deconz/.translations/pl.json +++ b/homeassistant/components/deconz/.translations/pl.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "Mostek jest ju\u017c skonfigurowany", - "already_in_progress": "Konfigurowanie mostka jest ju\u017c w toku.", + "already_configured": "Mostek jest ju\u017c skonfigurowany.", + "already_in_progress": "Konfiguracja mostka jest ju\u017c w toku.", "no_bridges": "Nie odkryto mostk\u00f3w deCONZ", "not_deconz_bridge": "To nie jest mostek deCONZ", "one_instance_only": "Komponent obs\u0142uguje tylko jedn\u0105 instancj\u0119 deCONZ", @@ -108,7 +108,8 @@ "allow_clip_sensor": "Zezwalaj na czujniki deCONZ CLIP", "allow_deconz_groups": "Zezwalaj na grupy \u015bwiate\u0142 deCONZ" }, - "description": "Skonfiguruj widoczno\u015b\u0107 typ\u00f3w urz\u0105dze\u0144 deCONZ" + "description": "Skonfiguruj widoczno\u015b\u0107 typ\u00f3w urz\u0105dze\u0144 deCONZ", + "title": "Opcje deCONZ" } } } diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index 3c61e447bca..054c85f595a 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -15,7 +15,7 @@ "step": { "hassio_confirm": { "data": { - "allow_clip_sensor": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0438\u043c\u043f\u043e\u0440\u0442 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u044b\u0445 \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u0432", + "allow_clip_sensor": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0438\u043c\u043f\u043e\u0440\u0442 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u044b\u0445 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432", "allow_deconz_groups": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0438\u043c\u043f\u043e\u0440\u0442 \u0433\u0440\u0443\u043f\u043f deCONZ" }, "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a deCONZ (\u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io \"{addon}\")?", @@ -34,7 +34,7 @@ }, "options": { "data": { - "allow_clip_sensor": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0438\u043c\u043f\u043e\u0440\u0442 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u044b\u0445 \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u0432", + "allow_clip_sensor": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0438\u043c\u043f\u043e\u0440\u0442 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u044b\u0445 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432", "allow_deconz_groups": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0438\u043c\u043f\u043e\u0440\u0442 \u0433\u0440\u0443\u043f\u043f deCONZ" }, "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 deCONZ" @@ -98,17 +98,18 @@ "step": { "async_step_deconz_devices": { "data": { - "allow_clip_sensor": "\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a\u0438 deCONZ CLIP", + "allow_clip_sensor": "\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0441\u0435\u043d\u0441\u043e\u0440\u044b deCONZ CLIP", "allow_deconz_groups": "\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0433\u0440\u0443\u043f\u043f\u044b \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u044f deCONZ" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u0438\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0442\u0438\u043f\u043e\u0432 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 deCONZ" }, "deconz_devices": { "data": { - "allow_clip_sensor": "\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a\u0438 deCONZ CLIP", + "allow_clip_sensor": "\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0441\u0435\u043d\u0441\u043e\u0440\u044b deCONZ CLIP", "allow_deconz_groups": "\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0433\u0440\u0443\u043f\u043f\u044b \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u044f deCONZ" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u0438\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0442\u0438\u043f\u043e\u0432 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 deCONZ" + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u0438\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0442\u0438\u043f\u043e\u0432 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 deCONZ", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 deCONZ" } } } diff --git a/homeassistant/components/deconz/.translations/sv.json b/homeassistant/components/deconz/.translations/sv.json index 02869dcf76e..3d74d6cb944 100644 --- a/homeassistant/components/deconz/.translations/sv.json +++ b/homeassistant/components/deconz/.translations/sv.json @@ -11,6 +11,7 @@ "error": { "no_key": "Det gick inte att ta emot en API-nyckel" }, + "flow_title": "deCONZ Zigbee gateway ({host})", "step": { "hassio_confirm": { "data": { @@ -40,5 +41,75 @@ } }, "title": "deCONZ Zigbee Gateway" + }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "B\u00e5da knapparna", + "button_1": "F\u00f6rsta knappen", + "button_2": "Andra knappen", + "button_3": "Tredje knappen", + "button_4": "Fj\u00e4rde knappen", + "close": "St\u00e4ng", + "dim_down": "Dimma ned", + "dim_up": "Dimma upp", + "left": "V\u00e4nster", + "open": "\u00d6ppen", + "right": "H\u00f6ger", + "side_1": "Sida 1", + "side_2": "Sida 2", + "side_3": "Sida 3", + "side_4": "Sida 4", + "side_5": "Sida 5", + "side_6": "Sida 6", + "turn_off": "St\u00e4ng av", + "turn_on": "Starta" + }, + "trigger_type": { + "remote_awakened": "Enheten v\u00e4cktes", + "remote_button_double_press": "\"{subtype}\"-knappen dubbelklickades", + "remote_button_long_press": "\"{subtype}\"-knappen kontinuerligt nedtryckt", + "remote_button_long_release": "\"{subtype}\"-knappen sl\u00e4pptes efter ett l\u00e5ngttryck", + "remote_button_quadruple_press": "\"{subtype}\"-knappen klickades \nfyrfaldigt", + "remote_button_quintuple_press": "\"{subtype}\"-knappen klickades \nfemfaldigt", + "remote_button_rotated": "Knappen roterade \"{subtype}\"", + "remote_button_rotation_stopped": "Knapprotationen \"{subtype}\" stoppades", + "remote_button_short_press": "\"{subtype}\"-knappen trycktes in", + "remote_button_short_release": "\"{subtype}\"-knappen sl\u00e4ppt", + "remote_button_triple_press": "\"{subtype}\"-knappen trippelklickad", + "remote_double_tap": "Enheten \"{subtype}\" dubbeltryckt", + "remote_double_tap_any_side": "Enheten dubbeltryckt p\u00e5 valfri sida", + "remote_falling": "Enhet i fritt fall", + "remote_flip_180_degrees": "Enheten v\u00e4nd 180 grader", + "remote_flip_90_degrees": "Enheten v\u00e4nd 90 grader", + "remote_gyro_activated": "Enhet skakad", + "remote_moved": "Enheten flyttades med \"{subtype}\" upp", + "remote_moved_any_side": "Enheten flyttades med valfri sida upp\u00e5t", + "remote_rotate_from_side_1": "Enheten roterades fr\u00e5n \"sida 1\" till \"{subtype}\"", + "remote_rotate_from_side_2": "Enheten roterades fr\u00e5n \"sida 2\" till \"{subtype}\"", + "remote_rotate_from_side_3": "Enheten roterades fr\u00e5n \"sida 3\" till \"{subtype}\"", + "remote_rotate_from_side_4": "Enheten roterades fr\u00e5n \"sida 4\" till \"{subtype}\"", + "remote_rotate_from_side_5": "Enheten roterades fr\u00e5n \"sida 5\" till \"{subtype}\"", + "remote_rotate_from_side_6": "Enheten roterades fr\u00e5n \"sida 6\" till \"{subtype}\"", + "remote_turned_clockwise": "Enheten vriden medurs", + "remote_turned_counter_clockwise": "Enheten v\u00e4nde moturs" + } + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "Till\u00e5t deCONZ CLIP-sensorer", + "allow_deconz_groups": "Till\u00e5t deCONZ ljusgrupper" + }, + "description": "Konfigurera synlighet f\u00f6r deCONZ-enhetstyper" + }, + "deconz_devices": { + "data": { + "allow_clip_sensor": "Till\u00e5t deCONZ CLIP-sensorer", + "allow_deconz_groups": "Till\u00e5t deCONZ ljusgrupper" + }, + "description": "Konfigurera synlighet f\u00f6r deCONZ-enhetstyper" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/zh-Hans.json b/homeassistant/components/deconz/.translations/zh-Hans.json index 37b82cff29c..ce51a54ac77 100644 --- a/homeassistant/components/deconz/.translations/zh-Hans.json +++ b/homeassistant/components/deconz/.translations/zh-Hans.json @@ -52,5 +52,12 @@ "remote_rotate_from_side_5": "\u8bbe\u5907\u4ece\u201c\u7b2c 5 \u9762\u201d\u7ffb\u8f6c\u5230\u201c{subtype}\u201d", "remote_rotate_from_side_6": "\u8bbe\u5907\u4ece\u201c\u7b2c 6 \u9762\u201d\u7ffb\u8f6c\u5230\u201c{subtype}\u201d" } + }, + "options": { + "step": { + "deconz_devices": { + "title": "deCONZ \u9009\u9879" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json index 96ab68a8dbb..073ebd784c6 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -108,7 +108,8 @@ "allow_clip_sensor": "\u5141\u8a31 deCONZ CLIP \u611f\u61c9\u5668", "allow_deconz_groups": "\u5141\u8a31 deCONZ \u71c8\u5149\u7fa4\u7d44" }, - "description": "\u8a2d\u5b9a deCONZ \u53ef\u8996\u8a2d\u5099\u985e\u578b" + "description": "\u8a2d\u5b9a deCONZ \u53ef\u8996\u8a2d\u5099\u985e\u578b", + "title": "deCONZ \u9078\u9805" } } } diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 667eb6db075..2514a49f23c 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -8,7 +8,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR from .deconz_device import DeconzDevice -from .gateway import DeconzEntityHandler, get_gateway_from_config_entry +from .gateway import get_gateway_from_config_entry ATTR_ORIENTATION = "orientation" ATTR_TILTANGLE = "tiltangle" @@ -23,8 +23,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the deCONZ binary sensor.""" gateway = get_gateway_from_config_entry(hass, config_entry) - entity_handler = DeconzEntityHandler(gateway) - @callback def async_add_sensor(sensors, new=True): """Add binary sensor from deCONZ.""" @@ -32,10 +30,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for sensor in sensors: - if new and sensor.BINARY: - new_sensor = DeconzBinarySensor(sensor, gateway) - entity_handler.add_entity(new_sensor) - entities.append(new_sensor) + if ( + new + and sensor.BINARY + and ( + gateway.option_allow_clip_sensor + or not sensor.type.startswith("CLIP") + ) + and sensor.deconz_id not in gateway.deconz_ids.values() + ): + entities.append(DeconzBinarySensor(sensor, gateway)) async_add_entities(entities, True) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index ba1f1ce846a..34cc0e0b832 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -37,7 +37,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for sensor in sensors: - if new and sensor.type in Thermostat.ZHATYPE: + if ( + new + and sensor.type in Thermostat.ZHATYPE + and ( + gateway.option_allow_clip_sensor + or not sensor.type.startswith("CLIP") + ) + and sensor.deconz_id not in gateway.deconz_ids.values() + ): entities.append(DeconzThermostat(sensor, gateway)) async_add_entities(entities, True) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 614d2378c88..3a38a67f0c6 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -1,5 +1,6 @@ """Config flow to configure deCONZ component.""" import asyncio +from pprint import pformat from urllib.parse import urlparse import async_timeout @@ -26,6 +27,7 @@ from .const import ( DEFAULT_ALLOW_DECONZ_GROUPS, DEFAULT_PORT, DOMAIN, + LOGGER, ) DECONZ_MANUFACTURERURL = "http://www.dresden-elektronik.de" @@ -93,6 +95,8 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): except (asyncio.TimeoutError, ResponseError): self.bridges = [] + LOGGER.debug("Discovered deCONZ gateways %s", pformat(self.bridges)) + if len(self.bridges) == 1: return await self.async_step_user(self.bridges[0]) @@ -121,6 +125,10 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Attempt to link with the deCONZ bridge.""" errors = {} + LOGGER.debug( + "Preparing linking with deCONZ gateway %s", pformat(self.deconz_config) + ) + if user_input is not None: session = aiohttp_client.async_get_clientsession(self.hass) @@ -170,6 +178,8 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ): return self.async_abort(reason="not_deconz_bridge") + LOGGER.debug("deCONZ SSDP discovery %s", pformat(discovery_info)) + self.bridge_id = normalize_bridge_id(discovery_info[ssdp.ATTR_UPNP_SERIAL]) parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]) @@ -196,6 +206,8 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): This flow is triggered by the discovery component. """ + LOGGER.debug("deCONZ HASSIO discovery %s", pformat(user_input)) + self.bridge_id = normalize_bridge_id(user_input[CONF_SERIAL]) await self.async_set_unique_id(self.bridge_id) diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index 293e0d9719c..11dbd07e86a 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -1,7 +1,7 @@ """Constants for the deCONZ component.""" import logging -_LOGGER = logging.getLogger(__package__) +LOGGER = logging.getLogger(__package__) DOMAIN = "deconz" diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index 06756bb49f6..b3dedf6cf00 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -63,17 +63,6 @@ class DeconzDevice(DeconzBase, Entity): Daylight is a virtual sensor from deCONZ that should never be enabled by default. """ - if not self.gateway.option_allow_clip_sensor and self._device.type.startswith( - "CLIP" - ): - return False - - if ( - not self.gateway.option_allow_deconz_groups - and self._device.type == "LightGroup" - ): - return False - if self._device.type == "Daylight": return False @@ -81,13 +70,18 @@ class DeconzDevice(DeconzBase, Entity): async def async_added_to_hass(self): """Subscribe to device events.""" - self._device.register_async_callback(self.async_update_callback) + self._device.register_callback(self.async_update_callback) self.gateway.deconz_ids[self.entity_id] = self._device.deconz_id self.listeners.append( async_dispatcher_connect( self.hass, self.gateway.signal_reachable, self.async_update_callback ) ) + self.listeners.append( + async_dispatcher_connect( + self.hass, self.gateway.signal_remove_entity, self.async_remove_self + ) + ) async def async_will_remove_from_hass(self) -> None: """Disconnect device object when removed.""" @@ -96,6 +90,15 @@ class DeconzDevice(DeconzBase, Entity): for unsub_dispatcher in self.listeners: unsub_dispatcher() + async def async_remove_self(self, deconz_ids: list) -> None: + """Schedule removal of this entity. + + Called by signal_remove_entity scheduled by async_added_to_hass. + """ + if self._device.deconz_id not in deconz_ids: + return + await self.async_remove() + @callback def async_update_callback(self, force_update=False, ignore_update=False): """Update the device's state.""" diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index 98a85a707bd..1009ae4e54c 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -3,7 +3,7 @@ from homeassistant.const import CONF_EVENT, CONF_ID from homeassistant.core import callback from homeassistant.util import slugify -from .const import _LOGGER, CONF_GESTURE +from .const import CONF_GESTURE, LOGGER from .deconz_device import DeconzBase CONF_DECONZ_EVENT = "deconz_event" @@ -21,11 +21,11 @@ class DeconzEvent(DeconzBase): """Register callback that will be used for signals.""" super().__init__(device, gateway) - self._device.register_async_callback(self.async_update_callback) + self._device.register_callback(self.async_update_callback) self.device_id = None self.event_id = slugify(self._device.name) - _LOGGER.debug("deCONZ event created: %s", self.event_id) + LOGGER.debug("deCONZ event created: %s", self.event_id) @property def device(self): diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 04452cc313c..0b69b82463c 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -9,24 +9,19 @@ from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) -from homeassistant.helpers.entity_registry import ( - DISABLED_CONFIG_ENTRY, - async_get_registry, -) +from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( - _LOGGER, CONF_ALLOW_CLIP_SENSOR, CONF_ALLOW_DECONZ_GROUPS, CONF_MASTER_GATEWAY, DEFAULT_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_DECONZ_GROUPS, DOMAIN, + LOGGER, NEW_DEVICE, + NEW_GROUP, + NEW_SENSOR, SUPPORTED_PLATFORMS, ) from .errors import AuthenticationRequired, CannotConnect @@ -52,6 +47,9 @@ class DeconzGateway: self.events = [] self.listeners = [] + self._current_option_allow_clip_sensor = self.option_allow_clip_sensor + self._current_option_allow_deconz_groups = self.option_allow_deconz_groups + @property def bridgeid(self) -> str: """Return the unique identifier of the gateway.""" @@ -103,7 +101,7 @@ class DeconzGateway: raise ConfigEntryNotReady except Exception as err: # pylint: disable=broad-except - _LOGGER.error("Error connecting with deCONZ gateway: %s", err) + LOGGER.error("Error connecting with deCONZ gateway: %s", err) return False for component in SUPPORTED_PLATFORMS: @@ -115,23 +113,64 @@ class DeconzGateway: self.api.start() - self.config_entry.add_update_listener(self.async_new_address) - self.config_entry.add_update_listener(self.async_options_updated) + self.config_entry.add_update_listener(self.async_config_entry_updated) return True @staticmethod - async def async_new_address(hass, entry) -> None: - """Handle signals of gateway getting new address. + async def async_config_entry_updated(hass, entry) -> None: + """Handle signals of config entry being updated. - This is a static method because a class method (bound method), - can not be used with weak references. + This is a static method because a class method (bound method), can not be used with weak references. + Causes for this is either discovery updating host address or config entry options changing. """ gateway = get_gateway_from_config_entry(hass, entry) if gateway.api.host != entry.data[CONF_HOST]: gateway.api.close() gateway.api.host = entry.data[CONF_HOST] gateway.api.start() + return + + await gateway.options_updated() + + async def options_updated(self): + """Manage entities affected by config entry options.""" + deconz_ids = [] + + if self._current_option_allow_clip_sensor != self.option_allow_clip_sensor: + self._current_option_allow_clip_sensor = self.option_allow_clip_sensor + + sensors = [ + sensor + for sensor in self.api.sensors.values() + if sensor.type.startswith("CLIP") + ] + + if self.option_allow_clip_sensor: + self.async_add_device_callback(NEW_SENSOR, sensors) + else: + deconz_ids += [sensor.deconz_id for sensor in sensors] + + if self._current_option_allow_deconz_groups != self.option_allow_deconz_groups: + self._current_option_allow_deconz_groups = self.option_allow_deconz_groups + + groups = list(self.api.groups.values()) + + if self.option_allow_deconz_groups: + self.async_add_device_callback(NEW_GROUP, groups) + else: + deconz_ids += [group.deconz_id for group in groups] + + if deconz_ids: + async_dispatcher_send(self.hass, self.signal_remove_entity, deconz_ids) + + entity_registry = await self.hass.helpers.entity_registry.async_get_registry() + + for entity_id, deconz_id in self.deconz_ids.items(): + if deconz_id in deconz_ids and entity_registry.async_is_registered( + entity_id + ): + entity_registry.async_remove(entity_id) @property def signal_reachable(self) -> str: @@ -144,24 +183,16 @@ class DeconzGateway: self.available = available async_dispatcher_send(self.hass, self.signal_reachable, True) - @property - def signal_options_update(self) -> str: - """Event specific per deCONZ entry to signal new options.""" - return f"deconz-options-{self.bridgeid}" - - @staticmethod - async def async_options_updated(hass, entry) -> None: - """Triggered by config entry options updates.""" - gateway = get_gateway_from_config_entry(hass, entry) - - registry = await async_get_registry(hass) - async_dispatcher_send(hass, gateway.signal_options_update, registry) - @callback def async_signal_new_device(self, device_type) -> str: """Gateway specific event to signal new device.""" return NEW_DEVICE[device_type].format(self.bridgeid) + @property + def signal_remove_entity(self) -> str: + """Gateway specific event to signal removal of entity.""" + return f"deconz-remove-{self.bridgeid}" + @callback def async_add_device_callback(self, device_type, device) -> None: """Handle event of new device creation in deCONZ.""" @@ -221,44 +252,9 @@ async def get_gateway( return deconz except errors.Unauthorized: - _LOGGER.warning("Invalid key for deCONZ at %s", config[CONF_HOST]) + LOGGER.warning("Invalid key for deCONZ at %s", config[CONF_HOST]) raise AuthenticationRequired except (asyncio.TimeoutError, errors.RequestError): - _LOGGER.error("Error connecting to deCONZ gateway at %s", config[CONF_HOST]) + LOGGER.error("Error connecting to deCONZ gateway at %s", config[CONF_HOST]) raise CannotConnect - - -class DeconzEntityHandler: - """Platform entity handler to help with updating disabled by.""" - - def __init__(self, gateway) -> None: - """Create an entity handler.""" - self.gateway = gateway - self._entities = [] - - gateway.listeners.append( - async_dispatcher_connect( - gateway.hass, gateway.signal_options_update, self.update_entity_registry - ) - ) - - @callback - def add_entity(self, entity) -> None: - """Add a new entity to handler.""" - self._entities.append(entity) - - @callback - def update_entity_registry(self, entity_registry) -> None: - """Update entity registry disabled by status.""" - for entity in self._entities: - - if entity.entity_registry_enabled_default != entity.enabled: - disabled_by = None - - if entity.enabled: - disabled_by = DISABLED_CONFIG_ENTRY - - entity_registry.async_update_entity( - entity.registry_entry.entity_id, disabled_by=disabled_by - ) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index ee22c86c44a..f62f9315c49 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -30,7 +30,7 @@ from .const import ( SWITCH_TYPES, ) from .deconz_device import DeconzDevice -from .gateway import DeconzEntityHandler, get_gateway_from_config_entry +from .gateway import get_gateway_from_config_entry async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -41,8 +41,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the deCONZ lights and groups from a config entry.""" gateway = get_gateway_from_config_entry(hass, config_entry) - entity_handler = DeconzEntityHandler(gateway) - @callback def async_add_light(lights): """Add light from deCONZ.""" @@ -63,13 +61,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def async_add_group(groups): """Add group from deCONZ.""" + if not gateway.option_allow_deconz_groups: + return + entities = [] for group in groups: - if group.lights: - new_group = DeconzGroup(group, gateway) - entity_handler.add_entity(new_group) - entities.append(new_group) + if group.lights and group.deconz_id not in gateway.deconz_ids.values(): + entities.append(DeconzGroup(group, gateway)) async_add_entities(entities, True) @@ -156,7 +155,7 @@ class DeconzLight(DeconzDevice, Light): if ATTR_TRANSITION in kwargs: data["transitiontime"] = int(kwargs[ATTR_TRANSITION] * 10) - elif "IKEA" in (self._device.manufacturer or ""): + elif "IKEA" in self._device.manufacturer: data["transitiontime"] = 0 if ATTR_FLASH in kwargs: diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index adac6f54493..425a44bf042 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==69" + "pydeconz==70" ], "ssdp": [ { diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 81804dfb9f6..6b88c414243 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -19,7 +19,7 @@ from homeassistant.helpers.dispatcher import ( from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR from .deconz_device import DeconzDevice from .deconz_event import DeconzEvent -from .gateway import DeconzEntityHandler, get_gateway_from_config_entry +from .gateway import get_gateway_from_config_entry ATTR_CURRENT = "current" ATTR_POWER = "power" @@ -37,7 +37,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): batteries = set() battery_handler = DeconzBatteryHandler(gateway) - entity_handler = DeconzEntityHandler(gateway) @callback def async_add_sensor(sensors, new=True): @@ -65,11 +64,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): new and sensor.BINARY is False and sensor.type not in Battery.ZHATYPE + Thermostat.ZHATYPE + and ( + gateway.option_allow_clip_sensor + or not sensor.type.startswith("CLIP") + ) + and sensor.deconz_id not in gateway.deconz_ids.values() ): - - new_sensor = DeconzSensor(sensor, gateway) - entity_handler.add_entity(new_sensor) - entities.append(new_sensor) + entities.append(DeconzSensor(sensor, gateway)) if sensor.battery is not None: new_battery = DeconzBattery(sensor, gateway) @@ -216,7 +217,7 @@ class DeconzSensorStateTracker: """Set up tracker.""" self.sensor = sensor self.gateway = gateway - sensor.register_async_callback(self.async_update_callback) + sensor.register_callback(self.async_update_callback) @callback def close(self): diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index f1b19c79fce..c85fa8073a3 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -6,9 +6,9 @@ from homeassistant.helpers import config_validation as cv from .config_flow import get_master_gateway from .const import ( - _LOGGER, CONF_BRIDGE_ID, DOMAIN, + LOGGER, NEW_GROUP, NEW_LIGHT, NEW_SCENE, @@ -110,7 +110,7 @@ async def async_configure_service(hass, data): try: field = gateway.deconz_ids[entity_id] + field except KeyError: - _LOGGER.error("Could not find the entity %s", entity_id) + LOGGER.error("Could not find the entity %s", entity_id) return await gateway.api.request("put", field, json=data) diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index b61ea6236da..52cd90e54a1 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -14,20 +14,9 @@ "title": "Link with deCONZ", "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ Settings -> Gateway -> Advanced\n2. Press \"Authenticate app\" button" }, - "options": { - "title": "Extra configuration options for deCONZ", - "data": { - "allow_clip_sensor": "Allow importing virtual sensors", - "allow_deconz_groups": "Allow importing deCONZ groups" - } - }, "hassio_confirm": { "title": "deCONZ Zigbee gateway via Hass.io add-on", - "description": "Do you want to configure Home Assistant to connect to the deCONZ gateway provided by the Hass.io add-on {addon}?", - "data": { - "allow_clip_sensor": "Allow importing virtual sensors", - "allow_deconz_groups": "Allow importing deCONZ groups" - } + "description": "Do you want to configure Home Assistant to connect to the deCONZ gateway provided by the Hass.io add-on {addon}?" } }, "error": { @@ -45,11 +34,12 @@ "options": { "step": { "deconz_devices": { - "description": "Configure visibility of deCONZ device types", "data": { "allow_clip_sensor": "Allow deCONZ CLIP sensors", "allow_deconz_groups": "Allow deCONZ light groups" - } + }, + "description": "Configure visibility of deCONZ device types", + "title": "deCONZ options" } } }, @@ -105,4 +95,4 @@ "side_6": "Side 6" } } -} +} \ No newline at end of file diff --git a/homeassistant/components/deluge/sensor.py b/homeassistant/components/deluge/sensor.py index 7df87490c60..55309ea8b31 100644 --- a/homeassistant/components/deluge/sensor.py +++ b/homeassistant/components/deluge/sensor.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_PORT, CONF_USERNAME, + DATA_RATE_KILOBYTES_PER_SECOND, STATE_IDLE, ) from homeassistant.exceptions import PlatformNotReady @@ -27,8 +28,8 @@ DHT_UPLOAD = 1000 DHT_DOWNLOAD = 1000 SENSOR_TYPES = { "current_status": ["Status", None], - "download_speed": ["Down Speed", "kB/s"], - "upload_speed": ["Up Speed", "kB/s"], + "download_speed": ["Down Speed", DATA_RATE_KILOBYTES_PER_SECOND], + "upload_speed": ["Up Speed", DATA_RATE_KILOBYTES_PER_SECOND], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/demo/.translations/ca.json b/homeassistant/components/demo/.translations/ca.json index 944d358e739..a29718fea7a 100644 --- a/homeassistant/components/demo/.translations/ca.json +++ b/homeassistant/components/demo/.translations/ca.json @@ -1,5 +1,22 @@ { "config": { "title": "Demostraci\u00f3" + }, + "options": { + "step": { + "options_1": { + "data": { + "bool": "Entrada booleana opcional", + "int": "Entrada num\u00e8rica" + } + }, + "options_2": { + "data": { + "multi": "Selecci\u00f3 m\u00faltiple", + "select": "Selecciona una opci\u00f3", + "string": "Valor de cadena (string)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/da.json b/homeassistant/components/demo/.translations/da.json index ef01fcb4f3c..fd2764e5ec9 100644 --- a/homeassistant/components/demo/.translations/da.json +++ b/homeassistant/components/demo/.translations/da.json @@ -1,5 +1,28 @@ { "config": { "title": "Demo" + }, + "options": { + "step": { + "init": { + "data": { + "one": "en", + "other": "anden" + } + }, + "options_1": { + "data": { + "bool": "Valgfri boolsk", + "int": "Numerisk input" + } + }, + "options_2": { + "data": { + "multi": "Multimarkering", + "select": "V\u00e6lg en mulighed", + "string": "Strengv\u00e6rdi" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/de.json b/homeassistant/components/demo/.translations/de.json index ef01fcb4f3c..a600790d2fc 100644 --- a/homeassistant/components/demo/.translations/de.json +++ b/homeassistant/components/demo/.translations/de.json @@ -1,5 +1,22 @@ { "config": { "title": "Demo" + }, + "options": { + "step": { + "options_1": { + "data": { + "bool": "Optionaler Boolescher Wert", + "int": "Numerische Eingabe" + } + }, + "options_2": { + "data": { + "multi": "Mehrfachauswahl", + "select": "W\u00e4hlen Sie eine Option", + "string": "String-Wert" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/en.json b/homeassistant/components/demo/.translations/en.json index ef01fcb4f3c..e49671c88c8 100644 --- a/homeassistant/components/demo/.translations/en.json +++ b/homeassistant/components/demo/.translations/en.json @@ -1,5 +1,22 @@ { "config": { "title": "Demo" + }, + "options": { + "step": { + "options_1": { + "data": { + "bool": "Optional boolean", + "int": "Numeric input" + } + }, + "options_2": { + "data": { + "multi": "Multiselect", + "select": "Select an option", + "string": "String value" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/es-419.json b/homeassistant/components/demo/.translations/es-419.json new file mode 100644 index 00000000000..ef01fcb4f3c --- /dev/null +++ b/homeassistant/components/demo/.translations/es-419.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demo" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/es.json b/homeassistant/components/demo/.translations/es.json index ef01fcb4f3c..73ed9809d65 100644 --- a/homeassistant/components/demo/.translations/es.json +++ b/homeassistant/components/demo/.translations/es.json @@ -1,5 +1,22 @@ { "config": { "title": "Demo" + }, + "options": { + "step": { + "options_1": { + "data": { + "bool": "Booleano opcional", + "int": "Entrada num\u00e9rica" + } + }, + "options_2": { + "data": { + "multi": "Multiselecci\u00f3n", + "select": "Selecciona una opci\u00f3n", + "string": "Valor de cadena" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/it.json b/homeassistant/components/demo/.translations/it.json index ef01fcb4f3c..7b299913c8e 100644 --- a/homeassistant/components/demo/.translations/it.json +++ b/homeassistant/components/demo/.translations/it.json @@ -1,5 +1,28 @@ { "config": { "title": "Demo" + }, + "options": { + "step": { + "init": { + "data": { + "one": "uno", + "other": "altro" + } + }, + "options_1": { + "data": { + "bool": "Valore booleano facoltativo", + "int": "Input numerico" + } + }, + "options_2": { + "data": { + "multi": "Selezione multipla", + "select": "Selezionare un'opzione", + "string": "Valore stringa" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/ko.json b/homeassistant/components/demo/.translations/ko.json index d20943c7b36..efe69b575fb 100644 --- a/homeassistant/components/demo/.translations/ko.json +++ b/homeassistant/components/demo/.translations/ko.json @@ -1,5 +1,22 @@ { "config": { "title": "\ub370\ubaa8" + }, + "options": { + "step": { + "options_1": { + "data": { + "bool": "\ub17c\ub9ac \uc120\ud0dd", + "int": "\uc22b\uc790 \uc785\ub825" + } + }, + "options_2": { + "data": { + "multi": "\ub2e4\uc911 \uc120\ud0dd", + "select": "\uc635\uc158 \uc120\ud0dd", + "string": "\ubb38\uc790\uc5f4 \uac12" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/lb.json b/homeassistant/components/demo/.translations/lb.json index ef01fcb4f3c..d968b43af8b 100644 --- a/homeassistant/components/demo/.translations/lb.json +++ b/homeassistant/components/demo/.translations/lb.json @@ -1,5 +1,14 @@ { "config": { "title": "Demo" + }, + "options": { + "step": { + "options_2": { + "data": { + "select": "Eng Optioun auswielen" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/nl.json b/homeassistant/components/demo/.translations/nl.json index ef01fcb4f3c..cb932a0d9d6 100644 --- a/homeassistant/components/demo/.translations/nl.json +++ b/homeassistant/components/demo/.translations/nl.json @@ -1,5 +1,28 @@ { "config": { "title": "Demo" + }, + "options": { + "step": { + "init": { + "data": { + "one": "Empty", + "other": "" + } + }, + "options_1": { + "data": { + "bool": "Optioneel Boolean", + "int": "Numerieke invoer" + } + }, + "options_2": { + "data": { + "multi": "Meerkeuze selectie", + "select": "Kies een optie", + "string": "String waarde" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/no.json b/homeassistant/components/demo/.translations/no.json index ef01fcb4f3c..a46606621b9 100644 --- a/homeassistant/components/demo/.translations/no.json +++ b/homeassistant/components/demo/.translations/no.json @@ -1,5 +1,22 @@ { "config": { "title": "Demo" + }, + "options": { + "step": { + "options_1": { + "data": { + "bool": "Valgfri boolean", + "int": "Numerisk inndata" + } + }, + "options_2": { + "data": { + "multi": "Flervalg", + "select": "Velg et alternativ", + "string": "Strengverdi" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/pl.json b/homeassistant/components/demo/.translations/pl.json index ef01fcb4f3c..f224d100929 100644 --- a/homeassistant/components/demo/.translations/pl.json +++ b/homeassistant/components/demo/.translations/pl.json @@ -1,5 +1,30 @@ { "config": { "title": "Demo" + }, + "options": { + "step": { + "init": { + "data": { + "few": "kilka", + "many": "wiele", + "one": "jedena", + "other": "inne" + } + }, + "options_1": { + "data": { + "bool": "Warto\u015b\u0107 logiczna", + "int": "Warto\u015b\u0107 numeryczna" + } + }, + "options_2": { + "data": { + "multi": "Wielokrotny wyb\u00f3r", + "select": "Wybierz opcj\u0119", + "string": "Warto\u015b\u0107 tekstowa" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/ru.json b/homeassistant/components/demo/.translations/ru.json index 0438252a429..22ea3d2e196 100644 --- a/homeassistant/components/demo/.translations/ru.json +++ b/homeassistant/components/demo/.translations/ru.json @@ -1,5 +1,22 @@ { "config": { "title": "\u0414\u0435\u043c\u043e" + }, + "options": { + "step": { + "options_1": { + "data": { + "bool": "\u041b\u043e\u0433\u0438\u0447\u0435\u0441\u043a\u0438\u0439", + "int": "\u0427\u0438\u0441\u043b\u043e\u0432\u043e\u0439" + } + }, + "options_2": { + "data": { + "multi": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e", + "select": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u043e\u043f\u0446\u0438\u044e", + "string": "\u0421\u0442\u0440\u043e\u043a\u043e\u0432\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/sl.json b/homeassistant/components/demo/.translations/sl.json index ef01fcb4f3c..b67d4d56fb1 100644 --- a/homeassistant/components/demo/.translations/sl.json +++ b/homeassistant/components/demo/.translations/sl.json @@ -1,5 +1,30 @@ { "config": { "title": "Demo" + }, + "options": { + "step": { + "init": { + "data": { + "few": "prazni", + "one": "prazen", + "other": "prazni", + "two": "prazna" + } + }, + "options_1": { + "data": { + "bool": "Izbirna logi\u010dna vrednost", + "int": "\u0160tevil\u010dni vnos" + } + }, + "options_2": { + "data": { + "multi": "Multiselect", + "select": "Izberite mo\u017enost", + "string": "Vrednost niza" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/sv.json b/homeassistant/components/demo/.translations/sv.json new file mode 100644 index 00000000000..4c5f477cc1c --- /dev/null +++ b/homeassistant/components/demo/.translations/sv.json @@ -0,0 +1,28 @@ +{ + "config": { + "title": "Demo" + }, + "options": { + "step": { + "init": { + "data": { + "one": "Tom", + "other": "Tomma" + } + }, + "options_1": { + "data": { + "bool": "Valfritt boolesk", + "int": "Numerisk inmatning" + } + }, + "options_2": { + "data": { + "multi": "Flera val", + "select": "V\u00e4lj ett alternativ", + "string": "Str\u00e4ngv\u00e4rde" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/zh-Hans.json b/homeassistant/components/demo/.translations/zh-Hans.json new file mode 100644 index 00000000000..9155b5066c5 --- /dev/null +++ b/homeassistant/components/demo/.translations/zh-Hans.json @@ -0,0 +1,19 @@ +{ + "options": { + "step": { + "options_1": { + "data": { + "bool": "\u5e03\u5c14\u9009\u9879", + "int": "\u6570\u503c\u8f93\u5165" + } + }, + "options_2": { + "data": { + "multi": "\u591a\u91cd\u9009\u62e9", + "select": "\u9009\u62e9\u9009\u9879", + "string": "\u5b57\u7b26\u4e32\u503c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/zh-Hant.json b/homeassistant/components/demo/.translations/zh-Hant.json index cfb0fced0c2..7f6ac42d609 100644 --- a/homeassistant/components/demo/.translations/zh-Hant.json +++ b/homeassistant/components/demo/.translations/zh-Hant.json @@ -1,5 +1,22 @@ { "config": { "title": "\u5c55\u793a" + }, + "options": { + "step": { + "options_1": { + "data": { + "bool": "\u9078\u9805\u5e03\u6797", + "int": "\u6578\u503c\u8f38\u5165" + } + }, + "options_2": { + "data": { + "multi": "\u591a\u91cd\u9078\u64c7", + "select": "\u9078\u64c7\u9078\u9805", + "string": "\u5b57\u4e32\u503c" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/demo/config_flow.py b/homeassistant/components/demo/config_flow.py index e6b275920c8..1f3975d0241 100644 --- a/homeassistant/components/demo/config_flow.py +++ b/homeassistant/components/demo/config_flow.py @@ -1,16 +1,97 @@ """Config flow to configure demo component.""" +import voluptuous as vol from homeassistant import config_entries +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv # pylint: disable=unused-import from . import DOMAIN +CONF_STRING = "string" +CONF_BOOLEAN = "bool" +CONF_INT = "int" +CONF_SELECT = "select" +CONF_MULTISELECT = "multi" + class DemoConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Demo configuration flow.""" VERSION = 1 + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + async def async_step_import(self, import_info): """Set the config entry up from yaml.""" return self.async_create_entry(title="Demo", data={}) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle options.""" + + def __init__(self, config_entry): + """Initialize options flow.""" + self.config_entry = config_entry + self.options = dict(config_entry.options) + + async def async_step_init(self, user_input=None): + """Manage the options.""" + return await self.async_step_options_1() + + async def async_step_options_1(self, user_input=None): + """Manage the options.""" + if user_input is not None: + self.options.update(user_input) + return await self.async_step_options_2() + + return self.async_show_form( + step_id="options_1", + data_schema=vol.Schema( + { + vol.Optional( + CONF_BOOLEAN, + default=self.config_entry.options.get(CONF_BOOLEAN, False), + ): bool, + vol.Optional( + CONF_INT, default=self.config_entry.options.get(CONF_INT, 10), + ): int, + } + ), + ) + + async def async_step_options_2(self, user_input=None): + """Manage the options 2.""" + if user_input is not None: + self.options.update(user_input) + return await self._update_options() + + return self.async_show_form( + step_id="options_2", + data_schema=vol.Schema( + { + vol.Optional( + CONF_STRING, + default=self.config_entry.options.get(CONF_STRING, "Default",), + ): str, + vol.Optional( + CONF_SELECT, + default=self.config_entry.options.get(CONF_SELECT, "default"), + ): vol.In(["default", "other"]), + vol.Optional( + CONF_MULTISELECT, + default=self.config_entry.options.get( + CONF_MULTISELECT, ["default"] + ), + ): cv.multi_select({"default": "Default", "other": "Other"}), + } + ), + ) + + async def _update_options(self): + """Update config entry options.""" + return self.async_create_entry(title="", data=self.options) diff --git a/homeassistant/components/demo/strings.json b/homeassistant/components/demo/strings.json index a2c0103280b..33f3b4229dc 100644 --- a/homeassistant/components/demo/strings.json +++ b/homeassistant/components/demo/strings.json @@ -1,5 +1,25 @@ { "config": { "title": "Demo" + }, + "options": { + "step": { + "init": { + "data": {} + }, + "options_1": { + "data": { + "bool": "Optional boolean", + "int": "Numeric input" + } + }, + "options_2": { + "data": { + "string": "String value", + "select": "Select an option", + "multi": "Multiselect" + } + } + } } } diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index 1ecbe5b939f..1387875c02d 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -2,7 +2,7 @@ "domain": "denonavr", "name": "Denon AVR Network Receivers", "documentation": "https://www.home-assistant.io/integrations/denonavr", - "requirements": ["denonavr==0.7.11"], + "requirements": ["denonavr==0.7.12"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 350d065f9d9..b14592d1b78 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -30,6 +30,7 @@ from homeassistant.const import ( CONF_TIMEOUT, CONF_ZONE, ENTITY_MATCH_ALL, + ENTITY_MATCH_NONE, STATE_OFF, STATE_ON, STATE_PAUSED, @@ -201,6 +202,10 @@ class DenonDevice(MediaPlayerDevice): def signal_handler(self, data): """Handle domain-specific signal by calling appropriate method.""" entity_ids = data[ATTR_ENTITY_ID] + + if entity_ids == ENTITY_MATCH_NONE: + return + if entity_ids == ENTITY_MATCH_ALL or self.entity_id in entity_ids: params = { key: value diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 56e087f0e5f..95b3fc9fdb3 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -1,5 +1,6 @@ """Helpers for device automations.""" import asyncio +from functools import wraps import logging from types import ModuleType from typing import Any, List, MutableMapping @@ -14,7 +15,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.loader import IntegrationNotFound, async_get_integration -from .exceptions import InvalidDeviceAutomationConfig +from .exceptions import DeviceNotFound, InvalidDeviceAutomationConfig # mypy: allow-untyped-calls, allow-untyped-defs @@ -117,6 +118,10 @@ async def _async_get_device_automations(hass, automation_type, device_id): domains = set() automations: List[MutableMapping[str, Any]] = [] device = device_registry.async_get(device_id) + + if device is None: + raise DeviceNotFound + for entry_id in device.config_entries: config_entry = hass.config_entries.async_get_entry(entry_id) domains.add(config_entry.domain) @@ -173,6 +178,21 @@ async def _async_get_device_automation_capabilities(hass, automation_type, autom return capabilities +def handle_device_errors(func): + """Handle device automation errors.""" + + @wraps(func) + async def with_error_handling(hass, connection, msg): + try: + await func(hass, connection, msg) + except DeviceNotFound: + connection.send_error( + msg["id"], websocket_api.const.ERR_NOT_FOUND, "Device not found" + ) + + return with_error_handling + + @websocket_api.websocket_command( { vol.Required("type"): "device_automation/action/list", @@ -180,6 +200,7 @@ async def _async_get_device_automation_capabilities(hass, automation_type, autom } ) @websocket_api.async_response +@handle_device_errors async def websocket_device_automation_list_actions(hass, connection, msg): """Handle request for device actions.""" device_id = msg["device_id"] @@ -194,6 +215,7 @@ async def websocket_device_automation_list_actions(hass, connection, msg): } ) @websocket_api.async_response +@handle_device_errors async def websocket_device_automation_list_conditions(hass, connection, msg): """Handle request for device conditions.""" device_id = msg["device_id"] @@ -208,6 +230,7 @@ async def websocket_device_automation_list_conditions(hass, connection, msg): } ) @websocket_api.async_response +@handle_device_errors async def websocket_device_automation_list_triggers(hass, connection, msg): """Handle request for device triggers.""" device_id = msg["device_id"] @@ -222,6 +245,7 @@ async def websocket_device_automation_list_triggers(hass, connection, msg): } ) @websocket_api.async_response +@handle_device_errors async def websocket_device_automation_get_action_capabilities(hass, connection, msg): """Handle request for device action capabilities.""" action = msg["action"] @@ -238,6 +262,7 @@ async def websocket_device_automation_get_action_capabilities(hass, connection, } ) @websocket_api.async_response +@handle_device_errors async def websocket_device_automation_get_condition_capabilities(hass, connection, msg): """Handle request for device condition capabilities.""" condition = msg["condition"] @@ -254,6 +279,7 @@ async def websocket_device_automation_get_condition_capabilities(hass, connectio } ) @websocket_api.async_response +@handle_device_errors async def websocket_device_automation_get_trigger_capabilities(hass, connection, msg): """Handle request for device trigger capabilities.""" trigger = msg["trigger"] diff --git a/homeassistant/components/device_automation/exceptions.py b/homeassistant/components/device_automation/exceptions.py index 2f7c0df0187..ad92696cb94 100644 --- a/homeassistant/components/device_automation/exceptions.py +++ b/homeassistant/components/device_automation/exceptions.py @@ -4,3 +4,7 @@ from homeassistant.exceptions import HomeAssistantError class InvalidDeviceAutomationConfig(HomeAssistantError): """When device automation config is invalid.""" + + +class DeviceNotFound(HomeAssistantError): + """When referenced device not found.""" diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index f6bb74edbec..a2dcd62db8c 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -74,10 +74,12 @@ ENTITY_TRIGGERS = [ }, ] +DEVICE_ACTION_TYPES = [CONF_TOGGLE, CONF_TURN_OFF, CONF_TURN_ON] + ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Required(CONF_TYPE): vol.In([CONF_TOGGLE, CONF_TURN_OFF, CONF_TURN_ON]), + vol.Required(CONF_TYPE): vol.In(DEVICE_ACTION_TYPES), } ) diff --git a/homeassistant/components/device_tracker/.translations/es-419.json b/homeassistant/components/device_tracker/.translations/es-419.json new file mode 100644 index 00000000000..cfbf7bcfe3e --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/es-419.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_home": "{entity_name} est\u00e1 en casa", + "is_not_home": "{entity_name} no est\u00e1 en casa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/device_tracker/.translations/hu.json b/homeassistant/components/device_tracker/.translations/hu.json new file mode 100644 index 00000000000..7302f40df9e --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/hu.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_home": "{entity_name} otthon van", + "is_not_home": "{entity_name} nincs otthon" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/device_tracker/.translations/sv.json b/homeassistant/components/device_tracker/.translations/sv.json new file mode 100644 index 00000000000..70287ad318a --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/sv.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_home": "{entity_name} \u00e4r hemma", + "is_not_home": "{entity_name} \u00e4r inte hemma" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index 6c5cacac591..059c51989fe 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -61,6 +61,11 @@ class BaseTrackerEntity(Entity): class TrackerEntity(BaseTrackerEntity): """Represent a tracked device.""" + @property + def force_update(self): + """All updates need to be written to the state machine.""" + return True + @property def location_accuracy(self): """Return the location accuracy of the device. diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index da3c945bc86..08bbed12519 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -197,7 +197,6 @@ class DeviceTracker: self.track_new, dev_id, mac, - (host_name or dev_id).replace("_", " "), picture=picture, icon=icon, hide_if_away=self.defaults.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE), @@ -342,7 +341,7 @@ class Device(RestoreEntity): @property def name(self): """Return the name of the entity.""" - return self.config_name or self.host_name or DEVICE_DEFAULT_NAME + return self.config_name or self.host_name or self.dev_id or DEVICE_DEFAULT_NAME @property def state(self): @@ -393,7 +392,7 @@ class Device(RestoreEntity): """Mark the device as seen.""" self.source_type = source_type self.last_seen = dt_util.utcnow() - self.host_name = host_name + self.host_name = host_name or self.host_name self.location_name = location_name self.consider_home = consider_home or self.consider_home diff --git a/homeassistant/components/directv/manifest.json b/homeassistant/components/directv/manifest.json index adf05621a2c..b0f0f8bb5eb 100644 --- a/homeassistant/components/directv/manifest.json +++ b/homeassistant/components/directv/manifest.json @@ -2,7 +2,7 @@ "domain": "directv", "name": "DirecTV", "documentation": "https://www.home-assistant.io/integrations/directv", - "requirements": ["directpy==0.5"], + "requirements": ["directpy==0.6"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/discord/manifest.json b/homeassistant/components/discord/manifest.json index a9aeea27aef..e496ad0d532 100644 --- a/homeassistant/components/discord/manifest.json +++ b/homeassistant/components/discord/manifest.json @@ -2,7 +2,7 @@ "domain": "discord", "name": "Discord", "documentation": "https://www.home-assistant.io/integrations/discord", - "requirements": ["discord.py==1.2.5"], + "requirements": ["discord.py==1.3.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index fa6b60d0c19..1e3ba840d6f 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -99,10 +99,10 @@ def catch_request_errors(): """Call wrapper for decorator.""" @functools.wraps(func) - def wrapper(self, *args, **kwargs): + async def wrapper(self, *args, **kwargs): """Catch asyncio.TimeoutError, aiohttp.ClientError errors.""" try: - return func(self, *args, **kwargs) + return await func(self, *args, **kwargs) except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Error during call %s", func.__name__) diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py index 9525f9e8ddf..65a32938140 100644 --- a/homeassistant/components/doods/image_processing.py +++ b/homeassistant/components/doods/image_processing.py @@ -285,7 +285,7 @@ class Doods(ImageProcessingEntity): ) # Run detection - start = time.time() + start = time.monotonic() response = self._doods.detect( image, dconfig=self._dconfig, detector_name=self._detector_name ) @@ -293,7 +293,7 @@ class Doods(ImageProcessingEntity): "doods detect: %s response: %s duration: %s", self._dconfig, response, - time.time() - start, + time.monotonic() - start, ) matches = {} diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index 551af839b5c..1ac905feac2 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -2,7 +2,10 @@ "domain": "doods", "name": "DOODS - Distributed Outside Object Detection Service", "documentation": "https://www.home-assistant.io/integrations/doods", - "requirements": ["pydoods==1.0.2", "pillow==6.2.1"], + "requirements": [ + "pydoods==1.0.2", + "pillow==7.0.0" + ], "dependencies": [], "codeowners": [] -} +} \ No newline at end of file diff --git a/homeassistant/components/dovado/sensor.py b/homeassistant/components/dovado/sensor.py index d3374c8d02a..ab85c376469 100644 --- a/homeassistant/components/dovado/sensor.py +++ b/homeassistant/components/dovado/sensor.py @@ -6,7 +6,7 @@ import re import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_SENSORS +from homeassistant.const import CONF_SENSORS, DATA_GIGABYTES import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -26,8 +26,13 @@ SENSORS = { SENSOR_NETWORK: ("signal strength", "Network", None, "mdi:access-point-network"), SENSOR_SIGNAL: ("signal strength", "Signal Strength", "%", "mdi:signal"), SENSOR_SMS_UNREAD: ("sms unread", "SMS unread", "", "mdi:message-text-outline"), - SENSOR_UPLOAD: ("traffic modem tx", "Sent", "GB", "mdi:cloud-upload"), - SENSOR_DOWNLOAD: ("traffic modem rx", "Received", "GB", "mdi:cloud-download"), + SENSOR_UPLOAD: ("traffic modem tx", "Sent", DATA_GIGABYTES, "mdi:cloud-upload"), + SENSOR_DOWNLOAD: ( + "traffic modem rx", + "Received", + DATA_GIGABYTES, + "mdi:cloud-download", + ), } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 253e8409f1b..54c8e3e29b2 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -1,6 +1,5 @@ """Support for Dutch Smart Meter (also known as Smartmeter or P1 port).""" import asyncio -from datetime import timedelta from functools import partial import logging @@ -32,9 +31,6 @@ ICON_POWER = "mdi:flash" ICON_POWER_FAILURE = "mdi:flash-off" ICON_SWELL_SAG = "mdi:pulse" -# Smart meter sends telegram every 10 seconds -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) - RECONNECT_INTERVAL = 5 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -42,7 +38,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string, vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_DSMR_VERSION, default=DEFAULT_DSMR_VERSION): vol.All( - cv.string, vol.In(["5", "4", "2.2"]) + cv.string, vol.In(["5B", "5", "4", "2.2"]) ), vol.Optional(CONF_RECONNECT_INTERVAL, default=30): int, vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): vol.Coerce(int), @@ -62,17 +58,18 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ["Power Consumption", obis_ref.CURRENT_ELECTRICITY_USAGE], ["Power Production", obis_ref.CURRENT_ELECTRICITY_DELIVERY], ["Power Tariff", obis_ref.ELECTRICITY_ACTIVE_TARIFF], - ["Power Consumption (total)", obis_ref.ELECTRICITY_IMPORTED_TOTAL], - ["Power Consumption (low)", obis_ref.ELECTRICITY_USED_TARIFF_1], - ["Power Consumption (normal)", obis_ref.ELECTRICITY_USED_TARIFF_2], - ["Power Production (low)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_1], - ["Power Production (normal)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_2], + ["Energy Consumption (total)", obis_ref.ELECTRICITY_IMPORTED_TOTAL], + ["Energy Consumption (tarif 1)", obis_ref.ELECTRICITY_USED_TARIFF_1], + ["Energy Consumption (tarif 2)", obis_ref.ELECTRICITY_USED_TARIFF_2], + ["Energy Production (tarif 1)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_1], + ["Energy Production (tarif 2)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_2], ["Power Consumption Phase L1", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE], ["Power Consumption Phase L2", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE], ["Power Consumption Phase L3", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE], ["Power Production Phase L1", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE], ["Power Production Phase L2", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE], ["Power Production Phase L3", obis_ref.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE], + ["Short Power Failure Count", obis_ref.SHORT_POWER_FAILURE_COUNT], ["Long Power Failure Count", obis_ref.LONG_POWER_FAILURE_COUNT], ["Voltage Sags Phase L1", obis_ref.VOLTAGE_SAG_L1_COUNT], ["Voltage Sags Phase L2", obis_ref.VOLTAGE_SAG_L2_COUNT], @@ -83,6 +80,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ["Voltage Phase L1", obis_ref.INSTANTANEOUS_VOLTAGE_L1], ["Voltage Phase L2", obis_ref.INSTANTANEOUS_VOLTAGE_L2], ["Voltage Phase L3", obis_ref.INSTANTANEOUS_VOLTAGE_L3], + ["Current Phase L1", obis_ref.INSTANTANEOUS_CURRENT_L1], + ["Current Phase L2", obis_ref.INSTANTANEOUS_CURRENT_L2], + ["Current Phase L3", obis_ref.INSTANTANEOUS_CURRENT_L3], ] # Generate device entities @@ -91,6 +91,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # Protocol version specific obis if dsmr_version in ("4", "5"): gas_obis = obis_ref.HOURLY_GAS_METER_READING + elif dsmr_version in ("5B"): + gas_obis = obis_ref.BELGIUM_HOURLY_GAS_METER_READING else: gas_obis = obis_ref.GAS_METER_READING @@ -214,7 +216,7 @@ class DSMREntity(Entity): value = self.get_dsmr_object_attr("value") if self._obis == obis_ref.ELECTRICITY_ACTIVE_TARIFF: - return self.translate_tariff(value) + return self.translate_tariff(value, self._config[CONF_DSMR_VERSION]) try: value = round(float(value), self._config[CONF_PRECISION]) @@ -232,8 +234,15 @@ class DSMREntity(Entity): return self.get_dsmr_object_attr("unit") @staticmethod - def translate_tariff(value): - """Convert 2/1 to normal/low.""" + def translate_tariff(value, dsmr_version): + """Convert 2/1 to normal/low depending on DSMR version.""" + # DSMR V5B: Note: In Belgium values are swapped: + # Rate code 2 is used for low rate and rate code 1 is used for normal rate. + if dsmr_version in ("5B"): + if value == "0001": + value = "0002" + elif value == "0002": + value = "0001" # DSMR V2.2: Note: Rate code 1 is used for low rate and rate code 2 is # used for normal rate. if value == "0002": diff --git a/homeassistant/components/duke_energy/__init__.py b/homeassistant/components/duke_energy/__init__.py deleted file mode 100644 index 5a1f29add43..00000000000 --- a/homeassistant/components/duke_energy/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The duke_energy component.""" diff --git a/homeassistant/components/duke_energy/manifest.json b/homeassistant/components/duke_energy/manifest.json deleted file mode 100644 index cebbf45df11..00000000000 --- a/homeassistant/components/duke_energy/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "duke_energy", - "name": "Duke Energy", - "documentation": "https://www.home-assistant.io/integrations/duke_energy", - "requirements": ["pydukeenergy==0.0.6"], - "dependencies": [], - "codeowners": [] -} diff --git a/homeassistant/components/duke_energy/sensor.py b/homeassistant/components/duke_energy/sensor.py deleted file mode 100644 index cd30ae96caf..00000000000 --- a/homeassistant/components/duke_energy/sensor.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Support for Duke Energy Gas and Electric meters.""" -import logging - -from pydukeenergy.api import DukeEnergy, DukeEnergyException -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity - -_LOGGER = logging.getLogger(__name__) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} -) - -LAST_BILL_USAGE = "last_bills_usage" -LAST_BILL_AVERAGE_USAGE = "last_bills_average_usage" -LAST_BILL_DAYS_BILLED = "last_bills_days_billed" - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up all Duke Energy meters.""" - - try: - duke = DukeEnergy( - config[CONF_USERNAME], config[CONF_PASSWORD], update_interval=120 - ) - except DukeEnergyException: - _LOGGER.error("Failed to set up Duke Energy") - return - - add_entities([DukeEnergyMeter(meter) for meter in duke.get_meters()]) - - -class DukeEnergyMeter(Entity): - """Representation of a Duke Energy meter.""" - - def __init__(self, meter): - """Initialize the meter.""" - self.duke_meter = meter - - @property - def name(self): - """Return the name.""" - return f"duke_energy_{self.duke_meter.id}" - - @property - def unique_id(self): - """Return the unique ID.""" - return self.duke_meter.id - - @property - def state(self): - """Return yesterdays usage.""" - return self.duke_meter.get_usage() - - @property - def unit_of_measurement(self): - """Return the unit of measurement this sensor expresses itself in.""" - return self.duke_meter.get_unit() - - @property - def device_state_attributes(self): - """Return the state attributes.""" - attributes = { - LAST_BILL_USAGE: self.duke_meter.get_total(), - LAST_BILL_AVERAGE_USAGE: self.duke_meter.get_average(), - LAST_BILL_DAYS_BILLED: self.duke_meter.get_days_billed(), - } - return attributes - - def update(self): - """Update meter.""" - self.duke_meter.update() diff --git a/homeassistant/components/dynalite/__init__.py b/homeassistant/components/dynalite/__init__.py new file mode 100755 index 00000000000..f4fc65b8261 --- /dev/null +++ b/homeassistant/components/dynalite/__init__.py @@ -0,0 +1,138 @@ +"""Support for the Dynalite networks.""" +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import config_validation as cv + +# Loading the config flow file will register the flow +from .bridge import DynaliteBridge +from .const import ( + CONF_ACTIVE, + CONF_AREA, + CONF_AUTO_DISCOVER, + CONF_BRIDGES, + CONF_CHANNEL, + CONF_DEFAULT, + CONF_FADE, + CONF_NAME, + CONF_POLLTIMER, + CONF_PORT, + DEFAULT_NAME, + DEFAULT_PORT, + DOMAIN, + LOGGER, +) + + +def num_string(value): + """Test if value is a string of digits, aka an integer.""" + new_value = str(value) + if new_value.isdigit(): + return new_value + raise vol.Invalid("Not a string with numbers") + + +CHANNEL_DATA_SCHEMA = vol.Schema( + {vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_FADE): vol.Coerce(float)} +) + +CHANNEL_SCHEMA = vol.Schema({num_string: CHANNEL_DATA_SCHEMA}) + +AREA_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_FADE): vol.Coerce(float), + vol.Optional(CONF_CHANNEL): CHANNEL_SCHEMA, + }, +) + +AREA_SCHEMA = vol.Schema({num_string: vol.Any(AREA_DATA_SCHEMA, None)}) + +PLATFORM_DEFAULTS_SCHEMA = vol.Schema({vol.Optional(CONF_FADE): vol.Coerce(float)}) + + +BRIDGE_SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): int, + vol.Optional(CONF_AUTO_DISCOVER, default=False): vol.Coerce(bool), + vol.Optional(CONF_POLLTIMER, default=1.0): vol.Coerce(float), + vol.Optional(CONF_AREA): AREA_SCHEMA, + vol.Optional(CONF_DEFAULT): PLATFORM_DEFAULTS_SCHEMA, + vol.Optional(CONF_ACTIVE, default=False): vol.Coerce(bool), + } +) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + {vol.Optional(CONF_BRIDGES): vol.All(cv.ensure_list, [BRIDGE_SCHEMA])} + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Set up the Dynalite platform.""" + + conf = config.get(DOMAIN) + LOGGER.debug("Setting up dynalite component config = %s", conf) + + if conf is None: + conf = {} + + hass.data[DOMAIN] = {} + + # User has configured bridges + if CONF_BRIDGES not in conf: + return True + + bridges = conf[CONF_BRIDGES] + + for bridge_conf in bridges: + host = bridge_conf[CONF_HOST] + LOGGER.debug("Starting config entry flow host=%s conf=%s", host, bridge_conf) + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=bridge_conf, + ) + ) + + return True + + +async def async_setup_entry(hass, entry): + """Set up a bridge from a config entry.""" + LOGGER.debug("Setting up entry %s", entry.data) + + bridge = DynaliteBridge(hass, entry.data) + + if not await bridge.async_setup(): + LOGGER.error("Could not set up bridge for entry %s", entry.data) + return False + + if not await bridge.try_connection(): + LOGGER.errot("Could not connect with entry %s", entry) + raise ConfigEntryNotReady + + hass.data[DOMAIN][entry.entry_id] = bridge + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "light") + ) + return True + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + LOGGER.debug("Unloading entry %s", entry.data) + hass.data[DOMAIN].pop(entry.entry_id) + result = await hass.config_entries.async_forward_entry_unload(entry, "light") + return result diff --git a/homeassistant/components/dynalite/bridge.py b/homeassistant/components/dynalite/bridge.py new file mode 100755 index 00000000000..cbe08fdadb5 --- /dev/null +++ b/homeassistant/components/dynalite/bridge.py @@ -0,0 +1,82 @@ +"""Code to handle a Dynalite bridge.""" + +import asyncio + +from dynalite_devices_lib import DynaliteDevices + +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from .const import CONF_ALL, CONF_HOST, LOGGER + +CONNECT_TIMEOUT = 30 +CONNECT_INTERVAL = 1 + + +class DynaliteBridge: + """Manages a single Dynalite bridge.""" + + def __init__(self, hass, config): + """Initialize the system based on host parameter.""" + self.hass = hass + self.area = {} + self.async_add_devices = None + self.waiting_devices = [] + self.host = config[CONF_HOST] + # Configure the dynalite devices + self.dynalite_devices = DynaliteDevices( + config=config, + newDeviceFunc=self.add_devices_when_registered, + updateDeviceFunc=self.update_device, + ) + + async def async_setup(self): + """Set up a Dynalite bridge.""" + # Configure the dynalite devices + return await self.dynalite_devices.async_setup() + + def update_signal(self, device=None): + """Create signal to use to trigger entity update.""" + if device: + signal = f"dynalite-update-{self.host}-{device.unique_id}" + else: + signal = f"dynalite-update-{self.host}" + return signal + + @callback + def update_device(self, device): + """Call when a device or all devices should be updated.""" + if device == CONF_ALL: + # This is used to signal connection or disconnection, so all devices may become available or not. + log_string = ( + "Connected" if self.dynalite_devices.available else "Disconnected" + ) + LOGGER.info("%s to dynalite host", log_string) + async_dispatcher_send(self.hass, self.update_signal()) + else: + async_dispatcher_send(self.hass, self.update_signal(device)) + + async def try_connection(self): + """Try to connect to dynalite with timeout.""" + # Currently by polling. Future - will need to change the library to be proactive + for _ in range(0, CONNECT_TIMEOUT): + if self.dynalite_devices.available: + return True + await asyncio.sleep(CONNECT_INTERVAL) + return False + + @callback + def register_add_devices(self, async_add_devices): + """Add an async_add_entities for a category.""" + self.async_add_devices = async_add_devices + if self.waiting_devices: + self.async_add_devices(self.waiting_devices) + + def add_devices_when_registered(self, devices): + """Add the devices to HA if the add devices callback was registered, otherwise queue until it is.""" + if not devices: + return + if self.async_add_devices: + self.async_add_devices(devices) + else: # handle it later when it is registered + self.waiting_devices.extend(devices) diff --git a/homeassistant/components/dynalite/config_flow.py b/homeassistant/components/dynalite/config_flow.py new file mode 100755 index 00000000000..aac42172181 --- /dev/null +++ b/homeassistant/components/dynalite/config_flow.py @@ -0,0 +1,35 @@ +"""Config flow to configure Dynalite hub.""" +from homeassistant import config_entries +from homeassistant.const import CONF_HOST + +from .bridge import DynaliteBridge +from .const import DOMAIN, LOGGER # pylint: disable=unused-import + + +class DynaliteFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Dynalite config flow.""" + + 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 Dynalite flow.""" + self.host = None + + async def async_step_import(self, import_info): + """Import a new bridge as a config entry.""" + LOGGER.debug("Starting async_step_import - %s", import_info) + host = import_info[CONF_HOST] + await self.async_set_unique_id(host) + self._abort_if_unique_id_configured(import_info) + # New entry + bridge = DynaliteBridge(self.hass, import_info) + if not await bridge.async_setup(): + LOGGER.error("Unable to setup bridge - import info=%s", import_info) + return self.async_abort(reason="bridge_setup_failed") + if not await bridge.try_connection(): + return self.async_abort(reason="no_connection") + LOGGER.debug("Creating entry for the bridge - %s", import_info) + return self.async_create_entry(title=host, data=import_info) diff --git a/homeassistant/components/dynalite/const.py b/homeassistant/components/dynalite/const.py new file mode 100755 index 00000000000..f7795554465 --- /dev/null +++ b/homeassistant/components/dynalite/const.py @@ -0,0 +1,21 @@ +"""Constants for the Dynalite component.""" +import logging + +LOGGER = logging.getLogger(__package__) +DOMAIN = "dynalite" + +CONF_ACTIVE = "active" +CONF_ALL = "ALL" +CONF_AREA = "area" +CONF_AUTO_DISCOVER = "autodiscover" +CONF_BRIDGES = "bridges" +CONF_CHANNEL = "channel" +CONF_DEFAULT = "default" +CONF_FADE = "fade" +CONF_HOST = "host" +CONF_NAME = "name" +CONF_POLLTIMER = "polltimer" +CONF_PORT = "port" + +DEFAULT_NAME = "dynalite" +DEFAULT_PORT = 12345 diff --git a/homeassistant/components/dynalite/light.py b/homeassistant/components/dynalite/light.py new file mode 100755 index 00000000000..652a6178705 --- /dev/null +++ b/homeassistant/components/dynalite/light.py @@ -0,0 +1,96 @@ +"""Support for Dynalite channels as lights.""" +from homeassistant.components.light import SUPPORT_BRIGHTNESS, Light +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DOMAIN, LOGGER + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Record the async_add_entities function to add them later when received from Dynalite.""" + LOGGER.debug("Setting up light entry = %s", config_entry.data) + bridge = hass.data[DOMAIN][config_entry.entry_id] + + @callback + def async_add_lights(devices): + added_lights = [] + for device in devices: + if device.category == "light": + added_lights.append(DynaliteLight(device, bridge)) + if added_lights: + async_add_entities(added_lights) + + bridge.register_add_devices(async_add_lights) + + +class DynaliteLight(Light): + """Representation of a Dynalite Channel as a Home Assistant Light.""" + + def __init__(self, device, bridge): + """Initialize the base class.""" + self._device = device + self._bridge = bridge + + @property + def name(self): + """Return the name of the entity.""" + return self._device.name + + @property + def unique_id(self): + """Return the unique ID of the entity.""" + return self._device.unique_id + + @property + def available(self): + """Return if entity is available.""" + return self._device.available + + async def async_update(self): + """Update the entity.""" + return + + @property + def device_info(self): + """Device info for this entity.""" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": "Dynalite", + } + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return self._device.brightness + + @property + def is_on(self): + """Return true if device is on.""" + return self._device.is_on + + async def async_turn_on(self, **kwargs): + """Turn the light on.""" + await self._device.async_turn_on(**kwargs) + + async def async_turn_off(self, **kwargs): + """Turn the light off.""" + await self._device.async_turn_off(**kwargs) + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS + + async def async_added_to_hass(self): + """Added to hass so need to register to dispatch.""" + # register for device specific update + async_dispatcher_connect( + self.hass, + self._bridge.update_signal(self._device), + self.async_schedule_update_ha_state, + ) + # register for wide update + async_dispatcher_connect( + self.hass, self._bridge.update_signal(), self.async_schedule_update_ha_state + ) diff --git a/homeassistant/components/dynalite/manifest.json b/homeassistant/components/dynalite/manifest.json new file mode 100755 index 00000000000..95667733d38 --- /dev/null +++ b/homeassistant/components/dynalite/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "dynalite", + "name": "Philips Dynalite", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/dynalite", + "dependencies": [], + "codeowners": ["@ziv1234"], + "requirements": ["dynalite_devices==0.1.22"] +} diff --git a/homeassistant/components/ebox/sensor.py b/homeassistant/components/ebox/sensor.py index 55504e8edf7..54355ed3bb8 100644 --- a/homeassistant/components/ebox/sensor.py +++ b/homeassistant/components/ebox/sensor.py @@ -19,6 +19,7 @@ from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, CONF_USERNAME, + DATA_GIGABITS, ) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -27,7 +28,6 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -GIGABITS = "Gb" PRICE = "CAD" DAYS = "days" PERCENT = "%" @@ -41,17 +41,21 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) SENSOR_TYPES = { "usage": ["Usage", PERCENT, "mdi:percent"], "balance": ["Balance", PRICE, "mdi:square-inc-cash"], - "limit": ["Data limit", GIGABITS, "mdi:download"], + "limit": ["Data limit", DATA_GIGABITS, "mdi:download"], "days_left": ["Days left", DAYS, "mdi:calendar-today"], - "before_offpeak_download": ["Download before offpeak", GIGABITS, "mdi:download"], - "before_offpeak_upload": ["Upload before offpeak", GIGABITS, "mdi:upload"], - "before_offpeak_total": ["Total before offpeak", GIGABITS, "mdi:download"], - "offpeak_download": ["Offpeak download", GIGABITS, "mdi:download"], - "offpeak_upload": ["Offpeak Upload", GIGABITS, "mdi:upload"], - "offpeak_total": ["Offpeak Total", GIGABITS, "mdi:download"], - "download": ["Download", GIGABITS, "mdi:download"], - "upload": ["Upload", GIGABITS, "mdi:upload"], - "total": ["Total", GIGABITS, "mdi:download"], + "before_offpeak_download": [ + "Download before offpeak", + DATA_GIGABITS, + "mdi:download", + ], + "before_offpeak_upload": ["Upload before offpeak", DATA_GIGABITS, "mdi:upload"], + "before_offpeak_total": ["Total before offpeak", DATA_GIGABITS, "mdi:download"], + "offpeak_download": ["Offpeak download", DATA_GIGABITS, "mdi:download"], + "offpeak_upload": ["Offpeak Upload", DATA_GIGABITS, "mdi:upload"], + "offpeak_total": ["Offpeak Total", DATA_GIGABITS, "mdi:download"], + "download": ["Download", DATA_GIGABITS, "mdi:download"], + "upload": ["Upload", DATA_GIGABITS, "mdi:upload"], + "total": ["Total", DATA_GIGABITS, "mdi:download"], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/ecobee/.translations/es-419.json b/homeassistant/components/ecobee/.translations/es-419.json new file mode 100644 index 00000000000..3e19977f10f --- /dev/null +++ b/homeassistant/components/ecobee/.translations/es-419.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "one_instance_only": "Esta integraci\u00f3n actualmente solo admite una instancia de ecobee." + }, + "error": { + "pin_request_failed": "Error al solicitar PIN de ecobee; verifique que la clave API sea correcta.", + "token_request_failed": "Error al solicitar tokens de ecobee; Int\u00e9ntelo de nuevo." + }, + "step": { + "authorize": { + "description": "Autorice esta aplicaci\u00f3n en https://www.ecobee.com/consumerportal/index.html con c\u00f3digo PIN: \n\n {pin} \n \n Luego, presione Enviar." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/hu.json b/homeassistant/components/ecobee/.translations/hu.json new file mode 100644 index 00000000000..0950d52bd0e --- /dev/null +++ b/homeassistant/components/ecobee/.translations/hu.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "Ez az integr\u00e1ci\u00f3 jelenleg csak egy ecobee p\u00e9ld\u00e1nyt t\u00e1mogat." + }, + "error": { + "pin_request_failed": "Hiba t\u00f6rt\u00e9nt a PIN-k\u00f3d ecobee-t\u0151l t\u00f6rt\u00e9n\u0151 k\u00e9r\u00e9sekor; ellen\u0151rizze, hogy az API-kulcs helyes-e.", + "token_request_failed": "Hiba t\u00f6rt\u00e9nt a tokenek ecobee-t\u0151l t\u00f6rt\u00e9n\u0151 ig\u00e9nyl\u00e9se k\u00f6zben; pr\u00f3b\u00e1lkozzon \u00fajra." + }, + "step": { + "authorize": { + "description": "K\u00e9rj\u00fck, enged\u00e9lyezze ezt az alkalmaz\u00e1st a https://www.ecobee.com/consumerportal/index.html c\u00edmen a k\u00f6vetkez\u0151 PIN-k\u00f3ddal: \n\n {pin} \n \n Ezut\u00e1n nyomja meg a K\u00fcld\u00e9s gombot.", + "title": "Alkalmaz\u00e1s enged\u00e9lyez\u00e9se ecobee.com-on" + }, + "user": { + "data": { + "api_key": "API kulcs" + }, + "description": "Adja meg az ecobee.com webhelyr\u0151l beszerzett API-kulcsot.", + "title": "ecobee API kulcs" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/sv.json b/homeassistant/components/ecobee/.translations/sv.json index f4a63bb449d..da62172dc10 100644 --- a/homeassistant/components/ecobee/.translations/sv.json +++ b/homeassistant/components/ecobee/.translations/sv.json @@ -1,11 +1,25 @@ { "config": { + "abort": { + "one_instance_only": "Denna integration st\u00f6der f\u00f6r n\u00e4rvarande endast en ecobee-instans." + }, + "error": { + "pin_request_failed": "Fel vid beg\u00e4ran av PIN-kod fr\u00e5n ecobee. kontrollera API-nyckeln \u00e4r korrekt.", + "token_request_failed": "Fel vid beg\u00e4ran av tokens fr\u00e5n ecobee; v\u00e4nligen f\u00f6rs\u00f6k igen." + }, "step": { + "authorize": { + "description": "V\u00e4nligen auktorisera denna app p\u00e5 https://www.ecobee.com/consumerportal/index.html med pin-kod:\n\n{pin}\n\nTryck sedan p\u00e5 Skicka.", + "title": "Auktorisera app p\u00e5 ecobee.com" + }, "user": { "data": { "api_key": "API-nyckel" - } + }, + "description": "V\u00e4nligen ange API-nyckeln som erh\u00e5llits fr\u00e5n ecobee.com.", + "title": "ecobee API-nyckel" } - } + }, + "title": "ecobee" } } \ No newline at end of file diff --git a/homeassistant/components/ecobee/__init__.py b/homeassistant/components/ecobee/__init__.py index 80c3be7954b..26bfbe5b3da 100644 --- a/homeassistant/components/ecobee/__init__.py +++ b/homeassistant/components/ecobee/__init__.py @@ -96,9 +96,7 @@ class EcobeeData: await self._hass.async_add_executor_job(self.ecobee.update) _LOGGER.debug("Updating ecobee") except ExpiredTokenError: - _LOGGER.warning( - "Ecobee update failed; attempting to refresh expired tokens" - ) + _LOGGER.debug("Refreshing expired ecobee tokens") await self.refresh() async def refresh(self) -> bool: @@ -113,7 +111,7 @@ class EcobeeData: }, ) return True - _LOGGER.error("Error updating ecobee tokens") + _LOGGER.error("Error refreshing ecobee tokens") return False diff --git a/homeassistant/components/ecobee/config_flow.py b/homeassistant/components/ecobee/config_flow.py index bb406d81e3a..cbe16832a34 100644 --- a/homeassistant/components/ecobee/config_flow.py +++ b/homeassistant/components/ecobee/config_flow.py @@ -107,7 +107,7 @@ class EcobeeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if await self.hass.async_add_executor_job(ecobee.refresh_tokens): # Credentials found and validated; create the entry. _LOGGER.debug( - "Valid ecobee configuration found for import, creating config entry" + "Valid ecobee configuration found for import, creating configuration entry" ) return self.async_create_entry( title=DOMAIN, diff --git a/homeassistant/components/ecobee/manifest.json b/homeassistant/components/ecobee/manifest.json index 32b58964926..8e21b9931cd 100644 --- a/homeassistant/components/ecobee/manifest.json +++ b/homeassistant/components/ecobee/manifest.json @@ -1,9 +1,9 @@ { "domain": "ecobee", - "name": "Ecobee", + "name": "ecobee", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ecobee", "dependencies": [], - "requirements": ["python-ecobee-api==0.1.4"], + "requirements": ["python-ecobee-api==0.2.1"], "codeowners": ["@marthoc"] } diff --git a/homeassistant/components/eddystone_temperature/sensor.py b/homeassistant/components/eddystone_temperature/sensor.py index 22d3533d32f..3b5aa95701f 100644 --- a/homeassistant/components/eddystone_temperature/sensor.py +++ b/homeassistant/components/eddystone_temperature/sensor.py @@ -91,7 +91,7 @@ def get_from_conf(config, config_key, length): string = config.get(config_key) if len(string) != length: _LOGGER.error( - "Error in config parameter %s: Must be exactly %d " + "Error in configuration parameter %s: Must be exactly %d " "bytes. Device will not be added", config_key, length / 2, diff --git a/homeassistant/components/edimax/manifest.json b/homeassistant/components/edimax/manifest.json index 20036311592..de8b978b9f9 100644 --- a/homeassistant/components/edimax/manifest.json +++ b/homeassistant/components/edimax/manifest.json @@ -2,7 +2,7 @@ "domain": "edimax", "name": "Edimax", "documentation": "https://www.home-assistant.io/integrations/edimax", - "requirements": ["pyedimax==0.1"], + "requirements": ["pyedimax==0.2.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/edimax/switch.py b/homeassistant/components/edimax/switch.py index 3d558f6c770..e44ec23bca7 100644 --- a/homeassistant/components/edimax/switch.py +++ b/homeassistant/components/edimax/switch.py @@ -10,6 +10,8 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) +DOMAIN = "edimax" + DEFAULT_NAME = "Edimax Smart Plug" DEFAULT_PASSWORD = "1234" DEFAULT_USERNAME = "admin" @@ -30,7 +32,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): auth = (config.get(CONF_USERNAME), config.get(CONF_PASSWORD)) name = config.get(CONF_NAME) - add_entities([SmartPlugSwitch(SmartPlug(host, auth), name)]) + add_entities([SmartPlugSwitch(SmartPlug(host, auth), name)], True) class SmartPlugSwitch(SwitchDevice): @@ -43,6 +45,14 @@ class SmartPlugSwitch(SwitchDevice): self._now_power = None self._now_energy_day = None self._state = False + self._supports_power_monitoring = False + self._info = None + self._mac = None + + @property + def unique_id(self): + """Return the device's MAC address.""" + return self._mac @property def name(self): @@ -74,14 +84,20 @@ class SmartPlugSwitch(SwitchDevice): def update(self): """Update edimax switch.""" - try: - self._now_power = float(self.smartplug.now_power) - except (TypeError, ValueError): - self._now_power = None + if not self._info: + self._info = self.smartplug.info + self._mac = self._info["mac"] + self._supports_power_monitoring = self._info["model"] != "SP1101W" - try: - self._now_energy_day = float(self.smartplug.now_energy_day) - except (TypeError, ValueError): - self._now_energy_day = None + if self._supports_power_monitoring: + try: + self._now_power = float(self.smartplug.now_power) + except (TypeError, ValueError): + self._now_power = None + + try: + self._now_energy_day = float(self.smartplug.now_energy_day) + except (TypeError, ValueError): + self._now_energy_day = None self._state = self.smartplug.state == "ON" diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py index a8a5a6e1fcc..595144013b6 100644 --- a/homeassistant/components/eight_sleep/__init__.py +++ b/homeassistant/components/eight_sleep/__init__.py @@ -43,12 +43,14 @@ SIGNAL_UPDATE_USER = "eight_user_update" NAME_MAP = { "left_current_sleep": "Left Sleep Session", + "left_current_sleep_fitness": "Left Sleep Fitness", "left_last_sleep": "Left Previous Sleep Session", "left_bed_state": "Left Bed State", "left_presence": "Left Bed Presence", "left_bed_temp": "Left Bed Temperature", "left_sleep_stage": "Left Sleep Stage", "right_current_sleep": "Right Sleep Session", + "right_current_sleep_fitness": "Right Sleep Fitness", "right_last_sleep": "Right Previous Sleep Session", "right_bed_state": "Right Bed State", "right_presence": "Right Bed Presence", @@ -57,14 +59,21 @@ NAME_MAP = { "room_temp": "Room Temperature", } -SENSORS = ["current_sleep", "last_sleep", "bed_state", "bed_temp", "sleep_stage"] +SENSORS = [ + "current_sleep", + "current_sleep_fitness", + "last_sleep", + "bed_state", + "bed_temp", + "sleep_stage", +] SERVICE_HEAT_SET = "heat_set" ATTR_TARGET_HEAT = "target" ATTR_HEAT_DURATION = "duration" -VALID_TARGET_HEAT = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=100)) +VALID_TARGET_HEAT = vol.All(vol.Coerce(int), vol.Clamp(min=-100, max=100)) VALID_DURATION = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=28800)) SERVICE_EIGHT_SCHEMA = vol.Schema( @@ -101,7 +110,7 @@ async def async_setup(hass, config): _LOGGER.error("Timezone is not set in Home Assistant.") return False - timezone = hass.config.time_zone + timezone = str(hass.config.time_zone) eight = EightSleep(user, password, timezone, partner, None, hass.loop) diff --git a/homeassistant/components/eight_sleep/manifest.json b/homeassistant/components/eight_sleep/manifest.json index 75998e71e5f..6372967b42b 100644 --- a/homeassistant/components/eight_sleep/manifest.json +++ b/homeassistant/components/eight_sleep/manifest.json @@ -2,7 +2,7 @@ "domain": "eight_sleep", "name": "Eight Sleep", "documentation": "https://www.home-assistant.io/integrations/eight_sleep", - "requirements": ["pyeight==0.1.2"], + "requirements": ["pyeight==0.1.3"], "dependencies": [], "codeowners": ["@mezz64"] } diff --git a/homeassistant/components/eight_sleep/sensor.py b/homeassistant/components/eight_sleep/sensor.py index d3d54fd58ca..af6de2657ce 100644 --- a/homeassistant/components/eight_sleep/sensor.py +++ b/homeassistant/components/eight_sleep/sensor.py @@ -28,6 +28,11 @@ ATTR_ACTIVE_HEAT = "Heating Active" ATTR_DURATION_HEAT = "Heating Time Remaining" ATTR_PROCESSING = "Processing" ATTR_SESSION_START = "Session Start" +ATTR_FIT_DATE = "Fitness Date" +ATTR_FIT_DURATION_SCORE = "Fitness Duration Score" +ATTR_FIT_ASLEEP_SCORE = "Fitness Asleep Score" +ATTR_FIT_OUT_SCORE = "Fitness Out-of-Bed Score" +ATTR_FIT_WAKEUP_SCORE = "Fitness Wakeup Score" _LOGGER = logging.getLogger(__name__) @@ -151,7 +156,11 @@ class EightUserSensor(EightSleepUserEntity): @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" - if "current_sleep" in self._sensor or "last_sleep" in self._sensor: + if ( + "current_sleep" in self._sensor + or "last_sleep" in self._sensor + or "current_sleep_fitness" in self._sensor + ): return "Score" if "bed_temp" in self._sensor: if self._units == "si": @@ -169,8 +178,12 @@ class EightUserSensor(EightSleepUserEntity): """Retrieve latest state.""" _LOGGER.debug("Updating User sensor: %s", self._sensor) if "current" in self._sensor: - self._state = self._usrobj.current_sleep_score - self._attr = self._usrobj.current_values + if "fitness" in self._sensor: + self._state = self._usrobj.current_sleep_fitness_score + self._attr = self._usrobj.current_fitness_values + else: + self._state = self._usrobj.current_sleep_score + self._attr = self._usrobj.current_values elif "last" in self._sensor: self._state = self._usrobj.last_sleep_score self._attr = self._usrobj.last_values @@ -193,6 +206,16 @@ class EightUserSensor(EightSleepUserEntity): # Skip attributes if sensor type doesn't support return None + if "fitness" in self._sensor_root: + state_attr = { + ATTR_FIT_DATE: self._attr["date"], + ATTR_FIT_DURATION_SCORE: self._attr["duration"], + ATTR_FIT_ASLEEP_SCORE: self._attr["asleep"], + ATTR_FIT_OUT_SCORE: self._attr["out"], + ATTR_FIT_WAKEUP_SCORE: self._attr["wakeup"], + } + return state_attr + state_attr = {ATTR_SESSION_START: self._attr["date"]} state_attr[ATTR_TNT] = self._attr["tnt"] state_attr[ATTR_PROCESSING] = self._attr["processing"] diff --git a/homeassistant/components/eight_sleep/services.yaml b/homeassistant/components/eight_sleep/services.yaml index db7690730dd..49a3c67d604 100644 --- a/homeassistant/components/eight_sleep/services.yaml +++ b/homeassistant/components/eight_sleep/services.yaml @@ -1,6 +1,6 @@ heat_set: - description: Set heating level for eight sleep. + description: Set heating/cooling level for eight sleep. fields: - duration: {description: Duration to heat at the target level in seconds., example: 3600} + duration: {description: Duration to heat/cool at the target level in seconds., example: 3600} entity_id: {description: Entity id of the bed state to adjust., example: sensor.eight_left_bed_state} - target: {description: Target heating level from 0-100., example: 35} + target: {description: Target cooling/heating level from -100 to 100., example: 35} diff --git a/homeassistant/components/elgato/.translations/es-419.json b/homeassistant/components/elgato/.translations/es-419.json new file mode 100644 index 00000000000..2653060030a --- /dev/null +++ b/homeassistant/components/elgato/.translations/es-419.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Host o direcci\u00f3n IP", + "port": "N\u00famero de puerto" + }, + "description": "Configure su Elgato Key Light para integrarse con Home Assistant." + }, + "zeroconf_confirm": { + "title": "Dispositivo Elgato Key Light descubierto" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/hu.json b/homeassistant/components/elgato/.translations/hu.json new file mode 100644 index 00000000000..d3618d0039d --- /dev/null +++ b/homeassistant/components/elgato/.translations/hu.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Ez az Elgato Key Light eszk\u00f6z m\u00e1r konfigur\u00e1lva van.", + "connection_error": "Nem siker\u00fclt csatlakozni az Elgato Key Light eszk\u00f6zh\u00f6z." + }, + "step": { + "user": { + "data": { + "host": "Hosztn\u00e9v vagy IP c\u00edm", + "port": "Portsz\u00e1m" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/sv.json b/homeassistant/components/elgato/.translations/sv.json new file mode 100644 index 00000000000..83850c186c7 --- /dev/null +++ b/homeassistant/components/elgato/.translations/sv.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Den h\u00e4r Elgato Key Light-enheten \u00e4r redan konfigurerad.", + "connection_error": "Det gick inte att ansluta till Elgato Key Light-enheten." + }, + "error": { + "connection_error": "Det gick inte att ansluta till Elgato Key Light-enheten." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "V\u00e4rd eller IP-adress", + "port": "Portnummer" + }, + "description": "St\u00e4ll in ditt Elgato Key Light f\u00f6r att integrera med Home Assistant.", + "title": "L\u00e4nk din Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "Vill du l\u00e4gga till Elgato Key Light med serienummer `{serial_number}` till Home Assistant?", + "title": "Uppt\u00e4ckte Elgato Key Light-enhet" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/config_flow.py b/homeassistant/components/elgato/config_flow.py index a8a81734999..2f3e05fd720 100644 --- a/homeassistant/components/elgato/config_flow.py +++ b/homeassistant/components/elgato/config_flow.py @@ -7,8 +7,8 @@ import voluptuous as vol from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow from homeassistant.const import CONF_HOST, CONF_PORT -from homeassistant.helpers import ConfigType from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import ConfigType from .const import CONF_SERIAL_NUMBER, DOMAIN # pylint: disable=unused-import diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index 34063e4c253..f956d3a7295 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -246,7 +246,7 @@ class EmonCmsData: self.data = req.json() else: _LOGGER.error( - "Please verify if the specified config value " + "Please verify if the specified configuration value " "'%s' is correct! (HTTP Status_code = %d)", CONF_URL, req.status_code, diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 56e76b1d499..e9e7114074a 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -718,7 +718,7 @@ def create_hue_success_response(entity_id, attr, value): def create_list_of_entities(config, request): - """Create a list of all entites.""" + """Create a list of all entities.""" hass = request.app["hass"] json_response = {} diff --git a/homeassistant/components/emulated_roku/.translations/pl.json b/homeassistant/components/emulated_roku/.translations/pl.json index 0ed3cc3d14a..0dd32f66c9f 100644 --- a/homeassistant/components/emulated_roku/.translations/pl.json +++ b/homeassistant/components/emulated_roku/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "name_exists": "Nazwa ju\u017c istnieje" + "name_exists": "Nazwa ju\u017c istnieje." }, "step": { "user": { diff --git a/homeassistant/components/emulated_roku/binding.py b/homeassistant/components/emulated_roku/binding.py index a44effff55a..1d233c9ed81 100644 --- a/homeassistant/components/emulated_roku/binding.py +++ b/homeassistant/components/emulated_roku/binding.py @@ -109,7 +109,7 @@ class EmulatedRoku: ) LOGGER.debug( - "Intializing emulated_roku %s on %s:%s", + "Initializing emulated_roku %s on %s:%s", self.roku_usn, self.host_ip, self.listen_port, diff --git a/homeassistant/components/esphome/.translations/pl.json b/homeassistant/components/esphome/.translations/pl.json index 9394b5af543..ebd201b5550 100644 --- a/homeassistant/components/esphome/.translations/pl.json +++ b/homeassistant/components/esphome/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "ESP jest ju\u017c skonfigurowane" + "already_configured": "ESP jest ju\u017c skonfigurowane." }, "error": { "connection_error": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z ESP. Upewnij si\u0119, \u017ce Tw\u00f3j plik YAML zawiera lini\u0119 'api:'.", diff --git a/homeassistant/components/esphome/.translations/zh-Hans.json b/homeassistant/components/esphome/.translations/zh-Hans.json index 46790868aba..4839167785d 100644 --- a/homeassistant/components/esphome/.translations/zh-Hans.json +++ b/homeassistant/components/esphome/.translations/zh-Hans.json @@ -8,6 +8,7 @@ "invalid_password": "\u65e0\u6548\u7684\u5bc6\u7801\uff01", "resolve_error": "\u65e0\u6cd5\u89e3\u6790 ESP \u7684\u5730\u5740\u3002\u5982\u679c\u6b64\u9519\u8bef\u6301\u7eed\u5b58\u5728\uff0c\u8bf7\u8bbe\u7f6e\u9759\u6001IP\u5730\u5740\uff1ahttps://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, + "flow_title": "ESPHome: {name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 53289799b43..17d3ed5f659 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -5,8 +5,8 @@ from typing import Optional from aioesphomeapi import APIClient, APIConnectionError import voluptuous as vol -from homeassistant import config_entries -from homeassistant.helpers import ConfigType +from homeassistant import config_entries, core +from homeassistant.helpers.typing import ConfigType from .entry_data import DATA_KEY, RuntimeEntryData @@ -115,6 +115,7 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): return await self.async_step_discovery_confirm() + @core.callback def _async_get_entry(self): return self.async_create_entry( title=self._name, diff --git a/homeassistant/components/essent/sensor.py b/homeassistant/components/essent/sensor.py index b106d9d2ae6..e3ce1ccaafa 100644 --- a/homeassistant/components/essent/sensor.py +++ b/homeassistant/components/essent/sensor.py @@ -1,5 +1,6 @@ """Support for Essent API.""" from datetime import timedelta +from typing import Optional from pyessent import PyEssent import voluptuous as vol @@ -73,7 +74,7 @@ class EssentBase: def update(self): """Retrieve the latest meter data from Essent.""" essent = PyEssent(self._username, self._password) - eans = essent.get_EANs() + eans = set(essent.get_EANs()) for possible_meter in eans: meter_data = essent.read_meter(possible_meter, only_last_meter_reading=True) if meter_data: @@ -92,6 +93,11 @@ class EssentMeter(Entity): self._tariff = tariff self._unit = unit + @property + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return f"{self._meter}-{self._type}-{self._tariff}" + @property def name(self): """Return the name of the sensor.""" diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 949471d64d0..1a408c0a660 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -148,7 +148,7 @@ def _handle_exception(err) -> bool: return False except aiohttp.ClientConnectionError: - # this appears to be a common occurance with the vendor's servers + # this appears to be a common occurrence with the vendor's servers _LOGGER.warning( "Unable to connect with the vendor's server. " "Check your network and the vendor's service status page. " @@ -184,7 +184,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: 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 + # any tokens won't be valid, and store might be be corrupt await store.async_save({}) return ({}, None) @@ -266,7 +266,7 @@ def setup_service_functions(hass: HomeAssistantType, broker): Not all Honeywell TCC-compatible systems support all operating modes. In addition, each mode will require any of four distinct service schemas. This has to be - enumerated before registering the approperiate handlers. + enumerated before registering the appropriate handlers. It appears that all TCC-compatible systems support the same three zones modes. """ diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index b7f6e965a8f..aece0f0ec0d 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -175,7 +175,7 @@ class EvoZone(EvoChild, EvoClimateDevice): if ATTR_DURATION_UNTIL in data: duration = data[ATTR_DURATION_UNTIL] - if duration == 0: + if duration.total_seconds() == 0: await self._update_schedule() until = parse_datetime(str(self.setpoints.get("next_sp_from"))) else: diff --git a/homeassistant/components/fan/.translations/es-419.json b/homeassistant/components/fan/.translations/es-419.json new file mode 100644 index 00000000000..dd0c006d760 --- /dev/null +++ b/homeassistant/components/fan/.translations/es-419.json @@ -0,0 +1,12 @@ +{ + "device_automation": { + "condition_type": { + "is_off": "{entity_name} est\u00e1 apagado", + "is_on": "{entity_name} est\u00e1 encendido" + }, + "trigger_type": { + "turned_off": "{entity_name} se apag\u00f3", + "turned_on": "{entity_name} se encendi\u00f3" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/sv.json b/homeassistant/components/fan/.translations/sv.json new file mode 100644 index 00000000000..c080d1b364b --- /dev/null +++ b/homeassistant/components/fan/.translations/sv.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "St\u00e4ng av {entity_name}", + "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} \u00e4r avst\u00e4ngd", + "is_on": "{entity_name} \u00e4r p\u00e5" + }, + "trigger_type": { + "turned_off": "{entity_name} st\u00e4ngdes av", + "turned_on": "{entity_name} aktiverades" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 38234a8f832..76bd16a6363 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -169,7 +169,7 @@ class FanEntity(ToggleEntity): @property def capability_attributes(self): - """Return capabilitiy attributes.""" + """Return capability attributes.""" return {ATTR_SPEED_LIST: self.speed_list} @property diff --git a/homeassistant/components/fastdotcom/sensor.py b/homeassistant/components/fastdotcom/sensor.py index 6d9445ce159..a6eaa21ae35 100644 --- a/homeassistant/components/fastdotcom/sensor.py +++ b/homeassistant/components/fastdotcom/sensor.py @@ -1,6 +1,7 @@ """Support for Fast.com internet speed testing sensor.""" import logging +from homeassistant.const import DATA_RATE_MEGABITS_PER_SECOND from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.restore_state import RestoreEntity @@ -11,8 +12,6 @@ _LOGGER = logging.getLogger(__name__) ICON = "mdi:speedometer" -UNIT_OF_MEASUREMENT = "Mbit/s" - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Fast.com sensor.""" @@ -41,7 +40,7 @@ class SpeedtestSensor(RestoreEntity): @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" - return UNIT_OF_MEASUREMENT + return DATA_RATE_MEGABITS_PER_SECOND @property def icon(self): diff --git a/homeassistant/components/fibaro/light.py b/homeassistant/components/fibaro/light.py index ba77942a448..38779a05cb0 100644 --- a/homeassistant/components/fibaro/light.py +++ b/homeassistant/components/fibaro/light.py @@ -68,7 +68,7 @@ class FibaroLight(FibaroDevice, Light): supports_dimming = "levelChange" in fibaro_device.interfaces supports_white_v = "setW" in fibaro_device.actions - # Configuration can overrride default capability detection + # Configuration can override default capability detection if devconf.get(CONF_DIMMING, supports_dimming): self._supported_flags |= SUPPORT_BRIGHTNESS if devconf.get(CONF_COLOR, supports_color): diff --git a/homeassistant/components/fido/sensor.py b/homeassistant/components/fido/sensor.py index 086ae87a529..f444abd25ee 100644 --- a/homeassistant/components/fido/sensor.py +++ b/homeassistant/components/fido/sensor.py @@ -20,6 +20,7 @@ from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, CONF_USERNAME, + DATA_KILOBITS, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -27,7 +28,6 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -KILOBITS = "Kb" PRICE = "CAD" MESSAGES = "messages" MINUTES = "minutes" @@ -40,9 +40,9 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) SENSOR_TYPES = { "fido_dollar": ["Fido dollar", PRICE, "mdi:square-inc-cash"], "balance": ["Balance", PRICE, "mdi:square-inc-cash"], - "data_used": ["Data used", KILOBITS, "mdi:download"], - "data_limit": ["Data limit", KILOBITS, "mdi:download"], - "data_remaining": ["Data remaining", KILOBITS, "mdi:download"], + "data_used": ["Data used", DATA_KILOBITS, "mdi:download"], + "data_limit": ["Data limit", DATA_KILOBITS, "mdi:download"], + "data_remaining": ["Data remaining", DATA_KILOBITS, "mdi:download"], "text_used": ["Text used", MESSAGES, "mdi:message-text"], "text_limit": ["Text limit", MESSAGES, "mdi:message-text"], "text_remaining": ["Text remaining", MESSAGES, "mdi:message-text"], diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index 8c6cd30b118..3d96aab04e9 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -6,6 +6,7 @@ import os import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import DATA_MEGABYTES import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -42,7 +43,7 @@ class Filesize(Entity): self._size = None self._last_updated = None self._name = path.split("/")[-1] - self._unit_of_measurement = "MB" + self._unit_of_measurement = DATA_MEGABYTES def update(self): """Update the sensor.""" diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index baa4f90af3f..77622f62b1d 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -377,7 +377,7 @@ class Filter: @property def skip_processing(self): - """Return wether the current filter_state should be skipped.""" + """Return whether the current filter_state should be skipped.""" return self._skip_processing def _filter_state(self, new_state): diff --git a/homeassistant/components/folder/sensor.py b/homeassistant/components/folder/sensor.py index a706ab2a0b5..19a5791d7cb 100644 --- a/homeassistant/components/folder/sensor.py +++ b/homeassistant/components/folder/sensor.py @@ -7,6 +7,7 @@ import os import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import DATA_MEGABYTES import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -63,7 +64,7 @@ class Folder(Entity): self._number_of_files = None self._size = None self._name = os.path.split(os.path.split(folder_path)[0])[1] - self._unit_of_measurement = "MB" + self._unit_of_measurement = DATA_MEGABYTES self._file_list = None def update(self): diff --git a/homeassistant/components/freebox/manifest.json b/homeassistant/components/freebox/manifest.json index 5a29a619a33..7a66490c90d 100644 --- a/homeassistant/components/freebox/manifest.json +++ b/homeassistant/components/freebox/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/freebox", "requirements": ["aiofreepybox==0.0.8"], "dependencies": [], + "after_dependencies": ["discovery"], "codeowners": ["@snoof85"] } diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 61ec670d217..0653120b49c 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -1,6 +1,7 @@ """Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" import logging +from homeassistant.const import DATA_RATE_KILOBYTES_PER_SECOND from homeassistant.helpers.entity import Entity from . import DATA_FREEBOX @@ -56,7 +57,7 @@ class FbxRXSensor(FbxSensor): """Update the Freebox RxSensor.""" _name = "Freebox download speed" - _unit = "KB/s" + _unit = DATA_RATE_KILOBYTES_PER_SECOND _icon = "mdi:download-network" async def async_update(self): @@ -70,7 +71,7 @@ class FbxTXSensor(FbxSensor): """Update the Freebox TxSensor.""" _name = "Freebox upload speed" - _unit = "KB/s" + _unit = DATA_RATE_KILOBYTES_PER_SECOND _icon = "mdi:upload-network" async def async_update(self): diff --git a/homeassistant/components/freebox/switch.py b/homeassistant/components/freebox/switch.py index b6655c9634f..062d6a699fe 100644 --- a/homeassistant/components/freebox/switch.py +++ b/homeassistant/components/freebox/switch.py @@ -18,7 +18,7 @@ class FbxWifiSwitch(SwitchDevice): """Representation of a freebox wifi switch.""" def __init__(self, fbx): - """Initilize the Wifi switch.""" + """Initialize the Wifi switch.""" self._name = "Freebox WiFi" self._state = None self._fbx = fbx diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index 21b86e26af1..5536e8fada3 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -1,6 +1,6 @@ { "domain": "fritz", - "name": "AVM Fritzbox", + "name": "AVM FRITZ!Box", "documentation": "https://www.home-assistant.io/integrations/fritz", "requirements": ["fritzconnection==1.2.0"], "dependencies": [], diff --git a/homeassistant/components/fritzdect/__init__.py b/homeassistant/components/fritzdect/__init__.py deleted file mode 100644 index d64990bc3f0..00000000000 --- a/homeassistant/components/fritzdect/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The fritzdect component.""" diff --git a/homeassistant/components/fritzdect/manifest.json b/homeassistant/components/fritzdect/manifest.json deleted file mode 100644 index 9fc91293608..00000000000 --- a/homeassistant/components/fritzdect/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "fritzdect", - "name": "AVM FRITZ!DECT", - "documentation": "https://www.home-assistant.io/integrations/fritzdect", - "requirements": ["fritzhome==1.0.4"], - "dependencies": [], - "codeowners": [] -} diff --git a/homeassistant/components/fritzdect/switch.py b/homeassistant/components/fritzdect/switch.py deleted file mode 100644 index f67c84ae552..00000000000 --- a/homeassistant/components/fritzdect/switch.py +++ /dev/null @@ -1,224 +0,0 @@ -"""Support for FRITZ!DECT Switches.""" -import logging - -from fritzhome.fritz import FritzBox -from requests.exceptions import HTTPError, RequestException -import voluptuous as vol - -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice -from homeassistant.const import ( - ATTR_TEMPERATURE, - CONF_HOST, - CONF_PASSWORD, - CONF_USERNAME, - ENERGY_KILO_WATT_HOUR, - POWER_WATT, - TEMP_CELSIUS, -) -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -# Standard Fritz Box IP -DEFAULT_HOST = "fritz.box" - -ATTR_CURRENT_CONSUMPTION = "current_consumption" -ATTR_CURRENT_CONSUMPTION_UNIT = "current_consumption_unit" -ATTR_CURRENT_CONSUMPTION_UNIT_VALUE = POWER_WATT - -ATTR_TOTAL_CONSUMPTION = "total_consumption" -ATTR_TOTAL_CONSUMPTION_UNIT = "total_consumption_unit" -ATTR_TOTAL_CONSUMPTION_UNIT_VALUE = ENERGY_KILO_WATT_HOUR - -ATTR_TEMPERATURE_UNIT = "temperature_unit" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - } -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Add all switches connected to Fritz Box.""" - - host = config.get(CONF_HOST) - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - - # Log into Fritz Box - fritz = FritzBox(host, username, password) - try: - fritz.login() - except Exception: # pylint: disable=broad-except - _LOGGER.error("Login to Fritz!Box failed") - return - - # Add all actors to hass - for actor in fritz.get_actors(): - # Only add devices that support switching - if actor.has_switch: - data = FritzDectSwitchData(fritz, actor.actor_id) - data.is_online = True - add_entities([FritzDectSwitch(hass, data, actor.name)], True) - - -class FritzDectSwitch(SwitchDevice): - """Representation of a FRITZ!DECT switch.""" - - def __init__(self, hass, data, name): - """Initialize the switch.""" - self.units = hass.config.units - self.data = data - self._name = name - - @property - def name(self): - """Return the name of the FRITZ!DECT switch, if any.""" - return self._name - - @property - def device_state_attributes(self): - """Return the state attributes of the device.""" - attrs = {} - - if ( - self.data.has_powermeter - and self.data.current_consumption is not None - and self.data.total_consumption is not None - ): - attrs[ATTR_CURRENT_CONSUMPTION] = "{:.1f}".format( - self.data.current_consumption - ) - attrs[ATTR_CURRENT_CONSUMPTION_UNIT] = "{}".format( - ATTR_CURRENT_CONSUMPTION_UNIT_VALUE - ) - attrs[ATTR_TOTAL_CONSUMPTION] = f"{self.data.total_consumption:.3f}" - attrs[ATTR_TOTAL_CONSUMPTION_UNIT] = "{}".format( - ATTR_TOTAL_CONSUMPTION_UNIT_VALUE - ) - - if self.data.has_temperature and self.data.temperature is not None: - attrs[ATTR_TEMPERATURE] = "{}".format( - self.units.temperature(self.data.temperature, TEMP_CELSIUS) - ) - attrs[ATTR_TEMPERATURE_UNIT] = f"{self.units.temperature_unit}" - return attrs - - @property - def current_power_w(self): - """Return the current power usage in Watt.""" - try: - return float(self.data.current_consumption) - except ValueError: - return None - - @property - def is_on(self): - """Return true if switch is on.""" - return self.data.state - - def turn_on(self, **kwargs): - """Turn the switch on.""" - if not self.data.is_online: - _LOGGER.error("turn_on: Not online skipping request") - return - - try: - actor = self.data.fritz.get_actor_by_ain(self.data.ain) - actor.switch_on() - except (RequestException, HTTPError): - _LOGGER.error("Fritz!Box query failed, triggering relogin") - self.data.is_online = False - - def turn_off(self, **kwargs): - """Turn the switch off.""" - if not self.data.is_online: - _LOGGER.error("turn_off: Not online skipping request") - return - - try: - actor = self.data.fritz.get_actor_by_ain(self.data.ain) - actor.switch_off() - except (RequestException, HTTPError): - _LOGGER.error("Fritz!Box query failed, triggering relogin") - self.data.is_online = False - - def update(self): - """Get the latest data from the fritz box and updates the states.""" - if not self.data.is_online: - _LOGGER.error("update: Not online, logging back in") - - try: - self.data.fritz.login() - except Exception: # pylint: disable=broad-except - _LOGGER.error("Login to Fritz!Box failed") - return - - self.data.is_online = True - - try: - self.data.update() - except Exception: # pylint: disable=broad-except - _LOGGER.error("Fritz!Box query failed, triggering relogin") - self.data.is_online = False - - -class FritzDectSwitchData: - """Get the latest data from the fritz box.""" - - def __init__(self, fritz, ain): - """Initialize the data object.""" - self.fritz = fritz - self.ain = ain - self.state = None - self.temperature = None - self.current_consumption = None - self.total_consumption = None - self.has_switch = False - self.has_temperature = False - self.has_powermeter = False - self.is_online = False - - def update(self): - """Get the latest data from the fritz box.""" - if not self.is_online: - _LOGGER.error("Not online skipping request") - return - - try: - actor = self.fritz.get_actor_by_ain(self.ain) - except (RequestException, HTTPError): - _LOGGER.error("Request to actor registry failed") - self.state = None - self.temperature = None - self.current_consumption = None - self.total_consumption = None - raise Exception("Request to actor registry failed") - - if actor is None: - _LOGGER.error("Actor could not be found") - self.state = None - self.temperature = None - self.current_consumption = None - self.total_consumption = None - raise Exception("Actor could not be found") - - try: - self.state = actor.get_state() - self.current_consumption = (actor.get_power() or 0.0) / 1000 - self.total_consumption = (actor.get_energy() or 0.0) / 1000 - except (RequestException, HTTPError): - _LOGGER.error("Request to actor failed") - self.state = None - self.temperature = None - self.current_consumption = None - self.total_consumption = None - raise Exception("Request to actor failed") - - self.temperature = actor.temperature - self.has_switch = actor.has_switch - self.has_temperature = actor.has_temperature - self.has_powermeter = actor.has_powermeter diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index fdea21fe91e..a6f531b6dd5 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -16,6 +16,7 @@ from homeassistant.components.http.view import HomeAssistantView from homeassistant.config import async_hass_config_yaml from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED from homeassistant.core import callback +from homeassistant.helpers import service import homeassistant.helpers.config_validation as cv from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass @@ -61,6 +62,10 @@ MANIFEST_JSON = { "short_name": "Assistant", "start_url": "/?homescreen=1", "theme_color": DEFAULT_THEME_COLOR, + "prefer_related_applications": True, + "related_applications": [ + {"platform": "play", "id": "io.homeassistant.companion.android"} + ], } DATA_PANELS = "frontend_panels" @@ -103,19 +108,6 @@ CONFIG_SCHEMA = vol.Schema( SERVICE_SET_THEME = "set_theme" SERVICE_RELOAD_THEMES = "reload_themes" -SERVICE_SET_THEME_SCHEMA = vol.Schema({vol.Required(CONF_NAME): cv.string}) -WS_TYPE_GET_PANELS = "get_panels" -SCHEMA_GET_PANELS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - {vol.Required("type"): WS_TYPE_GET_PANELS} -) -WS_TYPE_GET_THEMES = "frontend/get_themes" -SCHEMA_GET_THEMES = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - {vol.Required("type"): WS_TYPE_GET_THEMES} -) -WS_TYPE_GET_TRANSLATIONS = "frontend/get_translations" -SCHEMA_GET_TRANSLATIONS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - {vol.Required("type"): WS_TYPE_GET_TRANSLATIONS, vol.Required("language"): str} -) class Panel: @@ -251,15 +243,9 @@ def _frontend_root(dev_repo_path): async def async_setup(hass, config): """Set up the serving of the frontend.""" await async_setup_frontend_storage(hass) - hass.components.websocket_api.async_register_command( - WS_TYPE_GET_PANELS, websocket_get_panels, SCHEMA_GET_PANELS - ) - hass.components.websocket_api.async_register_command( - WS_TYPE_GET_THEMES, websocket_get_themes, SCHEMA_GET_THEMES - ) - hass.components.websocket_api.async_register_command( - WS_TYPE_GET_TRANSLATIONS, websocket_get_translations, SCHEMA_GET_TRANSLATIONS - ) + hass.components.websocket_api.async_register_command(websocket_get_panels) + hass.components.websocket_api.async_register_command(websocket_get_themes) + hass.components.websocket_api.async_register_command(websocket_get_translations) hass.http.register_view(ManifestJSONView) conf = config.get(DOMAIN, {}) @@ -331,11 +317,7 @@ async def async_setup(hass, config): def _async_setup_themes(hass, themes): """Set up themes data and services.""" hass.data[DATA_DEFAULT_THEME] = DEFAULT_THEME - if themes is None: - hass.data[DATA_THEMES] = {} - return - - hass.data[DATA_THEMES] = themes + hass.data[DATA_THEMES] = themes or {} @callback def update_theme_and_fire_event(): @@ -348,9 +330,7 @@ def _async_setup_themes(hass, themes): "app-header-background-color", themes[name].get(PRIMARY_COLOR, DEFAULT_THEME_COLOR), ) - hass.bus.async_fire( - EVENT_THEMES_UPDATED, {"themes": themes, "default_theme": name} - ) + hass.bus.async_fire(EVENT_THEMES_UPDATED) @callback def set_theme(call): @@ -373,10 +353,17 @@ def _async_setup_themes(hass, themes): hass.data[DATA_DEFAULT_THEME] = DEFAULT_THEME update_theme_and_fire_event() - hass.services.async_register( - DOMAIN, SERVICE_SET_THEME, set_theme, schema=SERVICE_SET_THEME_SCHEMA + service.async_register_admin_service( + hass, + DOMAIN, + SERVICE_SET_THEME, + set_theme, + vol.Schema({vol.Required(CONF_NAME): cv.string}), + ) + + service.async_register_admin_service( + hass, DOMAIN, SERVICE_RELOAD_THEMES, reload_themes ) - hass.services.async_register(DOMAIN, SERVICE_RELOAD_THEMES, reload_themes) class IndexView(web_urldispatcher.AbstractResource): @@ -498,6 +485,7 @@ class ManifestJSONView(HomeAssistantView): @callback +@websocket_api.websocket_command({"type": "get_panels"}) def websocket_get_panels(hass, connection, msg): """Handle get panels command. @@ -514,11 +502,29 @@ def websocket_get_panels(hass, connection, msg): @callback +@websocket_api.websocket_command({"type": "frontend/get_themes"}) def websocket_get_themes(hass, connection, msg): """Handle get themes command. Async friendly. """ + if hass.config.safe_mode: + connection.send_message( + websocket_api.result_message( + msg["id"], + { + "themes": { + "safe_mode": { + "primary-color": "#db4437", + "accent-color": "#eeee02", + } + }, + "default_theme": "safe_mode", + }, + ) + ) + return + connection.send_message( websocket_api.result_message( msg["id"], @@ -530,6 +536,9 @@ def websocket_get_themes(hass, connection, msg): ) +@websocket_api.websocket_command( + {"type": "frontend/get_translations", vol.Required("language"): str} +) @websocket_api.async_response async def websocket_get_translations(hass, connection, msg): """Handle get translations command. diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 10d2884f182..b9575b7f21a 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==20200130.3" + "home-assistant-frontend==20200220.4" ], "dependencies": [ "api", diff --git a/homeassistant/components/frontier_silicon/media_player.py b/homeassistant/components/frontier_silicon/media_player.py index 010420d0f98..627c3c079b9 100644 --- a/homeassistant/components/frontier_silicon/media_player.py +++ b/homeassistant/components/frontier_silicon/media_player.py @@ -275,7 +275,7 @@ class AFSAPIDevice(MediaPlayerDevice): async def async_set_volume_level(self, volume): """Set volume command.""" - await self.fs_device.set_volume(volume) + await self.fs_device.set_volume(int(volume * 20)) async def async_select_source(self, source): """Select input source.""" diff --git a/homeassistant/components/garmin_connect/.translations/es-419.json b/homeassistant/components/garmin_connect/.translations/es-419.json new file mode 100644 index 00000000000..6e20b4cd2cc --- /dev/null +++ b/homeassistant/components/garmin_connect/.translations/es-419.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Esta cuenta ya est\u00e1 configurada." + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente.", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "too_many_requests": "Demasiadas solicitudes, vuelva a intentarlo m\u00e1s tarde.", + "unknown": "Error inesperado." + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + }, + "description": "Ingrese sus credenciales." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/.translations/fr.json b/homeassistant/components/garmin_connect/.translations/fr.json new file mode 100644 index 00000000000..f0dd8a79e5b --- /dev/null +++ b/homeassistant/components/garmin_connect/.translations/fr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Ce compte est d\u00e9j\u00e0 configur\u00e9." + }, + "error": { + "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer.", + "invalid_auth": "Authentification non valide.", + "too_many_requests": "Trop de demandes, r\u00e9essayez plus tard.", + "unknown": "Erreur inattendue." + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, + "description": "Entrez vos informations d'identification.", + "title": "Garmin Connect" + } + }, + "title": "Garmin Connect" + } +} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/.translations/hu.json b/homeassistant/components/garmin_connect/.translations/hu.json index de4dea29166..931fa295962 100644 --- a/homeassistant/components/garmin_connect/.translations/hu.json +++ b/homeassistant/components/garmin_connect/.translations/hu.json @@ -2,6 +2,23 @@ "config": { "abort": { "already_configured": "Ez a fi\u00f3k m\u00e1r konfigur\u00e1lva van." - } + }, + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni, pr\u00f3b\u00e1lkozzon \u00fajra.", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s.", + "too_many_requests": "T\u00fal sok k\u00e9r\u00e9s, pr\u00f3b\u00e1lkozzon k\u00e9s\u0151bb \u00fajra.", + "unknown": "V\u00e1ratlan hiba." + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "Adja meg a hiteles\u00edt\u0151 adatait.", + "title": "Garmin Csatlakoz\u00e1s" + } + }, + "title": "Garmin Csatlakoz\u00e1s" } } \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/.translations/nl.json b/homeassistant/components/garmin_connect/.translations/nl.json new file mode 100644 index 00000000000..c7a690de6e2 --- /dev/null +++ b/homeassistant/components/garmin_connect/.translations/nl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Dit account is al geconfigureerd." + }, + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw.", + "invalid_auth": "Ongeldige authenticatie", + "too_many_requests": "Te veel aanvragen, probeer het later opnieuw.", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "description": "Voer uw gegevens in", + "title": "Garmin Connect" + } + }, + "title": "Garmin Connect" + } +} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/.translations/sl.json b/homeassistant/components/garmin_connect/.translations/sl.json new file mode 100644 index 00000000000..5b85611d5b7 --- /dev/null +++ b/homeassistant/components/garmin_connect/.translations/sl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Ta ra\u010dun je \u017ee konfiguriran." + }, + "error": { + "cannot_connect": "Povezava ni uspela, poskusite znova.", + "invalid_auth": "Neveljavna avtentikacija.", + "too_many_requests": "Preve\u010d zahtev, poskusite pozneje.", + "unknown": "Nepri\u010dakovana napaka." + }, + "step": { + "user": { + "data": { + "password": "Geslo", + "username": "Uporabni\u0161ko ime" + }, + "description": "Vnesite svoje poverilnice.", + "title": "Garmin Connect" + } + }, + "title": "Garmin Connect" + } +} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/__init__.py b/homeassistant/components/garmin_connect/__init__.py index 5336394f671..d63d82d1284 100644 --- a/homeassistant/components/garmin_connect/__init__.py +++ b/homeassistant/components/garmin_connect/__init__.py @@ -43,13 +43,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): GarminConnectAuthenticationError, GarminConnectTooManyRequestsError, ) as err: - _LOGGER.error("Error occured during Garmin Connect login: %s", err) + _LOGGER.error("Error occurred during Garmin Connect login: %s", err) return False except (GarminConnectConnectionError) as err: - _LOGGER.error("Error occured during Garmin Connect login: %s", err) + _LOGGER.error("Error occurred during Garmin Connect login: %s", err) raise ConfigEntryNotReady except Exception: # pylint: disable=broad-except - _LOGGER.error("Unknown error occured during Garmin Connect login") + _LOGGER.error("Unknown error occurred during Garmin Connect login") return False garmin_data = GarminConnectData(hass, garmin_client) @@ -98,11 +98,11 @@ class GarminConnectData: GarminConnectAuthenticationError, GarminConnectTooManyRequestsError, ) as err: - _LOGGER.error("Error occured during Garmin Connect get stats: %s", err) + _LOGGER.error("Error occurred during Garmin Connect get stats: %s", err) return except (GarminConnectConnectionError) as err: - _LOGGER.error("Error occured during Garmin Connect get stats: %s", err) + _LOGGER.error("Error occurred during Garmin Connect get stats: %s", err) return except Exception: # pylint: disable=broad-except - _LOGGER.error("Unknown error occured during Garmin Connect get stats") + _LOGGER.error("Unknown error occurred during Garmin Connect get stats") return diff --git a/homeassistant/components/garmin_connect/sensor.py b/homeassistant/components/garmin_connect/sensor.py index 0ebd39e78d4..5edf54d95dc 100644 --- a/homeassistant/components/garmin_connect/sensor.py +++ b/homeassistant/components/garmin_connect/sensor.py @@ -32,9 +32,9 @@ async def async_setup_entry( GarminConnectAuthenticationError, GarminConnectTooManyRequestsError, ) as err: - _LOGGER.error("Error occured during Garmin Connect Client update: %s", err) + _LOGGER.error("Error occurred during Garmin Connect Client update: %s", err) except Exception: # pylint: disable=broad-except - _LOGGER.error("Unknown error occured during Garmin Connect Client update.") + _LOGGER.error("Unknown error occurred during Garmin Connect Client update.") entities = [] for ( @@ -160,21 +160,20 @@ class GarminConnectSensor(Entity): return await self._data.async_update() - if not self._data.data: + data = self._data.data + if not data: _LOGGER.error("Didn't receive data from Garmin Connect") return - - data = self._data.data - try: - if "Duration" in self._type and data[self._type]: - self._state = data[self._type] // 60 - elif "Seconds" in self._type and data[self._type]: - self._state = data[self._type] // 60 - else: - self._state = data[self._type] - except KeyError: - _LOGGER.debug("Entity type %s not found in fetched data", self._type) + if data.get(self._type) is None: + _LOGGER.debug("Entity type %s not set in fetched data", self._type) + self._available = False return + self._available = True + + if "Duration" in self._type or "Seconds" in self._type: + self._state = data[self._type] // 60 + else: + self._state = data[self._type] _LOGGER.debug( "Entity %s set to state %s %s", self._type, self._state, self._unit diff --git a/homeassistant/components/gdacs/.translations/ca.json b/homeassistant/components/gdacs/.translations/ca.json new file mode 100644 index 00000000000..5f5acfe7ccf --- /dev/null +++ b/homeassistant/components/gdacs/.translations/ca.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada." + }, + "step": { + "user": { + "data": { + "radius": "Radi" + }, + "title": "Introducci\u00f3 dels detalls del filtre." + } + }, + "title": "Sistema Global de Coordinaci\u00f3 i Alerta per Desastres (GDACS)" + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/.translations/da.json b/homeassistant/components/gdacs/.translations/da.json new file mode 100644 index 00000000000..64f3dd000c4 --- /dev/null +++ b/homeassistant/components/gdacs/.translations/da.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Lokaliteten er allerede konfigureret." + }, + "step": { + "user": { + "data": { + "radius": "Radius" + }, + "title": "Udfyld dine filteroplysninger." + } + }, + "title": "Globalt katastrofevarslings- og koordineringssystem (GDACS)" + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/.translations/de.json b/homeassistant/components/gdacs/.translations/de.json new file mode 100644 index 00000000000..12f94250402 --- /dev/null +++ b/homeassistant/components/gdacs/.translations/de.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Der Standort ist bereits konfiguriert." + }, + "step": { + "user": { + "data": { + "radius": "Radius" + }, + "title": "F\u00fclle deine Filterangaben aus." + } + }, + "title": "Globales Katastrophenalarm- und Koordinierungssystem (GDACS)" + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/.translations/en.json b/homeassistant/components/gdacs/.translations/en.json new file mode 100644 index 00000000000..4e7ceb3846c --- /dev/null +++ b/homeassistant/components/gdacs/.translations/en.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Location is already configured." + }, + "step": { + "user": { + "data": { + "radius": "Radius" + }, + "title": "Fill in your filter details." + } + }, + "title": "Global Disaster Alert and Coordination System (GDACS)" + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/.translations/es.json b/homeassistant/components/gdacs/.translations/es.json new file mode 100644 index 00000000000..6c02b339541 --- /dev/null +++ b/homeassistant/components/gdacs/.translations/es.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada." + }, + "step": { + "user": { + "data": { + "radius": "Radio" + }, + "title": "Rellena los datos de tu filtro." + } + }, + "title": "Sistema Mundial de Alerta y Coordinaci\u00f3n de Desastres (GDACS)" + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/.translations/fr.json b/homeassistant/components/gdacs/.translations/fr.json new file mode 100644 index 00000000000..a4366cb5dc7 --- /dev/null +++ b/homeassistant/components/gdacs/.translations/fr.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9." + }, + "step": { + "user": { + "data": { + "radius": "Rayon" + }, + "title": "Remplissez les d\u00e9tails de votre filtre." + } + }, + "title": "Syst\u00e8me mondial d'alerte et de coordination en cas de catastrophe (GDACS)" + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/.translations/hu.json b/homeassistant/components/gdacs/.translations/hu.json new file mode 100644 index 00000000000..79bcba3388f --- /dev/null +++ b/homeassistant/components/gdacs/.translations/hu.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "A hely m\u00e1r konfigur\u00e1lva van." + }, + "step": { + "user": { + "data": { + "radius": "Sug\u00e1r" + }, + "title": "T\u00f6ltse ki a sz\u0171r\u0151 adatait." + } + }, + "title": "Glob\u00e1lis katasztr\u00f3fariaszt\u00e1si \u00e9s koordin\u00e1ci\u00f3s rendszer (GDACS)" + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/.translations/it.json b/homeassistant/components/gdacs/.translations/it.json new file mode 100644 index 00000000000..249b47f9f59 --- /dev/null +++ b/homeassistant/components/gdacs/.translations/it.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "La posizione \u00e8 gi\u00e0 configurata." + }, + "step": { + "user": { + "data": { + "radius": "Raggio" + }, + "title": "Inserisci i tuoi dettagli del filtro." + } + }, + "title": "Sistema globale di allerta e coordinamento delle catastrofi (GDACS)" + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/.translations/ko.json b/homeassistant/components/gdacs/.translations/ko.json new file mode 100644 index 00000000000..10d6f73e56f --- /dev/null +++ b/homeassistant/components/gdacs/.translations/ko.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "step": { + "user": { + "data": { + "radius": "\ubc18\uacbd" + }, + "title": "\ud544\ud130 \uc138\ubd80 \uc0ac\ud56d\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694." + } + }, + "title": "\uad6d\uc81c \uc7ac\ub09c \uacbd\ubcf4 \ubc0f \uc870\uc815 \uae30\uad6c (GDACS)" + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/.translations/lb.json b/homeassistant/components/gdacs/.translations/lb.json new file mode 100644 index 00000000000..a4077ed630e --- /dev/null +++ b/homeassistant/components/gdacs/.translations/lb.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Standuert ass scho konfigu\u00e9iert." + }, + "step": { + "user": { + "data": { + "radius": "Radius" + }, + "title": "F\u00ebllt \u00e4r Filter D\u00e9tailer aus." + } + }, + "title": "Globale D\u00e9saster Alerte a Koordinatioun System (GDACS)" + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/.translations/nl.json b/homeassistant/components/gdacs/.translations/nl.json new file mode 100644 index 00000000000..62383e43e36 --- /dev/null +++ b/homeassistant/components/gdacs/.translations/nl.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Locatie is al geconfigureerd." + }, + "step": { + "user": { + "data": { + "radius": "Radius" + }, + "title": "Vul uw filtergegevens in." + } + }, + "title": "Wereldwijd rampenwaarschuwings- en co\u00f6rdinatiesysteem (GDACS)" + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/.translations/no.json b/homeassistant/components/gdacs/.translations/no.json new file mode 100644 index 00000000000..54b3ca68451 --- /dev/null +++ b/homeassistant/components/gdacs/.translations/no.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Plasseringen er allerede konfigurert." + }, + "step": { + "user": { + "data": { + "radius": "Radius" + }, + "title": "Fyll ut filterdetaljene." + } + }, + "title": "Globalt katastrofevarslings- og koordineringssystem (GDACS)" + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/.translations/pl.json b/homeassistant/components/gdacs/.translations/pl.json new file mode 100644 index 00000000000..f4b90cc35c7 --- /dev/null +++ b/homeassistant/components/gdacs/.translations/pl.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Lokalizacja jest ju\u017c skonfigurowana." + }, + "step": { + "user": { + "data": { + "radius": "Promie\u0144" + }, + "title": "Wprowad\u017a szczeg\u00f3\u0142owe dane filtra." + } + }, + "title": "Globalny system ostrzegania i koordynacji w przypadku katastrof (GDACS)" + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/.translations/ru.json b/homeassistant/components/gdacs/.translations/ru.json new file mode 100644 index 00000000000..f006832b5be --- /dev/null +++ b/homeassistant/components/gdacs/.translations/ru.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e." + }, + "step": { + "user": { + "data": { + "radius": "\u0420\u0430\u0434\u0438\u0443\u0441" + }, + "title": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435" + } + }, + "title": "\u0413\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u043e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u044f \u0438 \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0446\u0438\u0438 \u0432 \u0441\u043b\u0443\u0447\u0430\u0435 \u0441\u0442\u0438\u0445\u0438\u0439\u043d\u044b\u0445 \u0431\u0435\u0434\u0441\u0442\u0432\u0438\u0439 (GDACS)" + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/.translations/sl.json b/homeassistant/components/gdacs/.translations/sl.json new file mode 100644 index 00000000000..fc522a1a263 --- /dev/null +++ b/homeassistant/components/gdacs/.translations/sl.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Lokacija je \u017ee nastavljena." + }, + "step": { + "user": { + "data": { + "radius": "Radij" + }, + "title": "Izpolnite podrobnosti filtra." + } + }, + "title": "Globalni sistem opozarjanja in koordinacije nesre\u010d (GDACS)" + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/.translations/sv.json b/homeassistant/components/gdacs/.translations/sv.json new file mode 100644 index 00000000000..3c7fb00056e --- /dev/null +++ b/homeassistant/components/gdacs/.translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Plats \u00e4r redan konfigurerad." + }, + "step": { + "user": { + "data": { + "radius": "Radie" + }, + "title": "Fyll i filterinformation." + } + }, + "title": "Globalt katastrofvarnings- och samordningssystem (GDACS)" + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/.translations/zh-Hant.json b/homeassistant/components/gdacs/.translations/zh-Hant.json new file mode 100644 index 00000000000..59f9b7be031 --- /dev/null +++ b/homeassistant/components/gdacs/.translations/zh-Hant.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "\u4f4d\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" + }, + "step": { + "user": { + "data": { + "radius": "\u534a\u5f91" + }, + "title": "\u586b\u5beb\u904e\u6ffe\u5668\u8cc7\u8a0a\u3002" + } + }, + "title": "\u5168\u7403\u707d\u96e3\u9810\u8b66\u548c\u5354\u8abf\u7cfb\u7d71\uff08GDACS\uff09" + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/__init__.py b/homeassistant/components/gdacs/__init__.py new file mode 100644 index 00000000000..34f1bdc88d8 --- /dev/null +++ b/homeassistant/components/gdacs/__init__.py @@ -0,0 +1,212 @@ +"""The Global Disaster Alert and Coordination System (GDACS) integration.""" +import asyncio +from datetime import timedelta +import logging + +from aio_georss_gdacs import GdacsFeedManager +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_RADIUS, + CONF_SCAN_INTERVAL, + CONF_UNIT_SYSTEM_IMPERIAL, + LENGTH_MILES, +) +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.unit_system import METRIC_SYSTEM + +from .const import ( + CONF_CATEGORIES, + DEFAULT_RADIUS, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + FEED, + PLATFORMS, + SIGNAL_DELETE_ENTITY, + SIGNAL_NEW_GEOLOCATION, + SIGNAL_STATUS, + SIGNAL_UPDATE_ENTITY, + VALID_CATEGORIES, +) + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Inclusive(CONF_LATITUDE, "coordinates"): cv.latitude, + vol.Inclusive(CONF_LONGITUDE, "coordinates"): cv.longitude, + vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): vol.Coerce(float), + vol.Optional( + CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL + ): cv.time_period, + vol.Optional(CONF_CATEGORIES, default=[]): vol.All( + cv.ensure_list, [vol.In(VALID_CATEGORIES)] + ), + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Set up the GDACS component.""" + if DOMAIN not in config: + return True + + conf = config[DOMAIN] + latitude = conf.get(CONF_LATITUDE, hass.config.latitude) + longitude = conf.get(CONF_LONGITUDE, hass.config.longitude) + scan_interval = conf[CONF_SCAN_INTERVAL] + categories = conf[CONF_CATEGORIES] + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_LATITUDE: latitude, + CONF_LONGITUDE: longitude, + CONF_RADIUS: conf[CONF_RADIUS], + CONF_SCAN_INTERVAL: scan_interval, + CONF_CATEGORIES: categories, + }, + ) + ) + + return True + + +async def async_setup_entry(hass, config_entry): + """Set up the GDACS component as config entry.""" + hass.data.setdefault(DOMAIN, {}) + feeds = hass.data[DOMAIN].setdefault(FEED, {}) + + radius = config_entry.data[CONF_RADIUS] + if hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: + radius = METRIC_SYSTEM.length(radius, LENGTH_MILES) + # Create feed entity manager for all platforms. + manager = GdacsFeedEntityManager(hass, config_entry, radius) + feeds[config_entry.entry_id] = manager + _LOGGER.debug("Feed entity manager added for %s", config_entry.entry_id) + await manager.async_init() + return True + + +async def async_unload_entry(hass, config_entry): + """Unload an GDACS component config entry.""" + manager = hass.data[DOMAIN][FEED].pop(config_entry.entry_id) + await manager.async_stop() + await asyncio.wait( + [ + hass.config_entries.async_forward_entry_unload(config_entry, domain) + for domain in PLATFORMS + ] + ) + return True + + +class GdacsFeedEntityManager: + """Feed Entity Manager for GDACS feed.""" + + def __init__(self, hass, config_entry, radius_in_km): + """Initialize the Feed Entity Manager.""" + self._hass = hass + self._config_entry = config_entry + coordinates = ( + config_entry.data[CONF_LATITUDE], + config_entry.data[CONF_LONGITUDE], + ) + categories = config_entry.data[CONF_CATEGORIES] + websession = aiohttp_client.async_get_clientsession(hass) + self._feed_manager = GdacsFeedManager( + websession, + self._generate_entity, + self._update_entity, + self._remove_entity, + coordinates, + filter_radius=radius_in_km, + filter_categories=categories, + status_async_callback=self._status_update, + ) + self._config_entry_id = config_entry.entry_id + self._scan_interval = timedelta(seconds=config_entry.data[CONF_SCAN_INTERVAL]) + self._track_time_remove_callback = None + self._status_info = None + self.listeners = [] + + async def async_init(self): + """Schedule initial and regular updates based on configured time interval.""" + + for domain in PLATFORMS: + self._hass.async_create_task( + self._hass.config_entries.async_forward_entry_setup( + self._config_entry, domain + ) + ) + + async def update(event_time): + """Update.""" + await self.async_update() + + # Trigger updates at regular intervals. + self._track_time_remove_callback = async_track_time_interval( + self._hass, update, self._scan_interval + ) + + _LOGGER.debug("Feed entity manager initialized") + + async def async_update(self): + """Refresh data.""" + await self._feed_manager.update() + _LOGGER.debug("Feed entity manager updated") + + async def async_stop(self): + """Stop this feed entity manager from refreshing.""" + for unsub_dispatcher in self.listeners: + unsub_dispatcher() + self.listeners = [] + if self._track_time_remove_callback: + self._track_time_remove_callback() + _LOGGER.debug("Feed entity manager stopped") + + @callback + def async_event_new_entity(self): + """Return manager specific event to signal new entity.""" + return SIGNAL_NEW_GEOLOCATION.format(self._config_entry_id) + + def get_entry(self, external_id): + """Get feed entry by external id.""" + return self._feed_manager.feed_entries.get(external_id) + + def status_info(self): + """Return latest status update info received.""" + return self._status_info + + async def _generate_entity(self, external_id): + """Generate new entity.""" + async_dispatcher_send( + self._hass, self.async_event_new_entity(), self, external_id + ) + + async def _update_entity(self, external_id): + """Update entity.""" + async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(external_id)) + + async def _remove_entity(self, external_id): + """Remove entity.""" + async_dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id)) + + async def _status_update(self, status_info): + """Propagate status update.""" + _LOGGER.debug("Status update received: %s", status_info) + self._status_info = status_info + async_dispatcher_send(self._hass, SIGNAL_STATUS.format(self._config_entry_id)) diff --git a/homeassistant/components/gdacs/config_flow.py b/homeassistant/components/gdacs/config_flow.py new file mode 100644 index 00000000000..1e12a116ed5 --- /dev/null +++ b/homeassistant/components/gdacs/config_flow.py @@ -0,0 +1,66 @@ +"""Config flow to configure the GDACS integration.""" +import logging + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_RADIUS, + CONF_SCAN_INTERVAL, +) +from homeassistant.helpers import config_validation as cv + +from .const import ( # pylint: disable=unused-import + CONF_CATEGORIES, + DEFAULT_RADIUS, + DEFAULT_SCAN_INTERVAL, + DOMAIN, +) + +DATA_SCHEMA = vol.Schema( + {vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): cv.positive_int} +) + +_LOGGER = logging.getLogger(__name__) + + +class GdacsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a GDACS config flow.""" + + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def _show_form(self, errors=None): + """Show the form to the user.""" + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors or {} + ) + + async def async_step_import(self, import_config): + """Import a config entry from configuration.yaml.""" + return await self.async_step_user(import_config) + + async def async_step_user(self, user_input=None): + """Handle the start of the config flow.""" + _LOGGER.debug("User input: %s", user_input) + if not user_input: + return await self._show_form() + + latitude = user_input.get(CONF_LATITUDE, self.hass.config.latitude) + user_input[CONF_LATITUDE] = latitude + longitude = user_input.get(CONF_LONGITUDE, self.hass.config.longitude) + user_input[CONF_LONGITUDE] = longitude + + identifier = f"{user_input[CONF_LATITUDE]}, {user_input[CONF_LONGITUDE]}" + + await self.async_set_unique_id(identifier) + self._abort_if_unique_id_configured() + + scan_interval = user_input.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + user_input[CONF_SCAN_INTERVAL] = scan_interval.seconds + + categories = user_input.get(CONF_CATEGORIES, []) + user_input[CONF_CATEGORIES] = categories + + return self.async_create_entry(title=identifier, data=user_input) diff --git a/homeassistant/components/gdacs/const.py b/homeassistant/components/gdacs/const.py new file mode 100644 index 00000000000..4579304f30d --- /dev/null +++ b/homeassistant/components/gdacs/const.py @@ -0,0 +1,25 @@ +"""Define constants for the GDACS integration.""" +from datetime import timedelta + +from aio_georss_gdacs.consts import EVENT_TYPE_MAP + +DOMAIN = "gdacs" + +PLATFORMS = ("sensor", "geo_location") + +FEED = "feed" + +CONF_CATEGORIES = "categories" + +DEFAULT_ICON = "mdi:alert" +DEFAULT_RADIUS = 500.0 +DEFAULT_SCAN_INTERVAL = timedelta(minutes=5) + +SIGNAL_DELETE_ENTITY = "gdacs_delete_{}" +SIGNAL_UPDATE_ENTITY = "gdacs_update_{}" +SIGNAL_STATUS = "gdacs_status_{}" + +SIGNAL_NEW_GEOLOCATION = "gdacs_new_geolocation_{}" + +# Fetch valid categories from integration library. +VALID_CATEGORIES = list(EVENT_TYPE_MAP.values()) diff --git a/homeassistant/components/gdacs/geo_location.py b/homeassistant/components/gdacs/geo_location.py new file mode 100644 index 00000000000..34da104e093 --- /dev/null +++ b/homeassistant/components/gdacs/geo_location.py @@ -0,0 +1,234 @@ +"""Geolocation support for GDACS Feed.""" +import logging +from typing import Optional + +from homeassistant.components.geo_location import GeolocationEvent +from homeassistant.const import ( + ATTR_ATTRIBUTION, + CONF_UNIT_SYSTEM_IMPERIAL, + LENGTH_KILOMETERS, + LENGTH_MILES, +) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util.unit_system import IMPERIAL_SYSTEM + +from .const import ( + DEFAULT_ICON, + DOMAIN, + FEED, + SIGNAL_DELETE_ENTITY, + SIGNAL_UPDATE_ENTITY, +) + +_LOGGER = logging.getLogger(__name__) + +ATTR_ALERT_LEVEL = "alert_level" +ATTR_COUNTRY = "country" +ATTR_DESCRIPTION = "description" +ATTR_DURATION_IN_WEEK = "duration_in_week" +ATTR_EVENT_TYPE = "event_type" +ATTR_EXTERNAL_ID = "external_id" +ATTR_FROM_DATE = "from_date" +ATTR_POPULATION = "population" +ATTR_SEVERITY = "severity" +ATTR_TO_DATE = "to_date" +ATTR_VULNERABILITY = "vulnerability" + +ICONS = { + "DR": "mdi:water-off", + "EQ": "mdi:pulse", + "FL": "mdi:home-flood", + "TC": "mdi:weather-hurricane", + "TS": "mdi:waves", + "VO": "mdi:image-filter-hdr", +} + +# An update of this entity is not making a web request, but uses internal data only. +PARALLEL_UPDATES = 0 + +SOURCE = "gdacs" + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the GDACS Feed platform.""" + manager = hass.data[DOMAIN][FEED][entry.entry_id] + + @callback + def async_add_geolocation(feed_manager, external_id): + """Add gelocation entity from feed.""" + new_entity = GdacsEvent(feed_manager, external_id) + _LOGGER.debug("Adding geolocation %s", new_entity) + async_add_entities([new_entity], True) + + manager.listeners.append( + async_dispatcher_connect( + hass, manager.async_event_new_entity(), async_add_geolocation + ) + ) + # Do not wait for update here so that the setup can be completed and because an + # update will fetch data from the feed via HTTP and then process that data. + hass.async_create_task(manager.async_update()) + _LOGGER.debug("Geolocation setup done") + + +class GdacsEvent(GeolocationEvent): + """This represents an external event with GDACS feed data.""" + + def __init__(self, feed_manager, external_id): + """Initialize entity with data from feed entry.""" + self._feed_manager = feed_manager + self._external_id = external_id + self._title = None + self._distance = None + self._latitude = None + self._longitude = None + self._attribution = None + self._alert_level = None + self._country = None + self._description = None + self._duration_in_week = None + self._event_type_short = None + self._event_type = None + self._from_date = None + self._to_date = None + self._population = None + self._severity = None + self._vulnerability = None + self._version = None + self._remove_signal_delete = None + self._remove_signal_update = None + + async def async_added_to_hass(self): + """Call when entity is added to hass.""" + self._remove_signal_delete = async_dispatcher_connect( + self.hass, + SIGNAL_DELETE_ENTITY.format(self._external_id), + self._delete_callback, + ) + self._remove_signal_update = async_dispatcher_connect( + self.hass, + SIGNAL_UPDATE_ENTITY.format(self._external_id), + self._update_callback, + ) + + async def async_will_remove_from_hass(self) -> None: + """Call when entity will be removed from hass.""" + self._remove_signal_delete() + self._remove_signal_update() + + @callback + def _delete_callback(self): + """Remove this entity.""" + self.hass.async_create_task(self.async_remove()) + + @callback + def _update_callback(self): + """Call update method.""" + self.async_schedule_update_ha_state(True) + + @property + def should_poll(self): + """No polling needed for GDACS feed location events.""" + return False + + async def async_update(self): + """Update this entity from the data held in the feed manager.""" + _LOGGER.debug("Updating %s", self._external_id) + feed_entry = self._feed_manager.get_entry(self._external_id) + if feed_entry: + self._update_from_feed(feed_entry) + + def _update_from_feed(self, feed_entry): + """Update the internal state from the provided feed entry.""" + event_name = feed_entry.event_name + if not event_name: + # Earthquakes usually don't have an event name. + event_name = f"{feed_entry.country} ({feed_entry.event_id})" + self._title = f"{feed_entry.event_type}: {event_name}" + # Convert distance if not metric system. + if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: + self._distance = IMPERIAL_SYSTEM.length( + feed_entry.distance_to_home, LENGTH_KILOMETERS + ) + else: + self._distance = feed_entry.distance_to_home + self._latitude = feed_entry.coordinates[0] + self._longitude = feed_entry.coordinates[1] + self._attribution = feed_entry.attribution + self._alert_level = feed_entry.alert_level + self._country = feed_entry.country + self._description = feed_entry.title + self._duration_in_week = feed_entry.duration_in_week + self._event_type_short = feed_entry.event_type_short + self._event_type = feed_entry.event_type + self._from_date = feed_entry.from_date + self._to_date = feed_entry.to_date + self._population = feed_entry.population + self._severity = feed_entry.severity + self._vulnerability = feed_entry.vulnerability + # Round vulnerability value if presented as float. + if isinstance(self._vulnerability, float): + self._vulnerability = round(self._vulnerability, 1) + self._version = feed_entry.version + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + if self._event_type_short and self._event_type_short in ICONS: + return ICONS[self._event_type_short] + return DEFAULT_ICON + + @property + def source(self) -> str: + """Return source value of this external event.""" + return SOURCE + + @property + def name(self) -> Optional[str]: + """Return the name of the entity.""" + return self._title + + @property + def distance(self) -> Optional[float]: + """Return distance value of this external event.""" + return self._distance + + @property + def latitude(self) -> Optional[float]: + """Return latitude value of this external event.""" + return self._latitude + + @property + def longitude(self) -> Optional[float]: + """Return longitude value of this external event.""" + return self._longitude + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: + return LENGTH_MILES + return LENGTH_KILOMETERS + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attributes = {} + for key, value in ( + (ATTR_EXTERNAL_ID, self._external_id), + (ATTR_DESCRIPTION, self._description), + (ATTR_ATTRIBUTION, self._attribution), + (ATTR_EVENT_TYPE, self._event_type), + (ATTR_ALERT_LEVEL, self._alert_level), + (ATTR_COUNTRY, self._country), + (ATTR_DURATION_IN_WEEK, self._duration_in_week), + (ATTR_FROM_DATE, self._from_date), + (ATTR_TO_DATE, self._to_date), + (ATTR_POPULATION, self._population), + (ATTR_SEVERITY, self._severity), + (ATTR_VULNERABILITY, self._vulnerability), + ): + if value or isinstance(value, bool): + attributes[key] = value + return attributes diff --git a/homeassistant/components/gdacs/manifest.json b/homeassistant/components/gdacs/manifest.json new file mode 100644 index 00000000000..45105b21ab4 --- /dev/null +++ b/homeassistant/components/gdacs/manifest.json @@ -0,0 +1,14 @@ +{ + "domain": "gdacs", + "name": "Global Disaster Alert and Coordination System (GDACS)", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/gdacs", + "requirements": [ + "aio_georss_gdacs==0.3" + ], + "dependencies": [], + "codeowners": [ + "@exxamalte" + ], + "quality_scale": "platinum" +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/sensor.py b/homeassistant/components/gdacs/sensor.py new file mode 100644 index 00000000000..e58090fd165 --- /dev/null +++ b/homeassistant/components/gdacs/sensor.py @@ -0,0 +1,140 @@ +"""Feed Entity Manager Sensor support for GDACS Feed.""" +import logging +from typing import Optional + +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity +from homeassistant.util import dt + +from .const import DEFAULT_ICON, DOMAIN, FEED, SIGNAL_STATUS + +_LOGGER = logging.getLogger(__name__) + +ATTR_STATUS = "status" +ATTR_LAST_UPDATE = "last_update" +ATTR_LAST_UPDATE_SUCCESSFUL = "last_update_successful" +ATTR_LAST_TIMESTAMP = "last_timestamp" +ATTR_CREATED = "created" +ATTR_UPDATED = "updated" +ATTR_REMOVED = "removed" + +DEFAULT_UNIT_OF_MEASUREMENT = "alerts" + +# An update of this entity is not making a web request, but uses internal data only. +PARALLEL_UPDATES = 0 + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the GDACS Feed platform.""" + manager = hass.data[DOMAIN][FEED][entry.entry_id] + sensor = GdacsSensor(entry.entry_id, entry.title, manager) + async_add_entities([sensor]) + _LOGGER.debug("Sensor setup done") + + +class GdacsSensor(Entity): + """This is a status sensor for the GDACS integration.""" + + def __init__(self, config_entry_id, config_title, manager): + """Initialize entity.""" + self._config_entry_id = config_entry_id + self._config_title = config_title + self._manager = manager + self._status = None + self._last_update = None + self._last_update_successful = None + self._last_timestamp = None + self._total = None + self._created = None + self._updated = None + self._removed = None + self._remove_signal_status = None + + async def async_added_to_hass(self): + """Call when entity is added to hass.""" + self._remove_signal_status = async_dispatcher_connect( + self.hass, + SIGNAL_STATUS.format(self._config_entry_id), + self._update_status_callback, + ) + _LOGGER.debug("Waiting for updates %s", self._config_entry_id) + # First update is manual because of how the feed entity manager is updated. + await self.async_update() + + async def async_will_remove_from_hass(self) -> None: + """Call when entity will be removed from hass.""" + if self._remove_signal_status: + self._remove_signal_status() + + @callback + def _update_status_callback(self): + """Call status update method.""" + _LOGGER.debug("Received status update for %s", self._config_entry_id) + self.async_schedule_update_ha_state(True) + + @property + def should_poll(self): + """No polling needed for GDACS status sensor.""" + return False + + async def async_update(self): + """Update this entity from the data held in the feed manager.""" + _LOGGER.debug("Updating %s", self._config_entry_id) + if self._manager: + status_info = self._manager.status_info() + if status_info: + self._update_from_status_info(status_info) + + def _update_from_status_info(self, status_info): + """Update the internal state from the provided information.""" + self._status = status_info.status + self._last_update = ( + dt.as_utc(status_info.last_update) if status_info.last_update else None + ) + if status_info.last_update_successful: + self._last_update_successful = dt.as_utc(status_info.last_update_successful) + else: + self._last_update_successful = None + self._last_timestamp = status_info.last_timestamp + self._total = status_info.total + self._created = status_info.created + self._updated = status_info.updated + self._removed = status_info.removed + + @property + def state(self): + """Return the state of the sensor.""" + return self._total + + @property + def name(self) -> Optional[str]: + """Return the name of the entity.""" + return f"GDACS ({self._config_title})" + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return DEFAULT_ICON + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return DEFAULT_UNIT_OF_MEASUREMENT + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attributes = {} + for key, value in ( + (ATTR_STATUS, self._status), + (ATTR_LAST_UPDATE, self._last_update), + (ATTR_LAST_UPDATE_SUCCESSFUL, self._last_update_successful), + (ATTR_LAST_TIMESTAMP, self._last_timestamp), + (ATTR_CREATED, self._created), + (ATTR_UPDATED, self._updated), + (ATTR_REMOVED, self._removed), + ): + if value or isinstance(value, bool): + attributes[key] = value + return attributes diff --git a/homeassistant/components/gdacs/strings.json b/homeassistant/components/gdacs/strings.json new file mode 100644 index 00000000000..353b1b85634 --- /dev/null +++ b/homeassistant/components/gdacs/strings.json @@ -0,0 +1,16 @@ +{ + "config": { + "title": "Global Disaster Alert and Coordination System (GDACS)", + "step": { + "user": { + "title": "Fill in your filter details.", + "data": { + "radius": "Radius" + } + } + }, + "abort": { + "already_configured": "Location is already configured." + } + } +} diff --git a/homeassistant/components/geonetnz_quakes/.translations/ko.json b/homeassistant/components/geonetnz_quakes/.translations/ko.json index 26caa2ebe54..66a216149dd 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/ko.json +++ b/homeassistant/components/geonetnz_quakes/.translations/ko.json @@ -9,7 +9,7 @@ "mmi": "MMI", "radius": "\ubc18\uacbd" }, - "title": "\ud544\ud130 \uc138\ubd80 \uc0ac\ud56d\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694" + "title": "\ud544\ud130 \uc138\ubd80 \uc0ac\ud56d\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694." } }, "title": "GeoNet NZ Quakes" diff --git a/homeassistant/components/geonetnz_quakes/.translations/pl.json b/homeassistant/components/geonetnz_quakes/.translations/pl.json index fd82bba43b5..bdd8f152d39 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/pl.json +++ b/homeassistant/components/geonetnz_quakes/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "Lokalizacja ju\u017c zarejestrowana" + "identifier_exists": "Lokalizacja jest ju\u017c zarejestrowana." }, "step": { "user": { diff --git a/homeassistant/components/geonetnz_quakes/.translations/sv.json b/homeassistant/components/geonetnz_quakes/.translations/sv.json new file mode 100644 index 00000000000..13058ad3ad2 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/sv.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "Plats redan registrerad" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Radie" + }, + "title": "Fyll i dina filterdetaljer." + } + }, + "title": "GeoNet NZ Quakes" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/manifest.json b/homeassistant/components/geonetnz_quakes/manifest.json index 775ca8760bc..50813b062f0 100644 --- a/homeassistant/components/geonetnz_quakes/manifest.json +++ b/homeassistant/components/geonetnz_quakes/manifest.json @@ -3,7 +3,7 @@ "name": "GeoNet NZ Quakes", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/geonetnz_quakes", - "requirements": ["aio_geojson_geonetnz_quakes==0.11"], + "requirements": ["aio_geojson_geonetnz_quakes==0.12"], "dependencies": [], "codeowners": ["@exxamalte"] } diff --git a/homeassistant/components/geonetnz_volcano/.translations/hu.json b/homeassistant/components/geonetnz_volcano/.translations/hu.json index 875a8330f76..e53a91bcb03 100644 --- a/homeassistant/components/geonetnz_volcano/.translations/hu.json +++ b/homeassistant/components/geonetnz_volcano/.translations/hu.json @@ -1,9 +1,16 @@ { "config": { + "error": { + "identifier_exists": "A hely m\u00e1r regisztr\u00e1lt" + }, "step": { "user": { + "data": { + "radius": "Sug\u00e1r" + }, "title": "T\u00f6ltse ki a sz\u0171r\u0151 adatait." } - } + }, + "title": "GeoNet NZ vulk\u00e1n" } } \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/ko.json b/homeassistant/components/geonetnz_volcano/.translations/ko.json index 5d393fef4c4..d19091e75e8 100644 --- a/homeassistant/components/geonetnz_volcano/.translations/ko.json +++ b/homeassistant/components/geonetnz_volcano/.translations/ko.json @@ -8,7 +8,7 @@ "data": { "radius": "\ubc18\uacbd" }, - "title": "\ud544\ud130 \uc138\ubd80 \uc0ac\ud56d\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694" + "title": "\ud544\ud130 \uc138\ubd80 \uc0ac\ud56d\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694." } }, "title": "GeoNet NZ Volcano" diff --git a/homeassistant/components/geonetnz_volcano/.translations/pl.json b/homeassistant/components/geonetnz_volcano/.translations/pl.json index 7d329815f3f..c51a69356a1 100644 --- a/homeassistant/components/geonetnz_volcano/.translations/pl.json +++ b/homeassistant/components/geonetnz_volcano/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "Lokalizacja ju\u017c zarejestrowana" + "identifier_exists": "Lokalizacja jest ju\u017c zarejestrowana." }, "step": { "user": { diff --git a/homeassistant/components/geonetnz_volcano/.translations/sv.json b/homeassistant/components/geonetnz_volcano/.translations/sv.json new file mode 100644 index 00000000000..35e7e24c926 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "Plats redan registrerad" + }, + "step": { + "user": { + "data": { + "radius": "Radie" + }, + "title": "Fyll i dina filterdetaljer." + } + }, + "title": "GeoNet NZ Volcano" + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/es-419.json b/homeassistant/components/gios/.translations/es-419.json new file mode 100644 index 00000000000..53439a7ab7b --- /dev/null +++ b/homeassistant/components/gios/.translations/es-419.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nombre de la integraci\u00f3n" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/hu.json b/homeassistant/components/gios/.translations/hu.json new file mode 100644 index 00000000000..75fcb2088a5 --- /dev/null +++ b/homeassistant/components/gios/.translations/hu.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "A GIO\u015a integr\u00e1ci\u00f3 ehhez a m\u00e9r\u0151\u00e1llom\u00e1shoz m\u00e1r konfigur\u00e1lva van." + }, + "error": { + "cannot_connect": "Nem lehet csatlakozni a GIO\u015a szerverhez.", + "invalid_sensors_data": "\u00c9rv\u00e9nytelen \u00e9rz\u00e9kel\u0151k adatai ehhez a m\u00e9r\u0151\u00e1llom\u00e1shoz.", + "wrong_station_id": "A m\u00e9r\u0151\u00e1llom\u00e1s azonos\u00edt\u00f3ja nem megfelel\u0151." + }, + "step": { + "user": { + "data": { + "name": "Az integr\u00e1ci\u00f3 neve", + "station_id": "A m\u00e9r\u0151\u00e1llom\u00e1s azonos\u00edt\u00f3ja" + }, + "description": "A GIO\u015a (lengyel k\u00f6rnyezetv\u00e9delmi f\u0151fel\u00fcgyel\u0151) leveg\u0151min\u0151s\u00e9gi integr\u00e1ci\u00f3j\u00e1nak be\u00e1ll\u00edt\u00e1sa. Ha seg\u00edts\u00e9gre van sz\u00fcks\u00e9ged a konfigur\u00e1ci\u00f3val kapcsolatban, l\u00e1togass ide: https://www.home-assistant.io/integrations/gios", + "title": "GIO\u015a (Lengyel K\u00f6rnyezetv\u00e9delmi F\u0151fel\u00fcgyel\u0151s\u00e9g)" + } + }, + "title": "GIO\u015a" + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/nl.json b/homeassistant/components/gios/.translations/nl.json new file mode 100644 index 00000000000..eb487681838 --- /dev/null +++ b/homeassistant/components/gios/.translations/nl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "GIO\u015a-integratie voor dit meetstation is al geconfigureerd." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken met de GIO\u015a-server.", + "invalid_sensors_data": "Ongeldige sensorgegevens voor dit meetstation.", + "wrong_station_id": "ID van het meetstation is niet correct." + }, + "step": { + "user": { + "data": { + "name": "Naam van de integratie", + "station_id": "ID van het meetstation" + }, + "description": "GIO\u015a (Poolse hoofdinspectie van milieubescherming) luchtkwaliteitintegratie instellen. Als u hulp nodig hebt bij de configuratie, kijk dan hier: https://www.home-assistant.io/integrations/gios", + "title": "GIO\u015a (Poolse hoofdinspectie van milieubescherming)" + } + }, + "title": "GIO\u015a" + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/ru.json b/homeassistant/components/gios/.translations/ru.json index 69ffff98517..0045b08cec8 100644 --- a/homeassistant/components/gios/.translations/ru.json +++ b/homeassistant/components/gios/.translations/ru.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 GIO\u015a.", - "invalid_sensors_data": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u0432 \u0434\u043b\u044f \u044d\u0442\u043e\u0439 \u0438\u0437\u043c\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0441\u0442\u0430\u043d\u0446\u0438\u0438.", + "invalid_sensors_data": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432 \u0434\u043b\u044f \u044d\u0442\u043e\u0439 \u0438\u0437\u043c\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0441\u0442\u0430\u043d\u0446\u0438\u0438.", "wrong_station_id": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 ID \u0438\u0437\u043c\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0441\u0442\u0430\u043d\u0446\u0438\u0438." }, "step": { diff --git a/homeassistant/components/gios/.translations/sl.json b/homeassistant/components/gios/.translations/sl.json index da3995dd0b3..089435dee3f 100644 --- a/homeassistant/components/gios/.translations/sl.json +++ b/homeassistant/components/gios/.translations/sl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "GIO\u015a integracija za to merilno postajo je \u017ee nastavljena." + }, "error": { "cannot_connect": "Ne morem se povezati s stre\u017enikom GIO\u015a.", "invalid_sensors_data": "Neveljavni podatki senzorjev za to merilno postajo.", diff --git a/homeassistant/components/gios/.translations/sv.json b/homeassistant/components/gios/.translations/sv.json new file mode 100644 index 00000000000..b5a865b5ccd --- /dev/null +++ b/homeassistant/components/gios/.translations/sv.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "GIO\u015a-integration f\u00f6r denna m\u00e4tstation \u00e4r redan konfigurerad." + }, + "error": { + "cannot_connect": "Det g\u00e5r inte att ansluta till GIO\u015a-servern.", + "invalid_sensors_data": "Ogiltig sensordata f\u00f6r denna m\u00e4tstation.", + "wrong_station_id": "M\u00e4tstationens ID \u00e4r inte korrekt." + }, + "step": { + "user": { + "data": { + "name": "Integrationens namn", + "station_id": "M\u00e4tstationens ID" + }, + "description": "St\u00e4ll in luftkvalitetintegration f\u00f6r GIO\u015a (polsk chefinspektorat f\u00f6r milj\u00f6skydd). Om du beh\u00f6ver hj\u00e4lp med konfigurationen titta h\u00e4r: https://www.home-assistant.io/integrations/gios", + "title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)" + } + }, + "title": "GIO\u015a" + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/es-419.json b/homeassistant/components/glances/.translations/es-419.json new file mode 100644 index 00000000000..6debc6da6c1 --- /dev/null +++ b/homeassistant/components/glances/.translations/es-419.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "cannot_connect": "No se puede conectar al host", + "wrong_version": "Versi\u00f3n no compatible (2 o 3 solamente)" + }, + "step": { + "user": { + "data": { + "name": "Nombre", + "password": "Contrase\u00f1a", + "port": "Puerto", + "username": "Nombre de usuario", + "verify_ssl": "Verificar la certificaci\u00f3n del sistema", + "version": "Versi\u00f3n de API de Glances (2 o 3)" + } + } + } + }, + "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/sv.json b/homeassistant/components/glances/.translations/sv.json new file mode 100644 index 00000000000..f4b95081a10 --- /dev/null +++ b/homeassistant/components/glances/.translations/sv.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "V\u00e4rden \u00e4r redan konfigurerad." + }, + "error": { + "cannot_connect": "Det g\u00e5r inte att ansluta till v\u00e4rden", + "wrong_version": "Version st\u00f6ds inte (endast 2 eller 3)" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "name": "Namn", + "password": "L\u00f6senord", + "port": "Port", + "ssl": "Anv\u00e4nd SSL / TLS f\u00f6r att ansluta till Glances-systemet", + "username": "Anv\u00e4ndarnamn", + "verify_ssl": "Verifiera certifieringen av systemet", + "version": "Glances API-version (2 eller 3)" + }, + "title": "St\u00e4ll in Glances" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Uppdateringsfrekvens" + }, + "description": "Konfigurera alternativ f\u00f6r Glances" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/const.py b/homeassistant/components/glances/const.py index e47586ea245..31a3f0f69e4 100644 --- a/homeassistant/components/glances/const.py +++ b/homeassistant/components/glances/const.py @@ -1,5 +1,5 @@ """Constants for Glances component.""" -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import DATA_GIBIBYTES, DATA_MEBIBYTES, TEMP_CELSIUS DOMAIN = "glances" CONF_VERSION = "version" @@ -14,23 +14,28 @@ 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"], + "disk_use_percent": ["fs", "used percent", "%", "mdi:harddisk"], + "disk_use": ["fs", "used", DATA_GIBIBYTES, "mdi:harddisk"], + "disk_free": ["fs", "free", DATA_GIBIBYTES, "mdi:harddisk"], + "memory_use_percent": ["mem", "RAM used percent", "%", "mdi:memory"], + "memory_use": ["mem", "RAM used", DATA_MEBIBYTES, "mdi:memory"], + "memory_free": ["mem", "RAM free", DATA_MEBIBYTES, "mdi:memory"], + "swap_use_percent": ["memswap", "Swap used percent", "%", "mdi:memory"], + "swap_use": ["memswap", "Swap used", DATA_GIBIBYTES, "mdi:memory"], + "swap_free": ["memswap", "Swap free", DATA_GIBIBYTES, "mdi:memory"], + "processor_load": ["load", "CPU load", "15 min", "mdi:memory"], + "process_running": ["processcount", "Running", "Count", "mdi:memory"], + "process_total": ["processcount", "Total", "Count", "mdi:memory"], + "process_thread": ["processcount", "Thread", "Count", "mdi:memory"], + "process_sleeping": ["processcount", "Sleeping", "Count", "mdi:memory"], + "cpu_use_percent": ["cpu", "CPU used", "%", "mdi:memory"], + "sensor_temp": ["sensors", "Temp", TEMP_CELSIUS, "mdi:thermometer"], + "docker_active": ["docker", "Containers active", "", "mdi:docker"], + "docker_cpu_use": ["docker", "Containers CPU used", "%", "mdi:docker"], + "docker_memory_use": [ + "docker", + "Containers RAM used", + DATA_MEBIBYTES, + "mdi:docker", + ], } diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index 968081cfc43..f701dfdb741 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -14,13 +14,51 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Glances sensors.""" - glances_data = hass.data[DOMAIN][config_entry.entry_id] + client = hass.data[DOMAIN][config_entry.entry_id] name = config_entry.data[CONF_NAME] dev = [] - for sensor_type in SENSOR_TYPES: - dev.append( - GlancesSensor(glances_data, name, SENSOR_TYPES[sensor_type][0], sensor_type) - ) + + for sensor_type, sensor_details in SENSOR_TYPES.items(): + if not sensor_details[0] in client.api.data: + continue + if sensor_details[0] in client.api.data: + if sensor_details[0] == "fs": + # fs will provide a list of disks attached + for disk in client.api.data[sensor_details[0]]: + dev.append( + GlancesSensor( + client, + name, + disk["mnt_point"], + SENSOR_TYPES[sensor_type][1], + sensor_type, + SENSOR_TYPES[sensor_type], + ) + ) + elif sensor_details[0] == "sensors": + # sensors will provide temp for different devices + for sensor in client.api.data[sensor_details[0]]: + dev.append( + GlancesSensor( + client, + name, + sensor["label"], + SENSOR_TYPES[sensor_type][1], + sensor_type, + SENSOR_TYPES[sensor_type], + ) + ) + elif client.api.data[sensor_details[0]]: + dev.append( + GlancesSensor( + client, + name, + "", + SENSOR_TYPES[sensor_type][1], + sensor_type, + SENSOR_TYPES[sensor_type], + ) + ) async_add_entities(dev, True) @@ -28,19 +66,29 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class GlancesSensor(Entity): """Implementation of a Glances sensor.""" - def __init__(self, glances_data, name, sensor_name, sensor_type): + def __init__( + self, + glances_data, + name, + sensor_name_prefix, + sensor_name_suffix, + sensor_type, + sensor_details, + ): """Initialize the sensor.""" self.glances_data = glances_data - self._sensor_name = sensor_name + self._sensor_name_prefix = sensor_name_prefix + self._sensor_name_suffix = sensor_name_suffix self._name = name self.type = sensor_type self._state = None - self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self.sensor_details = sensor_details + self.unsub_update = None @property def name(self): """Return the name of the sensor.""" - return f"{self._name} {self._sensor_name}" + return f"{self._name} {self._sensor_name_prefix} {self._sensor_name_suffix}" @property def unique_id(self): @@ -50,12 +98,12 @@ class GlancesSensor(Entity): @property def icon(self): """Icon to use in the frontend, if any.""" - return SENSOR_TYPES[self.type][2] + return self.sensor_details[3] @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" - return self._unit_of_measurement + return self.sensor_details[2] @property def available(self): @@ -74,7 +122,7 @@ class GlancesSensor(Entity): async def async_added_to_hass(self): """Handle entity which will be added.""" - async_dispatcher_connect( + self.unsub_update = async_dispatcher_connect( self.hass, DATA_UPDATED, self._schedule_immediate_update ) @@ -82,22 +130,40 @@ class GlancesSensor(Entity): def _schedule_immediate_update(self): self.async_schedule_update_ha_state(True) + async def will_remove_from_hass(self): + """Unsubscribe from update dispatcher.""" + if self.unsub_update: + self.unsub_update() + self.unsub_update = None + async def async_update(self): """Get the latest data from REST API.""" value = self.glances_data.api.data + if value is None: + return if value is not None: - if self.type == "disk_use_percent": - self._state = value["fs"][0]["percent"] - elif self.type == "disk_use": - self._state = round(value["fs"][0]["used"] / 1024 ** 3, 1) - elif self.type == "disk_free": - try: - self._state = round(value["fs"][0]["free"] / 1024 ** 3, 1) - except KeyError: - self._state = round( - (value["fs"][0]["size"] - value["fs"][0]["used"]) / 1024 ** 3, 1 - ) + if self.sensor_details[0] == "fs": + for var in value["fs"]: + if var["mnt_point"] == self._sensor_name_prefix: + disk = var + break + if self.type == "disk_use_percent": + self._state = disk["percent"] + elif self.type == "disk_use": + self._state = round(disk["used"] / 1024 ** 3, 1) + elif self.type == "disk_free": + try: + self._state = round(disk["free"] / 1024 ** 3, 1) + except KeyError: + self._state = round( + (disk["size"] - disk["used"]) / 1024 ** 3, 1, + ) + elif self.type == "sensor_temp": + for sensor in value["sensors"]: + if sensor["label"] == self._sensor_name_prefix: + self._state = sensor["value"] + break elif self.type == "memory_use_percent": self._state = value["mem"]["percent"] elif self.type == "memory_use": @@ -126,25 +192,6 @@ class GlancesSensor(Entity): self._state = value["processcount"]["sleeping"] elif self.type == "cpu_use_percent": self._state = value["quicklook"]["cpu"] - elif self.type == "cpu_temp": - for sensor in value["sensors"]: - if sensor["label"] in [ - "amdgpu 1", - "aml_thermal", - "Core 0", - "Core 1", - "CPU Temperature", - "CPU", - "cpu-thermal 1", - "cpu_thermal 1", - "exynos-therm 1", - "Package id 0", - "Physical id 0", - "radeon 1", - "soc-thermal 1", - "soc_thermal 1", - ]: - self._state = sensor["value"] elif self.type == "docker_active": count = 0 try: diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 7bd3583e5c2..60e4cdae6a5 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -180,7 +180,7 @@ class GoogleConfig(AbstractConfig): return 500 async def async_call_homegraph_api(self, url, data): - """Call a homegraph api with authenticaiton.""" + """Call a homegraph api with authentication.""" session = async_get_clientsession(self.hass) async def _call(): diff --git a/homeassistant/components/greeneye_monitor/manifest.json b/homeassistant/components/greeneye_monitor/manifest.json index 88183acf918..b10c4ad01a0 100644 --- a/homeassistant/components/greeneye_monitor/manifest.json +++ b/homeassistant/components/greeneye_monitor/manifest.json @@ -2,7 +2,7 @@ "domain": "greeneye_monitor", "name": "GreenEye Monitor (GEM)", "documentation": "https://www.home-assistant.io/integrations/greeneye_monitor", - "requirements": ["greeneye_monitor==1.0.1"], + "requirements": ["greeneye_monitor==2.0"], "dependencies": [], "codeowners": ["@jkeljo"] } diff --git a/homeassistant/components/greeneye_monitor/sensor.py b/homeassistant/components/greeneye_monitor/sensor.py index 19ef7529b0a..2640c701f92 100644 --- a/homeassistant/components/greeneye_monitor/sensor.py +++ b/homeassistant/components/greeneye_monitor/sensor.py @@ -294,12 +294,10 @@ class VoltageSensor(GEMSensor): def __init__(self, monitor_serial_number, number, name): """Construct the entity.""" super().__init__(monitor_serial_number, name, "volts", number) - self._monitor = None def _get_sensor(self, monitor): - """Wire the updates to a current channel.""" - self._monitor = monitor - return monitor.channels[self._number - 1] + """Wire the updates to the monitor itself, since there is no voltage element in the API.""" + return monitor @property def icon(self): @@ -309,10 +307,10 @@ class VoltageSensor(GEMSensor): @property def state(self): """Return the current voltage being reported by this sensor.""" - if not self._monitor.voltage: + if not self._sensor: return None - return self._monitor.voltage + return self._sensor.voltage @property def unit_of_measurement(self): diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index fc37f904e0d..7257959700f 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -13,6 +13,8 @@ from homeassistant.const import ( ATTR_NAME, CONF_ICON, CONF_NAME, + ENTITY_MATCH_ALL, + ENTITY_MATCH_NONE, SERVICE_RELOAD, STATE_CLOSED, STATE_HOME, @@ -134,7 +136,10 @@ def expand_entity_ids(hass: HomeAssistantType, entity_ids: Iterable[Any]) -> Lis """ found_ids: List[str] = [] for entity_id in entity_ids: - if not isinstance(entity_id, str): + if not isinstance(entity_id, str) or entity_id in ( + ENTITY_MATCH_NONE, + ENTITY_MATCH_ALL, + ): continue entity_id = entity_id.lower() diff --git a/homeassistant/components/hangouts/.translations/es-419.json b/homeassistant/components/hangouts/.translations/es-419.json index 3a297eb15ea..011060694a7 100644 --- a/homeassistant/components/hangouts/.translations/es-419.json +++ b/homeassistant/components/hangouts/.translations/es-419.json @@ -5,6 +5,7 @@ "unknown": "Se produjo un error desconocido." }, "error": { + "invalid_2fa": "Autenticaci\u00f3n de 2 factores no v\u00e1lida, intente nuevamente.", "invalid_login": "Inicio de sesi\u00f3n no v\u00e1lido, por favor, int\u00e9ntalo de nuevo." }, "step": { diff --git a/homeassistant/components/hangouts/.translations/pl.json b/homeassistant/components/hangouts/.translations/pl.json index 5da1e219799..1d08296007a 100644 --- a/homeassistant/components/hangouts/.translations/pl.json +++ b/homeassistant/components/hangouts/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Google Hangouts jest ju\u017c skonfigurowany", + "already_configured": "Google Hangouts jest ju\u017c skonfigurowany.", "unknown": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d." }, "error": { diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index c48d5fb00b0..bcc9d72ad08 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -397,7 +397,7 @@ class HarmonyRemote(remote.RemoteDevice): def write_config_file(self): """Write Harmony configuration file.""" _LOGGER.debug( - "%s: Writing hub config to file: %s", self.name, self._config_path + "%s: Writing hub configuration to file: %s", self.name, self._config_path ) if self._client.config is None: _LOGGER.warning("%s: No configuration received from hub", self.name) diff --git a/homeassistant/components/hdmi_cec/services.yaml b/homeassistant/components/hdmi_cec/services.yaml index f2e5f0b837a..63aee668062 100644 --- a/homeassistant/components/hdmi_cec/services.yaml +++ b/homeassistant/components/hdmi_cec/services.yaml @@ -3,7 +3,7 @@ select_device: description: Select HDMI device. fields: device: {description: 'Address of device to select. Can be entity_id, physical - address or alias from confuguration.', example: '"switch.hdmi_1" or "1.1.0.0" + address or alias from configuration.', example: '"switch.hdmi_1" or "1.1.0.0" or "01:10"'} send_command: description: Sends CEC command into HDMI CEC capable adapter. diff --git a/homeassistant/components/heos/.translations/es-419.json b/homeassistant/components/heos/.translations/es-419.json index 4d442a4543b..b0d1d7dc3fb 100644 --- a/homeassistant/components/heos/.translations/es-419.json +++ b/homeassistant/components/heos/.translations/es-419.json @@ -3,8 +3,12 @@ "abort": { "already_setup": "Solo puede configurar una sola conexi\u00f3n Heos, ya que ser\u00e1 compatible con todos los dispositivos de la red." }, + "error": { + "connection_failure": "No se puede conectar con el host especificado." + }, "step": { "user": { + "description": "Ingrese el nombre de host o la direcci\u00f3n IP de un dispositivo Heos (preferiblemente uno conectado por cable a la red).", "title": "Con\u00e9ctate a Heos" } }, diff --git a/homeassistant/components/hisense_aehw4a1/.translations/hu.json b/homeassistant/components/hisense_aehw4a1/.translations/hu.json new file mode 100644 index 00000000000..02716a96a73 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/hu.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "A h\u00e1l\u00f3zaton nem tal\u00e1lhat\u00f3 Hisense AEH-W4A1 eszk\u00f6z.", + "single_instance_allowed": "Csak egy konfigur\u00e1ci\u00f3 lehet Hisense AEH-W4A1 eset\u00e9n." + }, + "step": { + "confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani Hisense AEH-W4A1-et?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/sv.json b/homeassistant/components/hisense_aehw4a1/.translations/sv.json new file mode 100644 index 00000000000..6ec35452e8b --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Inga Hisense AEH-W4A1-enheter hittades i n\u00e4tverket.", + "single_instance_allowed": "Endast en enda konfiguration av Hisense AEH-W4A1 \u00e4r m\u00f6jlig." + }, + "step": { + "confirm": { + "description": "Vill du konfigurera Hisense AEH-W4A1?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index 8aa1d7e020a..7a0ae33345a 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -6,6 +6,7 @@ from typing import Awaitable import voluptuous as vol +from homeassistant.auth.permissions.const import CAT_ENTITIES, POLICY_CONTROL import homeassistant.config as conf_util from homeassistant.const import ( ATTR_ENTITY_ID, @@ -19,8 +20,8 @@ from homeassistant.const import ( SERVICE_TURN_ON, ) import homeassistant.core as ha -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_validation as cv, intent +from homeassistant.exceptions import HomeAssistantError, Unauthorized, UnknownUser +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.service import async_extract_entity_ids _LOGGER = logging.getLogger(__name__) @@ -54,6 +55,15 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: tasks = [] for domain, ent_ids in by_domain: + # This leads to endless loop. + if domain == DOMAIN: + _LOGGER.warning( + "Called service homeassistant.%s with invalid entity IDs %s", + service.service, + ", ".join(ent_ids), + ) + continue + # We want to block for all calls and only return when all calls # have been processed. If a service does not exist it causes a 10 # second delay while we're blocking waiting for a response. @@ -72,25 +82,19 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: hass.services.async_call(domain, service.service, data, blocking) ) - await asyncio.wait(tasks) + if tasks: + await asyncio.gather(*tasks) - hass.services.async_register(ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service) - hass.services.async_register(ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service) - hass.services.async_register(ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service) - hass.helpers.intent.async_register( - intent.ServiceIntentHandler( - intent.INTENT_TURN_ON, ha.DOMAIN, SERVICE_TURN_ON, "Turned {} on" - ) + service_schema = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids}, extra=vol.ALLOW_EXTRA) + + hass.services.async_register( + ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service, schema=service_schema ) - hass.helpers.intent.async_register( - intent.ServiceIntentHandler( - intent.INTENT_TURN_OFF, ha.DOMAIN, SERVICE_TURN_OFF, "Turned {} off" - ) + hass.services.async_register( + ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service, schema=service_schema ) - hass.helpers.intent.async_register( - intent.ServiceIntentHandler( - intent.INTENT_TOGGLE, ha.DOMAIN, SERVICE_TOGGLE, "Toggled {}" - ) + hass.services.async_register( + ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service, schema=service_schema ) async def async_handle_core_service(call): @@ -118,6 +122,25 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: async def async_handle_update_service(call): """Service handler for updating an entity.""" + if call.context.user_id: + user = await hass.auth.async_get_user(call.context.user_id) + + if user is None: + raise UnknownUser( + context=call.context, + permission=POLICY_CONTROL, + user_id=call.context.user_id, + ) + + for entity in call.data[ATTR_ENTITY_ID]: + if not user.permissions.check_entity(entity, POLICY_CONTROL): + raise Unauthorized( + context=call.context, + permission=POLICY_CONTROL, + user_id=call.context.user_id, + perm_category=CAT_ENTITIES, + ) + tasks = [ hass.helpers.entity_component.async_update_entity(entity) for entity in call.data[ATTR_ENTITY_ID] @@ -126,13 +149,13 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: if tasks: await asyncio.wait(tasks) - hass.services.async_register( + hass.helpers.service.async_register_admin_service( ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service ) - hass.services.async_register( + hass.helpers.service.async_register_admin_service( ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service ) - hass.services.async_register( + hass.helpers.service.async_register_admin_service( ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service ) hass.services.async_register( diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index af5f4cea828..9dad912886d 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -11,6 +11,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_STATE, CONF_ENTITIES, + CONF_ICON, CONF_ID, CONF_NAME, CONF_PLATFORM, @@ -75,16 +76,21 @@ CONF_SNAPSHOT = "snapshot_entities" DATA_PLATFORM = f"homeassistant_scene" STATES_SCHEMA = vol.All(dict, _convert_states) + PLATFORM_SCHEMA = vol.Schema( { vol.Required(CONF_PLATFORM): HA_DOMAIN, vol.Required(STATES): vol.All( cv.ensure_list, [ - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_ENTITIES): STATES_SCHEMA, - } + vol.Schema( + { + vol.Optional(CONF_ID): cv.string, + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_ICON): cv.icon, + vol.Required(CONF_ENTITIES): STATES_SCHEMA, + } + ) ], ), }, @@ -105,7 +111,7 @@ CREATE_SCENE_SCHEMA = vol.All( SERVICE_APPLY = "apply" SERVICE_CREATE = "create" -SCENECONFIG = namedtuple("SceneConfig", [CONF_NAME, STATES]) +SCENECONFIG = namedtuple("SceneConfig", [CONF_ID, CONF_NAME, CONF_ICON, STATES]) _LOGGER = logging.getLogger(__name__) @@ -213,7 +219,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= _LOGGER.warning("Empty scenes are not allowed") return - scene_config = SCENECONFIG(call.data[CONF_SCENE_ID], entities) + scene_config = SCENECONFIG(None, call.data[CONF_SCENE_ID], None, entities) entity_id = f"{SCENE_DOMAIN}.{scene_config.name}" old = platform.entities.get(entity_id) if old is not None: @@ -239,8 +245,12 @@ def _process_scenes_config(hass, async_add_entities, config): async_add_entities( HomeAssistantScene( hass, - SCENECONFIG(scene[CONF_NAME], scene[CONF_ENTITIES]), - scene.get(CONF_ID), + SCENECONFIG( + scene.get(CONF_ID), + scene[CONF_NAME], + scene.get(CONF_ICON), + scene[CONF_ENTITIES], + ), ) for scene in scene_config ) @@ -249,9 +259,8 @@ def _process_scenes_config(hass, async_add_entities, config): class HomeAssistantScene(Scene): """A scene is a group of entities and the states we want them to be.""" - def __init__(self, hass, scene_config, scene_id=None, from_service=False): + def __init__(self, hass, scene_config, from_service=False): """Initialize the scene.""" - self._id = scene_id self.hass = hass self.scene_config = scene_config self.from_service = from_service @@ -261,17 +270,23 @@ class HomeAssistantScene(Scene): """Return the name of the scene.""" return self.scene_config.name + @property + def icon(self): + """Return the icon of the scene.""" + return self.scene_config.icon + @property def unique_id(self): """Return unique ID.""" - return self._id + return self.scene_config.id @property def device_state_attributes(self): """Return the scene state attributes.""" attributes = {ATTR_ENTITY_ID: list(self.scene_config.states)} - if self._id is not None: - attributes[CONF_ID] = self._id + unique_id = self.unique_id + if unique_id is not None: + attributes[CONF_ID] = unique_id return attributes async def async_activate(self): diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 3fc6a0628ff..734568606b2 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -201,7 +201,7 @@ class Light(HomeAccessory): # But if it is set to 0, HomeKit will update the brightness to 100 as # it thinks 0 is off. # - # Therefore, if the the brighness is 0 and the device is still on, + # Therefore, if the the brightness is 0 and the device is still on, # the brightness is mapped to 1 otherwise the update is ignored in # order to avoid this incorrect behavior. if brightness == 0: diff --git a/homeassistant/components/homekit_controller/.translations/ca.json b/homeassistant/components/homekit_controller/.translations/ca.json index f2ed4bd0c21..1d2331870e1 100644 --- a/homeassistant/components/homekit_controller/.translations/ca.json +++ b/homeassistant/components/homekit_controller/.translations/ca.json @@ -3,7 +3,7 @@ "abort": { "accessory_not_found_error": "No s'ha pogut vincular, no s'ha trobat el dispositiu.", "already_configured": "Accessori ja configurat amb aquest controlador.", - "already_in_progress": "El flux de dades pel dispositiu ja est\u00e0 en curs.", + "already_in_progress": "El flux de dades de configuraci\u00f3 pel dispositiu ja est\u00e0 en curs.", "already_paired": "Aquest accessori ja est\u00e0 vinculat amb un altre dispositiu. Reinicia l'accessori i torna-ho a provar.", "ignored_model": "La disponibilitat de HomeKit per aquest model est\u00e0 bloquejada ja que, de moment, no hi ha una integraci\u00f3 nativa completa.", "invalid_config_entry": "Aquest dispositiu s'est\u00e0 mostrant com a llest per a ser vinculat per\u00f2, hi ha una entrada de configuraci\u00f3 conflictiva a Home Assistant que s'ha d'eliminar primer.", diff --git a/homeassistant/components/homekit_controller/.translations/de.json b/homeassistant/components/homekit_controller/.translations/de.json index e6942a125cd..8223616f11e 100644 --- a/homeassistant/components/homekit_controller/.translations/de.json +++ b/homeassistant/components/homekit_controller/.translations/de.json @@ -24,7 +24,7 @@ "data": { "pairing_code": "Kopplungscode" }, - "description": "Gebe deinen HomeKit-Kopplungscode ein, um dieses Zubeh\u00f6r zu verwenden", + "description": "Gib deinen HomeKit-Kopplungscode ein, um dieses Zubeh\u00f6r zu verwenden", "title": "Mit HomeKit Zubeh\u00f6r koppeln" }, "user": { diff --git a/homeassistant/components/homekit_controller/.translations/en.json b/homeassistant/components/homekit_controller/.translations/en.json index 31731a52203..72aa720b449 100644 --- a/homeassistant/components/homekit_controller/.translations/en.json +++ b/homeassistant/components/homekit_controller/.translations/en.json @@ -6,7 +6,7 @@ "already_in_progress": "Config flow for device is already in progress.", "already_paired": "This accessory is already paired to another device. Please reset the accessory and try again.", "ignored_model": "HomeKit support for this model is blocked as a more feature complete native integration is available.", - "invalid_config_entry": "This device is showing as ready to pair but there is already a conflicting config entry for it in Home Assistant that must first be removed.", + "invalid_config_entry": "This device is showing as ready to pair but there is already a conflicting configuration entry for it in Home Assistant that must first be removed.", "no_devices": "No unpaired devices could be found" }, "error": { diff --git a/homeassistant/components/homekit_controller/.translations/es-419.json b/homeassistant/components/homekit_controller/.translations/es-419.json index 67a65f752b4..a99011cf8b1 100644 --- a/homeassistant/components/homekit_controller/.translations/es-419.json +++ b/homeassistant/components/homekit_controller/.translations/es-419.json @@ -4,17 +4,26 @@ "already_configured": "El accesorio ya est\u00e1 configurado con este controlador.", "already_paired": "Este accesorio ya est\u00e1 emparejado con otro dispositivo. Por favor, reinicie el accesorio y vuelva a intentarlo." }, + "error": { + "pairing_failed": "Se produjo un error no controlado al intentar vincularse con este dispositivo. Esto puede ser una falla temporal o su dispositivo puede no ser compatible actualmente.", + "unable_to_pair": "No se puede vincular, por favor intente nuevamente.", + "unknown_error": "El dispositivo inform\u00f3 un error desconocido. Vinculaci\u00f3n fallida." + }, "flow_title": "Accesorio HomeKit: {name}", "step": { "pair": { "data": { "pairing_code": "C\u00f3digo de emparejamiento" - } + }, + "description": "Ingrese su c\u00f3digo de emparejamiento de HomeKit (en el formato XXX-XX-XXX) para usar este accesorio", + "title": "Vincular con el accesorio HomeKit" }, "user": { "data": { "device": "Dispositivo" - } + }, + "description": "Seleccione el dispositivo con el que desea vincular", + "title": "Vincular con el accesorio HomeKit" } }, "title": "Accesorio HomeKit" diff --git a/homeassistant/components/homekit_controller/.translations/hu.json b/homeassistant/components/homekit_controller/.translations/hu.json index 60bd173dc8e..264e635d7f4 100644 --- a/homeassistant/components/homekit_controller/.translations/hu.json +++ b/homeassistant/components/homekit_controller/.translations/hu.json @@ -1,11 +1,40 @@ { "config": { + "abort": { + "accessory_not_found_error": "Nem adhat\u00f3 hozz\u00e1 p\u00e1ros\u00edt\u00e1s, mert az eszk\u00f6z m\u00e1r nem tal\u00e1lhat\u00f3.", + "already_configured": "A tartoz\u00e9k m\u00e1r konfigur\u00e1lva van ezzel a vez\u00e9rl\u0151vel.", + "already_in_progress": "Az eszk\u00f6z konfigur\u00e1ci\u00f3ja m\u00e1r folyamatban van.", + "already_paired": "Ez a tartoz\u00e9k m\u00e1r p\u00e1ros\u00edtva van egy m\u00e1sik eszk\u00f6zzel. \u00c1ll\u00edtsa alaphelyzetbe a tartoz\u00e9kot, majd pr\u00f3b\u00e1lkozzon \u00fajra.", + "ignored_model": "A HomeKit t\u00e1mogat\u00e1sa e modelln\u00e9l blokkolva van, mivel a szolg\u00e1ltat\u00e1shoz teljes nat\u00edv integr\u00e1ci\u00f3 \u00e9rhet\u0151 el.", + "invalid_config_entry": "Ez az eszk\u00f6z k\u00e9szen \u00e1ll a p\u00e1ros\u00edt\u00e1sra, de m\u00e1r van egy \u00fctk\u00f6z\u0151 konfigur\u00e1ci\u00f3s bejegyz\u00e9s a Home Assistant-ben, amelyet el\u0151sz\u00f6r el kell t\u00e1vol\u00edtani.", + "no_devices": "Nem tal\u00e1lhat\u00f3 nem p\u00e1ros\u00edtott eszk\u00f6z" + }, + "error": { + "authentication_error": "Helytelen HomeKit k\u00f3d. K\u00e9rj\u00fck, ellen\u0151rizze, \u00e9s pr\u00f3b\u00e1lja \u00fajra.", + "busy_error": "Az eszk\u00f6z megtagadta a p\u00e1ros\u00edt\u00e1s hozz\u00e1ad\u00e1s\u00e1t, mivel m\u00e1r p\u00e1ros\u00edtva van egy m\u00e1sik vez\u00e9rl\u0151vel.", + "max_peers_error": "Az eszk\u00f6z megtagadta a p\u00e1ros\u00edt\u00e1s hozz\u00e1ad\u00e1s\u00e1t, mivel nincs ingyenes p\u00e1ros\u00edt\u00e1si t\u00e1rhely.", + "max_tries_error": "Az eszk\u00f6z megtagadta a p\u00e1ros\u00edt\u00e1s hozz\u00e1ad\u00e1s\u00e1t, mivel t\u00f6bb mint 100 sikertelen hiteles\u00edt\u00e9si k\u00eds\u00e9rletet kapott.", + "pairing_failed": "Nem kezelt hiba t\u00f6rt\u00e9nt az eszk\u00f6zzel val\u00f3 p\u00e1ros\u00edt\u00e1s sor\u00e1n. Lehet, hogy ez \u00e1tmeneti hiba, vagy jelenleg nem t\u00e1mogatja az eszk\u00f6zt.", + "unable_to_pair": "Nem siker\u00fclt p\u00e1ros\u00edtani, pr\u00f3b\u00e1ld \u00fajra.", + "unknown_error": "Az eszk\u00f6z ismeretlen hib\u00e1t jelentett. A p\u00e1ros\u00edt\u00e1s sikertelen." + }, + "flow_title": "HomeKit tartoz\u00e9k: {name}", "step": { + "pair": { + "data": { + "pairing_code": "P\u00e1ros\u00edt\u00e1si k\u00f3d" + }, + "description": "\u00cdrja be a HomeKit p\u00e1ros\u00edt\u00e1si k\u00f3dj\u00e1t (XXX-XX-XXX form\u00e1tumban) a kieg\u00e9sz\u00edt\u0151 haszn\u00e1lat\u00e1hoz", + "title": "HomeKit tartoz\u00e9k p\u00e1ros\u00edt\u00e1sa" + }, "user": { "data": { "device": "Eszk\u00f6z" - } + }, + "description": "V\u00e1lassza ki azt az eszk\u00f6zt, amelyet p\u00e1ros\u00edtani szeretne", + "title": "HomeKit tartoz\u00e9k p\u00e1ros\u00edt\u00e1sa" } - } + }, + "title": "HomeKit tartoz\u00e9k" } } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/.translations/it.json b/homeassistant/components/homekit_controller/.translations/it.json index 7ed026a529c..69bb1f13c84 100644 --- a/homeassistant/components/homekit_controller/.translations/it.json +++ b/homeassistant/components/homekit_controller/.translations/it.json @@ -6,7 +6,7 @@ "already_in_progress": "Il flusso di configurazione per il dispositivo \u00e8 gi\u00e0 in corso.", "already_paired": "Questo accessorio \u00e8 gi\u00e0 associato a un altro dispositivo. Si prega di resettare l'accessorio e riprovare.", "ignored_model": "Il supporto di HomeKit per questo modello \u00e8 bloccato poich\u00e9 \u00e8 disponibile un'integrazione nativa con pi\u00f9 funzionalit\u00e0.", - "invalid_config_entry": "Questo dispositivo viene visualizzato come pronto per l'associazione, ma c'\u00e8 gi\u00e0 una voce di configurazione in conflitto in Home Assistant che deve prima essere rimossa.", + "invalid_config_entry": "Questo dispositivo viene visualizzato come pronto per l'associazione, ma c'\u00e8 gi\u00e0 una voce di configurazione in conflitto in Home Assistant che prima deve essere rimossa.", "no_devices": "Non \u00e8 stato possibile trovare dispositivi non associati" }, "error": { diff --git a/homeassistant/components/homekit_controller/.translations/no.json b/homeassistant/components/homekit_controller/.translations/no.json index db8b8b035e0..a2816fa92f0 100644 --- a/homeassistant/components/homekit_controller/.translations/no.json +++ b/homeassistant/components/homekit_controller/.translations/no.json @@ -6,7 +6,7 @@ "already_in_progress": "Konfigurasjonsflyt for enhet p\u00e5g\u00e5r allerede.", "already_paired": "Dette tilbeh\u00f8ret er allerede sammenkoblet med en annen enhet. Vennligst tilbakestill tilbeh\u00f8ret og pr\u00f8v igjen.", "ignored_model": "HomeKit st\u00f8tte for denne modellen er blokkert da en mer funksjonsrik standard integrering er tilgjengelig.", - "invalid_config_entry": "Denne enheten vises som klar til \u00e5 sammenkoble, men det er allerede en motstridende konfigurasjonsoppf\u00f8ring for den i Home Assistant som m\u00e5 fjernes f\u00f8rst.", + "invalid_config_entry": "Denne enheten vises som klar til sammenkobling, men det er allerede en motstridende konfigurasjonsoppf\u00f8ring for den i Hjelpeassistenten som f\u00f8rst m\u00e5 fjernes.", "no_devices": "Ingen ukoblede enheter ble funnet" }, "error": { diff --git a/homeassistant/components/homekit_controller/.translations/pl.json b/homeassistant/components/homekit_controller/.translations/pl.json index e66353c5000..33cd20dc9c9 100644 --- a/homeassistant/components/homekit_controller/.translations/pl.json +++ b/homeassistant/components/homekit_controller/.translations/pl.json @@ -3,7 +3,7 @@ "abort": { "accessory_not_found_error": "Nie mo\u017cna rozpocz\u0105\u0107 parowania, poniewa\u017c nie znaleziono urz\u0105dzenia.", "already_configured": "Akcesorium jest ju\u017c skonfigurowane z tym kontrolerem.", - "already_in_progress": "Konfigurowanie urz\u0105dzenia jest ju\u017c w toku.", + "already_in_progress": "Konfiguracja urz\u0105dzenia jest ju\u017c w toku.", "already_paired": "To akcesorium jest ju\u017c sparowane z innym urz\u0105dzeniem. Zresetuj akcesorium i spr\u00f3buj ponownie.", "ignored_model": "Obs\u0142uga HomeKit dla tego modelu jest zablokowana, poniewa\u017c dost\u0119pna jest pe\u0142niejsza integracja natywna.", "invalid_config_entry": "To urz\u0105dzenie jest wy\u015bwietlane jako gotowe do sparowania, ale istnieje ju\u017c konfliktowy wpis konfiguracyjny dla niego w Home Assistant, kt\u00f3ry musi zosta\u0107 najpierw usuni\u0119ty.", diff --git a/homeassistant/components/homekit_controller/.translations/zh-Hant.json b/homeassistant/components/homekit_controller/.translations/zh-Hant.json index 68e87e9aea8..c2092c2016b 100644 --- a/homeassistant/components/homekit_controller/.translations/zh-Hant.json +++ b/homeassistant/components/homekit_controller/.translations/zh-Hant.json @@ -6,7 +6,7 @@ "already_in_progress": "\u8a2d\u5099\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", "already_paired": "\u914d\u4ef6\u5df2\u7d93\u8207\u5176\u4ed6\u8a2d\u5099\u914d\u5c0d\uff0c\u8acb\u91cd\u7f6e\u914d\u4ef6\u5f8c\u518d\u8a66\u4e00\u6b21\u3002", "ignored_model": "\u7531\u65bc\u6b64\u578b\u865f\u53ef\u539f\u751f\u652f\u63f4\u66f4\u5b8c\u6574\u529f\u80fd\uff0c\u56e0\u6b64 Homekit \u652f\u63f4\u5df2\u88ab\u7981\u6b62\u3002", - "invalid_config_entry": "\u8a2d\u5099\u986f\u793a\u7b49\u5f85\u9032\u884c\u914d\u5c0d\uff0c\u4f46 Home Assistant \u986f\u793a\u6709\u76f8\u885d\u7a81\u8a2d\u5b9a\u7269\u4ef6\u5fc5\u9808\u5148\u884c\u79fb\u9664\u3002", + "invalid_config_entry": "\u6b64\u8a2d\u5099\u986f\u793a\u7b49\u5f85\u9032\u884c\u914d\u5c0d\uff0c\u4f46 Home Assistant \u986f\u793a\u6709\u76f8\u885d\u7a81\u8a2d\u5b9a\u7269\u4ef6\u5fc5\u9808\u5148\u884c\u79fb\u9664\u3002", "no_devices": "\u627e\u4e0d\u5230\u4efb\u4f55\u672a\u914d\u5c0d\u8a2d\u5099" }, "error": { diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index dc65796a569..b2275282293 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -63,7 +63,7 @@ class HomeKitEntity(Entity): return False def setup(self): - """Configure an entity baed on its HomeKit characterstics metadata.""" + """Configure an entity baed on its HomeKit characteristics metadata.""" accessories = self._accessory.accessories get_uuid = CharacteristicsTypes.get_uuid @@ -124,7 +124,7 @@ class HomeKitEntity(Entity): """Collect new data from bridge and update the entity state in hass.""" accessory_state = self._accessory.current_state.get(self._aid, {}) for iid, result in accessory_state.items(): - # No value so dont process this result + # No value so don't process this result if "value" not in result: continue diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 507a5cbb70a..559e0b4a997 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -262,7 +262,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): _LOGGER.info( ( "Legacy configuration %s for homekit" - "accessory migrated to config entries" + "accessory migrated to configuration entries" ), hkid, ) @@ -352,6 +352,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): return self._async_step_pair_show_form(errors) + @callback def _async_step_pair_show_form(self, errors=None): return self.async_show_form( step_id="pair", diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 64581da45b1..11cb607842a 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -77,7 +77,7 @@ class HKDevice: # The platorms we have forwarded the config entry so far. If a new # accessory is added to a bridge we may have to load additional # platforms. We don't want to load all platforms up front if its just - # a lightbulb. And we dont want to forward a config entry twice + # a lightbulb. And we don't want to forward a config entry twice # (triggers a Config entry already set up error) self.platforms = set() @@ -331,7 +331,7 @@ class HKDevice: key = (row["aid"], row["iid"]) # If the key was returned by put_characteristics() then the - # change didnt work + # change didn't work if key in results: continue diff --git a/homeassistant/components/homekit_controller/strings.json b/homeassistant/components/homekit_controller/strings.json index b51dcb1f6d8..55718e35b59 100644 --- a/homeassistant/components/homekit_controller/strings.json +++ b/homeassistant/components/homekit_controller/strings.json @@ -32,7 +32,7 @@ "already_paired": "This accessory is already paired to another device. Please reset the accessory and try again.", "ignored_model": "HomeKit support for this model is blocked as a more feature complete native integration is available.", "already_configured": "Accessory is already configured with this controller.", - "invalid_config_entry": "This device is showing as ready to pair but there is already a conflicting config entry for it in Home Assistant that must first be removed.", + "invalid_config_entry": "This device is showing as ready to pair but there is already a conflicting configuration entry for it in Home Assistant that must first be removed.", "accessory_not_found_error": "Cannot add pairing as device can no longer be found.", "already_in_progress": "Config flow for device is already in progress." } diff --git a/homeassistant/components/homematic/manifest.json b/homeassistant/components/homematic/manifest.json index c4e09c36b8e..edf07c3e4d7 100644 --- a/homeassistant/components/homematic/manifest.json +++ b/homeassistant/components/homematic/manifest.json @@ -2,7 +2,7 @@ "domain": "homematic", "name": "Homematic", "documentation": "https://www.home-assistant.io/integrations/homematic", - "requirements": ["pyhomematic==0.1.63"], + "requirements": ["pyhomematic==0.1.64"], "dependencies": [], "codeowners": ["@pvizeli", "@danielperna84"] } diff --git a/homeassistant/components/homematicip_cloud/.translations/es-419.json b/homeassistant/components/homematicip_cloud/.translations/es-419.json index 5102b25aaee..0919e211617 100644 --- a/homeassistant/components/homematicip_cloud/.translations/es-419.json +++ b/homeassistant/components/homematicip_cloud/.translations/es-419.json @@ -16,7 +16,8 @@ "hapid": "ID de punto de acceso (SGTIN)", "name": "Nombre (opcional, usado como prefijo de nombre para todos los dispositivos)", "pin": "C\u00f3digo PIN (opcional)" - } + }, + "title": "Elija el punto de acceso HomematicIP" }, "link": { "description": "Presione el bot\u00f3n azul en el punto de acceso y el bot\u00f3n enviar para registrar HomematicIP con Home Assistant. \n\n ! [Ubicaci\u00f3n del bot\u00f3n en el puente] (/static/images/config_flows/config_homematicip_cloud.png)" diff --git a/homeassistant/components/homematicip_cloud/.translations/pl.json b/homeassistant/components/homematicip_cloud/.translations/pl.json index 7c8714c2c11..78905da208e 100644 --- a/homeassistant/components/homematicip_cloud/.translations/pl.json +++ b/homeassistant/components/homematicip_cloud/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Punkt dost\u0119pu jest ju\u017c skonfigurowany", + "already_configured": "Punkt dost\u0119pu jest ju\u017c skonfigurowany.", "connection_aborted": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z serwerem HMIP", "unknown": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d" }, diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index 46bf300753f..d1982e289a3 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -1,20 +1,13 @@ """Support for HomematicIP Cloud devices.""" import logging -from pathlib import Path -from typing import Optional -from homematicip.aio.device import AsyncSwitchMeasuring -from homematicip.aio.group import AsyncHeatingGroup -from homematicip.aio.home import AsyncHome -from homematicip.base.helpers import handle_config import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME +from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import comp_entity_ids from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .const import ( @@ -27,29 +20,10 @@ from .const import ( ) from .device import HomematicipGenericDevice # noqa: F401 from .hap import HomematicipAuth, HomematicipHAP # noqa: F401 +from .services import async_setup_services, async_unload_services _LOGGER = logging.getLogger(__name__) -ATTR_ACCESSPOINT_ID = "accesspoint_id" -ATTR_ANONYMIZE = "anonymize" -ATTR_CLIMATE_PROFILE_INDEX = "climate_profile_index" -ATTR_CONFIG_OUTPUT_FILE_PREFIX = "config_output_file_prefix" -ATTR_CONFIG_OUTPUT_PATH = "config_output_path" -ATTR_DURATION = "duration" -ATTR_ENDTIME = "endtime" -ATTR_TEMPERATURE = "temperature" - -DEFAULT_CONFIG_FILE_PREFIX = "hmip-config" - -SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION = "activate_eco_mode_with_duration" -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_DUMP_HAP_CONFIG = "dump_hap_config" -SERVICE_RESET_ENERGY_COUNTER = "reset_energy_counter" -SERVICE_SET_ACTIVE_CLIMATE_PROFILE = "set_active_climate_profile" - CONFIG_SCHEMA = vol.Schema( { vol.Optional(DOMAIN, default=[]): vol.All( @@ -68,59 +42,6 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -SCHEMA_ACTIVATE_ECO_MODE_WITH_DURATION = vol.Schema( - { - vol.Required(ATTR_DURATION): cv.positive_int, - vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24)), - } -) - -SCHEMA_ACTIVATE_ECO_MODE_WITH_PERIOD = vol.Schema( - { - vol.Required(ATTR_ENDTIME): cv.datetime, - vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24)), - } -) - -SCHEMA_ACTIVATE_VACATION = vol.Schema( - { - vol.Required(ATTR_ENDTIME): cv.datetime, - vol.Required(ATTR_TEMPERATURE, default=18.0): vol.All( - vol.Coerce(float), vol.Range(min=0, max=55) - ), - vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24)), - } -) - -SCHEMA_DEACTIVATE_ECO_MODE = vol.Schema( - {vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24))} -) - -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, - } -) - -SCHEMA_DUMP_HAP_CONFIG = vol.Schema( - { - vol.Optional(ATTR_CONFIG_OUTPUT_PATH): cv.string, - vol.Optional( - ATTR_CONFIG_OUTPUT_FILE_PREFIX, default=DEFAULT_CONFIG_FILE_PREFIX - ): cv.string, - vol.Optional(ATTR_ANONYMIZE, default=True): cv.boolean, - } -) - -SCHEMA_RESET_ENERGY_COUNTER = vol.Schema( - {vol.Required(ATTR_ENTITY_ID): comp_entity_ids} -) - async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Set up the HomematicIP Cloud component.""" @@ -145,189 +66,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: ) ) - async def _async_activate_eco_mode_with_duration(service) -> None: - """Service to activate eco mode with duration.""" - duration = service.data[ATTR_DURATION] - hapid = service.data.get(ATTR_ACCESSPOINT_ID) - - if hapid: - home = _get_home(hapid) - if home: - await home.activate_absence_with_duration(duration) - else: - for hap in hass.data[DOMAIN].values(): - await hap.home.activate_absence_with_duration(duration) - - hass.services.async_register( - DOMAIN, - SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION, - _async_activate_eco_mode_with_duration, - schema=SCHEMA_ACTIVATE_ECO_MODE_WITH_DURATION, - ) - - async def _async_activate_eco_mode_with_period(service) -> None: - """Service to activate eco mode with period.""" - endtime = service.data[ATTR_ENDTIME] - hapid = service.data.get(ATTR_ACCESSPOINT_ID) - - if hapid: - home = _get_home(hapid) - if home: - await home.activate_absence_with_period(endtime) - else: - for hap in hass.data[DOMAIN].values(): - await hap.home.activate_absence_with_period(endtime) - - hass.services.async_register( - DOMAIN, - SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD, - _async_activate_eco_mode_with_period, - schema=SCHEMA_ACTIVATE_ECO_MODE_WITH_PERIOD, - ) - - async def _async_activate_vacation(service) -> None: - """Service to activate vacation.""" - endtime = service.data[ATTR_ENDTIME] - temperature = service.data[ATTR_TEMPERATURE] - hapid = service.data.get(ATTR_ACCESSPOINT_ID) - - if hapid: - home = _get_home(hapid) - if home: - await home.activate_vacation(endtime, temperature) - else: - for hap in hass.data[DOMAIN].values(): - await hap.home.activate_vacation(endtime, temperature) - - hass.services.async_register( - DOMAIN, - SERVICE_ACTIVATE_VACATION, - _async_activate_vacation, - schema=SCHEMA_ACTIVATE_VACATION, - ) - - async def _async_deactivate_eco_mode(service) -> None: - """Service to deactivate eco mode.""" - hapid = service.data.get(ATTR_ACCESSPOINT_ID) - - if hapid: - home = _get_home(hapid) - if home: - await home.deactivate_absence() - else: - for hap in hass.data[DOMAIN].values(): - await hap.home.deactivate_absence() - - hass.services.async_register( - DOMAIN, - SERVICE_DEACTIVATE_ECO_MODE, - _async_deactivate_eco_mode, - schema=SCHEMA_DEACTIVATE_ECO_MODE, - ) - - async def _async_deactivate_vacation(service) -> None: - """Service to deactivate vacation.""" - hapid = service.data.get(ATTR_ACCESSPOINT_ID) - - if hapid: - home = _get_home(hapid) - if home: - await home.deactivate_vacation() - else: - for hap in hass.data[DOMAIN].values(): - await hap.home.deactivate_vacation() - - hass.services.async_register( - DOMAIN, - SERVICE_DEACTIVATE_VACATION, - _async_deactivate_vacation, - schema=SCHEMA_DEACTIVATE_VACATION, - ) - - async def _set_active_climate_profile(service) -> None: - """Service to set the active climate profile.""" - entity_id_list = service.data[ATTR_ENTITY_ID] - climate_profile_index = service.data[ATTR_CLIMATE_PROFILE_INDEX] - 1 - - 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 and isinstance(group, AsyncHeatingGroup): - 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, - ) - - async def _async_dump_hap_config(service) -> None: - """Service to dump the configuration of a Homematic IP Access Point.""" - config_path = ( - service.data.get(ATTR_CONFIG_OUTPUT_PATH) or hass.config.config_dir - ) - config_file_prefix = service.data[ATTR_CONFIG_OUTPUT_FILE_PREFIX] - anonymize = service.data[ATTR_ANONYMIZE] - - for hap in hass.data[DOMAIN].values(): - hap_sgtin = hap.config_entry.unique_id - - if anonymize: - hap_sgtin = hap_sgtin[-4:] - - file_name = f"{config_file_prefix}_{hap_sgtin}.json" - path = Path(config_path) - config_file = path / file_name - - json_state = await hap.home.download_configuration() - json_state = handle_config(json_state, anonymize) - - config_file.write_text(json_state, encoding="utf8") - - hass.services.async_register( - DOMAIN, - SERVICE_DUMP_HAP_CONFIG, - _async_dump_hap_config, - schema=SCHEMA_DUMP_HAP_CONFIG, - ) - - async def _async_reset_energy_counter(service): - """Service to reset the energy counter.""" - entity_id_list = service.data[ATTR_ENTITY_ID] - - for hap in hass.data[DOMAIN].values(): - if entity_id_list != "all": - for entity_id in entity_id_list: - device = hap.hmip_device_by_entity_id.get(entity_id) - if device and isinstance(device, AsyncSwitchMeasuring): - await device.reset_energy_counter() - else: - for device in hap.home.devices: - if isinstance(device, AsyncSwitchMeasuring): - await device.reset_energy_counter() - - hass.helpers.service.async_register_admin_service( - DOMAIN, - SERVICE_RESET_ENERGY_COUNTER, - _async_reset_energy_counter, - schema=SCHEMA_RESET_ENERGY_COUNTER, - ) - - def _get_home(hapid: str) -> Optional[AsyncHome]: - """Return a HmIP home.""" - 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 @@ -348,6 +86,13 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool if not await hap.async_setup(): return False + await async_setup_services(hass) + + # Register on HA stop event to gracefully shutdown HomematicIP Cloud connection + hap.reset_connection_listener = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, hap.shutdown + ) + # Register hap as device in registry. device_registry = await dr.async_get_registry(hass) home = hap.home @@ -367,4 +112,8 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Unload a config entry.""" hap = hass.data[DOMAIN].pop(entry.unique_id) + hap.reset_connection_listener() + + await async_unload_services(hass) + return await hap.async_reset() diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index c2f4d833a35..f5316350091 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -16,6 +16,7 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) +from homeassistant.core import callback from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN as HMIPC_DOMAIN @@ -95,10 +96,18 @@ class HomematicipAlarmControlPanel(AlarmControlPanel): """Register callbacks.""" self._home.on_update(self._async_device_changed) + @callback def _async_device_changed(self, *args, **kwargs) -> None: """Handle device state changes.""" - _LOGGER.debug("Event %s (%s)", self.name, CONST_ALARM_CONTROL_PANEL_NAME) - self.async_schedule_update_ha_state() + # Don't update disabled entities + if self.enabled: + _LOGGER.debug("Event %s (%s)", self.name, CONST_ALARM_CONTROL_PANEL_NAME) + self.async_schedule_update_ha_state() + else: + _LOGGER.debug( + "Device Changed Event for %s (Alarm Control Panel) not fired. Entity is disabled.", + self.name, + ) @property def name(self) -> str: diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index 0d2131f9cb3..768c893a100 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -7,6 +7,7 @@ from homematicip.aio.device import ( AsyncFullFlushShutter, AsyncGarageDoorModuleTormatic, ) +from homematicip.aio.group import AsyncExtendedLinkedShutterGroup from homematicip.base.enums import DoorCommand, DoorState from homeassistant.components.cover import ( @@ -18,6 +19,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericDevice +from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -41,6 +43,10 @@ async def async_setup_entry( elif isinstance(device, AsyncGarageDoorModuleTormatic): entities.append(HomematicipGarageDoorModuleTormatic(hap, device)) + for group in hap.home.groups: + if isinstance(group, AsyncExtendedLinkedShutterGroup): + entities.append(HomematicipCoverShutterGroup(hap, group)) + if entities: async_add_entities(entities) @@ -142,3 +148,12 @@ class HomematicipGarageDoorModuleTormatic(HomematicipGenericDevice, CoverDevice) async def async_stop_cover(self, **kwargs) -> None: """Stop the cover.""" await self._device.send_door_command(DoorCommand.STOP) + + +class HomematicipCoverShutterGroup(HomematicipCoverSlats, CoverDevice): + """Representation of a HomematicIP Cloud cover shutter group.""" + + def __init__(self, hap: HomematicipHAP, device, post: str = "ShutterGroup") -> None: + """Initialize switching group.""" + device.modelType = f"HmIP-{post}" + super().__init__(hap, device, post) diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 3c97cc1af9f..0d6fc726050 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -81,6 +81,7 @@ class HomematicipHAP: self._tries = 0 self._accesspoint_connected = True self.hmip_device_by_entity_id = {} + self.reset_connection_listener = None async def async_setup(self, tries: int = 0) -> bool: """Initialize connection.""" @@ -93,6 +94,9 @@ class HomematicipHAP: ) except HmipcConnectionError: raise ConfigEntryNotReady + except Exception as err: # pylint: disable=broad-except + _LOGGER.error("Error connecting with HomematicIP Cloud: %s", err) + return False _LOGGER.info( "Connected to HomematicIP with HAP %s", self.config_entry.unique_id @@ -223,6 +227,17 @@ class HomematicipHAP: self.hmip_device_by_entity_id = {} return True + @callback + def shutdown(self, event) -> None: + """Wrap the call to async_reset. + + Used as an argument to EventBus.async_listen_once. + """ + self.hass.async_create_task(self.async_reset()) + _LOGGER.debug( + "Reset connection to access point id %s", self.config_entry.unique_id + ) + async def get_hap( self, hass: HomeAssistantType, hapid: str, authtoken: str, name: str ) -> AsyncHome: diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index d823621f6cb..9ecdb0ad80d 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -3,7 +3,7 @@ "name": "HomematicIP Cloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", - "requirements": ["homematicip==0.10.15"], + "requirements": ["homematicip==0.10.17"], "dependencies": [], "codeowners": ["@SukramJ"], "quality_scale": "platinum" diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index ebbee1abc44..d6a226a83dc 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -14,6 +14,7 @@ from homematicip.aio.device import ( AsyncPassageDetector, AsyncPlugableSwitchMeasuring, AsyncPresenceDetectorIndoor, + AsyncRoomControlDeviceAnalog, AsyncTemperatureHumiditySensorDisplay, AsyncTemperatureHumiditySensorOutdoor, AsyncTemperatureHumiditySensorWithoutDisplay, @@ -79,6 +80,8 @@ async def async_setup_entry( ): entities.append(HomematicipTemperatureSensor(hap, device)) entities.append(HomematicipHumiditySensor(hap, device)) + elif isinstance(device, (AsyncRoomControlDeviceAnalog,)): + entities.append(HomematicipTemperatureSensor(hap, device)) if isinstance( device, ( @@ -305,7 +308,7 @@ class HomematicipPowerSensor(HomematicipGenericDevice): @property def state(self) -> float: - """Representation of the HomematicIP power comsumption value.""" + """Representation of the HomematicIP power consumption value.""" return self._device.currentPowerConsumption @property @@ -356,7 +359,7 @@ class HomematicipTodayRainSensor(HomematicipGenericDevice): @property def state(self) -> float: - """Representation of the HomematicIP todays rain value.""" + """Representation of the HomematicIP today's rain value.""" return round(self._device.todayRainCounter, 2) @property diff --git a/homeassistant/components/homematicip_cloud/services.py b/homeassistant/components/homematicip_cloud/services.py new file mode 100644 index 00000000000..193cac94629 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/services.py @@ -0,0 +1,357 @@ +"""Support for HomematicIP Cloud devices.""" +import logging +from pathlib import Path +from typing import Optional + +from homematicip.aio.device import AsyncSwitchMeasuring +from homematicip.aio.group import AsyncHeatingGroup +from homematicip.aio.home import AsyncHome +from homematicip.base.helpers import handle_config +import voluptuous as vol + +from homeassistant.const import ATTR_ENTITY_ID +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.config_validation import comp_entity_ids +from homeassistant.helpers.typing import HomeAssistantType, ServiceCallType + +from .const import DOMAIN as HMIPC_DOMAIN + +_LOGGER = logging.getLogger(__name__) + +ATTR_ACCESSPOINT_ID = "accesspoint_id" +ATTR_ANONYMIZE = "anonymize" +ATTR_CLIMATE_PROFILE_INDEX = "climate_profile_index" +ATTR_CONFIG_OUTPUT_FILE_PREFIX = "config_output_file_prefix" +ATTR_CONFIG_OUTPUT_PATH = "config_output_path" +ATTR_DURATION = "duration" +ATTR_ENDTIME = "endtime" +ATTR_TEMPERATURE = "temperature" + +DEFAULT_CONFIG_FILE_PREFIX = "hmip-config" + +SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION = "activate_eco_mode_with_duration" +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_DUMP_HAP_CONFIG = "dump_hap_config" +SERVICE_RESET_ENERGY_COUNTER = "reset_energy_counter" +SERVICE_SET_ACTIVE_CLIMATE_PROFILE = "set_active_climate_profile" + +HMIPC_SERVICES2 = { + SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION: "_async_activate_eco_mode_with_duration", + SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD: "_async_activate_eco_mode_with_period", + SERVICE_ACTIVATE_VACATION: "_async_activate_vacation", + SERVICE_DEACTIVATE_ECO_MODE: "SERVICE_DEACTIVATE_ECO_MODE", + SERVICE_DEACTIVATE_VACATION: "_async_deactivate_vacation", + SERVICE_DUMP_HAP_CONFIG: "_async_dump_hap_config", + SERVICE_RESET_ENERGY_COUNTER: "_async_reset_energy_counter", + SERVICE_SET_ACTIVE_CLIMATE_PROFILE: "_set_active_climate_profile", +} + +HMIPC_SERVICES = [ + SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION, + SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD, + SERVICE_ACTIVATE_VACATION, + SERVICE_DEACTIVATE_ECO_MODE, + SERVICE_DEACTIVATE_VACATION, + SERVICE_DUMP_HAP_CONFIG, + SERVICE_RESET_ENERGY_COUNTER, + SERVICE_SET_ACTIVE_CLIMATE_PROFILE, +] + +SCHEMA_ACTIVATE_ECO_MODE_WITH_DURATION = vol.Schema( + { + vol.Required(ATTR_DURATION): cv.positive_int, + vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24)), + } +) + +SCHEMA_ACTIVATE_ECO_MODE_WITH_PERIOD = vol.Schema( + { + vol.Required(ATTR_ENDTIME): cv.datetime, + vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24)), + } +) + +SCHEMA_ACTIVATE_VACATION = vol.Schema( + { + vol.Required(ATTR_ENDTIME): cv.datetime, + vol.Required(ATTR_TEMPERATURE, default=18.0): vol.All( + vol.Coerce(float), vol.Range(min=0, max=55) + ), + vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24)), + } +) + +SCHEMA_DEACTIVATE_ECO_MODE = vol.Schema( + {vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24))} +) + +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, + } +) + +SCHEMA_DUMP_HAP_CONFIG = vol.Schema( + { + vol.Optional(ATTR_CONFIG_OUTPUT_PATH): cv.string, + vol.Optional( + ATTR_CONFIG_OUTPUT_FILE_PREFIX, default=DEFAULT_CONFIG_FILE_PREFIX + ): cv.string, + vol.Optional(ATTR_ANONYMIZE, default=True): cv.boolean, + } +) + +SCHEMA_RESET_ENERGY_COUNTER = vol.Schema( + {vol.Required(ATTR_ENTITY_ID): comp_entity_ids} +) + + +async def async_setup_services(hass: HomeAssistantType) -> None: + """Set up the HomematicIP Cloud services.""" + + if hass.services.async_services().get(HMIPC_DOMAIN): + return + + async def async_call_hmipc_service(service: ServiceCallType): + """Call correct HomematicIP Cloud service.""" + service_name = service.service + + if service_name == SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION: + await _async_activate_eco_mode_with_duration(hass, service) + elif service_name == SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD: + await _async_activate_eco_mode_with_period(hass, service) + elif service_name == SERVICE_ACTIVATE_VACATION: + await _async_activate_vacation(hass, service) + elif service_name == SERVICE_DEACTIVATE_ECO_MODE: + await _async_deactivate_eco_mode(hass, service) + elif service_name == SERVICE_DEACTIVATE_VACATION: + await _async_deactivate_vacation(hass, service) + elif service_name == SERVICE_DUMP_HAP_CONFIG: + await _async_dump_hap_config(hass, service) + elif service_name == SERVICE_RESET_ENERGY_COUNTER: + await _async_reset_energy_counter(hass, service) + elif service_name == SERVICE_SET_ACTIVE_CLIMATE_PROFILE: + await _set_active_climate_profile(hass, service) + + hass.services.async_register( + HMIPC_DOMAIN, + SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION, + async_call_hmipc_service, + schema=SCHEMA_ACTIVATE_ECO_MODE_WITH_DURATION, + ) + + hass.services.async_register( + HMIPC_DOMAIN, + SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD, + async_call_hmipc_service, + schema=SCHEMA_ACTIVATE_ECO_MODE_WITH_PERIOD, + ) + + hass.services.async_register( + HMIPC_DOMAIN, + SERVICE_ACTIVATE_VACATION, + async_call_hmipc_service, + schema=SCHEMA_ACTIVATE_VACATION, + ) + + hass.services.async_register( + HMIPC_DOMAIN, + SERVICE_DEACTIVATE_ECO_MODE, + async_call_hmipc_service, + schema=SCHEMA_DEACTIVATE_ECO_MODE, + ) + + hass.services.async_register( + HMIPC_DOMAIN, + SERVICE_DEACTIVATE_VACATION, + async_call_hmipc_service, + schema=SCHEMA_DEACTIVATE_VACATION, + ) + + hass.services.async_register( + HMIPC_DOMAIN, + SERVICE_SET_ACTIVE_CLIMATE_PROFILE, + async_call_hmipc_service, + schema=SCHEMA_SET_ACTIVE_CLIMATE_PROFILE, + ) + + hass.services.async_register( + HMIPC_DOMAIN, + SERVICE_DUMP_HAP_CONFIG, + async_call_hmipc_service, + schema=SCHEMA_DUMP_HAP_CONFIG, + ) + + hass.helpers.service.async_register_admin_service( + HMIPC_DOMAIN, + SERVICE_RESET_ENERGY_COUNTER, + async_call_hmipc_service, + schema=SCHEMA_RESET_ENERGY_COUNTER, + ) + + +async def async_unload_services(hass: HomeAssistantType): + """Unload HomematicIP Cloud services.""" + if hass.data[HMIPC_DOMAIN]: + return + + for hmipc_service in HMIPC_SERVICES: + hass.services.async_remove(HMIPC_DOMAIN, hmipc_service) + + +async def _async_activate_eco_mode_with_duration( + hass: HomeAssistantType, service: ServiceCallType +) -> None: + """Service to activate eco mode with duration.""" + duration = service.data[ATTR_DURATION] + hapid = service.data.get(ATTR_ACCESSPOINT_ID) + + if hapid: + home = _get_home(hass, hapid) + if home: + await home.activate_absence_with_duration(duration) + else: + for hap in hass.data[HMIPC_DOMAIN].values(): + await hap.home.activate_absence_with_duration(duration) + + +async def _async_activate_eco_mode_with_period( + hass: HomeAssistantType, service: ServiceCallType +) -> None: + """Service to activate eco mode with period.""" + endtime = service.data[ATTR_ENDTIME] + hapid = service.data.get(ATTR_ACCESSPOINT_ID) + + if hapid: + home = _get_home(hass, hapid) + if home: + await home.activate_absence_with_period(endtime) + else: + for hap in hass.data[HMIPC_DOMAIN].values(): + await hap.home.activate_absence_with_period(endtime) + + +async def _async_activate_vacation( + hass: HomeAssistantType, service: ServiceCallType +) -> None: + """Service to activate vacation.""" + endtime = service.data[ATTR_ENDTIME] + temperature = service.data[ATTR_TEMPERATURE] + hapid = service.data.get(ATTR_ACCESSPOINT_ID) + + if hapid: + home = _get_home(hass, hapid) + if home: + await home.activate_vacation(endtime, temperature) + else: + for hap in hass.data[HMIPC_DOMAIN].values(): + await hap.home.activate_vacation(endtime, temperature) + + +async def _async_deactivate_eco_mode( + hass: HomeAssistantType, service: ServiceCallType +) -> None: + """Service to deactivate eco mode.""" + hapid = service.data.get(ATTR_ACCESSPOINT_ID) + + if hapid: + home = _get_home(hass, hapid) + if home: + await home.deactivate_absence() + else: + for hap in hass.data[HMIPC_DOMAIN].values(): + await hap.home.deactivate_absence() + + +async def _async_deactivate_vacation( + hass: HomeAssistantType, service: ServiceCallType +) -> None: + """Service to deactivate vacation.""" + hapid = service.data.get(ATTR_ACCESSPOINT_ID) + + if hapid: + home = _get_home(hass, hapid) + if home: + await home.deactivate_vacation() + else: + for hap in hass.data[HMIPC_DOMAIN].values(): + await hap.home.deactivate_vacation() + + +async def _set_active_climate_profile( + hass: HomeAssistantType, service: ServiceCallType +) -> None: + """Service to set the active climate profile.""" + entity_id_list = service.data[ATTR_ENTITY_ID] + climate_profile_index = service.data[ATTR_CLIMATE_PROFILE_INDEX] - 1 + + for hap in hass.data[HMIPC_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 and isinstance(group, AsyncHeatingGroup): + 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) + + +async def _async_dump_hap_config( + hass: HomeAssistantType, service: ServiceCallType +) -> None: + """Service to dump the configuration of a Homematic IP Access Point.""" + config_path = service.data.get(ATTR_CONFIG_OUTPUT_PATH) or hass.config.config_dir + config_file_prefix = service.data[ATTR_CONFIG_OUTPUT_FILE_PREFIX] + anonymize = service.data[ATTR_ANONYMIZE] + + for hap in hass.data[HMIPC_DOMAIN].values(): + hap_sgtin = hap.config_entry.unique_id + + if anonymize: + hap_sgtin = hap_sgtin[-4:] + + file_name = f"{config_file_prefix}_{hap_sgtin}.json" + path = Path(config_path) + config_file = path / file_name + + json_state = await hap.home.download_configuration() + json_state = handle_config(json_state, anonymize) + + config_file.write_text(json_state, encoding="utf8") + + +async def _async_reset_energy_counter( + hass: HomeAssistantType, service: ServiceCallType +): + """Service to reset the energy counter.""" + entity_id_list = service.data[ATTR_ENTITY_ID] + + for hap in hass.data[HMIPC_DOMAIN].values(): + if entity_id_list != "all": + for entity_id in entity_id_list: + device = hap.hmip_device_by_entity_id.get(entity_id) + if device and isinstance(device, AsyncSwitchMeasuring): + await device.reset_energy_counter() + else: + for device in hap.home.devices: + if isinstance(device, AsyncSwitchMeasuring): + await device.reset_energy_counter() + + +def _get_home(hass: HomeAssistantType, hapid: str) -> Optional[AsyncHome]: + """Return a HmIP home.""" + hap = hass.data[HMIPC_DOMAIN].get(hapid) + if hap: + return hap.home + + _LOGGER.info("No matching access point found for access point id %s", hapid) + return None diff --git a/homeassistant/components/huawei_lte/.translations/ca.json b/homeassistant/components/huawei_lte/.translations/ca.json index 7a3ae23e275..a52d101a486 100644 --- a/homeassistant/components/huawei_lte/.translations/ca.json +++ b/homeassistant/components/huawei_lte/.translations/ca.json @@ -33,7 +33,7 @@ "step": { "init": { "data": { - "name": "Nom del servei de notificacions", + "name": "Nom del servei de notificacions (reinici necessari si canvia)", "recipient": "Destinataris de notificacions SMS", "track_new_devices": "Segueix dispositius nous" } diff --git a/homeassistant/components/huawei_lte/.translations/hu.json b/homeassistant/components/huawei_lte/.translations/hu.json new file mode 100644 index 00000000000..9f012c1c405 --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/hu.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Ez az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "Ez az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "not_huawei_lte": "Nem Huawei LTE eszk\u00f6z" + }, + "error": { + "connection_failed": "Kapcsol\u00f3d\u00e1s sikertelen", + "connection_timeout": "Kapcsolat id\u0151t\u00fall\u00e9p\u00e9se", + "incorrect_password": "Hib\u00e1s jelsz\u00f3", + "incorrect_username": "Helytelen felhaszn\u00e1l\u00f3n\u00e9v", + "incorrect_username_or_password": "Helytelen felhaszn\u00e1l\u00f3n\u00e9v vagy jelsz\u00f3", + "invalid_url": "\u00c9rv\u00e9nytelen URL" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "url": "URL", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "title": "Huawei LTE konfigur\u00e1l\u00e1sa" + } + }, + "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "name": "\u00c9rtes\u00edt\u00e9si szolg\u00e1ltat\u00e1s neve (a m\u00f3dos\u00edt\u00e1s \u00fajraind\u00edt\u00e1st ig\u00e9nyel)", + "recipient": "SMS-\u00e9rtes\u00edt\u00e9s c\u00edmzettjei", + "track_new_devices": "\u00daj eszk\u00f6z\u00f6k nyomk\u00f6vet\u00e9se" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/pl.json b/homeassistant/components/huawei_lte/.translations/pl.json index a4e7d72852a..4029b24df3f 100644 --- a/homeassistant/components/huawei_lte/.translations/pl.json +++ b/homeassistant/components/huawei_lte/.translations/pl.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", - "already_in_progress": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_configured": "To urz\u0105dzenie jest ju\u017c skonfigurowane.", + "already_in_progress": "To urz\u0105dzenie jest ju\u017c skonfigurowane.", "not_huawei_lte": "To nie jest urz\u0105dzenie Huawei LTE" }, "error": { diff --git a/homeassistant/components/huawei_lte/.translations/sv.json b/homeassistant/components/huawei_lte/.translations/sv.json index fb73612d897..16b192d16a1 100644 --- a/homeassistant/components/huawei_lte/.translations/sv.json +++ b/homeassistant/components/huawei_lte/.translations/sv.json @@ -1,7 +1,43 @@ { "config": { "abort": { - "already_configured": "Den h\u00e4r enheten har redan konfigurerats" + "already_configured": "Den h\u00e4r enheten har redan konfigurerats", + "already_in_progress": "Den h\u00e4r enheten har redan konfigurerats", + "not_huawei_lte": "Inte en Huawei LTE-enhet" + }, + "error": { + "connection_failed": "Anslutningen misslyckades", + "connection_timeout": "Timeout f\u00f6r anslutning", + "incorrect_password": "Felaktigt l\u00f6senord", + "incorrect_username": "Felaktigt anv\u00e4ndarnamn", + "incorrect_username_or_password": "Felaktigt anv\u00e4ndarnamn eller l\u00f6senord", + "invalid_url": "Ogiltig URL", + "login_attempts_exceeded": "Maximala inloggningsf\u00f6rs\u00f6k har \u00f6verskridits, f\u00f6rs\u00f6k igen senare", + "response_error": "Ok\u00e4nt fel fr\u00e5n enheten", + "unknown_connection_error": "Ok\u00e4nt fel vid anslutning till enheten" + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "url": "URL", + "username": "Anv\u00e4ndarnamn" + }, + "description": "Ange information om enhets\u00e5tkomst. Det \u00e4r valfritt att ange anv\u00e4ndarnamn och l\u00f6senord, men st\u00f6djer d\u00e5 fler integrationsfunktioner. \u00c5 andra sidan kan anv\u00e4ndning av en auktoriserad anslutning orsaka problem med att komma \u00e5t enhetens webbgr\u00e4nssnitt utanf\u00f6r Home Assistant medan integrationen \u00e4r aktiv och tv\u00e4rtom.", + "title": "Konfigurera Huawei LTE" + } + }, + "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "name": "Namn p\u00e5 meddelandetj\u00e4nsten (\u00e4ndring kr\u00e4ver omstart)", + "recipient": "Mottagare av SMS-meddelanden", + "track_new_devices": "Sp\u00e5ra nya enheter" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 1d54f972907..d3b2d5b1abd 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -5,6 +5,7 @@ from datetime import timedelta from functools import partial import ipaddress import logging +import time from typing import Any, Callable, Dict, List, Set, Tuple from urllib.parse import urlparse @@ -65,6 +66,7 @@ from .const import ( KEY_MONITORING_STATUS, KEY_MONITORING_TRAFFIC_STATISTICS, KEY_WLAN_HOST_LIST, + NOTIFY_SUPPRESS_TIMEOUT, SERVICE_CLEAR_TRAFFIC_STATISTICS, SERVICE_REBOOT, SERVICE_RESUME_INTEGRATION, @@ -138,9 +140,11 @@ class Router: init=False, factory=lambda: defaultdict(set, ((x, {"initial_scan"}) for x in ALL_KEYS)), ) + inflight_gets: Set[str] = attr.ib(init=False, factory=set) unload_handlers: List[CALLBACK_TYPE] = attr.ib(init=False, factory=list) client: Client suspended = attr.ib(init=False, default=False) + notify_last_attempt: float = attr.ib(init=False, default=-1) def __attrs_post_init__(self): """Set up internal state on init.""" @@ -167,6 +171,10 @@ class Router: def _get_data(self, key: str, func: Callable[[None], Any]) -> None: if not self.subscriptions.get(key): return + if key in self.inflight_gets: + _LOGGER.debug("Skipping already inflight get for %s", key) + return + self.inflight_gets.add(key) _LOGGER.debug("Getting %s for subscribers %s", key, self.subscriptions[key]) try: self.data[key] = func() @@ -189,7 +197,21 @@ class Router: "%s requires authorization, excluding from future updates", key ) self.subscriptions.pop(key) + except Timeout: + grace_left = ( + self.notify_last_attempt - time.monotonic() + NOTIFY_SUPPRESS_TIMEOUT + ) + if grace_left > 0: + _LOGGER.debug( + "%s timed out, %.1fs notify timeout suppress grace remaining", + key, + grace_left, + exc_info=True, + ) + else: + raise finally: + self.inflight_gets.discard(key) _LOGGER.debug("%s=%s", key, self.data.get(key)) def update(self) -> None: diff --git a/homeassistant/components/huawei_lte/const.py b/homeassistant/components/huawei_lte/const.py index c6837fce06c..6d699420283 100644 --- a/homeassistant/components/huawei_lte/const.py +++ b/homeassistant/components/huawei_lte/const.py @@ -8,10 +8,10 @@ DEFAULT_NOTIFY_SERVICE_NAME = DOMAIN UPDATE_SIGNAL = f"{DOMAIN}_update" UPDATE_OPTIONS_SIGNAL = f"{DOMAIN}_options_update" -UNIT_BYTES = "B" UNIT_SECONDS = "s" CONNECTION_TIMEOUT = 10 +NOTIFY_SUPPRESS_TIMEOUT = 30 SERVICE_CLEAR_TRAFFIC_STATISTICS = "clear_traffic_statistics" SERVICE_REBOOT = "reboot" diff --git a/homeassistant/components/huawei_lte/notify.py b/homeassistant/components/huawei_lte/notify.py index 5619a5d702c..91cc8864eb0 100644 --- a/homeassistant/components/huawei_lte/notify.py +++ b/homeassistant/components/huawei_lte/notify.py @@ -1,6 +1,7 @@ """Support for Huawei LTE router notifications.""" import logging +import time from typing import Any, List import attr @@ -57,3 +58,5 @@ class HuaweiLteSmsNotificationService(BaseNotificationService): _LOGGER.debug("Sent to %s: %s", targets, resp) except ResponseErrorException as ex: _LOGGER.error("Could not send to %s: %s", targets, ex) + finally: + self.router.notify_last_attempt = time.monotonic() diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 3b6b75edfba..54c5441c6e2 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -10,7 +10,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_SIGNAL_STRENGTH, DOMAIN as SENSOR_DOMAIN, ) -from homeassistant.const import CONF_URL, STATE_UNKNOWN +from homeassistant.const import CONF_URL, DATA_BYTES, STATE_UNKNOWN from . import HuaweiLteBaseEntity from .const import ( @@ -18,7 +18,6 @@ from .const import ( KEY_DEVICE_INFORMATION, KEY_DEVICE_SIGNAL, KEY_MONITORING_TRAFFIC_STATISTICS, - UNIT_BYTES, UNIT_SECONDS, ) @@ -126,19 +125,19 @@ SENSOR_META = { name="Current connection duration", unit=UNIT_SECONDS, icon="mdi:timer" ), (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentDownload"): dict( - name="Current connection download", unit=UNIT_BYTES, icon="mdi:download" + name="Current connection download", unit=DATA_BYTES, icon="mdi:download" ), (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentUpload"): dict( - name="Current connection upload", unit=UNIT_BYTES, icon="mdi:upload" + name="Current connection upload", unit=DATA_BYTES, icon="mdi:upload" ), (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalConnectTime"): dict( name="Total connected duration", unit=UNIT_SECONDS, icon="mdi:timer" ), (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalDownload"): dict( - name="Total download", unit=UNIT_BYTES, icon="mdi:download" + name="Total download", unit=DATA_BYTES, icon="mdi:download" ), (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalUpload"): dict( - name="Total upload", unit=UNIT_BYTES, icon="mdi:upload" + name="Total upload", unit=DATA_BYTES, icon="mdi:upload" ), } diff --git a/homeassistant/components/hue/.translations/pl.json b/homeassistant/components/hue/.translations/pl.json index 3866af9d7fc..00b9374459c 100644 --- a/homeassistant/components/hue/.translations/pl.json +++ b/homeassistant/components/hue/.translations/pl.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "all_configured": "Wszystkie mostki Hue s\u0105 ju\u017c skonfigurowane", - "already_configured": "Mostek jest ju\u017c skonfigurowany", - "already_in_progress": "Konfigurowanie mostka jest ju\u017c w toku.", + "all_configured": "Wszystkie mostki Hue s\u0105 ju\u017c skonfigurowane.", + "already_configured": "Mostek jest ju\u017c skonfigurowany.", + "already_in_progress": "Konfiguracja mostka jest ju\u017c w toku.", "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z mostkiem", "discover_timeout": "Nie mo\u017cna wykry\u0107 \u017cadnych mostk\u00f3w Hue", "no_bridges": "Nie wykryto \u017cadnych mostk\u00f3w Hue", diff --git a/homeassistant/components/hue/.translations/zh-Hans.json b/homeassistant/components/hue/.translations/zh-Hans.json index 1c6d78f9343..5e2f35bfea8 100644 --- a/homeassistant/components/hue/.translations/zh-Hans.json +++ b/homeassistant/components/hue/.translations/zh-Hans.json @@ -5,7 +5,7 @@ "already_configured": "\u98de\u5229\u6d66 Hue Bridge \u5df2\u914d\u7f6e\u5b8c\u6210", "already_in_progress": "\u7f51\u6865\u7684\u914d\u7f6e\u6d41\u5df2\u5728\u8fdb\u884c\u4e2d\u3002", "cannot_connect": "\u65e0\u6cd5\u8fde\u63a5\u5230 \u98de\u5229\u6d66 Hue Bridge", - "discover_timeout": "\u65e0\u6cd5\u55c5\u63a2 Hue \u6865\u63a5\u5668", + "discover_timeout": "\u65e0\u6cd5\u55c5\u63a2\u5230 Hue \u6865\u63a5\u5668", "no_bridges": "\u672a\u53d1\u73b0\u98de\u5229\u6d66 Hue Bridge", "unknown": "\u51fa\u73b0\u672a\u77e5\u7684\u9519\u8bef" }, diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index ff51fc667e6..7510ff22f16 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -11,22 +11,22 @@ from homeassistant.const import CONF_HOST from homeassistant.helpers import config_validation as cv, device_registry as dr from .bridge import HueBridge -from .const import DOMAIN +from .const import ( + CONF_ALLOW_HUE_GROUPS, + CONF_ALLOW_UNREACHABLE, + DEFAULT_ALLOW_HUE_GROUPS, + DEFAULT_ALLOW_UNREACHABLE, + DOMAIN, +) _LOGGER = logging.getLogger(__name__) CONF_BRIDGES = "bridges" -CONF_ALLOW_UNREACHABLE = "allow_unreachable" -DEFAULT_ALLOW_UNREACHABLE = False - DATA_CONFIGS = "hue_configs" PHUE_CONFIG_FILE = "phue.conf" -CONF_ALLOW_HUE_GROUPS = "allow_hue_groups" -DEFAULT_ALLOW_HUE_GROUPS = True - BRIDGE_CONFIG_SCHEMA = vol.Schema( { # Validate as IP address and then convert back to a string. @@ -46,13 +46,7 @@ CONFIG_SCHEMA = vol.Schema( DOMAIN: vol.Schema( { vol.Optional(CONF_BRIDGES): vol.All( - cv.ensure_list, - [ - vol.All( - cv.deprecated("filename", invalidation_version="0.106.0"), - BRIDGE_CONFIG_SCHEMA, - ), - ], + cv.ensure_list, [BRIDGE_CONFIG_SCHEMA], ) } ) @@ -112,8 +106,10 @@ async def async_setup_entry( config = hass.data[DATA_CONFIGS].get(host) if config is None: - allow_unreachable = DEFAULT_ALLOW_UNREACHABLE - allow_groups = DEFAULT_ALLOW_HUE_GROUPS + allow_unreachable = entry.data.get( + CONF_ALLOW_UNREACHABLE, DEFAULT_ALLOW_UNREACHABLE + ) + allow_groups = entry.data.get(CONF_ALLOW_HUE_GROUPS, DEFAULT_ALLOW_HUE_GROUPS) else: allow_unreachable = config[CONF_ALLOW_UNREACHABLE] allow_groups = config[CONF_ALLOW_HUE_GROUPS] diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index a153ed7a096..2c164e5769a 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -1,6 +1,8 @@ """Code to handle a Hue bridge.""" import asyncio +from functools import partial +from aiohttp import client_exceptions import aiohue import async_timeout import slugify as unicode_slug @@ -21,6 +23,8 @@ ATTR_SCENE_NAME = "scene_name" SCENE_SCHEMA = vol.Schema( {vol.Required(ATTR_GROUP_NAME): cv.string, vol.Required(ATTR_SCENE_NAME): cv.string} ) +# How long should we sleep if the hub is busy +HUB_BUSY_SLEEP = 0.01 class HueBridge: @@ -101,11 +105,33 @@ class HueBridge: self.authorized = True return True - async def async_request_call(self, coro): - """Process request batched.""" + async def async_request_call(self, task): + """Limit parallel requests to Hue hub. + The Hue hub can only handle a certain amount of parallel requests, total. + Although we limit our parallel requests, we still will run into issues because + other products are hitting up Hue. + + ClientOSError means hub closed the socket on us. + ContentResponseError means hub raised an error. + Since we don't make bad requests, this is on them. + """ async with self.parallel_updates_semaphore: - return await coro + for tries in range(4): + try: + return await task() + except ( + client_exceptions.ClientOSError, + client_exceptions.ClientResponseError, + ) as err: + if tries == 3 or ( + # We only retry if it's a server error. So raise on all 4XX errors. + isinstance(err, client_exceptions.ClientResponseError) + and err.status < 500 + ): + raise + + await asyncio.sleep(HUB_BUSY_SLEEP * tries) async def async_reset(self): """Reset this bridge to default state. @@ -167,8 +193,8 @@ class HueBridge: # If we can't find it, fetch latest info. if not updated and (group is None or scene is None): - await self.api.groups.update() - await self.api.scenes.update() + await self.async_request_call(self.api.groups.update) + await self.async_request_call(self.api.scenes.update) await self.hue_activate_scene(call, updated=True) return @@ -180,7 +206,7 @@ class HueBridge: LOGGER.warning("Unable to find scene %s", scene_name) return - await group.set_action(scene=scene.id) + await self.async_request_call(partial(group.set_action, scene=scene.id)) async def handle_unauthorized_error(self): """Create a new config flow when the authorization is no longer valid.""" @@ -210,7 +236,7 @@ async def authenticate_bridge(hass: core.HomeAssistant, bridge: aiohue.Bridge): except (aiohue.LinkButtonNotPressed, aiohue.Unauthorized): raise AuthenticationRequired - except (asyncio.TimeoutError, aiohue.RequestError): + except (asyncio.TimeoutError, client_exceptions.ClientOSError): raise CannotConnect except aiohue.AiohueException: LOGGER.exception("Unknown Hue linking error occurred") diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index a46f8816fbb..d214c5509ea 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -14,7 +14,11 @@ from homeassistant.const import CONF_HOST from homeassistant.helpers import aiohttp_client from .bridge import authenticate_bridge -from .const import DOMAIN, LOGGER # pylint: disable=unused-import +from .const import ( # pylint: disable=unused-import + CONF_ALLOW_HUE_GROUPS, + DOMAIN, + LOGGER, +) from .errors import AuthenticationRequired, CannotConnect HUE_MANUFACTURERURL = "http://www.philips.com" @@ -125,7 +129,11 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry( title=bridge.config.name, - data={"host": bridge.host, "username": bridge.username}, + data={ + "host": bridge.host, + "username": bridge.username, + CONF_ALLOW_HUE_GROUPS: False, + }, ) except AuthenticationRequired: errors["base"] = "register_failed" diff --git a/homeassistant/components/hue/const.py b/homeassistant/components/hue/const.py index e48cd4a8583..e2189515482 100644 --- a/homeassistant/components/hue/const.py +++ b/homeassistant/components/hue/const.py @@ -3,8 +3,13 @@ import logging LOGGER = logging.getLogger(__package__) DOMAIN = "hue" -API_NUPNP = "https://www.meethue.com/api/nupnp" # How long to wait to actually do the refresh after requesting it. # We wait some time so if we control multiple lights, we batch requests. REQUEST_REFRESH_DELAY = 0.3 + +CONF_ALLOW_UNREACHABLE = "allow_unreachable" +DEFAULT_ALLOW_UNREACHABLE = False + +CONF_ALLOW_HUE_GROUPS = "allow_hue_groups" +DEFAULT_ALLOW_HUE_GROUPS = True diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index 7ed2dcc84f2..1678dbbfc62 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -72,6 +72,21 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= pass +def create_light(item_class, coordinator, bridge, is_group, api, item_id): + """Create the light.""" + if is_group: + supported_features = 0 + for light_id in api[item_id].lights: + if light_id not in bridge.api.lights: + continue + light = bridge.api.lights[light_id] + supported_features |= SUPPORT_HUE.get(light.type, SUPPORT_HUE_EXTENDED) + supported_features = supported_features or SUPPORT_HUE_EXTENDED + else: + supported_features = SUPPORT_HUE.get(api[item_id].type, SUPPORT_HUE_EXTENDED) + return item_class(coordinator, bridge, is_group, api[item_id], supported_features) + + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Hue lights from a config entry.""" bridge = hass.data[HUE_DOMAIN][config_entry.entry_id] @@ -79,17 +94,19 @@ async def async_setup_entry(hass, config_entry, async_add_entities): light_coordinator = DataUpdateCoordinator( hass, _LOGGER, - "light", - partial(async_safe_fetch, bridge, bridge.api.lights.update), - SCAN_INTERVAL, - Debouncer(bridge.hass, _LOGGER, REQUEST_REFRESH_DELAY, True), + name="light", + update_method=partial(async_safe_fetch, bridge, bridge.api.lights.update), + update_interval=SCAN_INTERVAL, + request_refresh_debouncer=Debouncer( + bridge.hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True + ), ) # First do a refresh to see if we can reach the hub. # Otherwise we will declare not ready. await light_coordinator.async_refresh() - if light_coordinator.failed_last_update: + if not light_coordinator.last_update_success: raise PlatformNotReady update_lights = partial( @@ -98,7 +115,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): bridge.api.lights, {}, async_add_entities, - partial(HueLight, light_coordinator, bridge, False), + partial(create_light, HueLight, light_coordinator, bridge, False), ) # We add a listener after fetching the data, so manually trigger listener @@ -122,10 +139,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): group_coordinator = DataUpdateCoordinator( hass, _LOGGER, - "group", - partial(async_safe_fetch, bridge, bridge.api.groups.update), - SCAN_INTERVAL, - Debouncer(bridge.hass, _LOGGER, REQUEST_REFRESH_DELAY, True), + name="group", + update_method=partial(async_safe_fetch, bridge, bridge.api.groups.update), + update_interval=SCAN_INTERVAL, + request_refresh_debouncer=Debouncer( + bridge.hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True + ), ) update_groups = partial( @@ -134,7 +153,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): bridge.api.groups, {}, async_add_entities, - partial(HueLight, group_coordinator, bridge, True), + partial(create_light, HueLight, group_coordinator, bridge, True), ) group_coordinator.async_add_listener(update_groups) @@ -149,7 +168,7 @@ async def async_safe_fetch(bridge, fetch_method): """Safely fetch data.""" try: with async_timeout.timeout(4): - return await bridge.async_request_call(fetch_method()) + return await bridge.async_request_call(fetch_method) except aiohue.Unauthorized: await bridge.handle_unauthorized_error() raise UpdateFailed @@ -166,7 +185,7 @@ def async_update_items(bridge, api, current, async_add_entities, create_item): if item_id in current: continue - current[item_id] = create_item(api[item_id]) + current[item_id] = create_item(api, item_id) new_items.append(current[item_id]) bridge.hass.async_create_task(remove_devices(bridge, api, current)) @@ -178,12 +197,13 @@ def async_update_items(bridge, api, current, async_add_entities, create_item): class HueLight(Light): """Representation of a Hue light.""" - def __init__(self, coordinator, bridge, is_group, light): + def __init__(self, coordinator, bridge, is_group, light, supported_features): """Initialize the light.""" self.light = light self.coordinator = coordinator self.bridge = bridge self.is_group = is_group + self._supported_features = supported_features if is_group: self.is_osram = False @@ -277,7 +297,7 @@ class HueLight(Light): @property def available(self): """Return if light is available.""" - return not self.coordinator.failed_last_update and ( + return self.coordinator.last_update_success and ( self.is_group or self.bridge.allow_unreachable or self.light.state["reachable"] @@ -286,7 +306,7 @@ class HueLight(Light): @property def supported_features(self): """Flag supported features.""" - return SUPPORT_HUE.get(self.light.type, SUPPORT_HUE_EXTENDED) + return self._supported_features @property def effect(self): @@ -372,9 +392,13 @@ class HueLight(Light): command["effect"] = "none" if self.is_group: - await self.bridge.async_request_call(self.light.set_action(**command)) + await self.bridge.async_request_call( + partial(self.light.set_action, **command) + ) else: - await self.bridge.async_request_call(self.light.set_state(**command)) + await self.bridge.async_request_call( + partial(self.light.set_state, **command) + ) await self.coordinator.async_request_refresh() @@ -397,9 +421,13 @@ class HueLight(Light): command["alert"] = "none" if self.is_group: - await self.bridge.async_request_call(self.light.set_action(**command)) + await self.bridge.async_request_call( + partial(self.light.set_action, **command) + ) else: - await self.bridge.async_request_call(self.light.set_state(**command)) + await self.bridge.async_request_call( + partial(self.light.set_state, **command) + ) await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index ea01da0980f..5471632f9c5 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==1.10.1"], + "requirements": ["aiohue==2.0.0"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index f57b0f98d30..0bc7cd53536 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -42,10 +42,12 @@ class SensorManager: self.coordinator = DataUpdateCoordinator( bridge.hass, _LOGGER, - "sensor", - self.async_update_data, - self.SCAN_INTERVAL, - debounce.Debouncer(bridge.hass, _LOGGER, REQUEST_REFRESH_DELAY, True), + name="sensor", + update_method=self.async_update_data, + update_interval=self.SCAN_INTERVAL, + request_refresh_debouncer=debounce.Debouncer( + bridge.hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True + ), ) async def async_update_data(self): @@ -53,7 +55,7 @@ class SensorManager: try: with async_timeout.timeout(4): return await self.bridge.async_request_call( - self.bridge.api.sensors.update() + self.bridge.api.sensors.update ) except Unauthorized: await self.bridge.handle_unauthorized_error() @@ -183,7 +185,7 @@ class GenericHueSensor(entity.Entity): @property def available(self): """Return if sensor is available.""" - return not self.bridge.sensor_manager.coordinator.failed_last_update and ( + return self.bridge.sensor_manager.coordinator.last_update_success and ( self.bridge.allow_unreachable or self.sensor.config["reachable"] ) diff --git a/homeassistant/components/iaqualink/.translations/hu.json b/homeassistant/components/iaqualink/.translations/hu.json new file mode 100644 index 00000000000..b0b9393acde --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/hu.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v / e-mail c\u00edm" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/pl.json b/homeassistant/components/iaqualink/.translations/pl.json index 211a65f5ccb..d14a2775c15 100644 --- a/homeassistant/components/iaqualink/.translations/pl.json +++ b/homeassistant/components/iaqualink/.translations/pl.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "Has\u0142o", - "username": "Nazwa u\u017cytkownika / adres e-mail" + "username": "Nazwa u\u017cytkownika/adres e-mail" }, "description": "Wprowad\u017a nazw\u0119 u\u017cytkownika i has\u0142o do konta iAqualink.", "title": "Po\u0142\u0105cz z iAqualink" diff --git a/homeassistant/components/iaqualink/.translations/sv.json b/homeassistant/components/iaqualink/.translations/sv.json new file mode 100644 index 00000000000..aa2b4142616 --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan bara konfigurera en enda iAqualink-anslutning." + }, + "error": { + "connection_failure": "Det g\u00e5r inte att ansluta till iAqualink. Kontrollera ditt anv\u00e4ndarnamn och l\u00f6senord." + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn / E-postadress" + }, + "description": "V\u00e4nligen ange anv\u00e4ndarnamn och l\u00f6senord f\u00f6r ditt iAqualink-konto.", + "title": "Anslut till iAqualink" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/config_flow.py b/homeassistant/components/iaqualink/config_flow.py index d577fe448aa..d64ec711198 100644 --- a/homeassistant/components/iaqualink/config_flow.py +++ b/homeassistant/components/iaqualink/config_flow.py @@ -6,7 +6,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers import ConfigType +from homeassistant.helpers.typing import ConfigType from .const import DOMAIN diff --git a/homeassistant/components/icloud/.translations/hu.json b/homeassistant/components/icloud/.translations/hu.json new file mode 100644 index 00000000000..14c8c8e4e2f --- /dev/null +++ b/homeassistant/components/icloud/.translations/hu.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "login": "Bejelentkez\u00e9si hiba: k\u00e9rj\u00fck, ellen\u0151rizze e-mail c\u00edm\u00e9t \u00e9s jelszav\u00e1t", + "send_verification_code": "Nem siker\u00fclt elk\u00fcldeni az ellen\u0151rz\u0151 k\u00f3dot", + "validate_verification_code": "Nem siker\u00fclt ellen\u0151rizni az ellen\u0151rz\u0151 k\u00f3dot, ki kell v\u00e1lasztania egy megb\u00edzhat\u00f3s\u00e1gi eszk\u00f6zt, \u00e9s \u00fajra kell ind\u00edtania az ellen\u0151rz\u00e9st" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Megb\u00edzhat\u00f3 eszk\u00f6z" + }, + "description": "V\u00e1lassza ki a megb\u00edzhat\u00f3 eszk\u00f6zt", + "title": "iCloud megb\u00edzhat\u00f3 eszk\u00f6z" + }, + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "E-mail" + }, + "description": "Adja meg hiteles\u00edt\u0151 adatait", + "title": "iCloud hiteles\u00edt\u0151 adatok" + }, + "verification_code": { + "data": { + "verification_code": "Ellen\u0151rz\u0151 k\u00f3d" + }, + "description": "K\u00e9rj\u00fck, \u00edrja be az iCloud-t\u00f3l \u00e9ppen kapott ellen\u0151rz\u0151 k\u00f3dot", + "title": "iCloud ellen\u0151rz\u0151 k\u00f3d" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/pl.json b/homeassistant/components/icloud/.translations/pl.json index 169fe2eac2d..41e182eceee 100644 --- a/homeassistant/components/icloud/.translations/pl.json +++ b/homeassistant/components/icloud/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Konto jest ju\u017c skonfigurowane" + "already_configured": "Konto jest ju\u017c skonfigurowane." }, "error": { "login": "B\u0142\u0105d logowania: sprawd\u017a adres e-mail i has\u0142o", diff --git a/homeassistant/components/icloud/.translations/sv.json b/homeassistant/components/icloud/.translations/sv.json index 8c4c45f9c89..fc5b81b6591 100644 --- a/homeassistant/components/icloud/.translations/sv.json +++ b/homeassistant/components/icloud/.translations/sv.json @@ -1,5 +1,37 @@ { "config": { - "title": "" + "abort": { + "already_configured": "Kontot har redan konfigurerats" + }, + "error": { + "login": "Inloggningsfel: var god att kontrollera din e-postadress och l\u00f6senord", + "send_verification_code": "Det gick inte att skicka verifieringskod", + "validate_verification_code": "Det gick inte att verifiera verifieringskoden, v\u00e4lj en betrodd enhet och starta verifieringen igen" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Betrodd enhet" + }, + "description": "V\u00e4lj din betrodda enhet", + "title": "Betrodd iCloud-enhet" + }, + "user": { + "data": { + "password": "L\u00f6senord", + "username": "E-post" + }, + "description": "Ange dina autentiseringsuppgifter", + "title": "iCloud-autentiseringsuppgifter" + }, + "verification_code": { + "data": { + "verification_code": "Verifieringskod" + }, + "description": "V\u00e4nligen ange verifieringskoden som du just f\u00e5tt fr\u00e5n iCloud", + "title": "iCloud-verifieringskod" + } + }, + "title": "Apple iCloud" } } \ No newline at end of file diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py index 62eb2fb91ac..687c6bf93de 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -1,4 +1,5 @@ """The iCloud component.""" +import asyncio import logging import voluptuous as vol @@ -16,7 +17,7 @@ from .const import ( DEFAULT_GPS_ACCURACY_THRESHOLD, DEFAULT_MAX_INTERVAL, DOMAIN, - ICLOUD_COMPONENTS, + PLATFORMS, STORAGE_KEY, STORAGE_VERSION, ) @@ -127,9 +128,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool hass.data[DOMAIN][username] = account - for component in ICLOUD_COMPONENTS: + for platform in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup(entry, platform) ) def play_sound(service: ServiceDataType) -> None: @@ -212,3 +213,19 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool ) return True + + +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.data[CONF_USERNAME]) + + return unload_ok diff --git a/homeassistant/components/icloud/const.py b/homeassistant/components/icloud/const.py index 57a3f48936c..3349615ed57 100644 --- a/homeassistant/components/icloud/const.py +++ b/homeassistant/components/icloud/const.py @@ -13,7 +13,7 @@ DEFAULT_GPS_ACCURACY_THRESHOLD = 500 # meters STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -ICLOUD_COMPONENTS = ["device_tracker", "sensor"] +PLATFORMS = ["device_tracker", "sensor"] # pyicloud.AppleDevice status DEVICE_BATTERY_LEVEL = "batteryLevel" diff --git a/homeassistant/components/ihc/manifest.json b/homeassistant/components/ihc/manifest.json index ac9f2f60218..559ed7c9060 100644 --- a/homeassistant/components/ihc/manifest.json +++ b/homeassistant/components/ihc/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/ihc", "requirements": [ "defusedxml==0.6.0", - "ihcsdk==2.5.0" + "ihcsdk==2.6.0" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index df6fa626a4f..ce17cc6c77d 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -1,278 +1,43 @@ """Support for INSTEON Modems (PLM and Hub).""" -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 ( - CONF_ADDRESS, - CONF_ENTITY_ID, CONF_HOST, CONF_PLATFORM, CONF_PORT, - ENTITY_MATCH_ALL, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import callback -from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send -from homeassistant.helpers.entity import Entity + +from .const import ( + CONF_CAT, + CONF_DIM_STEPS, + CONF_FIRMWARE, + CONF_HOUSECODE, + CONF_HUB_PASSWORD, + CONF_HUB_USERNAME, + CONF_HUB_VERSION, + CONF_IP_PORT, + CONF_OVERRIDE, + CONF_PRODUCT_KEY, + CONF_SUBCAT, + CONF_UNITCODE, + CONF_X10, + CONF_X10_ALL_LIGHTS_OFF, + CONF_X10_ALL_LIGHTS_ON, + CONF_X10_ALL_UNITS_OFF, + DOMAIN, + INSTEON_ENTITIES, +) +from .schemas import CONFIG_SCHEMA # noqa F440 +from .utils import async_register_services, register_new_device_callback _LOGGER = logging.getLogger(__name__) -DOMAIN = "insteon" -INSTEON_ENTITIES = "entities" - -CONF_IP_PORT = "ip_port" -CONF_HUB_USERNAME = "username" -CONF_HUB_PASSWORD = "password" -CONF_HUB_VERSION = "hub_version" -CONF_OVERRIDE = "device_override" -CONF_PLM_HUB_MSG = "Must configure either a PLM port or a Hub host" -CONF_CAT = "cat" -CONF_SUBCAT = "subcat" -CONF_FIRMWARE = "firmware" -CONF_PRODUCT_KEY = "product_key" -CONF_X10 = "x10_devices" -CONF_HOUSECODE = "housecode" -CONF_UNITCODE = "unitcode" -CONF_DIM_STEPS = "dim_steps" -CONF_X10_ALL_UNITS_OFF = "x10_all_units_off" -CONF_X10_ALL_LIGHTS_ON = "x10_all_lights_on" -CONF_X10_ALL_LIGHTS_OFF = "x10_all_lights_off" - -SRV_ADD_ALL_LINK = "add_all_link" -SRV_DEL_ALL_LINK = "delete_all_link" -SRV_LOAD_ALDB = "load_all_link_database" -SRV_PRINT_ALDB = "print_all_link_database" -SRV_PRINT_IM_ALDB = "print_im_all_link_database" -SRV_X10_ALL_UNITS_OFF = "x10_all_units_off" -SRV_X10_ALL_LIGHTS_OFF = "x10_all_lights_off" -SRV_X10_ALL_LIGHTS_ON = "x10_all_lights_on" -SRV_ALL_LINK_GROUP = "group" -SRV_ALL_LINK_MODE = "mode" -SRV_LOAD_DB_RELOAD = "reload" -SRV_CONTROLLER = "controller" -SRV_RESPONDER = "responder" -SRV_HOUSECODE = "housecode" -SRV_SCENE_ON = "scene_on" -SRV_SCENE_OFF = "scene_off" - -SIGNAL_LOAD_ALDB = "load_aldb" -SIGNAL_PRINT_ALDB = "print_aldb" - -HOUSECODES = [ - "a", - "b", - "c", - "d", - "e", - "f", - "g", - "h", - "i", - "j", - "k", - "l", - "m", - "n", - "o", - "p", -] - -BUTTON_PRESSED_STATE_NAME = "onLevelButton" -EVENT_BUTTON_ON = "insteon.button_on" -EVENT_BUTTON_OFF = "insteon.button_off" -EVENT_CONF_BUTTON = "button" - - -def set_default_port(schema: Dict) -> Dict: - """Set the default port based on the Hub version.""" - # If the ip_port is found do nothing - # If it is not found the set the default - ip_port = schema.get(CONF_IP_PORT) - if not ip_port: - hub_version = schema.get(CONF_HUB_VERSION) - # Found hub_version but not ip_port - if hub_version == 1: - schema[CONF_IP_PORT] = 9761 - else: - schema[CONF_IP_PORT] = 25105 - return schema - - -CONF_DEVICE_OVERRIDE_SCHEMA = vol.All( - cv.deprecated(CONF_PLATFORM), - vol.Schema( - { - vol.Required(CONF_ADDRESS): cv.string, - vol.Optional(CONF_CAT): cv.byte, - vol.Optional(CONF_SUBCAT): cv.byte, - vol.Optional(CONF_FIRMWARE): cv.byte, - vol.Optional(CONF_PRODUCT_KEY): cv.byte, - vol.Optional(CONF_PLATFORM): cv.string, - } - ), -) - - -CONF_X10_SCHEMA = vol.All( - vol.Schema( - { - vol.Required(CONF_HOUSECODE): cv.string, - vol.Required(CONF_UNITCODE): vol.Range(min=1, max=16), - vol.Required(CONF_PLATFORM): cv.string, - vol.Optional(CONF_DIM_STEPS): vol.Range(min=2, max=255), - } - ) -) - - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.All( - vol.Schema( - { - vol.Exclusive( - CONF_PORT, "plm_or_hub", msg=CONF_PLM_HUB_MSG - ): cv.string, - vol.Exclusive( - CONF_HOST, "plm_or_hub", msg=CONF_PLM_HUB_MSG - ): cv.string, - vol.Optional(CONF_IP_PORT): cv.port, - vol.Optional(CONF_HUB_USERNAME): cv.string, - vol.Optional(CONF_HUB_PASSWORD): cv.string, - vol.Optional(CONF_HUB_VERSION, default=2): vol.In([1, 2]), - vol.Optional(CONF_OVERRIDE): vol.All( - cv.ensure_list_csv, [CONF_DEVICE_OVERRIDE_SCHEMA] - ), - vol.Optional(CONF_X10_ALL_UNITS_OFF): vol.In(HOUSECODES), - vol.Optional(CONF_X10_ALL_LIGHTS_ON): vol.In(HOUSECODES), - vol.Optional(CONF_X10_ALL_LIGHTS_OFF): vol.In(HOUSECODES), - vol.Optional(CONF_X10): vol.All( - cv.ensure_list_csv, [CONF_X10_SCHEMA] - ), - }, - extra=vol.ALLOW_EXTRA, - required=True, - ), - cv.has_at_least_one_key(CONF_PORT, CONF_HOST), - set_default_port, - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -ADD_ALL_LINK_SCHEMA = vol.Schema( - { - vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255), - vol.Required(SRV_ALL_LINK_MODE): vol.In([SRV_CONTROLLER, SRV_RESPONDER]), - } -) - - -DEL_ALL_LINK_SCHEMA = vol.Schema( - {vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255)} -) - - -LOAD_ALDB_SCHEMA = vol.Schema( - { - vol.Required(CONF_ENTITY_ID): vol.Any(cv.entity_id, ENTITY_MATCH_ALL), - vol.Optional(SRV_LOAD_DB_RELOAD, default=False): cv.boolean, - } -) - - -PRINT_ALDB_SCHEMA = vol.Schema({vol.Required(CONF_ENTITY_ID): cv.entity_id}) - - -X10_HOUSECODE_SCHEMA = vol.Schema({vol.Required(SRV_HOUSECODE): vol.In(HOUSECODES)}) - - -TRIGGER_SCENE_SCHEMA = vol.Schema( - {vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255)} -) - - -STATE_NAME_LABEL_MAP = { - "keypadButtonA": "Button A", - "keypadButtonB": "Button B", - "keypadButtonC": "Button C", - "keypadButtonD": "Button D", - "keypadButtonE": "Button E", - "keypadButtonF": "Button F", - "keypadButtonG": "Button G", - "keypadButtonH": "Button H", - "keypadButtonMain": "Main", - "onOffButtonA": "Button A", - "onOffButtonB": "Button B", - "onOffButtonC": "Button C", - "onOffButtonD": "Button D", - "onOffButtonE": "Button E", - "onOffButtonF": "Button F", - "onOffButtonG": "Button G", - "onOffButtonH": "Button H", - "onOffButtonMain": "Main", - "fanOnLevel": "Fan", - "lightOnLevel": "Light", - "coolSetPoint": "Cool Set", - "heatSetPoint": "HeatSet", - "statusReport": "Status", - "generalSensor": "Sensor", - "motionSensor": "Motion", - "lightSensor": "Light", - "batterySensor": "Battery", - "dryLeakSensor": "Dry", - "wetLeakSensor": "Wet", - "heartbeatLeakSensor": "Heartbeat", - "openClosedRelay": "Relay", - "openClosedSensor": "Sensor", - "lightOnOff": "Light", - "outletTopOnOff": "Top", - "outletBottomOnOff": "Bottom", - "coverOpenLevel": "Cover", -} - async def async_setup(hass, config): """Set up the connection to the modem.""" - ipdb = IPDB() insteon_modem = None conf = config[DOMAIN] @@ -288,163 +53,6 @@ async def async_setup(hass, config): x10_all_lights_on_housecode = conf.get(CONF_X10_ALL_LIGHTS_ON) x10_all_lights_off_housecode = conf.get(CONF_X10_ALL_LIGHTS_OFF) - @callback - def async_new_insteon_device(device): - """Detect device from transport to be delegated to platform.""" - for state_key in device.states: - platform_info = ipdb[device.states[state_key]] - if platform_info and platform_info.platform: - platform = platform_info.platform - - if platform == "on_off_events": - device.states[state_key].register_updates(_fire_button_on_off_event) - - else: - _LOGGER.info( - "New INSTEON device: %s (%s) %s", - device.address, - device.states[state_key].name, - platform, - ) - - hass.async_create_task( - discovery.async_load_platform( - hass, - platform, - DOMAIN, - discovered={ - "address": device.address.id, - "state_key": state_key, - }, - hass_config=config, - ) - ) - - def add_all_link(service): - """Add an INSTEON All-Link between two devices.""" - group = service.data.get(SRV_ALL_LINK_GROUP) - mode = service.data.get(SRV_ALL_LINK_MODE) - link_mode = 1 if mode.lower() == SRV_CONTROLLER else 0 - insteon_modem.start_all_linking(link_mode, group) - - def del_all_link(service): - """Delete an INSTEON All-Link between two devices.""" - group = service.data.get(SRV_ALL_LINK_GROUP) - insteon_modem.start_all_linking(255, group) - - def load_aldb(service): - """Load the device All-Link database.""" - entity_id = service.data[CONF_ENTITY_ID] - reload = service.data[SRV_LOAD_DB_RELOAD] - if entity_id.lower() == ENTITY_MATCH_ALL: - for entity_id in hass.data[DOMAIN].get(INSTEON_ENTITIES): - _send_load_aldb_signal(entity_id, reload) - else: - _send_load_aldb_signal(entity_id, reload) - - def _send_load_aldb_signal(entity_id, reload): - """Send the load All-Link database signal to INSTEON entity.""" - signal = f"{entity_id}_{SIGNAL_LOAD_ALDB}" - dispatcher_send(hass, signal, reload) - - def print_aldb(service): - """Print the All-Link Database for a device.""" - # For now this sends logs to the log file. - # Furture direction is to create an INSTEON control panel. - entity_id = service.data[CONF_ENTITY_ID] - signal = f"{entity_id}_{SIGNAL_PRINT_ALDB}" - dispatcher_send(hass, signal) - - def print_im_aldb(service): - """Print the All-Link Database for a device.""" - # For now this sends logs to the log file. - # Furture direction is to create an INSTEON control panel. - print_aldb_to_log(insteon_modem.aldb) - - def x10_all_units_off(service): - """Send the X10 All Units Off command.""" - housecode = service.data.get(SRV_HOUSECODE) - insteon_modem.x10_all_units_off(housecode) - - def x10_all_lights_off(service): - """Send the X10 All Lights Off command.""" - housecode = service.data.get(SRV_HOUSECODE) - insteon_modem.x10_all_lights_off(housecode) - - def x10_all_lights_on(service): - """Send the X10 All Lights On command.""" - housecode = service.data.get(SRV_HOUSECODE) - insteon_modem.x10_all_lights_on(housecode) - - def scene_on(service): - """Trigger an INSTEON scene ON.""" - group = service.data.get(SRV_ALL_LINK_GROUP) - insteon_modem.trigger_group_on(group) - - def scene_off(service): - """Trigger an INSTEON scene ON.""" - group = service.data.get(SRV_ALL_LINK_GROUP) - insteon_modem.trigger_group_off(group) - - def _register_services(): - hass.services.register( - DOMAIN, SRV_ADD_ALL_LINK, add_all_link, schema=ADD_ALL_LINK_SCHEMA - ) - hass.services.register( - DOMAIN, SRV_DEL_ALL_LINK, del_all_link, schema=DEL_ALL_LINK_SCHEMA - ) - hass.services.register( - DOMAIN, SRV_LOAD_ALDB, load_aldb, schema=LOAD_ALDB_SCHEMA - ) - hass.services.register( - DOMAIN, SRV_PRINT_ALDB, print_aldb, schema=PRINT_ALDB_SCHEMA - ) - hass.services.register(DOMAIN, SRV_PRINT_IM_ALDB, print_im_aldb, schema=None) - hass.services.register( - DOMAIN, - SRV_X10_ALL_UNITS_OFF, - x10_all_units_off, - schema=X10_HOUSECODE_SCHEMA, - ) - hass.services.register( - DOMAIN, - SRV_X10_ALL_LIGHTS_OFF, - x10_all_lights_off, - schema=X10_HOUSECODE_SCHEMA, - ) - hass.services.register( - DOMAIN, - SRV_X10_ALL_LIGHTS_ON, - x10_all_lights_on, - schema=X10_HOUSECODE_SCHEMA, - ) - hass.services.register( - DOMAIN, SRV_SCENE_ON, scene_on, schema=TRIGGER_SCENE_SCHEMA - ) - hass.services.register( - DOMAIN, SRV_SCENE_OFF, scene_off, schema=TRIGGER_SCENE_SCHEMA - ) - _LOGGER.debug("Insteon Services registered") - - def _fire_button_on_off_event(address, group, val): - # Firing an event when a button is pressed. - device = insteon_modem.devices[address.hex] - state_name = device.states[group].name - button = ( - "" if state_name == BUTTON_PRESSED_STATE_NAME else state_name[-1].lower() - ) - schema = {CONF_ADDRESS: address.hex} - if button != "": - schema[EVENT_CONF_BUTTON] = button - if val: - event = EVENT_BUTTON_ON - else: - event = EVENT_BUTTON_OFF - _LOGGER.debug( - "Firing event %s with address %s and button %s", event, address.hex, button - ) - hass.bus.fire(event, schema) - if host: _LOGGER.info("Connecting to Insteon Hub on %s", host) conn = await insteonplm.Connection.create( @@ -464,6 +72,14 @@ async def async_setup(hass, config): insteon_modem = conn.protocol + hass.data[DOMAIN] = {} + hass.data[DOMAIN]["modem"] = insteon_modem + hass.data[DOMAIN][INSTEON_ENTITIES] = set() + + register_new_device_callback(hass, config, insteon_modem) + async_register_services(hass, config, insteon_modem) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, conn.close) + for device_override in overrides: # # Override the device default capabilities for a specific address @@ -477,14 +93,6 @@ async def async_setup(hass, config): address, CONF_PRODUCT_KEY, device_override[prop] ) - hass.data[DOMAIN] = {} - hass.data[DOMAIN]["modem"] = insteon_modem - hass.data[DOMAIN][INSTEON_ENTITIES] = {} - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, conn.close) - - insteon_modem.devices.add_device_callback(async_new_insteon_device) - if x10_all_units_off_housecode: device = insteon_modem.add_x10_device( x10_all_units_off_housecode, 20, "allunitsoff" @@ -513,199 +121,4 @@ async def async_setup(hass, config): if device and hasattr(device.states[0x01], "steps"): device.states[0x01].steps = steps - hass.async_add_job(_register_services) - return True - - -State = collections.namedtuple("Product", "stateType platform") - - -class IPDB: - """Embodies the INSTEON Product Database static data and access methods.""" - - def __init__(self): - """Create the INSTEON Product Database (IPDB).""" - self.states = [ - State(Cover, "cover"), - State(OnOffSwitch_OutletTop, "switch"), - State(OnOffSwitch_OutletBottom, "switch"), - State(OpenClosedRelay, "switch"), - State(OnOffSwitch, "switch"), - State(OnOffKeypadA, "switch"), - State(OnOffKeypad, "switch"), - State(LeakSensorDryWet, "binary_sensor"), - State(IoLincSensor, "binary_sensor"), - State(SmokeCO2Sensor, "sensor"), - State(OnOffSensor, "binary_sensor"), - State(VariableSensor, "sensor"), - State(DimmableSwitch_Fan, "fan"), - State(DimmableSwitch, "light"), - State(DimmableRemote, "on_off_events"), - State(DimmableKeypadA, "light"), - State(X10DimmableSwitch, "light"), - State(X10OnOffSwitch, "switch"), - State(X10OnOffSensor, "binary_sensor"), - State(X10AllUnitsOffSensor, "binary_sensor"), - State(X10AllLightsOnSensor, "binary_sensor"), - State(X10AllLightsOffSensor, "binary_sensor"), - ] - - def __len__(self): - """Return the number of INSTEON state types mapped to HA platforms.""" - return len(self.states) - - def __iter__(self): - """Itterate through the INSTEON state types to HA platforms.""" - for product in self.states: - yield product - - def __getitem__(self, key): - """Return a Home Assistant platform from an INSTEON state type.""" - for state in self.states: - if isinstance(key, state.stateType): - return state - return None - - -class InsteonEntity(Entity): - """INSTEON abstract base entity.""" - - def __init__(self, device, state_key): - """Initialize the INSTEON binary sensor.""" - self._insteon_device_state = device.states[state_key] - self._insteon_device = device - self._insteon_device.aldb.add_loaded_callback(self._aldb_loaded) - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def address(self): - """Return the address of the node.""" - return self._insteon_device.address.human - - @property - def group(self): - """Return the INSTEON group that the entity responds to.""" - return self._insteon_device_state.group - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - if self._insteon_device_state.group == 0x01: - uid = self._insteon_device.id - else: - uid = "{:s}_{:d}".format( - self._insteon_device.id, self._insteon_device_state.group - ) - return uid - - @property - def name(self): - """Return the name of the node (used for Entity_ID).""" - # Set a base description - description = self._insteon_device.description - if self._insteon_device.description is None: - description = "Unknown Device" - - # Get an extension label if there is one - extension = self._get_label() - if extension: - extension = f" {extension}" - name = "{:s} {:s}{:s}".format( - description, self._insteon_device.address.human, extension - ) - return name - - @property - def device_state_attributes(self): - """Provide attributes for display on device card.""" - attributes = {"INSTEON Address": self.address, "INSTEON Group": self.group} - return attributes - - @callback - def async_entity_update(self, deviceid, group, val): - """Receive notification from transport that new data exists.""" - _LOGGER.debug( - "Received update for device %s group %d value %s", - deviceid.human, - group, - val, - ) - self.async_schedule_update_ha_state() - - async def async_added_to_hass(self): - """Register INSTEON update events.""" - _LOGGER.debug( - "Tracking updates for device %s group %d statename %s", - self.address, - self.group, - self._insteon_device_state.name, - ) - self._insteon_device_state.register_updates(self.async_entity_update) - self.hass.data[DOMAIN][INSTEON_ENTITIES][self.entity_id] = self - load_signal = f"{self.entity_id}_{SIGNAL_LOAD_ALDB}" - async_dispatcher_connect(self.hass, load_signal, self._load_aldb) - print_signal = f"{self.entity_id}_{SIGNAL_PRINT_ALDB}" - async_dispatcher_connect(self.hass, print_signal, self._print_aldb) - - def _load_aldb(self, reload=False): - """Load the device All-Link Database.""" - if reload: - self._insteon_device.aldb.clear() - self._insteon_device.read_aldb() - - def _print_aldb(self): - """Print the device ALDB to the log file.""" - print_aldb_to_log(self._insteon_device.aldb) - - @callback - def _aldb_loaded(self): - """All-Link Database loaded for the device.""" - self._print_aldb() - - def _get_label(self): - """Get the device label for grouped devices.""" - label = "" - if len(self._insteon_device.states) > 1: - if self._insteon_device_state.name in STATE_NAME_LABEL_MAP: - label = STATE_NAME_LABEL_MAP[self._insteon_device_state.name] - else: - label = f"Group {self.group:d}" - return label - - -def print_aldb_to_log(aldb): - """Print the All-Link Database to the log file.""" - _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") - _LOGGER.warning("Use service insteon.load_aldb first") - return - - _LOGGER.info("RecID In Use Mode HWM Group Address Data 1 Data 2 Data 3") - _LOGGER.info("----- ------ ---- --- ----- -------- ------ ------ ------") - for mem_addr in aldb: - rec = aldb[mem_addr] - # For now we write this to the log - # Roadmap is to create a configuration panel - in_use = "Y" if rec.control_flags.is_in_use else "N" - mode = "C" if rec.control_flags.is_controller else "R" - hwm = "Y" if rec.control_flags.is_high_water_mark else "N" - _LOGGER.info( - " {:04x} {:s} {:s} {:s} {:3d} {:s}" - " {:3d} {:3d} {:3d}".format( - rec.mem_addr, - in_use, - mode, - hwm, - rec.group, - rec.address.human, - rec.data1, - rec.data2, - rec.data3, - ) - ) diff --git a/homeassistant/components/insteon/binary_sensor.py b/homeassistant/components/insteon/binary_sensor.py index 68ea07cdb49..395c0a3ac20 100644 --- a/homeassistant/components/insteon/binary_sensor.py +++ b/homeassistant/components/insteon/binary_sensor.py @@ -3,7 +3,7 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from . import InsteonEntity +from .insteon_entity import InsteonEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/insteon/const.py b/homeassistant/components/insteon/const.py new file mode 100644 index 00000000000..b01409f49ff --- /dev/null +++ b/homeassistant/components/insteon/const.py @@ -0,0 +1,106 @@ +"""Constants used by insteon component.""" + +DOMAIN = "insteon" +INSTEON_ENTITIES = "entities" + +CONF_IP_PORT = "ip_port" +CONF_HUB_USERNAME = "username" +CONF_HUB_PASSWORD = "password" +CONF_HUB_VERSION = "hub_version" +CONF_OVERRIDE = "device_override" +CONF_PLM_HUB_MSG = "Must configure either a PLM port or a Hub host" +CONF_CAT = "cat" +CONF_SUBCAT = "subcat" +CONF_FIRMWARE = "firmware" +CONF_PRODUCT_KEY = "product_key" +CONF_X10 = "x10_devices" +CONF_HOUSECODE = "housecode" +CONF_UNITCODE = "unitcode" +CONF_DIM_STEPS = "dim_steps" +CONF_X10_ALL_UNITS_OFF = "x10_all_units_off" +CONF_X10_ALL_LIGHTS_ON = "x10_all_lights_on" +CONF_X10_ALL_LIGHTS_OFF = "x10_all_lights_off" + +SRV_ADD_ALL_LINK = "add_all_link" +SRV_DEL_ALL_LINK = "delete_all_link" +SRV_LOAD_ALDB = "load_all_link_database" +SRV_PRINT_ALDB = "print_all_link_database" +SRV_PRINT_IM_ALDB = "print_im_all_link_database" +SRV_X10_ALL_UNITS_OFF = "x10_all_units_off" +SRV_X10_ALL_LIGHTS_OFF = "x10_all_lights_off" +SRV_X10_ALL_LIGHTS_ON = "x10_all_lights_on" +SRV_ALL_LINK_GROUP = "group" +SRV_ALL_LINK_MODE = "mode" +SRV_LOAD_DB_RELOAD = "reload" +SRV_CONTROLLER = "controller" +SRV_RESPONDER = "responder" +SRV_HOUSECODE = "housecode" +SRV_SCENE_ON = "scene_on" +SRV_SCENE_OFF = "scene_off" + +SIGNAL_LOAD_ALDB = "load_aldb" +SIGNAL_PRINT_ALDB = "print_aldb" + +HOUSECODES = [ + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", +] + +BUTTON_PRESSED_STATE_NAME = "onLevelButton" +EVENT_BUTTON_ON = "insteon.button_on" +EVENT_BUTTON_OFF = "insteon.button_off" +EVENT_CONF_BUTTON = "button" + + +STATE_NAME_LABEL_MAP = { + "keypadButtonA": "Button A", + "keypadButtonB": "Button B", + "keypadButtonC": "Button C", + "keypadButtonD": "Button D", + "keypadButtonE": "Button E", + "keypadButtonF": "Button F", + "keypadButtonG": "Button G", + "keypadButtonH": "Button H", + "keypadButtonMain": "Main", + "onOffButtonA": "Button A", + "onOffButtonB": "Button B", + "onOffButtonC": "Button C", + "onOffButtonD": "Button D", + "onOffButtonE": "Button E", + "onOffButtonF": "Button F", + "onOffButtonG": "Button G", + "onOffButtonH": "Button H", + "onOffButtonMain": "Main", + "fanOnLevel": "Fan", + "lightOnLevel": "Light", + "coolSetPoint": "Cool Set", + "heatSetPoint": "HeatSet", + "statusReport": "Status", + "generalSensor": "Sensor", + "motionSensor": "Motion", + "lightSensor": "Light", + "batterySensor": "Battery", + "dryLeakSensor": "Dry", + "wetLeakSensor": "Wet", + "heartbeatLeakSensor": "Heartbeat", + "openClosedRelay": "Relay", + "openClosedSensor": "Sensor", + "lightOnOff": "Light", + "outletTopOnOff": "Top", + "outletBottomOnOff": "Bottom", + "coverOpenLevel": "Cover", +} diff --git a/homeassistant/components/insteon/cover.py b/homeassistant/components/insteon/cover.py index f9399d7b13f..575799cbf67 100644 --- a/homeassistant/components/insteon/cover.py +++ b/homeassistant/components/insteon/cover.py @@ -10,7 +10,7 @@ from homeassistant.components.cover import ( CoverDevice, ) -from . import InsteonEntity +from .insteon_entity import InsteonEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/insteon/fan.py b/homeassistant/components/insteon/fan.py index d88348b1a5d..6ad7436faf5 100644 --- a/homeassistant/components/insteon/fan.py +++ b/homeassistant/components/insteon/fan.py @@ -11,7 +11,7 @@ from homeassistant.components.fan import ( ) from homeassistant.const import STATE_OFF -from . import InsteonEntity +from .insteon_entity import InsteonEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/insteon/insteon_entity.py b/homeassistant/components/insteon/insteon_entity.py new file mode 100644 index 00000000000..c489dd8e382 --- /dev/null +++ b/homeassistant/components/insteon/insteon_entity.py @@ -0,0 +1,123 @@ +"""Insteon base entity.""" +import logging + +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity + +from .const import ( + DOMAIN, + INSTEON_ENTITIES, + SIGNAL_LOAD_ALDB, + SIGNAL_PRINT_ALDB, + STATE_NAME_LABEL_MAP, +) +from .utils import print_aldb_to_log + +_LOGGER = logging.getLogger(__name__) + + +class InsteonEntity(Entity): + """INSTEON abstract base entity.""" + + def __init__(self, device, state_key): + """Initialize the INSTEON binary sensor.""" + self._insteon_device_state = device.states[state_key] + self._insteon_device = device + self._insteon_device.aldb.add_loaded_callback(self._aldb_loaded) + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def address(self): + """Return the address of the node.""" + return self._insteon_device.address.human + + @property + def group(self): + """Return the INSTEON group that the entity responds to.""" + return self._insteon_device_state.group + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + if self._insteon_device_state.group == 0x01: + uid = self._insteon_device.id + else: + uid = f"{self._insteon_device.id}_{self._insteon_device_state.group}" + return uid + + @property + def name(self): + """Return the name of the node (used for Entity_ID).""" + # Set a base description + description = self._insteon_device.description + if self._insteon_device.description is None: + description = "Unknown Device" + + # Get an extension label if there is one + extension = self._get_label() + if extension: + extension = f" {extension}" + name = f"{description} {self._insteon_device.address.human}{extension}" + return name + + @property + def device_state_attributes(self): + """Provide attributes for display on device card.""" + attributes = {"insteon_address": self.address, "insteon_group": self.group} + return attributes + + @callback + def async_entity_update(self, deviceid, group, val): + """Receive notification from transport that new data exists.""" + _LOGGER.debug( + "Received update for device %s group %d value %s", + deviceid.human, + group, + val, + ) + self.async_schedule_update_ha_state() + + async def async_added_to_hass(self): + """Register INSTEON update events.""" + _LOGGER.debug( + "Tracking updates for device %s group %d statename %s", + self.address, + self.group, + self._insteon_device_state.name, + ) + self._insteon_device_state.register_updates(self.async_entity_update) + self.hass.data[DOMAIN][INSTEON_ENTITIES].add(self.entity_id) + load_signal = f"{self.entity_id}_{SIGNAL_LOAD_ALDB}" + async_dispatcher_connect(self.hass, load_signal, self._load_aldb) + print_signal = f"{self.entity_id}_{SIGNAL_PRINT_ALDB}" + async_dispatcher_connect(self.hass, print_signal, self._print_aldb) + + def _load_aldb(self, reload=False): + """Load the device All-Link Database.""" + if reload: + self._insteon_device.aldb.clear() + self._insteon_device.read_aldb() + + def _print_aldb(self): + """Print the device ALDB to the log file.""" + print_aldb_to_log(self._insteon_device.aldb) + + @callback + def _aldb_loaded(self): + """All-Link Database loaded for the device.""" + self._print_aldb() + + def _get_label(self): + """Get the device label for grouped devices.""" + label = "" + if len(self._insteon_device.states) > 1: + if self._insteon_device_state.name in STATE_NAME_LABEL_MAP: + label = STATE_NAME_LABEL_MAP[self._insteon_device_state.name] + else: + label = f"Group {self.group:d}" + return label diff --git a/homeassistant/components/insteon/ipdb.py b/homeassistant/components/insteon/ipdb.py new file mode 100644 index 00000000000..1618518a0eb --- /dev/null +++ b/homeassistant/components/insteon/ipdb.py @@ -0,0 +1,82 @@ +"""Insteon product database.""" +import collections + +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, +) + +State = collections.namedtuple("Product", "stateType platform") + + +class IPDB: + """Embodies the INSTEON Product Database static data and access methods.""" + + def __init__(self): + """Create the INSTEON Product Database (IPDB).""" + self.states = [ + State(Cover, "cover"), + State(OnOffSwitch_OutletTop, "switch"), + State(OnOffSwitch_OutletBottom, "switch"), + State(OpenClosedRelay, "switch"), + State(OnOffSwitch, "switch"), + State(OnOffKeypadA, "switch"), + State(OnOffKeypad, "switch"), + State(LeakSensorDryWet, "binary_sensor"), + State(IoLincSensor, "binary_sensor"), + State(SmokeCO2Sensor, "sensor"), + State(OnOffSensor, "binary_sensor"), + State(VariableSensor, "sensor"), + State(DimmableSwitch_Fan, "fan"), + State(DimmableSwitch, "light"), + State(DimmableRemote, "on_off_events"), + State(DimmableKeypadA, "light"), + State(X10DimmableSwitch, "light"), + State(X10OnOffSwitch, "switch"), + State(X10OnOffSensor, "binary_sensor"), + State(X10AllUnitsOffSensor, "binary_sensor"), + State(X10AllLightsOnSensor, "binary_sensor"), + State(X10AllLightsOffSensor, "binary_sensor"), + ] + + def __len__(self): + """Return the number of INSTEON state types mapped to HA platforms.""" + return len(self.states) + + def __iter__(self): + """Itterate through the INSTEON state types to HA platforms.""" + for product in self.states: + yield product + + def __getitem__(self, key): + """Return a Home Assistant platform from an INSTEON state type.""" + for state in self.states: + if isinstance(key, state.stateType): + return state + return None diff --git a/homeassistant/components/insteon/light.py b/homeassistant/components/insteon/light.py index 3a44d89add0..60a27b3acb8 100644 --- a/homeassistant/components/insteon/light.py +++ b/homeassistant/components/insteon/light.py @@ -3,7 +3,7 @@ import logging from homeassistant.components.light import ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light -from . import InsteonEntity +from .insteon_entity import InsteonEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index 74d8274796b..69c35477b8d 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -2,7 +2,7 @@ "domain": "insteon", "name": "Insteon", "documentation": "https://www.home-assistant.io/integrations/insteon", - "requirements": ["insteonplm==0.16.6"], + "requirements": ["insteonplm==0.16.7"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/insteon/schemas.py b/homeassistant/components/insteon/schemas.py new file mode 100644 index 00000000000..20399195365 --- /dev/null +++ b/homeassistant/components/insteon/schemas.py @@ -0,0 +1,156 @@ +"""Schemas used by insteon component.""" + +from typing import Dict + +import voluptuous as vol + +from homeassistant.const import ( + CONF_ADDRESS, + CONF_ENTITY_ID, + CONF_HOST, + CONF_PLATFORM, + CONF_PORT, + ENTITY_MATCH_ALL, + ENTITY_MATCH_NONE, +) +import homeassistant.helpers.config_validation as cv + +from .const import ( + CONF_CAT, + CONF_DIM_STEPS, + CONF_FIRMWARE, + CONF_HOUSECODE, + CONF_HUB_PASSWORD, + CONF_HUB_USERNAME, + CONF_HUB_VERSION, + CONF_IP_PORT, + CONF_OVERRIDE, + CONF_PLM_HUB_MSG, + CONF_PRODUCT_KEY, + CONF_SUBCAT, + CONF_UNITCODE, + CONF_X10, + CONF_X10_ALL_LIGHTS_OFF, + CONF_X10_ALL_LIGHTS_ON, + CONF_X10_ALL_UNITS_OFF, + DOMAIN, + HOUSECODES, + SRV_ALL_LINK_GROUP, + SRV_ALL_LINK_MODE, + SRV_CONTROLLER, + SRV_HOUSECODE, + SRV_LOAD_DB_RELOAD, + SRV_RESPONDER, +) + + +def set_default_port(schema: Dict) -> Dict: + """Set the default port based on the Hub version.""" + # If the ip_port is found do nothing + # If it is not found the set the default + ip_port = schema.get(CONF_IP_PORT) + if not ip_port: + hub_version = schema.get(CONF_HUB_VERSION) + # Found hub_version but not ip_port + if hub_version == 1: + schema[CONF_IP_PORT] = 9761 + else: + schema[CONF_IP_PORT] = 25105 + return schema + + +CONF_DEVICE_OVERRIDE_SCHEMA = vol.All( + cv.deprecated(CONF_PLATFORM), + vol.Schema( + { + vol.Required(CONF_ADDRESS): cv.string, + vol.Optional(CONF_CAT): cv.byte, + vol.Optional(CONF_SUBCAT): cv.byte, + vol.Optional(CONF_FIRMWARE): cv.byte, + vol.Optional(CONF_PRODUCT_KEY): cv.byte, + vol.Optional(CONF_PLATFORM): cv.string, + } + ), +) + + +CONF_X10_SCHEMA = vol.All( + vol.Schema( + { + vol.Required(CONF_HOUSECODE): cv.string, + vol.Required(CONF_UNITCODE): vol.Range(min=1, max=16), + vol.Required(CONF_PLATFORM): cv.string, + vol.Optional(CONF_DIM_STEPS): vol.Range(min=2, max=255), + } + ) +) + + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.All( + vol.Schema( + { + vol.Exclusive( + CONF_PORT, "plm_or_hub", msg=CONF_PLM_HUB_MSG + ): cv.string, + vol.Exclusive( + CONF_HOST, "plm_or_hub", msg=CONF_PLM_HUB_MSG + ): cv.string, + vol.Optional(CONF_IP_PORT): cv.port, + vol.Optional(CONF_HUB_USERNAME): cv.string, + vol.Optional(CONF_HUB_PASSWORD): cv.string, + vol.Optional(CONF_HUB_VERSION, default=2): vol.In([1, 2]), + vol.Optional(CONF_OVERRIDE): vol.All( + cv.ensure_list_csv, [CONF_DEVICE_OVERRIDE_SCHEMA] + ), + vol.Optional(CONF_X10_ALL_UNITS_OFF): vol.In(HOUSECODES), + vol.Optional(CONF_X10_ALL_LIGHTS_ON): vol.In(HOUSECODES), + vol.Optional(CONF_X10_ALL_LIGHTS_OFF): vol.In(HOUSECODES), + vol.Optional(CONF_X10): vol.All( + cv.ensure_list_csv, [CONF_X10_SCHEMA] + ), + }, + extra=vol.ALLOW_EXTRA, + required=True, + ), + cv.has_at_least_one_key(CONF_PORT, CONF_HOST), + set_default_port, + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +ADD_ALL_LINK_SCHEMA = vol.Schema( + { + vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255), + vol.Required(SRV_ALL_LINK_MODE): vol.In([SRV_CONTROLLER, SRV_RESPONDER]), + } +) + + +DEL_ALL_LINK_SCHEMA = vol.Schema( + {vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255)} +) + + +LOAD_ALDB_SCHEMA = vol.Schema( + { + vol.Required(CONF_ENTITY_ID): vol.Any( + cv.entity_id, ENTITY_MATCH_ALL, ENTITY_MATCH_NONE + ), + vol.Optional(SRV_LOAD_DB_RELOAD, default=False): cv.boolean, + } +) + + +PRINT_ALDB_SCHEMA = vol.Schema({vol.Required(CONF_ENTITY_ID): cv.entity_id}) + + +X10_HOUSECODE_SCHEMA = vol.Schema({vol.Required(SRV_HOUSECODE): vol.In(HOUSECODES)}) + + +TRIGGER_SCENE_SCHEMA = vol.Schema( + {vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255)} +) diff --git a/homeassistant/components/insteon/sensor.py b/homeassistant/components/insteon/sensor.py index 0e8a592b92d..475723b105d 100644 --- a/homeassistant/components/insteon/sensor.py +++ b/homeassistant/components/insteon/sensor.py @@ -3,7 +3,7 @@ import logging from homeassistant.helpers.entity import Entity -from . import InsteonEntity +from .insteon_entity import InsteonEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/insteon/switch.py b/homeassistant/components/insteon/switch.py index c36e60c2eff..eec7874c7fb 100644 --- a/homeassistant/components/insteon/switch.py +++ b/homeassistant/components/insteon/switch.py @@ -3,7 +3,7 @@ import logging from homeassistant.components.switch import SwitchDevice -from . import InsteonEntity +from .insteon_entity import InsteonEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/insteon/utils.py b/homeassistant/components/insteon/utils.py new file mode 100644 index 00000000000..f195a458477 --- /dev/null +++ b/homeassistant/components/insteon/utils.py @@ -0,0 +1,239 @@ +"""Utilities used by insteon component.""" + +import logging + +from insteonplm.devices import ALDBStatus + +from homeassistant.const import CONF_ADDRESS, CONF_ENTITY_ID, ENTITY_MATCH_ALL +from homeassistant.core import callback +from homeassistant.helpers import discovery +from homeassistant.helpers.dispatcher import dispatcher_send + +from .const import ( + BUTTON_PRESSED_STATE_NAME, + DOMAIN, + EVENT_BUTTON_OFF, + EVENT_BUTTON_ON, + EVENT_CONF_BUTTON, + INSTEON_ENTITIES, + SIGNAL_LOAD_ALDB, + SIGNAL_PRINT_ALDB, + SRV_ADD_ALL_LINK, + SRV_ALL_LINK_GROUP, + SRV_ALL_LINK_MODE, + SRV_CONTROLLER, + SRV_DEL_ALL_LINK, + SRV_HOUSECODE, + SRV_LOAD_ALDB, + SRV_LOAD_DB_RELOAD, + SRV_PRINT_ALDB, + SRV_PRINT_IM_ALDB, + SRV_SCENE_OFF, + SRV_SCENE_ON, + SRV_X10_ALL_LIGHTS_OFF, + SRV_X10_ALL_LIGHTS_ON, + SRV_X10_ALL_UNITS_OFF, +) +from .ipdb import IPDB +from .schemas import ( + ADD_ALL_LINK_SCHEMA, + DEL_ALL_LINK_SCHEMA, + LOAD_ALDB_SCHEMA, + PRINT_ALDB_SCHEMA, + TRIGGER_SCENE_SCHEMA, + X10_HOUSECODE_SCHEMA, +) + +_LOGGER = logging.getLogger(__name__) + + +def register_new_device_callback(hass, config, insteon_modem): + """Register callback for new Insteon device.""" + + def _fire_button_on_off_event(address, group, val): + # Firing an event when a button is pressed. + device = insteon_modem.devices[address.hex] + state_name = device.states[group].name + button = ( + "" if state_name == BUTTON_PRESSED_STATE_NAME else state_name[-1].lower() + ) + schema = {CONF_ADDRESS: address.hex} + if button != "": + schema[EVENT_CONF_BUTTON] = button + if val: + event = EVENT_BUTTON_ON + else: + event = EVENT_BUTTON_OFF + _LOGGER.debug( + "Firing event %s with address %s and button %s", event, address.hex, button + ) + hass.bus.fire(event, schema) + + @callback + def async_new_insteon_device(device): + """Detect device from transport to be delegated to platform.""" + ipdb = IPDB() + for state_key in device.states: + platform_info = ipdb[device.states[state_key]] + if platform_info and platform_info.platform: + platform = platform_info.platform + + if platform == "on_off_events": + device.states[state_key].register_updates(_fire_button_on_off_event) + + else: + _LOGGER.info( + "New INSTEON device: %s (%s) %s", + device.address, + device.states[state_key].name, + platform, + ) + + hass.async_create_task( + discovery.async_load_platform( + hass, + platform, + DOMAIN, + discovered={ + "address": device.address.id, + "state_key": state_key, + }, + hass_config=config, + ) + ) + + insteon_modem.devices.add_device_callback(async_new_insteon_device) + + +@callback +def async_register_services(hass, config, insteon_modem): + """Register services used by insteon component.""" + + def add_all_link(service): + """Add an INSTEON All-Link between two devices.""" + group = service.data.get(SRV_ALL_LINK_GROUP) + mode = service.data.get(SRV_ALL_LINK_MODE) + link_mode = 1 if mode.lower() == SRV_CONTROLLER else 0 + insteon_modem.start_all_linking(link_mode, group) + + def del_all_link(service): + """Delete an INSTEON All-Link between two devices.""" + group = service.data.get(SRV_ALL_LINK_GROUP) + insteon_modem.start_all_linking(255, group) + + def load_aldb(service): + """Load the device All-Link database.""" + entity_id = service.data[CONF_ENTITY_ID] + reload = service.data[SRV_LOAD_DB_RELOAD] + if entity_id.lower() == ENTITY_MATCH_ALL: + for entity_id in hass.data[DOMAIN][INSTEON_ENTITIES]: + _send_load_aldb_signal(entity_id, reload) + else: + _send_load_aldb_signal(entity_id, reload) + + def _send_load_aldb_signal(entity_id, reload): + """Send the load All-Link database signal to INSTEON entity.""" + signal = f"{entity_id}_{SIGNAL_LOAD_ALDB}" + dispatcher_send(hass, signal, reload) + + def print_aldb(service): + """Print the All-Link Database for a device.""" + # For now this sends logs to the log file. + # Future direction is to create an INSTEON control panel. + entity_id = service.data[CONF_ENTITY_ID] + signal = f"{entity_id}_{SIGNAL_PRINT_ALDB}" + dispatcher_send(hass, signal) + + def print_im_aldb(service): + """Print the All-Link Database for a device.""" + # For now this sends logs to the log file. + # Future direction is to create an INSTEON control panel. + print_aldb_to_log(insteon_modem.aldb) + + def x10_all_units_off(service): + """Send the X10 All Units Off command.""" + housecode = service.data.get(SRV_HOUSECODE) + insteon_modem.x10_all_units_off(housecode) + + def x10_all_lights_off(service): + """Send the X10 All Lights Off command.""" + housecode = service.data.get(SRV_HOUSECODE) + insteon_modem.x10_all_lights_off(housecode) + + def x10_all_lights_on(service): + """Send the X10 All Lights On command.""" + housecode = service.data.get(SRV_HOUSECODE) + insteon_modem.x10_all_lights_on(housecode) + + def scene_on(service): + """Trigger an INSTEON scene ON.""" + group = service.data.get(SRV_ALL_LINK_GROUP) + insteon_modem.trigger_group_on(group) + + def scene_off(service): + """Trigger an INSTEON scene ON.""" + group = service.data.get(SRV_ALL_LINK_GROUP) + insteon_modem.trigger_group_off(group) + + hass.services.async_register( + DOMAIN, SRV_ADD_ALL_LINK, add_all_link, schema=ADD_ALL_LINK_SCHEMA + ) + hass.services.async_register( + DOMAIN, SRV_DEL_ALL_LINK, del_all_link, schema=DEL_ALL_LINK_SCHEMA + ) + hass.services.async_register( + DOMAIN, SRV_LOAD_ALDB, load_aldb, schema=LOAD_ALDB_SCHEMA + ) + hass.services.async_register( + DOMAIN, SRV_PRINT_ALDB, print_aldb, schema=PRINT_ALDB_SCHEMA + ) + hass.services.async_register(DOMAIN, SRV_PRINT_IM_ALDB, print_im_aldb, schema=None) + hass.services.async_register( + DOMAIN, SRV_X10_ALL_UNITS_OFF, x10_all_units_off, schema=X10_HOUSECODE_SCHEMA, + ) + hass.services.async_register( + DOMAIN, SRV_X10_ALL_LIGHTS_OFF, x10_all_lights_off, schema=X10_HOUSECODE_SCHEMA, + ) + hass.services.async_register( + DOMAIN, SRV_X10_ALL_LIGHTS_ON, x10_all_lights_on, schema=X10_HOUSECODE_SCHEMA, + ) + hass.services.async_register( + DOMAIN, SRV_SCENE_ON, scene_on, schema=TRIGGER_SCENE_SCHEMA + ) + hass.services.async_register( + DOMAIN, SRV_SCENE_OFF, scene_off, schema=TRIGGER_SCENE_SCHEMA + ) + _LOGGER.debug("Insteon Services registered") + + +def print_aldb_to_log(aldb): + """Print the All-Link Database to the log file.""" + _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") + _LOGGER.warning("Use service insteon.load_aldb first") + return + + _LOGGER.info("RecID In Use Mode HWM Group Address Data 1 Data 2 Data 3") + _LOGGER.info("----- ------ ---- --- ----- -------- ------ ------ ------") + for mem_addr in aldb: + rec = aldb[mem_addr] + # For now we write this to the log + # Roadmap is to create a configuration panel + in_use = "Y" if rec.control_flags.is_in_use else "N" + mode = "C" if rec.control_flags.is_controller else "R" + hwm = "Y" if rec.control_flags.is_high_water_mark else "N" + _LOGGER.info( + " {:04x} {:s} {:s} {:s} {:3d} {:s}" + " {:3d} {:3d} {:3d}".format( + rec.mem_addr, + in_use, + mode, + hwm, + rec.group, + rec.address.human, + rec.data1, + rec.data2, + rec.data3, + ) + ) diff --git a/homeassistant/components/intent/__init__.py b/homeassistant/components/intent/__init__.py index bdf612b2e83..37761e88347 100644 --- a/homeassistant/components/intent/__init__.py +++ b/homeassistant/components/intent/__init__.py @@ -5,7 +5,8 @@ import voluptuous as vol from homeassistant.components import http from homeassistant.components.http.data_validator import RequestDataValidator -from homeassistant.core import HomeAssistant +from homeassistant.const import SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant from homeassistant.helpers import config_validation as cv, integration_platform, intent from .const import DOMAIN @@ -22,6 +23,22 @@ async def async_setup(hass: HomeAssistant, config: dict): hass, DOMAIN, _async_process_intent ) + hass.helpers.intent.async_register( + intent.ServiceIntentHandler( + intent.INTENT_TURN_ON, HA_DOMAIN, SERVICE_TURN_ON, "Turned {} on" + ) + ) + hass.helpers.intent.async_register( + intent.ServiceIntentHandler( + intent.INTENT_TURN_OFF, HA_DOMAIN, SERVICE_TURN_OFF, "Turned {} off" + ) + ) + hass.helpers.intent.async_register( + intent.ServiceIntentHandler( + intent.INTENT_TOGGLE, HA_DOMAIN, SERVICE_TOGGLE, "Toggled {}" + ) + ) + return True diff --git a/homeassistant/components/ios/__init__.py b/homeassistant/components/ios/__init__.py index 75622c29b1c..3f193993c2b 100644 --- a/homeassistant/components/ios/__init__.py +++ b/homeassistant/components/ios/__init__.py @@ -279,7 +279,7 @@ class iOSIdentifyDeviceView(HomeAssistantView): name = "api:ios:identify" def __init__(self, config_path): - """Initiliaze the view.""" + """Initialize the view.""" self._config_path = config_path async def post(self, request): diff --git a/homeassistant/components/iperf3/__init__.py b/homeassistant/components/iperf3/__init__.py index 9272a725bb7..bd5aeac099a 100644 --- a/homeassistant/components/iperf3/__init__.py +++ b/homeassistant/components/iperf3/__init__.py @@ -13,6 +13,7 @@ from homeassistant.const import ( CONF_PORT, CONF_PROTOCOL, CONF_SCAN_INTERVAL, + DATA_RATE_MEGABITS_PER_SECOND, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform @@ -39,11 +40,9 @@ ATTR_UPLOAD = "upload" ATTR_VERSION = "Version" ATTR_HOST = "host" -UNIT_OF_MEASUREMENT = "Mbit/s" - SENSOR_TYPES = { - ATTR_DOWNLOAD: [ATTR_DOWNLOAD.capitalize(), UNIT_OF_MEASUREMENT], - ATTR_UPLOAD: [ATTR_UPLOAD.capitalize(), UNIT_OF_MEASUREMENT], + ATTR_DOWNLOAD: [ATTR_DOWNLOAD.capitalize(), DATA_RATE_MEGABITS_PER_SECOND], + ATTR_UPLOAD: [ATTR_UPLOAD.capitalize(), DATA_RATE_MEGABITS_PER_SECOND], } PROTOCOLS = ["tcp", "udp"] diff --git a/homeassistant/components/ipma/.translations/ca.json b/homeassistant/components/ipma/.translations/ca.json index 29dbaa4f58d..ad2d37524c5 100644 --- a/homeassistant/components/ipma/.translations/ca.json +++ b/homeassistant/components/ipma/.translations/ca.json @@ -8,6 +8,7 @@ "data": { "latitude": "Latitud", "longitude": "Longitud", + "mode": "Mode", "name": "Nom" }, "description": "Instituto Portugu\u00eas do Mar e Atmosfera", diff --git a/homeassistant/components/ipma/.translations/da.json b/homeassistant/components/ipma/.translations/da.json index 017aff4d0ec..e2f72db7c4d 100644 --- a/homeassistant/components/ipma/.translations/da.json +++ b/homeassistant/components/ipma/.translations/da.json @@ -8,6 +8,7 @@ "data": { "latitude": "Breddegrad", "longitude": "L\u00e6ngdegrad", + "mode": "Tilstand", "name": "Navn" }, "description": "Instituto Portugu\u00eas do Mar e Atmosfera", diff --git a/homeassistant/components/ipma/.translations/de.json b/homeassistant/components/ipma/.translations/de.json index 9e717b77843..977b69576de 100644 --- a/homeassistant/components/ipma/.translations/de.json +++ b/homeassistant/components/ipma/.translations/de.json @@ -8,6 +8,7 @@ "data": { "latitude": "Breitengrad", "longitude": "L\u00e4ngengrad", + "mode": "Modus", "name": "Name" }, "description": "Instituto Portugu\u00eas do Mar e Atmosfera", diff --git a/homeassistant/components/ipma/.translations/en.json b/homeassistant/components/ipma/.translations/en.json index 15459b91f2a..d47f0dfb501 100644 --- a/homeassistant/components/ipma/.translations/en.json +++ b/homeassistant/components/ipma/.translations/en.json @@ -8,6 +8,7 @@ "data": { "latitude": "Latitude", "longitude": "Longitude", + "mode": "Mode", "name": "Name" }, "description": "Instituto Portugu\u00eas do Mar e Atmosfera", diff --git a/homeassistant/components/ipma/.translations/es.json b/homeassistant/components/ipma/.translations/es.json index acb8b51a44c..d6a43fc790b 100644 --- a/homeassistant/components/ipma/.translations/es.json +++ b/homeassistant/components/ipma/.translations/es.json @@ -8,6 +8,7 @@ "data": { "latitude": "Latitud", "longitude": "Longitud", + "mode": "Modo", "name": "Nombre" }, "description": "Instituto Portugu\u00eas do Mar e Atmosfera", diff --git a/homeassistant/components/ipma/.translations/fr.json b/homeassistant/components/ipma/.translations/fr.json index 64d03c6ae71..46b99e6651a 100644 --- a/homeassistant/components/ipma/.translations/fr.json +++ b/homeassistant/components/ipma/.translations/fr.json @@ -8,6 +8,7 @@ "data": { "latitude": "Latitude", "longitude": "Longitude", + "mode": "Mode", "name": "Nom" }, "description": "Instituto Portugu\u00eas do Mar e Atmosfera", diff --git a/homeassistant/components/ipma/.translations/hu.json b/homeassistant/components/ipma/.translations/hu.json index 62ddd85e6ef..5165fec9d90 100644 --- a/homeassistant/components/ipma/.translations/hu.json +++ b/homeassistant/components/ipma/.translations/hu.json @@ -8,6 +8,7 @@ "data": { "latitude": "Sz\u00e9less\u00e9g", "longitude": "Hossz\u00fas\u00e1g", + "mode": "M\u00f3d", "name": "N\u00e9v" }, "description": "Portug\u00e1l Atmoszf\u00e9ra Int\u00e9zet", diff --git a/homeassistant/components/ipma/.translations/it.json b/homeassistant/components/ipma/.translations/it.json index 954ff6e9ee1..6e89d425934 100644 --- a/homeassistant/components/ipma/.translations/it.json +++ b/homeassistant/components/ipma/.translations/it.json @@ -8,6 +8,7 @@ "data": { "latitude": "Latitudine", "longitude": "Longitudine", + "mode": "Modalit\u00e0", "name": "Nome" }, "description": "Instituto Portugu\u00eas do Mar e Atmosfera", diff --git a/homeassistant/components/ipma/.translations/ko.json b/homeassistant/components/ipma/.translations/ko.json index 828733c9195..c5614e17034 100644 --- a/homeassistant/components/ipma/.translations/ko.json +++ b/homeassistant/components/ipma/.translations/ko.json @@ -8,6 +8,7 @@ "data": { "latitude": "\uc704\ub3c4", "longitude": "\uacbd\ub3c4", + "mode": "\ubaa8\ub4dc", "name": "\uc774\ub984" }, "description": "\ud3ec\ub974\ud22c\uac08 \ud574\uc591 \ubc0f \ub300\uae30 \uc5f0\uad6c\uc18c (Instituto Portugu\u00eas do Mar e Atmosfera)", diff --git a/homeassistant/components/ipma/.translations/lb.json b/homeassistant/components/ipma/.translations/lb.json index c9eb3a01941..7d8280998fe 100644 --- a/homeassistant/components/ipma/.translations/lb.json +++ b/homeassistant/components/ipma/.translations/lb.json @@ -8,6 +8,7 @@ "data": { "latitude": "Breedegrad", "longitude": "L\u00e4ngegrad", + "mode": "Modus", "name": "Numm" }, "description": "Instituto Portugu\u00eas do Mar e Atmosfera", diff --git a/homeassistant/components/ipma/.translations/nl.json b/homeassistant/components/ipma/.translations/nl.json index bc10eb3573e..00b9881fd97 100644 --- a/homeassistant/components/ipma/.translations/nl.json +++ b/homeassistant/components/ipma/.translations/nl.json @@ -8,6 +8,7 @@ "data": { "latitude": "Latitude", "longitude": "Longitude", + "mode": "Mode", "name": "Naam" }, "description": "Instituto Portugu\u00eas do Mar e Atmosfera", diff --git a/homeassistant/components/ipma/.translations/no.json b/homeassistant/components/ipma/.translations/no.json index 1d5aa9c40cf..d7261732431 100644 --- a/homeassistant/components/ipma/.translations/no.json +++ b/homeassistant/components/ipma/.translations/no.json @@ -8,6 +8,7 @@ "data": { "latitude": "Breddegrad", "longitude": "Lengdegrad", + "mode": "Modus", "name": "Navn" }, "description": "Instituto Portugu\u00eas do Mar e Atmosfera", diff --git a/homeassistant/components/ipma/.translations/pl.json b/homeassistant/components/ipma/.translations/pl.json index 735f5a4a126..267b4e79137 100644 --- a/homeassistant/components/ipma/.translations/pl.json +++ b/homeassistant/components/ipma/.translations/pl.json @@ -1,13 +1,14 @@ { "config": { "error": { - "name_exists": "Nazwa ju\u017c istnieje" + "name_exists": "Nazwa ju\u017c istnieje." }, "step": { "user": { "data": { "latitude": "Szeroko\u015b\u0107 geograficzna", "longitude": "D\u0142ugo\u015b\u0107 geograficzna", + "mode": "Tryb", "name": "Nazwa" }, "description": "Portugalski Instytut Morza i Atmosfery", diff --git a/homeassistant/components/ipma/.translations/ru.json b/homeassistant/components/ipma/.translations/ru.json index 0db504c629c..96d4ee90407 100644 --- a/homeassistant/components/ipma/.translations/ru.json +++ b/homeassistant/components/ipma/.translations/ru.json @@ -8,6 +8,7 @@ "data": { "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", + "mode": "\u0420\u0435\u0436\u0438\u043c", "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.", diff --git a/homeassistant/components/ipma/.translations/sl.json b/homeassistant/components/ipma/.translations/sl.json index da6a1dac859..2dcfcde7404 100644 --- a/homeassistant/components/ipma/.translations/sl.json +++ b/homeassistant/components/ipma/.translations/sl.json @@ -8,6 +8,7 @@ "data": { "latitude": "Zemljepisna \u0161irina", "longitude": "Zemljepisna dol\u017eina", + "mode": "Na\u010din", "name": "Ime" }, "description": "Instituto Portugu\u00eas do Mar e Atmosfera", diff --git a/homeassistant/components/ipma/.translations/sv.json b/homeassistant/components/ipma/.translations/sv.json index 4bdba6f0d08..e8cba56a0a0 100644 --- a/homeassistant/components/ipma/.translations/sv.json +++ b/homeassistant/components/ipma/.translations/sv.json @@ -8,6 +8,7 @@ "data": { "latitude": "Latitud", "longitude": "Longitud", + "mode": "L\u00e4ge", "name": "Namn" }, "description": "Portugisiska institutet f\u00f6r hav och atmosf\u00e4ren", diff --git a/homeassistant/components/ipma/.translations/zh-Hans.json b/homeassistant/components/ipma/.translations/zh-Hans.json index 6c5654b6388..10d51832964 100644 --- a/homeassistant/components/ipma/.translations/zh-Hans.json +++ b/homeassistant/components/ipma/.translations/zh-Hans.json @@ -8,6 +8,7 @@ "data": { "latitude": "\u7eac\u5ea6", "longitude": "\u7ecf\u5ea6", + "mode": "\u6a21\u5f0f", "name": "\u540d\u79f0" }, "description": "\u8461\u8404\u7259\u56fd\u5bb6\u5927\u6c14\u7814\u7a76\u6240", diff --git a/homeassistant/components/ipma/.translations/zh-Hant.json b/homeassistant/components/ipma/.translations/zh-Hant.json index 25c832e51c6..de36336f2c4 100644 --- a/homeassistant/components/ipma/.translations/zh-Hant.json +++ b/homeassistant/components/ipma/.translations/zh-Hant.json @@ -8,6 +8,7 @@ "data": { "latitude": "\u7def\u5ea6", "longitude": "\u7d93\u5ea6", + "mode": "\u6a21\u5f0f", "name": "\u540d\u7a31" }, "description": "Instituto Portugu\u00eas do Mar e Atmosfera", diff --git a/homeassistant/components/ipma/config_flow.py b/homeassistant/components/ipma/config_flow.py index d1532066f68..3811d30bfbe 100644 --- a/homeassistant/components/ipma/config_flow.py +++ b/homeassistant/components/ipma/config_flow.py @@ -2,10 +2,11 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, CONF_NAME import homeassistant.helpers.config_validation as cv from .const import DOMAIN, HOME_LOCATION_NAME +from .weather import FORECAST_MODE @config_entries.HANDLERS.register(DOMAIN) @@ -49,6 +50,7 @@ class IpmaFlowHandler(config_entries.ConfigFlow): vol.Required(CONF_NAME, default=name): str, vol.Required(CONF_LATITUDE, default=latitude): cv.latitude, vol.Required(CONF_LONGITUDE, default=longitude): cv.longitude, + vol.Required(CONF_MODE, default="daily"): vol.In(FORECAST_MODE), } ), errors=self._errors, diff --git a/homeassistant/components/ipma/manifest.json b/homeassistant/components/ipma/manifest.json index cd66ce7461b..02d4e459f72 100644 --- a/homeassistant/components/ipma/manifest.json +++ b/homeassistant/components/ipma/manifest.json @@ -3,7 +3,7 @@ "name": "Instituto Português do Mar e Atmosfera (IPMA)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ipma", - "requirements": ["pyipma==2.0.2"], + "requirements": ["pyipma==2.0.3"], "dependencies": [], "codeowners": ["@dgomes", "@abmantis"] } diff --git a/homeassistant/components/ipma/strings.json b/homeassistant/components/ipma/strings.json index f22d1b62fe4..ea8b9edcc86 100644 --- a/homeassistant/components/ipma/strings.json +++ b/homeassistant/components/ipma/strings.json @@ -8,7 +8,8 @@ "data": { "name": "Name", "latitude": "Latitude", - "longitude": "Longitude" + "longitude": "Longitude", + "mode": "Mode" } } }, diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index 7b07406d007..1fce3922b58 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -13,13 +13,22 @@ from homeassistant.components.weather import ( ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, + 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.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_MODE, + CONF_NAME, + TEMP_CELSIUS, +) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import Throttle +from homeassistant.util.dt import now, parse_datetime _LOGGER = logging.getLogger(__name__) @@ -44,11 +53,14 @@ CONDITION_CLASSES = { "exceptional": [], } +FORECAST_MODE = ["hourly", "daily"] + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_LATITUDE): cv.latitude, vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_MODE, default="daily"): vol.In(FORECAST_MODE), } ) @@ -96,10 +108,12 @@ async def async_get_location(hass, api, latitude, longitude): location = await Location.get(api, float(latitude), float(longitude)) _LOGGER.debug( - "Initializing for coordinates %s, %s -> station %s", + "Initializing for coordinates %s, %s -> station %s (%d, %d)", latitude, longitude, location.station, + location.id_station, + location.global_id_local, ) return location @@ -112,6 +126,7 @@ class IPMAWeather(WeatherEntity): """Initialise the platform with a data instance and station name.""" self._api = api self._location_name = config.get(CONF_NAME, location.name) + self._mode = config.get(CONF_MODE) self._location = location self._observation = None self._forecast = None @@ -129,7 +144,7 @@ class IPMAWeather(WeatherEntity): _LOGGER.warning("Could not update weather observation") if new_forecast: - self._forecast = [f for f in new_forecast if f.forecasted_hours == 24] + self._forecast = new_forecast else: _LOGGER.warning("Could not update weather forecast") @@ -220,22 +235,57 @@ class IPMAWeather(WeatherEntity): if not self._forecast: return [] - fcdata_out = [ - { - ATTR_FORECAST_TIME: data_in.forecast_date, - ATTR_FORECAST_CONDITION: next( - ( - k - for k, v in CONDITION_CLASSES.items() - if int(data_in.weather_type) in v + if self._mode == "hourly": + forecast_filtered = [ + x + for x in self._forecast + if x.forecasted_hours == 1 + and parse_datetime(x.forecast_date) + > (now().utcnow() - timedelta(hours=1)) + ] + + fcdata_out = [ + { + ATTR_FORECAST_TIME: data_in.forecast_date, + ATTR_FORECAST_CONDITION: next( + ( + k + for k, v in CONDITION_CLASSES.items() + if int(data_in.weather_type) in v + ), + None, ), - None, - ), - ATTR_FORECAST_TEMP_LOW: data_in.min_temperature, - ATTR_FORECAST_TEMP: data_in.max_temperature, - ATTR_FORECAST_PRECIPITATION: data_in.precipitation_probability, - } - for data_in in self._forecast - ] + ATTR_FORECAST_TEMP: float(data_in.feels_like_temperature), + ATTR_FORECAST_PRECIPITATION: ( + data_in.precipitation_probability + if float(data_in.precipitation_probability) >= 0 + else None + ), + ATTR_FORECAST_WIND_SPEED: data_in.wind_strength, + ATTR_FORECAST_WIND_BEARING: data_in.wind_direction, + } + for data_in in forecast_filtered + ] + else: + forecast_filtered = [f for f in self._forecast if f.forecasted_hours == 24] + fcdata_out = [ + { + ATTR_FORECAST_TIME: data_in.forecast_date, + ATTR_FORECAST_CONDITION: next( + ( + k + for k, v in CONDITION_CLASSES.items() + if int(data_in.weather_type) in v + ), + None, + ), + ATTR_FORECAST_TEMP_LOW: data_in.min_temperature, + ATTR_FORECAST_TEMP: data_in.max_temperature, + ATTR_FORECAST_PRECIPITATION: data_in.precipitation_probability, + ATTR_FORECAST_WIND_SPEED: data_in.wind_strength, + ATTR_FORECAST_WIND_BEARING: data_in.wind_direction, + } + for data_in in forecast_filtered + ] return fcdata_out diff --git a/homeassistant/components/iqvia/.translations/pl.json b/homeassistant/components/iqvia/.translations/pl.json index b528cdeb70f..b8c014c3dc9 100644 --- a/homeassistant/components/iqvia/.translations/pl.json +++ b/homeassistant/components/iqvia/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "Kod pocztowy ju\u017c zarejestrowany", + "identifier_exists": "Kod pocztowy jest ju\u017c zarejestrowany.", "invalid_zip_code": "Kod pocztowy jest nieprawid\u0142owy" }, "step": { diff --git a/homeassistant/components/iqvia/const.py b/homeassistant/components/iqvia/const.py index 09548ee929a..52e657bc2c0 100644 --- a/homeassistant/components/iqvia/const.py +++ b/homeassistant/components/iqvia/const.py @@ -6,7 +6,7 @@ CONF_ZIP_CODE = "zip_code" DATA_CLIENT = "client" DATA_LISTENER = "listener" -TOPIC_DATA_UPDATE = "data_update" +TOPIC_DATA_UPDATE = f"{DOMAIN}_data_update" TYPE_ALLERGY_FORECAST = "allergy_average_forecasted" TYPE_ALLERGY_INDEX = "allergy_index" diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 7a5eb7e56df..363269bc589 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,7 +3,7 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": ["numpy==1.17.4", "pyiqvia==0.2.1"], + "requirements": ["numpy==1.18.1", "pyiqvia==0.2.1"], "dependencies": [], "codeowners": ["@bachya"] } diff --git a/homeassistant/components/izone/.translations/hu.json b/homeassistant/components/izone/.translations/hu.json new file mode 100644 index 00000000000..79c621ce125 --- /dev/null +++ b/homeassistant/components/izone/.translations/hu.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani az iZone-t?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/sv.json b/homeassistant/components/izone/.translations/sv.json new file mode 100644 index 00000000000..c2c952d69fe --- /dev/null +++ b/homeassistant/components/izone/.translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Inga iZone-enheter hittades i n\u00e4tverket.", + "single_instance_allowed": "Endast en enda konfiguration av iZone \u00e4r n\u00f6dv\u00e4ndig." + }, + "step": { + "confirm": { + "description": "Vill du konfigurera iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/discovery.py b/homeassistant/components/izone/discovery.py index c49144f1db9..7690600786e 100644 --- a/homeassistant/components/izone/discovery.py +++ b/homeassistant/components/izone/discovery.py @@ -44,11 +44,11 @@ class DiscoveryService(pizone.Listener): async_dispatcher_send(self.hass, DISPATCH_CONTROLLER_RECONNECTED, ctrl) def controller_update(self, ctrl: pizone.Controller) -> None: - """System update message is recieved from the controller.""" + """System update message is received from the controller.""" async_dispatcher_send(self.hass, DISPATCH_CONTROLLER_UPDATE, ctrl) def zone_update(self, ctrl: pizone.Controller, zone: pizone.Zone) -> None: - """Zone update message is recieved from the controller.""" + """Zone update message is received from the controller.""" async_dispatcher_send(self.hass, DISPATCH_ZONE_UPDATE, ctrl, zone) diff --git a/homeassistant/components/jewish_calendar/__init__.py b/homeassistant/components/jewish_calendar/__init__.py index 2e4644d7ef5..45f979874f7 100644 --- a/homeassistant/components/jewish_calendar/__init__.py +++ b/homeassistant/components/jewish_calendar/__init__.py @@ -21,6 +21,7 @@ SENSOR_TYPES = { "weekly_portion": ["Parshat Hashavua", "mdi:book-open-variant"], "holiday": ["Holiday", "mdi:calendar-star"], "omer_count": ["Day of the Omer", "mdi:counter"], + "daf_yomi": ["Daf Yomi", "mdi:book-open-variant"], }, "time": { "first_light": ["Alot Hashachar", "mdi:weather-sunset-up"], diff --git a/homeassistant/components/jewish_calendar/manifest.json b/homeassistant/components/jewish_calendar/manifest.json index 8e1781f310b..c4ebb382a44 100644 --- a/homeassistant/components/jewish_calendar/manifest.json +++ b/homeassistant/components/jewish_calendar/manifest.json @@ -2,7 +2,7 @@ "domain": "jewish_calendar", "name": "Jewish Calendar", "documentation": "https://www.home-assistant.io/integrations/jewish_calendar", - "requirements": ["hdate==0.9.3"], + "requirements": ["hdate==0.9.5"], "dependencies": [], "codeowners": ["@tsvi"] } diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index d0376694a44..7da9d7e31d0 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -73,7 +73,7 @@ class JewishCalendarSensor(Entity): _LOGGER.debug("Now: %s Sunset: %s", now, sunset) - date = hdate.HDate(today, diaspora=self._diaspora, hebrew=self._hebrew) + daytime_date = hdate.HDate(today, diaspora=self._diaspora, hebrew=self._hebrew) # The Jewish day starts after darkness (called "tzais") and finishes at # sunset ("shkia"). The time in between is a gray area (aka "Bein @@ -82,16 +82,16 @@ class JewishCalendarSensor(Entity): # For some sensors, it is more interesting to consider the date to be # tomorrow based on sunset ("shkia"), for others based on "tzais". # Hence the following variables. - after_tzais_date = after_shkia_date = date + after_tzais_date = after_shkia_date = daytime_date today_times = self.make_zmanim(today) if now > sunset: - after_shkia_date = date.next_day + after_shkia_date = daytime_date.next_day if today_times.havdalah and now > today_times.havdalah: - after_tzais_date = date.next_day + after_tzais_date = daytime_date.next_day - self._state = self.get_state(after_shkia_date, after_tzais_date) + self._state = self.get_state(daytime_date, after_shkia_date, after_tzais_date) _LOGGER.debug("New value for %s: %s", self._type, self._state) def make_zmanim(self, date): @@ -112,7 +112,7 @@ class JewishCalendarSensor(Entity): return {} - def get_state(self, after_shkia_date, after_tzais_date): + def get_state(self, daytime_date, 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. @@ -128,6 +128,8 @@ class JewishCalendarSensor(Entity): return after_shkia_date.holiday_description if self._type == "omer_count": return after_shkia_date.omer_day + if self._type == "daf_yomi": + return daytime_date.daf_yomi return None @@ -157,7 +159,7 @@ class JewishCalendarTimeSensor(JewishCalendarSensor): return attrs - def get_state(self, after_shkia_date, after_tzais_date): + def get_state(self, daytime_date, 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( diff --git a/homeassistant/components/kef/manifest.json b/homeassistant/components/kef/manifest.json index a2769cd8eb6..135f8e1cf54 100644 --- a/homeassistant/components/kef/manifest.json +++ b/homeassistant/components/kef/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/kef", "dependencies": [], "codeowners": ["@basnijholt"], - "requirements": ["aiokef==0.2.6", "getmac==0.8.1"] + "requirements": ["aiokef==0.2.7", "getmac==0.8.1"] } diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index dc91b94f5ef..d4a1d7a4df3 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -11,6 +11,10 @@ import voluptuous as vol from homeassistant.components.media_player import ( PLATFORM_SCHEMA, + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, @@ -214,6 +218,10 @@ class KefMediaPlayer(MediaPlayerDevice): | SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE | SUPPORT_TURN_OFF + | SUPPORT_NEXT_TRACK # only in Bluetooth and Wifi + | SUPPORT_PAUSE # only in Bluetooth and Wifi + | SUPPORT_PLAY # only in Bluetooth and Wifi + | SUPPORT_PREVIOUS_TRACK # only in Bluetooth and Wifi ) if self._supports_on: support_kef |= SUPPORT_TURN_ON @@ -280,3 +288,19 @@ class KefMediaPlayer(MediaPlayerDevice): await self._speaker.set_source(source) else: raise ValueError(f"Unknown input source: {source}.") + + async def async_media_play(self): + """Send play command.""" + await self._speaker.play_pause() + + async def async_media_pause(self): + """Send pause command.""" + await self._speaker.play_pause() + + async def async_media_previous_track(self): + """Send previous track command.""" + await self._speaker.prev_track() + + async def async_media_next_track(self): + """Send next track command.""" + await self._speaker.next_track() diff --git a/homeassistant/components/konnected/.translations/ca.json b/homeassistant/components/konnected/.translations/ca.json new file mode 100644 index 00000000000..ccb03ef7add --- /dev/null +++ b/homeassistant/components/konnected/.translations/ca.json @@ -0,0 +1,100 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de dades de configuraci\u00f3 pel dispositiu ja est\u00e0 en curs.", + "not_konn_panel": "No s'ha reconegut com a un dispositiu Konnected.io", + "unknown": "S'ha produ\u00eft un error desconegut" + }, + "error": { + "cannot_connect": "No s'ha pogut connectar amb el panell Konnected a {host}:{port}" + }, + "step": { + "confirm": { + "description": "Model: {model} \nAmfitri\u00f3: {host} \nPort: {port} \n\nPots configurar el comportament de les E/S (I/O) i del panell a la configuraci\u00f3 del panell d\u2019alarma Konnected.", + "title": "Dispositiu Konnected llest" + }, + "user": { + "data": { + "host": "Adre\u00e7a IP del dispositiu Konnected", + "port": "Port del dispositiu Konnected" + }, + "description": "Introdueix la informaci\u00f3 d'amfitri\u00f3 del panell Konnected.", + "title": "Descoberta de dispositiu Konnected" + } + }, + "title": "Konnected.io" + }, + "options": { + "abort": { + "not_konn_panel": "No s'ha reconegut com a un dispositiu Konnected.io" + }, + "step": { + "options_binary": { + "data": { + "inverse": "Inverteix l'estat obert/tancat", + "name": "Nom (opcional)", + "type": "Tipus de sensor binari" + }, + "description": "Selecciona les opcions pel sensor binari de {zone}", + "title": "Configuraci\u00f3 de sensor binari" + }, + "options_digital": { + "data": { + "name": "Nom (opcional)", + "poll_interval": "Interval de sondeig (minuts) (opcional)", + "type": "Tipus de sensor" + }, + "description": "Selecciona les opcions pel sensor digital de {zone}", + "title": "Configuraci\u00f3 de sensor digital" + }, + "options_io": { + "data": { + "1": "Zona 1", + "2": "Zona 2", + "3": "Zona 3", + "4": "Zona 4", + "5": "Zona 5", + "6": "Zona 6", + "7": "Zona 7", + "out": "OUT (sortida)" + }, + "description": "S'ha descobert {model} a {host}. Selecciona la configuraci\u00f3 b\u00e0sica de cada Entrada/Sortida (I/O), en funci\u00f3 del tipus que sigui pot ser que et permeti sensors binaris (contactes oberts/tancats), sensors digitals (DHT i ds18b20) o sortides commutables. M\u00e9s endavant les podr\u00e0s configurar de manera m\u00e9s detallada.", + "title": "Configuraci\u00f3 E/S (I/O)" + }, + "options_io_ext": { + "data": { + "10": "Zona 10", + "11": "Zona 11", + "12": "Zona 12", + "8": "Zona 8", + "9": "Zona 9", + "alarm1": "ALARMA1", + "alarm2_out2": "OUT2/ALARMA2", + "out1": "OUT1" + }, + "description": "Selecciona la configuraci\u00f3 de les E/S restants. Podr\u00e0s configurar opcions m\u00e9s detallades en els passos seg\u00fcents.", + "title": "Configuraci\u00f3 E/S (I/O) ampliades" + }, + "options_misc": { + "data": { + "blink": "Parpelleja el LED del panell quan s'envien canvis d'estat" + }, + "description": "Selecciona el comportament desitjat del panell", + "title": "Configuraci\u00f3 diversos" + }, + "options_switch": { + "data": { + "activation": "Sortida quan estigui ON", + "momentary": "Durada del pols (ms) (opcional)", + "name": "Nom (opcional)", + "pause": "Pausa entre polsos (ms) (opcional)", + "repeat": "Repeticions (-1 = infinit) (opcional)" + }, + "description": "Selecciona les opcions de sortida per a {zone}", + "title": "Configuraci\u00f3 de sortida commutable" + } + }, + "title": "Opcions del panell d'alarma Konnected" + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/da.json b/homeassistant/components/konnected/.translations/da.json new file mode 100644 index 00000000000..db37ad73610 --- /dev/null +++ b/homeassistant/components/konnected/.translations/da.json @@ -0,0 +1,104 @@ +{ + "config": { + "abort": { + "already_configured": "Enheden er allerede konfigureret", + "already_in_progress": "Enhedskonfiguration er allerede i gang.", + "not_konn_panel": "Ikke en genkendt Konnected.io-enhed", + "unknown": "Ukendt fejl opstod" + }, + "error": { + "cannot_connect": "Der kan ikke oprettes forbindelse til et Konnected-panel p\u00e5 {host}:{port}" + }, + "step": { + "confirm": { + "description": "Model: {model}\nV\u00e6rt: {host}\nPort: {port}\n\nDu kan konfigurere IO og panelfunktionsm\u00e5den i indstillingerne for Konnected-alarmpanel.", + "title": "Konnected-enhed klar" + }, + "user": { + "data": { + "host": "Konnected-enhedens IP-adresse", + "port": "Konnected-enhedsport" + }, + "description": "Indtast v\u00e6rtsinformationen for dit Konnected-panel.", + "title": "Find Konnected-enhed" + } + }, + "title": "Konnected.io" + }, + "options": { + "abort": { + "not_konn_panel": "Ikke en genkendt Konnected.io-enhed" + }, + "error": { + "one": "en", + "other": "anden" + }, + "step": { + "options_binary": { + "data": { + "inverse": "Inverter tilstanden \u00e5ben/lukket", + "name": "Navn (valgfrit)", + "type": "Bin\u00e6r sensortype" + }, + "description": "V\u00e6lg indstillingerne for den bin\u00e6re sensor, der er knyttet til {zone}", + "title": "Konfigurer bin\u00e6r sensor" + }, + "options_digital": { + "data": { + "name": "Navn (valgfrit)", + "poll_interval": "Foresp\u00f8rgselsinterval (minutter) (valgfrit)", + "type": "Sensortype" + }, + "description": "V\u00e6lg indstillingerne for den digitale sensor, der er knyttet til {zone}", + "title": "Konfigurer digital sensor" + }, + "options_io": { + "data": { + "1": "Zone 1", + "2": "Zone 2", + "3": "Zone 3", + "4": "Zone 4", + "5": "Zone 5", + "6": "Zone 6", + "7": "Zone 7", + "out": "OUT" + }, + "description": "Der blev fundet en {model} p\u00e5 {host}. V\u00e6lg basiskonfigurationen af hver I/O nedenfor - afh\u00e6ngigt af I/O kan det give mulighed for bin\u00e6re sensorer (\u00e5ben-/lukket-kontakter), digitale sensorer (dht og ds18b20) eller omskiftelige outputs. Du kan konfigurere detaljerede indstillinger i de n\u00e6ste trin.", + "title": "Konfigurer I/O" + }, + "options_io_ext": { + "data": { + "10": "Zone 10", + "11": "Zone 11", + "12": "Zone 12", + "8": "Zone 8", + "9": "Zone 9", + "alarm1": "ALARM1", + "alarm2_out2": "OUT2/ALARM2", + "out1": "OUT1" + }, + "description": "V\u00e6lg konfigurationen af det resterende I/O nedenfor. Du kan konfigurere detaljerede indstillinger i de n\u00e6ste trin.", + "title": "Konfigurer udvidet I/O" + }, + "options_misc": { + "data": { + "blink": "Blink panel-LED ved sending af tilstands\u00e6ndring" + }, + "description": "V\u00e6lg den \u00f8nskede funktionsm\u00e5de for panelet", + "title": "Konfigurer diverse" + }, + "options_switch": { + "data": { + "activation": "Output n\u00e5r der er t\u00e6ndt", + "momentary": "Impulsvarighed (ms) (valgfrit)", + "name": "Navn (valgfrit)", + "pause": "Pause mellem impulser (ms) (valgfrit)", + "repeat": "Gange til gentagelse (-1=uendelig) (valgfrit)" + }, + "description": "V\u00e6lg outputindstillingerne for {zone}", + "title": "Konfigurer skifteligt output" + } + }, + "title": "Indstillinger for Konnected-alarmpanel" + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/de.json b/homeassistant/components/konnected/.translations/de.json new file mode 100644 index 00000000000..fa5b1f53dfb --- /dev/null +++ b/homeassistant/components/konnected/.translations/de.json @@ -0,0 +1,95 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsfluss f\u00fcr das Ger\u00e4t wird bereits ausgef\u00fchrt.", + "not_konn_panel": "Kein anerkanntes Konnected.io-Ger\u00e4t", + "unknown": "Unbekannter Fehler ist aufgetreten" + }, + "error": { + "cannot_connect": "Es konnte keine Verbindung zu einem Konnected-Panel unter {host}:{port} hergestellt werden." + }, + "step": { + "confirm": { + "description": "Modell: {model} \nHost: {host} \nPort: {port} \n\nSie k\u00f6nnen das I / O - und Bedienfeldverhalten in den Einstellungen der verbundenen Alarmzentrale konfigurieren.", + "title": "Konnected Device Bereit" + }, + "import_confirm": { + "title": "Importieren von Konnected Ger\u00e4t" + }, + "user": { + "data": { + "host": "Konnected Ger\u00e4t IP-Adresse", + "port": "Konnected Device Port" + }, + "description": "Bitte geben Sie die Hostinformationen f\u00fcr Ihr Konnected Panel ein.", + "title": "Entdecken Sie Konnected Ger\u00e4t" + } + }, + "title": "Konnected.io" + }, + "options": { + "abort": { + "not_konn_panel": "Kein anerkanntes Konnected.io-Ger\u00e4t" + }, + "step": { + "options_binary": { + "data": { + "inverse": "Invertieren Sie den \u00d6ffnungs- / Schlie\u00dfzustand", + "name": "Name (optional)", + "type": "Bin\u00e4rer Sensortyp" + }, + "description": "Bitte w\u00e4hlen Sie die Optionen f\u00fcr den an {zone} angeschlossenen Bin\u00e4rsensor", + "title": "Konfigurieren Sie den Bin\u00e4rsensor" + }, + "options_digital": { + "data": { + "name": "Name (optional)", + "poll_interval": "Abfrageintervall (Minuten) (optional)", + "type": "Sensortyp" + }, + "description": "Bitte w\u00e4hlen Sie die Optionen f\u00fcr den an {zone} angeschlossenen digitalen Sensor aus", + "title": "Konfigurieren Sie den digitalen Sensor" + }, + "options_io": { + "data": { + "1": "Zone 1", + "2": "Zone 2", + "3": "Zone 3", + "4": "Zone 4", + "5": "Zone 5", + "6": "Zone 6", + "7": "Zone 7", + "out": "OUT" + }, + "title": "Konfigurieren von I/O" + }, + "options_io_ext": { + "data": { + "10": "Zone 10", + "11": "Zone 11", + "12": "Zone 12", + "8": "Zone 8", + "9": "Zone 9", + "alarm1": "ALARM1", + "alarm2_out2": "OUT2/ALARM2", + "out1": "OUT1" + } + }, + "options_misc": { + "description": "Bitte w\u00e4hlen Sie das gew\u00fcnschte Verhalten f\u00fcr Ihr Panel" + }, + "options_switch": { + "data": { + "activation": "Ausgabe, wenn eingeschaltet", + "momentary": "Impulsdauer (ms) (optional)", + "name": "Name (optional)", + "pause": "Pause zwischen Impulsen (ms) (optional)", + "repeat": "Zeit zum Wiederholen (-1 = unendlich) (optional)" + }, + "description": "Bitte w\u00e4hlen Sie die Ausgabeoptionen f\u00fcr {zone}" + } + }, + "title": "Konnected Alarm Panel-Optionen" + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/en.json b/homeassistant/components/konnected/.translations/en.json new file mode 100644 index 00000000000..fd0a8e84e37 --- /dev/null +++ b/homeassistant/components/konnected/.translations/en.json @@ -0,0 +1,104 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Config flow for device is already in progress.", + "not_konn_panel": "Not a recognized Konnected.io device", + "unknown": "Unknown error occurred" + }, + "error": { + "cannot_connect": "Unable to connect to a Konnected Panel at {host}:{port}" + }, + "step": { + "confirm": { + "description": "Model: {model}\nID: {id}\nHost: {host}\nPort: {port}\n\nYou can configure the IO and panel behavior in the Konnected Alarm Panel settings.", + "title": "Konnected Device Ready" + }, + "import_confirm": { + "description": "A Konnected Alarm Panel with ID {id} has been discovered in configuration.yaml. This flow will allow you to import it into a config entry.", + "title": "Import Konnected Device" + }, + "user": { + "data": { + "host": "Konnected device IP address", + "port": "Konnected device port" + }, + "description": "Please enter the host information for your Konnected Panel.", + "title": "Discover Konnected Device" + } + }, + "title": "Konnected.io" + }, + "options": { + "abort": { + "not_konn_panel": "Not a recognized Konnected.io device" + }, + "step": { + "options_binary": { + "data": { + "inverse": "Invert the open/close state", + "name": "Name (optional)", + "type": "Binary Sensor Type" + }, + "description": "Please select the options for the binary sensor attached to {zone}", + "title": "Configure Binary Sensor" + }, + "options_digital": { + "data": { + "name": "Name (optional)", + "poll_interval": "Poll Interval (minutes) (optional)", + "type": "Sensor Type" + }, + "description": "Please select the options for the digital sensor attached to {zone}", + "title": "Configure Digital Sensor" + }, + "options_io": { + "data": { + "1": "Zone 1", + "2": "Zone 2", + "3": "Zone 3", + "4": "Zone 4", + "5": "Zone 5", + "6": "Zone 6", + "7": "Zone 7", + "out": "OUT" + }, + "description": "Discovered a {model} at {host}. Select the base configuration of each I/O below - depending on the I/O it may allow for binary sensors (open/close contacts), digital sensors (dht and ds18b20), or switchable outputs. You'll be able to configure detailed options in the next steps.", + "title": "Configure I/O" + }, + "options_io_ext": { + "data": { + "10": "Zone 10", + "11": "Zone 11", + "12": "Zone 12", + "8": "Zone 8", + "9": "Zone 9", + "alarm1": "ALARM1", + "alarm2_out2": "OUT2/ALARM2", + "out1": "OUT1" + }, + "description": "Select the configuration of the remaining I/O below. You'll be able to configure detailed options in the next steps.", + "title": "Configure Extended I/O" + }, + "options_misc": { + "data": { + "blink": "Blink panel LED on when sending state change" + }, + "description": "Please select the desired behavior for your panel", + "title": "Configure Misc" + }, + "options_switch": { + "data": { + "activation": "Output when on", + "momentary": "Pulse duration (ms) (optional)", + "name": "Name (optional)", + "pause": "Pause between pulses (ms) (optional)", + "repeat": "Times to repeat (-1=infinite) (optional)" + }, + "description": "Please select the output options for {zone}", + "title": "Configure Switchable Output" + } + }, + "title": "Konnected Alarm Panel Options" + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/es.json b/homeassistant/components/konnected/.translations/es.json new file mode 100644 index 00000000000..f72a58cf649 --- /dev/null +++ b/homeassistant/components/konnected/.translations/es.json @@ -0,0 +1,104 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n para el dispositivo ya est\u00e1 en curso.", + "not_konn_panel": "No es un dispositivo Konnected.io reconocido", + "unknown": "Se produjo un error desconocido" + }, + "error": { + "cannot_connect": "No se puede conectar a un Panel conectado en {host}:{port}" + }, + "step": { + "confirm": { + "description": "Modelo: {model}\nHost: {host}\nPuerto: {port}\n\nPuede configurar las E/S y el comportamiento del panel en los ajustes del panel de alarmas Konnected.", + "title": "Dispositivo Konnected Listo" + }, + "user": { + "data": { + "host": "Direcci\u00f3n IP del dispositivo Konnected", + "port": "Puerto del dispositivo Konnected" + }, + "description": "Introduzca la informaci\u00f3n del host de su panel Konnected.", + "title": "Descubrir el dispositivo Konnected" + } + }, + "title": "Konnected.io" + }, + "options": { + "abort": { + "not_konn_panel": "No es un dispositivo Konnected.io reconocido" + }, + "error": { + "one": "", + "other": "otros" + }, + "step": { + "options_binary": { + "data": { + "inverse": "Invertir el estado de apertura/cierre", + "name": "Nombre (opcional)", + "type": "Tipo de sensor binario" + }, + "description": "Seleccione las opciones para el sensor binario conectado a {zone}", + "title": "Configurar sensor binario" + }, + "options_digital": { + "data": { + "name": "Nombre (opcional)", + "poll_interval": "Intervalo de sondeo (minutos) (opcional)", + "type": "Tipo de sensor" + }, + "description": "Seleccione las opciones para el sensor digital conectado a {zone}", + "title": "Configurar el sensor digital" + }, + "options_io": { + "data": { + "1": "Zona 1", + "2": "Zona 2", + "3": "Zona 3", + "4": "Zona 4", + "5": "Zona 5", + "6": "Zona 6", + "7": "Zona 7", + "out": "OUT" + }, + "description": "Descubierto un {model} en {host} . Seleccione la configuraci\u00f3n base de cada E / S a continuaci\u00f3n: seg\u00fan la E / S, puede permitir sensores binarios (contactos de apertura / cierre), sensores digitales (dht y ds18b20) o salidas conmutables. Podr\u00e1 configurar opciones detalladas en los pr\u00f3ximos pasos.", + "title": "Configurar E/S" + }, + "options_io_ext": { + "data": { + "10": "Zona 10", + "11": "Zona 11", + "12": "Zona 12", + "8": "Zona 8", + "9": "Zona 9", + "alarm1": "ALARMA1", + "alarm2_out2": "OUT2/ALARM2", + "out1": "OUT1" + }, + "description": "Seleccione la configuraci\u00f3n de las E/S restantes a continuaci\u00f3n. Podr\u00e1s configurar opciones detalladas en los pr\u00f3ximos pasos.", + "title": "Configurar E/S extendidas" + }, + "options_misc": { + "data": { + "blink": "Parpadea el LED del panel cuando se env\u00eda un cambio de estado" + }, + "description": "Seleccione el comportamiento deseado para su panel", + "title": "Configurar miscel\u00e1neos" + }, + "options_switch": { + "data": { + "activation": "Salida cuando est\u00e1 activada", + "momentary": "Duraci\u00f3n del pulso (ms) (opcional)", + "name": "Nombre (opcional)", + "pause": "Pausa entre pulsos (ms) (opcional)", + "repeat": "Tiempos de repetici\u00f3n (-1 = infinito) (opcional)" + }, + "description": "Por favor, seleccione las opciones de salida para {zone}", + "title": "Configurar la salida conmutable" + } + }, + "title": "Opciones del panel de alarma Konnected" + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/fr.json b/homeassistant/components/konnected/.translations/fr.json new file mode 100644 index 00000000000..e6c0cded9fc --- /dev/null +++ b/homeassistant/components/konnected/.translations/fr.json @@ -0,0 +1,72 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "not_konn_panel": "Non reconnu comme appareil Konnected.io", + "unknown": "Une erreur inconnue s'est produite" + }, + "step": { + "confirm": { + "title": "Appareil Konnected pr\u00eat" + }, + "user": { + "data": { + "host": "Adresse IP de l\u2019appareil Konnected" + } + } + }, + "title": "Konnected.io" + }, + "options": { + "error": { + "one": "Vide", + "other": "Vide" + }, + "step": { + "options_binary": { + "data": { + "inverse": "Inverser l'\u00e9tat ouvert / ferm\u00e9", + "name": "Nom (facultatif)", + "type": "Type de capteur binaire" + }, + "description": "Veuillez s\u00e9lectionner les options du capteur binaire attach\u00e9 \u00e0 {zone}", + "title": "Configurer le capteur binaire" + }, + "options_digital": { + "data": { + "name": "Nom (facultatif)", + "poll_interval": "Intervalle d'interrogation (minutes) (facultatif)", + "type": "Type de capteur" + }, + "description": "Veuillez s\u00e9lectionner les options du capteur digital attach\u00e9 \u00e0 {zone}", + "title": "Configurer le capteur digital" + }, + "options_io": { + "data": { + "1": "Zone 1", + "2": "Zone 2", + "3": "Zone 3", + "4": "Zone 4", + "5": "Zone 5", + "6": "Zone 6", + "7": "Zone 7" + } + }, + "options_io_ext": { + "data": { + "10": "Zone 10", + "11": "Zone 11", + "12": "Zone 12", + "8": "Zone 8", + "9": "Zone 9", + "alarm1": "ALARME1" + } + }, + "options_switch": { + "data": { + "name": "Nom (facultatif)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/hu.json b/homeassistant/components/konnected/.translations/hu.json new file mode 100644 index 00000000000..35a4adfebe3 --- /dev/null +++ b/homeassistant/components/konnected/.translations/hu.json @@ -0,0 +1,17 @@ +{ + "options": { + "step": { + "options_digital": { + "data": { + "name": "N\u00e9v (nem k\u00f6telez\u0151)", + "type": "\u00c9rz\u00e9kel\u0151 t\u00edpusa" + } + }, + "options_switch": { + "data": { + "name": "N\u00e9v (nem k\u00f6telez\u0151)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/it.json b/homeassistant/components/konnected/.translations/it.json new file mode 100644 index 00000000000..fb18ece10f8 --- /dev/null +++ b/homeassistant/components/konnected/.translations/it.json @@ -0,0 +1,104 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione per il dispositivo \u00e8 gi\u00e0 in corso.", + "not_konn_panel": "Non \u00e8 un dispositivo Konnected.io riconosciuto", + "unknown": "Si \u00e8 verificato un errore sconosciuto" + }, + "error": { + "cannot_connect": "Impossibile connettersi ad un Pannello Konnected su {host}:{port}." + }, + "step": { + "confirm": { + "description": "Modello: {model}\nHost: {host}\nPorta: {port}\n\n\u00c8 possibile configurare il comportamento di I/O e del pannello nelle impostazioni del Pannello Allarmi di Konnected.", + "title": "Dispositivo Konnected pronto" + }, + "user": { + "data": { + "host": "Indirizzo IP del dispositivo Konnected", + "port": "Porta del dispositivo Konnected" + }, + "description": "Si prega di inserire le informazioni dell'host per il tuo Pannello Konnected.", + "title": "Rileva il dispositivo Konnected" + } + }, + "title": "Konnected.io" + }, + "options": { + "abort": { + "not_konn_panel": "Non \u00e8 un dispositivo Konnected.io riconosciuto" + }, + "error": { + "one": "uno", + "other": "altro" + }, + "step": { + "options_binary": { + "data": { + "inverse": "Invertire lo stato di apertura/chiusura", + "name": "Nome (opzionale)", + "type": "Tipo di sensore binario" + }, + "description": "Si prega di selezionare le opzioni per il sensore binario collegato alla {zone}", + "title": "Configurare il Sensore Binario" + }, + "options_digital": { + "data": { + "name": "Nome (opzionale)", + "poll_interval": "Intervallo di sondaggio (minuti) (opzionale)", + "type": "Tipo di sensore" + }, + "description": "Si prega di selezionare le opzioni per il sensore digitale collegato alla {zone}", + "title": "Configurare il Sensore Digitale" + }, + "options_io": { + "data": { + "1": "Zona 1", + "2": "Zona 2", + "3": "Zona 3", + "4": "Zona 4", + "5": "Zona 5", + "6": "Zona 6", + "7": "Zona 7", + "out": "OUT" + }, + "description": "Rilevato un {model} su {host}. Selezionare la configurazione di base di ciascun I/O di seguito: a seconda dell'I/O, essa pu\u00f2 consentire sensori binari (contatti aperti/chiusi), sensori digitali (DHT e DS18B20) o uscite commutabili. Sarai in grado di configurare le opzioni dettagliate nei prossimi passi.", + "title": "Configura I/O" + }, + "options_io_ext": { + "data": { + "10": "Zona 10", + "11": "Zona 11", + "12": "Zona 12", + "8": "Zona 8", + "9": "Zona 9", + "alarm1": "ALARM1", + "alarm2_out2": "OUT2 / ALARM2", + "out1": "OUT1" + }, + "description": "Selezionare di seguito la configurazione degli I/O rimanenti. Potrete configurare opzioni dettagliate nei prossimi passi.", + "title": "Configurazione I/O Esteso" + }, + "options_misc": { + "data": { + "blink": "Attiva il lampeggio del LED del pannello durante l'invio del cambiamento di stato " + }, + "description": "Seleziona il comportamento desiderato per il tuo pannello", + "title": "Configura Altro" + }, + "options_switch": { + "data": { + "activation": "Uscita quando acceso", + "momentary": "Durata impulso (ms) (opzionale)", + "name": "Nome (opzionale)", + "pause": "Pausa tra gli impulsi (ms) (opzionale)", + "repeat": "Numero di volte da ripetere (-1 = infinito) (opzionale)" + }, + "description": "Selezionare le opzioni di uscita per {zona}", + "title": "Configurare l'uscita commutabile" + } + }, + "title": "Opzioni del pannello di allarme Konnected" + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/ko.json b/homeassistant/components/konnected/.translations/ko.json new file mode 100644 index 00000000000..fe196050766 --- /dev/null +++ b/homeassistant/components/konnected/.translations/ko.json @@ -0,0 +1,104 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589\uc911\uc785\ub2c8\ub2e4.", + "not_konn_panel": "\uc778\uc2dd\ub41c Konnected.io \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4", + "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "{host}:{port} \uc758 Konnected \ud328\ub110\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "step": { + "confirm": { + "description": "\ubaa8\ub378: {model}\n\ud638\uc2a4\ud2b8: {host}\n\ud3ec\ud2b8: {port}\n\nKonnected \uc54c\ub78c \ud328\ub110 \uc124\uc815\uc5d0\uc11c IO \uc640 \ud328\ub110 \ub3d9\uc791\uc744 \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "title": "Konnected \uae30\uae30 \uc900\ube44" + }, + "import_confirm": { + "description": "Konnected \uc54c\ub78c \ud328\ub110 ID {id} \uac00 configuration.yaml \uc5d0\uc11c \ubc1c\uacac\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uc774 \uacfc\uc815\uc744 \ud1b5\ud574 \uad6c\uc131 \ud56d\ubaa9\uc73c\ub85c \uac00\uc838\uc62c \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "title": "Konnected \uae30\uae30 \uac00\uc838\uc624\uae30" + }, + "user": { + "data": { + "host": "Konnected \uae30\uae30 IP \uc8fc\uc18c", + "port": "Konnected \uae30\uae30 \ud3ec\ud2b8" + }, + "description": "Konnected \ud328\ub110\uc758 \ud638\uc2a4\ud2b8 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "Konnected \uae30\uae30 \ucc3e\uae30" + } + }, + "title": "Konnected.io" + }, + "options": { + "abort": { + "not_konn_panel": "\uc778\uc2dd\ub41c Konnected.io \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4" + }, + "step": { + "options_binary": { + "data": { + "inverse": "\uc5f4\ub9bc / \ub2eb\ud798 \uc0c1\ud0dc \ubc18\uc804", + "name": "\uc774\ub984 (\uc120\ud0dd \uc0ac\ud56d)", + "type": "\uc774\uc9c4 \uc13c\uc11c \uc720\ud615" + }, + "description": "{zone} \uc5d0 \uc5f0\uacb0\ub41c \uc774\uc9c4 \uc13c\uc11c\uc5d0 \ub300\ud55c \uc635\uc158\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694", + "title": "\uc774\uc9c4 \uc13c\uc11c \uad6c\uc131" + }, + "options_digital": { + "data": { + "name": "\uc774\ub984 (\uc120\ud0dd \uc0ac\ud56d)", + "poll_interval": "\ud3f4\ub9c1 \uac04\uaca9 (\ubd84) (\uc120\ud0dd \uc0ac\ud56d)", + "type": "\uc13c\uc11c \uc720\ud615" + }, + "description": "{zone} \uc5d0 \uc5f0\uacb0\ub41c \ub514\uc9c0\ud138 \uc13c\uc11c\uc5d0 \ub300\ud55c \uc635\uc158\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694", + "title": "\ub514\uc9c0\ud138 \uc13c\uc11c \uad6c\uc131" + }, + "options_io": { + "data": { + "1": "\uad6c\uc5ed 1", + "2": "\uad6c\uc5ed 2", + "3": "\uad6c\uc5ed 3", + "4": "\uad6c\uc5ed 4", + "5": "\uad6c\uc5ed 5", + "6": "\uad6c\uc5ed 6", + "7": "\uad6c\uc5ed 7", + "out": "\uc678\ubd80" + }, + "description": "{host} \uc5d0\uc11c {model} \uc744(\ub97c) \ubc1c\uacac\ud588\uc2b5\ub2c8\ub2e4. \uc774\uc9c4 \uc13c\uc11c(\uac1c\ud3d0 \uc811\uc810), \ub514\uc9c0\ud138 \uc13c\uc11c(dht \ubc0f ds18b20) \ub610\ub294 \uc2a4\uc704\uce58\uac00 \uac00\ub2a5\ud55c \uc13c\uc11c\uc758 I/O \uc5d0 \ub530\ub77c \uc544\ub798\uc5d0\uc11c \uac01 I/O \uc758 \uae30\ubcf8 \uad6c\uc131\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \ub2e4\uc74c \ub2e8\uacc4\uc5d0\uc11c \uc138\ubd80 \uc635\uc158\uc744 \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "title": "I/O \uad6c\uc131" + }, + "options_io_ext": { + "data": { + "10": "\uad6c\uc5ed 10", + "11": "\uad6c\uc5ed 11", + "12": "\uad6c\uc5ed 12", + "8": "\uad6c\uc5ed 8", + "9": "\uad6c\uc5ed 9", + "alarm1": "\uc54c\ub78c 1", + "alarm2_out2": "\ucd9c\ub825 2 / \uc54c\ub78c 2", + "out1": "\ucd9c\ub825 1" + }, + "description": "\uc544\ub798\uc758 \ub098\uba38\uc9c0 I/O \uad6c\uc131\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \ub2e4\uc74c \ub2e8\uacc4\uc5d0\uc11c \uc138\ubd80 \uc635\uc158\uc744 \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "title": "\ud655\uc7a5 I/O \uad6c\uc131" + }, + "options_misc": { + "data": { + "blink": "\uc0c1\ud0dc \ubcc0\uacbd\uc744 \ubcf4\ub0bc \ub54c \uae5c\ubc15\uc784 \ud328\ub110 LED \ub97c \ucf2d\ub2c8\ub2e4" + }, + "description": "\ud328\ub110\uc5d0 \uc6d0\ud558\ub294 \ub3d9\uc791\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694", + "title": "\uae30\ud0c0 \uad6c\uc131" + }, + "options_switch": { + "data": { + "activation": "\uc2a4\uc704\uce58\uac00 \ucf1c\uc9c8 \ub54c \ucd9c\ub825", + "momentary": "\ud384\uc2a4 \uc9c0\uc18d\uc2dc\uac04 (ms) (\uc120\ud0dd \uc0ac\ud56d)", + "name": "\uc774\ub984 (\uc120\ud0dd \uc0ac\ud56d)", + "pause": "\ud384\uc2a4 \uac04 \uc77c\uc2dc\uc815\uc9c0 \uc2dc\uac04 (ms) (\uc120\ud0dd \uc0ac\ud56d)", + "repeat": "\ubc18\ubcf5 \uc2dc\uac04 (-1 = \ubb34\ud55c) (\uc120\ud0dd \uc0ac\ud56d)" + }, + "description": "{zone} \ub300\ud55c \ucd9c\ub825 \uc635\uc158\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694", + "title": "\uc2a4\uc704\uce58 \ucd9c\ub825 \uad6c\uc131" + } + }, + "title": "Konnected \uc54c\ub78c \ud328\ub110 \uc635\uc158" + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/lb.json b/homeassistant/components/konnected/.translations/lb.json new file mode 100644 index 00000000000..2e37ecb8e92 --- /dev/null +++ b/homeassistant/components/konnected/.translations/lb.json @@ -0,0 +1,104 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert", + "already_in_progress": "Konfiguratioun's Oflaf fir den Apparat ass schonn am gaangen.", + "not_konn_panel": "Keen erkannten Konnected.io Apparat", + "unknown": "Onbekannten Fehler opgetrueden" + }, + "error": { + "cannot_connect": "Kann sech net mam Konnected Panel um {host}:{port} verbannen" + }, + "step": { + "confirm": { + "description": "Modell: {model}\nHost: {host}\nPort: {port}\n\nDir k\u00ebnnt den I/O a Panel Verhaalen an de Konnected Alarm Panel Astellunge konfigur\u00e9ieren.", + "title": "Konnected Apparat parat" + }, + "user": { + "data": { + "host": "Konnected Apparat IP Adress", + "port": "Konnected Apparat Port" + }, + "description": "Informatioune vum Konnected Panel aginn.", + "title": "Konnected Apparat entdecken" + } + }, + "title": "Konnected.io" + }, + "options": { + "abort": { + "not_konn_panel": "Keen erkannten Konnected.io Apparat" + }, + "error": { + "one": "Ee", + "other": "M\u00e9i" + }, + "step": { + "options_binary": { + "data": { + "inverse": "Op/Zou Zoustand vertauschen", + "name": "Numm (optional)", + "type": "Typ vun Bin\u00e4re Sensor" + }, + "description": "Wiel d'Optioune fir den bin\u00e4ren Sensor dee mat {zone} verbonnen ass", + "title": "Bin\u00e4re Sensor konfigur\u00e9ieren" + }, + "options_digital": { + "data": { + "name": "Numm (optional)", + "poll_interval": "Intervall vun den Offroen (Minutten) (optional)", + "type": "Typ vum Sensor" + }, + "description": "Wiel d'Optioune fir den digitale Sensor dee mat {zone} verbonnen ass", + "title": "Digitale Sensor konfigur\u00e9ieren" + }, + "options_io": { + "data": { + "1": "Zon 2", + "2": "Zon 1", + "3": "Zon 3", + "4": "Zon 4", + "5": "Zon 5", + "6": "Zon 6", + "7": "Zon 7", + "out": "OUT" + }, + "description": "{model} um {host} entdeckt.\u00a0Wiel Basis Konfiguratioun vun den I/O hei dr\u00ebnner aus - ofh\u00e4ngeg vum I/O erlaabt et bin\u00e4r Sensoren (op / zou Kontakter), digital Sensoren (dht an ds18b20) oder schaltbar Ausgab. D\u00e9i detaill\u00e9iert Optioune k\u00ebnnen en an den n\u00e4chste Schr\u00ebtt konfigur\u00e9iert ginn.", + "title": "I/O konfigur\u00e9ieren" + }, + "options_io_ext": { + "data": { + "10": "Zon 10", + "11": "Zon 11", + "12": "Zon 12", + "8": "Zon 8", + "9": "Zon 9", + "alarm1": "ALARM1", + "alarm2_out2": "OUT2/ALARM2", + "out1": "OUT1" + }, + "description": "Wiel d'Konfiguratioun vun de verbleiwenden I/O hei dr\u00ebnner. D\u00e9i detaill\u00e9iert Optioune k\u00ebnnen en an den n\u00e4chste Schr\u00ebtt konfigur\u00e9iert ginn.", + "title": "Erweiderten I/O konfigur\u00e9ieren" + }, + "options_misc": { + "data": { + "blink": "Blink panel LED un wann Status \u00c4nnerung gesch\u00e9ckt g\u00ebtt" + }, + "description": "Wielt w.e.g. dat gew\u00ebnschte Verhalen fir \u00c4re Panel aus", + "title": "Divers Optioune astellen" + }, + "options_switch": { + "data": { + "activation": "Ausgang wann un", + "momentary": "Pulsatiounsdauer (ms) (optional)", + "name": "Numm (optional)", + "pause": "Pausen zw\u00ebscht den Impulser (ms) (optional)", + "repeat": "Unzuel vu Widderhuelungen (-1= onendlech) (optional)" + }, + "description": "Wielt w.e.g. d'Ausgaboptiounen fir {zone}", + "title": "\u00cbmschltbaren Ausgang konfigur\u00e9ieren" + } + }, + "title": "Konnected Alarm Panneau Optiounen" + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/nl.json b/homeassistant/components/konnected/.translations/nl.json new file mode 100644 index 00000000000..1b6242b37f4 --- /dev/null +++ b/homeassistant/components/konnected/.translations/nl.json @@ -0,0 +1,74 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "unknown": "Onbekende fout opgetreden" + }, + "step": { + "confirm": { + "title": "Konnected Apparaat Klaar" + }, + "user": { + "data": { + "host": "IP-adres van Konnected apparaat", + "port": "Konnected apparaat poort" + }, + "description": "Voer de host-informatie in voor uw Konnected-paneel.", + "title": "Ontdek Konnected Device" + } + }, + "title": "Konnected.io" + }, + "options": { + "abort": { + "not_konn_panel": "Geen herkend Konnected.io apparaat" + }, + "error": { + "one": "Leeg", + "other": "Leeg" + }, + "step": { + "options_binary": { + "data": { + "inverse": "Keer de open / dicht status om", + "name": "Naam (optioneel)", + "type": "Type binaire sensor" + }, + "title": "Binaire sensor configureren" + }, + "options_digital": { + "data": { + "name": "Naam (optioneel)", + "type": "Type sensor" + } + }, + "options_io": { + "data": { + "1": "Zone 1", + "2": "Zone 2", + "3": "Zone 3", + "4": "Zone 4", + "5": "Zone 5", + "6": "Zone 6", + "7": "Zone 7" + } + }, + "options_io_ext": { + "data": { + "10": "Zone 10", + "11": "Zone 11", + "12": "Zone 12", + "8": "Zone 8", + "9": "Zone 9" + } + }, + "options_switch": { + "data": { + "name": "Naam (optioneel)" + }, + "title": "Schakelbare uitgang configureren" + } + }, + "title": "Konnected Alarm Paneel Opties" + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/no.json b/homeassistant/components/konnected/.translations/no.json new file mode 100644 index 00000000000..569dac5756f --- /dev/null +++ b/homeassistant/components/konnected/.translations/no.json @@ -0,0 +1,100 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyt for enhet p\u00e5g\u00e5r allerede.", + "not_konn_panel": "Ikke en anerkjent Konnected.io-enhet", + "unknown": "Ukjent feil oppstod" + }, + "error": { + "cannot_connect": "Kan ikke koble til et Konnected Panel p\u00e5 {host} : {port}" + }, + "step": { + "confirm": { + "description": "Modell: {model}\nVert: {host}\nPort: {port}\n\nDu kan konfigurere IO og panel atferd i Konnected Alarm Panel innstillinger.", + "title": "Konnected Enhet klar" + }, + "user": { + "data": { + "host": "Konnected enhet IP-adresse", + "port": "Koblet enhetsport" + }, + "description": "Vennligst skriv inn verten informasjon for din Konnected Panel.", + "title": "Oppdag Konnected Enheten" + } + }, + "title": "Konnected.io" + }, + "options": { + "abort": { + "not_konn_panel": "Ikke en anerkjent Konnected.io-enhet" + }, + "step": { + "options_binary": { + "data": { + "inverse": "Inverter \u00e5pen / lukk tilstand", + "name": "Navn (valgfritt)", + "type": "Bin\u00e6r sensortype" + }, + "description": "Vennligst velg alternativer for bin\u00e6re sensor koblet til {sone}", + "title": "Konfigurer bin\u00e6r sensor" + }, + "options_digital": { + "data": { + "name": "Navn (valgfritt)", + "poll_interval": "Avstemningsintervall (minutter) (valgfritt)", + "type": "Sensortype" + }, + "description": "Vennligst velg alternativene for den digitale sensor som er koblet til {sone}", + "title": "Konfigurere Digital Sensor" + }, + "options_io": { + "data": { + "1": "Sone 1", + "2": "Sone 2", + "3": "Sone 3", + "4": "Sone 4", + "5": "Sone 5", + "6": "Sone 6", + "7": "Sone 7", + "out": "OUT" + }, + "description": "Oppdaget en {model} hos {host} . Velg basiskonfigurasjon for hver I / O nedenfor - avhengig av I / O kan det gi rom for bin\u00e6re sensorer (\u00e5pne / lukke kontakter), digitale sensorer (dht og ds18b20), eller switchbare utganger. Du vil kunne konfigurere detaljerte alternativer i de neste trinnene.", + "title": "Konfigurere I/O" + }, + "options_io_ext": { + "data": { + "10": "Sone 10", + "11": "Sone 11", + "12": "Sone 12", + "8": "Sone 8", + "9": "Sone 9", + "alarm1": "ALARM1", + "alarm2_out2": "OUT2/ALARM2", + "out1": "OUT1" + }, + "description": "Velg konfigurasjonen av de gjenv\u00e6rende I/O nedenfor. Du vil v\u00e6re i stand til \u00e5 konfigurere detaljerte alternativer i de neste trinnene.", + "title": "Konfigurer utvidet I / O" + }, + "options_misc": { + "data": { + "blink": "Blink p\u00e5 LED-lampen n\u00e5r du sender statusendring" + }, + "description": "Vennligst velg \u00f8nsket atferd for din panel", + "title": "Konfigurere Diverse" + }, + "options_switch": { + "data": { + "activation": "Utgang n\u00e5r den er p\u00e5", + "momentary": "Pulsvarighet (ms) (valgfritt)", + "name": "Navn (valgfritt)", + "pause": "Pause mellom pulser (ms) (valgfritt)", + "repeat": "Tider \u00e5 gjenta (-1 = uendelig) (valgfritt)" + }, + "description": "Velg outputalternativer for {zone}", + "title": "Konfigurere Valgbare Utgang" + } + }, + "title": "Alternativer for Konnected Alarm Panel" + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/pl.json b/homeassistant/components/konnected/.translations/pl.json new file mode 100644 index 00000000000..b0d721891c0 --- /dev/null +++ b/homeassistant/components/konnected/.translations/pl.json @@ -0,0 +1,106 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "already_in_progress": "Konfiguracja urz\u0105dzenia jest ju\u017c w toku.", + "not_konn_panel": "Nie rozpoznano urz\u0105dzenia Konnected.io", + "unknown": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d." + }, + "error": { + "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z panelem Konnected na {host}:{port}" + }, + "step": { + "confirm": { + "description": "Model: {model} \nHost: {host} \nPort: {port} \n\nMo\u017cesz skonfigurowa\u0107 IO i zachowanie panelu w ustawieniach Konnected Alarm Panel.", + "title": "Urz\u0105dzenie Konnected gotowe" + }, + "user": { + "data": { + "host": "Adres IP urz\u0105dzenia Konnected", + "port": "Port urz\u0105dzenia Konnected urz\u0105dzenia" + }, + "description": "Wprowad\u017a informacje o ho\u015bcie panelu Konnected.", + "title": "Wykryj urz\u0105dzenie Konnected" + } + }, + "title": "Konnected.io" + }, + "options": { + "abort": { + "not_konn_panel": "Nie rozpoznano urz\u0105dzenia Konnected.io" + }, + "error": { + "few": "kilka", + "many": "wiele", + "one": "jeden", + "other": "inny" + }, + "step": { + "options_binary": { + "data": { + "inverse": "Odwr\u00f3\u0107 stan otwarty/zamkni\u0119ty", + "name": "Nazwa (opcjonalnie)", + "type": "Typ sensora binarnego" + }, + "description": "Wybierz opcje dla sensora binarnego powi\u0105zanego ze {zone}", + "title": "Konfiguracja sensora binarnego" + }, + "options_digital": { + "data": { + "name": "Nazwa (opcjonalnie)", + "poll_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji (minuty) (opcjonalnie)", + "type": "Typ sensora" + }, + "description": "Wybierz opcje dla cyfrowego sensora powi\u0105zanego ze {zone}", + "title": "Konfiguracja sensora cyfrowego" + }, + "options_io": { + "data": { + "1": "Strefa 1", + "2": "Strefa 2", + "3": "Strefa 3", + "4": "Strefa 4", + "5": "Strefa 5", + "6": "Strefa 6", + "7": "Strefa 7", + "out": "OUT" + }, + "description": "Wykryto {model} na ho\u015bcie {host}. Wybierz podstawow\u0105 konfiguracj\u0119 ka\u017cdego wej\u015bcia/wyj\u015bcia poni\u017cej \u2014 w zale\u017cno\u015bci od typu wej\u015b\u0107/wyj\u015b\u0107 mo\u017ce zastosowa\u0107 sensory binarne (otwarte/ amkni\u0119te), sensory cyfrowe (dht i ds18b20) lub prze\u0142\u0105czane wyj\u015bcia. B\u0119dziesz m\u00f3g\u0142 skonfigurowa\u0107 szczeg\u00f3\u0142owe opcje w kolejnych krokach.", + "title": "Konfiguracja wej\u015bcia/wyj\u015bcia" + }, + "options_io_ext": { + "data": { + "10": "Strefa 10", + "11": "Strefa 11", + "12": "Strefa 12", + "8": "Strefa 8", + "9": "Strefa 9", + "alarm1": "ALARM1", + "alarm2_out2": "OUT2/ALARM2", + "out1": "OUT1" + }, + "description": "Wybierz konfiguracj\u0119 pozosta\u0142ych wej\u015b\u0107/wyj\u015b\u0107 poni\u017cej. B\u0119dziesz m\u00f3g\u0142 skonfigurowa\u0107 szczeg\u00f3\u0142owe opcje w kolejnych krokach.", + "title": "Konfiguracja rozszerzonego wej\u015bcia/wyj\u015bcia" + }, + "options_misc": { + "data": { + "blink": "Miganie diody LED panelu podczas wysy\u0142ania zmiany stanu" + }, + "description": "Wybierz po\u017c\u0105dane zachowanie dla swojego panelu", + "title": "R\u00f3\u017cne opcje" + }, + "options_switch": { + "data": { + "activation": "Wyj\u015bcie, gdy w\u0142\u0105czone", + "momentary": "Czas trwania impulsu (ms) (opcjonalnie)", + "name": "Nazwa (opcjonalnie)", + "pause": "Przerwa mi\u0119dzy impulsami (ms) (opcjonalnie)", + "repeat": "Ilo\u015b\u0107 powt\u00f3rze\u0144 (-1=niesko\u0144czenie) (opcjonalnie)" + }, + "description": "Wybierz opcje wyj\u015bcia dla {zone}", + "title": "Konfiguracja prze\u0142\u0105czalne wyj\u015bcie" + } + }, + "title": "Opcje panelu alarmu Konnected" + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/ru.json b/homeassistant/components/konnected/.translations/ru.json new file mode 100644 index 00000000000..25cb03b1578 --- /dev/null +++ b/homeassistant/components/konnected/.translations/ru.json @@ -0,0 +1,100 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \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 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "not_konn_panel": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Konnected.io \u043d\u0435 \u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u043d\u043e.", + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043f\u0430\u043d\u0435\u043b\u0438 Konnected {host}:{port}." + }, + "step": { + "confirm": { + "description": "\u041c\u043e\u0434\u0435\u043b\u044c: {model}\n\u0425\u043e\u0441\u0442: {host}\n\u041f\u043e\u0440\u0442: {port}\n\n\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u043b\u043e\u0433\u0438\u043a\u0438 \u0440\u0430\u0431\u043e\u0442\u044b \u043f\u0430\u043d\u0435\u043b\u0438, \u0430 \u0442\u0430\u043a\u0436\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0432\u0445\u043e\u0434\u043e\u0432 \u0438 \u0432\u044b\u0445\u043e\u0434\u043e\u0432 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u043f\u0430\u043d\u0435\u043b\u0438 \u0441\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 Konnected.", + "title": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Konnected \u0433\u043e\u0442\u043e\u0432\u043e \u043a \u0440\u0430\u0431\u043e\u0442\u0435." + }, + "user": { + "data": { + "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a \u043f\u0430\u043d\u0435\u043b\u0438 Konnected.", + "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Konnected" + } + }, + "title": "Konnected.io" + }, + "options": { + "abort": { + "not_konn_panel": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Konnected.io \u043d\u0435 \u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u043d\u043e." + }, + "step": { + "options_binary": { + "data": { + "inverse": "\u0418\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043e\u0442\u043a\u0440\u044b\u0442\u043e\u0435/\u0437\u0430\u043a\u0440\u044b\u0442\u043e\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", + "type": "\u0422\u0438\u043f \u0431\u0438\u043d\u0430\u0440\u043d\u043e\u0433\u043e \u0441\u0435\u043d\u0441\u043e\u0440\u0430" + }, + "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0431\u0438\u043d\u0430\u0440\u043d\u043e\u0433\u043e \u0441\u0435\u043d\u0441\u043e\u0440\u0430, \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u0433\u043e \u043a {zone}.", + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0431\u0438\u043d\u0430\u0440\u043d\u043e\u0433\u043e \u0441\u0435\u043d\u0441\u043e\u0440\u0430" + }, + "options_digital": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", + "poll_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0440\u043e\u0441\u0430 \u0432 \u043c\u0438\u043d\u0443\u0442\u0430\u0445 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", + "type": "\u0422\u0438\u043f \u0441\u0435\u043d\u0441\u043e\u0440\u0430" + }, + "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0441\u0435\u043d\u0441\u043e\u0440\u0430, \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u0433\u043e \u043a {zone}.", + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0441\u0435\u043d\u0441\u043e\u0440\u0430" + }, + "options_io": { + "data": { + "1": "\u0417\u043e\u043d\u0430 1", + "2": "\u0417\u043e\u043d\u0430 2", + "3": "\u0417\u043e\u043d\u0430 3", + "4": "\u0417\u043e\u043d\u0430 4", + "5": "\u0417\u043e\u043d\u0430 5", + "6": "\u0417\u043e\u043d\u0430 6", + "7": "\u0417\u043e\u043d\u0430 7", + "out": "\u0412\u042b\u0425\u041e\u0414" + }, + "description": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e {model} \u0441 \u0430\u0434\u0440\u0435\u0441\u043e\u043c {host}. \u0412 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0442 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0432\u0445\u043e\u0434\u043e\u0432/\u0432\u044b\u0445\u043e\u0434\u043e\u0432, \u043a \u043f\u0430\u043d\u0435\u043b\u0438 \u043c\u043e\u0433\u0443\u0442 \u0431\u044b\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u044b \u0431\u0438\u043d\u0430\u0440\u043d\u044b\u0435 \u0441\u0435\u043d\u0441\u043e\u0440\u044b (\u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u043e\u0442\u043a\u0440\u044b\u0442\u0438\u044f/\u0437\u0430\u043a\u0440\u044b\u0442\u0438\u044f), \u0446\u0438\u0444\u0440\u043e\u0432\u044b\u0435 \u0441\u0435\u043d\u0441\u043e\u0440\u044b (dht \u0438 ds18b20) \u0438\u043b\u0438 \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0430\u0435\u043c\u044b\u0435 \u0432\u044b\u0445\u043e\u0434\u044b. \u0411\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u043d\u0430 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0445 \u0448\u0430\u0433\u0430\u0445.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0432\u0445\u043e\u0434\u043e\u0432/\u0432\u044b\u0445\u043e\u0434\u043e\u0432" + }, + "options_io_ext": { + "data": { + "10": "\u0417\u043e\u043d\u0430 10", + "11": "\u0417\u043e\u043d\u0430 11", + "12": "\u0417\u043e\u043d\u0430 12", + "8": "\u0417\u043e\u043d\u0430 8", + "9": "\u0417\u043e\u043d\u0430 9", + "alarm1": "\u0422\u0420\u0415\u0412\u041e\u0413\u04101", + "alarm2_out2": "\u0412\u042b\u0425\u041e\u04142/\u0422\u0420\u0415\u0412\u041e\u0413\u04102", + "out1": "\u0412\u042b\u0425\u041e\u04141" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u043e\u0441\u0442\u0430\u0432\u0448\u0438\u0445\u0441\u044f \u0432\u0445\u043e\u0434\u043e\u0432/\u0432\u044b\u0445\u043e\u0434\u043e\u0432. \u0411\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u043d\u0430 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0445 \u0448\u0430\u0433\u0430\u0445.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u0432\u0445\u043e\u0434\u043e\u0432/\u0432\u044b\u0445\u043e\u0434\u043e\u0432" + }, + "options_misc": { + "data": { + "blink": "LED-\u0438\u043d\u0434\u0438\u043a\u0430\u0446\u0438\u044f \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 \u043f\u0440\u0438 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0436\u0435\u043b\u0430\u0435\u043c\u043e\u0435 \u043f\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u0435 \u0434\u043b\u044f \u0412\u0430\u0448\u0435\u0439 \u043f\u0430\u043d\u0435\u043b\u0438.", + "title": "\u041f\u0440\u043e\u0447\u0438\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + }, + "options_switch": { + "data": { + "activation": "\u0412\u044b\u0445\u043e\u0434 \u043f\u0440\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", + "momentary": "\u0414\u043b\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u0438\u043c\u043f\u0443\u043b\u044c\u0441\u0430 (\u043c\u0441) (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", + "pause": "\u041f\u0430\u0443\u0437\u0430 \u043c\u0435\u0436\u0434\u0443 \u0438\u043c\u043f\u0443\u043b\u044c\u0441\u0430\u043c\u0438 (\u043c\u0441) (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", + "repeat": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u0432\u0442\u043e\u0440\u0435\u043d\u0438\u0439 (-1 = \u0431\u0435\u0441\u043a\u043e\u043d\u0435\u0447\u043d\u043e) (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0432\u044b\u0445\u043e\u0434\u0430 \u0434\u043b\u044f {zone}.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0430\u0435\u043c\u043e\u0433\u043e \u0432\u044b\u0445\u043e\u0434\u0430" + } + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u0430\u043d\u0435\u043b\u0438 \u0441\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 Konnected" + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/sl.json b/homeassistant/components/konnected/.translations/sl.json new file mode 100644 index 00000000000..38396d0832d --- /dev/null +++ b/homeassistant/components/konnected/.translations/sl.json @@ -0,0 +1,106 @@ +{ + "config": { + "abort": { + "already_configured": "Naprava je \u017ee konfigurirana", + "already_in_progress": "Konfiguracijski tok za napravo je \u017ee v teku.", + "not_konn_panel": "Ni prepoznana kot Konnected.io naprava", + "unknown": "Pri\u0161lo je do neznane napake" + }, + "error": { + "cannot_connect": "Ni mogo\u010de vzpostaviti povezave s Konnected plo\u0161\u010do v {Host}: {Port}" + }, + "step": { + "confirm": { + "description": "Model: {model}\nGostitelj: {host}\nVrata: {port}\n\nV nastavitvah lahko nastavite vedenje I / O in plo\u0161\u010de Konnected alarma. ", + "title": "Konnected naprava pripravljena" + }, + "user": { + "data": { + "host": "IP-naslov Konnected naprave", + "port": "Vrata Konnected naprave" + }, + "description": "Vnesite podatke o gostitelju v svoj Konnected Panel.", + "title": "Odkrijte Konnected napravo" + } + }, + "title": "Konnected.io" + }, + "options": { + "abort": { + "not_konn_panel": "Ni prepoznana kot Konnected.io naprava" + }, + "error": { + "few": "nekaj", + "one": "ena", + "other": "drugo", + "two": "dva" + }, + "step": { + "options_binary": { + "data": { + "inverse": "Zamenjajte odprto / zaprto stanje", + "name": "Ime (neobvezno)", + "type": "Vrsta binarnega senzorja" + }, + "description": "Izberite mo\u017enosti za binarni senzor, priklju\u010den na {zone}", + "title": "Konfigurirajte binarni senzor" + }, + "options_digital": { + "data": { + "name": "Ime (neobvezno)", + "poll_interval": "Interval osve\u017eevanja (minute) (neobvezno)", + "type": "Vrsta tipala" + }, + "description": "Izberite mo\u017enosti za digitalni senzor, priklju\u010den na {zone}", + "title": "Konfigurirajte digitalni senzor" + }, + "options_io": { + "data": { + "1": "Zone 1", + "2": "Zone 2", + "3": "Zone 3", + "4": "Zone 4", + "5": "Zone 5", + "6": "Zone 6", + "7": "Zone 7", + "out": "OUT" + }, + "description": "Odkrili {model} na {host} . Spodaj izberite osnovno konfiguracijo vsakega I / O - odvisno od I / O lahko omogo\u010da binarne senzorje (odpiranje / zapiranje kontaktov), digitalne senzorje (dht in ds18b20) ali preklopne izhode. Podrobne mo\u017enosti boste lahko konfigurirali v naslednjih korakih.", + "title": "Konfigurirajte I / O" + }, + "options_io_ext": { + "data": { + "10": "Obmo\u010dje 10", + "11": "Obmo\u010dje 11", + "12": "Obmo\u010dje 12", + "8": "Obmo\u010dje 8", + "9": "Obmo\u010dje 9", + "alarm1": "ALARM1", + "alarm2_out2": "OUT2 / ALARM2", + "out1": "OUT1" + }, + "description": "Izberite konfiguracijo preostalega I/O spodaj. Podrobne mo\u017enosti boste lahko konfigurirali v naslednjih korakih.", + "title": "Konfigurirajte raz\u0161irjeni I/O" + }, + "options_misc": { + "data": { + "blink": "Lu\u010dka LED na zaslonu utripa, ko po\u0161iljate spremembo stanja" + }, + "description": "Izberite \u017eeleno vedenje za va\u0161o plo\u0161\u010do", + "title": "Konfigurirajte Razno" + }, + "options_switch": { + "data": { + "activation": "Izhod, ko je vklopljen", + "momentary": "Trajanje impulza (ms) (neobvezno)", + "name": "Ime (neobvezno)", + "pause": "Premor med impulzi (ms) (neobvezno)", + "repeat": "\u010casi ponovitve (-1 = neskon\u010dno) (neobvezno)" + }, + "description": "Izberite izhodne mo\u017enosti za {zone}", + "title": "Konfigurirajte preklopni izhod" + } + }, + "title": "Mo\u017enosti Konnected alarm-a" + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/sv.json b/homeassistant/components/konnected/.translations/sv.json new file mode 100644 index 00000000000..7e035264215 --- /dev/null +++ b/homeassistant/components/konnected/.translations/sv.json @@ -0,0 +1,104 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6de f\u00f6r enhet p\u00e5g\u00e5r redan.", + "not_konn_panel": "Inte en erk\u00e4nd Konnected.io-enhet", + "unknown": "Ett ok\u00e4nt fel har intr\u00e4ffat" + }, + "error": { + "cannot_connect": "Det g\u00e5r inte att ansluta till en ansluten panel p\u00e5 {host}:{port}" + }, + "step": { + "confirm": { + "description": "Modell: {modell}\nV\u00e4rd: {host}\nPort: {port}\n\nDu kan konfigurera IO- och panelbeteendet i inst\u00e4llningarna f\u00f6r Konnected Alarm Panel.", + "title": "Konnected-enheten redo" + }, + "user": { + "data": { + "host": "Konnected-enhetens IP-adress", + "port": "Konnected-enhetens port" + }, + "description": "Ange v\u00e4rdinformationen f\u00f6r din Konnected Panel.", + "title": "Uppt\u00e4ck Konnected-enhet" + } + }, + "title": "Konnected.io" + }, + "options": { + "abort": { + "not_konn_panel": "Inte en erk\u00e4nd Konnected.io-enhet" + }, + "error": { + "one": "Tom", + "other": "Tomma" + }, + "step": { + "options_binary": { + "data": { + "inverse": "Invertera \u00f6ppet/st\u00e4ngt tillst\u00e5nd", + "name": "Namn (valfritt)", + "type": "Bin\u00e4r sensortyp" + }, + "description": "V\u00e4lj alternativ f\u00f6r den bin\u00e4ra sensorn som \u00e4r ansluten till {zone}", + "title": "Konfigurera Bin\u00e4r Sensor" + }, + "options_digital": { + "data": { + "name": "Namn (valfritt)", + "poll_interval": "H\u00e4mtningsintervall (minuter) (valfritt)", + "type": "Sensortyp" + }, + "description": "V\u00e4lj alternativ f\u00f6r den digitala sensorn som \u00e4r ansluten till {zone}", + "title": "Konfigurera Digital Sensor" + }, + "options_io": { + "data": { + "1": "Zon 1", + "2": "Zon 2", + "3": "Zon 3", + "4": "Zon 4", + "5": "Zon 5", + "6": "Zon 6", + "7": "Zon 7", + "out": "UT" + }, + "description": "Uppt\u00e4ckte en {model} p\u00e5 {host}. V\u00e4lj baskonfigurationen f\u00f6r varje I/O nedan - beroende p\u00e5 I/O kan det m\u00f6jligg\u00f6ra bin\u00e4ra sensorer (\u00f6ppen/st\u00e4ngd kontakter), digitala sensorer (dht och ds18b20) eller omkopplingsbara utg\u00e5ngar. Du kan konfigurera detaljerade alternativ i n\u00e4sta steg.", + "title": "Konfigurera I/O" + }, + "options_io_ext": { + "data": { + "10": "Zon 10", + "11": "Zon 11", + "12": "Zon 12", + "8": "Zon 8", + "9": "Zon 9", + "alarm1": "ALARM1", + "alarm2_out2": "OUT2/ALARM2", + "out1": "OUT1" + }, + "description": "V\u00e4lj den konfiguration av resterande I/O nedan. Du kommer att kunna konfigurera detaljerade alternativ i n\u00e4sta steg.", + "title": "Konfigurera ut\u00f6kat I/O" + }, + "options_misc": { + "data": { + "blink": "Blinka p\u00e5 panel-LED n\u00e4r du skickar tillst\u00e5nds\u00e4ndring" + }, + "description": "V\u00e4lj \u00f6nskat beteende f\u00f6r din panel", + "title": "Konfigurera \u00d6vrigt" + }, + "options_switch": { + "data": { + "activation": "Utdata n\u00e4r den \u00e4r p\u00e5", + "momentary": "Pulsvarighet (ms) (valfritt)", + "name": "Namn (valfritt)", + "pause": "Paus mellan pulser (ms) (valfritt)", + "repeat": "G\u00e5nger att upprepa (-1=o\u00e4ndligt) (tillval)" + }, + "description": "V\u00e4lj utdataalternativ f\u00f6r {zone}", + "title": "Konfigurera v\u00e4xelbar utdata" + } + }, + "title": "Alternativ f\u00f6r Konnected alarmpanel" + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/zh-Hans.json b/homeassistant/components/konnected/.translations/zh-Hans.json new file mode 100644 index 00000000000..2bba1260764 --- /dev/null +++ b/homeassistant/components/konnected/.translations/zh-Hans.json @@ -0,0 +1,9 @@ +{ + "options": { + "step": { + "options_switch": { + "description": "\u8bf7\u9009\u62e9 {zone}\u8f93\u51fa\u9009\u9879" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/zh-Hant.json b/homeassistant/components/konnected/.translations/zh-Hant.json new file mode 100644 index 00000000000..0ecd6c9fc25 --- /dev/null +++ b/homeassistant/components/konnected/.translations/zh-Hant.json @@ -0,0 +1,100 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5099\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", + "not_konn_panel": "\u4e26\u975e\u53ef\u8b58\u5225 Konnected.io \u8a2d\u5099", + "unknown": "\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002" + }, + "error": { + "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Konnected \u9762\u677f\uff1a{host}:{port}" + }, + "step": { + "confirm": { + "description": "\u578b\u865f\uff1a{model}\n\u4e3b\u6a5f\u7aef\uff1a{host}\n\u901a\u8a0a\u57e0\uff1a{port}\n\n\u53ef\u4ee5\u65bc Konncected \u8b66\u5831\u9762\u677f\u8a2d\u5b9a\u4e2d\u8a2d\u5b9a IO \u8207\u9762\u677f\u884c\u70ba\u3002", + "title": "Konnected \u8a2d\u5099\u5df2\u5099\u59a5" + }, + "user": { + "data": { + "host": "Konnected \u8a2d\u5099 IP \u4f4d\u5740", + "port": "Konnected \u8a2d\u5099\u901a\u8a0a\u57e0" + }, + "description": "\u8acb\u8f38\u5165 Konnected \u9762\u677f\u4e3b\u6a5f\u7aef\u8cc7\u8a0a\u3002", + "title": "\u641c\u7d22 Konnected \u8a2d\u5099" + } + }, + "title": "Konnected.io" + }, + "options": { + "abort": { + "not_konn_panel": "\u4e26\u975e\u53ef\u8b58\u5225 Konnected.io \u8a2d\u5099" + }, + "step": { + "options_binary": { + "data": { + "inverse": "\u53cd\u8f49\u958b\u555f/\u95dc\u9589\u72c0\u614b", + "name": "\u540d\u7a31\uff08\u9078\u9805\uff09", + "type": "\u4e8c\u9032\u4f4d\u611f\u61c9\u5668\u985e\u578b" + }, + "description": "\u8acb\u9078\u64c7\u6b78\u7d0d\u70ba {zone}\u7684\u4e8c\u9032\u4f4d\u611f\u61c9\u5668\u8f38\u51fa\u9078\u9805", + "title": "\u8a2d\u5b9a\u4e8c\u9032\u4f4d\u611f\u61c9\u5668" + }, + "options_digital": { + "data": { + "name": "\u540d\u7a31\uff08\u9078\u9805\uff09", + "poll_interval": "\u66f4\u65b0\u9593\u8ddd\uff08\u5206\u9418\uff09\uff08\u9078\u9805\uff09", + "type": "\u611f\u61c9\u5668\u985e\u578b" + }, + "description": "\u8acb\u9078\u64c7\u6b78\u7d0d\u70ba {zone}\u7684\u6578\u4f4d\u611f\u61c9\u5668\u8f38\u51fa\u9078\u9805", + "title": "\u8a2d\u5b9a\u6578\u4f4d\u611f\u61c9\u5668" + }, + "options_io": { + "data": { + "1": "\u5340\u57df 1", + "2": "\u5340\u57df 2", + "3": "\u5340\u57df 3", + "4": "\u5340\u57df 4", + "5": "\u5340\u57df 5", + "6": "\u5340\u57df 6", + "7": "\u5340\u57df 7", + "out": "OUT" + }, + "description": "\u65bc {host} \u767c\u73fe {model}\u3002\u8acb\u65bc\u4e0b\u65b9\u6bcf\u4e00\u500b I/O \u9078\u64c7\u57fa\u672c\u8a2d\u5b9a - \u96a8\u8457 I/O \u4e0d\u540c\uff0c\u53ef\u5141\u8a31\u4e8c\u9032\u4f4d\u611f\u61c9\u5668\uff08\u958b\u555f/\u95dc\u9589\u72c0\u614b\uff09\u3001\u6578\u4f4d\u611f\u61c9\u5668\uff08DHT \u53ca ds18b20\uff09\uff0c\u6216\u8005\u53ef\u5207\u63db\u8f38\u51fa\u3002\u53ef\u4ee5\u65bc\u4e0b\u4e00\u6b65\u8a2d\u5b9a\u8a73\u7d30\u9078\u9805\u3002", + "title": "\u8a2d\u5b9a I/O" + }, + "options_io_ext": { + "data": { + "10": "\u5340\u57df 10", + "11": "\u5340\u57df 11", + "12": "\u5340\u57df 12", + "8": "\u5340\u57df 8", + "9": "\u5340\u57df 9", + "alarm1": "ALARM1", + "alarm2_out2": "OUT2/ALARM2", + "out1": "OUT1" + }, + "description": "\u9078\u64c7\u4e0b\u65b9\u5269\u9918 I/O \u8a2d\u5b9a\u3002\u53ef\u4ee5\u65bc\u4e0b\u4e00\u6b65\u8a2d\u5b9a\u8a73\u7d30\u9078\u9805\u3002", + "title": "\u8a2d\u5b9a\u5ef6\u4f38 I/O" + }, + "options_misc": { + "data": { + "blink": "\u7576\u50b3\u9001\u72c0\u614b\u8b8a\u66f4\u6642\u3001\u9583\u720d\u9762\u677f LED" + }, + "description": "\u8acb\u9078\u64c7\u9762\u677f\u671f\u671b\u884c\u70ba", + "title": "\u5176\u4ed6\u8a2d\u5b9a" + }, + "options_switch": { + "data": { + "activation": "\u958b\u555f\u6642\u8f38\u51fa", + "momentary": "\u6301\u7e8c\u6642\u9593\uff08ms\uff09\uff08\u9078\u9805\uff09", + "name": "\u540d\u7a31\uff08\u9078\u9805\uff09", + "pause": "\u66ab\u505c\u9593\u8ddd\uff08ms\uff09\uff08\u9078\u9805\uff09", + "repeat": "\u91cd\u8907\u6642\u9593\uff08-1=\u7121\u9650\uff09\uff08\u9078\u9805\uff09" + }, + "description": "\u8acb\u9078\u64c7 {zone}\u8f38\u51fa\u9078\u9805", + "title": "\u8a2d\u5b9a Switchable \u8f38\u51fa" + } + }, + "title": "Konnected \u8b66\u5831\u9762\u677f\u9078\u9805" + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py index 28e62c322ad..94508b01483 100644 --- a/homeassistant/components/konnected/__init__.py +++ b/homeassistant/components/konnected/__init__.py @@ -1,20 +1,20 @@ """Support for Konnected devices.""" import asyncio +import copy import hmac import json import logging from aiohttp.hdrs import AUTHORIZATION from aiohttp.web import Request, Response -import konnected import voluptuous as vol +from homeassistant import config_entries from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA -from homeassistant.components.discovery import SERVICE_KONNECTED from homeassistant.components.http import HomeAssistantView +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, - ATTR_STATE, CONF_ACCESS_TOKEN, CONF_BINARY_SENSORS, CONF_DEVICES, @@ -27,45 +27,106 @@ from homeassistant.const import ( CONF_SWITCHES, CONF_TYPE, CONF_ZONE, - EVENT_HOMEASSISTANT_START, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_UNAUTHORIZED, + STATE_OFF, STATE_ON, ) -from homeassistant.helpers import config_validation as cv, discovery -from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv +from .config_flow import ( # Loading the config flow file will register the flow + CONF_DEFAULT_OPTIONS, + CONF_IO, + CONF_IO_BIN, + CONF_IO_DIG, + CONF_IO_SWI, + OPTIONS_SCHEMA, +) 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, DOMAIN, - ENDPOINT_ROOT, PIN_TO_ZONE, - SIGNAL_SENSOR_UPDATE, STATE_HIGH, STATE_LOW, UPDATE_ENDPOINT, ZONE_TO_PIN, + ZONES, ) +from .errors import CannotConnect from .handlers import HANDLERS +from .panel import AlarmPanel _LOGGER = logging.getLogger(__name__) -_BINARY_SENSOR_SCHEMA = vol.All( + +def ensure_pin(value): + """Check if valid pin and coerce to string.""" + if value is None: + raise vol.Invalid("pin value is None") + + if PIN_TO_ZONE.get(str(value)) is None: + raise vol.Invalid("pin not valid") + + return str(value) + + +def ensure_zone(value): + """Check if valid zone and coerce to string.""" + if value is None: + raise vol.Invalid("zone value is None") + + if str(value) not in ZONES is None: + raise vol.Invalid("zone not valid") + + return str(value) + + +def import_validator(config): + """Validate zones and reformat for import.""" + config = copy.deepcopy(config) + io_cfgs = {} + # Replace pins with zones + for conf_platform, conf_io in ( + (CONF_BINARY_SENSORS, CONF_IO_BIN), + (CONF_SENSORS, CONF_IO_DIG), + (CONF_SWITCHES, CONF_IO_SWI), + ): + for zone in config.get(conf_platform, []): + if zone.get(CONF_PIN): + zone[CONF_ZONE] = PIN_TO_ZONE[zone[CONF_PIN]] + del zone[CONF_PIN] + io_cfgs[zone[CONF_ZONE]] = conf_io + + # Migrate config_entry data into default_options structure + config[CONF_IO] = io_cfgs + config[CONF_DEFAULT_OPTIONS] = OPTIONS_SCHEMA(config) + + # clean up fields migrated to options + config.pop(CONF_BINARY_SENSORS, None) + config.pop(CONF_SENSORS, None) + config.pop(CONF_SWITCHES, None) + config.pop(CONF_BLINK, None) + config.pop(CONF_DISCOVERY, None) + config.pop(CONF_IO, None) + return config + + +# configuration.yaml schemas (legacy) +BINARY_SENSOR_SCHEMA_YAML = vol.All( vol.Schema( { - vol.Exclusive(CONF_PIN, "s_pin"): vol.Any(*PIN_TO_ZONE), - vol.Exclusive(CONF_ZONE, "s_pin"): vol.Any(*ZONE_TO_PIN), + vol.Exclusive(CONF_ZONE, "s_io"): ensure_zone, + vol.Exclusive(CONF_PIN, "s_io"): ensure_pin, vol.Required(CONF_TYPE): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_INVERSE, default=False): cv.boolean, @@ -74,14 +135,14 @@ _BINARY_SENSOR_SCHEMA = vol.All( cv.has_at_least_one_key(CONF_PIN, CONF_ZONE), ) -_SENSOR_SCHEMA = vol.All( +SENSOR_SCHEMA_YAML = vol.All( vol.Schema( { - vol.Exclusive(CONF_PIN, "s_pin"): vol.Any(*PIN_TO_ZONE), - vol.Exclusive(CONF_ZONE, "s_pin"): vol.Any(*ZONE_TO_PIN), + vol.Exclusive(CONF_ZONE, "s_io"): ensure_zone, + vol.Exclusive(CONF_PIN, "s_io"): ensure_pin, vol.Required(CONF_TYPE): vol.All(vol.Lower, vol.In(["dht", "ds18b20"])), vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_POLL_INTERVAL): vol.All( + vol.Optional(CONF_POLL_INTERVAL, default=3): vol.All( vol.Coerce(int), vol.Range(min=1) ), } @@ -89,11 +150,11 @@ _SENSOR_SCHEMA = vol.All( cv.has_at_least_one_key(CONF_PIN, CONF_ZONE), ) -_SWITCH_SCHEMA = vol.All( +SWITCH_SCHEMA_YAML = vol.All( vol.Schema( { - vol.Exclusive(CONF_PIN, "a_pin"): vol.Any(*PIN_TO_ZONE), - vol.Exclusive(CONF_ZONE, "a_pin"): vol.Any(*ZONE_TO_PIN), + vol.Exclusive(CONF_ZONE, "s_io"): ensure_zone, + vol.Exclusive(CONF_PIN, "s_io"): ensure_pin, vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_ACTIVATION, default=STATE_HIGH): vol.All( vol.Lower, vol.Any(STATE_HIGH, STATE_LOW) @@ -106,6 +167,24 @@ _SWITCH_SCHEMA = vol.All( cv.has_at_least_one_key(CONF_PIN, CONF_ZONE), ) +DEVICE_SCHEMA_YAML = vol.All( + vol.Schema( + { + vol.Required(CONF_ID): cv.matches_regex("[0-9a-f]{12}"), + vol.Optional(CONF_BINARY_SENSORS): vol.All( + cv.ensure_list, [BINARY_SENSOR_SCHEMA_YAML] + ), + vol.Optional(CONF_SENSORS): vol.All(cv.ensure_list, [SENSOR_SCHEMA_YAML]), + vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [SWITCH_SCHEMA_YAML]), + vol.Inclusive(CONF_HOST, "host_info"): cv.string, + vol.Inclusive(CONF_PORT, "host_info"): cv.port, + vol.Optional(CONF_BLINK, default=True): cv.boolean, + vol.Optional(CONF_DISCOVERY, default=True): cv.boolean, + } + ), + import_validator, +) + # pylint: disable=no-value-for-parameter CONFIG_SCHEMA = vol.Schema( { @@ -113,352 +192,88 @@ CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_ACCESS_TOKEN): cv.string, vol.Optional(CONF_API_HOST): vol.Url(), - vol.Required(CONF_DEVICES): [ - { - vol.Required(CONF_ID): cv.matches_regex("[0-9a-f]{12}"), - vol.Optional(CONF_BINARY_SENSORS): vol.All( - cv.ensure_list, [_BINARY_SENSOR_SCHEMA] - ), - vol.Optional(CONF_SENSORS): vol.All( - cv.ensure_list, [_SENSOR_SCHEMA] - ), - vol.Optional(CONF_SWITCHES): vol.All( - cv.ensure_list, [_SWITCH_SCHEMA] - ), - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_PORT): cv.port, - vol.Optional(CONF_BLINK, default=True): cv.boolean, - vol.Optional(CONF_DISCOVERY, default=True): cv.boolean, - } - ], + vol.Optional(CONF_DEVICES): vol.All( + cv.ensure_list, [DEVICE_SCHEMA_YAML] + ), } ) }, extra=vol.ALLOW_EXTRA, ) +YAML_CONFIGS = "yaml_configs" +PLATFORMS = ["binary_sensor", "sensor", "switch"] -async def async_setup(hass, config): + +async def async_setup(hass: HomeAssistant, config: dict): """Set up the Konnected platform.""" cfg = config.get(DOMAIN) if cfg is None: cfg = {} - access_token = cfg.get(CONF_ACCESS_TOKEN) if DOMAIN not in hass.data: hass.data[DOMAIN] = { - CONF_ACCESS_TOKEN: access_token, + CONF_ACCESS_TOKEN: cfg.get(CONF_ACCESS_TOKEN), CONF_API_HOST: cfg.get(CONF_API_HOST), + CONF_DEVICES: {}, } - def setup_device(host, port): - """Set up a Konnected device at `host` listening on `port`.""" - discovered = DiscoveredDevice(hass, host, port) - if discovered.is_configured: - discovered.setup() - else: - _LOGGER.warning( - "Konnected device %s was discovered on the network" - " but not specified in configuration.yaml", - discovered.device_id, + hass.http.register_view(KonnectedView) + + # Check if they have yaml configured devices + if CONF_DEVICES not in cfg: + return True + + for device in cfg.get(CONF_DEVICES, []): + # Attempt to importing the cfg. Use + # hass.async_add_job to avoid a deadlock. + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=device, ) - - def device_discovered(service, info): - """Call when a Konnected device has been discovered.""" - host = info.get(CONF_HOST) - port = info.get(CONF_PORT) - setup_device(host, port) - - async def manual_discovery(event): - """Init devices on the network with manually assigned addresses.""" - specified = [ - dev - for dev in cfg.get(CONF_DEVICES) - if dev.get(CONF_HOST) and dev.get(CONF_PORT) - ] - - while specified: - for dev in specified: - _LOGGER.debug( - "Discovering Konnected device %s at %s:%s", - dev.get(CONF_ID), - dev.get(CONF_HOST), - dev.get(CONF_PORT), - ) - try: - await hass.async_add_executor_job( - setup_device, dev.get(CONF_HOST), dev.get(CONF_PORT) - ) - specified.remove(dev) - except konnected.Client.ClientError as err: - _LOGGER.error(err) - await asyncio.sleep(10) # try again in 10 seconds - - # Initialize devices specified in the configuration on boot - for device in cfg.get(CONF_DEVICES): - ConfiguredDevice(hass, device, config).save_data() - - discovery.async_listen(hass, SERVICE_KONNECTED, device_discovered) - - hass.http.register_view(KonnectedView(access_token)) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, manual_discovery) - + ) return True -class ConfiguredDevice: - """A representation of a configured Konnected device.""" +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up panel from a config entry.""" + client = AlarmPanel(hass, entry) + # create a data store in hass.data[DOMAIN][CONF_DEVICES] + await client.async_save_data() - def __init__(self, hass, config, hass_config): - """Initialize the Konnected device.""" - self.hass = hass - self.config = config - self.hass_config = hass_config + try: + await client.async_connect() + except CannotConnect: + # this will trigger a retry in the future + raise config_entries.ConfigEntryNotReady - @property - def device_id(self): - """Device id is the MAC address as string with punctuation removed.""" - return self.config.get(CONF_ID) - - def save_data(self): - """Save the device configuration to `hass.data`.""" - binary_sensors = {} - for entity in self.config.get(CONF_BINARY_SENSORS) or []: - if CONF_ZONE in entity: - pin = ZONE_TO_PIN[entity[CONF_ZONE]] - else: - pin = entity[CONF_PIN] - - binary_sensors[pin] = { - CONF_TYPE: entity[CONF_TYPE], - CONF_NAME: entity.get( - CONF_NAME, - "Konnected {} Zone {}".format(self.device_id[6:], PIN_TO_ZONE[pin]), - ), - CONF_INVERSE: entity.get(CONF_INVERSE), - ATTR_STATE: None, - } - _LOGGER.debug( - "Set up binary_sensor %s (initial state: %s)", - binary_sensors[pin].get("name"), - binary_sensors[pin].get(ATTR_STATE), - ) - - actuators = [] - for entity in self.config.get(CONF_SWITCHES) or []: - if CONF_ZONE in entity: - pin = ZONE_TO_PIN[entity[CONF_ZONE]] - else: - pin = entity[CONF_PIN] - - act = { - CONF_PIN: pin, - CONF_NAME: entity.get( - CONF_NAME, - "Konnected {} Actuator {}".format( - self.device_id[6:], PIN_TO_ZONE[pin] - ), - ), - ATTR_STATE: None, - CONF_ACTIVATION: entity[CONF_ACTIVATION], - CONF_MOMENTARY: entity.get(CONF_MOMENTARY), - CONF_PAUSE: entity.get(CONF_PAUSE), - CONF_REPEAT: entity.get(CONF_REPEAT), - } - actuators.append(act) - _LOGGER.debug("Set up switch %s", act) - - sensors = [] - for entity in self.config.get(CONF_SENSORS) or []: - if CONF_ZONE in entity: - pin = ZONE_TO_PIN[entity[CONF_ZONE]] - else: - pin = entity[CONF_PIN] - - sensor = { - CONF_PIN: pin, - CONF_NAME: entity.get( - CONF_NAME, - "Konnected {} Sensor {}".format( - self.device_id[6:], PIN_TO_ZONE[pin] - ), - ), - CONF_TYPE: entity[CONF_TYPE], - CONF_POLL_INTERVAL: entity.get(CONF_POLL_INTERVAL), - } - sensors.append(sensor) - _LOGGER.debug( - "Set up %s sensor %s (initial state: %s)", - sensor.get(CONF_TYPE), - sensor.get(CONF_NAME), - sensor.get(ATTR_STATE), - ) - - device_data = { - CONF_BINARY_SENSORS: binary_sensors, - CONF_SENSORS: sensors, - CONF_SWITCHES: actuators, - CONF_BLINK: self.config.get(CONF_BLINK), - CONF_DISCOVERY: self.config.get(CONF_DISCOVERY), - } - - if CONF_DEVICES not in self.hass.data[DOMAIN]: - self.hass.data[DOMAIN][CONF_DEVICES] = {} - - _LOGGER.debug( - "Storing data in hass.data[%s][%s][%s]: %s", - DOMAIN, - CONF_DEVICES, - self.device_id, - device_data, + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) ) - self.hass.data[DOMAIN][CONF_DEVICES][self.device_id] = device_data - - for platform in ["binary_sensor", "sensor", "switch"]: - discovery.load_platform( - self.hass, - platform, - DOMAIN, - {"device_id": self.device_id}, - self.hass_config, - ) + entry.add_update_listener(async_entry_updated) + return True -class DiscoveredDevice: - """A representation of a discovered Konnected device.""" - - def __init__(self, hass, host, port): - """Initialize the Konnected device.""" - self.hass = hass - self.host = host - self.port = port - - self.client = konnected.Client(host, str(port)) - self.status = self.client.get_status() - - def setup(self): - """Set up a newly discovered Konnected device.""" - _LOGGER.info( - "Discovered Konnected device %s. Open http://%s:%s in a " - "web browser to view device status.", - self.device_id, - self.host, - self.port, +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] ) - self.save_data() - self.update_initial_states() - self.sync_device_config() + ) + if unload_ok: + hass.data[DOMAIN][CONF_DEVICES].pop(entry.data[CONF_ID]) - def save_data(self): - """Save the discovery information to `hass.data`.""" - self.stored_configuration["client"] = self.client - self.stored_configuration["host"] = self.host - self.stored_configuration["port"] = self.port + return unload_ok - @property - def device_id(self): - """Device id is the MAC address as string with punctuation removed.""" - return self.status["mac"].replace(":", "") - @property - def is_configured(self): - """Return true if device_id is specified in the configuration.""" - return bool(self.hass.data[DOMAIN][CONF_DEVICES].get(self.device_id)) - - @property - def stored_configuration(self): - """Return the configuration stored in `hass.data` for this device.""" - return self.hass.data[DOMAIN][CONF_DEVICES].get(self.device_id) - - def binary_sensor_configuration(self): - """Return the configuration map for syncing binary sensors.""" - return [{"pin": p} for p in self.stored_configuration[CONF_BINARY_SENSORS]] - - def actuator_configuration(self): - """Return the configuration map for syncing actuators.""" - return [ - { - "pin": data.get(CONF_PIN), - "trigger": (0 if data.get(CONF_ACTIVATION) in [0, STATE_LOW] else 1), - } - for data in self.stored_configuration[CONF_SWITCHES] - ] - - def dht_sensor_configuration(self): - """Return the configuration map for syncing DHT sensors.""" - return [ - {CONF_PIN: sensor[CONF_PIN], CONF_POLL_INTERVAL: sensor[CONF_POLL_INTERVAL]} - for sensor in self.stored_configuration[CONF_SENSORS] - if sensor[CONF_TYPE] == "dht" - ] - - def ds18b20_sensor_configuration(self): - """Return the configuration map for syncing DS18B20 sensors.""" - return [ - {"pin": sensor[CONF_PIN]} - for sensor in self.stored_configuration[CONF_SENSORS] - if sensor[CONF_TYPE] == "ds18b20" - ] - - def update_initial_states(self): - """Update the initial state of each sensor from status poll.""" - for sensor_data in self.status.get("sensors"): - sensor_config = self.stored_configuration[CONF_BINARY_SENSORS].get( - sensor_data.get(CONF_PIN), {} - ) - entity_id = sensor_config.get(ATTR_ENTITY_ID) - - state = bool(sensor_data.get(ATTR_STATE)) - if sensor_config.get(CONF_INVERSE): - state = not state - - dispatcher_send(self.hass, SIGNAL_SENSOR_UPDATE.format(entity_id), state) - - def desired_settings_payload(self): - """Return a dict representing the desired device configuration.""" - desired_api_host = ( - self.hass.data[DOMAIN].get(CONF_API_HOST) or self.hass.config.api.base_url - ) - desired_api_endpoint = desired_api_host + ENDPOINT_ROOT - - return { - "sensors": self.binary_sensor_configuration(), - "actuators": self.actuator_configuration(), - "dht_sensors": self.dht_sensor_configuration(), - "ds18b20_sensors": self.ds18b20_sensor_configuration(), - "auth_token": self.hass.data[DOMAIN].get(CONF_ACCESS_TOKEN), - "endpoint": desired_api_endpoint, - "blink": self.stored_configuration.get(CONF_BLINK), - "discovery": self.stored_configuration.get(CONF_DISCOVERY), - } - - def current_settings_payload(self): - """Return a dict of configuration currently stored on the device.""" - settings = self.status["settings"] - if not settings: - settings = {} - - return { - "sensors": [{"pin": s[CONF_PIN]} for s in self.status.get("sensors")], - "actuators": self.status.get("actuators"), - "dht_sensors": self.status.get(CONF_DHT_SENSORS), - "ds18b20_sensors": self.status.get(CONF_DS18B20_SENSORS), - "auth_token": settings.get("token"), - "endpoint": settings.get("apiUrl"), - "blink": settings.get(CONF_BLINK), - "discovery": settings.get(CONF_DISCOVERY), - } - - def sync_device_config(self): - """Sync the new pin configuration to the Konnected device if needed.""" - _LOGGER.debug( - "Device %s settings payload: %s", - self.device_id, - self.desired_settings_payload(), - ) - if self.desired_settings_payload() != self.current_settings_payload(): - _LOGGER.info("pushing settings to device %s", self.device_id) - self.client.put_settings(**self.desired_settings_payload()) +async def async_entry_updated(hass: HomeAssistant, entry: ConfigEntry): + """Reload the config entry when options change.""" + await hass.config_entries.async_reload(entry.entry_id) class KonnectedView(HomeAssistantView): @@ -468,9 +283,8 @@ class KonnectedView(HomeAssistantView): name = "api:konnected" requires_auth = False # Uses access token from configuration - def __init__(self, auth_token): + def __init__(self): """Initialize the view.""" - self.auth_token = auth_token @staticmethod def binary_value(state, activation): @@ -479,50 +293,29 @@ class KonnectedView(HomeAssistantView): return 1 if state == STATE_ON else 0 return 0 if state == STATE_ON else 1 - async def get(self, request: Request, device_id) -> Response: - """Return the current binary state of a switch.""" + async def update_sensor(self, request: Request, device_id) -> Response: + """Process a put or post.""" hass = request.app["hass"] - pin_num = int(request.query.get("pin")) data = hass.data[DOMAIN] - device = data[CONF_DEVICES][device_id] - if not device: - return self.json_message( - f"Device {device_id} not configured", status_code=HTTP_NOT_FOUND - ) - - try: - pin = next( - filter( - lambda switch: switch[CONF_PIN] == pin_num, device[CONF_SWITCHES] - ) - ) - except StopIteration: - pin = None - - if not pin: - return self.json_message( - format("Switch on pin {} not configured", pin_num), - status_code=HTTP_NOT_FOUND, - ) - - return self.json( - { - "pin": pin_num, - "state": self.binary_value( - hass.states.get(pin[ATTR_ENTITY_ID]).state, pin[CONF_ACTIVATION] - ), - } + auth = request.headers.get(AUTHORIZATION, None) + tokens = [] + if hass.data[DOMAIN].get(CONF_ACCESS_TOKEN): + tokens.extend([hass.data[DOMAIN][CONF_ACCESS_TOKEN]]) + tokens.extend( + [ + entry.data[CONF_ACCESS_TOKEN] + for entry in hass.config_entries.async_entries(DOMAIN) + ] ) - - async def put(self, request: Request, device_id) -> Response: - """Receive a sensor update via PUT request and async set state.""" - hass = request.app["hass"] - data = hass.data[DOMAIN] + if auth is None or not next( + (True for token in tokens if hmac.compare_digest(f"Bearer {token}", auth)), + False, + ): + return self.json_message("unauthorized", status_code=HTTP_UNAUTHORIZED) try: # Konnected 2.2.0 and above supports JSON payloads payload = await request.json() - pin_num = payload["pin"] except json.decoder.JSONDecodeError: _LOGGER.error( ( @@ -532,30 +325,97 @@ class KonnectedView(HomeAssistantView): ) ) - auth = request.headers.get(AUTHORIZATION, None) - if not hmac.compare_digest(f"Bearer {self.auth_token}", auth): - return self.json_message("unauthorized", status_code=HTTP_UNAUTHORIZED) - pin_num = int(pin_num) device = data[CONF_DEVICES].get(device_id) if device is None: return self.json_message( "unregistered device", status_code=HTTP_BAD_REQUEST ) - pin_data = device[CONF_BINARY_SENSORS].get(pin_num) or next( - (s for s in device[CONF_SENSORS] if s[CONF_PIN] == pin_num), None - ) - if pin_data is None: + try: + zone_num = str(payload.get(CONF_ZONE) or PIN_TO_ZONE[payload[CONF_PIN]]) + zone_data = device[CONF_BINARY_SENSORS].get(zone_num) or next( + (s for s in device[CONF_SENSORS] if s[CONF_ZONE] == zone_num), None + ) + except KeyError: + zone_data = None + + if zone_data is None: return self.json_message( "unregistered sensor/actuator", status_code=HTTP_BAD_REQUEST ) - pin_data["device_id"] = device_id + zone_data["device_id"] = device_id for attr in ["state", "temp", "humi", "addr"]: value = payload.get(attr) handler = HANDLERS.get(attr) if value is not None and handler: - hass.async_create_task(handler(hass, pin_data, payload)) + hass.async_create_task(handler(hass, zone_data, payload)) return self.json_message("ok") + + async def get(self, request: Request, device_id) -> Response: + """Return the current binary state of a switch.""" + hass = request.app["hass"] + data = hass.data[DOMAIN] + + device = data[CONF_DEVICES].get(device_id) + if not device: + return self.json_message( + f"Device {device_id} not configured", status_code=HTTP_NOT_FOUND + ) + + # Our data model is based on zone ids but we convert from/to pin ids + # based on whether they are specified in the request + try: + zone_num = str( + request.query.get(CONF_ZONE) or PIN_TO_ZONE[request.query[CONF_PIN]] + ) + zone = next( + ( + switch + for switch in device[CONF_SWITCHES] + if switch[CONF_ZONE] == zone_num + ) + ) + + except StopIteration: + zone = None + except KeyError: + zone = None + zone_num = None + + if not zone: + target = request.query.get( + CONF_ZONE, request.query.get(CONF_PIN, "unknown") + ) + return self.json_message( + f"Switch on zone or pin {target} not configured", + status_code=HTTP_NOT_FOUND, + ) + + resp = {} + if request.query.get(CONF_ZONE): + resp[CONF_ZONE] = zone_num + else: + resp[CONF_PIN] = ZONE_TO_PIN[zone_num] + + # Make sure entity is setup + zone_entity_id = zone.get(ATTR_ENTITY_ID) + if zone_entity_id: + resp["state"] = self.binary_value( + hass.states.get(zone_entity_id).state, zone[CONF_ACTIVATION], + ) + return self.json(resp) + + _LOGGER.warning("Konnected entity not yet setup, returning default") + resp["state"] = self.binary_value(STATE_OFF, zone[CONF_ACTIVATION]) + return self.json(resp) + + async def put(self, request: Request, device_id) -> Response: + """Receive a sensor update via PUT request and async set state.""" + return await self.update_sensor(request, device_id) + + async def post(self, request: Request, device_id) -> Response: + """Receive a sensor update via POST request and async set state.""" + return await self.update_sensor(request, device_id) diff --git a/homeassistant/components/konnected/binary_sensor.py b/homeassistant/components/konnected/binary_sensor.py index 486c228d6fb..dc4dae7787f 100644 --- a/homeassistant/components/konnected/binary_sensor.py +++ b/homeassistant/components/konnected/binary_sensor.py @@ -13,18 +13,15 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import DOMAIN as KONNECTED_DOMAIN, PIN_TO_ZONE, SIGNAL_SENSOR_UPDATE +from .const import DOMAIN as KONNECTED_DOMAIN, SIGNAL_SENSOR_UPDATE _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up binary sensors attached to a Konnected device.""" - if discovery_info is None: - return - +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up binary sensors attached to a Konnected device from a config entry.""" data = hass.data[KONNECTED_DOMAIN] - device_id = discovery_info["device_id"] + device_id = config_entry.data["id"] sensors = [ KonnectedBinarySensor(device_id, pin_num, pin_data) for pin_num, pin_data in data[CONF_DEVICES][device_id][ @@ -37,14 +34,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class KonnectedBinarySensor(BinarySensorDevice): """Representation of a Konnected binary sensor.""" - def __init__(self, device_id, pin_num, data): + def __init__(self, device_id, zone_num, data): """Initialize the Konnected binary sensor.""" self._data = data self._device_id = device_id - self._pin_num = pin_num + self._zone_num = zone_num self._state = self._data.get(ATTR_STATE) self._device_class = self._data.get(CONF_TYPE) - self._unique_id = "{}-{}".format(device_id, PIN_TO_ZONE[pin_num]) + self._unique_id = f"{device_id}-{zone_num}" self._name = self._data.get(CONF_NAME) @property @@ -72,6 +69,13 @@ class KonnectedBinarySensor(BinarySensorDevice): """Return the device class.""" return self._device_class + @property + def device_info(self): + """Return the device info.""" + return { + "identifiers": {(KONNECTED_DOMAIN, self._device_id)}, + } + async def async_added_to_hass(self): """Store entity_id and register state change callback.""" self._data[ATTR_ENTITY_ID] = self.entity_id diff --git a/homeassistant/components/konnected/config_flow.py b/homeassistant/components/konnected/config_flow.py new file mode 100644 index 00000000000..cb9004c9efe --- /dev/null +++ b/homeassistant/components/konnected/config_flow.py @@ -0,0 +1,766 @@ +"""Config flow for konnected.io integration.""" +import asyncio +import copy +import logging +import random +import string +from urllib.parse import urlparse + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_DOOR, + DEVICE_CLASSES_SCHEMA, +) +from homeassistant.components.ssdp import ATTR_UPNP_MANUFACTURER, ATTR_UPNP_MODEL_NAME +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_BINARY_SENSORS, + CONF_HOST, + CONF_ID, + CONF_NAME, + CONF_PORT, + CONF_SENSORS, + CONF_SWITCHES, + CONF_TYPE, + CONF_ZONE, +) +from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv + +from .const import ( + CONF_ACTIVATION, + CONF_BLINK, + CONF_DEFAULT_OPTIONS, + CONF_DISCOVERY, + CONF_INVERSE, + CONF_MODEL, + CONF_MOMENTARY, + CONF_PAUSE, + CONF_POLL_INTERVAL, + CONF_REPEAT, + DOMAIN, + STATE_HIGH, + STATE_LOW, + ZONES, +) +from .errors import CannotConnect +from .panel import KONN_MODEL, KONN_MODEL_PRO, get_status + +_LOGGER = logging.getLogger(__name__) + +ATTR_KONN_UPNP_MODEL_NAME = "model_name" # standard upnp is modelName +CONF_IO = "io" +CONF_IO_DIS = "Disabled" +CONF_IO_BIN = "Binary Sensor" +CONF_IO_DIG = "Digital Sensor" +CONF_IO_SWI = "Switchable Output" + +KONN_MANUFACTURER = "konnected.io" +KONN_PANEL_MODEL_NAMES = { + KONN_MODEL: "Konnected Alarm Panel", + KONN_MODEL_PRO: "Konnected Alarm Panel Pro", +} + +OPTIONS_IO_ANY = vol.In([CONF_IO_DIS, CONF_IO_BIN, CONF_IO_DIG, CONF_IO_SWI]) +OPTIONS_IO_INPUT_ONLY = vol.In([CONF_IO_DIS, CONF_IO_BIN, CONF_IO_DIG]) +OPTIONS_IO_OUTPUT_ONLY = vol.In([CONF_IO_DIS, CONF_IO_SWI]) + + +# Config entry schemas +IO_SCHEMA = vol.Schema( + { + vol.Optional("1", default=CONF_IO_DIS): OPTIONS_IO_ANY, + vol.Optional("2", default=CONF_IO_DIS): OPTIONS_IO_ANY, + vol.Optional("3", default=CONF_IO_DIS): OPTIONS_IO_ANY, + vol.Optional("4", default=CONF_IO_DIS): OPTIONS_IO_ANY, + vol.Optional("5", default=CONF_IO_DIS): OPTIONS_IO_ANY, + vol.Optional("6", default=CONF_IO_DIS): OPTIONS_IO_ANY, + vol.Optional("7", default=CONF_IO_DIS): OPTIONS_IO_ANY, + vol.Optional("8", default=CONF_IO_DIS): OPTIONS_IO_ANY, + vol.Optional("9", default=CONF_IO_DIS): OPTIONS_IO_INPUT_ONLY, + vol.Optional("10", default=CONF_IO_DIS): OPTIONS_IO_INPUT_ONLY, + vol.Optional("11", default=CONF_IO_DIS): OPTIONS_IO_INPUT_ONLY, + vol.Optional("12", default=CONF_IO_DIS): OPTIONS_IO_INPUT_ONLY, + vol.Optional("out", default=CONF_IO_DIS): OPTIONS_IO_OUTPUT_ONLY, + vol.Optional("alarm1", default=CONF_IO_DIS): OPTIONS_IO_OUTPUT_ONLY, + vol.Optional("out1", default=CONF_IO_DIS): OPTIONS_IO_OUTPUT_ONLY, + vol.Optional("alarm2_out2", default=CONF_IO_DIS): OPTIONS_IO_OUTPUT_ONLY, + } +) + +BINARY_SENSOR_SCHEMA = vol.Schema( + { + vol.Required(CONF_ZONE): vol.In(ZONES), + vol.Required(CONF_TYPE, default=DEVICE_CLASS_DOOR): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_INVERSE, default=False): cv.boolean, + } +) + +SENSOR_SCHEMA = vol.Schema( + { + vol.Required(CONF_ZONE): vol.In(ZONES), + vol.Required(CONF_TYPE, default="dht"): vol.All( + vol.Lower, vol.In(["dht", "ds18b20"]) + ), + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_POLL_INTERVAL, default=3): vol.All( + vol.Coerce(int), vol.Range(min=1) + ), + } +) + +SWITCH_SCHEMA = vol.Schema( + { + vol.Required(CONF_ZONE): vol.In(ZONES), + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_ACTIVATION, default=STATE_HIGH): vol.All( + vol.Lower, vol.Any(STATE_HIGH, STATE_LOW) + ), + vol.Optional(CONF_MOMENTARY): vol.All(vol.Coerce(int), vol.Range(min=10)), + vol.Optional(CONF_PAUSE): vol.All(vol.Coerce(int), vol.Range(min=10)), + vol.Optional(CONF_REPEAT): vol.All(vol.Coerce(int), vol.Range(min=-1)), + } +) + +OPTIONS_SCHEMA = vol.Schema( + { + vol.Required(CONF_IO): IO_SCHEMA, + vol.Optional(CONF_BINARY_SENSORS): vol.All( + cv.ensure_list, [BINARY_SENSOR_SCHEMA] + ), + vol.Optional(CONF_SENSORS): vol.All(cv.ensure_list, [SENSOR_SCHEMA]), + vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [SWITCH_SCHEMA]), + vol.Optional(CONF_BLINK, default=True): cv.boolean, + vol.Optional(CONF_DISCOVERY, default=True): cv.boolean, + }, + extra=vol.REMOVE_EXTRA, +) + +CONFIG_ENTRY_SCHEMA = vol.Schema( + { + vol.Required(CONF_ID): cv.matches_regex("[0-9a-f]{12}"), + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PORT): cv.port, + vol.Required(CONF_MODEL): vol.Any(*KONN_PANEL_MODEL_NAMES), + vol.Required(CONF_ACCESS_TOKEN): cv.matches_regex("[a-zA-Z0-9]+"), + vol.Required(CONF_DEFAULT_OPTIONS): OPTIONS_SCHEMA, + }, + extra=vol.REMOVE_EXTRA, +) + + +class KonnectedFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for NEW_NAME.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + # class variable to store/share discovered host information + discovered_hosts = {} + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + + def __init__(self): + """Initialize the Konnected flow.""" + self.data = {} + self.options = OPTIONS_SCHEMA({CONF_IO: {}}) + + async def async_gen_config(self, host, port): + """Populate self.data based on panel status. + + This will raise CannotConnect if an error occurs + """ + self.data[CONF_HOST] = host + self.data[CONF_PORT] = port + try: + status = await get_status(self.hass, host, port) + self.data[CONF_ID] = status["mac"].replace(":", "") + except (CannotConnect, KeyError): + raise CannotConnect + else: + self.data[CONF_MODEL] = status.get("model", KONN_MODEL) + self.data[CONF_ACCESS_TOKEN] = "".join( + random.choices(f"{string.ascii_uppercase}{string.digits}", k=20) + ) + + async def async_step_import(self, device_config): + """Import a configuration.yaml config. + + This flow is triggered by `async_setup` for configured panels. + """ + _LOGGER.debug(device_config) + + # save the data and confirm connection via user step + await self.async_set_unique_id(device_config["id"]) + self.options = device_config[CONF_DEFAULT_OPTIONS] + + # config schema ensures we have port if we have host + if device_config.get(CONF_HOST): + # automatically connect if we have host info + return await self.async_step_user( + user_input={ + CONF_HOST: device_config[CONF_HOST], + CONF_PORT: device_config[CONF_PORT], + } + ) + + # if we have no host info wait for it or abort if previously configured + self._abort_if_unique_id_configured() + return await self.async_step_import_confirm() + + async def async_step_import_confirm(self, user_input=None): + """Confirm the user wants to import the config entry.""" + if user_input is None: + return self.async_show_form( + step_id="import_confirm", + description_placeholders={"id": self.unique_id}, + ) + + # if we have ssdp discovered applicable host info use it + if KonnectedFlowHandler.discovered_hosts.get(self.unique_id): + return await self.async_step_user( + user_input={ + CONF_HOST: KonnectedFlowHandler.discovered_hosts[self.unique_id][ + CONF_HOST + ], + CONF_PORT: KonnectedFlowHandler.discovered_hosts[self.unique_id][ + CONF_PORT + ], + } + ) + return await self.async_step_user() + + async def async_step_ssdp(self, discovery_info): + """Handle a discovered konnected panel. + + This flow is triggered by the SSDP component. It will check if the + device is already configured and attempt to finish the config if not. + """ + _LOGGER.debug(discovery_info) + + try: + if discovery_info[ATTR_UPNP_MANUFACTURER] != KONN_MANUFACTURER: + return self.async_abort(reason="not_konn_panel") + + if not any( + name in discovery_info[ATTR_UPNP_MODEL_NAME] + for name in KONN_PANEL_MODEL_NAMES + ): + _LOGGER.warning( + "Discovered unrecognized Konnected device %s", + discovery_info.get(ATTR_UPNP_MODEL_NAME, "Unknown"), + ) + return self.async_abort(reason="not_konn_panel") + + # If MAC is missing it is a bug in the device fw but we'll guard + # against it since the field is so vital + except KeyError: + _LOGGER.error("Malformed Konnected SSDP info") + else: + # extract host/port from ssdp_location + netloc = urlparse(discovery_info["ssdp_location"]).netloc.split(":") + return await self.async_step_user( + user_input={CONF_HOST: netloc[0], CONF_PORT: int(netloc[1])} + ) + + return self.async_abort(reason="unknown") + + async def async_step_user(self, user_input=None): + """Connect to panel and get config.""" + errors = {} + if user_input: + # build config info and wait for user confirmation + self.data[CONF_HOST] = user_input[CONF_HOST] + self.data[CONF_PORT] = user_input[CONF_PORT] + self.data[CONF_ACCESS_TOKEN] = self.hass.data.get(DOMAIN, {}).get( + CONF_ACCESS_TOKEN + ) or "".join( + random.choices(f"{string.ascii_uppercase}{string.digits}", k=20) + ) + + # brief delay to allow processing of recent status req + await asyncio.sleep(0.1) + try: + status = await get_status( + self.hass, self.data[CONF_HOST], self.data[CONF_PORT] + ) + except CannotConnect: + errors["base"] = "cannot_connect" + else: + self.data[CONF_ID] = status["mac"].replace(":", "") + self.data[CONF_MODEL] = status.get("model", KONN_MODEL) + + # save off our discovered host info + KonnectedFlowHandler.discovered_hosts[self.data[CONF_ID]] = { + CONF_HOST: self.data[CONF_HOST], + CONF_PORT: self.data[CONF_PORT], + } + return await self.async_step_confirm() + + return self.async_show_form( + step_id="user", + description_placeholders={ + "host": self.data.get(CONF_HOST, "Unknown"), + "port": self.data.get(CONF_PORT, "Unknown"), + }, + data_schema=vol.Schema( + { + vol.Required(CONF_HOST, default=self.data.get(CONF_HOST)): str, + vol.Required(CONF_PORT, default=self.data.get(CONF_PORT)): int, + } + ), + errors=errors, + ) + + async def async_step_confirm(self, user_input=None): + """Attempt to link with the Konnected panel. + + Given a configured host, will ask the user to confirm and finalize + the connection. + """ + if user_input is None: + # abort and update an existing config entry if host info changes + await self.async_set_unique_id(self.data[CONF_ID]) + self._abort_if_unique_id_configured(updates=self.data) + return self.async_show_form( + step_id="confirm", + description_placeholders={ + "model": KONN_PANEL_MODEL_NAMES[self.data[CONF_MODEL]], + "id": self.unique_id, + "host": self.data[CONF_HOST], + "port": self.data[CONF_PORT], + }, + ) + + # Attach default options and create entry + self.data[CONF_DEFAULT_OPTIONS] = self.options + return self.async_create_entry( + title=KONN_PANEL_MODEL_NAMES[self.data[CONF_MODEL]], data=self.data, + ) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Return the Options Flow.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow for a Konnected Panel.""" + + def __init__(self, config_entry: config_entries.ConfigEntry): + """Initialize options flow.""" + self.entry = config_entry + self.model = self.entry.data[CONF_MODEL] + self.current_opt = self.entry.options or self.entry.data[CONF_DEFAULT_OPTIONS] + + # as config proceeds we'll build up new options and then replace what's in the config entry + self.new_opt = {CONF_IO: {}} + self.active_cfg = None + self.io_cfg = {} + + @callback + def get_current_cfg(self, io_type, zone): + """Get the current zone config.""" + return next( + ( + cfg + for cfg in self.current_opt.get(io_type, []) + if cfg[CONF_ZONE] == zone + ), + {}, + ) + + async def async_step_init(self, user_input=None): + """Handle options flow.""" + return await self.async_step_options_io() + + async def async_step_options_io(self, user_input=None): + """Configure legacy panel IO or first half of pro IO.""" + errors = {} + current_io = self.current_opt.get(CONF_IO, {}) + + if user_input is not None: + # strip out disabled io and save for options cfg + for key, value in user_input.items(): + if value != CONF_IO_DIS: + self.new_opt[CONF_IO][key] = value + return await self.async_step_options_io_ext() + + if self.model == KONN_MODEL: + return self.async_show_form( + step_id="options_io", + data_schema=vol.Schema( + { + vol.Required( + "1", default=current_io.get("1", CONF_IO_DIS) + ): OPTIONS_IO_ANY, + vol.Required( + "2", default=current_io.get("2", CONF_IO_DIS) + ): OPTIONS_IO_ANY, + vol.Required( + "3", default=current_io.get("3", CONF_IO_DIS) + ): OPTIONS_IO_ANY, + vol.Required( + "4", default=current_io.get("4", CONF_IO_DIS) + ): OPTIONS_IO_ANY, + vol.Required( + "5", default=current_io.get("5", CONF_IO_DIS) + ): OPTIONS_IO_ANY, + vol.Required( + "6", default=current_io.get("6", CONF_IO_DIS) + ): OPTIONS_IO_ANY, + vol.Required( + "out", default=current_io.get("out", CONF_IO_DIS) + ): OPTIONS_IO_OUTPUT_ONLY, + } + ), + description_placeholders={ + "model": KONN_PANEL_MODEL_NAMES[self.model], + "host": self.entry.data[CONF_HOST], + }, + errors=errors, + ) + + # configure the first half of the pro board io + if self.model == KONN_MODEL_PRO: + return self.async_show_form( + step_id="options_io", + data_schema=vol.Schema( + { + vol.Required( + "1", default=current_io.get("1", CONF_IO_DIS) + ): OPTIONS_IO_ANY, + vol.Required( + "2", default=current_io.get("2", CONF_IO_DIS) + ): OPTIONS_IO_ANY, + vol.Required( + "3", default=current_io.get("3", CONF_IO_DIS) + ): OPTIONS_IO_ANY, + vol.Required( + "4", default=current_io.get("4", CONF_IO_DIS) + ): OPTIONS_IO_ANY, + vol.Required( + "5", default=current_io.get("5", CONF_IO_DIS) + ): OPTIONS_IO_ANY, + vol.Required( + "6", default=current_io.get("6", CONF_IO_DIS) + ): OPTIONS_IO_ANY, + vol.Required( + "7", default=current_io.get("7", CONF_IO_DIS) + ): OPTIONS_IO_ANY, + } + ), + description_placeholders={ + "model": KONN_PANEL_MODEL_NAMES[self.model], + "host": self.entry.data[CONF_HOST], + }, + errors=errors, + ) + + return self.async_abort(reason="not_konn_panel") + + async def async_step_options_io_ext(self, user_input=None): + """Allow the user to configure the extended IO for pro.""" + errors = {} + current_io = self.current_opt.get(CONF_IO, {}) + + if user_input is not None: + # strip out disabled io and save for options cfg + for key, value in user_input.items(): + if value != CONF_IO_DIS: + self.new_opt[CONF_IO].update({key: value}) + self.io_cfg = copy.deepcopy(self.new_opt[CONF_IO]) + return await self.async_step_options_binary() + + if self.model == KONN_MODEL: + self.io_cfg = copy.deepcopy(self.new_opt[CONF_IO]) + return await self.async_step_options_binary() + + if self.model == KONN_MODEL_PRO: + return self.async_show_form( + step_id="options_io_ext", + data_schema=vol.Schema( + { + vol.Required( + "8", default=current_io.get("8", CONF_IO_DIS) + ): OPTIONS_IO_ANY, + vol.Required( + "9", default=current_io.get("9", CONF_IO_DIS) + ): OPTIONS_IO_INPUT_ONLY, + vol.Required( + "10", default=current_io.get("10", CONF_IO_DIS) + ): OPTIONS_IO_INPUT_ONLY, + vol.Required( + "11", default=current_io.get("11", CONF_IO_DIS) + ): OPTIONS_IO_INPUT_ONLY, + vol.Required( + "12", default=current_io.get("12", CONF_IO_DIS) + ): OPTIONS_IO_INPUT_ONLY, + vol.Required( + "alarm1", default=current_io.get("alarm1", CONF_IO_DIS) + ): OPTIONS_IO_OUTPUT_ONLY, + vol.Required( + "out1", default=current_io.get("out1", CONF_IO_DIS) + ): OPTIONS_IO_OUTPUT_ONLY, + vol.Required( + "alarm2_out2", + default=current_io.get("alarm2_out2", CONF_IO_DIS), + ): OPTIONS_IO_OUTPUT_ONLY, + } + ), + description_placeholders={ + "model": KONN_PANEL_MODEL_NAMES[self.model], + "host": self.entry.data[CONF_HOST], + }, + errors=errors, + ) + + return self.async_abort(reason="not_konn_panel") + + async def async_step_options_binary(self, user_input=None): + """Allow the user to configure the IO options for binary sensors.""" + errors = {} + if user_input is not None: + zone = {"zone": self.active_cfg} + zone.update(user_input) + self.new_opt[CONF_BINARY_SENSORS] = self.new_opt.get( + CONF_BINARY_SENSORS, [] + ) + [zone] + self.io_cfg.pop(self.active_cfg) + self.active_cfg = None + + if self.active_cfg: + current_cfg = self.get_current_cfg(CONF_BINARY_SENSORS, self.active_cfg) + return self.async_show_form( + step_id="options_binary", + data_schema=vol.Schema( + { + vol.Required( + CONF_TYPE, + default=current_cfg.get(CONF_TYPE, DEVICE_CLASS_DOOR), + ): DEVICE_CLASSES_SCHEMA, + vol.Optional( + CONF_NAME, default=current_cfg.get(CONF_NAME, vol.UNDEFINED) + ): str, + vol.Optional( + CONF_INVERSE, default=current_cfg.get(CONF_INVERSE, False) + ): bool, + } + ), + description_placeholders={ + "zone": f"Zone {self.active_cfg}" + if len(self.active_cfg) < 3 + else self.active_cfg.upper + }, + errors=errors, + ) + + # find the next unconfigured binary sensor + for key, value in self.io_cfg.items(): + if value == CONF_IO_BIN: + self.active_cfg = key + current_cfg = self.get_current_cfg(CONF_BINARY_SENSORS, self.active_cfg) + return self.async_show_form( + step_id="options_binary", + data_schema=vol.Schema( + { + vol.Required( + CONF_TYPE, + default=current_cfg.get(CONF_TYPE, DEVICE_CLASS_DOOR), + ): DEVICE_CLASSES_SCHEMA, + vol.Optional( + CONF_NAME, + default=current_cfg.get(CONF_NAME, vol.UNDEFINED), + ): str, + vol.Optional( + CONF_INVERSE, + default=current_cfg.get(CONF_INVERSE, False), + ): bool, + } + ), + description_placeholders={ + "zone": f"Zone {self.active_cfg}" + if len(self.active_cfg) < 3 + else self.active_cfg.upper + }, + errors=errors, + ) + + return await self.async_step_options_digital() + + async def async_step_options_digital(self, user_input=None): + """Allow the user to configure the IO options for digital sensors.""" + errors = {} + if user_input is not None: + zone = {"zone": self.active_cfg} + zone.update(user_input) + self.new_opt[CONF_SENSORS] = self.new_opt.get(CONF_SENSORS, []) + [zone] + self.io_cfg.pop(self.active_cfg) + self.active_cfg = None + + if self.active_cfg: + current_cfg = self.get_current_cfg(CONF_SENSORS, self.active_cfg) + return self.async_show_form( + step_id="options_digital", + data_schema=vol.Schema( + { + vol.Required( + CONF_TYPE, default=current_cfg.get(CONF_TYPE, "dht") + ): vol.All(vol.Lower, vol.In(["dht", "ds18b20"])), + vol.Optional( + CONF_NAME, default=current_cfg.get(CONF_NAME, vol.UNDEFINED) + ): str, + vol.Optional( + CONF_POLL_INTERVAL, + default=current_cfg.get(CONF_POLL_INTERVAL, 3), + ): vol.All(vol.Coerce(int), vol.Range(min=1)), + } + ), + description_placeholders={ + "zone": f"Zone {self.active_cfg}" + if len(self.active_cfg) < 3 + else self.active_cfg.upper() + }, + errors=errors, + ) + + # find the next unconfigured digital sensor + for key, value in self.io_cfg.items(): + if value == CONF_IO_DIG: + self.active_cfg = key + current_cfg = self.get_current_cfg(CONF_SENSORS, self.active_cfg) + return self.async_show_form( + step_id="options_digital", + data_schema=vol.Schema( + { + vol.Required( + CONF_TYPE, default=current_cfg.get(CONF_TYPE, "dht") + ): vol.All(vol.Lower, vol.In(["dht", "ds18b20"])), + vol.Optional( + CONF_NAME, + default=current_cfg.get(CONF_NAME, vol.UNDEFINED), + ): str, + vol.Optional( + CONF_POLL_INTERVAL, + default=current_cfg.get(CONF_POLL_INTERVAL, 3), + ): vol.All(vol.Coerce(int), vol.Range(min=1)), + } + ), + description_placeholders={ + "zone": f"Zone {self.active_cfg}" + if len(self.active_cfg) < 3 + else self.active_cfg.upper() + }, + errors=errors, + ) + + return await self.async_step_options_switch() + + async def async_step_options_switch(self, user_input=None): + """Allow the user to configure the IO options for switches.""" + errors = {} + if user_input is not None: + zone = {"zone": self.active_cfg} + zone.update(user_input) + self.new_opt[CONF_SWITCHES] = self.new_opt.get(CONF_SWITCHES, []) + [zone] + self.io_cfg.pop(self.active_cfg) + self.active_cfg = None + + if self.active_cfg: + current_cfg = self.get_current_cfg(CONF_SWITCHES, self.active_cfg) + return self.async_show_form( + step_id="options_switch", + data_schema=vol.Schema( + { + vol.Optional( + CONF_NAME, default=current_cfg.get(CONF_NAME, vol.UNDEFINED) + ): str, + vol.Optional( + CONF_ACTIVATION, + default=current_cfg.get(CONF_ACTIVATION, STATE_HIGH), + ): vol.All(vol.Lower, vol.Any(STATE_HIGH, STATE_LOW)), + vol.Optional( + CONF_MOMENTARY, + default=current_cfg.get(CONF_MOMENTARY, vol.UNDEFINED), + ): vol.All(vol.Coerce(int), vol.Range(min=10)), + vol.Optional( + CONF_PAUSE, + default=current_cfg.get(CONF_PAUSE, vol.UNDEFINED), + ): vol.All(vol.Coerce(int), vol.Range(min=10)), + vol.Optional( + CONF_REPEAT, + default=current_cfg.get(CONF_REPEAT, vol.UNDEFINED), + ): vol.All(vol.Coerce(int), vol.Range(min=-1)), + } + ), + description_placeholders={ + "zone": f"Zone {self.active_cfg}" + if len(self.active_cfg) < 3 + else self.active_cfg.upper() + }, + errors=errors, + ) + + # find the next unconfigured switch + for key, value in self.io_cfg.items(): + if value == CONF_IO_SWI: + self.active_cfg = key + current_cfg = self.get_current_cfg(CONF_SWITCHES, self.active_cfg) + return self.async_show_form( + step_id="options_switch", + data_schema=vol.Schema( + { + vol.Optional( + CONF_NAME, + default=current_cfg.get(CONF_NAME, vol.UNDEFINED), + ): str, + vol.Optional( + CONF_ACTIVATION, + default=current_cfg.get(CONF_ACTIVATION, "high"), + ): vol.In(["low", "high"]), + vol.Optional( + CONF_MOMENTARY, + default=current_cfg.get(CONF_MOMENTARY, vol.UNDEFINED), + ): vol.All(vol.Coerce(int), vol.Range(min=10)), + vol.Optional( + CONF_PAUSE, + default=current_cfg.get(CONF_PAUSE, vol.UNDEFINED), + ): vol.All(vol.Coerce(int), vol.Range(min=10)), + vol.Optional( + CONF_REPEAT, + default=current_cfg.get(CONF_REPEAT, vol.UNDEFINED), + ): vol.All(vol.Coerce(int), vol.Range(min=-1)), + } + ), + description_placeholders={ + "zone": f"Zone {self.active_cfg}" + if len(self.active_cfg) < 3 + else self.active_cfg.upper() + }, + errors=errors, + ) + + return await self.async_step_options_misc() + + async def async_step_options_misc(self, user_input=None): + """Allow the user to configure the LED behavior.""" + errors = {} + if user_input is not None: + self.new_opt[CONF_BLINK] = user_input[CONF_BLINK] + return self.async_create_entry(title="", data=self.new_opt) + + return self.async_show_form( + step_id="options_misc", + data_schema=vol.Schema( + { + vol.Required( + CONF_BLINK, default=self.current_opt.get(CONF_BLINK, True) + ): bool, + } + ), + errors=errors, + ) diff --git a/homeassistant/components/konnected/const.py b/homeassistant/components/konnected/const.py index 0107b341532..d6819dcf71f 100644 --- a/homeassistant/components/konnected/const.py +++ b/homeassistant/components/konnected/const.py @@ -4,6 +4,7 @@ DOMAIN = "konnected" CONF_ACTIVATION = "activation" CONF_API_HOST = "api_host" +CONF_DEFAULT_OPTIONS = "default_options" CONF_MOMENTARY = "momentary" CONF_PAUSE = "pause" CONF_POLL_INTERVAL = "poll_interval" @@ -14,11 +15,33 @@ CONF_BLINK = "blink" CONF_DISCOVERY = "discovery" CONF_DHT_SENSORS = "dht_sensors" CONF_DS18B20_SENSORS = "ds18b20_sensors" +CONF_MODEL = "model" STATE_LOW = "low" STATE_HIGH = "high" -PIN_TO_ZONE = {1: 1, 2: 2, 5: 3, 6: 4, 7: 5, 8: "out", 9: 6} +ZONES = [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "alarm1", + "out1", + "alarm2_out2", + "out", +] + +# alarm panel pro only handles zones, +# alarm panel allows specifying pins via configuration.yaml +PIN_TO_ZONE = {"1": "1", "2": "2", "5": "3", "6": "4", "7": "5", "8": "out", "9": "6"} ZONE_TO_PIN = {zone: pin for pin, zone in PIN_TO_ZONE.items()} ENDPOINT_ROOT = "/api/konnected" diff --git a/homeassistant/components/konnected/errors.py b/homeassistant/components/konnected/errors.py new file mode 100644 index 00000000000..5a0207f3f8d --- /dev/null +++ b/homeassistant/components/konnected/errors.py @@ -0,0 +1,10 @@ +"""Errors for the Konnected component.""" +from homeassistant.exceptions import HomeAssistantError + + +class KonnectedException(HomeAssistantError): + """Base class for Konnected exceptions.""" + + +class CannotConnect(KonnectedException): + """Unable to connect to the panel.""" diff --git a/homeassistant/components/konnected/manifest.json b/homeassistant/components/konnected/manifest.json index feb6a4589cb..3a74e2165df 100644 --- a/homeassistant/components/konnected/manifest.json +++ b/homeassistant/components/konnected/manifest.json @@ -1,8 +1,21 @@ { "domain": "konnected", "name": "Konnected", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/konnected", - "requirements": ["konnected==0.1.5"], - "dependencies": ["http"], - "codeowners": ["@heythisisnate"] + "requirements": [ + "konnected==1.1.0" + ], + "ssdp": [ + { + "manufacturer": "konnected.io" + } + ], + "dependencies": [ + "http" + ], + "codeowners": [ + "@heythisisnate", + "@kit-klein" + ] } diff --git a/homeassistant/components/konnected/panel.py b/homeassistant/components/konnected/panel.py new file mode 100644 index 00000000000..2668a382ccc --- /dev/null +++ b/homeassistant/components/konnected/panel.py @@ -0,0 +1,362 @@ +"""Support for Konnected devices.""" +import asyncio +import logging + +import konnected + +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, +) +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client, device_registry as dr +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from .const import ( + CONF_ACTIVATION, + CONF_API_HOST, + CONF_BLINK, + CONF_DEFAULT_OPTIONS, + CONF_DHT_SENSORS, + CONF_DISCOVERY, + CONF_DS18B20_SENSORS, + CONF_INVERSE, + CONF_MOMENTARY, + CONF_PAUSE, + CONF_POLL_INTERVAL, + CONF_REPEAT, + DOMAIN, + ENDPOINT_ROOT, + SIGNAL_SENSOR_UPDATE, + STATE_LOW, + ZONE_TO_PIN, +) +from .errors import CannotConnect + +_LOGGER = logging.getLogger(__name__) + +KONN_MODEL = "Konnected" +KONN_MODEL_PRO = "Konnected Pro" + +# Indicate how each unit is controlled (pin or zone) +KONN_API_VERSIONS = { + KONN_MODEL: CONF_PIN, + KONN_MODEL_PRO: CONF_ZONE, +} + + +class AlarmPanel: + """A representation of a Konnected alarm panel.""" + + def __init__(self, hass, config_entry): + """Initialize the Konnected device.""" + self.hass = hass + self.config_entry = config_entry + self.config = config_entry.data + self.options = config_entry.options or config_entry.data.get( + CONF_DEFAULT_OPTIONS, {} + ) + self.host = self.config.get(CONF_HOST) + self.port = self.config.get(CONF_PORT) + self.client = None + self.status = None + self.api_version = KONN_API_VERSIONS[KONN_MODEL] + + @property + def device_id(self): + """Device id is the MAC address as string with punctuation removed.""" + return self.config.get(CONF_ID) + + @property + def stored_configuration(self): + """Return the configuration stored in `hass.data` for this device.""" + return self.hass.data[DOMAIN][CONF_DEVICES].get(self.device_id) + + def format_zone(self, zone, other_items=None): + """Get zone or pin based dict based on the client type.""" + payload = { + self.api_version: zone + if self.api_version == CONF_ZONE + else ZONE_TO_PIN[zone] + } + payload.update(other_items or {}) + return payload + + async def async_connect(self): + """Connect to and setup a Konnected device.""" + try: + self.client = konnected.Client( + host=self.host, + port=str(self.port), + websession=aiohttp_client.async_get_clientsession(self.hass), + ) + self.status = await self.client.get_status() + self.api_version = KONN_API_VERSIONS.get( + self.status.get("model", KONN_MODEL), KONN_API_VERSIONS[KONN_MODEL] + ) + _LOGGER.info( + "Connected to new %s device", self.status.get("model", "Konnected") + ) + _LOGGER.debug(self.status) + + await self.async_update_initial_states() + # brief delay to allow processing of recent status req + await asyncio.sleep(0.1) + await self.async_sync_device_config() + + except self.client.ClientError as err: + _LOGGER.warning("Exception trying to connect to panel: %s", err) + raise CannotConnect + + _LOGGER.info( + "Set up Konnected device %s. Open http://%s:%s in a " + "web browser to view device status", + self.device_id, + self.host, + self.port, + ) + + device_registry = await dr.async_get_registry(self.hass) + + device_registry.async_get_or_create( + config_entry_id=self.config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, self.status.get("mac"))}, + identifiers={(DOMAIN, self.device_id)}, + manufacturer="Konnected.io", + name=self.config_entry.title, + model=self.config_entry.title, + sw_version=self.status.get("swVersion"), + ) + + async def update_switch(self, zone, state, momentary=None, times=None, pause=None): + """Update the state of a switchable output.""" + try: + if self.client: + if self.api_version == CONF_ZONE: + return await self.client.put_zone( + zone, state, momentary, times, pause, + ) + + # device endpoint uses pin number instead of zone + return await self.client.put_device( + ZONE_TO_PIN[zone], state, momentary, times, pause, + ) + + except self.client.ClientError as err: + _LOGGER.warning("Exception trying to update panel: %s", err) + + raise CannotConnect + + async def async_save_data(self): + """Save the device configuration to `hass.data`.""" + binary_sensors = {} + for entity in self.options.get(CONF_BINARY_SENSORS) or []: + zone = entity[CONF_ZONE] + + binary_sensors[zone] = { + CONF_TYPE: entity[CONF_TYPE], + CONF_NAME: entity.get( + CONF_NAME, f"Konnected {self.device_id[6:]} Zone {zone}" + ), + CONF_INVERSE: entity.get(CONF_INVERSE), + ATTR_STATE: None, + } + _LOGGER.debug( + "Set up binary_sensor %s (initial state: %s)", + binary_sensors[zone].get("name"), + binary_sensors[zone].get(ATTR_STATE), + ) + + actuators = [] + for entity in self.options.get(CONF_SWITCHES) or []: + zone = entity[CONF_ZONE] + + act = { + CONF_ZONE: zone, + CONF_NAME: entity.get( + CONF_NAME, f"Konnected {self.device_id[6:]} Actuator {zone}", + ), + ATTR_STATE: None, + CONF_ACTIVATION: entity[CONF_ACTIVATION], + CONF_MOMENTARY: entity.get(CONF_MOMENTARY), + CONF_PAUSE: entity.get(CONF_PAUSE), + CONF_REPEAT: entity.get(CONF_REPEAT), + } + actuators.append(act) + _LOGGER.debug("Set up switch %s", act) + + sensors = [] + for entity in self.options.get(CONF_SENSORS) or []: + zone = entity[CONF_ZONE] + + sensor = { + CONF_ZONE: zone, + CONF_NAME: entity.get( + CONF_NAME, f"Konnected {self.device_id[6:]} Sensor {zone}" + ), + CONF_TYPE: entity[CONF_TYPE], + CONF_POLL_INTERVAL: entity.get(CONF_POLL_INTERVAL), + } + sensors.append(sensor) + _LOGGER.debug( + "Set up %s sensor %s (initial state: %s)", + sensor.get(CONF_TYPE), + sensor.get(CONF_NAME), + sensor.get(ATTR_STATE), + ) + + device_data = { + CONF_BINARY_SENSORS: binary_sensors, + CONF_SENSORS: sensors, + CONF_SWITCHES: actuators, + CONF_BLINK: self.options.get(CONF_BLINK), + CONF_DISCOVERY: self.options.get(CONF_DISCOVERY), + CONF_HOST: self.host, + CONF_PORT: self.port, + "panel": self, + } + + if CONF_DEVICES not in self.hass.data[DOMAIN]: + self.hass.data[DOMAIN][CONF_DEVICES] = {} + + _LOGGER.debug( + "Storing data in hass.data[%s][%s][%s]: %s", + DOMAIN, + CONF_DEVICES, + self.device_id, + device_data, + ) + self.hass.data[DOMAIN][CONF_DEVICES][self.device_id] = device_data + + @callback + def async_binary_sensor_configuration(self): + """Return the configuration map for syncing binary sensors.""" + return [ + self.format_zone(p) for p in self.stored_configuration[CONF_BINARY_SENSORS] + ] + + @callback + def async_actuator_configuration(self): + """Return the configuration map for syncing actuators.""" + return [ + self.format_zone( + data[CONF_ZONE], + {"trigger": (0 if data.get(CONF_ACTIVATION) in [0, STATE_LOW] else 1)}, + ) + for data in self.stored_configuration[CONF_SWITCHES] + ] + + @callback + def async_dht_sensor_configuration(self): + """Return the configuration map for syncing DHT sensors.""" + return [ + self.format_zone( + sensor[CONF_ZONE], {CONF_POLL_INTERVAL: sensor[CONF_POLL_INTERVAL]} + ) + for sensor in self.stored_configuration[CONF_SENSORS] + if sensor[CONF_TYPE] == "dht" + ] + + @callback + def async_ds18b20_sensor_configuration(self): + """Return the configuration map for syncing DS18B20 sensors.""" + return [ + self.format_zone(sensor[CONF_ZONE]) + for sensor in self.stored_configuration[CONF_SENSORS] + if sensor[CONF_TYPE] == "ds18b20" + ] + + async def async_update_initial_states(self): + """Update the initial state of each sensor from status poll.""" + for sensor_data in self.status.get("sensors"): + sensor_config = self.stored_configuration[CONF_BINARY_SENSORS].get( + sensor_data.get(CONF_ZONE, sensor_data.get(CONF_PIN)), {} + ) + entity_id = sensor_config.get(ATTR_ENTITY_ID) + + state = bool(sensor_data.get(ATTR_STATE)) + if sensor_config.get(CONF_INVERSE): + state = not state + + async_dispatcher_send( + self.hass, SIGNAL_SENSOR_UPDATE.format(entity_id), state + ) + + @callback + def async_desired_settings_payload(self): + """Return a dict representing the desired device configuration.""" + desired_api_host = ( + self.hass.data[DOMAIN].get(CONF_API_HOST) or self.hass.config.api.base_url + ) + desired_api_endpoint = desired_api_host + ENDPOINT_ROOT + + return { + "sensors": self.async_binary_sensor_configuration(), + "actuators": self.async_actuator_configuration(), + "dht_sensors": self.async_dht_sensor_configuration(), + "ds18b20_sensors": self.async_ds18b20_sensor_configuration(), + "auth_token": self.config.get(CONF_ACCESS_TOKEN), + "endpoint": desired_api_endpoint, + "blink": self.options.get(CONF_BLINK, True), + "discovery": self.options.get(CONF_DISCOVERY, True), + } + + @callback + def async_current_settings_payload(self): + """Return a dict of configuration currently stored on the device.""" + settings = self.status["settings"] + if not settings: + settings = {} + + return { + "sensors": [ + {self.api_version: s[self.api_version]} + for s in self.status.get("sensors") + ], + "actuators": self.status.get("actuators"), + "dht_sensors": self.status.get(CONF_DHT_SENSORS), + "ds18b20_sensors": self.status.get(CONF_DS18B20_SENSORS), + "auth_token": settings.get("token"), + "endpoint": settings.get("endpoint"), + "blink": settings.get(CONF_BLINK), + "discovery": settings.get(CONF_DISCOVERY), + } + + async def async_sync_device_config(self): + """Sync the new zone configuration to the Konnected device if needed.""" + _LOGGER.debug( + "Device %s settings payload: %s", + self.device_id, + self.async_desired_settings_payload(), + ) + if ( + self.async_desired_settings_payload() + != self.async_current_settings_payload() + ): + _LOGGER.info("pushing settings to device %s", self.device_id) + await self.client.put_settings(**self.async_desired_settings_payload()) + + +async def get_status(hass, host, port): + """Get the status of a Konnected Panel.""" + client = konnected.Client( + host, str(port), aiohttp_client.async_get_clientsession(hass) + ) + try: + return await client.get_status() + + except client.ClientError as err: + _LOGGER.error("Exception trying to get panel status: %s", err) + raise CannotConnect diff --git a/homeassistant/components/konnected/sensor.py b/homeassistant/components/konnected/sensor.py index 7498f2bde1d..d189ac8809a 100644 --- a/homeassistant/components/konnected/sensor.py +++ b/homeassistant/components/konnected/sensor.py @@ -4,9 +4,9 @@ import logging from homeassistant.const import ( CONF_DEVICES, CONF_NAME, - CONF_PIN, CONF_SENSORS, CONF_TYPE, + CONF_ZONE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, @@ -25,13 +25,10 @@ SENSOR_TYPES = { } -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up sensors attached to a Konnected device.""" - if discovery_info is None: - return - +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up sensors attached to a Konnected device from a config entry.""" data = hass.data[KONNECTED_DOMAIN] - device_id = discovery_info["device_id"] + device_id = config_entry.data["id"] sensors = [] # Initialize all DHT sensors. @@ -53,7 +50,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ( s for s in data[CONF_DEVICES][device_id][CONF_SENSORS] - if s[CONF_TYPE] == "ds18b20" and s[CONF_PIN] == attrs.get(CONF_PIN) + if s[CONF_TYPE] == "ds18b20" and s[CONF_ZONE] == attrs.get(CONF_ZONE) ), None, ) @@ -85,10 +82,10 @@ class KonnectedSensor(Entity): self._data = data self._device_id = device_id self._type = sensor_type - self._pin_num = self._data.get(CONF_PIN) + self._zone_num = self._data.get(CONF_ZONE) self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] self._unique_id = addr or "{}-{}-{}".format( - device_id, self._pin_num, sensor_type + device_id, self._zone_num, sensor_type ) # set initial state if known at initialization @@ -99,7 +96,7 @@ class KonnectedSensor(Entity): # set entity name if given self._name = self._data.get(CONF_NAME) if self._name: - self._name += " " + SENSOR_TYPES[sensor_type][0] + self._name += f" {SENSOR_TYPES[sensor_type][0]}" @property def unique_id(self) -> str: @@ -121,6 +118,13 @@ class KonnectedSensor(Entity): """Return the unit of measurement.""" return self._unit_of_measurement + @property + def device_info(self): + """Return the device info.""" + return { + "identifiers": {(KONNECTED_DOMAIN, self._device_id)}, + } + async def async_added_to_hass(self): """Store entity_id and register state change callback.""" entity_id_key = self._addr or self._type diff --git a/homeassistant/components/konnected/strings.json b/homeassistant/components/konnected/strings.json new file mode 100644 index 00000000000..4d923238df4 --- /dev/null +++ b/homeassistant/components/konnected/strings.json @@ -0,0 +1,105 @@ +{ + "config": { + "title": "Konnected.io", + "step": { + "import_confirm": { + "title": "Import Konnected Device", + "description": "A Konnected Alarm Panel with ID {id} has been discovered in configuration.yaml. This flow will allow you to import it into a config entry." + }, + "user": { + "title": "Discover Konnected Device", + "description": "Please enter the host information for your Konnected Panel.", + "data": { + "host": "Konnected device IP address", + "port": "Konnected device port" + } + }, + "confirm": { + "title": "Konnected Device Ready", + "description": "Model: {model}\nID: {id}\nHost: {host}\nPort: {port}\n\nYou can configure the IO and panel behavior in the Konnected Alarm Panel settings." + } + }, + "error": { + "cannot_connect": "Unable to connect to a Konnected Panel at {host}:{port}" + }, + "abort": { + "unknown": "Unknown error occurred", + "already_configured": "Device is already configured", + "already_in_progress": "Config flow for device is already in progress.", + "not_konn_panel": "Not a recognized Konnected.io device" + } + }, + "options": { + "title": "Konnected Alarm Panel Options", + "step": { + "options_io": { + "title": "Configure I/O", + "description": "Discovered a {model} at {host}. Select the base configuration of each I/O below - depending on the I/O it may allow for binary sensors (open/close contacts), digital sensors (dht and ds18b20), or switchable outputs. You'll be able to configure detailed options in the next steps.", + "data": { + "1": "Zone 1", + "2": "Zone 2", + "3": "Zone 3", + "4": "Zone 4", + "5": "Zone 5", + "6": "Zone 6", + "7": "Zone 7", + "out": "OUT" + } + }, + "options_io_ext": { + "title": "Configure Extended I/O", + "description": "Select the configuration of the remaining I/O below. You'll be able to configure detailed options in the next steps.", + "data": { + "8": "Zone 8", + "9": "Zone 9", + "10": "Zone 10", + "11": "Zone 11", + "12": "Zone 12", + "out1": "OUT1", + "alarm1": "ALARM1", + "alarm2_out2": "OUT2/ALARM2" + } + }, + "options_binary": { + "title": "Configure Binary Sensor", + "description": "Please select the options for the binary sensor attached to {zone}", + "data": { + "type": "Binary Sensor Type", + "name": "Name (optional)", + "inverse": "Invert the open/close state" + } + }, + "options_digital": { + "title": "Configure Digital Sensor", + "description": "Please select the options for the digital sensor attached to {zone}", + "data": { + "type": "Sensor Type", + "name": "Name (optional)", + "poll_interval": "Poll Interval (minutes) (optional)" + } + }, + "options_switch": { + "title": "Configure Switchable Output", + "description": "Please select the output options for {zone}", + "data": { + "name": "Name (optional)", + "activation": "Output when on", + "momentary": "Pulse duration (ms) (optional)", + "pause": "Pause between pulses (ms) (optional)", + "repeat": "Times to repeat (-1=infinite) (optional)" + } + }, + "options_misc": { + "title": "Configure Misc", + "description": "Please select the desired behavior for your panel", + "data": { + "blink": "Blink panel LED on when sending state change" + } + } + }, + "error": {}, + "abort": { + "not_konn_panel": "Not a recognized Konnected.io device" + } + } +} diff --git a/homeassistant/components/konnected/switch.py b/homeassistant/components/konnected/switch.py index a88281826c0..d16051eb8da 100644 --- a/homeassistant/components/konnected/switch.py +++ b/homeassistant/components/konnected/switch.py @@ -5,12 +5,12 @@ from homeassistant.const import ( ATTR_STATE, CONF_DEVICES, CONF_NAME, - CONF_PIN, CONF_SWITCHES, + CONF_ZONE, ) from homeassistant.helpers.entity import ToggleEntity -from . import ( +from .const import ( CONF_ACTIVATION, CONF_MOMENTARY, CONF_PAUSE, @@ -23,16 +23,13 @@ from . import ( _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set switches attached to a Konnected device.""" - if discovery_info is None: - return - +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up switches attached to a Konnected device from a config entry.""" data = hass.data[KONNECTED_DOMAIN] - device_id = discovery_info["device_id"] + device_id = config_entry.data["id"] switches = [ - KonnectedSwitch(device_id, pin_data.get(CONF_PIN), pin_data) - for pin_data in data[CONF_DEVICES][device_id][CONF_SWITCHES] + KonnectedSwitch(device_id, zone_data.get(CONF_ZONE), zone_data) + for zone_data in data[CONF_DEVICES][device_id][CONF_SWITCHES] ] async_add_entities(switches) @@ -40,11 +37,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class KonnectedSwitch(ToggleEntity): """Representation of a Konnected switch.""" - def __init__(self, device_id, pin_num, data): + def __init__(self, device_id, zone_num, data): """Initialize the Konnected switch.""" self._data = data self._device_id = device_id - self._pin_num = pin_num + self._zone_num = zone_num self._activation = self._data.get(CONF_ACTIVATION, STATE_HIGH) self._momentary = self._data.get(CONF_MOMENTARY) self._pause = self._data.get(CONF_PAUSE) @@ -52,7 +49,7 @@ class KonnectedSwitch(ToggleEntity): self._state = self._boolean_state(self._data.get(ATTR_STATE)) self._name = self._data.get(CONF_NAME) self._unique_id = "{}-{}-{}-{}-{}".format( - device_id, self._pin_num, self._momentary, self._pause, self._repeat + device_id, self._zone_num, self._momentary, self._pause, self._repeat ) @property @@ -71,16 +68,22 @@ class KonnectedSwitch(ToggleEntity): return self._state @property - def client(self): + def panel(self): """Return the Konnected HTTP client.""" - return self.hass.data[KONNECTED_DOMAIN][CONF_DEVICES][self._device_id].get( - "client" - ) + device_data = self.hass.data[KONNECTED_DOMAIN][CONF_DEVICES][self._device_id] + return device_data.get("panel") - def turn_on(self, **kwargs): + @property + def device_info(self): + """Return the device info.""" + return { + "identifiers": {(KONNECTED_DOMAIN, self._device_id)}, + } + + async def async_turn_on(self, **kwargs): """Send a command to turn on the switch.""" - resp = self.client.put_device( - self._pin_num, + resp = await self.panel.update_switch( + self._zone_num, int(self._activation == STATE_HIGH), self._momentary, self._repeat, @@ -94,9 +97,11 @@ class KonnectedSwitch(ToggleEntity): # Immediately set the state back off for momentary switches self._set_state(False) - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Send a command to turn off the switch.""" - resp = self.client.put_device(self._pin_num, int(self._activation == STATE_LOW)) + resp = await self.panel.update_switch( + self._zone_num, int(self._activation == STATE_LOW) + ) if resp.get(ATTR_STATE) is not None: self._set_state(self._boolean_state(resp.get(ATTR_STATE))) @@ -111,9 +116,9 @@ class KonnectedSwitch(ToggleEntity): def _set_state(self, state): self._state = state - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() _LOGGER.debug( - "Setting status of %s actuator pin %s to %s", + "Setting status of %s actuator zone %s to %s", self._device_id, self.name, state, diff --git a/homeassistant/components/lastfm/sensor.py b/homeassistant/components/lastfm/sensor.py index 3a830b9f4e6..1a5b7a56e8e 100644 --- a/homeassistant/components/lastfm/sensor.py +++ b/homeassistant/components/lastfm/sensor.py @@ -58,7 +58,6 @@ class LastfmSensor(Entity): self._unique_id = hashlib.sha256(user.encode("utf-8")).hexdigest() self._user = lastfm_api.get_user(user) self._name = user - self._entity_id = user self._lastfm = lastfm_api self._state = "Not Scrobbling" self._playcount = None @@ -76,11 +75,6 @@ class LastfmSensor(Entity): """Return the name of the sensor.""" return self._name - @property - def entity_id(self): - """Return the entity ID.""" - return f"sensor.lastfm_{self._entity_id}" - @property def state(self): """Return the state of the sensor.""" diff --git a/homeassistant/components/lcn/services.py b/homeassistant/components/lcn/services.py index c35a0cc00bf..ba9d52b7721 100644 --- a/homeassistant/components/lcn/services.py +++ b/homeassistant/components/lcn/services.py @@ -178,7 +178,7 @@ class VarAbs(LcnServiceCall): """Set absolute value of a variable or setpoint. Variable has to be set as counter! - Reguator setpoints can also be set using R1VARSETPOINT, R2VARSETPOINT. + Regulator setpoints can also be set using R1VARSETPOINT, R2VARSETPOINT. """ schema = LcnServiceCall.schema.extend( diff --git a/homeassistant/components/lg_netcast/media_player.py b/homeassistant/components/lg_netcast/media_player.py index 0be51c337e8..cb91257f83d 100644 --- a/homeassistant/components/lg_netcast/media_player.py +++ b/homeassistant/components/lg_netcast/media_player.py @@ -1,5 +1,5 @@ """Support for LG TV running on NetCast 3 or 4.""" -from datetime import timedelta +from datetime import datetime, timedelta import logging from pylgnetcast import LgNetCastClient, LgNetCastError @@ -16,6 +16,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, ) @@ -28,11 +29,14 @@ from homeassistant.const import ( STATE_PLAYING, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.script import Script _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "LG TV Remote" +CONF_ON_ACTION = "turn_on_action" + MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) @@ -49,6 +53,7 @@ SUPPORT_LGTV = ( PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { + vol.Optional(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_ACCESS_TOKEN): vol.All(cv.string, vol.Length(max=6)), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -62,20 +67,23 @@ def setup_platform(hass, config, add_entities, discovery_info=None): host = config.get(CONF_HOST) access_token = config.get(CONF_ACCESS_TOKEN) name = config.get(CONF_NAME) + on_action = config.get(CONF_ON_ACTION) client = LgNetCastClient(host, access_token) + on_action_script = Script(hass, on_action) if on_action else None - add_entities([LgTVDevice(client, name)], True) + add_entities([LgTVDevice(client, name, on_action_script)], True) class LgTVDevice(MediaPlayerDevice): """Representation of a LG TV.""" - def __init__(self, client, name): + def __init__(self, client, name, on_action_script): """Initialize the LG TV device.""" self._client = client self._name = name self._muted = False + self._on_action_script = on_action_script # Assume that the TV is in Play mode self._playing = True self._volume = 0 @@ -112,6 +120,10 @@ class LgTVDevice(MediaPlayerDevice): channel_info = channel_info[0] self._channel_name = channel_info.find("chname").text self._program_name = channel_info.find("progName").text + if self._channel_name is None: + self._channel_name = channel_info.find("inputSourceName").text + if self._program_name is None: + self._program_name = channel_info.find("labelName").text channel_list = client.query_data("channel_list") if channel_list: @@ -180,17 +192,26 @@ class LgTVDevice(MediaPlayerDevice): @property def supported_features(self): """Flag media player features that are supported.""" + if self._on_action_script: + return SUPPORT_LGTV | SUPPORT_TURN_ON return SUPPORT_LGTV @property def media_image_url(self): """URL for obtaining a screen capture.""" - return self._client.url + "data?target=screen_image" + return ( + f"{self._client.url}data?target=screen_image&_={datetime.now().timestamp()}" + ) def turn_off(self): """Turn off media player.""" self.send_command(1) + def turn_on(self): + """Turn on the media player.""" + if self._on_action_script: + self._on_action_script.run() + def volume_up(self): """Volume up the media player.""" self.send_command(24) diff --git a/homeassistant/components/life360/.translations/hu.json b/homeassistant/components/life360/.translations/hu.json index 227e784b065..7f158a24622 100644 --- a/homeassistant/components/life360/.translations/hu.json +++ b/homeassistant/components/life360/.translations/hu.json @@ -1,7 +1,16 @@ { "config": { "error": { + "invalid_username": "\u00c9rv\u00e9nytelen felhaszn\u00e1l\u00f3n\u00e9v", "unexpected": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt a kommunik\u00e1ci\u00f3ban a Life360 szerverrel" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/pl.json b/homeassistant/components/life360/.translations/pl.json index e9cd9920304..f82c8325828 100644 --- a/homeassistant/components/life360/.translations/pl.json +++ b/homeassistant/components/life360/.translations/pl.json @@ -2,7 +2,7 @@ "config": { "abort": { "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce", - "user_already_configured": "Konto jest ju\u017c skonfigurowane" + "user_already_configured": "Konto jest ju\u017c skonfigurowane." }, "create_entry": { "default": "Aby skonfigurowa\u0107 zaawansowane ustawienia, zapoznaj si\u0119 z [dokumentacj\u0105 Life360]({docs_url})." @@ -11,7 +11,7 @@ "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce", "invalid_username": "Nieprawid\u0142owa nazwa u\u017cytkownika", "unexpected": "Nieoczekiwany b\u0142\u0105d komunikacji z serwerem Life360", - "user_already_configured": "Konto jest ju\u017c skonfigurowane" + "user_already_configured": "Konto jest ju\u017c skonfigurowane." }, "step": { "user": { diff --git a/homeassistant/components/life360/.translations/sv.json b/homeassistant/components/life360/.translations/sv.json index 836680aad6a..ba28d973ec3 100644 --- a/homeassistant/components/life360/.translations/sv.json +++ b/homeassistant/components/life360/.translations/sv.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "Ogiltiga autentiseringsuppgifter", "invalid_username": "Ogiltigt anv\u00e4ndarnmn", + "unexpected": "Ov\u00e4ntat fel vid kommunikation med Life360-servern", "user_already_configured": "Konto har redan konfigurerats" }, "step": { diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 4e845a07854..5bc0c1bc53b 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -39,6 +39,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, ENTITY_MATCH_ALL, + ENTITY_MATCH_NONE, EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import callback @@ -374,6 +375,9 @@ class LIFXManager: async def async_service_to_entities(self, service): """Return the known entities that a service call mentions.""" + if service.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_NONE: + return [] + if service.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL: return self.entities.values() diff --git a/homeassistant/components/light/.translations/sv.json b/homeassistant/components/light/.translations/sv.json new file mode 100644 index 00000000000..8df3f3d382b --- /dev/null +++ b/homeassistant/components/light/.translations/sv.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "V\u00e4xla {entity_name}", + "turn_off": "St\u00e4ng av {entity_name}", + "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} \u00e4r avst\u00e4ngd", + "is_on": "{entity_name} \u00e4r p\u00e5" + }, + "trigger_type": { + "turned_off": "{entity_name} avst\u00e4ngd", + "turned_on": "{entity_name} slogs p\u00e5" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 791f7328cf8..5b9b923cc56 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -1,5 +1,4 @@ """Provides functionality to interact with lights.""" -import asyncio import csv from datetime import timedelta import logging @@ -8,15 +7,12 @@ from typing import Dict, Optional, Tuple import voluptuous as vol -from homeassistant.auth.permissions.const import POLICY_CONTROL from homeassistant.const import ( - ATTR_ENTITY_ID, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.exceptions import Unauthorized, UnknownUser import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, @@ -61,6 +57,8 @@ ATTR_WHITE_VALUE = "white_value" # Brightness of the light, 0..255 or percentage ATTR_BRIGHTNESS = "brightness" ATTR_BRIGHTNESS_PCT = "brightness_pct" +ATTR_BRIGHTNESS_STEP = "brightness_step" +ATTR_BRIGHTNESS_STEP_PCT = "brightness_step_pct" # String representing a profile (built-in ones or external defined). ATTR_PROFILE = "profile" @@ -87,12 +85,16 @@ LIGHT_PROFILES_FILE = "light_profiles.csv" VALID_TRANSITION = vol.All(vol.Coerce(float), vol.Clamp(min=0, max=6553)) VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)) VALID_BRIGHTNESS_PCT = vol.All(vol.Coerce(float), vol.Range(min=0, max=100)) +VALID_BRIGHTNESS_STEP = vol.All(vol.Coerce(int), vol.Clamp(min=-255, max=255)) +VALID_BRIGHTNESS_STEP_PCT = vol.All(vol.Coerce(float), vol.Clamp(min=-100, max=100)) LIGHT_TURN_ON_SCHEMA = { vol.Exclusive(ATTR_PROFILE, COLOR_GROUP): cv.string, ATTR_TRANSITION: VALID_TRANSITION, - ATTR_BRIGHTNESS: VALID_BRIGHTNESS, - ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, + vol.Exclusive(ATTR_BRIGHTNESS, ATTR_BRIGHTNESS): VALID_BRIGHTNESS, + vol.Exclusive(ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_PCT, + vol.Exclusive(ATTR_BRIGHTNESS_STEP, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_STEP, + vol.Exclusive(ATTR_BRIGHTNESS_STEP_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_STEP_PCT, vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string, vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): vol.All( vol.ExactSequence((cv.byte, cv.byte, cv.byte)), vol.Coerce(tuple) @@ -169,7 +171,7 @@ def preprocess_turn_off(params): """Process data for turning light off if brightness is 0.""" if ATTR_BRIGHTNESS in params and params[ATTR_BRIGHTNESS] == 0: # Zero brightness: Light will be turned off - params = {k: v for k, v in params.items() if k in [ATTR_TRANSITION, ATTR_FLASH]} + params = {k: v for k, v in params.items() if k in (ATTR_TRANSITION, ATTR_FLASH)} return (True, params) # Light should be turned off return (False, None) # Light should be turned on @@ -187,70 +189,65 @@ async def async_setup(hass, config): if not profiles_valid: return False - async def async_handle_light_on_service(service): - """Handle a turn light on service call.""" - # Get the validated data - params = service.data.copy() + def preprocess_data(data): + """Preprocess the service data.""" + base = {} - # Convert the entity ids to valid light ids - target_lights = await component.async_extract_from_service(service) - params.pop(ATTR_ENTITY_ID, None) + for entity_field in cv.ENTITY_SERVICE_FIELDS: + if entity_field in data: + base[entity_field] = data.pop(entity_field) - if service.context.user_id: - user = await hass.auth.async_get_user(service.context.user_id) - if user is None: - raise UnknownUser(context=service.context) + preprocess_turn_on_alternatives(data) + turn_lights_off, off_params = preprocess_turn_off(data) - entity_perms = user.permissions.check_entity + base["params"] = data + base["turn_lights_off"] = turn_lights_off + base["off_params"] = off_params - for light in target_lights: - if not entity_perms(light, POLICY_CONTROL): - raise Unauthorized( - context=service.context, - entity_id=light, - permission=POLICY_CONTROL, - ) + return base - preprocess_turn_on_alternatives(params) - turn_lights_off, off_params = preprocess_turn_off(params) + async def async_handle_light_on_service(light, call): + """Handle turning a light on. - poll_lights = [] - change_tasks = [] - for light in target_lights: - light.async_set_context(service.context) + If brightness is set to 0, this service will turn the light off. + """ + params = call.data["params"] + turn_light_off = call.data["turn_lights_off"] + off_params = call.data["off_params"] + + if not params: + default_profile = Profiles.get_default(light.entity_id) + + if default_profile is not None: + params = {ATTR_PROFILE: default_profile} + preprocess_turn_on_alternatives(params) + turn_light_off, off_params = preprocess_turn_off(params) + + elif ATTR_BRIGHTNESS_STEP in params or ATTR_BRIGHTNESS_STEP_PCT in params: + brightness = light.brightness if light.is_on else 0 + + params = params.copy() + + if ATTR_BRIGHTNESS_STEP in params: + brightness += params.pop(ATTR_BRIGHTNESS_STEP) - pars = params - off_pars = off_params - turn_light_off = turn_lights_off - if not pars: - pars = params.copy() - pars[ATTR_PROFILE] = Profiles.get_default(light.entity_id) - preprocess_turn_on_alternatives(pars) - turn_light_off, off_pars = preprocess_turn_off(pars) - if turn_light_off: - task = light.async_request_call(light.async_turn_off(**off_pars)) else: - task = light.async_request_call(light.async_turn_on(**pars)) + brightness += int(params.pop(ATTR_BRIGHTNESS_STEP_PCT) / 100 * 255) - change_tasks.append(task) + params[ATTR_BRIGHTNESS] = max(0, min(255, brightness)) + turn_light_off, off_params = preprocess_turn_off(params) - if light.should_poll: - poll_lights.append(light) - - 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] - ) + if turn_light_off: + await light.async_turn_off(**off_params) + else: + await light.async_turn_on(**params) # Listen for light on and light off service calls. - hass.services.async_register( - DOMAIN, + + component.async_register_entity_service( SERVICE_TURN_ON, + vol.All(cv.make_entity_service_schema(LIGHT_TURN_ON_SCHEMA), preprocess_data), async_handle_light_on_service, - schema=cv.make_entity_service_schema(LIGHT_TURN_ON_SCHEMA), ) component.async_register_entity_service( diff --git a/homeassistant/components/light/device_action.py b/homeassistant/components/light/device_action.py index c436ce7886a..99f5b6b12bc 100644 --- a/homeassistant/components/light/device_action.py +++ b/homeassistant/components/light/device_action.py @@ -4,13 +4,32 @@ from typing import List import voluptuous as vol from homeassistant.components.device_automation import toggle_entity -from homeassistant.const import CONF_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + CONF_DOMAIN, + CONF_TYPE, + SERVICE_TURN_ON, +) from homeassistant.core import Context, HomeAssistant +from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.helpers.typing import ConfigType, TemplateVarsType -from . import DOMAIN +from . import ATTR_BRIGHTNESS_STEP_PCT, DOMAIN, SUPPORT_BRIGHTNESS -ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN}) +TYPE_BRIGHTNESS_INCREASE = "brightness_increase" +TYPE_BRIGHTNESS_DECREASE = "brightness_decrease" + +ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(CONF_DOMAIN): DOMAIN, + vol.Required(CONF_TYPE): vol.In( + toggle_entity.DEVICE_ACTION_TYPES + + [TYPE_BRIGHTNESS_INCREASE, TYPE_BRIGHTNESS_DECREASE] + ), + } +) async def async_call_action_from_config( @@ -20,11 +39,57 @@ async def async_call_action_from_config( context: Context, ) -> None: """Change state based on configuration.""" - await toggle_entity.async_call_action_from_config( - hass, config, variables, context, DOMAIN + if config[CONF_TYPE] in toggle_entity.DEVICE_ACTION_TYPES: + await toggle_entity.async_call_action_from_config( + hass, config, variables, context, DOMAIN + ) + return + + data = {ATTR_ENTITY_ID: config[ATTR_ENTITY_ID]} + + if config[CONF_TYPE] == TYPE_BRIGHTNESS_INCREASE: + data[ATTR_BRIGHTNESS_STEP_PCT] = 10 + else: + data[ATTR_BRIGHTNESS_STEP_PCT] = -10 + + await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, data, blocking=True, context=context ) async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: """List device actions.""" - return await toggle_entity.async_get_actions(hass, device_id, DOMAIN) + actions = await toggle_entity.async_get_actions(hass, device_id, DOMAIN) + + registry = await entity_registry.async_get_registry(hass) + + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + state = hass.states.get(entry.entity_id) + + if state: + supported_features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + else: + supported_features = entry.supported_features + + if supported_features & SUPPORT_BRIGHTNESS: + actions.extend( + ( + { + CONF_TYPE: TYPE_BRIGHTNESS_INCREASE, + "device_id": device_id, + "entity_id": entry.entity_id, + "domain": DOMAIN, + }, + { + CONF_TYPE: TYPE_BRIGHTNESS_DECREASE, + "device_id": device_id, + "entity_id": entry.entity_id, + "domain": DOMAIN, + }, + ) + ) + + return actions diff --git a/homeassistant/components/light/intent.py b/homeassistant/components/light/intent.py index ea8899c44fc..c172ac1330a 100644 --- a/homeassistant/components/light/intent.py +++ b/homeassistant/components/light/intent.py @@ -1,6 +1,7 @@ """Intents for the light integration.""" import voluptuous as vol +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.helpers import intent import homeassistant.helpers.config_validation as cv @@ -8,7 +9,6 @@ import homeassistant.util.color as color_util from . import ( ATTR_BRIGHTNESS_PCT, - ATTR_ENTITY_ID, ATTR_RGB_COLOR, DOMAIN, SERVICE_TURN_ON, diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 449e5ea5aaf..a2b71f5632b 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -36,6 +36,12 @@ turn_on: 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 + brightness_step: + description: Change brightness by an amount. Should be between -255..255. + example: -25.5 + brightness_step_pct: + description: Change brightness by a percentage. Should be between -100..100. + example: -10 profile: description: Name of a light profile to use. example: relax diff --git a/homeassistant/components/linky/.translations/hu.json b/homeassistant/components/linky/.translations/hu.json index 436e8b1fb7d..f5c5f788063 100644 --- a/homeassistant/components/linky/.translations/hu.json +++ b/homeassistant/components/linky/.translations/hu.json @@ -2,6 +2,19 @@ "config": { "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "access": "Nem siker\u00fclt el\u00e9rni az Enedis.fr webhelyet, ellen\u0151rizze internet-kapcsolat\u00e1t", + "enedis": "Az Enedis.fr hib\u00e1val v\u00e1laszolt: k\u00e9rj\u00fck, pr\u00f3b\u00e1lkozzon k\u00e9s\u0151bb \u00fajra (\u00e1ltal\u00e1ban nem 23:00 \u00e9s 2:00 k\u00f6z\u00f6tt)", + "unknown": "Ismeretlen hiba: pr\u00f3b\u00e1lkozzon k\u00e9s\u0151bb (\u00e1ltal\u00e1ban nem 23:00 \u00e9s 2:00 \u00f3ra k\u00f6z\u00f6tt)" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "E-mail" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/nl.json b/homeassistant/components/linky/.translations/nl.json index ecc566c8b87..5654edb08f4 100644 --- a/homeassistant/components/linky/.translations/nl.json +++ b/homeassistant/components/linky/.translations/nl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Account al geconfigureerd" + }, "error": { "access": "Geen toegang tot Enedis.fr, controleer uw internetverbinding", "enedis": "Enedis.fr antwoordde met een fout: probeer het later opnieuw (meestal niet tussen 23.00 en 02.00 uur)", diff --git a/homeassistant/components/linky/.translations/pl.json b/homeassistant/components/linky/.translations/pl.json index 51f96dcf17a..62da10e1c96 100644 --- a/homeassistant/components/linky/.translations/pl.json +++ b/homeassistant/components/linky/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Konto jest ju\u017c skonfigurowane" + "already_configured": "Konto jest ju\u017c skonfigurowane." }, "error": { "access": "Nie mo\u017cna uzyska\u0107 dost\u0119pu do Enedis.fr, sprawd\u017a po\u0142\u0105czenie internetowe", diff --git a/homeassistant/components/linky/.translations/sl.json b/homeassistant/components/linky/.translations/sl.json index ab5e054db1e..6ebe598e882 100644 --- a/homeassistant/components/linky/.translations/sl.json +++ b/homeassistant/components/linky/.translations/sl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ra\u010dun \u017ee nastavljen" + }, "error": { "access": "Do Enedis.fr ni bilo mogo\u010de dostopati, preverite internetno povezavo", "enedis": "Enedis.fr je odgovoril z napako: poskusite pozneje (ponavadi med 23. in 2. uro)", diff --git a/homeassistant/components/linky/.translations/sv.json b/homeassistant/components/linky/.translations/sv.json index 4e7be709482..4880e065fa2 100644 --- a/homeassistant/components/linky/.translations/sv.json +++ b/homeassistant/components/linky/.translations/sv.json @@ -2,6 +2,23 @@ "config": { "abort": { "already_configured": "Kontot har redan konfigurerats." - } + }, + "error": { + "access": "Det gick inte att komma \u00e5t Enedis.fr, kontrollera din internetanslutning", + "enedis": "Enedis.fr svarade med ett fel: f\u00f6rs\u00f6k igen senare (vanligtvis inte mellan 23:00 och 02:00)", + "unknown": "Ok\u00e4nt fel: f\u00f6rs\u00f6k igen senare (vanligtvis inte mellan 23:00 och 02:00)", + "wrong_login": "Inloggningsfel: v\u00e4nligen kontrollera din e-post och l\u00f6senord" + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "username": "E-post" + }, + "description": "Ange dina autentiseringsuppgifter", + "title": "Linky" + } + }, + "title": "Linky" } } \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/zh-Hans.json b/homeassistant/components/linky/.translations/zh-Hans.json index 2c6b3ba34b5..62138856078 100644 --- a/homeassistant/components/linky/.translations/zh-Hans.json +++ b/homeassistant/components/linky/.translations/zh-Hans.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "wrong_login": "\u767b\u5f55\u51fa\u9519\uff1a\u8bf7\u68c0\u67e5\u60a8\u7684\u7535\u5b50\u90ae\u7bb1\u548c\u5bc6\u7801" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/linky/sensor.py b/homeassistant/components/linky/sensor.py index 9beb9acc403..846b7eeb99f 100644 --- a/homeassistant/components/linky/sensor.py +++ b/homeassistant/components/linky/sensor.py @@ -13,6 +13,7 @@ from homeassistant.const import ( CONF_USERNAME, ENERGY_KILO_WATT_HOUR, ) +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import HomeAssistantType @@ -73,6 +74,7 @@ class LinkyAccount: _LOGGER.debug(json.dumps(self._data, indent=2)) except PyLinkyException as exp: _LOGGER.error(exp) + raise PlatformNotReady finally: client.close_session() @@ -146,6 +148,9 @@ class LinkySensor(Entity): async def async_update(self) -> None: """Retrieve the new data for the sensor.""" + if self._account.data is None: + return + data = self._account.data[self._scale][self._when] self._consumption = data[CONSUMPTION] self._time = data[TIME] diff --git a/homeassistant/components/liveboxplaytv/__init__.py b/homeassistant/components/liveboxplaytv/__init__.py deleted file mode 100644 index 384c0e4c34b..00000000000 --- a/homeassistant/components/liveboxplaytv/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The liveboxplaytv component.""" diff --git a/homeassistant/components/liveboxplaytv/manifest.json b/homeassistant/components/liveboxplaytv/manifest.json deleted file mode 100644 index a05ff27ca90..00000000000 --- a/homeassistant/components/liveboxplaytv/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "liveboxplaytv", - "name": "Orange Livebox Play TV", - "documentation": "https://www.home-assistant.io/integrations/liveboxplaytv", - "requirements": ["liveboxplaytv==2.0.3", "pyteleloisirs==3.6"], - "dependencies": [], - "codeowners": ["@pschmitt"] -} diff --git a/homeassistant/components/liveboxplaytv/media_player.py b/homeassistant/components/liveboxplaytv/media_player.py deleted file mode 100644 index 66fb383d677..00000000000 --- a/homeassistant/components/liveboxplaytv/media_player.py +++ /dev/null @@ -1,273 +0,0 @@ -"""Support for interface with an Orange Livebox Play TV appliance.""" -from datetime import timedelta -import logging - -from liveboxplaytv import LiveboxPlayTv -import pyteleloisirs -import requests -import voluptuous as vol - -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice -from homeassistant.components.media_player.const import ( - MEDIA_TYPE_CHANNEL, - SUPPORT_NEXT_TRACK, - SUPPORT_PAUSE, - SUPPORT_PLAY, - SUPPORT_PREVIOUS_TRACK, - SUPPORT_SELECT_SOURCE, - SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, - SUPPORT_VOLUME_STEP, -) -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PORT, - STATE_OFF, - STATE_ON, - STATE_PAUSED, - STATE_PLAYING, -) -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = "Livebox Play TV" -DEFAULT_PORT = 8080 - -SUPPORT_LIVEBOXPLAYTV = ( - SUPPORT_TURN_OFF - | SUPPORT_TURN_ON - | SUPPORT_NEXT_TRACK - | SUPPORT_PAUSE - | SUPPORT_PREVIOUS_TRACK - | SUPPORT_VOLUME_STEP - | SUPPORT_VOLUME_MUTE - | SUPPORT_SELECT_SOURCE - | SUPPORT_PLAY -) - -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Orange Livebox Play TV platform.""" - host = config.get(CONF_HOST) - port = config.get(CONF_PORT) - name = config.get(CONF_NAME) - - livebox_devices = [] - - try: - device = LiveboxPlayTvDevice(host, port, name) - livebox_devices.append(device) - except OSError: - _LOGGER.error( - "Failed to connect to Livebox Play TV at %s:%s. " - "Please check your configuration", - host, - port, - ) - async_add_entities(livebox_devices, True) - - -class LiveboxPlayTvDevice(MediaPlayerDevice): - """Representation of an Orange Livebox Play TV.""" - - def __init__(self, host, port, name): - """Initialize the Livebox Play TV device.""" - - self._client = LiveboxPlayTv(host, port) - # Assume that the appliance is not muted - self._muted = False - self._name = name - self._current_source = None - self._state = None - self._channel_list = {} - self._current_channel = None - self._current_program = None - self._media_duration = None - self._media_remaining_time = None - self._media_image_url = None - self._media_last_updated = None - - async def async_update(self): - """Retrieve the latest data.""" - - try: - self._state = self.refresh_state() - # Update channel list - self.refresh_channel_list() - # Update current channel - channel = self._client.channel - if channel is not None: - self._current_channel = channel - program = await self._client.async_get_current_program() - if program and self._current_program != program.get("name"): - self._current_program = program.get("name") - # Media progress info - self._media_duration = pyteleloisirs.get_program_duration(program) - rtime = pyteleloisirs.get_remaining_time(program) - if rtime != self._media_remaining_time: - self._media_remaining_time = rtime - self._media_last_updated = dt_util.utcnow() - # Set media image to current program if a thumbnail is - # available. Otherwise we'll use the channel's image. - img_size = 800 - prg_img_url = await self._client.async_get_current_program_image( - img_size - ) - if prg_img_url: - self._media_image_url = prg_img_url - else: - chan_img_url = self._client.get_current_channel_image(img_size) - self._media_image_url = chan_img_url - except requests.ConnectionError: - self._state = None - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def is_volume_muted(self): - """Boolean if volume is currently muted.""" - return self._muted - - @property - def source(self): - """Return the current input source.""" - return self._current_channel - - @property - def source_list(self): - """List of available input sources.""" - # Sort channels by tvIndex - return [self._channel_list[c] for c in sorted(self._channel_list.keys())] - - @property - def media_content_type(self): - """Content type of current playing media.""" - # return self._client.media_type - return MEDIA_TYPE_CHANNEL - - @property - def media_image_url(self): - """Image url of current playing media.""" - return self._media_image_url - - @property - def media_title(self): - """Title of current playing media.""" - if self._current_channel: - if self._current_program: - return f"{self._current_channel}: {self._current_program}" - return self._current_channel - - @property - def media_duration(self): - """Duration of current playing media in seconds.""" - return self._media_duration - - @property - def media_position(self): - """Position of current playing media in seconds.""" - return self._media_remaining_time - - @property - def media_position_updated_at(self): - """When was the position of the current playing media valid. - - Returns value from homeassistant.util.dt.utcnow(). - """ - return self._media_last_updated - - @property - def supported_features(self): - """Flag media player features that are supported.""" - return SUPPORT_LIVEBOXPLAYTV - - def refresh_channel_list(self): - """Refresh the list of available channels.""" - new_channel_list = {} - # update channels - for channel in self._client.get_channels(): - new_channel_list[int(channel["index"])] = channel["name"] - self._channel_list = new_channel_list - - def refresh_state(self): - """Refresh the current media state.""" - state = self._client.media_state - if state == "PLAY": - return STATE_PLAYING - if state == "PAUSE": - return STATE_PAUSED - - return STATE_ON if self._client.is_on else STATE_OFF - - def turn_off(self): - """Turn off media player.""" - self._state = STATE_OFF - self._client.turn_off() - - def turn_on(self): - """Turn on the media player.""" - self._state = STATE_ON - self._client.turn_on() - - def volume_up(self): - """Volume up the media player.""" - self._client.volume_up() - - def volume_down(self): - """Volume down media player.""" - self._client.volume_down() - - def mute_volume(self, mute): - """Send mute command.""" - self._muted = mute - self._client.mute() - - def media_play_pause(self): - """Simulate play pause media player.""" - self._client.play_pause() - - def select_source(self, source): - """Select input source.""" - self._current_source = source - self._client.set_channel(source) - - def media_play(self): - """Send play command.""" - self._state = STATE_PLAYING - self._client.play() - - def media_pause(self): - """Send media pause command to media player.""" - self._state = STATE_PAUSED - self._client.pause() - - def media_next_track(self): - """Send next track command.""" - self._client.channel_up() - - def media_previous_track(self): - """Send the previous track command.""" - self._client.channel_down() diff --git a/homeassistant/components/local_ip/.translations/hu.json b/homeassistant/components/local_ip/.translations/hu.json new file mode 100644 index 00000000000..7a78029c379 --- /dev/null +++ b/homeassistant/components/local_ip/.translations/hu.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Az integr\u00e1ci\u00f3 m\u00e1r konfigur\u00e1lva van egy ilyen nev\u0171 l\u00e9tez\u0151 \u00e9rz\u00e9kel\u0151vel" + }, + "step": { + "user": { + "data": { + "name": "\u00c9rz\u00e9kel\u0151 neve" + }, + "title": "Helyi IP c\u00edm" + } + }, + "title": "Helyi IP c\u00edm" + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/.translations/nl.json b/homeassistant/components/local_ip/.translations/nl.json index 4f0d9a437db..6f22d2c585a 100644 --- a/homeassistant/components/local_ip/.translations/nl.json +++ b/homeassistant/components/local_ip/.translations/nl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Integratie is al geconfigureerd met een bestaande sensor met die naam" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/local_ip/.translations/pl.json b/homeassistant/components/local_ip/.translations/pl.json index a4032eeebd1..82b614a8e17 100644 --- a/homeassistant/components/local_ip/.translations/pl.json +++ b/homeassistant/components/local_ip/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Integracja jest ju\u017c skonfigurowana z istniej\u0105cym sensorem o tej nazwie" + "already_configured": "Integracja jest ju\u017c skonfigurowana z istniej\u0105cym sensorem o tej nazwie." }, "step": { "user": { diff --git a/homeassistant/components/local_ip/.translations/ru.json b/homeassistant/components/local_ip/.translations/ru.json index de92b9680f0..2cf8791e505 100644 --- a/homeassistant/components/local_ip/.translations/ru.json +++ b/homeassistant/components/local_ip/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430 \u0441 \u0442\u0430\u043a\u0438\u043c \u0436\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c \u0434\u0430\u0442\u0447\u0438\u043a\u0430." + "already_configured": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430 \u0441 \u0442\u0430\u043a\u0438\u043c \u0436\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c." }, "step": { "user": { diff --git a/homeassistant/components/local_ip/.translations/sv.json b/homeassistant/components/local_ip/.translations/sv.json new file mode 100644 index 00000000000..d9f9b474f9c --- /dev/null +++ b/homeassistant/components/local_ip/.translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Integrationen \u00e4r redan konfigurerad med en befintlig sensor med det namnet" + }, + "step": { + "user": { + "data": { + "name": "Sensor Namn" + }, + "title": "Lokal IP-adress" + } + }, + "title": "Lokal IP-adress" + } +} \ No newline at end of file diff --git a/homeassistant/components/locative/.translations/pl.json b/homeassistant/components/locative/.translations/pl.json index 9c22a8e3fea..23a4c98a54c 100644 --- a/homeassistant/components/locative/.translations/pl.json +++ b/homeassistant/components/locative/.translations/pl.json @@ -10,7 +10,7 @@ "step": { "user": { "description": "Na pewno chcesz skonfigurowa\u0107 Locative Webhook?", - "title": "Skonfiguruj Locative Webhook" + "title": "Konfiguracja Locative Webhook" } }, "title": "Locative Webhook" diff --git a/homeassistant/components/lock/.translations/sv.json b/homeassistant/components/lock/.translations/sv.json new file mode 100644 index 00000000000..7d50b4ea61a --- /dev/null +++ b/homeassistant/components/lock/.translations/sv.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "lock": "L\u00e5s {entity_name}", + "open": "\u00d6ppna {entity_name}", + "unlock": "L\u00e5s upp {entity_name}" + }, + "condition_type": { + "is_locked": "{entity_name} \u00e4r l\u00e5st", + "is_unlocked": "{entity_name} \u00e4r ol\u00e5st" + }, + "trigger_type": { + "locked": "{entity_name} l\u00e5st", + "unlocked": "{entity_name} ol\u00e5st" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/logger/__init__.py b/homeassistant/components/logger/__init__.py index 8043469d43b..961718a30ee 100644 --- a/homeassistant/components/logger/__init__.py +++ b/homeassistant/components/logger/__init__.py @@ -1,4 +1,4 @@ -"""Support for settting the level of logging for components.""" +"""Support for setting the level of logging for components.""" from collections import OrderedDict import logging diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 6c99356907f..fc8cb67894b 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -89,6 +89,9 @@ class LovelaceStorage: async def async_load(self, force): """Load config.""" + if self._hass.config.safe_mode: + raise ConfigNotFound + if self._data is None: await self._load() diff --git a/homeassistant/components/luftdaten/.translations/pl.json b/homeassistant/components/luftdaten/.translations/pl.json index 5a2c30db44c..19e71b5156f 100644 --- a/homeassistant/components/luftdaten/.translations/pl.json +++ b/homeassistant/components/luftdaten/.translations/pl.json @@ -3,7 +3,7 @@ "error": { "communication_error": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z API Luftdaten", "invalid_sensor": "Sensor niedost\u0119pny lub nieprawid\u0142owy", - "sensor_exists": "Sensor zosta\u0142 ju\u017c zarejestrowany" + "sensor_exists": "Sensor zosta\u0142 ju\u017c zarejestrowany." }, "step": { "user": { diff --git a/homeassistant/components/luftdaten/.translations/ru.json b/homeassistant/components/luftdaten/.translations/ru.json index 1a05137f82d..759fd926bdc 100644 --- a/homeassistant/components/luftdaten/.translations/ru.json +++ b/homeassistant/components/luftdaten/.translations/ru.json @@ -2,14 +2,14 @@ "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.", - "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." + "invalid_sensor": "\u0421\u0435\u043d\u0441\u043e\u0440 \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": "\u0421\u0435\u043d\u0441\u043e\u0440 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d." }, "step": { "user": { "data": { "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043d\u0430 \u043a\u0430\u0440\u0442\u0435", - "station_id": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0434\u0430\u0442\u0447\u0438\u043a\u0430 Luftdaten" + "station_id": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0441\u0435\u043d\u0441\u043e\u0440\u0430 Luftdaten" }, "title": "Luftdaten" } diff --git a/homeassistant/components/lutron_caseta/.translations/hu.json b/homeassistant/components/lutron_caseta/.translations/hu.json new file mode 100644 index 00000000000..cfc3c290afe --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/hu.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/sv.json b/homeassistant/components/lutron_caseta/.translations/sv.json new file mode 100644 index 00000000000..cfc3c290afe --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/sv.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/maxcube/__init__.py b/homeassistant/components/maxcube/__init__.py index 1b65cb161e1..3e6ecbc948b 100644 --- a/homeassistant/components/maxcube/__init__.py +++ b/homeassistant/components/maxcube/__init__.py @@ -90,14 +90,14 @@ class MaxCubeHandle: self.cube = cube self.scan_interval = scan_interval self.mutex = Lock() - self._updatets = time.time() + self._updatets = time.monotonic() def update(self): """Pull the latest data from the MAX! Cube.""" # Acquire mutex to prevent simultaneous update from multiple threads with self.mutex: # Only update every update_interval - if (time.time() - self._updatets) >= self.scan_interval: + if (time.monotonic() - self._updatets) >= self.scan_interval: _LOGGER.debug("Updating") try: @@ -106,6 +106,6 @@ class MaxCubeHandle: _LOGGER.error("Max!Cube connection failed") return False - self._updatets = time.time() + self._updatets = time.monotonic() else: _LOGGER.debug("Skipping update") diff --git a/homeassistant/components/mcp23017/binary_sensor.py b/homeassistant/components/mcp23017/binary_sensor.py index e95b91389cd..59f268e657c 100644 --- a/homeassistant/components/mcp23017/binary_sensor.py +++ b/homeassistant/components/mcp23017/binary_sensor.py @@ -1,7 +1,7 @@ """Support for binary sensor using I2C MCP23017 chip.""" import logging -import adafruit_mcp230xx # pylint: disable=import-error +from adafruit_mcp230xx.mcp23017 import MCP23017 # pylint: disable=import-error import board # pylint: disable=import-error import busio # pylint: disable=import-error import digitalio # pylint: disable=import-error @@ -46,7 +46,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): i2c_address = config[CONF_I2C_ADDRESS] i2c = busio.I2C(board.SCL, board.SDA) - mcp = adafruit_mcp230xx.MCP23017(i2c, address=i2c_address) + mcp = MCP23017(i2c, address=i2c_address) binary_sensors = [] pins = config[CONF_PINS] diff --git a/homeassistant/components/mcp23017/manifest.json b/homeassistant/components/mcp23017/manifest.json index ebf796fe3db..8bdd897d34e 100644 --- a/homeassistant/components/mcp23017/manifest.json +++ b/homeassistant/components/mcp23017/manifest.json @@ -4,8 +4,8 @@ "documentation": "https://www.home-assistant.io/integrations/mcp23017", "requirements": [ "RPi.GPIO==0.7.0", - "adafruit-blinka==1.2.1", - "adafruit-circuitpython-mcp230xx==1.1.2" + "adafruit-blinka==3.9.0", + "adafruit-circuitpython-mcp230xx==2.2.2" ], "dependencies": [], "codeowners": ["@jardiamj"] diff --git a/homeassistant/components/mcp23017/switch.py b/homeassistant/components/mcp23017/switch.py index 8506106b705..fe76f4ce632 100644 --- a/homeassistant/components/mcp23017/switch.py +++ b/homeassistant/components/mcp23017/switch.py @@ -1,7 +1,7 @@ """Support for switch sensor using I2C MCP23017 chip.""" import logging -import adafruit_mcp230xx # pylint: disable=import-error +from adafruit_mcp230xx.mcp23017 import MCP23017 # pylint: disable=import-error import board # pylint: disable=import-error import busio # pylint: disable=import-error import digitalio # pylint: disable=import-error @@ -39,7 +39,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): i2c_address = config.get(CONF_I2C_ADDRESS) i2c = busio.I2C(board.SCL, board.SDA) - mcp = adafruit_mcp230xx.MCP23017(i2c, address=i2c_address) + mcp = MCP23017(i2c, address=i2c_address) switches = [] pins = config.get(CONF_PINS) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 28bbc92b850..815a00c5223 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -2,7 +2,7 @@ "domain": "media_extractor", "name": "Media Extractor", "documentation": "https://www.home-assistant.io/integrations/media_extractor", - "requirements": ["youtube_dl==2020.01.24"], + "requirements": ["youtube_dl==2020.02.16"], "dependencies": ["media_player"], "codeowners": [], "quality_scale": "internal" diff --git a/homeassistant/components/media_player/.translations/hu.json b/homeassistant/components/media_player/.translations/hu.json new file mode 100644 index 00000000000..fbefbc43e08 --- /dev/null +++ b/homeassistant/components/media_player/.translations/hu.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} t\u00e9tlen", + "is_off": "{entity_name} ki van kapcsolva", + "is_on": "{entity_name} be van kapcsolva", + "is_paused": "{entity_name} sz\u00fcneteltetve van", + "is_playing": "{entity_name} lej\u00e1tszik" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 2911a143a3c..8a31dbe6bdb 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -766,7 +766,7 @@ class MediaPlayerDevice(Entity): @property def capability_attributes(self): - """Return capabilitiy attributes.""" + """Return capability attributes.""" supported_features = self.supported_features or 0 data = {} diff --git a/homeassistant/components/melcloud/.translations/ca.json b/homeassistant/components/melcloud/.translations/ca.json new file mode 100644 index 00000000000..1dc5156f7e7 --- /dev/null +++ b/homeassistant/components/melcloud/.translations/ca.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "La integraci\u00f3 MELCloud ja est\u00e0 configurada amb aquest correu electr\u00f2nic. El testimoni d'acc\u00e9s s'ha actualitzat." + }, + "error": { + "cannot_connect": "No s'ha pogut connectar, torna-ho a provar", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya de MELCloud.", + "username": "Correu electr\u00f2nic d'inici de sessi\u00f3 a MELCloud." + }, + "description": "Connecta\u2019t amb el teu compte de MELCloud.", + "title": "Connexi\u00f3 amb MELCloud" + } + }, + "title": "MELCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/.translations/da.json b/homeassistant/components/melcloud/.translations/da.json new file mode 100644 index 00000000000..6901ed22934 --- /dev/null +++ b/homeassistant/components/melcloud/.translations/da.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "MELCloud-integration er allerede konfigureret for denne e-mail. Adgangstoken er blevet opdateret." + }, + "error": { + "cannot_connect": "Kunne ikke oprette forbindelse. Pr\u00f8v igen", + "invalid_auth": "Ugyldig godkendelse", + "unknown": "Uventet fejl" + }, + "step": { + "user": { + "data": { + "password": "MELCloud-adgangskode.", + "username": "E-mail, der bruges til at logge ind p\u00e5 MELCloud." + }, + "description": "Opret forbindelse ved hj\u00e6lp af din MELCloud-konto.", + "title": "Opret forbindelse til MELCloud" + } + }, + "title": "MELCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/.translations/de.json b/homeassistant/components/melcloud/.translations/de.json new file mode 100644 index 00000000000..f4e2a3b1ebc --- /dev/null +++ b/homeassistant/components/melcloud/.translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Die MELCloud-Integration ist bereits f\u00fcr diese E-Mail konfiguriert. Das Zugriffstoken wurde aktualisiert." + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen. Bitte versuchen Sie es erneut.", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "password": "MELCloud Passwort.", + "username": "E-Mail-Adresse f\u00fcr die Anmeldung bei MELCloud." + }, + "description": "Verbinden Sie sich mit Ihrem MELCloud-Konto.", + "title": "Stellen Sie eine Verbindung zu MELCloud her" + } + }, + "title": "MELCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/.translations/en.json b/homeassistant/components/melcloud/.translations/en.json new file mode 100644 index 00000000000..48682f617a3 --- /dev/null +++ b/homeassistant/components/melcloud/.translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "MELCloud integration already configured for this email. Access token has been refreshed." + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "password": "MELCloud password.", + "username": "Email used to login to MELCloud." + }, + "description": "Connect using your MELCloud account.", + "title": "Connect to MELCloud" + } + }, + "title": "MELCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/.translations/es.json b/homeassistant/components/melcloud/.translations/es.json new file mode 100644 index 00000000000..182f06c33c3 --- /dev/null +++ b/homeassistant/components/melcloud/.translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Integraci\u00f3n mELCloud ya configurada para este correo electr\u00f3nico. Se ha actualizado el token de acceso." + }, + "error": { + "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntelo de nuevo.", + "invalid_auth": "Autentificaci\u00f3n inv\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a de MELCloud.", + "username": "Correo electr\u00f3nico utilizado para iniciar sesi\u00f3n en MELCloud." + }, + "description": "Con\u00e9ctate usando tu cuenta de MELCloud.", + "title": "Con\u00e9ctese a MELCloud" + } + }, + "title": "MELCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/.translations/fr.json b/homeassistant/components/melcloud/.translations/fr.json new file mode 100644 index 00000000000..e442325d9dc --- /dev/null +++ b/homeassistant/components/melcloud/.translations/fr.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "username": "E-mail utilis\u00e9e pour vous connecter \u00e0 MELCloud." + }, + "description": "Se connecter en utilisant votre MELCloud compte.", + "title": "Se connecter \u00e0 MELCloud" + } + }, + "title": "MELCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/.translations/it.json b/homeassistant/components/melcloud/.translations/it.json new file mode 100644 index 00000000000..029fc2526b2 --- /dev/null +++ b/homeassistant/components/melcloud/.translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Integrazione MELCloud gi\u00e0 configurata per questa e-mail. Il token di accesso \u00e8 stato aggiornato." + }, + "error": { + "cannot_connect": "Impossibile connettersi, si prega di riprovare.", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "password": "Password MELCloud.", + "username": "Email utilizzata per accedere a MELCloud." + }, + "description": "Connettiti utilizzando il tuo account MELCloud.", + "title": "Connettersi a MELCloud" + } + }, + "title": "MELCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/.translations/ko.json b/homeassistant/components/melcloud/.translations/ko.json new file mode 100644 index 00000000000..1557abf5a32 --- /dev/null +++ b/homeassistant/components/melcloud/.translations/ko.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\uc774 \uc774\uba54\uc77c\uc5d0 \ub300\ud55c MELCloud \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uac31\uc2e0\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "password": "MELCloud \uc758 \ube44\ubc00\ubc88\ud638\ub97c \ub123\uc5b4\uc8fc\uc138\uc694.", + "username": "MELCloud \ub85c\uadf8\uc778 \uc774\uba54\uc77c \uc8fc\uc18c\ub97c \ub123\uc5b4\uc8fc\uc138\uc694." + }, + "description": "MELCloud \uacc4\uc815\uc73c\ub85c \uc5f0\uacb0\ud558\uc138\uc694.", + "title": "MELCloud \uc5f0\uacb0" + } + }, + "title": "MELCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/.translations/lb.json b/homeassistant/components/melcloud/.translations/lb.json new file mode 100644 index 00000000000..b082ef78965 --- /dev/null +++ b/homeassistant/components/melcloud/.translations/lb.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "MELCloud Integratioun ass scho konfigur\u00e9iert fir d\u00ebs Email. Acc\u00e8s Jeton gouf erneiert." + }, + "error": { + "cannot_connect": "Feeler beim verbannen, prob\u00e9iert w.e.g. nach emol.", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "password": "MELCloud Passwuert", + "username": "Email d\u00e9i benotz g\u00ebtt fir sech mat MELCloud ze verbannen" + }, + "description": "Verbann dech mat dengem MElCloud Kont.", + "title": "Mat MELCloud verbannen" + } + }, + "title": "MELCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/.translations/nl.json b/homeassistant/components/melcloud/.translations/nl.json new file mode 100644 index 00000000000..b60495e7f47 --- /dev/null +++ b/homeassistant/components/melcloud/.translations/nl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "MELCloud integratie is al geconfigureerd voor deze e-mail. Toegangstoken is vernieuwd." + }, + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "password": "MELCloud wachtwoord.", + "username": "E-mail gebruikt om in te loggen op MELCloud." + }, + "description": "Maak verbinding via uw MELCloud account.", + "title": "Maak verbinding met MELCloud" + } + }, + "title": "MELCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/.translations/no.json b/homeassistant/components/melcloud/.translations/no.json new file mode 100644 index 00000000000..a464bbfda19 --- /dev/null +++ b/homeassistant/components/melcloud/.translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "MELCloud integrasjon er allerede konfigurert p\u00e5 denne e-posten. Access token har blitt oppdatert." + }, + "error": { + "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "password": "MELCloud passord.", + "username": "E-post som blir brukt til \u00e5 logge inn p\u00e5 MELCloud." + }, + "description": "Koble til ved hjelp av MELCloud-kontoen din.", + "title": "Koble til MELCloud" + } + }, + "title": "MELCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/.translations/pl.json b/homeassistant/components/melcloud/.translations/pl.json new file mode 100644 index 00000000000..9abb68ca85a --- /dev/null +++ b/homeassistant/components/melcloud/.translations/pl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Integracja MELCloud jest ju\u017c skonfigurowana dla tego adresu e-mail. Token dost\u0119pu zosta\u0142 od\u015bwie\u017cony." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", + "invalid_auth": "Niepoprawne uwierzytelnienie.", + "unknown": "Niespodziewany b\u0142\u0105d." + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o MELCloud.", + "username": "Adres e-mail u\u017cywany do logowania do MELCloud" + }, + "description": "Po\u0142\u0105cz u\u017cywaj\u0105c swojego konta MELCloud.", + "title": "Po\u0142\u0105cz si\u0119 z MELCloud" + } + }, + "title": "MELCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/.translations/ru.json b/homeassistant/components/melcloud/.translations/ru.json new file mode 100644 index 00000000000..d4bab0e417e --- /dev/null +++ b/homeassistant/components/melcloud/.translations/ru.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f MELCloud \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0430\u0434\u0440\u0435\u0441\u0430 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b. \u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c MELCloud.", + "username": "\u042d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0430\u044f \u043f\u043e\u0447\u0442\u0430, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0430\u044f \u0434\u043b\u044f \u0432\u0445\u043e\u0434\u0430 \u0432 MELCloud." + }, + "description": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0435\u0441\u044c, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0441\u0432\u043e\u044e \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c MELCloud.", + "title": "MELCloud" + } + }, + "title": "MELCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/.translations/sl.json b/homeassistant/components/melcloud/.translations/sl.json new file mode 100644 index 00000000000..04dbb953d0d --- /dev/null +++ b/homeassistant/components/melcloud/.translations/sl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Za to e-po\u0161to je \u017ee konfigurirana integracija MELCloud. \u017deton za dostop je bil osve\u017een." + }, + "error": { + "cannot_connect": "Povezava ni uspela, poskusite znova", + "invalid_auth": "Neveljavna avtentikacija", + "unknown": "Nepri\u010dakovana napaka" + }, + "step": { + "user": { + "data": { + "password": "MELCloud geslo.", + "username": "E-po\u0161tni naslov za prijavo v MELCloud." + }, + "description": "Pove\u017eite se s svojim ra\u010dunom MELCloud.", + "title": "Pove\u017eite se z MELCloud" + } + }, + "title": "MELCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/.translations/sv.json b/homeassistant/components/melcloud/.translations/sv.json new file mode 100644 index 00000000000..72a251ef9d0 --- /dev/null +++ b/homeassistant/components/melcloud/.translations/sv.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "MELCloud-integration redan konfigurerad f\u00f6r den h\u00e4r e-postadressen. \u00c5tkomsttoken har uppdaterats." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "password": "MELCloud-l\u00f6senord.", + "username": "E-post som anv\u00e4nds f\u00f6r att logga in p\u00e5 MELCloud." + }, + "description": "Anslut med ditt MELCloud-konto.", + "title": "Anslut till MELCloud" + } + }, + "title": "MELCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/.translations/zh-Hant.json b/homeassistant/components/melcloud/.translations/zh-Hant.json new file mode 100644 index 00000000000..c098d041598 --- /dev/null +++ b/homeassistant/components/melcloud/.translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u5df2\u4f7f\u7528\u6b64\u90f5\u4ef6\u8a2d\u5b9a MELCloud \u6574\u5408\u3002\u5b58\u53d6\u5bc6\u9470\u5df2\u66f4\u65b0\u3002" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "password": "MELCloud \u5bc6\u78bc\u3002", + "username": "MELCloud \u767b\u5165\u90f5\u4ef6\u3002" + }, + "description": "\u4f7f\u7528 MELCloud \u5e33\u865f\u9032\u884c\u9023\u7dda\u3002", + "title": "\u9023\u7dda\u81f3 MELCloud" + } + }, + "title": "MELCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py new file mode 100644 index 00000000000..ef932f36aa4 --- /dev/null +++ b/homeassistant/components/melcloud/__init__.py @@ -0,0 +1,160 @@ +"""The MELCloud Climate integration.""" +import asyncio +from datetime import timedelta +import logging +from typing import Any, Dict, List + +from aiohttp import ClientConnectionError +from async_timeout import timeout +from pymelcloud import Device, get_devices +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_TOKEN, CONF_USERNAME +from homeassistant.exceptions import ConfigEntryNotReady +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util import Throttle + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) + +PLATFORMS = ["climate", "sensor"] + +CONF_LANGUAGE = "language" +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_TOKEN): cv.string, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass: HomeAssistantType, config: ConfigEntry): + """Establish connection with MELCloud.""" + if DOMAIN not in config: + return True + + username = config[DOMAIN][CONF_USERNAME] + token = config[DOMAIN][CONF_TOKEN] + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_USERNAME: username, CONF_TOKEN: token}, + ) + ) + return True + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Establish connection with MELClooud.""" + conf = entry.data + mel_devices = await mel_devices_setup(hass, conf[CONF_TOKEN]) + hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: mel_devices}) + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + return True + + +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS + ] + ) + hass.data[DOMAIN].pop(config_entry.entry_id) + if not hass.data[DOMAIN]: + hass.data.pop(DOMAIN) + return True + + +class MelCloudDevice: + """MELCloud Device instance.""" + + def __init__(self, device: Device): + """Construct a device wrapper.""" + self.device = device + self.name = device.name + self._available = True + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def async_update(self, **kwargs): + """Pull the latest data from MELCloud.""" + try: + await self.device.update() + self._available = True + except ClientConnectionError: + _LOGGER.warning("Connection failed for %s", self.name) + self._available = False + + async def async_set(self, properties: Dict[str, Any]): + """Write state changes to the MELCloud API.""" + try: + await self.device.set(properties) + self._available = True + except ClientConnectionError: + _LOGGER.warning("Connection failed for %s", self.name) + self._available = False + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + + @property + def device_id(self): + """Return device ID.""" + return self.device.device_id + + @property + def building_id(self): + """Return building ID of the device.""" + return self.device.building_id + + @property + def device_info(self): + """Return a device description for device registry.""" + _device_info = { + "identifiers": {(DOMAIN, f"{self.device.mac}-{self.device.serial}")}, + "manufacturer": "Mitsubishi Electric", + "name": self.name, + } + unit_infos = self.device.units + if unit_infos is not None: + _device_info["model"] = ", ".join( + [x["model"] for x in unit_infos if x["model"]] + ) + return _device_info + + +async def mel_devices_setup(hass, token) -> List[MelCloudDevice]: + """Query connected devices from MELCloud.""" + session = hass.helpers.aiohttp_client.async_get_clientsession() + try: + with timeout(10): + all_devices = await get_devices( + token, + session, + conf_update_interval=timedelta(minutes=5), + device_set_debounce=timedelta(seconds=1), + ) + except (asyncio.TimeoutError, ClientConnectionError) as ex: + raise ConfigEntryNotReady() from ex + + wrapped_devices = {} + for device_type, devices in all_devices.items(): + wrapped_devices[device_type] = [MelCloudDevice(device) for device in devices] + return wrapped_devices diff --git a/homeassistant/components/melcloud/climate.py b/homeassistant/components/melcloud/climate.py new file mode 100644 index 00000000000..95cb1489f45 --- /dev/null +++ b/homeassistant/components/melcloud/climate.py @@ -0,0 +1,171 @@ +"""Platform for climate integration.""" +from datetime import timedelta +import logging +from typing import List, Optional + +from pymelcloud import DEVICE_TYPE_ATA + +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + DEFAULT_MAX_TEMP, + DEFAULT_MIN_TEMP, + HVAC_MODE_OFF, + SUPPORT_FAN_MODE, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import TEMP_CELSIUS +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util.temperature import convert as convert_temperature + +from . import MelCloudDevice +from .const import DOMAIN, HVAC_MODE_LOOKUP, HVAC_MODE_REVERSE_LOOKUP, TEMP_UNIT_LOOKUP + +SCAN_INTERVAL = timedelta(seconds=60) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +): + """Set up MelCloud device climate based on config_entry.""" + mel_devices = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + [AtaDeviceClimate(mel_device) for mel_device in mel_devices[DEVICE_TYPE_ATA]], + True, + ) + + +class AtaDeviceClimate(ClimateDevice): + """Air-to-Air climate device.""" + + def __init__(self, device: MelCloudDevice): + """Initialize the climate.""" + self._api = device + self._device = self._api.device + self._name = device.name + + @property + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return f"{self._device.serial}-{self._device.mac}" + + @property + def name(self): + """Return the display name of this light.""" + return self._name + + async def async_update(self): + """Update state from MELCloud.""" + await self._api.async_update() + + @property + def device_info(self): + """Return a device description for device registry.""" + return self._api.device_info + + @property + def temperature_unit(self) -> str: + """Return the unit of measurement used by the platform.""" + return TEMP_UNIT_LOOKUP.get(self._device.temp_unit, TEMP_CELSIUS) + + @property + def hvac_mode(self) -> str: + """Return hvac operation ie. heat, cool mode.""" + mode = self._device.operation_mode + if not self._device.power or mode is None: + return HVAC_MODE_OFF + return HVAC_MODE_LOOKUP.get(mode) + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set new target hvac mode.""" + if hvac_mode == HVAC_MODE_OFF: + await self._device.set({"power": False}) + return + + operation_mode = HVAC_MODE_REVERSE_LOOKUP.get(hvac_mode) + if operation_mode is None: + raise ValueError(f"Invalid hvac_mode [{hvac_mode}]") + + props = {"operation_mode": operation_mode} + if self.hvac_mode == HVAC_MODE_OFF: + props["power"] = True + await self._device.set(props) + + @property + def hvac_modes(self) -> List[str]: + """Return the list of available hvac operation modes.""" + return [HVAC_MODE_OFF] + [ + HVAC_MODE_LOOKUP.get(mode) for mode in self._device.operation_modes + ] + + @property + def current_temperature(self) -> Optional[float]: + """Return the current temperature.""" + return self._device.room_temperature + + @property + def target_temperature(self) -> Optional[float]: + """Return the temperature we try to reach.""" + return self._device.target_temperature + + async def async_set_temperature(self, **kwargs) -> None: + """Set new target temperature.""" + await self._device.set( + {"target_temperature": kwargs.get("temperature", self.target_temperature)} + ) + + @property + def target_temperature_step(self) -> Optional[float]: + """Return the supported step of target temperature.""" + return self._device.target_temperature_step + + @property + def fan_mode(self) -> Optional[str]: + """Return the fan setting.""" + return self._device.fan_speed + + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set new target fan mode.""" + await self._device.set({"fan_speed": fan_mode}) + + @property + def fan_modes(self) -> Optional[List[str]]: + """Return the list of available fan modes.""" + return self._device.fan_speeds + + async def async_turn_on(self) -> None: + """Turn the entity on.""" + await self._device.set({"power": True}) + + async def async_turn_off(self) -> None: + """Turn the entity off.""" + await self._device.set({"power": False}) + + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_FAN_MODE | SUPPORT_TARGET_TEMPERATURE + + @property + def min_temp(self) -> float: + """Return the minimum temperature.""" + min_value = self._device.target_temperature_min + if min_value is not None: + return min_value + + return convert_temperature( + DEFAULT_MIN_TEMP, TEMP_CELSIUS, self.temperature_unit + ) + + @property + def max_temp(self) -> float: + """Return the maximum temperature.""" + max_value = self._device.target_temperature_max + if max_value is not None: + return max_value + + return convert_temperature( + DEFAULT_MAX_TEMP, TEMP_CELSIUS, self.temperature_unit + ) diff --git a/homeassistant/components/melcloud/config_flow.py b/homeassistant/components/melcloud/config_flow.py new file mode 100644 index 00000000000..6bda8cc3c28 --- /dev/null +++ b/homeassistant/components/melcloud/config_flow.py @@ -0,0 +1,84 @@ +"""Config flow for the MELCloud platform.""" +import asyncio +import logging +from typing import Optional + +from aiohttp import ClientError, ClientResponseError +from async_timeout import timeout +import pymelcloud +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME + +from .const import DOMAIN # pylint: disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def _create_entry(self, username: str, token: str): + """Register new entry.""" + await self.async_set_unique_id(username) + self._abort_if_unique_id_configured({CONF_TOKEN: token}) + return self.async_create_entry( + title=username, data={CONF_USERNAME: username, CONF_TOKEN: token}, + ) + + async def _create_client( + self, + username: str, + *, + password: Optional[str] = None, + token: Optional[str] = None, + ): + """Create client.""" + if password is None and token is None: + raise ValueError( + "Invalid internal state. Called without either password or token", + ) + + try: + with timeout(10): + acquired_token = token + if acquired_token is None: + acquired_token = await pymelcloud.login( + username, + password, + self.hass.helpers.aiohttp_client.async_get_clientsession(), + ) + await pymelcloud.get_devices( + acquired_token, + self.hass.helpers.aiohttp_client.async_get_clientsession(), + ) + except ClientResponseError as err: + if err.status == 401 or err.status == 403: + return self.async_abort(reason="invalid_auth") + return self.async_abort(reason="cannot_connect") + except (asyncio.TimeoutError, ClientError): + return self.async_abort(reason="cannot_connect") + + return await self._create_entry(username, acquired_token) + + async def async_step_user(self, user_input=None): + """User initiated config flow.""" + if user_input is None: + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} + ), + ) + username = user_input[CONF_USERNAME] + return await self._create_client(username, password=user_input[CONF_PASSWORD]) + + async def async_step_import(self, user_input): + """Import a config entry.""" + return await self._create_client( + user_input[CONF_USERNAME], token=user_input[CONF_TOKEN] + ) diff --git a/homeassistant/components/melcloud/const.py b/homeassistant/components/melcloud/const.py new file mode 100644 index 00000000000..e262be2c3fb --- /dev/null +++ b/homeassistant/components/melcloud/const.py @@ -0,0 +1,29 @@ +"""Constants for the MELCloud Climate integration.""" +import pymelcloud.ata_device as ata_device +from pymelcloud.const import UNIT_TEMP_CELSIUS, UNIT_TEMP_FAHRENHEIT + +from homeassistant.components.climate.const import ( + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, +) +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT + +DOMAIN = "melcloud" + +HVAC_MODE_LOOKUP = { + ata_device.OPERATION_MODE_HEAT: HVAC_MODE_HEAT, + ata_device.OPERATION_MODE_DRY: HVAC_MODE_DRY, + ata_device.OPERATION_MODE_COOL: HVAC_MODE_COOL, + ata_device.OPERATION_MODE_FAN_ONLY: HVAC_MODE_FAN_ONLY, + ata_device.OPERATION_MODE_HEAT_COOL: HVAC_MODE_HEAT_COOL, +} +HVAC_MODE_REVERSE_LOOKUP = {v: k for k, v in HVAC_MODE_LOOKUP.items()} + +TEMP_UNIT_LOOKUP = { + UNIT_TEMP_CELSIUS: TEMP_CELSIUS, + UNIT_TEMP_FAHRENHEIT: TEMP_FAHRENHEIT, +} +TEMP_UNIT_REVERSE_LOOKUP = {v: k for k, v in TEMP_UNIT_LOOKUP.items()} diff --git a/homeassistant/components/melcloud/manifest.json b/homeassistant/components/melcloud/manifest.json new file mode 100644 index 00000000000..55edcdd0d9f --- /dev/null +++ b/homeassistant/components/melcloud/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "melcloud", + "name": "MELCloud", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/melcloud", + "requirements": ["pymelcloud==2.1.0"], + "dependencies": [], + "codeowners": ["@vilppuvuorinen"] +} diff --git a/homeassistant/components/melcloud/sensor.py b/homeassistant/components/melcloud/sensor.py new file mode 100644 index 00000000000..8f55906443e --- /dev/null +++ b/homeassistant/components/melcloud/sensor.py @@ -0,0 +1,102 @@ +"""Support for MelCloud device sensors.""" +import logging + +from pymelcloud import DEVICE_TYPE_ATA + +from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS +from homeassistant.helpers.entity import Entity + +from . import MelCloudDevice +from .const import DOMAIN, TEMP_UNIT_LOOKUP + +ATTR_MEASUREMENT_NAME = "measurement_name" +ATTR_ICON = "icon" +ATTR_UNIT_FN = "unit_fn" +ATTR_DEVICE_CLASS = "device_class" +ATTR_VALUE_FN = "value_fn" +ATTR_ENABLED_FN = "enabled" + +SENSORS = { + "room_temperature": { + ATTR_MEASUREMENT_NAME: "Room Temperature", + ATTR_ICON: "mdi:thermometer", + ATTR_UNIT_FN: lambda x: TEMP_UNIT_LOOKUP.get(x.device.temp_unit, TEMP_CELSIUS), + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_VALUE_FN: lambda x: x.device.room_temperature, + ATTR_ENABLED_FN: lambda x: True, + }, + "energy": { + ATTR_MEASUREMENT_NAME: "Energy", + ATTR_ICON: "mdi:factory", + ATTR_UNIT_FN: lambda x: "kWh", + ATTR_DEVICE_CLASS: None, + ATTR_VALUE_FN: lambda x: x.device.total_energy_consumed, + ATTR_ENABLED_FN: lambda x: x.device.has_energy_consumed_meter, + }, +} + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up MELCloud device sensors based on config_entry.""" + mel_devices = hass.data[DOMAIN].get(entry.entry_id) + async_add_entities( + [ + MelCloudSensor(mel_device, measurement, definition) + for measurement, definition in SENSORS.items() + for mel_device in mel_devices[DEVICE_TYPE_ATA] + if definition[ATTR_ENABLED_FN](mel_device) + ], + True, + ) + + +class MelCloudSensor(Entity): + """Representation of a Sensor.""" + + def __init__(self, device: MelCloudDevice, measurement, definition): + """Initialize the sensor.""" + self._api = device + self._name_slug = device.name + self._measurement = measurement + self._def = definition + + @property + def unique_id(self): + """Return a unique ID.""" + return f"{self._api.device.serial}-{self._api.device.mac}-{self._measurement}" + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return self._def[ATTR_ICON] + + @property + def name(self): + """Return the name of the sensor.""" + return f"{self._name_slug} {self._def[ATTR_MEASUREMENT_NAME]}" + + @property + def state(self): + """Return the state of the sensor.""" + return self._def[ATTR_VALUE_FN](self._api) + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._def[ATTR_UNIT_FN](self._api) + + @property + def device_class(self): + """Return device class.""" + return self._def[ATTR_DEVICE_CLASS] + + async def async_update(self): + """Retrieve latest state.""" + await self._api.async_update() + + @property + def device_info(self): + """Return a device description for device registry.""" + return self._api.device_info diff --git a/homeassistant/components/melcloud/strings.json b/homeassistant/components/melcloud/strings.json new file mode 100644 index 00000000000..477ca7eb5e2 --- /dev/null +++ b/homeassistant/components/melcloud/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "title": "MELCloud", + "step": { + "user": { + "title": "Connect to MELCloud", + "description": "Connect using your MELCloud account.", + "data": { + "username": "Email used to login to MELCloud.", + "password": "MELCloud password." + } + } + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "MELCloud integration already configured for this email. Access token has been refreshed." + } + } +} diff --git a/homeassistant/components/met/.translations/pl.json b/homeassistant/components/met/.translations/pl.json index f647dcf7b45..e22ac763d56 100644 --- a/homeassistant/components/met/.translations/pl.json +++ b/homeassistant/components/met/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Lokalizacja ju\u017c istnieje" + "name_exists": "Lokalizacja ju\u017c istnieje." }, "step": { "user": { diff --git a/homeassistant/components/met/.translations/sv.json b/homeassistant/components/met/.translations/sv.json index aa860e27307..d8b461913da 100644 --- a/homeassistant/components/met/.translations/sv.json +++ b/homeassistant/components/met/.translations/sv.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Namnet finns redan" + "name_exists": "Plats finns redan" }, "step": { "user": { diff --git a/homeassistant/components/met/config_flow.py b/homeassistant/components/met/config_flow.py index 759f7f6fc89..683390429c3 100644 --- a/homeassistant/components/met/config_flow.py +++ b/homeassistant/components/met/config_flow.py @@ -12,15 +12,15 @@ from .const import CONF_TRACK_HOME, DOMAIN, HOME_LOCATION_NAME @callback def configured_instances(hass): """Return a set of configured SimpliSafe instances.""" - entites = [] + entries = [] for entry in hass.config_entries.async_entries(DOMAIN): if entry.data.get("track_home"): - entites.append("home") + entries.append("home") continue - entites.append( + entries.append( f"{entry.data.get(CONF_LATITUDE)}-{entry.data.get(CONF_LONGITUDE)}" ) - return set(entites) + return set(entries) class MetFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/meteo_france/.translations/ca.json b/homeassistant/components/meteo_france/.translations/ca.json index 6f2fd707045..aeceb80a063 100644 --- a/homeassistant/components/meteo_france/.translations/ca.json +++ b/homeassistant/components/meteo_france/.translations/ca.json @@ -1,10 +1,15 @@ { "config": { + "abort": { + "already_configured": "Ciutat ja configurada", + "unknown": "Error desconegut: torna-ho a provar m\u00e9s tard" + }, "step": { "user": { "data": { "city": "Ciutat" }, + "description": "Introdueix el codi postal (nom\u00e9s recomanat per Fran\u00e7a) o nom de la ciutat", "title": "M\u00e9t\u00e9o-France" } }, diff --git a/homeassistant/components/meteo_france/.translations/de.json b/homeassistant/components/meteo_france/.translations/de.json index 0e99c1de0ce..8f05ad18df3 100644 --- a/homeassistant/components/meteo_france/.translations/de.json +++ b/homeassistant/components/meteo_france/.translations/de.json @@ -9,6 +9,7 @@ "data": { "city": "Stadt" }, + "description": "Geben Sie die Postleitzahl (nur f\u00fcr Frankreich empfohlen) oder den St\u00e4dtenamen ein", "title": "M\u00e9t\u00e9o-France" } }, diff --git a/homeassistant/components/meteo_france/.translations/es.json b/homeassistant/components/meteo_france/.translations/es.json new file mode 100644 index 00000000000..3cd7ee56252 --- /dev/null +++ b/homeassistant/components/meteo_france/.translations/es.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "La ciudad ya est\u00e1 configurada", + "unknown": "Error desconocido: por favor, vuelva a intentarlo m\u00e1s tarde" + }, + "step": { + "user": { + "data": { + "city": "Ciudad" + }, + "description": "Introduzca el c\u00f3digo postal (solo para Francia, recomendado) o el nombre de la ciudad", + "title": "M\u00e9t\u00e9o-France" + } + }, + "title": "M\u00e9t\u00e9o-France" + } +} \ No newline at end of file diff --git a/homeassistant/components/meteo_france/.translations/fr.json b/homeassistant/components/meteo_france/.translations/fr.json new file mode 100644 index 00000000000..7dff0d237fd --- /dev/null +++ b/homeassistant/components/meteo_france/.translations/fr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Ville d\u00e9j\u00e0 configur\u00e9e", + "unknown": "Erreur inconnue: veuillez r\u00e9essayer plus tard" + }, + "step": { + "user": { + "data": { + "city": "Ville" + }, + "description": "Entrez le code postal (uniquement pour la France, recommand\u00e9) ou le nom de la ville", + "title": "M\u00e9t\u00e9o-France" + } + }, + "title": "M\u00e9t\u00e9o-France" + } +} \ No newline at end of file diff --git a/homeassistant/components/meteo_france/.translations/hu.json b/homeassistant/components/meteo_france/.translations/hu.json new file mode 100644 index 00000000000..f1719f4bf30 --- /dev/null +++ b/homeassistant/components/meteo_france/.translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "A v\u00e1ros m\u00e1r konfigur\u00e1lva van", + "unknown": "Ismeretlen hiba: k\u00e9rj\u00fck, pr\u00f3b\u00e1lja \u00fajra k\u00e9s\u0151bb" + }, + "step": { + "user": { + "data": { + "city": "V\u00e1ros" + }, + "description": "\u00cdrja be az ir\u00e1ny\u00edt\u00f3sz\u00e1mot (csak Franciaorsz\u00e1g eset\u00e9ben aj\u00e1nlott) vagy a v\u00e1ros nev\u00e9t", + "title": "M\u00e9t\u00e9o-France" + } + }, + "title": "M\u00e9t\u00e9o-France" + } +} \ No newline at end of file diff --git a/homeassistant/components/meteo_france/.translations/it.json b/homeassistant/components/meteo_france/.translations/it.json new file mode 100644 index 00000000000..5a067430906 --- /dev/null +++ b/homeassistant/components/meteo_france/.translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Citt\u00e0 gi\u00e0 configurata", + "unknown": "Errore sconosciuto: riprovare pi\u00f9 tardi" + }, + "step": { + "user": { + "data": { + "city": "Citt\u00e0" + }, + "description": "Inserisci il codice postale (solo per la Francia, consigliato) o il nome della citt\u00e0", + "title": "M\u00e9t\u00e9o-France" + } + }, + "title": "M\u00e9t\u00e9o-France" + } +} \ No newline at end of file diff --git a/homeassistant/components/meteo_france/.translations/nl.json b/homeassistant/components/meteo_france/.translations/nl.json new file mode 100644 index 00000000000..648ef0c5fbd --- /dev/null +++ b/homeassistant/components/meteo_france/.translations/nl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Stad al geconfigureerd", + "unknown": "Onbekende fout: probeer het later nog eens" + }, + "step": { + "user": { + "data": { + "city": "Stad" + }, + "description": "Vul de postcode (alleen voor Frankrijk, aanbevolen) of de plaatsnaam in", + "title": "M\u00e9t\u00e9o-France" + } + }, + "title": "M\u00e9t\u00e9o-France" + } +} \ No newline at end of file diff --git a/homeassistant/components/meteo_france/.translations/pl.json b/homeassistant/components/meteo_france/.translations/pl.json index a519eaead5d..38aa1944fac 100644 --- a/homeassistant/components/meteo_france/.translations/pl.json +++ b/homeassistant/components/meteo_france/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Miasto jest ju\u017c skonfigurowane", + "already_configured": "Miasto jest ju\u017c skonfigurowane.", "unknown": "Nieznany b\u0142\u0105d: spr\u00f3buj ponownie p\u00f3\u017aniej" }, "step": { diff --git a/homeassistant/components/meteo_france/.translations/sl.json b/homeassistant/components/meteo_france/.translations/sl.json new file mode 100644 index 00000000000..845a89c4775 --- /dev/null +++ b/homeassistant/components/meteo_france/.translations/sl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Mesto je \u017ee konfigurirano", + "unknown": "Neznana napaka: poskusite pozneje" + }, + "step": { + "user": { + "data": { + "city": "Mesto" + }, + "description": "Vnesite po\u0161tno \u0161tevilko (samo za Francijo) ali ime mesta", + "title": "M\u00e9t\u00e9o-France" + } + }, + "title": "M\u00e9t\u00e9o-France" + } +} \ No newline at end of file diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index 73b8dbb0e39..b7eda51b955 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -1,4 +1,5 @@ """Support for Meteo-France weather data.""" +import asyncio import datetime import logging @@ -6,116 +7,96 @@ from meteofrance.client import meteofranceClient, meteofranceError from vigilancemeteo import VigilanceMeteoError, VigilanceMeteoFranceProxy import voluptuous as vol -from homeassistant.const import CONF_MONITORED_CONDITIONS +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util import Throttle -from .const import CONF_CITY, DATA_METEO_FRANCE, DOMAIN, SENSOR_TYPES +from .const import CONF_CITY, DOMAIN, PLATFORMS _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = datetime.timedelta(minutes=5) -def has_all_unique_cities(value): - """Validate that all cities are unique.""" - cities = [location[CONF_CITY] for location in value] - vol.Schema(vol.Unique())(cities) - return value - +CITY_SCHEMA = vol.Schema({vol.Required(CONF_CITY): cv.string}) CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.All( - cv.ensure_list, - [ - vol.Schema( - { - vol.Required(CONF_CITY): cv.string, - vol.Optional(CONF_MONITORED_CONDITIONS): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] - ), - } - ) - ], - has_all_unique_cities, - ) - }, - extra=vol.ALLOW_EXTRA, + {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [CITY_SCHEMA]))}, extra=vol.ALLOW_EXTRA, ) -def setup(hass, config): - """Set up the Meteo-France component.""" - hass.data[DATA_METEO_FRANCE] = {} +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: + """Set up Meteo-France from legacy config file.""" - # Check if at least weather alert have to be monitored for one location. - need_weather_alert_watcher = False - for location in config[DOMAIN]: - if ( - CONF_MONITORED_CONDITIONS in location - and "weather_alert" in location[CONF_MONITORED_CONDITIONS] - ): - need_weather_alert_watcher = True + conf = config.get(DOMAIN) + if conf is None: + return True - # If weather alert monitoring is expected initiate a client to be used by - # all weather_alert entities. - if need_weather_alert_watcher: - _LOGGER.debug("Weather Alert monitoring expected. Loading vigilancemeteo") - - weather_alert_client = VigilanceMeteoFranceProxy() - try: - weather_alert_client.update_data() - except VigilanceMeteoError as exp: - _LOGGER.error( - "Unexpected error when creating the vigilance_meteoFrance proxy: %s ", - exp, + for city_conf in conf: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=city_conf.copy() ) - else: - weather_alert_client = None - hass.data[DATA_METEO_FRANCE]["weather_alert_client"] = weather_alert_client - - for location in config[DOMAIN]: - - city = location[CONF_CITY] - - try: - client = meteofranceClient(city) - except meteofranceError as exp: - _LOGGER.error( - "Unexpected error when creating the meteofrance proxy: %s", exp - ) - return - - client.need_rain_forecast = bool( - CONF_MONITORED_CONDITIONS in location - and "next_rain" in location[CONF_MONITORED_CONDITIONS] ) - hass.data[DATA_METEO_FRANCE][city] = MeteoFranceUpdater(client) - hass.data[DATA_METEO_FRANCE][city].update() - - if CONF_MONITORED_CONDITIONS in location: - monitored_conditions = location[CONF_MONITORED_CONDITIONS] - _LOGGER.debug("meteo_france sensor platform loaded for %s", city) - load_platform( - hass, - "sensor", - DOMAIN, - {CONF_CITY: city, CONF_MONITORED_CONDITIONS: monitored_conditions}, - config, - ) - - load_platform(hass, "weather", DOMAIN, {CONF_CITY: city}, config) - return True +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: + """Set up an Meteo-France account from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + + # Weather alert + weather_alert_client = VigilanceMeteoFranceProxy() + try: + await hass.async_add_executor_job(weather_alert_client.update_data) + except VigilanceMeteoError as exp: + _LOGGER.error( + "Unexpected error when creating the vigilance_meteoFrance proxy: %s ", exp + ) + return False + hass.data[DOMAIN]["weather_alert_client"] = weather_alert_client + + # Weather + city = entry.data[CONF_CITY] + try: + client = await hass.async_add_executor_job(meteofranceClient, city) + except meteofranceError as exp: + _LOGGER.error("Unexpected error when creating the meteofrance proxy: %s", exp) + return False + + hass.data[DOMAIN][city] = MeteoFranceUpdater(client) + await hass.async_add_executor_job(hass.data[DOMAIN][city].update) + + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + _LOGGER.debug("meteo_france sensor platform loaded for %s", city) + return True + + +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.data[CONF_CITY]) + + return unload_ok + + class MeteoFranceUpdater: """Update data from Meteo-France.""" - def __init__(self, client): + def __init__(self, client: meteofranceClient): """Initialize the data object.""" self._client = client diff --git a/homeassistant/components/meteo_france/config_flow.py b/homeassistant/components/meteo_france/config_flow.py new file mode 100644 index 00000000000..c7673020360 --- /dev/null +++ b/homeassistant/components/meteo_france/config_flow.py @@ -0,0 +1,62 @@ +"""Config flow to configure the Meteo-France integration.""" +import logging + +from meteofrance.client import meteofranceClient, meteofranceError +import voluptuous as vol + +from homeassistant import config_entries + +from .const import CONF_CITY +from .const import DOMAIN # pylint: disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Meteo-France config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def _show_setup_form(self, user_input=None, errors=None): + """Show the setup form to the user.""" + + if user_input is None: + user_input = {} + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_CITY, default=user_input.get(CONF_CITY, "")): str} + ), + errors=errors or {}, + ) + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + errors = {} + + if user_input is None: + return self._show_setup_form(user_input, errors) + + city = user_input[CONF_CITY] # Might be a city name or a postal code + city_name = None + + try: + client = await self.hass.async_add_executor_job(meteofranceClient, city) + city_name = client.get_data()["name"] + except meteofranceError as exp: + _LOGGER.error( + "Unexpected error when creating the meteofrance proxy: %s", exp + ) + return self.async_abort(reason="unknown") + + # Check if already configured + await self.async_set_unique_id(city_name) + self._abort_if_unique_id_configured() + + return self.async_create_entry(title=city_name, data={CONF_CITY: city}) + + async def async_step_import(self, user_input): + """Import a config entry.""" + return await self.async_step_user(user_input) diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py index 223aca20bac..fae2000b19a 100644 --- a/homeassistant/components/meteo_france/const.py +++ b/homeassistant/components/meteo_france/const.py @@ -3,7 +3,7 @@ from homeassistant.const import TEMP_CELSIUS DOMAIN = "meteo_france" -DATA_METEO_FRANCE = "data_meteo_france" +PLATFORMS = ["sensor", "weather"] ATTRIBUTION = "Data provided by Météo-France" CONF_CITY = "city" diff --git a/homeassistant/components/meteo_france/manifest.json b/homeassistant/components/meteo_france/manifest.json index 41a003ea4f7..77f8fca984d 100644 --- a/homeassistant/components/meteo_france/manifest.json +++ b/homeassistant/components/meteo_france/manifest.json @@ -1,8 +1,9 @@ { "domain": "meteo_france", "name": "Météo-France", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/meteo_france", "requirements": ["meteofrance==0.3.7", "vigilancemeteo==3.0.0"], "dependencies": [], - "codeowners": ["@victorcerutti", "@oncleben31"] + "codeowners": ["@victorcerutti", "@oncleben31", "@Quentame"] } diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index f0c08ac1822..cf28b9ea558 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -1,15 +1,18 @@ """Support for Meteo-France raining forecast sensor.""" import logging -from vigilancemeteo import DepartmentWeatherAlert +from meteofrance.client import meteofranceClient +from vigilancemeteo import DepartmentWeatherAlert, VigilanceMeteoFranceProxy -from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType from .const import ( ATTRIBUTION, CONF_CITY, - DATA_METEO_FRANCE, + DOMAIN, SENSOR_TYPE_CLASS, SENSOR_TYPE_ICON, SENSOR_TYPE_NAME, @@ -23,52 +26,47 @@ STATE_ATTR_FORECAST = "1h rain forecast" STATE_ATTR_BULLETIN_TIME = "Bulletin date" -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Meteo-France sensor.""" - if discovery_info is None: - return - - city = discovery_info[CONF_CITY] - monitored_conditions = discovery_info[CONF_MONITORED_CONDITIONS] - client = hass.data[DATA_METEO_FRANCE][city] - weather_alert_client = hass.data[DATA_METEO_FRANCE]["weather_alert_client"] +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: + """Set up the Meteo-France sensor platform.""" + city = entry.data[CONF_CITY] + client = hass.data[DOMAIN][city] + weather_alert_client = hass.data[DOMAIN]["weather_alert_client"] alert_watcher = None - if "weather_alert" in monitored_conditions: - datas = hass.data[DATA_METEO_FRANCE][city].get_data() - # Check if a department code is available for this city. - if "dept" in datas: - try: - # If yes create the watcher DepartmentWeatherAlert object. - alert_watcher = DepartmentWeatherAlert( - datas["dept"], weather_alert_client - ) - except ValueError as exp: - _LOGGER.error( - "Unexpected error when creating the weather alert sensor for %s in department %s: %s", - city, - datas["dept"], - exp, - ) - alert_watcher = None - else: - _LOGGER.info( - "Weather alert watcher added for %s in department %s", - city, - datas["dept"], - ) - else: - _LOGGER.warning( - "No 'dept' key found for '%s'. So weather alert information won't be available", - city, + datas = client.get_data() + # Check if a department code is available for this city. + if "dept" in datas: + try: + # If yes create the watcher DepartmentWeatherAlert object. + alert_watcher = await hass.async_add_executor_job( + DepartmentWeatherAlert, datas["dept"], weather_alert_client ) - # Exit and don't create the sensor if no department code available. - return + _LOGGER.info( + "Weather alert watcher added for %s in department %s", + city, + datas["dept"], + ) + except ValueError as exp: + _LOGGER.error( + "Unexpected error when creating the weather alert sensor for %s in department %s: %s", + city, + datas["dept"], + exp, + ) + else: + _LOGGER.warning( + "No 'dept' key found for '%s'. So weather alert information won't be available", + city, + ) + # Exit and don't create the sensor if no department code available. + return - add_entities( + async_add_entities( [ - MeteoFranceSensor(variable, client, alert_watcher) - for variable in monitored_conditions + MeteoFranceSensor(sensor_type, client, alert_watcher) + for sensor_type in SENSOR_TYPES ], True, ) @@ -77,9 +75,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class MeteoFranceSensor(Entity): """Representation of a Meteo-France sensor.""" - def __init__(self, condition, client, alert_watcher): + def __init__( + self, + sensor_type: str, + client: meteofranceClient, + alert_watcher: VigilanceMeteoFranceProxy, + ): """Initialize the Meteo-France sensor.""" - self._condition = condition + self._type = sensor_type self._client = client self._alert_watcher = alert_watcher self._state = None @@ -88,7 +91,12 @@ class MeteoFranceSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return f"{self._data['name']} {SENSOR_TYPES[self._condition][SENSOR_TYPE_NAME]}" + return f"{self._data['name']} {SENSOR_TYPES[self._type][SENSOR_TYPE_NAME]}" + + @property + def unique_id(self): + """Return the unique id of the sensor.""" + return self.name @property def state(self): @@ -99,7 +107,7 @@ class MeteoFranceSensor(Entity): def device_state_attributes(self): """Return the state attributes of the sensor.""" # Attributes for next_rain sensor. - if self._condition == "next_rain" and "rain_forecast" in self._data: + if self._type == "next_rain" and "rain_forecast" in self._data: return { **{STATE_ATTR_FORECAST: self._data["rain_forecast"]}, **self._data["next_rain_intervals"], @@ -107,7 +115,7 @@ class MeteoFranceSensor(Entity): } # Attributes for weather_alert sensor. - if self._condition == "weather_alert" and self._alert_watcher is not None: + if self._type == "weather_alert" and self._alert_watcher is not None: return { **{STATE_ATTR_BULLETIN_TIME: self._alert_watcher.bulletin_date}, **self._alert_watcher.alerts_list, @@ -120,17 +128,17 @@ class MeteoFranceSensor(Entity): @property def unit_of_measurement(self): """Return the unit of measurement.""" - return SENSOR_TYPES[self._condition][SENSOR_TYPE_UNIT] + return SENSOR_TYPES[self._type][SENSOR_TYPE_UNIT] @property def icon(self): """Return the icon.""" - return SENSOR_TYPES[self._condition][SENSOR_TYPE_ICON] + return SENSOR_TYPES[self._type][SENSOR_TYPE_ICON] @property def device_class(self): """Return the device class of the sensor.""" - return SENSOR_TYPES[self._condition][SENSOR_TYPE_CLASS] + return SENSOR_TYPES[self._type][SENSOR_TYPE_CLASS] def update(self): """Fetch new state data for the sensor.""" @@ -138,13 +146,12 @@ class MeteoFranceSensor(Entity): self._client.update() self._data = self._client.get_data() - if self._condition == "weather_alert": + if self._type == "weather_alert": if self._alert_watcher is not None: self._alert_watcher.update_department_status() self._state = self._alert_watcher.department_color _LOGGER.debug( - "weather alert watcher for %s updated. Proxy" - " have the status: %s", + "weather alert watcher for %s updated. Proxy have the status: %s", self._data["name"], self._alert_watcher.proxy.status, ) @@ -153,9 +160,9 @@ class MeteoFranceSensor(Entity): "No weather alert data for location %s", self._data["name"] ) else: - self._state = self._data[self._condition] + self._state = self._data[self._type] except KeyError: _LOGGER.error( - "No condition %s for location %s", self._condition, self._data["name"] + "No condition %s for location %s", self._type, self._data["name"] ) self._state = None diff --git a/homeassistant/components/meteo_france/strings.json b/homeassistant/components/meteo_france/strings.json new file mode 100644 index 00000000000..8bb02f28bd0 --- /dev/null +++ b/homeassistant/components/meteo_france/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "title": "Météo-France", + "step": { + "user": { + "title": "Météo-France", + "description": "Enter the postal code (only for France, recommended) or city name", + "data": { + "city": "City" + } + } + }, + "abort":{ + "already_configured": "City already configured", + "unknown": "Unknown error: please retry later" + } + } +} diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index c96080808e9..1bdea073aae 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from meteofrance.client import meteofranceClient + from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, ATTR_FORECAST_TEMP, @@ -9,29 +11,30 @@ from homeassistant.components.weather import ( ATTR_FORECAST_TIME, WeatherEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS +from homeassistant.helpers.typing import HomeAssistantType import homeassistant.util.dt as dt_util -from .const import ATTRIBUTION, CONDITION_CLASSES, CONF_CITY, DATA_METEO_FRANCE +from .const import ATTRIBUTION, CONDITION_CLASSES, CONF_CITY, DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: """Set up the Meteo-France weather platform.""" - if discovery_info is None: - return + city = entry.data[CONF_CITY] + client = hass.data[DOMAIN][city] - city = discovery_info[CONF_CITY] - client = hass.data[DATA_METEO_FRANCE][city] - - add_entities([MeteoFranceWeather(client)], True) + async_add_entities([MeteoFranceWeather(client)], True) class MeteoFranceWeather(WeatherEntity): """Representation of a weather condition.""" - def __init__(self, client): + def __init__(self, client: meteofranceClient): """Initialise the platform with a data instance and station name.""" self._client = client self._data = {} @@ -46,6 +49,11 @@ class MeteoFranceWeather(WeatherEntity): """Return the name of the sensor.""" return self._data["name"] + @property + def unique_id(self): + """Return the unique id of the sensor.""" + return self.name + @property def condition(self): """Return the current condition.""" diff --git a/homeassistant/components/mfi/switch.py b/homeassistant/components/mfi/switch.py index 18809f08d4f..b3d3e0ea285 100644 --- a/homeassistant/components/mfi/switch.py +++ b/homeassistant/components/mfi/switch.py @@ -113,7 +113,7 @@ class MfiSwitch(SwitchDevice): @property def device_state_attributes(self): - """Return the state attributes fof the device.""" + """Return the state attributes for the device.""" attr = {} attr["volts"] = round(self._port.data.get("v_rms", 0), 1) attr["amps"] = round(self._port.data.get("i_rms", 0), 1) diff --git a/homeassistant/components/mikrotik/.translations/de.json b/homeassistant/components/mikrotik/.translations/de.json index d3328e9c305..97d28db4cfb 100644 --- a/homeassistant/components/mikrotik/.translations/de.json +++ b/homeassistant/components/mikrotik/.translations/de.json @@ -27,6 +27,7 @@ "step": { "device_tracker": { "data": { + "arp_ping": "ARP Ping aktivieren", "force_dhcp": "Erzwingen Sie das Scannen \u00fcber DHCP" } } diff --git a/homeassistant/components/mikrotik/.translations/fr.json b/homeassistant/components/mikrotik/.translations/fr.json new file mode 100644 index 00000000000..220da6fcbaf --- /dev/null +++ b/homeassistant/components/mikrotik/.translations/fr.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Mikrotik est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de la connexion", + "name_exists": "Le nom existe", + "wrong_credentials": "Identifiants erron\u00e9s" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "name": "Nom", + "password": "Mot de passe", + "port": "Port", + "username": "Nom d'utilisateur", + "verify_ssl": "Utiliser SSL" + }, + "title": "Configurer le routeur Mikrotik" + } + }, + "title": "Mikrotik" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "Activer le ping ARP", + "force_dhcp": "Forcer l'analyse \u00e0 l'aide de DHCP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/.translations/hu.json b/homeassistant/components/mikrotik/.translations/hu.json new file mode 100644 index 00000000000..8afbeb69925 --- /dev/null +++ b/homeassistant/components/mikrotik/.translations/hu.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "A Mikrotik m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "A kapcsolat sikertelen", + "name_exists": "A n\u00e9v m\u00e1r l\u00e9tezik", + "wrong_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u0151 adatok" + }, + "step": { + "user": { + "data": { + "host": "Kiszolg\u00e1l\u00f3", + "name": "N\u00e9v", + "password": "Jelsz\u00f3", + "port": "Port", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v", + "verify_ssl": "SSL haszn\u00e1lata" + }, + "title": "Mikrotik \u00fatv\u00e1laszt\u00f3 be\u00e1ll\u00edt\u00e1sa" + } + }, + "title": "Mikrotik" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "ARP-ping enged\u00e9lyez\u00e9se", + "detection_time": "Otthoni intervallumk\u00e9nt vegye figyelembe", + "force_dhcp": "A szkennel\u00e9s k\u00e9nyszer\u00edt\u00e9se DHCP seg\u00edts\u00e9g\u00e9vel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/.translations/nl.json b/homeassistant/components/mikrotik/.translations/nl.json new file mode 100644 index 00000000000..d4996d492a5 --- /dev/null +++ b/homeassistant/components/mikrotik/.translations/nl.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Mikrotik is al geconfigureerd" + }, + "error": { + "cannot_connect": "Verbinding niet geslaagd", + "name_exists": "Naam bestaat al", + "wrong_credentials": "Ongeldige inloggegevens" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Naam", + "password": "Wachtwoord", + "port": "Poort", + "username": "Gebruikersnaam", + "verify_ssl": "Gebruik SSL" + }, + "title": "Mikrotik Router instellen" + } + }, + "title": "Mikrotik" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "ARP-ping inschakelen", + "detection_time": "Overweeg thuisinterval", + "force_dhcp": "Forceer scannen met DHCP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/.translations/pl.json b/homeassistant/components/mikrotik/.translations/pl.json index 1971f1866e1..6d807672398 100644 --- a/homeassistant/components/mikrotik/.translations/pl.json +++ b/homeassistant/components/mikrotik/.translations/pl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "Mikronik jest ju\u017c skonfigurowany" + "already_configured": "Mikronik jest ju\u017c skonfigurowany." }, "error": { "cannot_connect": "Po\u0142\u0105czenie nie powiod\u0142o si\u0119", - "name_exists": "Nazwa ju\u017c istnieje", + "name_exists": "Nazwa ju\u017c istnieje.", "wrong_credentials": "B\u0142\u0119dne dane uwierzytelniaj\u0105ce" }, "step": { @@ -18,7 +18,7 @@ "username": "Nazwa u\u017cytkownika", "verify_ssl": "U\u017cyj SSL" }, - "title": "Skonfiguruj router Mikrotik" + "title": "Konfiguracja routera Mikrotik" } }, "title": "Mikrotik" diff --git a/homeassistant/components/mikrotik/.translations/sl.json b/homeassistant/components/mikrotik/.translations/sl.json new file mode 100644 index 00000000000..a10508f8bbe --- /dev/null +++ b/homeassistant/components/mikrotik/.translations/sl.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Mikrotik je \u017ee konfiguriran" + }, + "error": { + "cannot_connect": "Povezava ni uspela", + "name_exists": "Ime obstaja", + "wrong_credentials": "Napa\u010dne poverilnice" + }, + "step": { + "user": { + "data": { + "host": "Gostitelj", + "name": "Ime", + "password": "Geslo", + "port": "Vrata", + "username": "Uporabni\u0161ko ime", + "verify_ssl": "Uporaba SSL" + }, + "title": "Nastavite Mikrotik usmerjevalnik" + } + }, + "title": "Mikrotik" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "Omogo\u010di ARP ping", + "detection_time": "Interval \"doma\" ", + "force_dhcp": "Vsilite skeniranje z uporabo DHCP-ja" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/.translations/sv.json b/homeassistant/components/mikrotik/.translations/sv.json index 572b159221d..7be080d96a2 100644 --- a/homeassistant/components/mikrotik/.translations/sv.json +++ b/homeassistant/components/mikrotik/.translations/sv.json @@ -1,10 +1,37 @@ { "config": { + "abort": { + "already_configured": "Mikrotik \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Anslutningen misslyckades", + "name_exists": "Namnet finns", + "wrong_credentials": "Fel autentiseringsuppgifter" + }, "step": { "user": { + "data": { + "host": "V\u00e4rd", + "name": "Namn", + "password": "L\u00f6senord", + "port": "Port", + "username": "Anv\u00e4ndarnamn", + "verify_ssl": "Anv\u00e4nd ssl" + }, "title": "Konfigurera Mikrotik-router" } }, "title": "Mikrotik" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "Aktivera ARP-ping", + "detection_time": "Intervall f\u00f6r att betraktas som hemma", + "force_dhcp": "Tvinga skanning med DHCP" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mikrotik/.translations/zh-Hans.json b/homeassistant/components/mikrotik/.translations/zh-Hans.json new file mode 100644 index 00000000000..9604af53495 --- /dev/null +++ b/homeassistant/components/mikrotik/.translations/zh-Hans.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u4e3b\u673a", + "name": "\u540d\u5b57", + "password": "\u5bc6\u7801", + "port": "\u7aef\u53e3", + "username": "\u7528\u6237\u540d", + "verify_ssl": "\u4f7f\u7528 ssl" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/const.py b/homeassistant/components/mikrotik/const.py index d66a441aaf7..d81e8878d1c 100644 --- a/homeassistant/components/mikrotik/const.py +++ b/homeassistant/components/mikrotik/const.py @@ -24,6 +24,7 @@ CAPSMAN = "capsman" DHCP = "dhcp" WIRELESS = "wireless" IS_WIRELESS = "is_wireless" +IS_CAPSMAN = "is_capsman" MIKROTIK_SERVICES = { ARP: "/ip/arp/getall", @@ -33,6 +34,7 @@ MIKROTIK_SERVICES = { INFO: "/system/routerboard/getall", WIRELESS: "/interface/wireless/registration-table/getall", IS_WIRELESS: "/interface/wireless/print", + IS_CAPSMAN: "/caps-man/interface/print", } ATTR_DEVICE_TRACKER = [ diff --git a/homeassistant/components/mikrotik/hub.py b/homeassistant/components/mikrotik/hub.py index 6e64c443d8b..300d73b6b11 100644 --- a/homeassistant/components/mikrotik/hub.py +++ b/homeassistant/components/mikrotik/hub.py @@ -27,6 +27,7 @@ from .const import ( DHCP, IDENTITY, INFO, + IS_CAPSMAN, IS_WIRELESS, MIKROTIK_SERVICES, NAME, @@ -95,7 +96,8 @@ class MikrotikData: self.all_devices = {} self.devices = {} self.available = True - self.support_wireless = bool(self.command(MIKROTIK_SERVICES[IS_WIRELESS])) + self.support_capsman = False + self.support_wireless = False self.hostname = None self.model = None self.firmware = None @@ -135,6 +137,8 @@ class MikrotikData: self.model = self.get_info(ATTR_MODEL) self.firmware = self.get_info(ATTR_FIRMWARE) self.serial_number = self.get_info(ATTR_SERIAL_NUMBER) + self.support_capsman = bool(self.command(MIKROTIK_SERVICES[IS_CAPSMAN])) + self.support_wireless = bool(self.command(MIKROTIK_SERVICES[IS_WIRELESS])) def connect_to_hub(self): """Connect to hub.""" @@ -158,25 +162,23 @@ class MikrotikData: def update_devices(self): """Get list of devices with latest status.""" arp_devices = {} - wireless_devices = {} device_list = {} + wireless_devices = {} try: self.all_devices = self.get_list_from_interface(DHCP) - if self.support_wireless: - _LOGGER.debug("wireless is supported") - for interface in [CAPSMAN, WIRELESS]: - wireless_devices = self.get_list_from_interface(interface) - if wireless_devices: - _LOGGER.debug("Scanning wireless devices using %s", interface) - break + if self.support_capsman: + _LOGGER.debug("Hub is a CAPSman manager") + device_list = wireless_devices = self.get_list_from_interface(CAPSMAN) + elif self.support_wireless: + _LOGGER.debug("Hub supports wireless Interface") + device_list = wireless_devices = self.get_list_from_interface(WIRELESS) - if self.support_wireless and not self.force_dhcp: - device_list = wireless_devices - else: + if not device_list or self.force_dhcp: device_list = self.all_devices _LOGGER.debug("Falling back to DHCP for scanning devices") if self.arp_enabled: + _LOGGER.debug("Using arp-ping to check devices") arp_devices = self.get_list_from_interface(ARP) # get new hub firmware version if updated @@ -299,7 +301,7 @@ class MikrotikHub: @property def firmware(self): - """Return the firware of the hub.""" + """Return the firmware of the hub.""" return self._mk_data.firmware @property diff --git a/homeassistant/components/minecraft_server/.translations/ca.json b/homeassistant/components/minecraft_server/.translations/ca.json new file mode 100644 index 00000000000..86856ac2d11 --- /dev/null +++ b/homeassistant/components/minecraft_server/.translations/ca.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "L'amfitri\u00f3 ja est\u00e0 configurat." + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3 amb el servidor. Comprova l'amfitri\u00f3 i el port i torna-ho a provar. Assegurat que estas utilitzant la versi\u00f3 del servidor 1.7 o superior.", + "invalid_ip": "L\u2019adre\u00e7a IP \u00e9s inv\u00e0lida (no s\u2019ha pogut determinar l\u2019adre\u00e7a MAC). Corregeix-la i torna-ho a provar.", + "invalid_port": "El port ha d'estar compr\u00e8s entre 1024 i 65535. Corregeix-lo i torna-ho a provar." + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "name": "Nom", + "port": "Port" + }, + "description": "Configuraci\u00f3 d'una inst\u00e0ncia de servidor de Minecraft per poder monitoritzar-lo.", + "title": "Enlla\u00e7 del servidor de Minecraft" + } + }, + "title": "Servidor de Minecraft" + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/.translations/da.json b/homeassistant/components/minecraft_server/.translations/da.json new file mode 100644 index 00000000000..bf930f2f277 --- /dev/null +++ b/homeassistant/components/minecraft_server/.translations/da.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "V\u00e6rten er allerede konfigureret." + }, + "error": { + "cannot_connect": "Det lykkedes ikke at oprette forbindelse til serveren. Kontroller v\u00e6rten og porten, og pr\u00f8v igen. S\u00f8rg ogs\u00e5 for, at du k\u00f8rer mindst Minecraft version 1.7 p\u00e5 din server.", + "invalid_ip": "IP-adressen er ugyldig (MAC-adressen kunne ikke bestemmes). Ret den, og pr\u00f8v igen.", + "invalid_port": "Porten skal v\u00e6re i intervallet fra 1024 til 65535. Ret den, og pr\u00f8v igen." + }, + "step": { + "user": { + "data": { + "host": "V\u00e6rt", + "name": "Navn", + "port": "Port" + }, + "description": "Konfigurer din Minecraft-server-instans for at tillade overv\u00e5gning.", + "title": "Forbind din Minecraft-server" + } + }, + "title": "Minecraft-server" + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/.translations/de.json b/homeassistant/components/minecraft_server/.translations/de.json new file mode 100644 index 00000000000..00426308239 --- /dev/null +++ b/homeassistant/components/minecraft_server/.translations/de.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Der Host ist bereits konfiguriert." + }, + "error": { + "cannot_connect": "Verbindung zum Server fehlgeschlagen. Bitte \u00fcberpr\u00fcfe den Host und den Port und versuche es erneut. Stelle au\u00dferdem sicher, dass Du mindestens Minecraft Version 1.7 auf Deinem Server ausf\u00fchrst.", + "invalid_ip": "IP-Adresse ist ung\u00fcltig (MAC-Adresse konnte nicht ermittelt werden). Bitte korrigieren und erneut versuchen.", + "invalid_port": "Der Port muss im Bereich von 1024 bis 65535 liegen. Bitte korrigieren und erneut versuchen." + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Name", + "port": "Port" + }, + "description": "Richte deine Minecraft Server-Instanz ein, um es \u00fcberwachen zu k\u00f6nnen.", + "title": "Verkn\u00fcpfe deinen Minecraft Server" + } + }, + "title": "Minecraft Server" + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/.translations/en.json b/homeassistant/components/minecraft_server/.translations/en.json new file mode 100644 index 00000000000..d0f7a5d6300 --- /dev/null +++ b/homeassistant/components/minecraft_server/.translations/en.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Host is already configured." + }, + "error": { + "cannot_connect": "Failed to connect to server. Please check the host and port and try again. Also ensure that you are running at least Minecraft version 1.7 on your server.", + "invalid_ip": "IP address is invalid (MAC address could not be determined). Please correct it and try again.", + "invalid_port": "Port must be in range from 1024 to 65535. Please correct it and try again." + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Name", + "port": "Port" + }, + "description": "Set up your Minecraft Server instance to allow monitoring.", + "title": "Link your Minecraft Server" + } + }, + "title": "Minecraft Server" + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/.translations/es.json b/homeassistant/components/minecraft_server/.translations/es.json new file mode 100644 index 00000000000..14831ef45e1 --- /dev/null +++ b/homeassistant/components/minecraft_server/.translations/es.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "El host ya est\u00e1 configurado." + }, + "error": { + "cannot_connect": "No se pudo conectar al servidor. Compruebe el host y el puerto e int\u00e9ntelo de nuevo. Tambi\u00e9n aseg\u00farese de que est\u00e1 ejecutando al menos Minecraft versi\u00f3n 1.7 en su servidor.", + "invalid_ip": "La direcci\u00f3n IP no es valida (no se pudo determinar la direcci\u00f3n MAC). Por favor, corr\u00edgelo e int\u00e9ntalo de nuevo.", + "invalid_port": "El puerto debe estar en el rango de 1024 a 65535. Por favor, corr\u00edgelo e int\u00e9ntalo de nuevo." + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nombre", + "port": "Puerto" + }, + "description": "Configura tu instancia de Minecraft Server para permitir la supervisi\u00f3n.", + "title": "Enlace su servidor Minecraft" + } + }, + "title": "Servidor Minecraft" + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/.translations/fr.json b/homeassistant/components/minecraft_server/.translations/fr.json new file mode 100644 index 00000000000..bf87c6f3d73 --- /dev/null +++ b/homeassistant/components/minecraft_server/.translations/fr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "L'h\u00f4te est d\u00e9j\u00e0 configur\u00e9." + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "name": "Nom", + "port": "Port" + }, + "title": "Reliez votre serveur Minecraft" + } + }, + "title": "Serveur Minecraft" + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/.translations/hu.json b/homeassistant/components/minecraft_server/.translations/hu.json new file mode 100644 index 00000000000..9341bdbe4d1 --- /dev/null +++ b/homeassistant/components/minecraft_server/.translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Kiszolg\u00e1l\u00f3 m\u00e1r konfigur\u00e1lva van." + }, + "step": { + "user": { + "data": { + "host": "Kiszolg\u00e1l\u00f3", + "name": "N\u00e9v", + "port": "Port" + }, + "title": "Kapcsolja \u00f6ssze a Minecraft szervert" + } + }, + "title": "Minecraft szerver" + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/.translations/it.json b/homeassistant/components/minecraft_server/.translations/it.json new file mode 100644 index 00000000000..5861eebcc9a --- /dev/null +++ b/homeassistant/components/minecraft_server/.translations/it.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "L'host \u00e8 gi\u00e0 configurato." + }, + "error": { + "cannot_connect": "Impossibile connettersi al server. Controllare l'host e la porta e riprovare. Assicurarsi inoltre che si esegue almeno Minecraft versione 1.7 sul server.", + "invalid_ip": "L'indirizzo IP non \u00e8 valido (non \u00e8 stato possibile determinare l'indirizzo MAC). Correggilo e riprova.", + "invalid_port": "La porta deve essere compresa tra 1024 e 65535. Correggila e riprova." + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nome", + "port": "Porta" + }, + "description": "Configurare l'istanza del Server Minecraft per consentire il monitoraggio.", + "title": "Collega il tuo Server Minecraft" + } + }, + "title": "Server Minecraft" + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/.translations/ko.json b/homeassistant/components/minecraft_server/.translations/ko.json new file mode 100644 index 00000000000..66b281cc5d9 --- /dev/null +++ b/homeassistant/components/minecraft_server/.translations/ko.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\ud638\uc2a4\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\ub97c \ud655\uc778\ud55c \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694. \ub610\ud55c \uc11c\ubc84\uc5d0\uc11c Minecraft \ubc84\uc804 1.7 \uc774\uc0c1\uc744 \uc2e4\ud589 \uc911\uc778\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", + "invalid_ip": "IP \uc8fc\uc18c\uac00 \uc720\ud6a8\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4 (MAC \uc8fc\uc18c\ub97c \ud655\uc778\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4). \uc218\uc815 \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "invalid_port": "\ud3ec\ud2b8\ub294 1024-65535 \ubc94\uc704\uc5d0 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4. \ud3ec\ud2b8\ub97c \uc218\uc815\ud55c \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." + }, + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "name": "\uc774\ub984", + "port": "\ud3ec\ud2b8" + }, + "description": "\ubaa8\ub2c8\ud130\ub9c1\uc774 \uac00\ub2a5\ud558\ub3c4\ub85d Minecraft \uc11c\ubc84 \uc778\uc2a4\ud134\uc2a4\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694.", + "title": "Minecraft \uc11c\ubc84 \uc5f0\uacb0" + } + }, + "title": "Minecraft \uc11c\ubc84" + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/.translations/lb.json b/homeassistant/components/minecraft_server/.translations/lb.json new file mode 100644 index 00000000000..f95dd062005 --- /dev/null +++ b/homeassistant/components/minecraft_server/.translations/lb.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen mam Server. Iwwerpr\u00e9if den Numm a Port a prob\u00e9ier nach emol. G\u00e9i och s\u00e9cher dass op d'mannst Minecraft Versioun 1.7 um Server leeft.", + "invalid_ip": "IP Adress ass ong\u00eblteg (MAC Adress konnt net best\u00ebmmt ginn). Korrig\u00e9iert et a prob\u00e9iert et nach eng K\u00e9ier w.e.g.", + "invalid_port": "Port muss zw\u00ebscht 1024 a 65535 sinn. Korrig\u00e9iert et a prob\u00e9iert et nach eng K\u00e9ier w.e.g." + }, + "step": { + "user": { + "data": { + "host": "Apparat", + "name": "Numm", + "port": "Port" + }, + "description": "Riicht deng Minecraft Server Instanz a fir d'Iwwerwaachung z'erlaben", + "title": "Verbann d\u00e4in Minecraft Server" + } + }, + "title": "Minecraft Server" + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/.translations/nl.json b/homeassistant/components/minecraft_server/.translations/nl.json new file mode 100644 index 00000000000..75e19bc2550 --- /dev/null +++ b/homeassistant/components/minecraft_server/.translations/nl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Host is al geconfigureerd." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken met de server. Controleer de host en de poort en probeer het opnieuw. Zorg er ook voor dat u minimaal Minecraft versie 1.7 op uw server uitvoert.", + "invalid_ip": "IP-adres is ongeldig (MAC-adres kon niet worden bepaald). Corrigeer het en probeer het opnieuw.", + "invalid_port": "Poort moet tussen 1024 en 65535 liggen. Corrigeer dit en probeer het opnieuw." + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Naam", + "port": "Poort" + }, + "description": "Stel uw Minecraft server in om monitoring toe te staan.", + "title": "Koppel uw Minecraft server" + } + }, + "title": "Minecraft server" + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/.translations/no.json b/homeassistant/components/minecraft_server/.translations/no.json new file mode 100644 index 00000000000..f7be289d48c --- /dev/null +++ b/homeassistant/components/minecraft_server/.translations/no.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Verten er allerede konfigurert." + }, + "error": { + "cannot_connect": "Kan ikke koble til serveren. Kontroller verten og porten, og pr\u00f8v p\u00e5 nytt. S\u00f8rg ogs\u00e5 for at du kj\u00f8rer minst Minecraft versjon 1.7 p\u00e5 serveren din.", + "invalid_ip": "IP-adressen er ugyldig (MAC-adressen kan ikke fastsl\u00e5s). Vennligst korriger den og pr\u00f8v p\u00e5 nytt.", + "invalid_port": "Porten m\u00e5 v\u00e6re i omr\u00e5det 1024 til 65535. Vennligst korriger den og pr\u00f8v p\u00e5 nytt." + }, + "step": { + "user": { + "data": { + "host": "Vert", + "name": "Navn", + "port": "Port" + }, + "description": "Konfigurer Minecraft Server-forekomsten slik at den kan overv\u00e5kes.", + "title": "Link din Minecraft Server" + } + }, + "title": "Minecraft Server" + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/.translations/pl.json b/homeassistant/components/minecraft_server/.translations/pl.json new file mode 100644 index 00000000000..f9c4a515566 --- /dev/null +++ b/homeassistant/components/minecraft_server/.translations/pl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Host jest ju\u017c skonfigurowany." + }, + "error": { + "cannot_connect": "B\u0142\u0105d po\u0142\u0105czenia z serwerem. Sprawd\u017a adres hosta i port i spr\u00f3buj ponownie. Upewnij si\u0119 tak\u017ce, \u017ce na serwerze dzia\u0142a Minecraft w wersji przynajmniej 1.7.", + "invalid_ip": "Adres IP jest nieprawid\u0142owy (nie mo\u017cna ustali\u0107 adresu MAC). Popraw to i spr\u00f3buj ponownie.", + "invalid_port": "Port musi znajdowa\u0107 si\u0119 w zakresie od 1024 do 65535. Popraw go i spr\u00f3buj ponownie." + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nazwa", + "port": "Port" + }, + "description": "Skonfiguruj instancj\u0119 serwera Minecraft, aby umo\u017cliwi\u0107 monitorowanie.", + "title": "Po\u0142\u0105cz sw\u00f3j serwer Minecraft" + } + }, + "title": "Serwer Minecraft" + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/.translations/ru.json b/homeassistant/components/minecraft_server/.translations/ru.json new file mode 100644 index 00000000000..916b342ee4a --- /dev/null +++ b/homeassistant/components/minecraft_server/.translations/ru.json @@ -0,0 +1,24 @@ +{ + "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 \u0441\u0435\u0440\u0432\u0435\u0440\u0443. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445 \u0438 \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430. \u0422\u0430\u043a\u0436\u0435 \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u043d\u0430 \u0412\u0430\u0448\u0435\u043c \u0441\u0435\u0440\u0432\u0435\u0440\u0435 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d Minecraft \u0432\u0435\u0440\u0441\u0438\u0438 1.7, \u0438\u043b\u0438 \u0432\u044b\u0448\u0435.", + "invalid_ip": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441 (\u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c MAC-\u0430\u0434\u0440\u0435\u0441).", + "invalid_port": "\u041f\u043e\u0440\u0442 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432 \u0434\u0438\u0430\u043f\u0430\u0437\u043e\u043d\u0435 \u043e\u0442 1024 \u0434\u043e 65535." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u044d\u0442\u043e\u0442 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0412\u0430\u0448\u0435\u0433\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 Minecraft.", + "title": "Minecraft Server" + } + }, + "title": "Minecraft Server" + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/.translations/sl.json b/homeassistant/components/minecraft_server/.translations/sl.json new file mode 100644 index 00000000000..cf8a8af54ee --- /dev/null +++ b/homeassistant/components/minecraft_server/.translations/sl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Gostitelj je \u017ee konfiguriran." + }, + "error": { + "cannot_connect": "Povezava s stre\u017enikom ni uspela. Preverite gostitelja in vrata in poskusite znova. Zagotovite tudi, da na stre\u017eniku izvajate vsaj Minecraft razli\u010dice 1.7.", + "invalid_ip": "IP naslov ni veljaven (MAC naslova ni mogo\u010de dolo\u010diti). Popravite ga in poskusite znova.", + "invalid_port": "Vrata morajo biti v razponu od 1024 do 65535. Prosimo, popravite in poskusite znova." + }, + "step": { + "user": { + "data": { + "host": "Gostitelj", + "name": "Ime", + "port": "Vrata" + }, + "description": "Nastavite svoj Minecraft stre\u017enik, da omogo\u010dite spremljanje.", + "title": "Pove\u017eite svoj Minecraft stre\u017enik" + } + }, + "title": "Minecraft stre\u017enik" + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/.translations/sv.json b/homeassistant/components/minecraft_server/.translations/sv.json new file mode 100644 index 00000000000..acf941878dd --- /dev/null +++ b/homeassistant/components/minecraft_server/.translations/sv.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "V\u00e4rden \u00e4r redan konfigurerad." + }, + "error": { + "cannot_connect": "Misslyckades med att ansluta till servern. Kontrollera v\u00e4rden och porten och f\u00f6rs\u00f6k igen. Se ocks\u00e5 till att du k\u00f6r minst Minecraft version 1.7 p\u00e5 din server.", + "invalid_ip": "IP-adressen \u00e4r ogiltig (MAC-adressen kunde inte fastst\u00e4llas). Korrigera det och f\u00f6rs\u00f6k igen.", + "invalid_port": "Porten m\u00e5ste ligga inom intervallet 1024 till 65535. Korrigera den och f\u00f6rs\u00f6k igen." + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "name": "Namn", + "port": "Port" + }, + "description": "St\u00e4ll in din Minecraft Server-instans f\u00f6r att till\u00e5ta \u00f6vervakning.", + "title": "L\u00e4nka din Minecraft-server" + } + }, + "title": "Minecraft-server" + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/.translations/tr.json b/homeassistant/components/minecraft_server/.translations/tr.json new file mode 100644 index 00000000000..595c1686982 --- /dev/null +++ b/homeassistant/components/minecraft_server/.translations/tr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Host zaten ayarlanm\u0131\u015f." + }, + "error": { + "cannot_connect": "Server ile ba\u011flant\u0131 kurulamad\u0131. L\u00fctfen host ve port ayarlar\u0131n\u0131 kontrol et ve tekrar dene. Ayr\u0131ca, serverda en az Minecraft s\u00fcr\u00fcm 1.7 \u00e7al\u0131\u015ft\u0131rd\u0131\u011f\u0131ndan emin ol.", + "invalid_ip": "IP adresi ge\u00e7ersiz (MAC adresi belirlenemedi). L\u00fctfen d\u00fczelt ve tekrar dene.", + "invalid_port": "Port 1024 ile 65535 aral\u0131\u011f\u0131nda olmal\u0131d\u0131r. L\u00fctfen d\u00fczelt ve yeniden dene." + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Ad", + "port": "Port" + }, + "description": "G\u00f6zetmeye izin vermek i\u00e7in Minecraft server nesnesini ayarla.", + "title": "Minecraft Servern\u0131 ba\u011fla" + } + }, + "title": "Minecraft Server" + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/.translations/zh-Hant.json b/homeassistant/components/minecraft_server/.translations/zh-Hant.json new file mode 100644 index 00000000000..c451ad71065 --- /dev/null +++ b/homeassistant/components/minecraft_server/.translations/zh-Hant.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u4e3b\u6a5f\u7aef\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" + }, + "error": { + "cannot_connect": "\u4f3a\u670d\u5668\u9023\u7dda\u5931\u6557\u3002\u8acb\u6aa2\u67e5\u4e3b\u6a5f\u7aef\u8207\u901a\u8a0a\u57e0\u5f8c\u518d\u8a66\u4e00\u6b21\u3002\u53e6\u8acb\u78ba\u8a8d\u65bc\u4f3a\u670d\u5668\u4e0a\u57f7\u884c\u6700\u65b0\u7248\u672c Minecraft 1.7 \u7248\u3002", + "invalid_ip": "IP \u4f4d\u5740\u7121\u6548\uff08MAC \u4f4d\u5740\u7121\u6cd5\u78ba\u8a8d\uff09\u3002\u8acb\u4fee\u6b63\u5f8c\u3001\u518d\u8a66\u4e00\u6b21\u3002", + "invalid_port": "\u901a\u8a0a\u57e0\u7bc4\u570d\u4ecb\u65bc 1024 \u81f3 65535\u3002\u8acb\u4fee\u6b63\u5f8c\u3001\u518d\u8a66\u4e00\u6b21\u3002" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "name": "\u540d\u7a31", + "port": "\u901a\u8a0a\u57e0" + }, + "description": "\u8a2d\u5b9a Minecraft \u4f3a\u670d\u5668\u4ee5\u9032\u884c\u76e3\u63a7\u3002", + "title": "\u9023\u7d50 Minecraft \u4f3a\u670d\u5668" + } + }, + "title": "Minecraft \u4f3a\u670d\u5668" + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py new file mode 100644 index 00000000000..789e4d8f1b8 --- /dev/null +++ b/homeassistant/components/minecraft_server/__init__.py @@ -0,0 +1,273 @@ +"""The Minecraft Server integration.""" + +import asyncio +from datetime import datetime, timedelta +import logging +from typing import Any, Dict + +from mcstatus.server import MinecraftServer as MCStatus + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import ConfigType, HomeAssistantType + +from .const import DOMAIN, MANUFACTURER, SCAN_INTERVAL, SIGNAL_NAME_PREFIX + +PLATFORMS = ["binary_sensor", "sensor"] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: + """Set up the Minecraft Server component.""" + return True + + +async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) -> bool: + """Set up Minecraft Server from a config entry.""" + domain_data = hass.data.setdefault(DOMAIN, {}) + + # Create and store server instance. + unique_id = config_entry.unique_id + _LOGGER.debug( + "Creating server instance for '%s' (host='%s', port=%s)", + config_entry.data[CONF_NAME], + config_entry.data[CONF_HOST], + config_entry.data[CONF_PORT], + ) + server = MinecraftServer(hass, unique_id, config_entry.data) + domain_data[unique_id] = server + await server.async_update() + server.start_periodic_update() + + # Set up platform(s). + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, platform) + ) + + return True + + +async def async_unload_entry( + hass: HomeAssistantType, config_entry: ConfigEntry +) -> bool: + """Unload Minecraft Server config entry.""" + unique_id = config_entry.unique_id + server = hass.data[DOMAIN][unique_id] + + # Unload platforms. + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS + ] + ) + + # Clean up. + server.stop_periodic_update() + hass.data[DOMAIN].pop(unique_id) + + return True + + +class MinecraftServer: + """Representation of a Minecraft server.""" + + # Private constants + _MAX_RETRIES_PING = 3 + _MAX_RETRIES_STATUS = 3 + + def __init__( + self, hass: HomeAssistantType, unique_id: str, config_data: ConfigType + ) -> None: + """Initialize server instance.""" + self._hass = hass + + # Server data + self.unique_id = unique_id + self.name = config_data[CONF_NAME] + self.host = config_data[CONF_HOST] + self.port = config_data[CONF_PORT] + self.online = False + self._last_status_request_failed = False + + # 3rd party library instance + self._mc_status = MCStatus(self.host, self.port) + + # Data provided by 3rd party library + self.description = None + self.version = None + self.protocol_version = None + self.latency_time = None + self.players_online = None + self.players_max = None + self.players_list = None + + # Dispatcher signal name + self.signal_name = f"{SIGNAL_NAME_PREFIX}_{self.unique_id}" + + # Callback for stopping periodic update. + self._stop_periodic_update = None + + def start_periodic_update(self) -> None: + """Start periodic execution of update method.""" + self._stop_periodic_update = async_track_time_interval( + self._hass, self.async_update, timedelta(seconds=SCAN_INTERVAL) + ) + + def stop_periodic_update(self) -> None: + """Stop periodic execution of update method.""" + self._stop_periodic_update() + + async def async_check_connection(self) -> None: + """Check server connection using a 'ping' request and store result.""" + try: + await self._hass.async_add_executor_job( + self._mc_status.ping, self._MAX_RETRIES_PING + ) + self.online = True + except OSError as error: + _LOGGER.debug( + "Error occurred while trying to ping the server - OSError: %s", error + ) + self.online = False + + async def async_update(self, now: datetime = None) -> None: + """Get server data from 3rd party library and update properties.""" + # Check connection status. + server_online_old = self.online + await self.async_check_connection() + server_online = self.online + + # Inform user once about connection state changes if necessary. + if server_online_old and not server_online: + _LOGGER.warning("Connection to server lost") + elif not server_online_old and server_online: + _LOGGER.info("Connection to server (re-)established") + + # Update the server properties if server is online. + if server_online: + await self._async_status_request() + + # Notify sensors about new data. + async_dispatcher_send(self._hass, self.signal_name) + + async def _async_status_request(self) -> None: + """Request server status and update properties.""" + try: + status_response = await self._hass.async_add_executor_job( + self._mc_status.status, self._MAX_RETRIES_STATUS + ) + + # Got answer to request, update properties. + self.description = status_response.description["text"] + self.version = status_response.version.name + self.protocol_version = status_response.version.protocol + self.players_online = status_response.players.online + self.players_max = status_response.players.max + self.latency_time = status_response.latency + self.players_list = [] + if status_response.players.sample is not None: + for player in status_response.players.sample: + self.players_list.append(player.name) + + # Inform user once about successful update if necessary. + if self._last_status_request_failed: + _LOGGER.info("Updating the server properties succeeded again") + self._last_status_request_failed = False + except OSError as error: + # No answer to request, set all properties to unknown. + self.description = None + self.version = None + self.protocol_version = None + self.players_online = None + self.players_max = None + self.latency_time = None + self.players_list = None + + # Inform user once about failed update if necessary. + if not self._last_status_request_failed: + _LOGGER.warning( + "Updating the server properties failed - OSError: %s", error, + ) + self._last_status_request_failed = True + + +class MinecraftServerEntity(Entity): + """Representation of a Minecraft Server base entity.""" + + def __init__( + self, server: MinecraftServer, type_name: str, icon: str, device_class: str + ) -> None: + """Initialize base entity.""" + self._server = server + self._name = f"{server.name} {type_name}" + self._icon = icon + self._unique_id = f"{self._server.unique_id}-{type_name}" + self._device_info = { + "identifiers": {(DOMAIN, self._server.unique_id)}, + "name": self._server.name, + "manufacturer": MANUFACTURER, + "model": f"Minecraft Server ({self._server.version})", + "sw_version": self._server.protocol_version, + } + self._device_class = device_class + self._device_state_attributes = None + self._disconnect_dispatcher = None + + @property + def name(self) -> str: + """Return name.""" + return self._name + + @property + def unique_id(self) -> str: + """Return unique ID.""" + return self._unique_id + + @property + def device_info(self) -> Dict[str, Any]: + """Return device information.""" + return self._device_info + + @property + def device_class(self) -> str: + """Return device class.""" + return self._device_class + + @property + def icon(self) -> str: + """Return icon.""" + return self._icon + + @property + def should_poll(self) -> bool: + """Disable polling.""" + return False + + async def async_update(self) -> None: + """Fetch data from the server.""" + raise NotImplementedError() + + async def async_added_to_hass(self) -> None: + """Connect dispatcher to signal from server.""" + self._disconnect_dispatcher = async_dispatcher_connect( + self.hass, self._server.signal_name, self._update_callback + ) + + async def async_will_remove_from_hass(self) -> None: + """Disconnect dispatcher before removal.""" + self._disconnect_dispatcher() + + @callback + def _update_callback(self) -> None: + """Triggers update of properties after receiving signal from server.""" + self.async_schedule_update_ha_state(force_refresh=True) diff --git a/homeassistant/components/minecraft_server/binary_sensor.py b/homeassistant/components/minecraft_server/binary_sensor.py new file mode 100644 index 00000000000..cde2a414900 --- /dev/null +++ b/homeassistant/components/minecraft_server/binary_sensor.py @@ -0,0 +1,47 @@ +"""The Minecraft Server binary sensor platform.""" + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, + BinarySensorDevice, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType + +from . import MinecraftServer, MinecraftServerEntity +from .const import DOMAIN, ICON_STATUS, NAME_STATUS + + +async def async_setup_entry( + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities +) -> None: + """Set up the Minecraft Server binary sensor platform.""" + server = hass.data[DOMAIN][config_entry.unique_id] + + # Create entities list. + entities = [MinecraftServerStatusBinarySensor(server)] + + # Add binary sensor entities. + async_add_entities(entities, True) + + +class MinecraftServerStatusBinarySensor(MinecraftServerEntity, BinarySensorDevice): + """Representation of a Minecraft Server status binary sensor.""" + + def __init__(self, server: MinecraftServer) -> None: + """Initialize status binary sensor.""" + super().__init__( + server=server, + type_name=NAME_STATUS, + icon=ICON_STATUS, + device_class=DEVICE_CLASS_CONNECTIVITY, + ) + self._is_on = False + + @property + def is_on(self) -> bool: + """Return binary state.""" + return self._is_on + + async def async_update(self) -> None: + """Update status.""" + self._is_on = self._server.online diff --git a/homeassistant/components/minecraft_server/config_flow.py b/homeassistant/components/minecraft_server/config_flow.py new file mode 100644 index 00000000000..8c6049a2c1b --- /dev/null +++ b/homeassistant/components/minecraft_server/config_flow.py @@ -0,0 +1,116 @@ +"""Config flow for Minecraft Server integration.""" +from functools import partial +import ipaddress + +import getmac +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT + +from . import MinecraftServer +from .const import ( # pylint: disable=unused-import + DEFAULT_HOST, + DEFAULT_NAME, + DEFAULT_PORT, + DOMAIN, +) + + +class MinecraftServerConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Minecraft Server.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + + if user_input is not None: + # User inputs. + host = user_input[CONF_HOST] + port = user_input[CONF_PORT] + + unique_id = "" + + # Check if 'host' is a valid IP address and if so, get the MAC address. + ip_address = None + mac_address = None + try: + ip_address = ipaddress.ip_address(host) + except ValueError: + # Host is not a valid IP address. + pass + else: + # Host is a valid IP address. + if ip_address.version == 4: + # Address type is IPv4. + params = {"ip": host} + else: + # Address type is IPv6. + params = {"ip6": host} + mac_address = await self.hass.async_add_executor_job( + partial(getmac.get_mac_address, **params) + ) + + # Validate IP address via valid MAC address. + if ip_address is not None and mac_address is None: + errors["base"] = "invalid_ip" + # Validate port configuration (limit to user and dynamic port range). + elif (port < 1024) or (port > 65535): + errors["base"] = "invalid_port" + # Validate host and port via ping request to server. + else: + # Build unique_id. + if ip_address is not None: + # Since IP addresses can change and therefore are not allowed in a + # unique_id, fall back to the MAC address. + unique_id = f"{mac_address}-{port}" + else: + # Use host name in unique_id (host names should not change). + unique_id = f"{host}-{port}" + + # Abort in case the host was already configured before. + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + + # Create server instance with configuration data and try pinging the server. + server = MinecraftServer(self.hass, unique_id, user_input) + await server.async_check_connection() + if not server.online: + # Host or port invalid or server not reachable. + errors["base"] = "cannot_connect" + else: + # Configuration data are available and no error was detected, create configuration entry. + return self.async_create_entry( + title=f"{host}:{port}", data=user_input + ) + + # Show configuration form (default form in case of no user_input, + # form filled with user_input and eventually with errors otherwise). + return self._show_config_form(user_input, errors) + + def _show_config_form(self, user_input=None, errors=None): + """Show the setup form to the user.""" + if user_input is None: + user_input = {} + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_NAME, default=user_input.get(CONF_NAME, DEFAULT_NAME) + ): str, + vol.Required( + CONF_HOST, default=user_input.get(CONF_HOST, DEFAULT_HOST) + ): vol.All(str, vol.Lower), + vol.Optional( + CONF_PORT, default=user_input.get(CONF_PORT, DEFAULT_PORT) + ): int, + } + ), + errors=errors, + ) diff --git a/homeassistant/components/minecraft_server/const.py b/homeassistant/components/minecraft_server/const.py new file mode 100644 index 00000000000..c3ab6615481 --- /dev/null +++ b/homeassistant/components/minecraft_server/const.py @@ -0,0 +1,37 @@ +"""Constants for the Minecraft Server integration.""" + +ATTR_PLAYERS_LIST = "players_list" + +DEFAULT_HOST = "localhost" +DEFAULT_NAME = "Minecraft Server" +DEFAULT_PORT = 25565 + +DOMAIN = "minecraft_server" + +ICON_LATENCY_TIME = "mdi:signal" +ICON_PLAYERS_MAX = "mdi:account-multiple" +ICON_PLAYERS_ONLINE = "mdi:account-multiple" +ICON_PROTOCOL_VERSION = "mdi:numeric" +ICON_STATUS = "mdi:lan" +ICON_VERSION = "mdi:numeric" + +KEY_SERVERS = "servers" + +MANUFACTURER = "Mojang AB" + +NAME_LATENCY_TIME = "Latency Time" +NAME_PLAYERS_MAX = "Players Max" +NAME_PLAYERS_ONLINE = "Players Online" +NAME_PROTOCOL_VERSION = "Protocol Version" +NAME_STATUS = "Status" +NAME_VERSION = "Version" + +SCAN_INTERVAL = 60 + +SIGNAL_NAME_PREFIX = f"signal_{DOMAIN}" + +UNIT_LATENCY_TIME = "ms" +UNIT_PLAYERS_MAX = "players" +UNIT_PLAYERS_ONLINE = "players" +UNIT_PROTOCOL_VERSION = None +UNIT_VERSION = None diff --git a/homeassistant/components/minecraft_server/manifest.json b/homeassistant/components/minecraft_server/manifest.json new file mode 100644 index 00000000000..1dda76dee77 --- /dev/null +++ b/homeassistant/components/minecraft_server/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "minecraft_server", + "name": "Minecraft Server", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/minecraft_server", + "requirements": ["getmac==0.8.1", "mcstatus==2.3.0"], + "dependencies": [], + "codeowners": ["@elmurato"], + "quality_scale": "silver" +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/sensor.py b/homeassistant/components/minecraft_server/sensor.py new file mode 100644 index 00000000000..0b37a7d979b --- /dev/null +++ b/homeassistant/components/minecraft_server/sensor.py @@ -0,0 +1,177 @@ +"""The Minecraft Server sensor platform.""" + +import logging +from typing import Any, Dict + +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType + +from . import MinecraftServer, MinecraftServerEntity +from .const import ( + ATTR_PLAYERS_LIST, + DOMAIN, + ICON_LATENCY_TIME, + ICON_PLAYERS_MAX, + ICON_PLAYERS_ONLINE, + ICON_PROTOCOL_VERSION, + ICON_VERSION, + NAME_LATENCY_TIME, + NAME_PLAYERS_MAX, + NAME_PLAYERS_ONLINE, + NAME_PROTOCOL_VERSION, + NAME_VERSION, + UNIT_LATENCY_TIME, + UNIT_PLAYERS_MAX, + UNIT_PLAYERS_ONLINE, + UNIT_PROTOCOL_VERSION, + UNIT_VERSION, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities +) -> None: + """Set up the Minecraft Server sensor platform.""" + server = hass.data[DOMAIN][config_entry.unique_id] + + # Create entities list. + entities = [ + MinecraftServerVersionSensor(server), + MinecraftServerProtocolVersionSensor(server), + MinecraftServerLatencyTimeSensor(server), + MinecraftServerPlayersOnlineSensor(server), + MinecraftServerPlayersMaxSensor(server), + ] + + # Add sensor entities. + async_add_entities(entities, True) + + +class MinecraftServerSensorEntity(MinecraftServerEntity): + """Representation of a Minecraft Server sensor base entity.""" + + def __init__( + self, + server: MinecraftServer, + type_name: str, + icon: str = None, + unit: str = None, + device_class: str = None, + ) -> None: + """Initialize sensor base entity.""" + super().__init__(server, type_name, icon, device_class) + self._state = None + self._unit = unit + + @property + def available(self) -> bool: + """Return sensor availability.""" + return self._server.online + + @property + def state(self) -> Any: + """Return sensor state.""" + return self._state + + @property + def unit_of_measurement(self) -> str: + """Return sensor measurement unit.""" + return self._unit + + +class MinecraftServerVersionSensor(MinecraftServerSensorEntity): + """Representation of a Minecraft Server version sensor.""" + + def __init__(self, server: MinecraftServer) -> None: + """Initialize version sensor.""" + super().__init__( + server=server, type_name=NAME_VERSION, icon=ICON_VERSION, unit=UNIT_VERSION + ) + + async def async_update(self) -> None: + """Update version.""" + self._state = self._server.version + + +class MinecraftServerProtocolVersionSensor(MinecraftServerSensorEntity): + """Representation of a Minecraft Server protocol version sensor.""" + + def __init__(self, server: MinecraftServer) -> None: + """Initialize protocol version sensor.""" + super().__init__( + server=server, + type_name=NAME_PROTOCOL_VERSION, + icon=ICON_PROTOCOL_VERSION, + unit=UNIT_PROTOCOL_VERSION, + ) + + async def async_update(self) -> None: + """Update protocol version.""" + self._state = self._server.protocol_version + + +class MinecraftServerLatencyTimeSensor(MinecraftServerSensorEntity): + """Representation of a Minecraft Server latency time sensor.""" + + def __init__(self, server: MinecraftServer) -> None: + """Initialize latency time sensor.""" + super().__init__( + server=server, + type_name=NAME_LATENCY_TIME, + icon=ICON_LATENCY_TIME, + unit=UNIT_LATENCY_TIME, + ) + + async def async_update(self) -> None: + """Update latency time.""" + self._state = self._server.latency_time + + +class MinecraftServerPlayersOnlineSensor(MinecraftServerSensorEntity): + """Representation of a Minecraft Server online players sensor.""" + + def __init__(self, server: MinecraftServer) -> None: + """Initialize online players sensor.""" + super().__init__( + server=server, + type_name=NAME_PLAYERS_ONLINE, + icon=ICON_PLAYERS_ONLINE, + unit=UNIT_PLAYERS_ONLINE, + ) + + async def async_update(self) -> None: + """Update online players state and device state attributes.""" + self._state = self._server.players_online + + device_state_attributes = None + players_list = self._server.players_list + + if players_list is not None: + if len(players_list) != 0: + device_state_attributes = {ATTR_PLAYERS_LIST: self._server.players_list} + + self._device_state_attributes = device_state_attributes + + @property + def device_state_attributes(self) -> Dict[str, Any]: + """Return players list in device state attributes.""" + return self._device_state_attributes + + +class MinecraftServerPlayersMaxSensor(MinecraftServerSensorEntity): + """Representation of a Minecraft Server maximum number of players sensor.""" + + def __init__(self, server: MinecraftServer) -> None: + """Initialize maximum number of players sensor.""" + super().__init__( + server=server, + type_name=NAME_PLAYERS_MAX, + icon=ICON_PLAYERS_MAX, + unit=UNIT_PLAYERS_MAX, + ) + + async def async_update(self) -> None: + """Update maximum number of players.""" + self._state = self._server.players_max diff --git a/homeassistant/components/minecraft_server/strings.json b/homeassistant/components/minecraft_server/strings.json new file mode 100644 index 00000000000..7743d940be6 --- /dev/null +++ b/homeassistant/components/minecraft_server/strings.json @@ -0,0 +1,24 @@ +{ + "config": { + "title": "Minecraft Server", + "step": { + "user": { + "title": "Link your Minecraft Server", + "description": "Set up your Minecraft Server instance to allow monitoring.", + "data": { + "name": "Name", + "host": "Host", + "port": "Port" + } + } + }, + "error": { + "invalid_port": "Port must be in range from 1024 to 65535. Please correct it and try again.", + "cannot_connect": "Failed to connect to server. Please check the host and port and try again. Also ensure that you are running at least Minecraft version 1.7 on your server.", + "invalid_ip": "IP address is invalid (MAC address could not be determined). Please correct it and try again." + }, + "abort": { + "already_configured": "Host is already configured." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index 720cf7106e7..f43f1c88396 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -52,6 +52,8 @@ ATTR_WEBHOOK_ENCRYPTED = "encrypted" ATTR_WEBHOOK_ENCRYPTED_DATA = "encrypted_data" ATTR_WEBHOOK_TYPE = "type" +ERR_ENCRYPTION_ALREADY_ENABLED = "encryption_already_enabled" +ERR_ENCRYPTION_NOT_AVAILABLE = "encryption_not_available" ERR_ENCRYPTION_REQUIRED = "encryption_required" ERR_SENSOR_NOT_REGISTERED = "not_registered" ERR_SENSOR_DUPLICATE_UNIQUE_ID = "duplicate_unique_id" diff --git a/homeassistant/components/mobile_app/entity.py b/homeassistant/components/mobile_app/entity.py index 27cb9934b18..5200c6b0c12 100644 --- a/homeassistant/components/mobile_app/entity.py +++ b/homeassistant/components/mobile_app/entity.py @@ -7,6 +7,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from .const import ( + ATTR_DEVICE_NAME, ATTR_SENSOR_ATTRIBUTES, ATTR_SENSOR_DEVICE_CLASS, ATTR_SENSOR_ICON, @@ -38,6 +39,7 @@ class MobileAppEntity(Entity): ) self._entity_type = config[ATTR_SENSOR_TYPE] self.unsub_dispatcher = None + self._name = f"{entry.data[ATTR_DEVICE_NAME]} {config[ATTR_SENSOR_NAME]}" async def async_added_to_hass(self): """Register callbacks.""" @@ -58,7 +60,7 @@ class MobileAppEntity(Entity): @property def name(self): """Return the name of the mobile app sensor.""" - return self._config[ATTR_SENSOR_NAME] + return self._name @property def device_class(self): diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 3a477d89925..c47f38986a1 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -1,8 +1,10 @@ """Webhook handlers for mobile_app.""" from functools import wraps import logging +import secrets -from aiohttp.web import HTTPBadRequest, Request, Response +from aiohttp.web import HTTPBadRequest, Request, Response, json_response +from nacl.secret import SecretBox import voluptuous as vol from homeassistant.components.binary_sensor import ( @@ -71,6 +73,8 @@ from .const import ( DATA_DELETED_IDS, DATA_STORE, DOMAIN, + ERR_ENCRYPTION_ALREADY_ENABLED, + ERR_ENCRYPTION_NOT_AVAILABLE, ERR_ENCRYPTION_REQUIRED, ERR_SENSOR_DUPLICATE_UNIQUE_ID, ERR_SENSOR_NOT_REGISTERED, @@ -84,6 +88,7 @@ from .helpers import ( registration_context, safe_registration, savable_state, + supports_encryption, webhook_response, ) @@ -307,6 +312,34 @@ async def webhook_update_registration(hass, config_entry, data): ) +@WEBHOOK_COMMANDS.register("enable_encryption") +async def webhook_enable_encryption(hass, config_entry, data): + """Handle a encryption enable webhook.""" + if config_entry.data[ATTR_SUPPORTS_ENCRYPTION]: + _LOGGER.warning( + "Refusing to enable encryption for %s because it is already enabled!", + config_entry.data[ATTR_DEVICE_NAME], + ) + return error_response( + ERR_ENCRYPTION_ALREADY_ENABLED, "Encryption already enabled" + ) + + if not supports_encryption(): + _LOGGER.warning( + "Unable to enable encryption for %s because libsodium is unavailable!", + config_entry.data[ATTR_DEVICE_NAME], + ) + return error_response(ERR_ENCRYPTION_NOT_AVAILABLE, "Encryption is unavailable") + + secret = secrets.token_hex(SecretBox.KEY_SIZE) + + data = {**config_entry.data, ATTR_SUPPORTS_ENCRYPTION: True, CONF_SECRET: secret} + + hass.config_entries.async_update_entry(config_entry, data=data) + + return json_response({"secret": secret}) + + @WEBHOOK_COMMANDS.register("register_sensor") @validate_schema( { diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 823703ac4c9..218d3d3baa9 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -159,10 +159,10 @@ def setup(hass, config): def write_register(service): """Write Modbus registers.""" - unit = int(float(service.data.get(ATTR_UNIT))) - address = int(float(service.data.get(ATTR_ADDRESS))) - value = service.data.get(ATTR_VALUE) - client_name = service.data.get(ATTR_HUB) + unit = int(float(service.data[ATTR_UNIT])) + address = int(float(service.data[ATTR_ADDRESS])) + value = service.data[ATTR_VALUE] + client_name = service.data[ATTR_HUB] if isinstance(value, list): hub_collect[client_name].write_registers( unit, address, [int(float(i)) for i in value] @@ -172,10 +172,10 @@ def setup(hass, config): def write_coil(service): """Write Modbus coil.""" - unit = service.data.get(ATTR_UNIT) - address = service.data.get(ATTR_ADDRESS) - state = service.data.get(ATTR_STATE) - client_name = service.data.get(ATTR_HUB) + unit = service.data[ATTR_UNIT] + address = service.data[ATTR_ADDRESS] + state = service.data[ATTR_STATE] + client_name = service.data[ATTR_HUB] hub_collect[client_name].write_coil(unit, address, state) hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_modbus) @@ -213,6 +213,12 @@ class ModbusHub: kwargs = {"unit": unit} if unit else {} return self._client.read_coils(address, count, **kwargs) + def read_discrete_inputs(self, unit, address, count): + """Read discrete inputs.""" + with self._lock: + kwargs = {"unit": unit} if unit else {} + return self._client.read_discrete_inputs(address, count, **kwargs) + def read_input_registers(self, unit, address, count): """Read input registers.""" with self._lock: diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index 9a431d24b0c..8ea6e2dbfa6 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -1,7 +1,9 @@ -"""Support for Modbus Coil sensors.""" +"""Support for Modbus Coil and Discrete Input sensors.""" import logging from typing import Optional +from pymodbus.exceptions import ConnectionException, ModbusException +from pymodbus.pdu import ExceptionResponse import voluptuous as vol from homeassistant.components.binary_sensor import ( @@ -16,53 +18,76 @@ from . import CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN _LOGGER = logging.getLogger(__name__) -CONF_COIL = "coil" -CONF_COILS = "coils" +CONF_DEPRECATED_COIL = "coil" +CONF_DEPRECATED_COILS = "coils" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_COILS): [ - { - vol.Required(CONF_COIL): cv.positive_int, - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, - vol.Optional(CONF_SLAVE): cv.positive_int, - } - ] - } +CONF_INPUTS = "inputs" +CONF_INPUT_TYPE = "input_type" +CONF_ADDRESS = "address" + +DEFAULT_INPUT_TYPE_COIL = "coil" +DEFAULT_INPUT_TYPE_DISCRETE = "discrete_input" + +PLATFORM_SCHEMA = vol.All( + cv.deprecated(CONF_DEPRECATED_COILS, CONF_INPUTS), + PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_INPUTS): [ + vol.All( + cv.deprecated(CONF_DEPRECATED_COIL, CONF_ADDRESS), + vol.Schema( + { + vol.Required(CONF_ADDRESS): cv.positive_int, + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, + vol.Optional(CONF_SLAVE): cv.positive_int, + vol.Optional( + CONF_INPUT_TYPE, default=DEFAULT_INPUT_TYPE_COIL + ): vol.In( + [DEFAULT_INPUT_TYPE_COIL, DEFAULT_INPUT_TYPE_DISCRETE] + ), + } + ), + ) + ] + } + ), ) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Modbus binary sensors.""" sensors = [] - for coil in config.get(CONF_COILS): - hub = hass.data[MODBUS_DOMAIN][coil.get(CONF_HUB)] + for entry in config[CONF_INPUTS]: + hub = hass.data[MODBUS_DOMAIN][entry[CONF_HUB]] sensors.append( - ModbusCoilSensor( + ModbusBinarySensor( hub, - coil.get(CONF_NAME), - coil.get(CONF_SLAVE), - coil.get(CONF_COIL), - coil.get(CONF_DEVICE_CLASS), + entry[CONF_NAME], + entry.get(CONF_SLAVE), + entry[CONF_ADDRESS], + entry.get(CONF_DEVICE_CLASS), + entry[CONF_INPUT_TYPE], ) ) add_entities(sensors) -class ModbusCoilSensor(BinarySensorDevice): - """Modbus coil sensor.""" +class ModbusBinarySensor(BinarySensorDevice): + """Modbus binary sensor.""" - def __init__(self, hub, name, slave, coil, device_class): - """Initialize the Modbus coil sensor.""" + def __init__(self, hub, name, slave, address, device_class, input_type): + """Initialize the Modbus binary sensor.""" self._hub = hub self._name = name self._slave = int(slave) if slave else None - self._coil = int(coil) + self._address = int(address) self._device_class = device_class + self._input_type = input_type self._value = None + self._available = True @property def name(self): @@ -79,15 +104,38 @@ class ModbusCoilSensor(BinarySensorDevice): """Return the device class of the sensor.""" return self._device_class + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + def update(self): """Update the state of the sensor.""" - result = self._hub.read_coils(self._slave, self._coil, 1) try: - self._value = result.bits[0] - except AttributeError: - _LOGGER.error( - "No response from hub %s, slave %s, coil %s", - self._hub.name, - self._slave, - self._coil, - ) + if self._input_type == DEFAULT_INPUT_TYPE_COIL: + result = self._hub.read_coils(self._slave, self._address, 1) + else: + result = self._hub.read_discrete_inputs(self._slave, self._address, 1) + except ConnectionException: + self._set_unavailable() + return + + if isinstance(result, (ModbusException, ExceptionResponse)): + self._set_unavailable() + return + + self._value = result.bits[0] + self._available = True + + def _set_unavailable(self): + """Set unavailable state and log it as an error.""" + if not self._available: + return + + _LOGGER.error( + "No response from hub %s, slave %s, address %s", + self._hub.name, + self._slave, + self._address, + ) + self._available = False diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 99ea686543d..c0423849418 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -1,7 +1,10 @@ """Support for Generic Modbus Thermostats.""" import logging import struct +from typing import Optional +from pymodbus.exceptions import ConnectionException, ModbusException +from pymodbus.pdu import ExceptionResponse import voluptuous as vol from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice @@ -24,6 +27,7 @@ _LOGGER = logging.getLogger(__name__) CONF_TARGET_TEMP = "target_temp_register" CONF_CURRENT_TEMP = "current_temp_register" +CONF_CURRENT_TEMP_REGISTER_TYPE = "current_temp_register_type" CONF_DATA_TYPE = "data_type" CONF_COUNT = "data_count" CONF_PRECISION = "precision" @@ -39,6 +43,9 @@ CONF_STEP = "temp_step" SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE HVAC_MODES = [HVAC_MODE_AUTO] +DEFAULT_REGISTER_TYPE_HOLDING = "holding" +DEFAULT_REGISTER_TYPE_INPUT = "input" + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_CURRENT_TEMP): cv.positive_int, @@ -46,6 +53,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Required(CONF_SLAVE): cv.positive_int, vol.Required(CONF_TARGET_TEMP): cv.positive_int, vol.Optional(CONF_COUNT, default=2): cv.positive_int, + vol.Optional( + CONF_CURRENT_TEMP_REGISTER_TYPE, default=DEFAULT_REGISTER_TYPE_HOLDING + ): vol.In([DEFAULT_REGISTER_TYPE_HOLDING, DEFAULT_REGISTER_TYPE_INPUT]), vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_FLOAT): vol.In( [DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT] ), @@ -53,8 +63,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_PRECISION, default=1): cv.positive_int, vol.Optional(CONF_SCALE, default=1): vol.Coerce(float), vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float), - vol.Optional(CONF_MAX_TEMP, default=5): cv.positive_int, - vol.Optional(CONF_MIN_TEMP, default=35): cv.positive_int, + vol.Optional(CONF_MAX_TEMP, default=35): cv.positive_int, + vol.Optional(CONF_MIN_TEMP, default=5): cv.positive_int, vol.Optional(CONF_STEP, default=0.5): vol.Coerce(float), vol.Optional(CONF_UNIT, default="C"): cv.string, } @@ -63,20 +73,21 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Modbus Thermostat Platform.""" - name = config.get(CONF_NAME) - modbus_slave = config.get(CONF_SLAVE) - target_temp_register = config.get(CONF_TARGET_TEMP) - current_temp_register = config.get(CONF_CURRENT_TEMP) - data_type = config.get(CONF_DATA_TYPE) - count = config.get(CONF_COUNT) - precision = config.get(CONF_PRECISION) - scale = config.get(CONF_SCALE) - offset = config.get(CONF_OFFSET) - unit = config.get(CONF_UNIT) - max_temp = config.get(CONF_MAX_TEMP) - min_temp = config.get(CONF_MIN_TEMP) - temp_step = config.get(CONF_STEP) - hub_name = config.get(CONF_HUB) + name = config[CONF_NAME] + modbus_slave = config[CONF_SLAVE] + target_temp_register = config[CONF_TARGET_TEMP] + current_temp_register = config[CONF_CURRENT_TEMP] + current_temp_register_type = config[CONF_CURRENT_TEMP_REGISTER_TYPE] + data_type = config[CONF_DATA_TYPE] + count = config[CONF_COUNT] + precision = config[CONF_PRECISION] + scale = config[CONF_SCALE] + offset = config[CONF_OFFSET] + unit = config[CONF_UNIT] + max_temp = config[CONF_MAX_TEMP] + min_temp = config[CONF_MIN_TEMP] + temp_step = config[CONF_STEP] + hub_name = config[CONF_HUB] hub = hass.data[MODBUS_DOMAIN][hub_name] add_entities( @@ -87,6 +98,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): modbus_slave, target_temp_register, current_temp_register, + current_temp_register_type, data_type, count, precision, @@ -112,6 +124,7 @@ class ModbusThermostat(ClimateDevice): modbus_slave, target_temp_register, current_temp_register, + current_temp_register_type, data_type, count, precision, @@ -128,6 +141,7 @@ class ModbusThermostat(ClimateDevice): self._slave = modbus_slave self._target_temperature_register = target_temp_register self._current_temperature_register = current_temp_register + self._current_temperature_register_type = current_temp_register_type self._target_temperature = None self._current_temperature = None self._data_type = data_type @@ -140,6 +154,7 @@ class ModbusThermostat(ClimateDevice): self._min_temp = min_temp self._temp_step = temp_step self._structure = ">f" + self._available = True data_types = { DATA_TYPE_INT: {1: "h", 2: "i", 4: "q"}, @@ -156,9 +171,11 @@ class ModbusThermostat(ClimateDevice): def update(self): """Update Target & Current Temperature.""" - self._target_temperature = self.read_register(self._target_temperature_register) - self._current_temperature = self.read_register( - self._current_temperature_register + self._target_temperature = self._read_register( + DEFAULT_REGISTER_TYPE_HOLDING, self._target_temperature_register + ) + self._current_temperature = self._read_register( + self._current_temperature_register_type, self._current_temperature_register ) @property @@ -215,20 +232,32 @@ class ModbusThermostat(ClimateDevice): return byte_string = struct.pack(self._structure, target_temperature) register_value = struct.unpack(">h", byte_string[0:2])[0] + self._write_register(self._target_temperature_register, register_value) - try: - self.write_register(self._target_temperature_register, register_value) - except AttributeError as ex: - _LOGGER.error(ex) + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available - def read_register(self, register): - """Read holding register using the Modbus hub slave.""" + def _read_register(self, register_type, register) -> Optional[float]: + """Read register using the Modbus hub slave.""" try: - result = self._hub.read_holding_registers( - self._slave, register, self._count - ) - except AttributeError as ex: - _LOGGER.error(ex) + if register_type == DEFAULT_REGISTER_TYPE_INPUT: + result = self._hub.read_input_registers( + self._slave, register, self._count + ) + else: + result = self._hub.read_holding_registers( + self._slave, register, self._count + ) + except ConnectionException: + self._set_unavailable(register) + return + + if isinstance(result, (ModbusException, ExceptionResponse)): + self._set_unavailable(register) + return + byte_string = b"".join( [x.to_bytes(2, byteorder="big") for x in result.registers] ) @@ -237,8 +266,29 @@ class ModbusThermostat(ClimateDevice): (self._scale * val) + self._offset, f".{self._precision}f" ) register_value = float(register_value) + self._available = True + return register_value - def write_register(self, register, value): - """Write register using the Modbus hub slave.""" - self._hub.write_registers(self._slave, register, [value, 0]) + def _write_register(self, register, value): + """Write holding register using the Modbus hub slave.""" + try: + self._hub.write_registers(self._slave, register, [value, 0]) + except ConnectionException: + self._set_unavailable(register) + return + + self._available = True + + def _set_unavailable(self, register): + """Set unavailable state and log it as an error.""" + if not self._available: + return + + _LOGGER.error( + "No response from hub %s, slave %s, register %s", + self._hub.name, + self._slave, + register, + ) + self._available = False diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 484382983ac..716cb5299b7 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -3,6 +3,8 @@ import logging import struct from typing import Any, Optional, Union +from pymodbus.exceptions import ConnectionException, ModbusException +from pymodbus.pdu import ExceptionResponse import voluptuous as vol from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA @@ -35,8 +37,8 @@ DATA_TYPE_FLOAT = "float" DATA_TYPE_INT = "int" DATA_TYPE_UINT = "uint" -REGISTER_TYPE_HOLDING = "holding" -REGISTER_TYPE_INPUT = "input" +DEFAULT_REGISTER_TYPE_HOLDING = "holding" +DEFAULT_REGISTER_TYPE_INPUT = "input" def number(value: Any) -> Union[int, float]: @@ -72,9 +74,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, vol.Optional(CONF_OFFSET, default=0): number, vol.Optional(CONF_PRECISION, default=0): cv.positive_int, - vol.Optional(CONF_REGISTER_TYPE, default=REGISTER_TYPE_HOLDING): vol.In( - [REGISTER_TYPE_HOLDING, REGISTER_TYPE_INPUT] - ), + vol.Optional( + CONF_REGISTER_TYPE, default=DEFAULT_REGISTER_TYPE_HOLDING + ): vol.In([DEFAULT_REGISTER_TYPE_HOLDING, DEFAULT_REGISTER_TYPE_INPUT]), vol.Optional(CONF_REVERSE_ORDER, default=False): cv.boolean, vol.Optional(CONF_SCALE, default=1): number, vol.Optional(CONF_SLAVE): cv.positive_int, @@ -93,17 +95,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): data_types[DATA_TYPE_UINT] = {1: "H", 2: "I", 4: "Q"} data_types[DATA_TYPE_FLOAT] = {1: "e", 2: "f", 4: "d"} - for register in config.get(CONF_REGISTERS): + for register in config[CONF_REGISTERS]: structure = ">i" - if register.get(CONF_DATA_TYPE) != DATA_TYPE_CUSTOM: + if register[CONF_DATA_TYPE] != DATA_TYPE_CUSTOM: try: structure = ">{}".format( - data_types[register.get(CONF_DATA_TYPE)][register.get(CONF_COUNT)] + data_types[register[CONF_DATA_TYPE]][register[CONF_COUNT]] ) except KeyError: _LOGGER.error( "Unable to detect data type for %s sensor, try a custom type", - register.get(CONF_NAME), + register[CONF_NAME], ) continue else: @@ -112,35 +114,33 @@ def setup_platform(hass, config, add_entities, discovery_info=None): try: size = struct.calcsize(structure) except struct.error as err: - _LOGGER.error( - "Error in sensor %s structure: %s", register.get(CONF_NAME), err - ) + _LOGGER.error("Error in sensor %s structure: %s", register[CONF_NAME], err) continue - if register.get(CONF_COUNT) * 2 != size: + if register[CONF_COUNT] * 2 != size: _LOGGER.error( "Structure size (%d bytes) mismatch registers count (%d words)", size, - register.get(CONF_COUNT), + register[CONF_COUNT], ) continue - hub_name = register.get(CONF_HUB) + hub_name = register[CONF_HUB] hub = hass.data[MODBUS_DOMAIN][hub_name] sensors.append( ModbusRegisterSensor( hub, - register.get(CONF_NAME), + register[CONF_NAME], register.get(CONF_SLAVE), - register.get(CONF_REGISTER), - register.get(CONF_REGISTER_TYPE), + register[CONF_REGISTER], + register[CONF_REGISTER_TYPE], register.get(CONF_UNIT_OF_MEASUREMENT), - register.get(CONF_COUNT), - register.get(CONF_REVERSE_ORDER), - register.get(CONF_SCALE), - register.get(CONF_OFFSET), + register[CONF_COUNT], + register[CONF_REVERSE_ORDER], + register[CONF_SCALE], + register[CONF_OFFSET], structure, - register.get(CONF_PRECISION), + register[CONF_PRECISION], register.get(CONF_DEVICE_CLASS), ) ) @@ -184,6 +184,7 @@ class ModbusRegisterSensor(RestoreEntity): self._structure = structure self._device_class = device_class self._value = None + self._available = True async def async_added_to_hass(self): """Handle entity which will be added.""" @@ -212,30 +213,34 @@ class ModbusRegisterSensor(RestoreEntity): """Return the device class of the sensor.""" return self._device_class + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + def update(self): """Update the state of the sensor.""" - if self._register_type == REGISTER_TYPE_INPUT: - result = self._hub.read_input_registers( - self._slave, self._register, self._count - ) - else: - result = self._hub.read_holding_registers( - self._slave, self._register, self._count - ) - val = 0 - try: - registers = result.registers - if self._reverse_order: - registers.reverse() - except AttributeError: - _LOGGER.error( - "No response from hub %s, slave %s, register %s", - self._hub.name, - self._slave, - self._register, - ) + if self._register_type == DEFAULT_REGISTER_TYPE_INPUT: + result = self._hub.read_input_registers( + self._slave, self._register, self._count + ) + else: + result = self._hub.read_holding_registers( + self._slave, self._register, self._count + ) + except ConnectionException: + self._set_unavailable() return + + if isinstance(result, (ModbusException, ExceptionResponse)): + self._set_unavailable() + return + + registers = result.registers + if self._reverse_order: + registers.reverse() + byte_string = b"".join([x.to_bytes(2, byteorder="big") for x in registers]) val = struct.unpack(self._structure, byte_string)[0] val = self._scale * val + self._offset @@ -245,3 +250,18 @@ class ModbusRegisterSensor(RestoreEntity): self._value += "." + "0" * self._precision else: self._value = f"{val:.{self._precision}f}" + + self._available = True + + def _set_unavailable(self): + """Set unavailable state and log it as an error.""" + if not self._available: + return + + _LOGGER.error( + "No response from hub %s, slave %s, address %s", + self._hub.name, + self._slave, + self._register, + ) + self._available = False diff --git a/homeassistant/components/modbus/services.yaml b/homeassistant/components/modbus/services.yaml index 2158528814f..8c11209570b 100644 --- a/homeassistant/components/modbus/services.yaml +++ b/homeassistant/components/modbus/services.yaml @@ -4,9 +4,11 @@ write_coil: address: {description: Address of the register to write to., example: 0} state: {description: State to write., example: false} unit: {description: Address of the modbus unit., example: 21} + hub: {description: Optional Modbus hub name. A hub with the name 'default' is used if not specified., example: "hub1"} write_register: description: Write to a modbus holding register. fields: address: {description: Address of the holding register to write to., example: 0} unit: {description: Address of the modbus unit., example: 21} value: {description: Value (single value or array) to write., example: "0 or [4,0]"} + hub: {description: Optional Modbus hub name. A hub with the name 'default' is used if not specified., example: "hub1"} diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index 0ed33dedb57..d4f52622538 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -1,6 +1,9 @@ """Support for Modbus switches.""" import logging +from typing import Optional +from pymodbus.exceptions import ConnectionException, ModbusException +from pymodbus.pdu import ExceptionResponse import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA @@ -29,8 +32,8 @@ CONF_STATE_ON = "state_on" CONF_VERIFY_REGISTER = "verify_register" CONF_VERIFY_STATE = "verify_state" -REGISTER_TYPE_HOLDING = "holding" -REGISTER_TYPE_INPUT = "input" +DEFAULT_REGISTER_TYPE_HOLDING = "holding" +DEFAULT_REGISTER_TYPE_INPUT = "input" REGISTERS_SCHEMA = vol.Schema( { @@ -39,8 +42,8 @@ REGISTERS_SCHEMA = vol.Schema( vol.Required(CONF_NAME): cv.string, vol.Required(CONF_REGISTER): cv.positive_int, vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, - vol.Optional(CONF_REGISTER_TYPE, default=REGISTER_TYPE_HOLDING): vol.In( - [REGISTER_TYPE_HOLDING, REGISTER_TYPE_INPUT] + vol.Optional(CONF_REGISTER_TYPE, default=DEFAULT_REGISTER_TYPE_HOLDING): vol.In( + [DEFAULT_REGISTER_TYPE_HOLDING, DEFAULT_REGISTER_TYPE_INPUT] ), vol.Optional(CONF_SLAVE): cv.positive_int, vol.Optional(CONF_STATE_OFF): cv.positive_int, @@ -74,30 +77,30 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Read configuration and create Modbus devices.""" switches = [] if CONF_COILS in config: - for coil in config.get(CONF_COILS): - hub_name = coil.get(CONF_HUB) + for coil in config[CONF_COILS]: + hub_name = coil[CONF_HUB] hub = hass.data[MODBUS_DOMAIN][hub_name] switches.append( ModbusCoilSwitch( - hub, coil.get(CONF_NAME), coil.get(CONF_SLAVE), coil.get(CONF_COIL) + hub, coil[CONF_NAME], coil[CONF_SLAVE], coil[CONF_COIL] ) ) if CONF_REGISTERS in config: - for register in config.get(CONF_REGISTERS): - hub_name = register.get(CONF_HUB) + for register in config[CONF_REGISTERS]: + hub_name = register[CONF_HUB] hub = hass.data[MODBUS_DOMAIN][hub_name] switches.append( ModbusRegisterSwitch( hub, - register.get(CONF_NAME), + register[CONF_NAME], register.get(CONF_SLAVE), - register.get(CONF_REGISTER), - register.get(CONF_COMMAND_ON), - register.get(CONF_COMMAND_OFF), - register.get(CONF_VERIFY_STATE), + register[CONF_REGISTER], + register[CONF_COMMAND_ON], + register[CONF_COMMAND_OFF], + register[CONF_VERIFY_STATE], register.get(CONF_VERIFY_REGISTER), - register.get(CONF_REGISTER_TYPE), + register[CONF_REGISTER_TYPE], register.get(CONF_STATE_ON), register.get(CONF_STATE_OFF), ) @@ -116,6 +119,7 @@ class ModbusCoilSwitch(ToggleEntity, RestoreEntity): self._slave = int(slave) if slave else None self._coil = int(coil) self._is_on = None + self._available = True async def async_added_to_hass(self): """Handle entity which will be added.""" @@ -134,26 +138,62 @@ class ModbusCoilSwitch(ToggleEntity, RestoreEntity): """Return the name of the switch.""" return self._name + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + def turn_on(self, **kwargs): """Set switch on.""" - self._hub.write_coil(self._slave, self._coil, True) + self._write_coil(self._coil, True) def turn_off(self, **kwargs): """Set switch off.""" - self._hub.write_coil(self._slave, self._coil, False) + self._write_coil(self._coil, False) def update(self): """Update the state of the switch.""" - result = self._hub.read_coils(self._slave, self._coil, 1) + self._is_on = self._read_coil(self._coil) + + def _read_coil(self, coil) -> Optional[bool]: + """Read coil using the Modbus hub slave.""" try: - self._is_on = bool(result.bits[0]) - except AttributeError: - _LOGGER.error( - "No response from hub %s, slave %s, coil %s", - self._hub.name, - self._slave, - self._coil, - ) + result = self._hub.read_coils(self._slave, coil, 1) + except ConnectionException: + self._set_unavailable() + return + + if isinstance(result, (ModbusException, ExceptionResponse)): + self._set_unavailable() + return + + value = bool(result.bits[0]) + self._available = True + + return value + + def _write_coil(self, coil, value): + """Write coil using the Modbus hub slave.""" + try: + self._hub.write_coil(self._slave, coil, value) + except ConnectionException: + self._set_unavailable() + return + + self._available = True + + def _set_unavailable(self): + """Set unavailable state and log it as an error.""" + if not self._available: + return + + _LOGGER.error( + "No response from hub %s, slave %s, coil %s", + self._hub.name, + self._slave, + self._coil, + ) + self._available = False class ModbusRegisterSwitch(ModbusCoilSwitch): @@ -184,6 +224,7 @@ class ModbusRegisterSwitch(ModbusCoilSwitch): self._verify_state = verify_state self._verify_register = verify_register if verify_register else self._register self._register_type = register_type + self._available = True if state_on is not None: self._state_on = state_on @@ -199,46 +240,86 @@ class ModbusRegisterSwitch(ModbusCoilSwitch): def turn_on(self, **kwargs): """Set switch on.""" - self._hub.write_register(self._slave, self._register, self._command_on) - if not self._verify_state: - self._is_on = True + + # Only holding register is writable + if self._register_type == DEFAULT_REGISTER_TYPE_HOLDING: + self._write_register(self._command_on) + if not self._verify_state: + self._is_on = True def turn_off(self, **kwargs): """Set switch off.""" - self._hub.write_register(self._slave, self._register, self._command_off) - if not self._verify_state: - self._is_on = False + + # Only holding register is writable + if self._register_type == DEFAULT_REGISTER_TYPE_HOLDING: + self._write_register(self._command_off) + if not self._verify_state: + self._is_on = False + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available def update(self): """Update the state of the switch.""" if not self._verify_state: return - value = 0 - if self._register_type == REGISTER_TYPE_INPUT: - result = self._hub.read_input_registers(self._slave, self._register, 1) - else: - result = self._hub.read_holding_registers(self._slave, self._register, 1) - - try: - value = int(result.registers[0]) - except AttributeError: - _LOGGER.error( - "No response from hub %s, slave %s, register %s", - self._hub.name, - self._slave, - self._verify_register, - ) - + value = self._read_register() if value == self._state_on: self._is_on = True elif value == self._state_off: self._is_on = False - else: + elif value is not None: _LOGGER.error( "Unexpected response from hub %s, slave %s register %s, got 0x%2x", self._hub.name, self._slave, - self._verify_register, + self._register, value, ) + + def _read_register(self) -> Optional[int]: + try: + if self._register_type == DEFAULT_REGISTER_TYPE_INPUT: + result = self._hub.read_input_registers(self._slave, self._register, 1) + else: + result = self._hub.read_holding_registers( + self._slave, self._register, 1 + ) + except ConnectionException: + self._set_unavailable() + return + + if isinstance(result, (ModbusException, ExceptionResponse)): + self._set_unavailable() + return + + value = int(result.registers[0]) + self._available = True + + return value + + def _write_register(self, value): + """Write holding register using the Modbus hub slave.""" + try: + self._hub.write_register(self._slave, self._register, value) + except ConnectionException: + self._set_unavailable() + return + + self._available = True + + def _set_unavailable(self): + """Set unavailable state and log it as an error.""" + if not self._available: + return + + _LOGGER.error( + "No response from hub %s, slave %s, register %s", + self._hub.name, + self._slave, + self._register, + ) + self._available = False diff --git a/homeassistant/components/mqtt/.translations/ca.json b/homeassistant/components/mqtt/.translations/ca.json index 47dc4d344bc..b8cce4bd808 100644 --- a/homeassistant/components/mqtt/.translations/ca.json +++ b/homeassistant/components/mqtt/.translations/ca.json @@ -27,5 +27,27 @@ } }, "title": "MQTT" + }, + "device_automation": { + "trigger_subtype": { + "button_1": "Primer bot\u00f3", + "button_2": "Segon bot\u00f3", + "button_3": "Tercer bot\u00f3", + "button_4": "Quart bot\u00f3", + "button_5": "Cinqu\u00e8 bot\u00f3", + "button_6": "Sis\u00e8 bot\u00f3", + "turn_off": "Desactiva", + "turn_on": "Activa" + }, + "trigger_type": { + "button_double_press": "\"{subtype}\" clicat dues vegades", + "button_long_press": "\"{subtype}\" premut cont\u00ednuament", + "button_long_release": "\"{subtype}\" alliberat despr\u00e9s d'una estona premut", + "button_quadruple_press": "\"{subtype}\" clicat quatre vegades", + "button_quintuple_press": "\"{subtype}\" clicat cinc vegades", + "button_short_press": "\"{subtype}\" premut", + "button_short_release": "\"{subtype}\" alliberat", + "button_triple_press": "\"{subtype}\" clicat tres vegades" + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/da.json b/homeassistant/components/mqtt/.translations/da.json index 93ea57d49ea..e018ab7aa14 100644 --- a/homeassistant/components/mqtt/.translations/da.json +++ b/homeassistant/components/mqtt/.translations/da.json @@ -27,5 +27,27 @@ } }, "title": "MQTT" + }, + "device_automation": { + "trigger_subtype": { + "button_1": "F\u00f8rste knap", + "button_2": "Anden knap", + "button_3": "Tredje knap", + "button_4": "Fjerde knap", + "button_5": "Femte knap", + "button_6": "Sjette knap", + "turn_off": "Sluk", + "turn_on": "T\u00e6nd" + }, + "trigger_type": { + "button_double_press": "\"{subtype}\" dobbeltklikket", + "button_long_press": "\"{subtype}\" trykket p\u00e5 konstant", + "button_long_release": "\"{subtype}\" sluppet efter langt tryk", + "button_quadruple_press": "\"{subtype}\" firedobbelt-klikket", + "button_quintuple_press": "\"{subtype}\" femdobbelt-klikket", + "button_short_press": "\"{subtype}\" trykket p\u00e5", + "button_short_release": "\"{subtype}\" sluppet", + "button_triple_press": "\"{subtype}\" tredobbeltklikket" + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/de.json b/homeassistant/components/mqtt/.translations/de.json index d95c43cc618..87c6a989f52 100644 --- a/homeassistant/components/mqtt/.translations/de.json +++ b/homeassistant/components/mqtt/.translations/de.json @@ -22,10 +22,32 @@ "data": { "discovery": "Suche aktivieren" }, - "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass er eine Verbindung mit dem MQTT-Broker herstellt, der vom Add-on hass.io {addon} bereitgestellt wird?", + "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass er eine Verbindung mit dem MQTT-Broker herstellt, der vom Hass.io Add-on {addon} bereitgestellt wird?", "title": "MQTT Broker per Hass.io add-on" } }, "title": "MQTT" + }, + "device_automation": { + "trigger_subtype": { + "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", + "turn_off": "Ausschalten", + "turn_on": "Einschalten" + }, + "trigger_type": { + "button_double_press": "\"{subtype}\" doppelt angeklickt", + "button_long_press": "\"{subtype}\" kontinuierlich gedr\u00fcckt", + "button_long_release": "\"{subtype}\" nach langem Dr\u00fccken freigegeben", + "button_quadruple_press": "\"{subtype}\" Vierfach geklickt", + "button_quintuple_press": "\"{subtype}\" f\u00fcnffach geklickt", + "button_short_press": "\"{subtype}\" gedr\u00fcckt", + "button_short_release": "\"{subtype}\" freigegeben", + "button_triple_press": "\"{subtype}\" dreifach geklickt" + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/en.json b/homeassistant/components/mqtt/.translations/en.json index ad18951a9d7..55baf3b7f0e 100644 --- a/homeassistant/components/mqtt/.translations/en.json +++ b/homeassistant/components/mqtt/.translations/en.json @@ -27,5 +27,27 @@ } }, "title": "MQTT" + }, + "device_automation": { + "trigger_subtype": { + "button_1": "First button", + "button_2": "Second button", + "button_3": "Third button", + "button_4": "Fourth button", + "button_5": "Fifth button", + "button_6": "Sixth button", + "turn_off": "Turn off", + "turn_on": "Turn on" + }, + "trigger_type": { + "button_double_press": "\"{subtype}\" double clicked", + "button_long_press": "\"{subtype}\" continuously pressed", + "button_long_release": "\"{subtype}\" released after long press", + "button_quadruple_press": "\"{subtype}\" quadruple clicked", + "button_quintuple_press": "\"{subtype}\" quintuple clicked", + "button_short_press": "\"{subtype}\" pressed", + "button_short_release": "\"{subtype}\" released", + "button_triple_press": "\"{subtype}\" triple clicked" + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/es.json b/homeassistant/components/mqtt/.translations/es.json index e0c94ac621a..a705a885494 100644 --- a/homeassistant/components/mqtt/.translations/es.json +++ b/homeassistant/components/mqtt/.translations/es.json @@ -27,5 +27,27 @@ } }, "title": "MQTT" + }, + "device_automation": { + "trigger_subtype": { + "button_1": "Primer bot\u00f3n", + "button_2": "Segundo bot\u00f3n", + "button_3": "Tercer bot\u00f3n", + "button_4": "Cuarto bot\u00f3n", + "button_5": "Quinto bot\u00f3n", + "button_6": "Sexto bot\u00f3n", + "turn_off": "Apagar", + "turn_on": "Encender" + }, + "trigger_type": { + "button_double_press": "\"{subtype}\" doble pulsaci\u00f3n", + "button_long_press": "\"{subtype}\" pulsado continuamente", + "button_long_release": "\"{subtype}\" soltado despu\u00e9s de pulsaci\u00f3n larga", + "button_quadruple_press": "\"{subtype}\" cu\u00e1druple pulsaci\u00f3n", + "button_quintuple_press": "\"{subtype}\" quintuple pulsaci\u00f3n", + "button_short_press": "\"{subtype}\" pulsado", + "button_short_release": "\"{subtype}\" soltado", + "button_triple_press": "\"{subtype}\" triple pulsaci\u00f3n" + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/it.json b/homeassistant/components/mqtt/.translations/it.json index cf2b3ddf7d5..45f7f8dcdb5 100644 --- a/homeassistant/components/mqtt/.translations/it.json +++ b/homeassistant/components/mqtt/.translations/it.json @@ -27,5 +27,27 @@ } }, "title": "MQTT" + }, + "device_automation": { + "trigger_subtype": { + "button_1": "Primo pulsante", + "button_2": "Secondo pulsante", + "button_3": "Terzo pulsante", + "button_4": "Quarto pulsante", + "button_5": "Quinto pulsante", + "button_6": "Sesto pulsante", + "turn_off": "Spegni", + "turn_on": "Accendi" + }, + "trigger_type": { + "button_double_press": "\"{subtype}\" cliccato due volte", + "button_long_press": "\"{subtype}\" premuto continuamente", + "button_long_release": "\"{subtype}\" rilasciato dopo una lunga pressione", + "button_quadruple_press": "\"{subtype}\" cliccato quattro volte", + "button_quintuple_press": "\"{subtype}\" cliccato cinque volte", + "button_short_press": "\"{subtype}\" premuto", + "button_short_release": "\"{subtype}\" rilasciato", + "button_triple_press": "\"{subtype}\" cliccato tre volte" + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/ko.json b/homeassistant/components/mqtt/.translations/ko.json index 307a6aaadeb..8a0243013d9 100644 --- a/homeassistant/components/mqtt/.translations/ko.json +++ b/homeassistant/components/mqtt/.translations/ko.json @@ -27,5 +27,27 @@ } }, "title": "MQTT" + }, + "device_automation": { + "trigger_subtype": { + "button_1": "\uccab \ubc88\uc9f8 \ubc84\ud2bc", + "button_2": "\ub450 \ubc88\uc9f8 \ubc84\ud2bc", + "button_3": "\uc138 \ubc88\uc9f8 \ubc84\ud2bc", + "button_4": "\ub124 \ubc88\uc9f8 \ubc84\ud2bc", + "button_5": "\ub2e4\uc12f \ubc88\uc9f8 \ubc84\ud2bc", + "button_6": "\uc5ec\uc12f \ubc88\uc9f8 \ubc84\ud2bc", + "turn_off": "\ub044\uae30", + "turn_on": "\ucf1c\uae30" + }, + "trigger_type": { + "button_double_press": "\"{subtype}\" \uc774 \ub450 \ubc88 \ub20c\ub9b4 \ub54c", + "button_long_press": "\"{subtype}\" \uc774 \uacc4\uc18d \ub20c\ub824\uc9c8 \ub54c", + "button_long_release": "\"{subtype}\" \uc774 \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \uc190\uc744 \ub5c4 \ub54c", + "button_quadruple_press": "\"{subtype}\" \uc774 \ub124 \ubc88 \ub20c\ub9b4 \ub54c", + "button_quintuple_press": "\"{subtype}\" \uc774 \ub2e4\uc12f \ubc88 \ub20c\ub9b4 \ub54c", + "button_short_press": "\"{subtype}\" \uc774 \ub20c\ub9b4 \ub54c", + "button_short_release": "\"{subtype}\" \uc5d0\uc11c \uc190\uc744 \ub5c4 \ub54c", + "button_triple_press": "\"{subtype}\" \uc774 \uc138 \ubc88 \ub20c\ub9b4 \ub54c" + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/lb.json b/homeassistant/components/mqtt/.translations/lb.json index 9dcd9c58a3a..9467ab8a9a7 100644 --- a/homeassistant/components/mqtt/.translations/lb.json +++ b/homeassistant/components/mqtt/.translations/lb.json @@ -27,5 +27,17 @@ } }, "title": "MQTT" + }, + "device_automation": { + "trigger_subtype": { + "button_1": "\u00c9ischte Kn\u00e4ppchen", + "button_2": "Zweete Kn\u00e4ppchen", + "button_3": "Dr\u00ebtte Kn\u00e4ppchen", + "button_4": "V\u00e9ierte Kn\u00e4ppchen", + "button_5": "F\u00ebnnefte Kn\u00e4ppchen", + "button_6": "Sechste Kn\u00e4ppchen", + "turn_off": "Ausschalten", + "turn_on": "Uschalten" + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/no.json b/homeassistant/components/mqtt/.translations/no.json index 8dcc0bded9f..27a77a25226 100644 --- a/homeassistant/components/mqtt/.translations/no.json +++ b/homeassistant/components/mqtt/.translations/no.json @@ -27,5 +27,27 @@ } }, "title": "MQTT" + }, + "device_automation": { + "trigger_subtype": { + "button_1": "F\u00f8rste knapp", + "button_2": "Andre knapp", + "button_3": "Tredje knapp", + "button_4": "Fjerde knapp", + "button_5": "Femte knapp", + "button_6": "Sjette knapp", + "turn_off": "Skru av", + "turn_on": "Sl\u00e5 p\u00e5" + }, + "trigger_type": { + "button_double_press": "\"{subtype}\" dobbeltklikket", + "button_long_press": "{subtype}\" trykket kontinuerlig", + "button_long_release": "\"{subtype}\" utgitt etter lang trykk", + "button_quadruple_press": "\"{subtype}\" firedoblet klikket", + "button_quintuple_press": "\"{subtype}\" quintuple klikket", + "button_short_press": "{subtype}\u00bb trykket", + "button_short_release": "\"{subtype}\" utgitt", + "button_triple_press": "\"{subtype}\" trippel klikket" + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/pl.json b/homeassistant/components/mqtt/.translations/pl.json index 24cdeb0f12e..86561f89d2b 100644 --- a/homeassistant/components/mqtt/.translations/pl.json +++ b/homeassistant/components/mqtt/.translations/pl.json @@ -27,5 +27,27 @@ } }, "title": "MQTT" + }, + "device_automation": { + "trigger_subtype": { + "button_1": "pierwszy przycisk", + "button_2": "drugi przycisk", + "button_3": "trzeci przycisk", + "button_4": "czwarty przycisk", + "button_5": "pi\u0105ty przycisk", + "button_6": "sz\u00f3sty przycisk", + "turn_off": "nast\u0105pi wy\u0142\u0105czenie", + "turn_on": "nast\u0105pi w\u0142\u0105czenie" + }, + "trigger_type": { + "button_double_press": "\"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty", + "button_long_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", + "button_long_release": "\"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", + "button_quadruple_press": "\"{subtype}\" zostanie czterokrotnie naci\u015bni\u0119ty", + "button_quintuple_press": "\"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty", + "button_short_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty", + "button_short_release": "\"{subtype}\" zostanie zwolniony", + "button_triple_press": "\"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty" + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/ro.json b/homeassistant/components/mqtt/.translations/ro.json index bcd150e3063..0bbf39315d9 100644 --- a/homeassistant/components/mqtt/.translations/ro.json +++ b/homeassistant/components/mqtt/.translations/ro.json @@ -22,7 +22,7 @@ "data": { "discovery": "Activa\u021bi descoperirea" }, - "description": "Dori\u021bi s\u0103 configura\u021bi Home Assistant pentru a v\u0103 conecta la brokerul MQTT furnizat de addon-ul {addon} ?", + "description": "Dori\u021bi s\u0103 configura\u021bi Home Assistant pentru a se conecta la brokerul MQTT furnizat de addon-ul {addon} ?", "title": "MQTT Broker, prin intermediul Hass.io add-on" } }, diff --git a/homeassistant/components/mqtt/.translations/ru.json b/homeassistant/components/mqtt/.translations/ru.json index 925b8cf5ab4..3559fcc6b2b 100644 --- a/homeassistant/components/mqtt/.translations/ru.json +++ b/homeassistant/components/mqtt/.translations/ru.json @@ -27,5 +27,27 @@ } }, "title": "MQTT" + }, + "device_automation": { + "trigger_subtype": { + "button_1": "\u041f\u0435\u0440\u0432\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_2": "\u0412\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_3": "\u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "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", + "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": { + "button_double_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430", + "button_long_press": "\"{subtype}\" \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e \u043d\u0430\u0436\u0430\u0442\u0430", + "button_long_release": "\"{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", + "button_quadruple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430", + "button_quintuple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437", + "button_short_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430", + "button_short_release": "\"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430", + "button_triple_press": "\"{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/mqtt/.translations/zh-Hans.json b/homeassistant/components/mqtt/.translations/zh-Hans.json index f30e1bf10b4..c12004236bd 100644 --- a/homeassistant/components/mqtt/.translations/zh-Hans.json +++ b/homeassistant/components/mqtt/.translations/zh-Hans.json @@ -27,5 +27,27 @@ } }, "title": "MQTT" + }, + "device_automation": { + "trigger_subtype": { + "button_1": "\u7b2c\u4e00\u4e2a\u6309\u94ae", + "button_2": "\u7b2c\u4e8c\u4e2a\u6309\u94ae", + "button_3": "\u7b2c\u4e09\u4e2a\u6309\u94ae", + "button_4": "\u7b2c\u56db\u4e2a\u6309\u94ae", + "button_5": "\u7b2c\u4e94\u4e2a\u6309\u94ae", + "button_6": "\u7b2c\u516d\u4e2a\u6309\u94ae", + "turn_off": "\u5173\u95ed", + "turn_on": "\u6253\u5f00" + }, + "trigger_type": { + "button_double_press": "\"{subtype}\" \u53cc\u51fb", + "button_long_press": "\"{subtype}\" \u6301\u7eed\u6309\u4e0b", + "button_long_release": "\"{subtype}\" \u957f\u6309\u540e\u91ca\u653e", + "button_quadruple_press": "\"{subtype}\" \u56db\u8fde\u51fb", + "button_quintuple_press": "\"{subtype}\" \u4e94\u8fde\u51fb", + "button_short_press": "\"{subtype}\" \u6309\u4e0b", + "button_short_release": "\"{subtype}\" \u91ca\u653e", + "button_triple_press": "\"{subtype}\" \u4e09\u8fde\u51fb" + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/zh-Hant.json b/homeassistant/components/mqtt/.translations/zh-Hant.json index 09f2f44a902..3c57e7b6bb0 100644 --- a/homeassistant/components/mqtt/.translations/zh-Hant.json +++ b/homeassistant/components/mqtt/.translations/zh-Hant.json @@ -27,5 +27,27 @@ } }, "title": "MQTT" + }, + "device_automation": { + "trigger_subtype": { + "button_1": "\u7b2c\u4e00\u500b\u6309\u9215", + "button_2": "\u7b2c\u4e8c\u500b\u6309\u9215", + "button_3": "\u7b2c\u4e09\u500b\u6309\u9215", + "button_4": "\u7b2c\u56db\u500b\u6309\u9215", + "button_5": "\u7b2c\u4e94\u500b\u6309\u9215", + "button_6": "\u7b2c\u516d\u500b\u6309\u9215", + "turn_off": "\u95dc\u9589", + "turn_on": "\u958b\u555f" + }, + "trigger_type": { + "button_double_press": "\"{subtype}\" \u96d9\u64ca", + "button_long_press": "\"{subtype}\" \u6301\u7e8c\u6309\u4e0b", + "button_long_release": "\"{subtype}\" \u9577\u6309\u5f8c\u91cb\u653e", + "button_quadruple_press": "\"{subtype}\" \u56db\u9023\u64ca", + "button_quintuple_press": "\"{subtype}\" \u4e94\u9023\u64ca", + "button_short_press": "\"{subtype}\" \u6309\u4e0b", + "button_short_release": "\"{subtype}\" \u91cb\u653e", + "button_triple_press": "\"{subtype}\" \u4e09\u9023\u64ca" + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index f64c643f0f4..540d09d7c9f 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -568,7 +568,7 @@ async def async_setup_entry(hass, entry): conf = CONFIG_SCHEMA({DOMAIN: entry.data})[DOMAIN] elif any(key in conf for key in entry.data): _LOGGER.warning( - "Data in your config entry is going to override your " + "Data in your configuration entry is going to override your " "configuration.yaml: %s", entry.data, ) @@ -1194,6 +1194,34 @@ class MqttDiscoveryUpdate(Entity): ) +def device_info_from_config(config): + """Return a device description for device registry.""" + if not config: + return None + + info = { + "identifiers": {(DOMAIN, id_) for id_ in config[CONF_IDENTIFIERS]}, + "connections": {tuple(x) for x in config[CONF_CONNECTIONS]}, + } + + if CONF_MANUFACTURER in config: + info["manufacturer"] = config[CONF_MANUFACTURER] + + if CONF_MODEL in config: + info["model"] = config[CONF_MODEL] + + if CONF_NAME in config: + info["name"] = config[CONF_NAME] + + if CONF_SW_VERSION in config: + info["sw_version"] = config[CONF_SW_VERSION] + + if CONF_VIA_DEVICE in config: + info["via_device"] = (DOMAIN, config[CONF_VIA_DEVICE]) + + return info + + class MqttEntityDeviceInfo(Entity): """Mixin used for mqtt platforms that support the device registry.""" @@ -1216,32 +1244,7 @@ class MqttEntityDeviceInfo(Entity): @property def device_info(self): """Return a device description for device registry.""" - if not self._device_config: - return None - - info = { - "identifiers": { - (DOMAIN, id_) for id_ in self._device_config[CONF_IDENTIFIERS] - }, - "connections": {tuple(x) for x in self._device_config[CONF_CONNECTIONS]}, - } - - if CONF_MANUFACTURER in self._device_config: - info["manufacturer"] = self._device_config[CONF_MANUFACTURER] - - if CONF_MODEL in self._device_config: - info["model"] = self._device_config[CONF_MODEL] - - if CONF_NAME in self._device_config: - info["name"] = self._device_config[CONF_NAME] - - if CONF_SW_VERSION in self._device_config: - info["sw_version"] = self._device_config[CONF_SW_VERSION] - - if CONF_VIA_DEVICE in self._device_config: - info["via_device"] = (DOMAIN, self._device_config[CONF_VIA_DEVICE]) - - return info + return device_info_from_config(self._device_config) @websocket_api.async_response diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 6f9b1720102..6cfab66c3f1 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -3,6 +3,7 @@ ABBREVIATIONS = { "act_t": "action_topic", "act_tpl": "action_template", + "atype": "automation_type", "aux_cmd_t": "aux_command_topic", "aux_stat_tpl": "aux_state_template", "aux_stat_t": "aux_state_topic", @@ -80,6 +81,7 @@ ABBREVIATIONS = { "osc_cmd_t": "oscillation_command_topic", "osc_stat_t": "oscillation_state_topic", "osc_val_tpl": "oscillation_value_template", + "pl": "payload", "pl_arm_away": "payload_arm_away", "pl_arm_home": "payload_arm_home", "pl_arm_nite": "payload_arm_night", @@ -132,14 +134,17 @@ ABBREVIATIONS = { "spds": "speeds", "src_type": "source_type", "stat_clsd": "state_closed", + "stat_closing": "state_closing", "stat_off": "state_off", "stat_on": "state_on", "stat_open": "state_open", + "stat_opening": "state_opening", "stat_locked": "state_locked", "stat_unlocked": "state_unlocked", "stat_t": "state_topic", "stat_tpl": "state_template", "stat_val_tpl": "state_value_template", + "stype": "subtype", "sup_feat": "supported_features", "swing_mode_cmd_t": "swing_mode_command_topic", "swing_mode_stat_tpl": "swing_mode_state_template", diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index e6cfab90c26..885343b7090 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -25,7 +25,9 @@ from homeassistant.const import ( CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, STATE_CLOSED, + STATE_CLOSING, STATE_OPEN, + STATE_OPENING, STATE_UNKNOWN, ) from homeassistant.core import callback @@ -64,7 +66,9 @@ CONF_PAYLOAD_STOP = "payload_stop" CONF_POSITION_CLOSED = "position_closed" CONF_POSITION_OPEN = "position_open" CONF_STATE_CLOSED = "state_closed" +CONF_STATE_CLOSING = "state_closing" CONF_STATE_OPEN = "state_open" +CONF_STATE_OPENING = "state_opening" CONF_TILT_CLOSED_POSITION = "tilt_closed_value" CONF_TILT_INVERT_STATE = "tilt_invert_state" CONF_TILT_MAX = "tilt_max" @@ -131,7 +135,9 @@ PLATFORM_SCHEMA = vol.All( vol.Optional(CONF_SET_POSITION_TEMPLATE): cv.template, vol.Optional(CONF_SET_POSITION_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_STATE_CLOSED, default=STATE_CLOSED): cv.string, + vol.Optional(CONF_STATE_CLOSING, default=STATE_CLOSING): cv.string, vol.Optional(CONF_STATE_OPEN, default=STATE_OPEN): cv.string, + vol.Optional(CONF_STATE_OPENING, default=STATE_OPENING): cv.string, vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional( CONF_TILT_CLOSED_POSITION, default=DEFAULT_TILT_CLOSED_POSITION @@ -172,15 +178,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def async_discover(discovery_payload): """Discover and add an MQTT cover.""" + discovery_hash = discovery_payload.pop(ATTR_DISCOVERY_HASH) try: - discovery_hash = discovery_payload.pop(ATTR_DISCOVERY_HASH) config = PLATFORM_SCHEMA(discovery_payload) await _async_setup_entity( config, async_add_entities, config_entry, discovery_hash ) except Exception: - if discovery_hash: - clear_discovery_hash(hass, discovery_hash) + clear_discovery_hash(hass, discovery_hash) raise async_dispatcher_connect( @@ -289,12 +294,20 @@ class MqttCover( payload = template.async_render_with_possible_json_value(payload) if payload == self._config[CONF_STATE_OPEN]: - self._state = False + self._state = STATE_OPEN + elif payload == self._config[CONF_STATE_OPENING]: + self._state = STATE_OPENING elif payload == self._config[CONF_STATE_CLOSED]: - self._state = True + self._state = STATE_CLOSED + elif payload == self._config[CONF_STATE_CLOSING]: + self._state = STATE_CLOSING else: - _LOGGER.warning("Payload is not True or False: %s", payload) + _LOGGER.warning( + "Payload is not supported (e.g. open, closed, opening, closing): %s", + payload, + ) return + self.async_write_ha_state() @callback @@ -309,7 +322,11 @@ class MqttCover( float(payload), COVER_PAYLOAD ) self._position = percentage_payload - self._state = percentage_payload == DEFAULT_POSITION_CLOSED + self._state = ( + STATE_CLOSED + if percentage_payload == DEFAULT_POSITION_CLOSED + else STATE_OPEN + ) else: _LOGGER.warning("Payload is not integer within range: %s", payload) return @@ -370,8 +387,21 @@ class MqttCover( @property def is_closed(self): - """Return if the cover is closed.""" - return self._state + """Return true if the cover is closed or None if the status is unknown.""" + if self._state is None: + return None + + return self._state == STATE_CLOSED + + @property + def is_opening(self): + """Return true if the cover is actively opening.""" + return self._state == STATE_OPENING + + @property + def is_closing(self): + """Return true if the cover is actively closing.""" + return self._state == STATE_CLOSING @property def current_cover_position(self): @@ -423,7 +453,7 @@ class MqttCover( ) if self._optimistic: # Optimistically assume that cover has changed state. - self._state = False + self._state = STATE_OPEN if self._config.get(CONF_GET_POSITION_TOPIC): self._position = self.find_percentage_in_range( self._config[CONF_POSITION_OPEN], COVER_PAYLOAD @@ -444,7 +474,7 @@ class MqttCover( ) if self._optimistic: # Optimistically assume that cover has changed state. - self._state = True + self._state = STATE_CLOSED if self._config.get(CONF_GET_POSITION_TOPIC): self._position = self.find_percentage_in_range( self._config[CONF_POSITION_CLOSED], COVER_PAYLOAD @@ -538,7 +568,11 @@ class MqttCover( self._config[CONF_RETAIN], ) if self._optimistic: - self._state = percentage_position == self._config[CONF_POSITION_CLOSED] + self._state = ( + STATE_CLOSED + if percentage_position == self._config[CONF_POSITION_CLOSED] + else STATE_OPEN + ) self._position = percentage_position self.async_write_ha_state() diff --git a/homeassistant/components/mqtt/device_automation.py b/homeassistant/components/mqtt/device_automation.py new file mode 100644 index 00000000000..3f0889875d0 --- /dev/null +++ b/homeassistant/components/mqtt/device_automation.py @@ -0,0 +1,44 @@ +"""Provides device automations for MQTT.""" +import logging + +import voluptuous as vol + +from homeassistant.components import mqtt +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from . import ATTR_DISCOVERY_HASH, device_trigger +from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash + +_LOGGER = logging.getLogger(__name__) + +AUTOMATION_TYPE_TRIGGER = "trigger" +AUTOMATION_TYPES = [AUTOMATION_TYPE_TRIGGER] +AUTOMATION_TYPES_SCHEMA = vol.In(AUTOMATION_TYPES) +CONF_AUTOMATION_TYPE = "automation_type" + +PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( + {vol.Required(CONF_AUTOMATION_TYPE): AUTOMATION_TYPES_SCHEMA}, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup_entry(hass, config_entry): + """Set up MQTT device automation dynamically through MQTT discovery.""" + + async def async_discover(discovery_payload): + """Discover and add an MQTT device automation.""" + discovery_hash = discovery_payload.pop(ATTR_DISCOVERY_HASH) + try: + config = PLATFORM_SCHEMA(discovery_payload) + if config[CONF_AUTOMATION_TYPE] == AUTOMATION_TYPE_TRIGGER: + await device_trigger.async_setup_trigger( + hass, config, config_entry, discovery_hash + ) + except Exception: + if discovery_hash: + clear_discovery_hash(hass, discovery_hash) + raise + + async_dispatcher_connect( + hass, MQTT_DISCOVERY_NEW.format("device_automation", "mqtt"), async_discover + ) diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py new file mode 100644 index 00000000000..2149024266d --- /dev/null +++ b/homeassistant/components/mqtt/device_trigger.py @@ -0,0 +1,273 @@ +"""Provides device automations for MQTT.""" +import logging +from typing import List + +import attr +import voluptuous as vol + +from homeassistant.components import mqtt +from homeassistant.components.automation import AutomationActionType +import homeassistant.components.automation.mqtt as automation_mqtt +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import ConfigType, HomeAssistantType + +from . import ( + ATTR_DISCOVERY_HASH, + CONF_CONNECTIONS, + CONF_DEVICE, + CONF_IDENTIFIERS, + CONF_PAYLOAD, + CONF_QOS, + DOMAIN, +) +from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash + +_LOGGER = logging.getLogger(__name__) + +CONF_AUTOMATION_TYPE = "automation_type" +CONF_DISCOVERY_ID = "discovery_id" +CONF_SUBTYPE = "subtype" +CONF_TOPIC = "topic" +DEFAULT_ENCODING = "utf-8" +DEVICE = "device" + +MQTT_TRIGGER_BASE = { + # Trigger when MQTT message is received + CONF_PLATFORM: DEVICE, + CONF_DOMAIN: DOMAIN, +} + +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_PLATFORM): DEVICE, + vol.Required(CONF_DOMAIN): DOMAIN, + vol.Required(CONF_DEVICE_ID): str, + vol.Required(CONF_DISCOVERY_ID): str, + vol.Required(CONF_TYPE): cv.string, + vol.Required(CONF_SUBTYPE): cv.string, + } +) + +TRIGGER_DISCOVERY_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_AUTOMATION_TYPE): str, + vol.Required(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_PAYLOAD, default=None): vol.Any(None, cv.string), + vol.Required(CONF_TYPE): cv.string, + vol.Required(CONF_SUBTYPE): cv.string, + }, + mqtt.validate_device_has_at_least_one_identifier, +) + +DEVICE_TRIGGERS = "mqtt_device_triggers" + + +@attr.s(slots=True) +class TriggerInstance: + """Attached trigger settings.""" + + action = attr.ib(type=AutomationActionType) + automation_info = attr.ib(type=dict) + trigger = attr.ib(type="Trigger") + remove = attr.ib(type=CALLBACK_TYPE, default=None) + + async def async_attach_trigger(self): + """Attach MQTT trigger.""" + mqtt_config = { + automation_mqtt.CONF_TOPIC: self.trigger.topic, + automation_mqtt.CONF_ENCODING: DEFAULT_ENCODING, + automation_mqtt.CONF_QOS: self.trigger.qos, + } + if self.trigger.payload: + mqtt_config[CONF_PAYLOAD] = self.trigger.payload + + if self.remove: + self.remove() + self.remove = await automation_mqtt.async_attach_trigger( + self.trigger.hass, mqtt_config, self.action, self.automation_info, + ) + + +@attr.s(slots=True) +class Trigger: + """Device trigger settings.""" + + device_id = attr.ib(type=str) + hass = attr.ib(type=HomeAssistantType) + payload = attr.ib(type=str) + qos = attr.ib(type=int) + subtype = attr.ib(type=str) + topic = attr.ib(type=str) + type = attr.ib(type=str) + trigger_instances = attr.ib(type=[TriggerInstance], default=attr.Factory(list)) + + async def add_trigger(self, action, automation_info): + """Add MQTT trigger.""" + instance = TriggerInstance(action, automation_info, self) + self.trigger_instances.append(instance) + + if self.topic is not None: + # If we know about the trigger, subscribe to MQTT topic + await instance.async_attach_trigger() + + @callback + def async_remove() -> None: + """Remove trigger.""" + if instance not in self.trigger_instances: + raise HomeAssistantError("Can't remove trigger twice") + + if instance.remove: + instance.remove() + self.trigger_instances.remove(instance) + + return async_remove + + async def update_trigger(self, config): + """Update MQTT device trigger.""" + self.type = config[CONF_TYPE] + self.subtype = config[CONF_SUBTYPE] + self.topic = config[CONF_TOPIC] + self.payload = config[CONF_PAYLOAD] + self.qos = config[CONF_QOS] + + # Unsubscribe+subscribe if this trigger is in use + for trig in self.trigger_instances: + await trig.async_attach_trigger() + + def detach_trigger(self): + """Remove MQTT device trigger.""" + # Mark trigger as unknown + + self.topic = None + # Unsubscribe if this trigger is in use + for trig in self.trigger_instances: + if trig.remove: + trig.remove() + trig.remove = None + + +async def _update_device(hass, config_entry, config): + """Update device registry.""" + device_registry = await hass.helpers.device_registry.async_get_registry() + config_entry_id = config_entry.entry_id + device_info = mqtt.device_info_from_config(config[CONF_DEVICE]) + + if config_entry_id is not None and device_info is not None: + device_info["config_entry_id"] = config_entry_id + device_registry.async_get_or_create(**device_info) + + +async def async_setup_trigger(hass, config, config_entry, discovery_hash): + """Set up the MQTT device trigger.""" + config = TRIGGER_DISCOVERY_SCHEMA(config) + discovery_id = discovery_hash[1] + remove_signal = None + + async def discovery_update(payload): + """Handle discovery update.""" + _LOGGER.info( + "Got update for trigger with hash: %s '%s'", discovery_hash, payload + ) + if not payload: + # Empty payload: Remove trigger + _LOGGER.info("Removing trigger: %s", discovery_hash) + if discovery_id in hass.data[DEVICE_TRIGGERS]: + device_trigger = hass.data[DEVICE_TRIGGERS][discovery_id] + device_trigger.detach_trigger() + clear_discovery_hash(hass, discovery_hash) + remove_signal() + else: + # Non-empty payload: Update trigger + _LOGGER.info("Updating trigger: %s", discovery_hash) + payload.pop(ATTR_DISCOVERY_HASH) + config = TRIGGER_DISCOVERY_SCHEMA(payload) + await _update_device(hass, config_entry, config) + device_trigger = hass.data[DEVICE_TRIGGERS][discovery_id] + await device_trigger.update_trigger(config) + + remove_signal = async_dispatcher_connect( + hass, MQTT_DISCOVERY_UPDATED.format(discovery_hash), discovery_update + ) + + await _update_device(hass, config_entry, config) + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get_device( + {(DOMAIN, id_) for id_ in config[CONF_DEVICE][CONF_IDENTIFIERS]}, + {tuple(x) for x in config[CONF_DEVICE][CONF_CONNECTIONS]}, + ) + + if device is None: + return + + if DEVICE_TRIGGERS not in hass.data: + hass.data[DEVICE_TRIGGERS] = {} + if discovery_id not in hass.data[DEVICE_TRIGGERS]: + hass.data[DEVICE_TRIGGERS][discovery_id] = Trigger( + hass=hass, + device_id=device.id, + type=config[CONF_TYPE], + subtype=config[CONF_SUBTYPE], + topic=config[CONF_TOPIC], + payload=config[CONF_PAYLOAD], + qos=config[CONF_QOS], + ) + else: + await hass.data[DEVICE_TRIGGERS][discovery_id].update_trigger(config) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device triggers for MQTT devices.""" + triggers = [] + + if DEVICE_TRIGGERS not in hass.data: + return triggers + + for discovery_id, trig in hass.data[DEVICE_TRIGGERS].items(): + if trig.device_id != device_id or trig.topic is None: + continue + + trigger = { + **MQTT_TRIGGER_BASE, + "device_id": device_id, + "type": trig.type, + "subtype": trig.subtype, + "discovery_id": discovery_id, + } + triggers.append(trigger) + + return triggers + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + if DEVICE_TRIGGERS not in hass.data: + hass.data[DEVICE_TRIGGERS] = {} + config = TRIGGER_SCHEMA(config) + device_id = config[CONF_DEVICE_ID] + discovery_id = config[CONF_DISCOVERY_ID] + + if discovery_id not in hass.data[DEVICE_TRIGGERS]: + hass.data[DEVICE_TRIGGERS][discovery_id] = Trigger( + hass=hass, + device_id=device_id, + type=config[CONF_TYPE], + subtype=config[CONF_SUBTYPE], + topic=None, + payload=None, + qos=None, + ) + return await hass.data[DEVICE_TRIGGERS][discovery_id].add_trigger( + action, automation_info + ) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index f393c315793..418f648564d 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -26,6 +26,7 @@ SUPPORTED_COMPONENTS = [ "camera", "climate", "cover", + "device_automation", "fan", "light", "lock", @@ -40,6 +41,7 @@ CONFIG_ENTRY_COMPONENTS = [ "camera", "climate", "cover", + "device_automation", "fan", "light", "lock", @@ -197,9 +199,15 @@ async def async_start( config_entries_key = "{}.{}".format(component, "mqtt") async with hass.data[DATA_CONFIG_ENTRY_LOCK]: if config_entries_key not in hass.data[CONFIG_ENTRY_IS_SETUP]: - await hass.config_entries.async_forward_entry_setup( - config_entry, component - ) + if component == "device_automation": + # Local import to avoid circular dependencies + from . import device_automation + + await device_automation.async_setup_entry(hass, config_entry) + else: + await hass.config_entries.async_forward_entry_setup( + config_entry, component + ) hass.data[CONFIG_ENTRY_IS_SETUP].add(config_entries_key) async_dispatcher_send( diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 8bacfa530bd..f0a38bcbc55 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -27,5 +27,27 @@ "error": { "cannot_connect": "Unable to connect to the broker." } + }, + "device_automation": { + "trigger_type": { + "button_short_press": "\"{subtype}\" pressed", + "button_short_release": "\"{subtype}\" released", + "button_long_press": "\"{subtype}\" continuously pressed", + "button_long_release": "\"{subtype}\" released after long press", + "button_double_press": "\"{subtype}\" double clicked", + "button_triple_press": "\"{subtype}\" triple clicked", + "button_quadruple_press": "\"{subtype}\" quadruple clicked", + "button_quintuple_press": "\"{subtype}\" quintuple clicked" + }, + "trigger_subtype": { + "turn_on": "Turn on", + "turn_off": "Turn off", + "button_1": "First button", + "button_2": "Second button", + "button_3": "Third button", + "button_4": "Fourth button", + "button_5": "Fifth button", + "button_6": "Sixth button" + } } } diff --git a/homeassistant/components/mysensors/light.py b/homeassistant/components/mysensors/light.py index 19eb8e9e92c..45da4a77d5f 100644 --- a/homeassistant/components/mysensors/light.py +++ b/homeassistant/components/mysensors/light.py @@ -11,6 +11,7 @@ from homeassistant.components.light import ( Light, ) from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import callback import homeassistant.util.color as color_util from homeassistant.util.color import rgb_hex_to_rgb_list @@ -150,11 +151,13 @@ class MySensorsLight(mysensors.device.MySensorsEntity, Light): self._values[value_type] = STATE_OFF self.async_schedule_update_ha_state() + @callback def _async_update_light(self): """Update the controller with values from light child.""" value_type = self.gateway.const.SetReq.V_LIGHT self._state = self._values[value_type] == STATE_ON + @callback def _async_update_dimmer(self): """Update the controller with values from dimmer child.""" value_type = self.gateway.const.SetReq.V_DIMMER @@ -163,6 +166,7 @@ class MySensorsLight(mysensors.device.MySensorsEntity, Light): if self._brightness == 0: self._state = False + @callback def _async_update_rgb_or_w(self): """Update the controller with values from RGB or RGBW child.""" value = self._values[self.value_type] diff --git a/homeassistant/components/mystrom/switch.py b/homeassistant/components/mystrom/switch.py index 3a045e0391d..ca766810a3d 100644 --- a/homeassistant/components/mystrom/switch.py +++ b/homeassistant/components/mystrom/switch.py @@ -5,6 +5,7 @@ import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv DEFAULT_NAME = "myStrom Switch" @@ -30,7 +31,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): MyStromPlug(host).get_status() except exceptions.MyStromConnectionError: _LOGGER.error("No route to device: %s", host) - return + raise PlatformNotReady() add_entities([MyStromSwitch(name, host)]) @@ -46,7 +47,7 @@ class MyStromSwitch(SwitchDevice): self._resource = resource self.data = {} self.plug = MyStromPlug(self._resource) - self.update() + self._available = True @property def name(self): @@ -63,6 +64,11 @@ class MyStromSwitch(SwitchDevice): """Return the current power consumption in W.""" return round(self.data["power"], 2) + @property + def available(self): + """Could the device be accessed during the last update call.""" + return self._available + def turn_on(self, **kwargs): """Turn the switch on.""" from pymystrom import exceptions @@ -87,6 +93,8 @@ class MyStromSwitch(SwitchDevice): try: self.data = self.plug.get_status() + self._available = True except exceptions.MyStromConnectionError: self.data = {"power": 0, "relay": False} + self._available = False _LOGGER.error("No route to device: %s", self._resource) diff --git a/homeassistant/components/neato/.translations/hu.json b/homeassistant/components/neato/.translations/hu.json new file mode 100644 index 00000000000..50fa4b5866f --- /dev/null +++ b/homeassistant/components/neato/.translations/hu.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "M\u00e1r konfigur\u00e1lva van", + "invalid_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u0151 adatok" + }, + "create_entry": { + "default": "L\u00e1sd: [Neato dokument\u00e1ci\u00f3] ( {docs_url} )." + }, + "error": { + "invalid_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u0151 adatok", + "unexpected_error": "V\u00e1ratlan hiba" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v", + "vendor": "Sz\u00e1ll\u00edt\u00f3" + }, + "description": "L\u00e1sd: [Neato dokument\u00e1ci\u00f3] ( {docs_url} ).", + "title": "Neato Fi\u00f3kinform\u00e1ci\u00f3" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/pl.json b/homeassistant/components/neato/.translations/pl.json index caea115b7d5..e6b55b12c53 100644 --- a/homeassistant/components/neato/.translations/pl.json +++ b/homeassistant/components/neato/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Konto jest ju\u017c skonfigurowane", + "already_configured": "Konto jest ju\u017c skonfigurowane.", "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce" }, "create_entry": { diff --git a/homeassistant/components/neato/.translations/sv.json b/homeassistant/components/neato/.translations/sv.json new file mode 100644 index 00000000000..64edf9e93ce --- /dev/null +++ b/homeassistant/components/neato/.translations/sv.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Redan konfigurerad", + "invalid_credentials": "Ogiltiga autentiseringsuppgifter" + }, + "create_entry": { + "default": "Se [Neato-dokumentation]({docs_url})." + }, + "error": { + "invalid_credentials": "Ogiltiga autentiseringsuppgifter", + "unexpected_error": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn", + "vendor": "Leverant\u00f6r" + }, + "description": "Se [Neato-dokumentation] ({docs_url}).", + "title": "Neato-kontoinfo" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/nederlandse_spoorwegen/manifest.json b/homeassistant/components/nederlandse_spoorwegen/manifest.json index 8718843d73d..c6025abe0b5 100644 --- a/homeassistant/components/nederlandse_spoorwegen/manifest.json +++ b/homeassistant/components/nederlandse_spoorwegen/manifest.json @@ -2,7 +2,7 @@ "domain": "nederlandse_spoorwegen", "name": "Nederlandse Spoorwegen (NS)", "documentation": "https://www.home-assistant.io/integrations/nederlandse_spoorwegen", - "requirements": ["nsapi==3.0.2"], + "requirements": ["nsapi==3.0.3"], "dependencies": [], "codeowners": ["@YarmoM"] } diff --git a/homeassistant/components/nederlandse_spoorwegen/sensor.py b/homeassistant/components/nederlandse_spoorwegen/sensor.py index df37fad2aa3..45413d4a15a 100644 --- a/homeassistant/components/nederlandse_spoorwegen/sensor.py +++ b/homeassistant/components/nederlandse_spoorwegen/sensor.py @@ -20,6 +20,7 @@ CONF_ROUTES = "routes" CONF_FROM = "from" CONF_TO = "to" CONF_VIA = "via" +CONF_TIME = "time" ICON = "mdi:train" @@ -31,6 +32,7 @@ ROUTE_SCHEMA = vol.Schema( vol.Required(CONF_FROM): cv.string, vol.Required(CONF_TO): cv.string, vol.Optional(CONF_VIA): cv.string, + vol.Optional(CONF_TIME): cv.time, } ) @@ -68,6 +70,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): departure.get(CONF_FROM), departure.get(CONF_TO), departure.get(CONF_VIA), + departure.get(CONF_TIME), ) ) if sensors: @@ -88,13 +91,14 @@ def valid_stations(stations, given_stations): class NSDepartureSensor(Entity): """Implementation of a NS Departure Sensor.""" - def __init__(self, nsapi, name, departure, heading, via): + def __init__(self, nsapi, name, departure, heading, via, time): """Initialize the sensor.""" self._nsapi = nsapi self._name = name self._departure = departure self._via = via self._heading = heading + self._time = time self._state = None self._trips = None @@ -138,6 +142,7 @@ class NSDepartureSensor(Entity): "arrival_platform_planned": self._trips[0].arrival_platform_planned, "arrival_platform_actual": self._trips[0].arrival_platform_actual, "next": None, + "punctuality": None, "status": self._trips[0].status.lower(), "transfers": self._trips[0].nr_transfers, "route": route, @@ -186,26 +191,49 @@ class NSDepartureSensor(Entity): ): attributes["arrival_delay"] = True + # Punctuality attributes + if self._trips[0].punctuality is not None: + attributes["punctuality"] = self._trips[0].punctuality + # Next attributes - if self._trips[1].departure_time_actual is not None: - attributes["next"] = self._trips[1].departure_time_actual.strftime("%H:%M") - elif self._trips[1].departure_time_planned is not None: - attributes["next"] = self._trips[1].departure_time_planned.strftime("%H:%M") + if len(self._trips) > 1: + if self._trips[1].departure_time_actual is not None: + attributes["next"] = self._trips[1].departure_time_actual.strftime( + "%H:%M" + ) + elif self._trips[1].departure_time_planned is not None: + attributes["next"] = self._trips[1].departure_time_planned.strftime( + "%H:%M" + ) return attributes @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the trip information.""" + + # If looking for a specific trip time, update around that trip time only. + if self._time and ( + (datetime.now() + timedelta(minutes=30)).time() < self._time + or (datetime.now() - timedelta(minutes=30)).time() > self._time + ): + self._state = None + self._trips = None + return + + # Set the search parameter to search from a specific trip time or to just search for next trip. + if self._time: + trip_time = ( + datetime.today() + .replace(hour=self._time.hour, minute=self._time.minute) + .strftime("%d-%m-%Y %H:%M") + ) + else: + trip_time = datetime.now().strftime("%d-%m-%Y %H:%M") + try: self._trips = self._nsapi.get_trips( - datetime.now().strftime("%d-%m-%Y %H:%M"), - self._departure, - self._via, - self._heading, - True, - 0, - 2, + trip_time, self._departure, self._via, self._heading, True, 0, 2 ) if self._trips: if self._trips[0].departure_time_actual is None: diff --git a/homeassistant/components/nest/.translations/zh-Hans.json b/homeassistant/components/nest/.translations/zh-Hans.json index 0b5cbc989fd..0825fdfdc79 100644 --- a/homeassistant/components/nest/.translations/zh-Hans.json +++ b/homeassistant/components/nest/.translations/zh-Hans.json @@ -8,14 +8,14 @@ }, "error": { "internal_error": "\u9a8c\u8bc1\u4ee3\u7801\u65f6\u53d1\u751f\u5185\u90e8\u9519\u8bef", - "invalid_code": "\u65e0\u6548\u4ee3\u7801", - "timeout": "\u4ee3\u7801\u9a8c\u8bc1\u8d85\u65f6", - "unknown": "\u9a8c\u8bc1\u4ee3\u7801\u65f6\u53d1\u751f\u672a\u77e5\u9519\u8bef" + "invalid_code": "\u9a8c\u8bc1\u7801\u65e0\u6548", + "timeout": "\u9a8c\u8bc1\u7801\u8d85\u65f6", + "unknown": "\u9a8c\u8bc1\u7801\u672a\u77e5\u9519\u8bef" }, "step": { "init": { "data": { - "flow_impl": "\u63d0\u4f9b\u8005" + "flow_impl": "\u8ba4\u8bc1\u63d0\u4f9b\u8005" }, "description": "\u9009\u62e9\u60a8\u60f3\u901a\u8fc7\u54ea\u4e2a\u6388\u6743\u63d0\u4f9b\u8005\u4e0e Nest \u8fdb\u884c\u6388\u6743\u3002", "title": "\u6388\u6743\u63d0\u4f9b\u8005" diff --git a/homeassistant/components/netatmo/.translations/hu.json b/homeassistant/components/netatmo/.translations/hu.json new file mode 100644 index 00000000000..9994e527f01 --- /dev/null +++ b/homeassistant/components/netatmo/.translations/hu.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_setup": "Csak egy Netatmo-fi\u00f3kot \u00e1ll\u00edthatsz be.", + "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n." + }, + "create_entry": { + "default": "A Netatmo sikeresen hiteles\u00edtett." + }, + "step": { + "pick_implementation": { + "title": "V\u00e1lassza ki a hiteles\u00edt\u00e9si m\u00f3dszert" + } + }, + "title": "Netatmo" + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/.translations/nl.json b/homeassistant/components/netatmo/.translations/nl.json index d9062850f2a..5f5fe375117 100644 --- a/homeassistant/components/netatmo/.translations/nl.json +++ b/homeassistant/components/netatmo/.translations/nl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_setup": "U kunt slechts \u00e9\u00e9n Netatmo account configureren.", "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen." }, diff --git a/homeassistant/components/netatmo/.translations/sl.json b/homeassistant/components/netatmo/.translations/sl.json new file mode 100644 index 00000000000..5288c84e44b --- /dev/null +++ b/homeassistant/components/netatmo/.translations/sl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "Konfigurirate lahko samo en ra\u010dun Netatmo.", + "authorize_url_timeout": "\u010casovna omejitev za generiranje potrditvenega URL-ja je potekla.", + "missing_configuration": "Komponenta Netatmo ni konfigurirana. Prosimo, upo\u0161tevajte dokumentacijo." + }, + "create_entry": { + "default": "Uspe\u0161no overjeno z Netatmo." + }, + "step": { + "pick_implementation": { + "title": "Izberite na\u010din preverjanja pristnosti" + } + }, + "title": "Netatmo" + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 14ec2e61b9c..6fe084cc885 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==3.2.2" + "pyatmo==3.2.4" ], "dependencies": [ "webhook" diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index afdb7c053f3..818662ee69c 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -1,7 +1,6 @@ """Support for the Netatmo Weather Service.""" from datetime import timedelta import logging -from time import time import pyatmo @@ -519,7 +518,6 @@ class NetatmoData: """Initialize the data object.""" self.data = {} self.station_data = station_data - self._next_update = time() self.auth = auth def get_module_infos(self): diff --git a/homeassistant/components/netgear/device_tracker.py b/homeassistant/components/netgear/device_tracker.py index 23b1034a5b3..5f18553ba62 100644 --- a/homeassistant/components/netgear/device_tracker.py +++ b/homeassistant/components/netgear/device_tracker.py @@ -1,5 +1,6 @@ """Support for Netgear routers.""" import logging +from pprint import pformat from pynetgear import Netgear import voluptuous as vol @@ -30,7 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_SSL, default=False): cv.boolean, vol.Optional(CONF_USERNAME, default=""): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PORT, default=None): vol.Any(None, cv.port), + vol.Optional(CONF_PORT): cv.port, vol.Optional(CONF_DEVICES, default=[]): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_EXCLUDE, default=[]): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_APS, default=[]): vol.All(cv.ensure_list, [cv.string]), @@ -41,55 +42,43 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_scanner(hass, config): """Validate the configuration and returns a Netgear scanner.""" info = config[DOMAIN] - host = info.get(CONF_HOST) - ssl = info.get(CONF_SSL) - username = info.get(CONF_USERNAME) - password = info.get(CONF_PASSWORD) + host = info[CONF_HOST] + ssl = info[CONF_SSL] + username = info[CONF_USERNAME] + password = info[CONF_PASSWORD] port = info.get(CONF_PORT) - devices = info.get(CONF_DEVICES) - excluded_devices = info.get(CONF_EXCLUDE) - accesspoints = info.get(CONF_APS) + devices = info[CONF_DEVICES] + excluded_devices = info[CONF_EXCLUDE] + accesspoints = info[CONF_APS] - scanner = NetgearDeviceScanner( - host, ssl, username, password, port, devices, excluded_devices, accesspoints - ) + api = Netgear(password, host, username, port, ssl) + scanner = NetgearDeviceScanner(api, devices, excluded_devices, accesspoints) - return scanner if scanner.success_init else None + _LOGGER.debug("Logging in") + + results = scanner.get_attached_devices() + + if results is not None: + scanner.last_results = results + else: + _LOGGER.error("Failed to Login") + return None + + return scanner class NetgearDeviceScanner(DeviceScanner): """Queries a Netgear wireless router using the SOAP-API.""" def __init__( - self, - host, - ssl, - username, - password, - port, - devices, - excluded_devices, - accesspoints, + self, api, devices, excluded_devices, accesspoints, ): """Initialize the scanner.""" - self.tracked_devices = devices self.excluded_devices = excluded_devices self.tracked_accesspoints = accesspoints - self.last_results = [] - self._api = Netgear(password, host, username, port, ssl) - - _LOGGER.info("Logging in") - - results = self.get_attached_devices() - - self.success_init = results is not None - - if self.success_init: - self.last_results = results - else: - _LOGGER.error("Failed to Login") + self._api = api def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" @@ -110,10 +99,7 @@ class NetgearDeviceScanner(DeviceScanner): or dev.name in self.excluded_devices ) ) - - # when link_rate is None this means the router still knows about - # the device, but it is not in range. - if tracked and dev.link_rate is not None: + if tracked: devices.append(dev.mac) if ( self.tracked_accesspoints @@ -156,21 +142,20 @@ class NetgearDeviceScanner(DeviceScanner): Returns boolean if scanning successful. """ - if not self.success_init: - return - - _LOGGER.info("Scanning") + _LOGGER.debug("Scanning") results = self.get_attached_devices() + if _LOGGER.isEnabledFor(logging.DEBUG): + _LOGGER.debug("Scan result: \n%s", pformat(results)) + if results is None: _LOGGER.warning("Error scanning devices") self.last_results = results or [] def get_attached_devices(self): - """ - List attached devices with pynetgear. + """List attached devices with pynetgear. The v2 method takes more time and is more heavy on the router so we only use it if we need connected AP info. diff --git a/homeassistant/components/netgear/manifest.json b/homeassistant/components/netgear/manifest.json index 4cfffab7d73..c5685411045 100644 --- a/homeassistant/components/netgear/manifest.json +++ b/homeassistant/components/netgear/manifest.json @@ -1,6 +1,6 @@ { "domain": "netgear", - "name": "Netgear", + "name": "NETGEAR", "documentation": "https://www.home-assistant.io/integrations/netgear", "requirements": ["pynetgear==0.6.1"], "dependencies": [], diff --git a/homeassistant/components/netgear_lte/manifest.json b/homeassistant/components/netgear_lte/manifest.json index 0be2ca146b1..43cf6e34480 100644 --- a/homeassistant/components/netgear_lte/manifest.json +++ b/homeassistant/components/netgear_lte/manifest.json @@ -1,6 +1,6 @@ { "domain": "netgear_lte", - "name": "Netgear LTE", + "name": "NETGEAR LTE", "documentation": "https://www.home-assistant.io/integrations/netgear_lte", "requirements": ["eternalegypt==0.0.11"], "dependencies": [], diff --git a/homeassistant/components/netgear_lte/sensor_types.py b/homeassistant/components/netgear_lte/sensor_types.py index e1a9d1a23d2..a744937dacd 100644 --- a/homeassistant/components/netgear_lte/sensor_types.py +++ b/homeassistant/components/netgear_lte/sensor_types.py @@ -1,6 +1,7 @@ """Define possible sensor types.""" from homeassistant.components.binary_sensor import DEVICE_CLASS_CONNECTIVITY +from homeassistant.const import DATA_MEBIBYTES SENSOR_SMS = "sms" SENSOR_SMS_TOTAL = "sms_total" @@ -9,7 +10,7 @@ SENSOR_USAGE = "usage" SENSOR_UNITS = { SENSOR_SMS: "unread", SENSOR_SMS_TOTAL: "messages", - SENSOR_USAGE: "MiB", + SENSOR_USAGE: DATA_MEBIBYTES, "radio_quality": "%", "rx_level": "dBm", "tx_level": "dBm", diff --git a/homeassistant/components/notion/.translations/ca.json b/homeassistant/components/notion/.translations/ca.json index 0b6a24626be..09f598ef5d1 100644 --- a/homeassistant/components/notion/.translations/ca.json +++ b/homeassistant/components/notion/.translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Aquest nom d'usuari ja est\u00e0 en \u00fas." + }, "error": { "identifier_exists": "Nom d'usuari ja registrat", "invalid_credentials": "Nom d'usuari o contrasenya incorrectes", diff --git a/homeassistant/components/notion/.translations/de.json b/homeassistant/components/notion/.translations/de.json index e9c735001e9..e11a16458c9 100644 --- a/homeassistant/components/notion/.translations/de.json +++ b/homeassistant/components/notion/.translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Dieser Benutzername wird bereits benutzt." + }, "error": { "identifier_exists": "Benutzername bereits registriert", "invalid_credentials": "Ung\u00fcltiger Benutzername oder Passwort", diff --git a/homeassistant/components/notion/.translations/en.json b/homeassistant/components/notion/.translations/en.json index b05f613a73f..2476293a216 100644 --- a/homeassistant/components/notion/.translations/en.json +++ b/homeassistant/components/notion/.translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "This username is already in use." + }, "error": { "identifier_exists": "Username already registered", "invalid_credentials": "Invalid username or password", diff --git a/homeassistant/components/notion/.translations/ko.json b/homeassistant/components/notion/.translations/ko.json index 76dc91cf46b..52c7b6339cb 100644 --- a/homeassistant/components/notion/.translations/ko.json +++ b/homeassistant/components/notion/.translations/ko.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\uc774 \uc0ac\uc6a9\uc790 \uc774\ub984\uc740 \uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4." + }, "error": { "identifier_exists": "\uc0ac\uc6a9\uc790 \uc774\ub984\uc774 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "invalid_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ub610\ub294 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", diff --git a/homeassistant/components/notion/.translations/no.json b/homeassistant/components/notion/.translations/no.json index 2798db1cbc3..16105e680c5 100644 --- a/homeassistant/components/notion/.translations/no.json +++ b/homeassistant/components/notion/.translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Dette brukernavnet er allerede i bruk." + }, "error": { "identifier_exists": "Brukernavn er allerede registrert", "invalid_credentials": "Ugyldig brukernavn eller passord", diff --git a/homeassistant/components/notion/.translations/pl.json b/homeassistant/components/notion/.translations/pl.json index 380d4ad151e..07facb21e93 100644 --- a/homeassistant/components/notion/.translations/pl.json +++ b/homeassistant/components/notion/.translations/pl.json @@ -1,7 +1,10 @@ { "config": { + "abort": { + "already_configured": "Ta nazwa u\u017cytkownika jest ju\u017c w u\u017cyciu." + }, "error": { - "identifier_exists": "Nazwa u\u017cytkownika ju\u017c zarejestrowana", + "identifier_exists": "Nazwa u\u017cytkownika jest ju\u017c zarejestrowana.", "invalid_credentials": "Nieprawid\u0142owa nazwa u\u017cytkownika lub has\u0142o", "no_devices": "Nie znaleziono urz\u0105dze\u0144 na koncie" }, @@ -9,11 +12,11 @@ "user": { "data": { "password": "Has\u0142o", - "username": "Nazwa u\u017cytkownika / adres e-mail" + "username": "Nazwa u\u017cytkownika/adres e-mail" }, "title": "Wprowad\u017a dane" } }, - "title": "Poj\u0119cie" + "title": "Notion" } } \ No newline at end of file diff --git a/homeassistant/components/notion/.translations/zh-Hant.json b/homeassistant/components/notion/.translations/zh-Hant.json index f672f519f40..c426dfa3265 100644 --- a/homeassistant/components/notion/.translations/zh-Hant.json +++ b/homeassistant/components/notion/.translations/zh-Hant.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u6b64\u4f7f\u7528\u8005\u540d\u7a31\u5df2\u88ab\u4f7f\u7528\u3002" + }, "error": { "identifier_exists": "\u4f7f\u7528\u8005\u540d\u7a31\u5df2\u8a3b\u518a", "invalid_credentials": "\u4f7f\u7528\u8005\u540d\u7a31\u6216\u5bc6\u78bc\u7121\u6548", diff --git a/homeassistant/components/notion/const.py b/homeassistant/components/notion/const.py index 2f81cb72ac0..6ce5c4e5bc7 100644 --- a/homeassistant/components/notion/const.py +++ b/homeassistant/components/notion/const.py @@ -7,7 +7,7 @@ DEFAULT_SCAN_INTERVAL = timedelta(minutes=1) DATA_CLIENT = "client" -TOPIC_DATA_UPDATE = "data_update" +TOPIC_DATA_UPDATE = f"{DOMAIN}_data_update" TYPE_BINARY_SENSOR = "binary_sensor" TYPE_SENSOR = "sensor" diff --git a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py index a04d2bd69b2..f0d8c901387 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py +++ b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py @@ -18,13 +18,13 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import callback -from homeassistant.helpers import ConfigType, aiohttp_client, config_validation as cv +from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.typing import ConfigType, HomeAssistantType _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json index b2dcfe10cf6..1c2aa268ca2 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json +++ b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json @@ -2,7 +2,7 @@ "domain": "nsw_rural_fire_service_feed", "name": "NSW Rural Fire Service Incidents", "documentation": "https://www.home-assistant.io/integrations/nsw_rural_fire_service_feed", - "requirements": ["aio_geojson_nsw_rfs_incidents==0.1"], + "requirements": ["aio_geojson_nsw_rfs_incidents==0.3"], "dependencies": [], "codeowners": ["@exxamalte"] } diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index 3556c88a6da..24ef18ab985 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -1,6 +1,7 @@ """Monitor the NZBGet API.""" import logging +from homeassistant.const import DATA_MEGABYTES, DATA_RATE_MEGABYTES_PER_SECOND from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity @@ -12,15 +13,19 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "NZBGet" SENSOR_TYPES = { - "article_cache": ["ArticleCacheMB", "Article Cache", "MB"], - "average_download_rate": ["AverageDownloadRate", "Average Speed", "MB/s"], + "article_cache": ["ArticleCacheMB", "Article Cache", DATA_MEGABYTES], + "average_download_rate": [ + "AverageDownloadRate", + "Average Speed", + DATA_RATE_MEGABYTES_PER_SECOND, + ], "download_paused": ["DownloadPaused", "Download Paused", None], - "download_rate": ["DownloadRate", "Speed", "MB/s"], - "download_size": ["DownloadedSizeMB", "Size", "MB"], - "free_disk_space": ["FreeDiskSpaceMB", "Disk Free", "MB"], + "download_rate": ["DownloadRate", "Speed", DATA_RATE_MEGABYTES_PER_SECOND], + "download_size": ["DownloadedSizeMB", "Size", DATA_MEGABYTES], + "free_disk_space": ["FreeDiskSpaceMB", "Disk Free", DATA_MEGABYTES], "post_job_count": ["PostJobCount", "Post Processing Jobs", "Jobs"], "post_paused": ["PostPaused", "Post Processing Paused", None], - "remaining_size": ["RemainingSizeMB", "Queue Size", "MB"], + "remaining_size": ["RemainingSizeMB", "Queue Size", DATA_MEGABYTES], "uptime": ["UpTimeSec", "Uptime", "min"], } diff --git a/homeassistant/components/nzbget/services.yaml b/homeassistant/components/nzbget/services.yaml index 84e4af15b5d..88a6267860e 100644 --- a/homeassistant/components/nzbget/services.yaml +++ b/homeassistant/components/nzbget/services.yaml @@ -10,5 +10,5 @@ set_speed: description: Set download speed limit fields: speed: - description: Speed limit in KB/s. 0 is unlimited. - example: 1000 \ No newline at end of file + description: Speed limit in kB/s. 0 is unlimited. + example: 1000 diff --git a/homeassistant/components/octoprint/manifest.json b/homeassistant/components/octoprint/manifest.json index d63a543f227..98e7c320a60 100644 --- a/homeassistant/components/octoprint/manifest.json +++ b/homeassistant/components/octoprint/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/octoprint", "requirements": [], "dependencies": [], + "after_dependencies": ["discovery"], "codeowners": [] } diff --git a/homeassistant/components/octoprint/sensor.py b/homeassistant/components/octoprint/sensor.py index d21aac9ff65..98d878fc2ea 100644 --- a/homeassistant/components/octoprint/sensor.py +++ b/homeassistant/components/octoprint/sensor.py @@ -32,7 +32,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): "If you do not want to have your printer on
" " at all times, and you would like to monitor
" "temperatures, please add
" - "bed and/or number_of_tools to your config
" + "bed and/or number_of_tools to your configuration
" "and restart.", title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID, diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index 936bf9f751b..6a7f282ac87 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -19,6 +19,7 @@ CONF_NAMES = "names" DEFAULT_MOUNT_DIR = "/sys/bus/w1/devices/" DEVICE_SENSORS = { + # Family : { SensorType: owfs path } "10": {"temperature": "temperature"}, "12": {"temperature": "TAI8570/temperature", "pressure": "TAI8570/pressure"}, "22": {"temperature": "temperature"}, @@ -27,6 +28,9 @@ DEVICE_SENSORS = { "humidity": "humidity", "pressure": "B1-R1-A/pressure", "illuminance": "S3-R1-A/illuminance", + "voltage_VAD": "VAD", + "voltage_VDD": "VDD", + "current": "IAD", }, "28": {"temperature": "temperature"}, "3B": {"temperature": "temperature"}, @@ -54,6 +58,7 @@ HOBBYBOARD_EF = { } SENSOR_TYPES = { + # SensorType: [ Measured unit, Unit ] "temperature": ["temperature", TEMP_CELSIUS], "humidity": ["humidity", "%"], "humidity_raw": ["humidity", "%"], @@ -70,6 +75,10 @@ SENSOR_TYPES = { "counter_a": ["counter", "count"], "counter_b": ["counter", "count"], "HobbyBoard": ["none", "none"], + "voltage": ["voltage", "V"], + "voltage_VAD": ["voltage", "V"], + "voltage_VDD": ["voltage", "V"], + "current": ["current", "A"], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -95,11 +104,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): base_dir = config[CONF_MOUNT_DIR] owport = config[CONF_PORT] owhost = config.get(CONF_HOST) + if owhost: + _LOGGER.debug("Initializing using %s:%s", owhost, owport) + else: + _LOGGER.debug("Initializing using %s", base_dir) + devs = [] device_names = {} - if "names" in config: - if isinstance(config["names"], dict): - device_names = config["names"] + if CONF_NAMES in config: + if isinstance(config[CONF_NAMES], dict): + device_names = config[CONF_NAMES] # We have an owserver on a remote(or local) host/port if owhost: @@ -112,7 +126,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) devices = [] for device in devices: - _LOGGER.debug("found device: %s", device) + _LOGGER.debug("Found device: %s", device) family = owproxy.read(f"{device}family").decode() dev_type = "std" if "EF" in family: @@ -200,6 +214,7 @@ class OneWire(Entity): self._device_file = device_file self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] self._state = None + self._value_raw = None def _read_value_raw(self): """Read the value as it is returned by the sensor.""" @@ -224,6 +239,16 @@ class OneWire(Entity): """Return the unit the value is expressed in.""" return self._unit_of_measurement + @property + def device_state_attributes(self): + """Return the state attributes of the sensor.""" + return {"device_file": self._device_file, "raw_value": self._value_raw} + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._device_file + class OneWireProxy(OneWire): """Implementation of a One wire Sensor through owserver.""" @@ -249,6 +274,7 @@ class OneWireProxy(OneWire): _LOGGER.error("Owserver failure in read(), got: %s", exc) if value_read: value = round(float(value_read), 1) + self._value_raw = float(value_read) self._state = value @@ -267,6 +293,7 @@ class OneWireDirect(OneWire): if equals_pos != -1: value_string = lines[1][equals_pos + 2 :] value = round(float(value_string) / 1000.0, 1) + self._value_raw = float(value_string) self._state = value @@ -280,6 +307,7 @@ class OneWireOWFS(OneWire): value_read = self._read_value_raw() if len(value_read) == 1: value = round(float(value_read[0]), 1) + self._value_raw = float(value_read[0]) except ValueError: _LOGGER.warning("Invalid value read from %s", self._device_file) except FileNotFoundError: diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index 3d13db3ead3..40ab3a8a7ed 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -2,7 +2,7 @@ "domain": "opencv", "name": "OpenCV", "documentation": "https://www.home-assistant.io/integrations/opencv", - "requirements": ["numpy==1.17.4", "opencv-python-headless==4.1.2.30"], + "requirements": ["numpy==1.18.1", "opencv-python-headless==4.1.2.30"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/opentherm_gw/.translations/hu.json b/homeassistant/components/opentherm_gw/.translations/hu.json new file mode 100644 index 00000000000..8a0780581fd --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/hu.json @@ -0,0 +1,33 @@ +{ + "config": { + "error": { + "already_configured": "Az \u00e1tj\u00e1r\u00f3 m\u00e1r konfigur\u00e1lva van", + "id_exists": "Az \u00e1tj\u00e1r\u00f3 azonos\u00edt\u00f3ja m\u00e1r l\u00e9tezik", + "serial_error": "Hiba t\u00f6rt\u00e9nt az eszk\u00f6zh\u00f6z val\u00f3 csatlakoz\u00e1skor", + "timeout": "A csatlakoz\u00e1si k\u00eds\u00e9rletre sz\u00e1nt id\u0151 lej\u00e1rt" + }, + "step": { + "init": { + "data": { + "device": "El\u00e9r\u00e9si \u00fat vagy URL", + "floor_temperature": "Padl\u00f3 kl\u00edma h\u0151m\u00e9rs\u00e9klete", + "id": "ID", + "name": "N\u00e9v", + "precision": "Kl\u00edma h\u0151m\u00e9rs\u00e9klet pontoss\u00e1ga" + }, + "title": "OpenTherm \u00e1tj\u00e1r\u00f3" + } + }, + "title": "OpenTherm \u00e1tj\u00e1r\u00f3" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Padl\u00f3 h\u0151m\u00e9rs\u00e9klete", + "precision": "Pontoss\u00e1g" + } + } + } + } +} \ 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 fe8ccfc8975..88791781e3f 100644 --- a/homeassistant/components/opentherm_gw/.translations/pl.json +++ b/homeassistant/components/opentherm_gw/.translations/pl.json @@ -1,8 +1,8 @@ { "config": { "error": { - "already_configured": "Bramka jest ju\u017c skonfigurowana", - "id_exists": "Identyfikator bramki ju\u017c istnieje", + "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": "Przekroczono limit czasu pr\u00f3by po\u0142\u0105czenia." }, diff --git a/homeassistant/components/opentherm_gw/.translations/sv.json b/homeassistant/components/opentherm_gw/.translations/sv.json new file mode 100644 index 00000000000..89ce4d75674 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/sv.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "already_configured": "Gateway redan konfigurerad", + "id_exists": "Gateway-id finns redan", + "serial_error": "Fel vid anslutning till enheten", + "timeout": "Anslutningsf\u00f6rs\u00f6ket avbr\u00f6ts" + }, + "step": { + "init": { + "data": { + "device": "S\u00f6kv\u00e4g eller URL", + "floor_temperature": "Golvtemperatur", + "id": "ID", + "name": "Namn", + "precision": "Klimatemperaturprecision" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Golvetemperatur", + "precision": "Precision" + }, + "description": "Alternativ f\u00f6r OpenTherm Gateway" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/de.json b/homeassistant/components/openuv/.translations/de.json index 7f8121dd96b..cc8ee92df4b 100644 --- a/homeassistant/components/openuv/.translations/de.json +++ b/homeassistant/components/openuv/.translations/de.json @@ -12,7 +12,7 @@ "latitude": "Breitengrad", "longitude": "L\u00e4ngengrad" }, - "title": "Gebe deine Informationen ein" + "title": "Gib deine Informationen ein" } }, "title": "OpenUV" diff --git a/homeassistant/components/openuv/.translations/pl.json b/homeassistant/components/openuv/.translations/pl.json index ee3875c2903..ff6d1b21055 100644 --- a/homeassistant/components/openuv/.translations/pl.json +++ b/homeassistant/components/openuv/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "Wsp\u00f3\u0142rz\u0119dne s\u0105 ju\u017c zarejestrowane", + "identifier_exists": "Wsp\u00f3\u0142rz\u0119dne s\u0105 ju\u017c zarejestrowane.", "invalid_api_key": "Nieprawid\u0142owy klucz API" }, "step": { diff --git a/homeassistant/components/pandora/media_player.py b/homeassistant/components/pandora/media_player.py index 417903c46e0..07f697a5a46 100644 --- a/homeassistant/components/pandora/media_player.py +++ b/homeassistant/components/pandora/media_player.py @@ -119,8 +119,8 @@ class PandoraMediaPlayer(MediaPlayerDevice): elif mode == 2: _LOGGER.warning( "The pianobar client is not configured to log in. " - "Please create a config file for it as described at " - "https://home-assistant.io/components/media_player.pandora/" + "Please create a configuration file for it as described at " + "https://home-assistant.io/integrations/pandora/" ) # pass through the email/password prompts to quit cleanly self._pianobar.sendcontrol("m") diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index ed6144af47e..5791d17f6dd 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -91,7 +91,7 @@ async def async_setup(hass, config): """Set up the pi_hole integration.""" def get_data(): - """Retrive component data.""" + """Retrieve component data.""" return hass.data[DOMAIN] def ensure_api_token(call_data): diff --git a/homeassistant/components/pi_hole/const.py b/homeassistant/components/pi_hole/const.py index 0ae62b31865..ca4eea32bd6 100644 --- a/homeassistant/components/pi_hole/const.py +++ b/homeassistant/components/pi_hole/const.py @@ -1,4 +1,4 @@ -"""Constants for the pi_hole intergration.""" +"""Constants for the pi_hole integration.""" from datetime import timedelta DOMAIN = "pi_hole" diff --git a/homeassistant/components/ping/device_tracker.py b/homeassistant/components/ping/device_tracker.py index c4d88f6061c..c0effda7a55 100644 --- a/homeassistant/components/ping/device_tracker.py +++ b/homeassistant/components/ping/device_tracker.py @@ -21,7 +21,7 @@ CONF_PING_COUNT = "count" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Required(const.CONF_HOSTS): {cv.string: cv.string}, + vol.Required(const.CONF_HOSTS): {cv.slug: cv.string}, vol.Optional(CONF_PING_COUNT, default=1): cv.positive_int, } ) diff --git a/homeassistant/components/pioneer/media_player.py b/homeassistant/components/pioneer/media_player.py index 3e71b54c9fa..b834a8e6829 100644 --- a/homeassistant/components/pioneer/media_player.py +++ b/homeassistant/components/pioneer/media_player.py @@ -13,6 +13,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, ) from homeassistant.const import ( CONF_HOST, @@ -36,6 +37,7 @@ DEFAULT_SOURCES = {} SUPPORT_PIONEER = ( SUPPORT_PAUSE | SUPPORT_VOLUME_SET + | SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF diff --git a/homeassistant/components/plex/.translations/ca.json b/homeassistant/components/plex/.translations/ca.json index 63cf65b8d6c..d562d62b602 100644 --- a/homeassistant/components/plex/.translations/ca.json +++ b/homeassistant/components/plex/.translations/ca.json @@ -53,6 +53,8 @@ "step": { "plex_mp_settings": { "data": { + "ignore_new_shared_users": "Ignora els nous usuaris gestionats/compartits", + "monitored_users": "Usuaris monitoritzats", "show_all_controls": "Mostra tots els controls", "use_episode_art": "Utilitza imatges de l'episodi" }, diff --git a/homeassistant/components/plex/.translations/da.json b/homeassistant/components/plex/.translations/da.json index 18dbbb840c3..9b80373727d 100644 --- a/homeassistant/components/plex/.translations/da.json +++ b/homeassistant/components/plex/.translations/da.json @@ -53,6 +53,8 @@ "step": { "plex_mp_settings": { "data": { + "ignore_new_shared_users": "Ignorer nye administrerede/delte brugere", + "monitored_users": "Monitorerede brugere", "show_all_controls": "Vis alle kontrolelementer", "use_episode_art": "Brug episodekunst" }, diff --git a/homeassistant/components/plex/.translations/de.json b/homeassistant/components/plex/.translations/de.json index aa8c5e08dd6..ea8f4b60de4 100644 --- a/homeassistant/components/plex/.translations/de.json +++ b/homeassistant/components/plex/.translations/de.json @@ -53,6 +53,8 @@ "step": { "plex_mp_settings": { "data": { + "ignore_new_shared_users": "Ignorieren neuer verwalteter/freigegebener Benutzer", + "monitored_users": "\u00dcberwachte Benutzer", "show_all_controls": "Alle Steuerelemente anzeigen", "use_episode_art": "Episode-Bilder verwenden" }, diff --git a/homeassistant/components/plex/.translations/en.json b/homeassistant/components/plex/.translations/en.json index 31211182f47..4567171af77 100644 --- a/homeassistant/components/plex/.translations/en.json +++ b/homeassistant/components/plex/.translations/en.json @@ -4,7 +4,7 @@ "all_configured": "All linked servers already configured", "already_configured": "This Plex server is already configured", "already_in_progress": "Plex is being configured", - "discovery_no_file": "No legacy config file found", + "discovery_no_file": "No legacy configuration file found", "invalid_import": "Imported configuration is invalid", "non-interactive": "Non-interactive import", "token_request_timeout": "Timed out obtaining token", @@ -53,6 +53,8 @@ "step": { "plex_mp_settings": { "data": { + "ignore_new_shared_users": "Ignore new managed/shared users", + "monitored_users": "Monitored users", "show_all_controls": "Show all controls", "use_episode_art": "Use episode art" }, diff --git a/homeassistant/components/plex/.translations/es.json b/homeassistant/components/plex/.translations/es.json index 53dd3228288..24127a7332c 100644 --- a/homeassistant/components/plex/.translations/es.json +++ b/homeassistant/components/plex/.translations/es.json @@ -53,6 +53,8 @@ "step": { "plex_mp_settings": { "data": { + "ignore_new_shared_users": "Ignorar nuevos usuarios administrados/compartidos", + "monitored_users": "Usuarios monitorizados", "show_all_controls": "Mostrar todos los controles", "use_episode_art": "Usar el arte de episodios" }, diff --git a/homeassistant/components/plex/.translations/hu.json b/homeassistant/components/plex/.translations/hu.json new file mode 100644 index 00000000000..4712fb37b55 --- /dev/null +++ b/homeassistant/components/plex/.translations/hu.json @@ -0,0 +1,57 @@ +{ + "config": { + "abort": { + "all_configured": "Az \u00f6sszes \u00f6sszekapcsolt szerver m\u00e1r konfigur\u00e1lva van", + "already_configured": "Ez a Plex szerver m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A Plex konfigur\u00e1l\u00e1sa folyamatban van", + "discovery_no_file": "Nem tal\u00e1lhat\u00f3 r\u00e9gi konfigur\u00e1ci\u00f3s f\u00e1jl", + "invalid_import": "Az import\u00e1lt konfigur\u00e1ci\u00f3 \u00e9rv\u00e9nytelen", + "non-interactive": "Nem interakt\u00edv import\u00e1l\u00e1s", + "token_request_timeout": "Token k\u00e9r\u00e9sre sz\u00e1nt id\u0151 lej\u00e1rt", + "unknown": "Ismeretlen okb\u00f3l nem siker\u00fclt" + }, + "error": { + "faulty_credentials": "A hiteles\u00edt\u00e9s sikertelen", + "no_servers": "Nincs szerver csatlakoztatva a fi\u00f3khoz", + "not_found": "A Plex szerver nem tal\u00e1lhat\u00f3" + }, + "step": { + "manual_setup": { + "data": { + "host": "Kiszolg\u00e1l\u00f3", + "port": "Port" + } + }, + "select_server": { + "data": { + "server": "szerver" + }, + "description": "T\u00f6bb szerver el\u00e9rhet\u0151, v\u00e1lasszon egyet:", + "title": "Plex-kiszolg\u00e1l\u00f3 kiv\u00e1laszt\u00e1sa" + }, + "start_website_auth": { + "description": "Folytassa az enged\u00e9lyez\u00e9st a plex.tv webhelyen.", + "title": "Plex-kiszolg\u00e1l\u00f3 csatlakoztat\u00e1sa" + }, + "user": { + "data": { + "token": "Plex token" + }, + "description": "Folytassa az enged\u00e9lyez\u00e9st a plex.tv webhelyen, vagy manu\u00e1lisan konfigur\u00e1lja a szervert.", + "title": "Plex-kiszolg\u00e1l\u00f3 csatlakoztat\u00e1sa" + } + }, + "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Az \u00f6sszes vez\u00e9rl\u0151 megjelen\u00edt\u00e9se", + "use_episode_art": "Haszn\u00e1lja az epiz\u00f3d bor\u00edt\u00f3j\u00e1t" + }, + "description": "Plex media lej\u00e1tsz\u00f3k be\u00e1ll\u00edt\u00e1sai" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/it.json b/homeassistant/components/plex/.translations/it.json index 06c20660fef..e5ff4e01dc0 100644 --- a/homeassistant/components/plex/.translations/it.json +++ b/homeassistant/components/plex/.translations/it.json @@ -4,7 +4,7 @@ "all_configured": "Tutti i server collegati sono gi\u00e0 configurati", "already_configured": "Questo server Plex \u00e8 gi\u00e0 configurato", "already_in_progress": "Plex \u00e8 in fase di configurazione", - "discovery_no_file": "Nessun file di configurazione legacy trovato", + "discovery_no_file": "Non \u00e8 stato trovato nessun file di configurazione da sostituire", "invalid_import": "La configurazione importata non \u00e8 valida", "non-interactive": "Importazione non interattiva", "token_request_timeout": "Timeout per l'ottenimento del token", @@ -53,6 +53,8 @@ "step": { "plex_mp_settings": { "data": { + "ignore_new_shared_users": "Ignora nuovi utenti gestiti/condivisi", + "monitored_users": "Utenti monitorati", "show_all_controls": "Mostra tutti i controlli", "use_episode_art": "Usa la grafica dell'episodio" }, diff --git a/homeassistant/components/plex/.translations/ko.json b/homeassistant/components/plex/.translations/ko.json index cf5a7946b9d..3292fab0a8e 100644 --- a/homeassistant/components/plex/.translations/ko.json +++ b/homeassistant/components/plex/.translations/ko.json @@ -53,6 +53,8 @@ "step": { "plex_mp_settings": { "data": { + "ignore_new_shared_users": "\uc0c8\ub85c\uc6b4 \uad00\ub9ac/\uacf5\uc720 \uc0ac\uc6a9\uc790 \ubb34\uc2dc", + "monitored_users": "\ubaa8\ub2c8\ud130\ub9c1\ub418\ub294 \uc0ac\uc6a9\uc790", "show_all_controls": "\ubaa8\ub4e0 \ucee8\ud2b8\ub864 \ud45c\uc2dc\ud558\uae30", "use_episode_art": "\uc5d0\ud53c\uc18c\ub4dc \uc544\ud2b8 \uc0ac\uc6a9" }, diff --git a/homeassistant/components/plex/.translations/lb.json b/homeassistant/components/plex/.translations/lb.json index c6fcabc40d7..6ed9d372fc1 100644 --- a/homeassistant/components/plex/.translations/lb.json +++ b/homeassistant/components/plex/.translations/lb.json @@ -53,6 +53,8 @@ "step": { "plex_mp_settings": { "data": { + "ignore_new_shared_users": "Nei verwalt / gedeelt Benotzer ignor\u00e9ieren", + "monitored_users": "Iwwerwaachte Benotzer", "show_all_controls": "Weis all Kontrollen", "use_episode_art": "Benotz Biller vun der Episode" }, diff --git a/homeassistant/components/plex/.translations/no.json b/homeassistant/components/plex/.translations/no.json index cc6dac8a35b..c80ba5f2e06 100644 --- a/homeassistant/components/plex/.translations/no.json +++ b/homeassistant/components/plex/.translations/no.json @@ -4,7 +4,7 @@ "all_configured": "Alle knyttet servere som allerede er konfigurert", "already_configured": "Denne Plex-serveren er allerede konfigurert", "already_in_progress": "Plex blir konfigurert", - "discovery_no_file": "Ingen eldre konfigurasjonsfil ble funnet", + "discovery_no_file": "Ingen eldre konfigurasjonsfil funnet", "invalid_import": "Den importerte konfigurasjonen er ugyldig", "non-interactive": "Ikke-interaktiv import", "token_request_timeout": "Tidsavbrudd ved innhenting av token", @@ -53,6 +53,8 @@ "step": { "plex_mp_settings": { "data": { + "ignore_new_shared_users": "Ignorer nye administrerte/delte brukere", + "monitored_users": "Overv\u00e5kede brukere", "show_all_controls": "Vis alle kontroller", "use_episode_art": "Bruk episode bilde" }, diff --git a/homeassistant/components/plex/.translations/pl.json b/homeassistant/components/plex/.translations/pl.json index d752899b9f0..6531b552000 100644 --- a/homeassistant/components/plex/.translations/pl.json +++ b/homeassistant/components/plex/.translations/pl.json @@ -2,7 +2,7 @@ "config": { "abort": { "all_configured": "Wszystkie znalezione serwery s\u0105 ju\u017c skonfigurowane.", - "already_configured": "Serwer Plex jest ju\u017c skonfigurowany", + "already_configured": "Ten 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", @@ -53,6 +53,8 @@ "step": { "plex_mp_settings": { "data": { + "ignore_new_shared_users": "Ignoruj nowych zarz\u0105dzanych/wsp\u00f3\u0142dzielonych u\u017cytkownik\u00f3w", + "monitored_users": "Monitorowani u\u017cytkownicy", "show_all_controls": "Poka\u017c wszystkie elementy steruj\u0105ce", "use_episode_art": "U\u017cyj grafiki odcinka" }, diff --git a/homeassistant/components/plex/.translations/ru.json b/homeassistant/components/plex/.translations/ru.json index 334a4e353d4..2da10b1e8c4 100644 --- a/homeassistant/components/plex/.translations/ru.json +++ b/homeassistant/components/plex/.translations/ru.json @@ -53,6 +53,8 @@ "step": { "plex_mp_settings": { "data": { + "ignore_new_shared_users": "\u0418\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0445 \u0443\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u043c\u044b\u0445/\u043e\u0431\u0449\u0438\u0445 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439", + "monitored_users": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438", "show_all_controls": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0432\u0441\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f", "use_episode_art": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043e\u0431\u043b\u043e\u0436\u043a\u0438 \u044d\u043f\u0438\u0437\u043e\u0434\u043e\u0432" }, diff --git a/homeassistant/components/plex/.translations/sv.json b/homeassistant/components/plex/.translations/sv.json index 702cec128c0..25152e9dc81 100644 --- a/homeassistant/components/plex/.translations/sv.json +++ b/homeassistant/components/plex/.translations/sv.json @@ -1,10 +1,62 @@ { + "config": { + "abort": { + "all_configured": "Alla l\u00e4nkade servrar har redan konfigurerats", + "already_configured": "Denna Plex-server \u00e4r redan konfigurerad", + "already_in_progress": "Plex konfigureras", + "discovery_no_file": "Ingen \u00e4ldre konfigurationsfil hittades", + "invalid_import": "Importerad konfiguration \u00e4r ogiltig", + "non-interactive": "Icke-interaktiv import", + "token_request_timeout": "Timeout att erh\u00e5lla token", + "unknown": "Misslyckades av ok\u00e4nd anledning" + }, + "error": { + "faulty_credentials": "Auktoriseringen misslyckades", + "no_servers": "Inga servrar l\u00e4nkade till konto", + "no_token": "Ange en token eller v\u00e4lj manuell inst\u00e4llning", + "not_found": "Plex-server hittades inte" + }, + "step": { + "manual_setup": { + "data": { + "host": "V\u00e4rd", + "port": "Port", + "ssl": "Anv\u00e4nd SSL", + "token": "Token (om det beh\u00f6vs)", + "verify_ssl": "Verifiera SSL-certifikat" + }, + "title": "Plex-server" + }, + "select_server": { + "data": { + "server": "Server" + }, + "description": "V\u00e4lj flera servrar tillg\u00e4ngliga, v\u00e4lj en:", + "title": "V\u00e4lj Plex-server" + }, + "start_website_auth": { + "description": "Forts\u00e4tt att auktorisera p\u00e5 plex.tv.", + "title": "Anslut Plex-servern" + }, + "user": { + "data": { + "manual_setup": "Manuell inst\u00e4llning", + "token": "Plex-token" + }, + "description": "Forts\u00e4tt att auktorisera p\u00e5 plex.tv eller konfigurera en server manuellt.", + "title": "Anslut Plex-servern" + } + }, + "title": "Plex" + }, "options": { "step": { "plex_mp_settings": { "data": { - "show_all_controls": "Visa alla kontroller" - } + "show_all_controls": "Visa alla kontroller", + "use_episode_art": "Anv\u00e4nd avsnittsbild" + }, + "description": "Alternativ f\u00f6r Plex-mediaspelare" } } } diff --git a/homeassistant/components/plex/.translations/zh-Hans.json b/homeassistant/components/plex/.translations/zh-Hans.json new file mode 100644 index 00000000000..614f83e3cc0 --- /dev/null +++ b/homeassistant/components/plex/.translations/zh-Hans.json @@ -0,0 +1,12 @@ +{ + "options": { + "step": { + "plex_mp_settings": { + "data": { + "ignore_new_shared_users": "\u5ffd\u7565\u65b0\u589e\u7ba1\u7406/\u5171\u4eab\u4f7f\u7528\u8005", + "monitored_users": "\u53d7\u76d1\u89c6\u7684\u7528\u6237" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/zh-Hant.json b/homeassistant/components/plex/.translations/zh-Hant.json index 5c05d2104f9..436333b0a79 100644 --- a/homeassistant/components/plex/.translations/zh-Hant.json +++ b/homeassistant/components/plex/.translations/zh-Hant.json @@ -53,6 +53,8 @@ "step": { "plex_mp_settings": { "data": { + "ignore_new_shared_users": "\u5ffd\u7565\u65b0\u589e\u7ba1\u7406/\u5206\u4eab\u4f7f\u7528\u8005", + "monitored_users": "\u5df2\u76e3\u63a7\u4f7f\u7528\u8005", "show_all_controls": "\u986f\u793a\u6240\u6709\u63a7\u5236", "use_episode_art": "\u4f7f\u7528\u5f71\u96c6\u5287\u7167" }, diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 89659769192..c9b120f75f6 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -19,6 +19,7 @@ from homeassistant.const import ( CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP, ) +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.dispatcher import ( @@ -27,6 +28,7 @@ from homeassistant.helpers.dispatcher import ( ) from .const import ( + CONF_IGNORE_NEW_SHARED_USERS, CONF_SERVER, CONF_SERVER_IDENTIFIER, CONF_SHOW_ALL_CONTROLS, @@ -50,6 +52,7 @@ MEDIA_PLAYER_SCHEMA = vol.Schema( { vol.Optional(CONF_USE_EPISODE_ART, default=False): cv.boolean, vol.Optional(CONF_SHOW_ALL_CONTROLS, default=False): cv.boolean, + vol.Optional(CONF_IGNORE_NEW_SHARED_USERS, default=False): cv.boolean, } ) @@ -127,7 +130,7 @@ async def async_setup_entry(hass, entry): server_config[CONF_URL], error, ) - return False + raise ConfigEntryNotReady except ( plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized, diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index d38d13c847e..19cec6dfb8b 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -14,12 +14,15 @@ from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.const import CONF_SSL, CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json from .const import ( # pylint: disable=unused-import AUTH_CALLBACK_NAME, AUTH_CALLBACK_PATH, CONF_CLIENT_IDENTIFIER, + CONF_IGNORE_NEW_SHARED_USERS, + CONF_MONITORED_USERS, CONF_SERVER, CONF_SERVER_IDENTIFIER, CONF_SHOW_ALL_CONTROLS, @@ -28,6 +31,7 @@ from .const import ( # pylint: disable=unused-import DOMAIN, PLEX_CONFIG_FILE, PLEX_SERVER_CONFIG, + SERVERS, X_PLEX_DEVICE_NAME, X_PLEX_PLATFORM, X_PLEX_PRODUCT, @@ -52,7 +56,7 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Plex config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH @staticmethod @callback @@ -254,6 +258,7 @@ class PlexOptionsFlowHandler(config_entries.OptionsFlow): def __init__(self, config_entry): """Initialize Plex options flow.""" self.options = copy.deepcopy(config_entry.options) + self.server_id = config_entry.data[CONF_SERVER_IDENTIFIER] async def async_step_init(self, user_input=None): """Manage the Plex options.""" @@ -261,6 +266,8 @@ class PlexOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_plex_mp_settings(self, user_input=None): """Manage the Plex media_player options.""" + plex_server = self.hass.data[DOMAIN][SERVERS][self.server_id] + if user_input is not None: self.options[MP_DOMAIN][CONF_USE_EPISODE_ART] = user_input[ CONF_USE_EPISODE_ART @@ -268,19 +275,56 @@ class PlexOptionsFlowHandler(config_entries.OptionsFlow): self.options[MP_DOMAIN][CONF_SHOW_ALL_CONTROLS] = user_input[ CONF_SHOW_ALL_CONTROLS ] + self.options[MP_DOMAIN][CONF_IGNORE_NEW_SHARED_USERS] = user_input[ + CONF_IGNORE_NEW_SHARED_USERS + ] + + account_data = { + user: {"enabled": bool(user in user_input[CONF_MONITORED_USERS])} + for user in plex_server.accounts + } + + self.options[MP_DOMAIN][CONF_MONITORED_USERS] = account_data + return self.async_create_entry(title="", data=self.options) + available_accounts = {name: name for name in plex_server.accounts} + available_accounts[plex_server.owner] += " [Owner]" + + default_accounts = plex_server.accounts + known_accounts = set(plex_server.option_monitored_users) + if known_accounts: + default_accounts = { + user + for user in plex_server.option_monitored_users + if plex_server.option_monitored_users[user]["enabled"] + } + for user in plex_server.accounts: + if user not in known_accounts: + available_accounts[user] += " [New]" + + if not plex_server.option_ignore_new_shared_users: + for new_user in plex_server.accounts - known_accounts: + default_accounts.add(new_user) + return self.async_show_form( step_id="plex_mp_settings", data_schema=vol.Schema( { vol.Required( CONF_USE_EPISODE_ART, - default=self.options[MP_DOMAIN][CONF_USE_EPISODE_ART], + default=plex_server.option_use_episode_art, ): bool, vol.Required( CONF_SHOW_ALL_CONTROLS, - default=self.options[MP_DOMAIN][CONF_SHOW_ALL_CONTROLS], + default=plex_server.option_show_all_controls, + ): bool, + vol.Optional( + CONF_MONITORED_USERS, default=default_accounts + ): cv.multi_select(available_accounts), + vol.Required( + CONF_IGNORE_NEW_SHARED_USERS, + default=plex_server.option_ignore_new_shared_users, ): bool, } ), diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index ad62bade1fd..7d6812674ca 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -3,6 +3,7 @@ from homeassistant.const import __version__ DOMAIN = "plex" NAME_FORMAT = "Plex ({})" +COMMON_PLAYERS = ["Plex Web"] DEFAULT_PORT = 32400 DEFAULT_SSL = False @@ -28,6 +29,8 @@ CONF_SERVER = "server" CONF_SERVER_IDENTIFIER = "server_id" CONF_USE_EPISODE_ART = "use_episode_art" CONF_SHOW_ALL_CONTROLS = "show_all_controls" +CONF_IGNORE_NEW_SHARED_USERS = "ignore_new_shared_users" +CONF_MONITORED_USERS = "monitored_users" AUTH_CALLBACK_PATH = "/auth/plex/callback" AUTH_CALLBACK_NAME = "auth:plex:callback" diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index d8155d1a43b..47e5ba6104f 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -21,19 +21,14 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, ) -from homeassistant.const import ( - DEVICE_DEFAULT_NAME, - STATE_IDLE, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.const import STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.util import dt as dt_util from .const import ( + COMMON_PLAYERS, CONF_SERVER_IDENTIFIER, DISPATCHERS, DOMAIN as PLEX_DOMAIN, @@ -114,6 +109,10 @@ class PlexMediaPlayer(MediaPlayerDevice): self._is_player_active = False self._machine_identifier = device.machineIdentifier self._make = "" + self._device_platform = None + self._device_product = None + self._device_title = None + self._device_version = None self._name = None self._player_state = "idle" self._previous_volume_level = 1 # Used in fake muting @@ -128,6 +127,7 @@ class PlexMediaPlayer(MediaPlayerDevice): self._media_content_type = None self._media_duration = None self._media_image_url = None + self._media_summary = None self._media_title = None self._media_position = None self._media_position_updated_at = None @@ -169,6 +169,7 @@ class PlexMediaPlayer(MediaPlayerDevice): self._media_content_type = None self._media_duration = None self._media_image_url = None + self._media_summary = None self._media_title = None # Music self._media_album_artist = None @@ -188,7 +189,6 @@ class PlexMediaPlayer(MediaPlayerDevice): self._clear_media_details() self._available = self.device or self.session - name_base = None if self.device: try: @@ -197,7 +197,10 @@ class PlexMediaPlayer(MediaPlayerDevice): device_url = "127.0.0.1" if "127.0.0.1" in device_url: self.device.proxyThroughServer() - name_base = self.device.title or self.device.product + self._device_platform = self.device.platform + self._device_product = self.device.product + self._device_title = self.device.title + self._device_version = self.device.version self._device_protocol_capabilities = self.device.protocolCapabilities self._player_state = self.device.state @@ -215,11 +218,15 @@ class PlexMediaPlayer(MediaPlayerDevice): 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 + self._device_platform = self._device_platform or session_device.platform + self._device_product = self._device_product or session_device.product + self._device_title = self._device_title or session_device.title + self._device_version = self._device_version or session_device.version else: _LOGGER.warning("No player associated with active session") - self._session_username = self.session.usernames[0] + if self.session.usernames: + self._session_username = self.session.usernames[0] # Calculate throttled position for proper progress display. position = int(self.session.viewOffset / 1000) @@ -237,13 +244,21 @@ class PlexMediaPlayer(MediaPlayerDevice): 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) + name_parts = [self._device_product, self._device_title or self._device_platform] + if (self._device_product in COMMON_PLAYERS) and self.make: + # Add more context in name for likely duplicates + name_parts.append(self.make) + if self.username and self.username != self.plex_server.owner: + # Prepend username for shared/managed clients + name_parts.insert(0, self.username) + self._name = NAME_FORMAT.format(" - ".join(name_parts)) 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) # title (movie name, tv episode name, music song name) + self._media_summary = self.session.summary self._media_title = self.session.title # media type self._set_media_type() @@ -260,7 +275,7 @@ class PlexMediaPlayer(MediaPlayerDevice): thumb_url = self.session.thumbUrl if ( self.media_content_type is MEDIA_TYPE_TVSHOW - and not self.plex_server.use_episode_art + and not self.plex_server.option_use_episode_art ): thumb_url = self.session.url(self.session.grandparentThumb) @@ -348,6 +363,11 @@ class PlexMediaPlayer(MediaPlayerDevice): """Return the name of the device.""" return self._name + @property + def username(self): + """Return the username of the client owner.""" + return self._session_username + @property def app_name(self): """Return the library name of playing media.""" @@ -428,6 +448,11 @@ class PlexMediaPlayer(MediaPlayerDevice): """Return the image URL of current playing media.""" return self._media_image_url + @property + def media_summary(self): + """Return the summary of current playing media.""" + return self._media_summary + @property def media_title(self): """Return the title of current playing media.""" @@ -457,7 +482,7 @@ class PlexMediaPlayer(MediaPlayerDevice): def supported_features(self): """Flag media player features that are supported.""" # force show all controls - if self.plex_server.show_all_controls: + if self.plex_server.option_show_all_controls: return ( SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK @@ -699,8 +724,24 @@ class PlexMediaPlayer(MediaPlayerDevice): """Return the scene state attributes.""" attr = { "media_content_rating": self._media_content_rating, - "session_username": self._session_username, + "session_username": self.username, "media_library_name": self._app_name, + "summary": self.media_summary, } return attr + + @property + def device_info(self): + """Return a device description for device registry.""" + if self.machine_identifier is None: + return None + + return { + "identifiers": {(PLEX_DOMAIN, self.machine_identifier)}, + "manufacturer": self._device_platform or "Plex", + "model": self._device_product or self.make, + "name": self.name, + "sw_version": self._device_version, + "via_device": (PLEX_DOMAIN, self.plex_server.machine_identifier), + } diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index 2aed57946eb..b1e93aec8c0 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -101,23 +101,24 @@ class PlexSensor(Entity): _LOGGER.debug("Refreshing sensor [%s]", self.unique_id) now_playing = [] for sess in self.sessions: + if sess.TYPE == "photo": + _LOGGER.debug("Photo session detected, skipping: %s", sess) + continue user = sess.usernames[0] device = sess.players[0].title now_playing_user = f"{user} - {device}" now_playing_title = "" - if sess.TYPE == "episode": + if sess.TYPE in ["clip", "episode"]: # example: - # "Supernatural (2005) - S01 · E13 - Route 666" + # "Supernatural (2005) - s01e13 - Route 666" season_title = sess.grandparentTitle if sess.show().year is not None: - season_title += " ({0})".format(sess.show().year) - season_episode = "S{0}".format(sess.parentIndex) - if sess.index is not None: - season_episode += f" · E{sess.index}" + season_title += f" ({sess.show().year!s})" + season_episode = sess.seasonEpisode episode_title = sess.title - now_playing_title = "{0} - {1} - {2}".format( - season_title, season_episode, episode_title + now_playing_title = ( + f"{season_title} - {season_episode} - {episode_title}" ) elif sess.TYPE == "track": # example: @@ -125,9 +126,7 @@ class PlexSensor(Entity): track_artist = sess.grandparentTitle track_album = sess.parentTitle track_title = sess.title - now_playing_title = "{0} - {1} - {2}".format( - track_artist, track_album, track_title - ) + now_playing_title = f"{track_artist} - {track_album} - {track_title}" else: # example: # "picture_of_last_summer_camp (2015)" @@ -139,3 +138,17 @@ class PlexSensor(Entity): now_playing.append((now_playing_user, now_playing_title)) self._state = len(self.sessions) self._now_playing = now_playing + + @property + def device_info(self): + """Return a device description for device registry.""" + if self.unique_id is None: + return None + + return { + "identifiers": {(PLEX_DOMAIN, self._server.machine_identifier)}, + "manufacturer": "Plex", + "model": "Plex Media Server", + "name": "Activity Sensor", + "sw_version": self._server.version, + } diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 46602cf6552..5532362b87a 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -13,6 +13,8 @@ from homeassistant.helpers.dispatcher import dispatcher_send from .const import ( CONF_CLIENT_IDENTIFIER, + CONF_IGNORE_NEW_SHARED_USERS, + CONF_MONITORED_USERS, CONF_SERVER, CONF_SHOW_ALL_CONTROLS, CONF_USE_EPISODE_ART, @@ -51,6 +53,9 @@ class PlexServer: self._verify_ssl = server_config.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL) self.options = options self.server_choice = None + self._accounts = [] + self._owner_username = None + self._version = None # Header conditionally added as it is not available in config entry v1 if CONF_CLIENT_IDENTIFIER in server_config: @@ -93,6 +98,22 @@ class PlexServer: else: _connect_with_token() + self._accounts = [ + account.name + for account in self._plex_server.systemAccounts() + if account.name + ] + + owner_account = [ + account.name + for account in self._plex_server.systemAccounts() + if account.accountID == 1 + ] + if owner_account: + self._owner_username = owner_account[0] + + self._version = self._plex_server.version + def refresh_entity(self, machine_identifier, device, session): """Forward refresh dispatch to media_player.""" unique_id = f"{self.machine_identifier}:{machine_identifier}" @@ -109,8 +130,22 @@ class PlexServer: _LOGGER.debug("Updating devices") available_clients = {} + ignored_clients = set() new_clients = set() + monitored_users = self.accounts + known_accounts = set(self.option_monitored_users) + if known_accounts: + monitored_users = { + user + for user in self.option_monitored_users + if self.option_monitored_users[user]["enabled"] + } + + if not self.option_ignore_new_shared_users: + for new_user in self.accounts - known_accounts: + monitored_users.add(new_user) + try: devices = self._plex_server.clients() sessions = self._plex_server.sessions() @@ -132,7 +167,15 @@ class PlexServer: _LOGGER.debug("New device: %s", device.machineIdentifier) for session in sessions: + if session.TYPE == "photo": + _LOGGER.debug("Photo session detected, skipping: %s", session) + continue + session_username = session.usernames[0] for player in session.players: + if session_username not in monitored_users: + ignored_clients.add(player.machineIdentifier) + _LOGGER.debug("Ignoring Plex client owned by %s", session_username) + continue self._known_idle.discard(player.machineIdentifier) available_clients.setdefault( player.machineIdentifier, {"device": player} @@ -145,6 +188,8 @@ class PlexServer: new_entity_configs = [] for client_id, client_data in available_clients.items(): + if client_id in ignored_clients: + continue if client_id in new_clients: new_entity_configs.append(client_data) else: @@ -152,11 +197,11 @@ class PlexServer: client_id, client_data["device"], client_data.get("session") ) - self._known_clients.update(new_clients) + self._known_clients.update(new_clients | ignored_clients) - idle_clients = (self._known_clients - self._known_idle).difference( - available_clients - ) + idle_clients = ( + self._known_clients - self._known_idle - ignored_clients + ).difference(available_clients) for client_id in idle_clients: self.refresh_entity(client_id, None, None) self._known_idle.add(client_id) @@ -179,6 +224,21 @@ class PlexServer: """Return the plexapi PlexServer instance.""" return self._plex_server + @property + def accounts(self): + """Return accounts associated with the Plex server.""" + return set(self._accounts) + + @property + def owner(self): + """Return the Plex server owner username.""" + return self._owner_username + + @property + def version(self): + """Return the version of the Plex server.""" + return self._version + @property def friendly_name(self): """Return name of connected Plex server.""" @@ -195,15 +255,25 @@ class PlexServer: return self._plex_server._baseurl # pylint: disable=protected-access @property - def use_episode_art(self): + def option_ignore_new_shared_users(self): + """Return ignore_new_shared_users option.""" + return self.options[MP_DOMAIN].get(CONF_IGNORE_NEW_SHARED_USERS, False) + + @property + def option_use_episode_art(self): """Return use_episode_art option.""" return self.options[MP_DOMAIN][CONF_USE_EPISODE_ART] @property - def show_all_controls(self): + def option_show_all_controls(self): """Return show_all_controls option.""" return self.options[MP_DOMAIN][CONF_SHOW_ALL_CONTROLS] + @property + def option_monitored_users(self): + """Return dict of monitored users option.""" + return self.options[MP_DOMAIN].get(CONF_MONITORED_USERS, {}) + @property def library(self): """Return library attribute from server object.""" diff --git a/homeassistant/components/plex/strings.json b/homeassistant/components/plex/strings.json index b6491db350c..1f99e28df8b 100644 --- a/homeassistant/components/plex/strings.json +++ b/homeassistant/components/plex/strings.json @@ -23,7 +23,7 @@ "all_configured": "All linked servers already configured", "already_configured": "This Plex server is already configured", "already_in_progress": "Plex is being configured", - "discovery_no_file": "No legacy config file found", + "discovery_no_file": "No legacy configuration file found", "invalid_import": "Imported configuration is invalid", "non-interactive": "Non-interactive import", "token_request_timeout": "Timed out obtaining token", @@ -36,7 +36,9 @@ "description": "Options for Plex Media Players", "data": { "use_episode_art": "Use episode art", - "show_all_controls": "Show all controls" + "show_all_controls": "Show all controls", + "ignore_new_shared_users": "Ignore new managed/shared users", + "monitored_users": "Monitored users" } } } diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index 5fc3d189b69..601f017d42f 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/plugwise", "dependencies": [], "codeowners": ["@laetificat", "@CoMPaTech", "@bouwew"], - "requirements": ["haanna==0.14.1"] + "requirements": ["haanna==0.14.3"] } diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index c20296a2c18..d77cb4f56da 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -334,7 +334,7 @@ class PrometheusMetrics: @staticmethod def _sensor_fallback_metric(state, unit): - """Get metric from fallback logic for compatability.""" + """Get metric from fallback logic for compatibility.""" if unit in (None, ""): _LOGGER.debug("Unsupported sensor: %s", state.entity_id) return None diff --git a/homeassistant/components/proxmoxve/__init__.py b/homeassistant/components/proxmoxve/__init__.py index 58cb50ee304..315fb8b1c91 100644 --- a/homeassistant/components/proxmoxve/__init__.py +++ b/homeassistant/components/proxmoxve/__init__.py @@ -147,12 +147,12 @@ class ProxmoxClient: verify_ssl=self._verify_ssl, ) - self._connection_start_time = time.time() + self._connection_start_time = time.monotonic() def get_api_client(self): """Return the ProxmoxAPI client and rebuild it if necessary.""" - connection_age = time.time() - self._connection_start_time + connection_age = time.monotonic() - self._connection_start_time # Workaround for the Proxmoxer bug where the connection stops working after some time if connection_age > 30 * 60: diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index 06498e51ad3..d12fbe2d3d7 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -2,7 +2,9 @@ "domain": "proxy", "name": "Camera Proxy", "documentation": "https://www.home-assistant.io/integrations/proxy", - "requirements": ["pillow==6.2.1"], + "requirements": [ + "pillow==7.0.0" + ], "dependencies": [], "codeowners": [] -} +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/de.json b/homeassistant/components/ps4/.translations/de.json index 6f4962a305d..66eaecbb548 100644 --- a/homeassistant/components/ps4/.translations/de.json +++ b/homeassistant/components/ps4/.translations/de.json @@ -25,7 +25,7 @@ "name": "Name", "region": "Region" }, - "description": "Geben deine PlayStation 4-Informationen ein. Navigiere f\u00fcr \"PIN\" auf der PlayStation 4-Konsole zu \"Einstellungen\". Navigiere dann zu \"Mobile App-Verbindungseinstellungen\" und w\u00e4hle \"Ger\u00e4t hinzuf\u00fcgen\" aus. Gib die angezeigte PIN ein.", + "description": "Gib deine PlayStation 4-Informationen ein. Navigiere f\u00fcr \"PIN\" auf der PlayStation 4-Konsole zu \"Einstellungen\". Navigiere dann zu \"Mobile App-Verbindungseinstellungen\" und w\u00e4hle \"Ger\u00e4t hinzuf\u00fcgen\" aus. Gib die angezeigte PIN ein.", "title": "PlayStation 4" }, "mode": { diff --git a/homeassistant/components/ps4/.translations/hu.json b/homeassistant/components/ps4/.translations/hu.json index 77b13f33a51..7a8623b9030 100644 --- a/homeassistant/components/ps4/.translations/hu.json +++ b/homeassistant/components/ps4/.translations/hu.json @@ -6,6 +6,7 @@ }, "link": { "data": { + "code": "PIN", "ip_address": "IP-c\u00edm", "name": "N\u00e9v", "region": "R\u00e9gi\u00f3" diff --git a/homeassistant/components/ps4/manifest.json b/homeassistant/components/ps4/manifest.json index 8551b3da3e6..80c12cc746c 100644 --- a/homeassistant/components/ps4/manifest.json +++ b/homeassistant/components/ps4/manifest.json @@ -3,7 +3,7 @@ "name": "Sony PlayStation 4", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ps4", - "requirements": ["pyps4-2ndscreen==1.0.6"], + "requirements": ["pyps4-2ndscreen==1.0.7"], "dependencies": [], "codeowners": ["@ktnrg45"] } diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index 33b5c556c7d..28d201d78cd 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -97,11 +97,7 @@ class PS4Device(MediaPlayerDevice): def status_callback(self): """Handle status callback. Parse status.""" self._parse_status() - - @callback - def schedule_update(self): - """Schedules update with HA.""" - self.async_schedule_update_ha_state() + self.async_write_ha_state() @callback def subscribe_to_protocol(self): @@ -184,7 +180,6 @@ class PS4Device(MediaPlayerDevice): self._media_content_id = title_id if self._use_saved(): _LOGGER.debug("Using saved data for media: %s", title_id) - self.schedule_update() return self._media_title = name @@ -223,13 +218,11 @@ class PS4Device(MediaPlayerDevice): """Set states for state idle.""" self.reset_title() self._state = STATE_IDLE - self.schedule_update() def state_standby(self): """Set states for state standby.""" self.reset_title() self._state = STATE_STANDBY - self.schedule_update() def state_unknown(self): """Set states for state unknown.""" @@ -286,8 +279,8 @@ class PS4Device(MediaPlayerDevice): self._media_image = art or None self._media_type = media_type - self.update_list() - self.schedule_update() + await self.hass.async_add_executor_job(self.update_list) + self.async_write_ha_state() def update_list(self): """Update Game List, Correct data if different.""" diff --git a/homeassistant/components/pushover/manifest.json b/homeassistant/components/pushover/manifest.json index 3428e429b8c..9bdd1bb53f9 100644 --- a/homeassistant/components/pushover/manifest.json +++ b/homeassistant/components/pushover/manifest.json @@ -2,7 +2,7 @@ "domain": "pushover", "name": "Pushover", "documentation": "https://www.home-assistant.io/integrations/pushover", - "requirements": ["python-pushover==0.4"], + "requirements": ["pushover_complete==1.1.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/pushover/notify.py b/homeassistant/components/pushover/notify.py index 1930ff66f2e..bc44cbeddb7 100644 --- a/homeassistant/components/pushover/notify.py +++ b/homeassistant/components/pushover/notify.py @@ -1,8 +1,7 @@ """Pushover platform for notify component.""" import logging -from pushover import Client, InitError, RequestError -import requests +from pushover_complete import PushoverAPI import voluptuous as vol from homeassistant.components.notify import ( @@ -19,6 +18,15 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) ATTR_ATTACHMENT = "attachment" +ATTR_URL = "url" +ATTR_URL_TITLE = "url_title" +ATTR_PRIORITY = "priority" +ATTR_RETRY = "retry" +ATTR_SOUND = "sound" +ATTR_HTML = "html" +ATTR_CALLBACK_URL = "callback_url" +ATTR_EXPIRE = "expire" +ATTR_TIMESTAMP = "timestamp" CONF_USER_KEY = "user_key" @@ -29,13 +37,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_service(hass, config, discovery_info=None): """Get the Pushover notification service.""" - try: - return PushoverNotificationService( - hass, config[CONF_USER_KEY], config[CONF_API_KEY] - ) - except InitError: - _LOGGER.error("Wrong API key supplied") - return None + return PushoverNotificationService( + hass, config[CONF_USER_KEY], config[CONF_API_KEY] + ) class PushoverNotificationService(BaseNotificationService): @@ -46,54 +50,42 @@ class PushoverNotificationService(BaseNotificationService): self._hass = hass self._user_key = user_key self._api_token = api_token - self.pushover = Client(self._user_key, api_token=self._api_token) + self.pushover = PushoverAPI(self._api_token) def send_message(self, message="", **kwargs): """Send a message to a user.""" - # Make a copy and use empty dict if necessary + + # Extract params from data dict + title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) data = dict(kwargs.get(ATTR_DATA) or {}) + url = data.get(ATTR_URL, None) + url_title = data.get(ATTR_URL_TITLE, None) + priority = data.get(ATTR_PRIORITY, None) + retry = data.get(ATTR_PRIORITY, None) + expire = data.get(ATTR_EXPIRE, None) + callback_url = data.get(ATTR_CALLBACK_URL, None) + timestamp = data.get(ATTR_TIMESTAMP, None) + sound = data.get(ATTR_SOUND, None) + html = 1 if data.get(ATTR_HTML, False) else 0 - data["title"] = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) - - # Check for attachment. - if ATTR_ATTACHMENT in data: - # If attachment is a URL, use requests to open it as a stream. - if data[ATTR_ATTACHMENT].startswith("http"): + image = data.get(ATTR_ATTACHMENT, None) + # Check for attachment + if image is not None: + # Only allow attachments from whitelisted paths, check valid path + if self._hass.config.is_allowed_path(data[ATTR_ATTACHMENT]): + # try to open it as a normal file. try: - response = requests.get( - data[ATTR_ATTACHMENT], stream=True, timeout=5 - ) - if response.status_code == 200: - # Replace the attachment identifier with file object. - data[ATTR_ATTACHMENT] = response.content - else: - _LOGGER.error( - "Failed to download image %s, response code: %d", - data[ATTR_ATTACHMENT], - response.status_code, - ) - # Remove attachment key to send without attachment. - del data[ATTR_ATTACHMENT] - except requests.exceptions.RequestException as ex_val: + file_handle = open(data[ATTR_ATTACHMENT], "rb") + # Replace the attachment identifier with file object. + image = file_handle + except OSError as ex_val: _LOGGER.error(ex_val) - # Remove attachment key to try sending without attachment - del data[ATTR_ATTACHMENT] - else: - # Not a URL, check valid path first - if self._hass.config.is_allowed_path(data[ATTR_ATTACHMENT]): - # try to open it as a normal file. - try: - file_handle = open(data[ATTR_ATTACHMENT], "rb") - # Replace the attachment identifier with file object. - data[ATTR_ATTACHMENT] = file_handle - except OSError as ex_val: - _LOGGER.error(ex_val) - # Remove attachment key to send without attachment. - del data[ATTR_ATTACHMENT] - else: - _LOGGER.error("Path is not whitelisted") # Remove attachment key to send without attachment. - del data[ATTR_ATTACHMENT] + image = None + else: + _LOGGER.error("Path is not whitelisted") + # Remove attachment key to send without attachment. + image = None targets = kwargs.get(ATTR_TARGET) @@ -101,12 +93,22 @@ class PushoverNotificationService(BaseNotificationService): targets = [targets] for target in targets: - if target is not None: - data["device"] = target - try: - self.pushover.send_message(message, **data) + self.pushover.send_message( + self._user_key, + message, + target, + title, + url, + url_title, + image, + priority, + retry, + expire, + callback_url, + timestamp, + sound, + html, + ) except ValueError as val_err: _LOGGER.error(val_err) - except RequestError: - _LOGGER.exception("Could not send pushover notification") diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index fd4461e3e1b..579919821a3 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_SSL, CONF_USERNAME, CONTENT_TYPE_JSON, + DATA_RATE_MEGABYTES_PER_SECOND, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -29,7 +30,7 @@ DEFAULT_PORT = 8000 MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=15) -SENSOR_TYPES = {"speed": ["speed", "Speed", "MB/s"]} +SENSOR_TYPES = {"speed": ["speed", "Speed", DATA_RATE_MEGABYTES_PER_SECOND]} PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { diff --git a/homeassistant/components/qbittorrent/sensor.py b/homeassistant/components/qbittorrent/sensor.py index 9544d74b1cd..46f82e99a62 100644 --- a/homeassistant/components/qbittorrent/sensor.py +++ b/homeassistant/components/qbittorrent/sensor.py @@ -11,6 +11,7 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_URL, CONF_USERNAME, + DATA_RATE_KILOBYTES_PER_SECOND, STATE_IDLE, ) from homeassistant.exceptions import PlatformNotReady @@ -27,8 +28,8 @@ DEFAULT_NAME = "qBittorrent" SENSOR_TYPES = { SENSOR_TYPE_CURRENT_STATUS: ["Status", None], - SENSOR_TYPE_DOWNLOAD_SPEED: ["Down Speed", "kB/s"], - SENSOR_TYPE_UPLOAD_SPEED: ["Up Speed", "kB/s"], + SENSOR_TYPE_DOWNLOAD_SPEED: ["Down Speed", DATA_RATE_KILOBYTES_PER_SECOND], + SENSOR_TYPE_UPLOAD_SPEED: ["Up Speed", DATA_RATE_KILOBYTES_PER_SECOND], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/qnap/sensor.py b/homeassistant/components/qnap/sensor.py index c3863bd0077..1ad53f4db48 100644 --- a/homeassistant/components/qnap/sensor.py +++ b/homeassistant/components/qnap/sensor.py @@ -16,6 +16,8 @@ from homeassistant.const import ( CONF_TIMEOUT, CONF_USERNAME, CONF_VERIFY_SSL, + DATA_GIBIBYTES, + DATA_RATE_MEBIBYTES_PER_SECOND, TEMP_CELSIUS, ) from homeassistant.exceptions import PlatformNotReady @@ -62,22 +64,22 @@ _CPU_MON_COND = { "cpu_usage": ["CPU Usage", "%", "mdi:chip"], } _MEMORY_MON_COND = { - "memory_free": ["Memory Available", "GB", "mdi:memory"], - "memory_used": ["Memory Used", "GB", "mdi:memory"], + "memory_free": ["Memory Available", DATA_GIBIBYTES, "mdi:memory"], + "memory_used": ["Memory Used", DATA_GIBIBYTES, "mdi:memory"], "memory_percent_used": ["Memory Usage", "%", "mdi:memory"], } _NETWORK_MON_COND = { "network_link_status": ["Network Link", None, "mdi:checkbox-marked-circle-outline"], - "network_tx": ["Network Up", "MB/s", "mdi:upload"], - "network_rx": ["Network Down", "MB/s", "mdi:download"], + "network_tx": ["Network Up", DATA_RATE_MEBIBYTES_PER_SECOND, "mdi:upload"], + "network_rx": ["Network Down", DATA_RATE_MEBIBYTES_PER_SECOND, "mdi:download"], } _DRIVE_MON_COND = { "drive_smart_status": ["SMART Status", None, "mdi:checkbox-marked-circle-outline"], "drive_temp": ["Temperature", TEMP_CELSIUS, "mdi:thermometer"], } _VOLUME_MON_COND = { - "volume_size_used": ["Used Space", "GB", "mdi:chart-pie"], - "volume_size_free": ["Free Space", "GB", "mdi:chart-pie"], + "volume_size_used": ["Used Space", DATA_GIBIBYTES, "mdi:chart-pie"], + "volume_size_free": ["Free Space", DATA_GIBIBYTES, "mdi:chart-pie"], "volume_percentage_used": ["Volume Used", "%", "mdi:chart-pie"], } @@ -270,7 +272,7 @@ class QNAPMemorySensor(QNAPSensor): if self._api.data: data = self._api.data["system_stats"]["memory"] size = round_nicely(float(data["total"]) / 1024) - return {ATTR_MEMORY_SIZE: f"{size} GB"} + return {ATTR_MEMORY_SIZE: f"{size} {DATA_GIBIBYTES}"} class QNAPNetworkSensor(QNAPSensor): @@ -399,4 +401,6 @@ class QNAPVolumeSensor(QNAPSensor): data = self._api.data["volumes"][self.monitor_device] total_gb = int(data["total_size"]) / 1024 / 1024 / 1024 - return {ATTR_VOLUME_SIZE: "{} GB".format(round_nicely(total_gb))} + return { + ATTR_VOLUME_SIZE: "{} {}".format(round_nicely(total_gb), DATA_GIBIBYTES) + } diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index 6ea6db621fb..cc2cde26aa5 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -2,7 +2,10 @@ "domain": "qrcode", "name": "QR Code", "documentation": "https://www.home-assistant.io/integrations/qrcode", - "requirements": ["pillow==6.2.1", "pyzbar==0.1.7"], + "requirements": [ + "pillow==7.0.0", + "pyzbar==0.1.7" + ], "dependencies": [], "codeowners": [] -} +} \ No newline at end of file diff --git a/homeassistant/components/radarr/sensor.py b/homeassistant/components/radarr/sensor.py index 79e45ffd9a8..6cfdd53653d 100644 --- a/homeassistant/components/radarr/sensor.py +++ b/homeassistant/components/radarr/sensor.py @@ -14,6 +14,15 @@ from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_PORT, CONF_SSL, + DATA_BYTES, + DATA_EXABYTES, + DATA_GIGABYTES, + DATA_KILOBYTES, + DATA_MEGABYTES, + DATA_PETABYTES, + DATA_TERABYTES, + DATA_YOTTABYTES, + DATA_ZETTABYTES, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -29,12 +38,12 @@ DEFAULT_HOST = "localhost" DEFAULT_PORT = 7878 DEFAULT_URLBASE = "" DEFAULT_DAYS = "1" -DEFAULT_UNIT = "GB" +DEFAULT_UNIT = DATA_GIGABYTES SCAN_INTERVAL = timedelta(minutes=10) SENSOR_TYPES = { - "diskspace": ["Disk Space", "GB", "mdi:harddisk"], + "diskspace": ["Disk Space", DATA_GIGABYTES, "mdi:harddisk"], "upcoming": ["Upcoming", "Movies", "mdi:television"], "wanted": ["Wanted", "Movies", "mdi:television"], "movies": ["Movies", "Movies", "mdi:television"], @@ -51,7 +60,17 @@ ENDPOINTS = { } # Support to Yottabytes for the future, why not -BYTE_SIZES = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] +BYTE_SIZES = [ + DATA_BYTES, + DATA_KILOBYTES, + DATA_MEGABYTES, + DATA_GIGABYTES, + DATA_TERABYTES, + DATA_PETABYTES, + DATA_EXABYTES, + DATA_ZETTABYTES, + DATA_YOTTABYTES, +] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, diff --git a/homeassistant/components/rainforest_eagle/manifest.json b/homeassistant/components/rainforest_eagle/manifest.json index 59e4947f9bb..cb8e95df42f 100644 --- a/homeassistant/components/rainforest_eagle/manifest.json +++ b/homeassistant/components/rainforest_eagle/manifest.json @@ -2,7 +2,11 @@ "domain": "rainforest_eagle", "name": "Rainforest Eagle-200", "documentation": "https://www.home-assistant.io/integrations/rainforest_eagle", - "requirements": ["eagle200_reader==0.2.1"], + "requirements": [ + "eagle200_reader==0.2.1", + "uEagle==0.0.1" + ], "dependencies": [], - "codeowners": ["@gtdiehl"] + "codeowners": ["@gtdiehl", + "@jcalbert"] } diff --git a/homeassistant/components/rainforest_eagle/sensor.py b/homeassistant/components/rainforest_eagle/sensor.py index 315bbdff51b..99751e63f5b 100644 --- a/homeassistant/components/rainforest_eagle/sensor.py +++ b/homeassistant/components/rainforest_eagle/sensor.py @@ -4,6 +4,7 @@ import logging from eagle200_reader import EagleReader from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout +from uEagle import Eagle as LegacyReader import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -49,6 +50,25 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) +def hwtest(cloud_id, install_code, ip_address): + """Try API call 'get_network_info' to see if target device is Legacy or Eagle-200.""" + reader = LeagleReader(cloud_id, install_code, ip_address) + response = reader.get_network_info() + + # Branch to test if target is Legacy Model + if "NetworkInfo" in response: + if response["NetworkInfo"].get("ModelId", None) == "Z109-EAGLE": + return reader + + # Branch to test if target is Eagle-200 Model + if "Response" in response: + if response["Response"].get("Command", None) == "get_network_info": + return EagleReader(ip_address, cloud_id, install_code) + + # Catch-all if hardware ID tests fail + raise ValueError("Couldn't determine device model.") + + def setup_platform(hass, config, add_entities, discovery_info=None): """Create the Eagle-200 sensor.""" ip_address = config[CONF_IP_ADDRESS] @@ -56,7 +76,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): install_code = config[CONF_INSTALL_CODE] try: - eagle_reader = EagleReader(ip_address, cloud_id, install_code) + eagle_reader = hwtest(cloud_id, install_code, ip_address) except (ConnectError, HTTPError, Timeout, ValueError) as error: _LOGGER.error("Failed to connect during setup: %s", error) return @@ -138,3 +158,21 @@ class EagleData: state = self.data.get(sensor_type) _LOGGER.debug("Updating: %s - %s", sensor_type, state) return state + + +class LeagleReader(LegacyReader): + """Wraps uEagle to make it behave like eagle_reader, offering update().""" + + def update(self): + """Fetch and return the four sensor values in a dict.""" + out = {} + + resp = self.get_instantaneous_demand()["InstantaneousDemand"] + out["instantanous_demand"] = resp["Demand"] + + resp = self.get_current_summation()["CurrentSummation"] + out["summation_delivered"] = resp["SummationDelivered"] + out["summation_received"] = resp["SummationReceived"] + out["summation_total"] = out["summation_delivered"] - out["summation_received"] + + return out diff --git a/homeassistant/components/rainmachine/.translations/ca.json b/homeassistant/components/rainmachine/.translations/ca.json index 60458f1469e..494b1ecc69c 100644 --- a/homeassistant/components/rainmachine/.translations/ca.json +++ b/homeassistant/components/rainmachine/.translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Aquest controlador RainMachine ja est\u00e0 configurat." + }, "error": { "identifier_exists": "Aquest compte ja est\u00e0 registrat", "invalid_credentials": "Credencials inv\u00e0lides" diff --git a/homeassistant/components/rainmachine/.translations/da.json b/homeassistant/components/rainmachine/.translations/da.json index 34f4fff4ed0..fe53a86993d 100644 --- a/homeassistant/components/rainmachine/.translations/da.json +++ b/homeassistant/components/rainmachine/.translations/da.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Denne RainMachine-controller er allerede konfigureret." + }, "error": { "identifier_exists": "Konto er allerede registreret", "invalid_credentials": "Ugyldige legitimationsoplysninger" diff --git a/homeassistant/components/rainmachine/.translations/de.json b/homeassistant/components/rainmachine/.translations/de.json index c262fa5a652..257a0908c6a 100644 --- a/homeassistant/components/rainmachine/.translations/de.json +++ b/homeassistant/components/rainmachine/.translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Dieser RainMachine-Kontroller ist bereits konfiguriert." + }, "error": { "identifier_exists": "Konto bereits registriert", "invalid_credentials": "Ung\u00fcltige Anmeldeinformationen" diff --git a/homeassistant/components/rainmachine/.translations/en.json b/homeassistant/components/rainmachine/.translations/en.json index 54b67066f2b..4ad5bfd7c0d 100644 --- a/homeassistant/components/rainmachine/.translations/en.json +++ b/homeassistant/components/rainmachine/.translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "This RainMachine controller is already configured." + }, "error": { "identifier_exists": "Account already registered", "invalid_credentials": "Invalid credentials" diff --git a/homeassistant/components/rainmachine/.translations/ko.json b/homeassistant/components/rainmachine/.translations/ko.json index 4e2df2ca217..66d6cb0b740 100644 --- a/homeassistant/components/rainmachine/.translations/ko.json +++ b/homeassistant/components/rainmachine/.translations/ko.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\uc774 RainMachine \ucee8\ud2b8\ub864\ub7ec\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, "error": { "identifier_exists": "\uacc4\uc815\uc774 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "invalid_credentials": "\ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" diff --git a/homeassistant/components/rainmachine/.translations/no.json b/homeassistant/components/rainmachine/.translations/no.json index 5ec4e5fdc34..980c2c693ce 100644 --- a/homeassistant/components/rainmachine/.translations/no.json +++ b/homeassistant/components/rainmachine/.translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Denne RainMachine-kontrolleren er allerede konfigurert." + }, "error": { "identifier_exists": "Konto er allerede registrert", "invalid_credentials": "Ugyldig legitimasjon" diff --git a/homeassistant/components/rainmachine/.translations/pl.json b/homeassistant/components/rainmachine/.translations/pl.json index cf842efe9f6..5e813243f13 100644 --- a/homeassistant/components/rainmachine/.translations/pl.json +++ b/homeassistant/components/rainmachine/.translations/pl.json @@ -1,7 +1,10 @@ { "config": { + "abort": { + "already_configured": "Ten kontroler RainMachine jest ju\u017c skonfigurowany." + }, "error": { - "identifier_exists": "Konto jest ju\u017c zarejestrowane", + "identifier_exists": "Konto jest ju\u017c zarejestrowane.", "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce" }, "step": { diff --git a/homeassistant/components/rainmachine/.translations/ru.json b/homeassistant/components/rainmachine/.translations/ru.json index ca535663f54..e1bce5874e3 100644 --- a/homeassistant/components/rainmachine/.translations/ru.json +++ b/homeassistant/components/rainmachine/.translations/ru.json @@ -1,5 +1,8 @@ { "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": { "identifier_exists": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." diff --git a/homeassistant/components/rainmachine/.translations/zh-Hant.json b/homeassistant/components/rainmachine/.translations/zh-Hant.json index 518cc54192f..3d9663a9a79 100644 --- a/homeassistant/components/rainmachine/.translations/zh-Hant.json +++ b/homeassistant/components/rainmachine/.translations/zh-Hant.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u6b64 RainMachine \u63a7\u5236\u5668\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" + }, "error": { "identifier_exists": "\u5e33\u865f\u5df2\u8a3b\u518a", "invalid_credentials": "\u6191\u8b49\u7121\u6548" diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index ab56a5fc33b..af34d4dd9f6 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -59,14 +59,16 @@ SERVICE_PURGE_SCHEMA = vol.Schema( DEFAULT_URL = "sqlite:///{hass_config_path}" DEFAULT_DB_FILE = "home-assistant_v2.db" +DEFAULT_DB_MAX_RETRIES = 10 +DEFAULT_DB_RETRY_WAIT = 3 CONF_DB_URL = "db_url" +CONF_DB_MAX_RETRIES = "db_max_retries" +CONF_DB_RETRY_WAIT = "db_retry_wait" CONF_PURGE_KEEP_DAYS = "purge_keep_days" CONF_PURGE_INTERVAL = "purge_interval" CONF_EVENT_TYPES = "event_types" -CONNECT_RETRY_WAIT = 3 - FILTER_SCHEMA = vol.Schema( { vol.Optional(CONF_EXCLUDE, default={}): vol.Schema( @@ -96,6 +98,12 @@ CONFIG_SCHEMA = vol.Schema( vol.Coerce(int), vol.Range(min=0) ), vol.Optional(CONF_DB_URL): cv.string, + vol.Optional( + CONF_DB_MAX_RETRIES, default=DEFAULT_DB_MAX_RETRIES + ): cv.positive_int, + vol.Optional( + CONF_DB_RETRY_WAIT, default=DEFAULT_DB_RETRY_WAIT + ): cv.positive_int, } ) }, @@ -133,6 +141,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: conf = config[DOMAIN] keep_days = conf.get(CONF_PURGE_KEEP_DAYS) purge_interval = conf.get(CONF_PURGE_INTERVAL) + db_max_retries = conf[CONF_DB_MAX_RETRIES] + db_retry_wait = conf[CONF_DB_RETRY_WAIT] db_url = conf.get(CONF_DB_URL, None) if not db_url: @@ -145,6 +155,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: keep_days=keep_days, purge_interval=purge_interval, uri=db_url, + db_max_retries=db_max_retries, + db_retry_wait=db_retry_wait, include=include, exclude=exclude, ) @@ -174,6 +186,8 @@ class Recorder(threading.Thread): keep_days: int, purge_interval: int, uri: str, + db_max_retries: int, + db_retry_wait: int, include: Dict, exclude: Dict, ) -> None: @@ -186,6 +200,8 @@ class Recorder(threading.Thread): self.queue: Any = queue.Queue() self.recording_start = dt_util.utcnow() self.db_url = uri + self.db_max_retries = db_max_retries + self.db_retry_wait = db_retry_wait self.async_db_ready = asyncio.Future() self.engine: Any = None self.run_info: Any = None @@ -217,9 +233,9 @@ class Recorder(threading.Thread): tries = 1 connected = False - while not connected and tries <= 10: + while not connected and tries <= self.db_max_retries: if tries != 1: - time.sleep(CONNECT_RETRY_WAIT) + time.sleep(self.db_retry_wait) try: self._setup_connection() migration.migrate_schema(self) @@ -230,7 +246,7 @@ class Recorder(threading.Thread): _LOGGER.error( "Error during connection setup: %s (retrying in %s seconds)", err, - CONNECT_RETRY_WAIT, + self.db_retry_wait, ) tries += 1 @@ -337,9 +353,9 @@ class Recorder(threading.Thread): tries = 1 updated = False - while not updated and tries <= 10: + while not updated and tries <= self.db_max_retries: if tries != 1: - time.sleep(CONNECT_RETRY_WAIT) + time.sleep(self.db_retry_wait) try: with session_scope(session=self.get_session()) as session: try: @@ -367,7 +383,7 @@ class Recorder(threading.Thread): "Error in database connectivity: %s. " "(retrying in %s seconds)", err, - CONNECT_RETRY_WAIT, + self.db_retry_wait, ) tries += 1 diff --git a/homeassistant/components/reddit/manifest.json b/homeassistant/components/reddit/manifest.json index 1c58366f6b5..f1687d73e04 100644 --- a/homeassistant/components/reddit/manifest.json +++ b/homeassistant/components/reddit/manifest.json @@ -2,7 +2,7 @@ "domain": "reddit", "name": "Reddit", "documentation": "https://www.home-assistant.io/integrations/reddit", - "requirements": ["praw==6.5.0"], + "requirements": ["praw==6.5.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index a3feccaf10c..3f8bded6a85 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -2,6 +2,7 @@ from datetime import timedelta import functools as ft import logging +from typing import Any, Iterable import voluptuous as vol @@ -19,9 +20,10 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 ) from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.loader import bind_hass -# mypy: allow-untyped-defs, no-check-untyped-defs +# mypy: allow-untyped-calls _LOGGER = logging.getLogger(__name__) @@ -57,12 +59,12 @@ REMOTE_SERVICE_ACTIVITY_SCHEMA = make_entity_service_schema( @bind_hass -def is_on(hass, entity_id): +def is_on(hass: HomeAssistantType, entity_id: str) -> bool: """Return if the remote is on based on the statemachine.""" return hass.states.is_state(entity_id, STATE_ON) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Track states and offer events for remotes.""" component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) await component.async_setup(config) @@ -111,24 +113,26 @@ class RemoteDevice(ToggleEntity): """Representation of a remote.""" @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" return 0 - def send_command(self, command, **kwargs): - """Send a command to a device.""" + def send_command(self, command: Iterable[str], **kwargs: Any) -> None: + """Send commands to a device.""" raise NotImplementedError() - async def async_send_command(self, command, **kwargs): - """Send a command to a device.""" + async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None: + """Send commands to a device.""" + assert self.hass is not None await self.hass.async_add_executor_job( ft.partial(self.send_command, command, **kwargs) ) - def learn_command(self, **kwargs): + def learn_command(self, **kwargs: Any) -> None: """Learn a command from a device.""" raise NotImplementedError() - async def async_learn_command(self, **kwargs): + async def async_learn_command(self, **kwargs: Any) -> None: """Learn a command from a device.""" + assert self.hass is not None await self.hass.async_add_executor_job(ft.partial(self.learn_command, **kwargs)) diff --git a/homeassistant/components/rest/manifest.json b/homeassistant/components/rest/manifest.json index 8c8b7f39609..fd7eea12f7e 100644 --- a/homeassistant/components/rest/manifest.json +++ b/homeassistant/components/rest/manifest.json @@ -2,7 +2,7 @@ "domain": "rest", "name": "RESTful", "documentation": "https://www.home-assistant.io/integrations/rest", - "requirements": [], + "requirements": ["jsonpath==0.82", "xmltodict==0.12.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index 51120cb350c..70424325241 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -1,10 +1,13 @@ """Support for RESTful API sensors.""" import json import logging +from xml.parsers.expat import ExpatError +from jsonpath import jsonpath import requests from requests.auth import HTTPBasicAuth, HTTPDigestAuth import voluptuous as vol +import xmltodict from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA from homeassistant.const import ( @@ -38,7 +41,9 @@ DEFAULT_VERIFY_SSL = True DEFAULT_FORCE_UPDATE = False DEFAULT_TIMEOUT = 10 + CONF_JSON_ATTRS = "json_attributes" +CONF_JSON_ATTRS_PATH = "json_attributes_path" METHODS = ["POST", "GET"] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -57,6 +62,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_JSON_ATTRS_PATH): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, @@ -84,6 +90,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device_class = config.get(CONF_DEVICE_CLASS) value_template = config.get(CONF_VALUE_TEMPLATE) json_attrs = config.get(CONF_JSON_ATTRS) + json_attrs_path = config.get(CONF_JSON_ATTRS_PATH) force_update = config.get(CONF_FORCE_UPDATE) timeout = config.get(CONF_TIMEOUT) @@ -120,6 +127,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): json_attrs, force_update, resource_template, + json_attrs_path, ) ], True, @@ -140,6 +148,7 @@ class RestSensor(Entity): json_attrs, force_update, resource_template, + json_attrs_path, ): """Initialize the REST sensor.""" self._hass = hass @@ -153,6 +162,7 @@ class RestSensor(Entity): self._attributes = None self._force_update = force_update self._resource_template = resource_template + self._json_attrs_path = json_attrs_path @property def name(self): @@ -191,12 +201,29 @@ class RestSensor(Entity): self.rest.update() value = self.rest.data + _LOGGER.debug("Data fetched from resource: %s", value) + content_type = self.rest.headers.get("content-type") + + if content_type and content_type.startswith("text/xml"): + try: + value = json.dumps(xmltodict.parse(value)) + _LOGGER.debug("JSON converted from XML: %s", value) + except ExpatError: + _LOGGER.warning( + "REST xml result could not be parsed and converted to JSON." + ) + _LOGGER.debug("Erroneous XML: %s", value) if self._json_attrs: self._attributes = {} if value: try: json_dict = json.loads(value) + if self._json_attrs_path is not None: + json_dict = jsonpath(json_dict, self._json_attrs_path) + # jsonpath will always store the result in json_dict[0] + # so the next line happens to work exactly as needed to + # find the result if isinstance(json_dict, list): json_dict = json_dict[0] if isinstance(json_dict, dict): @@ -240,6 +267,7 @@ class RestData: self._verify_ssl = verify_ssl self._timeout = timeout self.data = None + self.headers = None def set_url(self, url): """Set url.""" @@ -259,6 +287,8 @@ class RestData: verify=self._verify_ssl, ) self.data = response.text + self.headers = response.headers except requests.exceptions.RequestException as ex: _LOGGER.error("Error fetching data: %s failed with %s", self._resource, ex) self.data = None + self.headers = None diff --git a/homeassistant/components/rflink/cover.py b/homeassistant/components/rflink/cover.py index 5db82a1d4e8..794542cb9d4 100644 --- a/homeassistant/components/rflink/cover.py +++ b/homeassistant/components/rflink/cover.py @@ -23,6 +23,8 @@ from . import ( _LOGGER = logging.getLogger(__name__) +PARALLEL_UPDATES = 0 + TYPE_STANDARD = "standard" TYPE_INVERTED = "inverted" diff --git a/homeassistant/components/rflink/light.py b/homeassistant/components/rflink/light.py index db616b92fc4..1ed19569585 100644 --- a/homeassistant/components/rflink/light.py +++ b/homeassistant/components/rflink/light.py @@ -31,6 +31,8 @@ from . import ( _LOGGER = logging.getLogger(__name__) +PARALLEL_UPDATES = 0 + TYPE_DIMMABLE = "dimmable" TYPE_SWITCHABLE = "switchable" TYPE_HYBRID = "hybrid" diff --git a/homeassistant/components/rflink/switch.py b/homeassistant/components/rflink/switch.py index 8e0ce9a0c8e..990d76101cc 100644 --- a/homeassistant/components/rflink/switch.py +++ b/homeassistant/components/rflink/switch.py @@ -22,6 +22,8 @@ from . import ( _LOGGER = logging.getLogger(__name__) +PARALLEL_UPDATES = 0 + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional( diff --git a/homeassistant/components/ring/.translations/hu.json b/homeassistant/components/ring/.translations/hu.json new file mode 100644 index 00000000000..578399c8152 --- /dev/null +++ b/homeassistant/components/ring/.translations/hu.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6zt m\u00e1r konfigur\u00e1ltuk" + }, + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s.", + "unknown": "V\u00e1ratlan hiba" + }, + "step": { + "2fa": { + "data": { + "2fa": "K\u00e9tfaktoros k\u00f3d" + }, + "title": "K\u00e9tfaktoros hiteles\u00edt\u00e9s" + }, + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "title": "Bejelentkez\u00e9s a Ring fi\u00f3kkal" + } + }, + "title": "Ring" + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/.translations/nl.json b/homeassistant/components/ring/.translations/nl.json index 1bb012bd25e..70736b15a9c 100644 --- a/homeassistant/components/ring/.translations/nl.json +++ b/homeassistant/components/ring/.translations/nl.json @@ -9,6 +9,9 @@ }, "step": { "2fa": { + "data": { + "2fa": "Twee-factor code" + }, "title": "Tweestapsverificatie" }, "user": { diff --git a/homeassistant/components/ring/.translations/pl.json b/homeassistant/components/ring/.translations/pl.json index f34903ff7d1..e592522c43b 100644 --- a/homeassistant/components/ring/.translations/pl.json +++ b/homeassistant/components/ring/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." }, "error": { "invalid_auth": "Niepoprawne uwierzytelnienie", diff --git a/homeassistant/components/ring/.translations/sl.json b/homeassistant/components/ring/.translations/sl.json new file mode 100644 index 00000000000..58e86634312 --- /dev/null +++ b/homeassistant/components/ring/.translations/sl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Naprava je \u017ee konfigurirana" + }, + "error": { + "invalid_auth": "Neveljavna avtentikacija", + "unknown": "Nepri\u010dakovana napaka" + }, + "step": { + "2fa": { + "data": { + "2fa": "Dvofaktorska koda" + }, + "title": "Dvofaktorska avtentikacija" + }, + "user": { + "data": { + "password": "Geslo", + "username": "Uporabni\u0161ko ime" + }, + "title": "Prijava s ra\u010dunom Ring" + } + }, + "title": "Ring" + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/.translations/sv.json b/homeassistant/components/ring/.translations/sv.json index 54e9f5200f2..e92790740fb 100644 --- a/homeassistant/components/ring/.translations/sv.json +++ b/homeassistant/components/ring/.translations/sv.json @@ -4,6 +4,7 @@ "already_configured": "Enheten \u00e4r redan konfigurerad" }, "error": { + "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" }, "step": { diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index 34aa9f6b0ec..0d54db5993f 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -9,12 +9,9 @@ from typing import Optional from oauthlib.oauth2 import AccessDeniedError import requests from ring_doorbell import Auth, Ring -import voluptuous as vol -from homeassistant import config_entries -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, __version__ +from homeassistant.const import __version__ from homeassistant.core import HomeAssistant, callback -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval from homeassistant.util.async_ import run_callback_threadsafe @@ -30,18 +27,6 @@ DEFAULT_ENTITY_NAMESPACE = "ring" PLATFORMS = ("binary_sensor", "light", "sensor", "switch", "camera") -CONFIG_SCHEMA = vol.Schema( - { - vol.Optional(DOMAIN): vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - async def async_setup(hass, config): """Set up the Ring component.""" @@ -56,16 +41,6 @@ async def async_setup(hass, config): await hass.async_add_executor_job(legacy_cleanup) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - "username": config[DOMAIN]["username"], - "password": config[DOMAIN]["password"], - }, - ) - ) return True diff --git a/homeassistant/components/ring/config_flow.py b/homeassistant/components/ring/config_flow.py index a25e0283753..fd9dbe0a17e 100644 --- a/homeassistant/components/ring/config_flow.py +++ b/homeassistant/components/ring/config_flow.py @@ -75,13 +75,6 @@ class RingConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="2fa", data_schema=vol.Schema({"2fa": str}), ) - async def async_step_import(self, user_input): - """Handle import.""" - if self._async_current_entries(): - return self.async_abort(reason="already_configured") - - return await self.async_step_user(user_input) - class Require2FA(exceptions.HomeAssistantError): """Error to indicate we require 2FA.""" diff --git a/homeassistant/components/rmvtransport/sensor.py b/homeassistant/components/rmvtransport/sensor.py index 8df1191a420..507e3c133cc 100644 --- a/homeassistant/components/rmvtransport/sensor.py +++ b/homeassistant/components/rmvtransport/sensor.py @@ -233,7 +233,7 @@ class RMVDepartureData: ) except RMVtransportApiConnectionError: self.departures = [] - _LOGGER.warning("Could not retrive data from rmv.de") + _LOGGER.warning("Could not retrieve data from rmv.de") return self.station = _data.get("station") _deps = [] diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index b92a95af9d7..ba67f61b2ee 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/roku", "requirements": ["roku==4.0.0"], "dependencies": [], + "after_dependencies": ["discovery"], "codeowners": [] } diff --git a/homeassistant/components/rpi_gpio_pwm/light.py b/homeassistant/components/rpi_gpio_pwm/light.py index aededbc676c..96ac3c6f2ed 100644 --- a/homeassistant/components/rpi_gpio_pwm/light.py +++ b/homeassistant/components/rpi_gpio_pwm/light.py @@ -19,7 +19,7 @@ from homeassistant.components.light import ( SUPPORT_TRANSITION, Light, ) -from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_TYPE, STATE_ON +from homeassistant.const import CONF_ADDRESS, CONF_HOST, CONF_NAME, CONF_TYPE, STATE_ON import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.color as color_util @@ -58,6 +58,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Required(CONF_TYPE): vol.In(CONF_LED_TYPES), vol.Optional(CONF_FREQUENCY): cv.positive_int, vol.Optional(CONF_ADDRESS): cv.byte, + vol.Optional(CONF_HOST): cv.string, } ], ) @@ -76,6 +77,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if CONF_FREQUENCY in led_conf: opt_args["freq"] = led_conf[CONF_FREQUENCY] if driver_type == CONF_DRIVER_GPIO: + if CONF_HOST in led_conf: + opt_args["host"] = led_conf[CONF_HOST] driver = GpioDriver(pins, **opt_args) elif driver_type == CONF_DRIVER_PCA9685: if CONF_ADDRESS in led_conf: diff --git a/homeassistant/components/rpi_gpio_pwm/manifest.json b/homeassistant/components/rpi_gpio_pwm/manifest.json index 688cad8324e..46fe96a6426 100644 --- a/homeassistant/components/rpi_gpio_pwm/manifest.json +++ b/homeassistant/components/rpi_gpio_pwm/manifest.json @@ -2,7 +2,7 @@ "domain": "rpi_gpio_pwm", "name": "pigpio Daemon PWM LED", "documentation": "https://www.home-assistant.io/integrations/rpi_gpio_pwm", - "requirements": ["pwmled==1.4.1"], + "requirements": ["pwmled==1.5.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/rtorrent/sensor.py b/homeassistant/components/rtorrent/sensor.py index 4ae272ca9bd..c6833fcfda0 100644 --- a/homeassistant/components/rtorrent/sensor.py +++ b/homeassistant/components/rtorrent/sensor.py @@ -9,6 +9,7 @@ from homeassistant.const import ( CONF_MONITORED_VARIABLES, CONF_NAME, CONF_URL, + DATA_RATE_KILOBYTES_PER_SECOND, STATE_IDLE, ) from homeassistant.exceptions import PlatformNotReady @@ -24,8 +25,8 @@ SENSOR_TYPE_UPLOAD_SPEED = "upload_speed" DEFAULT_NAME = "rtorrent" SENSOR_TYPES = { SENSOR_TYPE_CURRENT_STATUS: ["Status", None], - SENSOR_TYPE_DOWNLOAD_SPEED: ["Down Speed", "kB/s"], - SENSOR_TYPE_UPLOAD_SPEED: ["Up Speed", "kB/s"], + SENSOR_TYPE_DOWNLOAD_SPEED: ["Down Speed", DATA_RATE_KILOBYTES_PER_SECOND], + SENSOR_TYPE_UPLOAD_SPEED: ["Up Speed", DATA_RATE_KILOBYTES_PER_SECOND], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py index f436bcb8a72..b36abbedb48 100644 --- a/homeassistant/components/sabnzbd/__init__.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -14,6 +14,9 @@ from homeassistant.const import ( CONF_PORT, CONF_SENSORS, CONF_SSL, + DATA_GIGABYTES, + DATA_MEGABYTES, + DATA_RATE_MEGABYTES_PER_SECOND, ) from homeassistant.core import callback from homeassistant.helpers import discovery @@ -49,16 +52,16 @@ SIGNAL_SABNZBD_UPDATED = "sabnzbd_updated" SENSOR_TYPES = { "current_status": ["Status", None, "status"], - "speed": ["Speed", "MB/s", "kbpersec"], - "queue_size": ["Queue", "MB", "mb"], - "queue_remaining": ["Left", "MB", "mbleft"], - "disk_size": ["Disk", "GB", "diskspacetotal1"], - "disk_free": ["Disk Free", "GB", "diskspace1"], + "speed": ["Speed", DATA_RATE_MEGABYTES_PER_SECOND, "kbpersec"], + "queue_size": ["Queue", DATA_MEGABYTES, "mb"], + "queue_remaining": ["Left", DATA_MEGABYTES, "mbleft"], + "disk_size": ["Disk", DATA_GIGABYTES, "diskspacetotal1"], + "disk_free": ["Disk Free", DATA_GIGABYTES, "diskspace1"], "queue_count": ["Queue Count", None, "noofslots_total"], - "day_size": ["Daily Total", "GB", "day_size"], - "week_size": ["Weekly Total", "GB", "week_size"], - "month_size": ["Monthly Total", "GB", "month_size"], - "total_size": ["Total", "GB", "total_size"], + "day_size": ["Daily Total", DATA_GIGABYTES, "day_size"], + "week_size": ["Weekly Total", DATA_GIGABYTES, "week_size"], + "month_size": ["Monthly Total", DATA_GIGABYTES, "month_size"], + "total_size": ["Total", DATA_GIGABYTES, "total_size"], } SPEED_LIMIT_SCHEMA = vol.Schema( diff --git a/homeassistant/components/sabnzbd/manifest.json b/homeassistant/components/sabnzbd/manifest.json index 78cfd4aa1f0..6fec5c008b3 100644 --- a/homeassistant/components/sabnzbd/manifest.json +++ b/homeassistant/components/sabnzbd/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/sabnzbd", "requirements": ["pysabnzbd==1.1.0"], "dependencies": ["configurator"], + "after_dependencies": ["discovery"], "codeowners": [] } diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index 30399640088..797780d562a 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -217,7 +217,7 @@ class SAJsensor(Entity): @property def per_total_basis(self) -> bool: - """Return if the sensors value is cummulative or not.""" + """Return if the sensors value is cumulative or not.""" return self._sensor.per_total_basis @property diff --git a/homeassistant/components/salt/__init__.py b/homeassistant/components/salt/__init__.py new file mode 100644 index 00000000000..29c371ece52 --- /dev/null +++ b/homeassistant/components/salt/__init__.py @@ -0,0 +1 @@ +"""The salt component.""" diff --git a/homeassistant/components/salt/device_tracker.py b/homeassistant/components/salt/device_tracker.py new file mode 100644 index 00000000000..7c03403622a --- /dev/null +++ b/homeassistant/components/salt/device_tracker.py @@ -0,0 +1,71 @@ +"""Support for Salt Fiber Box routers.""" +import logging + +from saltbox import RouterLoginException, RouterNotReachableException, SaltBox +import voluptuous as vol + +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__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + } +) + + +def get_scanner(hass, config): + """Return the Salt device scanner.""" + scanner = SaltDeviceScanner(config[DOMAIN]) + + # Test whether the router is accessible. + data = scanner.get_salt_data() + return scanner if data is not None else None + + +class SaltDeviceScanner(DeviceScanner): + """This class queries a Salt Fiber Box router.""" + + def __init__(self, config): + """Initialize the scanner.""" + host = config[CONF_HOST] + username = config[CONF_USERNAME] + password = config[CONF_PASSWORD] + self.saltbox = SaltBox(f"http://{host}", username, password) + self.online_clients = [] + + def scan_devices(self): + """Scan for new devices and return a list with found device IDs.""" + self._update_info() + return [client["mac"] for client in self.online_clients] + + def get_device_name(self, device): + """Return the name of the given device or None if we don't know.""" + for client in self.online_clients: + if client["mac"] == device: + return client["name"] + return None + + def get_salt_data(self): + """Retrieve data from Salt router and return parsed result.""" + try: + clients = self.saltbox.get_online_clients() + return clients + except (RouterLoginException, RouterNotReachableException) as error: + _LOGGER.warning(error) + return None + + def _update_info(self): + """Pull the current information from the Salt router.""" + _LOGGER.debug("Loading data from Salt Fiber Box") + data = self.get_salt_data() + self.online_clients = data or [] diff --git a/homeassistant/components/salt/manifest.json b/homeassistant/components/salt/manifest.json new file mode 100644 index 00000000000..019fdf9ae5f --- /dev/null +++ b/homeassistant/components/salt/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "salt", + "name": "Salt Fiber Box", + "documentation": "https://www.home-assistant.io/integrations/salt", + "requirements": ["saltbox==0.1.3"], + "dependencies": [], + "codeowners": ["@bjornorri"] +} diff --git a/homeassistant/components/samsungtv/.translations/ca.json b/homeassistant/components/samsungtv/.translations/ca.json index d938373797e..7ca5879a5c0 100644 --- a/homeassistant/components/samsungtv/.translations/ca.json +++ b/homeassistant/components/samsungtv/.translations/ca.json @@ -5,8 +5,10 @@ "already_in_progress": "La configuraci\u00f3 de la Samsung TV ja est\u00e0 en curs.", "auth_missing": "Home Assistant no est\u00e0 autenticat per connectar-se amb aquesta Samsung TV.", "not_found": "No s'han trobat Samsung TV's compatibles a la xarxa.", + "not_successful": "No s'ha pogut connectar amb el dispositiu Samsung TV.", "not_supported": "Actualment aquest dispositiu Samsung TV no \u00e9s compatible." }, + "flow_title": "Samsung TV: {model}", "step": { "confirm": { "description": "Vols configurar la Samsung TV {model}? Si mai abans l'has connectat a Home Assistant haur\u00edes de veure una finestra emergent a la TV demanant autenticaci\u00f3. Les configuracuons manuals d'aquesta TV es sobreescriuran.", diff --git a/homeassistant/components/samsungtv/.translations/da.json b/homeassistant/components/samsungtv/.translations/da.json index 117069eb016..379fd5d8b6d 100644 --- a/homeassistant/components/samsungtv/.translations/da.json +++ b/homeassistant/components/samsungtv/.translations/da.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Dette Samsung-tv er allerede konfigureret.", "already_in_progress": "Samsung-tv-konfiguration er allerede i gang.", - "auth_missing": "Home Assistant er ikke godkendt til at oprette forbindelse til dette Samsung-tv.", + "auth_missing": "Home Assistant er ikke godkendt til at oprette forbindelse til dette Samsung-tv. Tjek dit tvs indstillinger for at godkende Home Assistant.", "not_found": "Der blev ikke fundet nogen underst\u00f8ttede Samsung-tv-enheder p\u00e5 netv\u00e6rket.", "not_successful": "Kan ikke oprette forbindelse til denne Samsung tv-enhed.", "not_supported": "Dette Samsung TV underst\u00f8ttes i \u00f8jeblikket ikke." diff --git a/homeassistant/components/samsungtv/.translations/de.json b/homeassistant/components/samsungtv/.translations/de.json index 60372837ffc..27b9ecc37df 100644 --- a/homeassistant/components/samsungtv/.translations/de.json +++ b/homeassistant/components/samsungtv/.translations/de.json @@ -3,10 +3,12 @@ "abort": { "already_configured": "Dieser Samsung TV ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf f\u00fcr Samsung TV wird bereits ausgef\u00fchrt.", - "auth_missing": "Home Assistant ist nicht authentifiziert, um eine Verbindung zu diesem Samsung TV herzustellen.", + "auth_missing": "Home Assistant ist nicht berechtigt, eine Verbindung zu diesem Samsung TV herzustellen. \u00dcberpr\u00fcfe die Einstellungen deines Fernsehger\u00e4ts, um Home Assistant zu autorisieren.", "not_found": "Keine unterst\u00fctzten Samsung TV-Ger\u00e4te im Netzwerk gefunden.", + "not_successful": "Es kann keine Verbindung zu diesem Samsung-Fernsehger\u00e4t hergestellt werden.", "not_supported": "Dieses Samsung TV-Ger\u00e4t wird derzeit nicht unterst\u00fctzt." }, + "flow_title": "Samsung TV: {model}", "step": { "confirm": { "description": "M\u00f6chtest du Samsung TV {model} einrichten? Wenn du noch nie eine Verbindung zum Home Assistant hergestellt hast, solltest du ein Popup-Fenster auf deinem Fernseher sehen, das nach einer Authentifizierung fragt. Manuelle Konfigurationen f\u00fcr dieses Fernsehger\u00e4t werden \u00fcberschrieben.", @@ -17,7 +19,7 @@ "host": "Host oder IP-Adresse", "name": "Name" }, - "description": "Gebe deine Samsung TV-Informationen ein. Wenn du noch nie eine Verbindung zum Home Assistant hergestellt hast, solltest du ein Popup-Fenster auf deinem Fernseher sehen, das nach einer Authentifizierung fragt.", + "description": "Gib deine Samsung TV-Informationen ein. Wenn du noch nie eine Verbindung zum Home Assistant hergestellt hast, solltest du ein Popup-Fenster auf deinem Fernseher sehen, das nach einer Authentifizierung fragt.", "title": "Samsung TV" } }, diff --git a/homeassistant/components/samsungtv/.translations/es.json b/homeassistant/components/samsungtv/.translations/es.json index 3535d4bc65f..4466b329a2a 100644 --- a/homeassistant/components/samsungtv/.translations/es.json +++ b/homeassistant/components/samsungtv/.translations/es.json @@ -5,8 +5,10 @@ "already_in_progress": "La configuraci\u00f3n del televisor Samsung ya est\u00e1 en progreso.", "auth_missing": "Home Assistant no est\u00e1 autenticado para conectarse a este televisor Samsung.", "not_found": "No se encontraron televisiones Samsung compatibles en la red.", + "not_successful": "No se puede conectar a este dispositivo Samsung TV.", "not_supported": "Esta televisi\u00f3n Samsung actualmente no es compatible." }, + "flow_title": "Televisor Samsung: {model}", "step": { "confirm": { "description": "\u00bfDesea configurar el televisor Samsung {model} ? Si nunca conect\u00f3 Home Assistant antes, deber\u00eda ver una ventana emergente en su televisor pidiendo autenticaci\u00f3n. Las configuraciones manuales para este televisor se sobrescribir\u00e1n.", diff --git a/homeassistant/components/samsungtv/.translations/fr.json b/homeassistant/components/samsungtv/.translations/fr.json index b880e41e5df..e381660a3e2 100644 --- a/homeassistant/components/samsungtv/.translations/fr.json +++ b/homeassistant/components/samsungtv/.translations/fr.json @@ -5,8 +5,10 @@ "already_in_progress": "La configuration du t\u00e9l\u00e9viseur Samsung est d\u00e9j\u00e0 en cours.", "auth_missing": "Home Assistant n'est pas authentifi\u00e9 pour se connecter \u00e0 ce t\u00e9l\u00e9viseur Samsung.", "not_found": "Aucun t\u00e9l\u00e9viseur Samsung pris en charge trouv\u00e9 sur le r\u00e9seau.", + "not_successful": "Impossible de se connecter \u00e0 cet appareil Samsung TV.", "not_supported": "Ce t\u00e9l\u00e9viseur Samsung n'est actuellement pas pris en charge." }, + "flow_title": "Samsung TV: {model}", "step": { "confirm": { "description": "Voulez vous installer la TV {model} Samsung? Si vous n'avez jamais connect\u00e9 Home Assistant avant, vous devriez voir une fen\u00eatre contextuelle sur votre t\u00e9l\u00e9viseur demandant une authentification. Les configurations manuelles de ce t\u00e9l\u00e9viseur seront \u00e9cras\u00e9es.", diff --git a/homeassistant/components/samsungtv/.translations/hu.json b/homeassistant/components/samsungtv/.translations/hu.json index 6d816ecb95a..c7a046428bc 100644 --- a/homeassistant/components/samsungtv/.translations/hu.json +++ b/homeassistant/components/samsungtv/.translations/hu.json @@ -1,14 +1,28 @@ { "config": { "abort": { + "already_configured": "Ez a Samsung TV m\u00e1r konfigur\u00e1lva van.", + "already_in_progress": "A Samsung TV konfigur\u00e1l\u00e1sa m\u00e1r folyamatban van.", + "auth_missing": "A Home Assistant nem jogosult csatlakozni ehhez a Samsung TV-hez. Ellen\u0151rizze a TV-k\u00e9sz\u00fcl\u00e9k\u00e9ben a Home Assistant enged\u00e9lyez\u00e9si be\u00e1ll\u00edt\u00e1sait.", "not_found": "A h\u00e1l\u00f3zaton nem tal\u00e1lhat\u00f3 t\u00e1mogatott Samsung TV-eszk\u00f6z.", + "not_successful": "Nem lehet csatlakozni ehhez a Samsung TV k\u00e9sz\u00fcl\u00e9khez.", "not_supported": "Ez a Samsung TV k\u00e9sz\u00fcl\u00e9k jelenleg nem t\u00e1mogatott." }, + "flow_title": "Samsung TV: {model}", "step": { "confirm": { "description": "Be\u00e1ll\u00edtja a Samsung TV {model} k\u00e9sz\u00fcl\u00e9ket? Ha soha nem csatlakozott home assistant-hez ezel\u0151tt, meg kell jelennie egy felugr\u00f3 ablaknak a TV-ben, ahol hiteles\u00edt\u00e9st k\u00e9r. A tv-k\u00e9sz\u00fcl\u00e9k manu\u00e1lis konfigur\u00e1ci\u00f3i fel\u00fcl\u00edr\u00f3dnak.", "title": "Samsung TV" + }, + "user": { + "data": { + "host": "Hosztn\u00e9v vagy IP c\u00edm", + "name": "N\u00e9v" + }, + "description": "\u00cdrja be a Samsung TV adatait. Ha soha nem csatlakoztatta a Home Assistant alkalmaz\u00e1st ezel\u0151tt, l\u00e1tnia kell a t\u00e9v\u00e9ben egy felugr\u00f3 ablakot, amely enged\u00e9lyt k\u00e9r.", + "title": "Samsung TV" } - } + }, + "title": "Samsung TV" } } \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/it.json b/homeassistant/components/samsungtv/.translations/it.json index c783db24720..3d2d4dd8e11 100644 --- a/homeassistant/components/samsungtv/.translations/it.json +++ b/homeassistant/components/samsungtv/.translations/it.json @@ -3,13 +3,15 @@ "abort": { "already_configured": "Questo Samsung TV \u00e8 gi\u00e0 configurato.", "already_in_progress": "La configurazione di Samsung TV \u00e8 gi\u00e0 in corso.", - "auth_missing": "Home Assistant non \u00e8 autenticato per connettersi a questo Samsung TV.", + "auth_missing": "Home Assistant non \u00e8 autorizzato a connettersi a questo Samsung TV. Controlla le impostazioni del tuo TV per autorizzare Home Assistant.", "not_found": "Nessun dispositivo Samsung TV supportato trovato sulla rete.", + "not_successful": "Impossibile connettersi a questo dispositivo Samsung TV.", "not_supported": "Questo dispositivo Samsung TV non \u00e8 attualmente supportato." }, + "flow_title": "Samsung TV: {model}", "step": { "confirm": { - "description": "Vuoi configurare Samsung TV {model} ? Se non hai mai collegato Home Assistant dovresti vedere un popup sul televisore in cui viene richiesta l'autenticazione. Le configurazioni manuali per questo televisore verranno sovrascritte.", + "description": "Vuoi configurare Samsung TV {model}? Se non hai mai connesso Home Assistant in precedenza, dovresti vedere un messaggio sul tuo TV in cui \u00e8 richiesta l'autorizzazione. Le configurazioni manuali per questo TV verranno sovrascritte.", "title": "Samsung TV" }, "user": { @@ -17,7 +19,7 @@ "host": "Host o indirizzo IP", "name": "Nome" }, - "description": "Inserisci le informazioni del tuo Samsung TV. Se non hai mai connesso Home Assistant dovresti vedere un popup sul televisore in cui viene richiesta l'autenticazione.", + "description": "Inserisci le informazioni del tuo Samsung TV. Se non hai mai connesso Home Assistant in precedenza, dovresti vedere un messaggio sul TV in cui \u00e8 richiesta l'autorizzazione.", "title": "Samsung TV" } }, diff --git a/homeassistant/components/samsungtv/.translations/ko.json b/homeassistant/components/samsungtv/.translations/ko.json index f7656eb9035..0226fd52dc0 100644 --- a/homeassistant/components/samsungtv/.translations/ko.json +++ b/homeassistant/components/samsungtv/.translations/ko.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\uc774 \uc0bc\uc131 TV \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "already_in_progress": "\uc0bc\uc131 TV \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589\uc911\uc785\ub2c8\ub2e4.", - "auth_missing": "Home Assistant \uac00 \ud574\ub2f9 \uc0bc\uc131 TV \uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d \uc778\uc99d\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "auth_missing": "Home Assistant \uac00 \ud574\ub2f9 \uc0bc\uc131 TV \uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc788\ub294 \uad8c\ud55c\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. TV \uc124\uc815\uc744 \ud655\uc778\ud558\uc5ec Home Assistant \ub97c \uc2b9\uc778\ud574\uc8fc\uc138\uc694.", "not_found": "\uc9c0\uc6d0\ub418\ub294 \uc0bc\uc131 TV \ubaa8\ub378\uc774 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", "not_successful": "\uc0bc\uc131 TV \uae30\uae30\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", "not_supported": "\uc774 \uc0bc\uc131 TV \ubaa8\ub378\uc740 \ud604\uc7ac \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." diff --git a/homeassistant/components/samsungtv/.translations/nl.json b/homeassistant/components/samsungtv/.translations/nl.json index 93bb5953e31..09c0bba05a3 100644 --- a/homeassistant/components/samsungtv/.translations/nl.json +++ b/homeassistant/components/samsungtv/.translations/nl.json @@ -1,12 +1,17 @@ { "config": { "abort": { - "auth_missing": "Home Assistant is niet geverifieerd om verbinding te maken met deze Samsung TV.", + "already_configured": "Deze Samsung TV is al geconfigureerd.", + "already_in_progress": "Samsung TV configuratie is al in uitvoering.", + "auth_missing": "Home Assistant is niet geautoriseerd om verbinding te maken met deze Samsung TV.", "not_found": "Geen ondersteunde Samsung TV-apparaten gevonden op het netwerk.", - "not_supported": "Deze Samsung TV-apparaten wordt momenteel niet ondersteund." + "not_successful": "Niet in staat om verbinding te maken met dit Samsung TV toestel.", + "not_supported": "Deze Samsung TV wordt momenteel niet ondersteund." }, + "flow_title": "Samsung TV: {model}", "step": { "confirm": { + "description": "Wilt u Samsung TV {model} instellen? Als u nooit eerder Home Assistant hebt verbonden dan zou u een popup op uw TV moeten zien waarin u om toestemming wordt vraagt. Handmatige configuraties voor deze TV worden overschreven", "title": "Samsung TV" }, "user": { @@ -14,6 +19,7 @@ "host": "Hostnaam of IP-adres", "name": "Naam" }, + "description": "Voer uw Samsung TV informatie in. Als u nooit eerder Home Assistant hebt verbonden dan zou u een popup op uw TV moeten zien waarin u om toestemming wordt vraagt.", "title": "Samsung TV" } }, diff --git a/homeassistant/components/samsungtv/.translations/sl.json b/homeassistant/components/samsungtv/.translations/sl.json new file mode 100644 index 00000000000..95286476ed0 --- /dev/null +++ b/homeassistant/components/samsungtv/.translations/sl.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Ta televizor Samsung je \u017ee konfiguriran.", + "already_in_progress": "Konfiguracija Samsung TV je \u017ee v teku.", + "auth_missing": "Home Assistant nima dovoljenja za povezavo s tem televizorjem Samsung. Preverite nastavitve televizorja, da ga pooblastite.", + "not_found": "V omre\u017eju ni bilo najdenih nobenih podprtih naprav Samsung TV.", + "not_successful": "Povezave s to napravo Samsung TV ni mogo\u010de vzpostaviti.", + "not_supported": "Ta naprava Samsung TV trenutno ni podprta." + }, + "flow_title": "Samsung TV: {model}", + "step": { + "confirm": { + "description": "Vnesite podatke o televizorju Samsung. \u010ce \u0161e nikoli niste povezali Home Assistant, bi morali na televizorju videli pojavno okno, ki zahteva va\u0161e dovoljenje. Ro\u010dna konfiguracija za ta TV bo prepisana.", + "title": "Samsung TV" + }, + "user": { + "data": { + "host": "Gostitelj ali IP naslov", + "name": "Ime" + }, + "description": "Vnesite podatke o televizorju Samsung. \u010ce \u0161e nikoli niste povezali Home Assistant, bi morali na televizorju videli pojavno okno, ki zahteva va\u0161e dovoljenje.", + "title": "Samsung TV" + } + }, + "title": "Samsung TV" + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/tr.json b/homeassistant/components/samsungtv/.translations/tr.json new file mode 100644 index 00000000000..3cf1f135e1f --- /dev/null +++ b/homeassistant/components/samsungtv/.translations/tr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Bu Samsung TV zaten ayarlanm\u0131\u015f.", + "already_in_progress": "Samsung TV ayar\u0131 zaten s\u00fcr\u00fcyor.", + "auth_missing": "Home Assistant'\u0131n bu Samsung TV'ye ba\u011flanma izni yok. Home Assistant'\u0131 yetkilendirmek i\u00e7in l\u00fctfen TV'nin ayarlar\u0131n\u0131 kontrol et.", + "not_found": "A\u011fda desteklenen Samsung TV cihaz\u0131 bulunamad\u0131.", + "not_successful": "Bu Samsung TV cihaz\u0131na ba\u011flan\u0131lam\u0131yor.", + "not_supported": "Bu Samsung TV cihaz\u0131 \u015fu anda desteklenmiyor." + }, + "flow_title": "Samsung TV: {model}", + "step": { + "user": { + "data": { + "host": "Host veya IP adresi", + "name": "Ad" + }, + "description": "Samsung TV bilgilerini gir. Daha \u00f6nce hi\u00e7 Home Assistant'a ba\u011flamad\u0131ysan, TV'nde izin isteyen bir pencere g\u00f6receksindir.", + "title": "Samsung TV" + } + }, + "title": "Samsung TV" + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index debe7349b6c..e52123297ab 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -5,6 +5,7 @@ from urllib.parse import urlparse from samsungctl import Remote from samsungctl.exceptions import AccessDenied, UnhandledResponse import voluptuous as vol +from websocket import WebSocketException from homeassistant import config_entries from homeassistant.components.ssdp import ( @@ -23,7 +24,7 @@ from homeassistant.const import ( ) # pylint:disable=unused-import -from .const import CONF_MANUFACTURER, CONF_MODEL, DOMAIN, LOGGER, METHODS +from .const import CONF_MANUFACTURER, CONF_MODEL, DOMAIN, LOGGER DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str, vol.Required(CONF_NAME): str}) @@ -32,6 +33,12 @@ RESULT_SUCCESS = "success" RESULT_NOT_SUCCESSFUL = "not_successful" RESULT_NOT_SUPPORTED = "not_supported" +SUPPORTED_METHODS = ( + {"method": "websocket", "timeout": 1}, + # We need this high timeout because waiting for auth popup is just an open socket + {"method": "legacy", "timeout": 31}, +) + def _get_ip(host): if host is None: @@ -76,27 +83,25 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def _try_connect(self): """Try to connect and check auth.""" - for method in METHODS: + for cfg in SUPPORTED_METHODS: config = { "name": "HomeAssistant", "description": "HomeAssistant", "id": "ha.component.samsung", "host": self._host, - "method": method, "port": self._port, - # We need this high timeout because waiting for auth popup is just an open socket - "timeout": 31, } + config.update(cfg) try: LOGGER.debug("Try config: %s", config) with Remote(config.copy()): LOGGER.debug("Working config: %s", config) - self._method = method + self._method = cfg["method"] return RESULT_SUCCESS except AccessDenied: LOGGER.debug("Working but denied config: %s", config) return RESULT_AUTH_MISSING - except UnhandledResponse: + except (UnhandledResponse, WebSocketException): LOGGER.debug("Working but unsupported config: %s", config) return RESULT_NOT_SUPPORTED except OSError as err: diff --git a/homeassistant/components/samsungtv/const.py b/homeassistant/components/samsungtv/const.py index ea893390a5b..46f6fb59a8c 100644 --- a/homeassistant/components/samsungtv/const.py +++ b/homeassistant/components/samsungtv/const.py @@ -9,5 +9,3 @@ DEFAULT_NAME = "Samsung TV" CONF_MANUFACTURER = "manufacturer" CONF_MODEL = "model" CONF_ON_ACTION = "turn_on_action" - -METHODS = ("websocket", "legacy") diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index e0800cdef27..90352bbd108 100644 --- a/homeassistant/components/scrape/manifest.json +++ b/homeassistant/components/scrape/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/scrape", "requirements": ["beautifulsoup4==4.8.2"], "dependencies": [], + "after_dependencies": ["rest"], "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 44684656372..0a7b8596248 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -9,6 +9,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_NAME, CONF_ALIAS, + CONF_ICON, EVENT_SCRIPT_STARTED, SERVICE_RELOAD, SERVICE_TOGGLE, @@ -42,7 +43,8 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}" SCRIPT_ENTRY_SCHEMA = vol.Schema( { - CONF_ALIAS: cv.string, + vol.Optional(CONF_ALIAS): cv.string, + vol.Optional(CONF_ICON): cv.icon, vol.Required(CONF_SEQUENCE): cv.SCRIPT_SCHEMA, vol.Optional(CONF_DESCRIPTION, default=""): cv.string, vol.Optional(CONF_FIELDS, default={}): { @@ -207,9 +209,15 @@ async def _async_process_config(hass, config, component): scripts = [] for object_id, cfg in config.get(DOMAIN, {}).items(): - alias = cfg.get(CONF_ALIAS, object_id) - script = ScriptEntity(hass, object_id, alias, cfg[CONF_SEQUENCE]) - scripts.append(script) + scripts.append( + ScriptEntity( + hass, + object_id, + cfg.get(CONF_ALIAS, object_id), + cfg.get(CONF_ICON), + cfg[CONF_SEQUENCE], + ) + ) hass.services.async_register( DOMAIN, object_id, service_handler, schema=SCRIPT_SERVICE_SCHEMA ) @@ -227,9 +235,12 @@ async def _async_process_config(hass, config, component): class ScriptEntity(ToggleEntity): """Representation of a script entity.""" - def __init__(self, hass, object_id, name, sequence): + icon = None + + def __init__(self, hass, object_id, name, icon, sequence): """Initialize the script.""" self.object_id = object_id + self.icon = icon self.entity_id = ENTITY_ID_FORMAT.format(object_id) self.script = Script(hass, sequence, name, self.async_update_ha_state) diff --git a/homeassistant/components/sendgrid/manifest.json b/homeassistant/components/sendgrid/manifest.json index 8a87205d4b7..900fe9252b4 100644 --- a/homeassistant/components/sendgrid/manifest.json +++ b/homeassistant/components/sendgrid/manifest.json @@ -2,7 +2,7 @@ "domain": "sendgrid", "name": "SendGrid", "documentation": "https://www.home-assistant.io/integrations/sendgrid", - "requirements": ["sendgrid==6.1.0"], + "requirements": ["sendgrid==6.1.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/sensor/.translations/sv.json b/homeassistant/components/sensor/.translations/sv.json new file mode 100644 index 00000000000..90001148f12 --- /dev/null +++ b/homeassistant/components/sensor/.translations/sv.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "Aktuell {entity_name} batteriniv\u00e5", + "is_humidity": "Aktuell {entity_name} fuktighet", + "is_illuminance": "Aktuell {entity_name} belysning", + "is_power": "Aktuell {entity_name} str\u00f6m", + "is_pressure": "Aktuellt {entity_name} tryck", + "is_signal_strength": "Aktuell {entity_name} signalstyrka", + "is_temperature": "Aktuell {entity_name} temperatur", + "is_timestamp": "Aktuell {entity_name} tidsst\u00e4mpel", + "is_value": "Aktuellt {entity_name} v\u00e4rde" + }, + "trigger_type": { + "battery_level": "{entity_name} batteriniv\u00e5 \u00e4ndras", + "humidity": "{entity_name} fuktighet \u00e4ndras", + "illuminance": "{entity_name} belysning \u00e4ndras", + "power": "{entity_name} str\u00f6mf\u00f6r\u00e4ndringar", + "pressure": "{entity_name} tryckf\u00f6r\u00e4ndringar", + "signal_strength": "{entity_name} signalstyrka \u00e4ndras", + "temperature": "{entity_name} temperaturf\u00f6r\u00e4ndringar", + "timestamp": "{entity_name} tidst\u00e4mpel \u00e4ndras", + "value": "{entity_name} v\u00e4rde \u00e4ndras" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/.translations/af.json b/homeassistant/components/sentry/.translations/af.json new file mode 100644 index 00000000000..61ef8f8d389 --- /dev/null +++ b/homeassistant/components/sentry/.translations/af.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "Sentry" + } + }, + "title": "Sentry" + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/.translations/de.json b/homeassistant/components/sentry/.translations/de.json index ea1e3f674ae..db71d8818bc 100644 --- a/homeassistant/components/sentry/.translations/de.json +++ b/homeassistant/components/sentry/.translations/de.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Gebe deine Sentry-DSN ein", + "description": "Gib deine Sentry-DSN ein", "title": "Sentry" } }, diff --git a/homeassistant/components/sentry/.translations/hu.json b/homeassistant/components/sentry/.translations/hu.json new file mode 100644 index 00000000000..64318828e6d --- /dev/null +++ b/homeassistant/components/sentry/.translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Az Sentry m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "bad_dsn": "\u00c9rv\u00e9nytelen DSN", + "unknown": "V\u00e1ratlan hiba" + }, + "step": { + "user": { + "description": "Add meg a Sentry DSN-t", + "title": "Sentry" + } + }, + "title": "Sentry" + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/.translations/nl.json b/homeassistant/components/sentry/.translations/nl.json index 7e198e836d7..67bd1ea54e2 100644 --- a/homeassistant/components/sentry/.translations/nl.json +++ b/homeassistant/components/sentry/.translations/nl.json @@ -1,7 +1,18 @@ { "config": { + "abort": { + "already_configured": "Sentry is al geconfigureerd" + }, "error": { + "bad_dsn": "Ongeldige DSN", "unknown": "Onverwachte fout" - } + }, + "step": { + "user": { + "description": "Voer uw Sentry DSN in", + "title": "Sentry" + } + }, + "title": "Sentry" } } \ No newline at end of file diff --git a/homeassistant/components/sentry/.translations/pl.json b/homeassistant/components/sentry/.translations/pl.json index 4bb7abbc328..d97fa159a87 100644 --- a/homeassistant/components/sentry/.translations/pl.json +++ b/homeassistant/components/sentry/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Sentry jest ju\u017c skonfigurowane" + "already_configured": "Sentry jest ju\u017c skonfigurowane." }, "error": { "bad_dsn": "Nieprawid\u0142owy DSN", diff --git a/homeassistant/components/sentry/.translations/sv.json b/homeassistant/components/sentry/.translations/sv.json new file mode 100644 index 00000000000..7f0968e7dbe --- /dev/null +++ b/homeassistant/components/sentry/.translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Sentry har redan konfigurerats" + }, + "error": { + "bad_dsn": "Ogiltig DSN", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "description": "Ange din Sentry DSN", + "title": "Sentry" + } + }, + "title": "Sentry" + } +} \ No newline at end of file diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index 672679b7254..eba33e75f71 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -2,7 +2,9 @@ "domain": "seven_segments", "name": "Seven Segments OCR", "documentation": "https://www.home-assistant.io/integrations/seven_segments", - "requirements": ["pillow==6.2.1"], + "requirements": [ + "pillow==7.0.0" + ], "dependencies": [], "codeowners": [] -} +} \ No newline at end of file diff --git a/homeassistant/components/sigfox/sensor.py b/homeassistant/components/sigfox/sensor.py index 27e2fe9b563..da07290f422 100644 --- a/homeassistant/components/sigfox/sensor.py +++ b/homeassistant/components/sigfox/sensor.py @@ -100,7 +100,7 @@ class SigfoxAPI: @property def auth(self): - """Return the API authentification.""" + """Return the API authentication.""" return self._auth @property diff --git a/homeassistant/components/signal_messenger/manifest.json b/homeassistant/components/signal_messenger/manifest.json index 98a7b4e59a6..3efa1c33e85 100644 --- a/homeassistant/components/signal_messenger/manifest.json +++ b/homeassistant/components/signal_messenger/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/signal_messenger", "dependencies": [], "codeowners": ["@bbernhard"], - "requirements": ["pysignalclirestapi==0.1.4"] + "requirements": ["pysignalclirestapi==0.2.4"] } diff --git a/homeassistant/components/signal_messenger/notify.py b/homeassistant/components/signal_messenger/notify.py index 8fbf9c70873..cee871fb17e 100644 --- a/homeassistant/components/signal_messenger/notify.py +++ b/homeassistant/components/signal_messenger/notify.py @@ -17,6 +17,7 @@ CONF_SENDER_NR = "number" CONF_RECP_NR = "recipients" CONF_SIGNAL_CLI_REST_API = "url" ATTR_FILENAME = "attachment" +ATTR_FILENAMES = "attachments" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -34,9 +35,7 @@ def get_service(hass, config, discovery_info=None): recp_nrs = config[CONF_RECP_NR] signal_cli_rest_api_url = config[CONF_SIGNAL_CLI_REST_API] - signal_cli_rest_api = SignalCliRestApi( - signal_cli_rest_api_url, sender_nr, api_version=1 - ) + signal_cli_rest_api = SignalCliRestApi(signal_cli_rest_api_url, sender_nr) return SignalNotificationService(recp_nrs, signal_cli_rest_api) @@ -60,12 +59,21 @@ class SignalNotificationService(BaseNotificationService): data = kwargs.get(ATTR_DATA) - filename = None - if data is not None and ATTR_FILENAME in data: - filename = data[ATTR_FILENAME] + filenames = None + if data is not None: + if ATTR_FILENAMES in data: + filenames = data[ATTR_FILENAMES] + if ATTR_FILENAME in data: + _LOGGER.warning( + "The 'attachment' option is deprecated, please replace it with 'attachments'. This option will become invalid in version 0.108." + ) + if filenames is None: + filenames = [data[ATTR_FILENAME]] + else: + filenames.append(data[ATTR_FILENAME]) try: - self._signal_cli_rest_api.send_message(message, self._recp_nrs, filename) + self._signal_cli_rest_api.send_message(message, self._recp_nrs, filenames) except SignalCliRestApiError as ex: _LOGGER.error("%s", ex) raise ex diff --git a/homeassistant/components/simplisafe/.translations/ca.json b/homeassistant/components/simplisafe/.translations/ca.json index a02c3a5e28e..a89e4c753cb 100644 --- a/homeassistant/components/simplisafe/.translations/ca.json +++ b/homeassistant/components/simplisafe/.translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Aquest compte SimpliSafe ja est\u00e0 en \u00fas." + }, "error": { "identifier_exists": "Aquest compte ja est\u00e0 registrat", "invalid_credentials": "Credencials inv\u00e0lides" diff --git a/homeassistant/components/simplisafe/.translations/da.json b/homeassistant/components/simplisafe/.translations/da.json index 0d3970eeba5..ccd82979520 100644 --- a/homeassistant/components/simplisafe/.translations/da.json +++ b/homeassistant/components/simplisafe/.translations/da.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Denne SimpliSafe-konto er allerede i brug." + }, "error": { "identifier_exists": "Konto er allerede registreret", "invalid_credentials": "Ugyldige legitimationsoplysninger" diff --git a/homeassistant/components/simplisafe/.translations/de.json b/homeassistant/components/simplisafe/.translations/de.json index ee7eaecc852..4d5eefc480b 100644 --- a/homeassistant/components/simplisafe/.translations/de.json +++ b/homeassistant/components/simplisafe/.translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Dieses SimpliSafe-Konto wird bereits verwendet." + }, "error": { "identifier_exists": "Konto bereits registriert", "invalid_credentials": "Ung\u00fcltige Anmeldeinformationen" @@ -11,7 +14,7 @@ "password": "Passwort", "username": "E-Mail-Adresse" }, - "title": "Gebe deine Informationen ein" + "title": "Gib deine Informationen ein" } }, "title": "SimpliSafe" diff --git a/homeassistant/components/simplisafe/.translations/en.json b/homeassistant/components/simplisafe/.translations/en.json index b000335af8f..7e9c26291f7 100644 --- a/homeassistant/components/simplisafe/.translations/en.json +++ b/homeassistant/components/simplisafe/.translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "This SimpliSafe account is already in use." + }, "error": { "identifier_exists": "Account already registered", "invalid_credentials": "Invalid credentials" diff --git a/homeassistant/components/simplisafe/.translations/ko.json b/homeassistant/components/simplisafe/.translations/ko.json index 5cbe233a05e..3327ddf9ab1 100644 --- a/homeassistant/components/simplisafe/.translations/ko.json +++ b/homeassistant/components/simplisafe/.translations/ko.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\uc774 SimpliSafe \uacc4\uc815\uc740 \uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4." + }, "error": { "identifier_exists": "\uacc4\uc815\uc774 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "invalid_credentials": "\ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" diff --git a/homeassistant/components/simplisafe/.translations/no.json b/homeassistant/components/simplisafe/.translations/no.json index 7c28209514e..4c25893791b 100644 --- a/homeassistant/components/simplisafe/.translations/no.json +++ b/homeassistant/components/simplisafe/.translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Denne SimpliSafe-kontoen er allerede i bruk." + }, "error": { "identifier_exists": "Konto er allerede registrert", "invalid_credentials": "Ugyldig legitimasjon" diff --git a/homeassistant/components/simplisafe/.translations/pl.json b/homeassistant/components/simplisafe/.translations/pl.json index ad8a15d06b7..3a9c160a0c5 100644 --- a/homeassistant/components/simplisafe/.translations/pl.json +++ b/homeassistant/components/simplisafe/.translations/pl.json @@ -1,7 +1,10 @@ { "config": { + "abort": { + "already_configured": "To konto SimpliSafe jest ju\u017c w u\u017cyciu." + }, "error": { - "identifier_exists": "Konto jest ju\u017c zarejestrowane", + "identifier_exists": "Konto jest ju\u017c zarejestrowane.", "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce" }, "step": { diff --git a/homeassistant/components/simplisafe/.translations/ru.json b/homeassistant/components/simplisafe/.translations/ru.json index 301eed6d1c1..2d8b63c4bab 100644 --- a/homeassistant/components/simplisafe/.translations/ru.json +++ b/homeassistant/components/simplisafe/.translations/ru.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." + }, "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\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." diff --git a/homeassistant/components/simplisafe/.translations/zh-Hant.json b/homeassistant/components/simplisafe/.translations/zh-Hant.json index bd0b2c6f3d6..b456bde33c7 100644 --- a/homeassistant/components/simplisafe/.translations/zh-Hant.json +++ b/homeassistant/components/simplisafe/.translations/zh-Hant.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u6b64 SimpliSafe \u5e33\u865f\u5df2\u88ab\u4f7f\u7528\u3002" + }, "error": { "identifier_exists": "\u5e33\u865f\u5df2\u8a3b\u518a", "invalid_credentials": "\u6191\u8b49\u7121\u6548" diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index b3d3baff16f..09004189820 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -3,12 +3,25 @@ import asyncio import logging from simplipy import API -from simplipy.errors import InvalidCredentialsError, SimplipyError -from simplipy.system.v3 import VOLUME_HIGH, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_OFF +from simplipy.errors import InvalidCredentialsError, SimplipyError, WebsocketError +from simplipy.websocket import ( + EVENT_CAMERA_MOTION_DETECTED, + EVENT_DOORBELL_DETECTED, + EVENT_ENTRY_DETECTED, + EVENT_LOCK_LOCKED, + EVENT_LOCK_UNLOCKED, + EVENT_MOTION_DETECTED, +) import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME +from homeassistant.const import ( + ATTR_CODE, + CONF_CODE, + CONF_PASSWORD, + CONF_TOKEN, + CONF_USERNAME, +) from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import ( @@ -21,36 +34,65 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.event import async_call_later, async_track_time_interval from homeassistant.helpers.service import ( async_register_admin_service, verify_domain_control, ) from .config_flow import configured_instances -from .const import DATA_CLIENT, DEFAULT_SCAN_INTERVAL, DOMAIN, TOPIC_UPDATE +from .const import ( + ATTR_ALARM_DURATION, + ATTR_ALARM_VOLUME, + ATTR_CHIME_VOLUME, + ATTR_ENTRY_DELAY_AWAY, + ATTR_ENTRY_DELAY_HOME, + ATTR_EXIT_DELAY_AWAY, + ATTR_EXIT_DELAY_HOME, + ATTR_LIGHT, + ATTR_VOICE_PROMPT_VOLUME, + DATA_CLIENT, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + VOLUMES, +) _LOGGER = logging.getLogger(__name__) CONF_ACCOUNTS = "accounts" DATA_LISTENER = "listener" +TOPIC_UPDATE = "simplisafe_update_data_{0}" -ATTR_ALARM_DURATION = "alarm_duration" -ATTR_ALARM_VOLUME = "alarm_volume" -ATTR_CHIME_VOLUME = "chime_volume" -ATTR_ENTRY_DELAY_AWAY = "entry_delay_away" -ATTR_ENTRY_DELAY_HOME = "entry_delay_home" -ATTR_EXIT_DELAY_AWAY = "exit_delay_away" -ATTR_EXIT_DELAY_HOME = "exit_delay_home" -ATTR_LIGHT = "light" +EVENT_SIMPLISAFE_EVENT = "SIMPLISAFE_EVENT" +EVENT_SIMPLISAFE_NOTIFICATION = "SIMPLISAFE_NOTIFICATION" + +DEFAULT_SOCKET_MIN_RETRY = 15 +DEFAULT_WATCHDOG_SECONDS = 5 * 60 + +WEBSOCKET_EVENTS_REQUIRING_SERIAL = [EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED] +WEBSOCKET_EVENTS_TO_TRIGGER_HASS_EVENT = [ + EVENT_CAMERA_MOTION_DETECTED, + EVENT_DOORBELL_DETECTED, + EVENT_ENTRY_DETECTED, + EVENT_MOTION_DETECTED, +] + +ATTR_CATEGORY = "category" +ATTR_LAST_EVENT_CHANGED_BY = "last_event_changed_by" +ATTR_LAST_EVENT_INFO = "last_event_info" +ATTR_LAST_EVENT_SENSOR_NAME = "last_event_sensor_name" +ATTR_LAST_EVENT_SENSOR_SERIAL = "last_event_sensor_serial" +ATTR_LAST_EVENT_SENSOR_TYPE = "last_event_sensor_type" +ATTR_LAST_EVENT_TIMESTAMP = "last_event_timestamp" +ATTR_LAST_EVENT_TYPE = "last_event_type" +ATTR_LAST_EVENT_TYPE = "last_event_type" +ATTR_MESSAGE = "message" ATTR_PIN_LABEL = "label" ATTR_PIN_LABEL_OR_VALUE = "label_or_pin" ATTR_PIN_VALUE = "pin" ATTR_SYSTEM_ID = "system_id" -ATTR_VOICE_PROMPT_VOLUME = "voice_prompt_volume" - -VOLUMES = [VOLUME_OFF, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_HIGH] +ATTR_TIMESTAMP = "timestamp" SERVICE_BASE_SCHEMA = vol.Schema({vol.Required(ATTR_SYSTEM_ID): cv.positive_int}) @@ -176,9 +218,8 @@ async def async_setup_entry(hass, config_entry): _async_save_refresh_token(hass, config_entry, api.refresh_token) - systems = await api.get_systems() - simplisafe = SimpliSafe(hass, api, systems, config_entry) - await simplisafe.async_update() + simplisafe = SimpliSafe(hass, api, config_entry) + await simplisafe.async_init() hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = simplisafe for component in ("alarm_control_panel", "lock"): @@ -186,22 +227,6 @@ async def async_setup_entry(hass, config_entry): hass.config_entries.async_forward_entry_setup(config_entry, component) ) - async def refresh(event_time): - """Refresh data from the SimpliSafe account.""" - await simplisafe.async_update() - _LOGGER.debug("Updated data for all SimpliSafe systems") - async_dispatcher_send(hass, TOPIC_UPDATE) - - hass.data[DOMAIN][DATA_LISTENER][config_entry.entry_id] = async_track_time_interval( - hass, refresh, DEFAULT_SCAN_INTERVAL - ) - - # Register the base station for each system: - for system in systems.values(): - hass.async_create_task( - async_register_base_station(hass, system, config_entry.entry_id) - ) - @callback def verify_system_exists(coro): """Log an error if a service call uses an invalid system ID.""" @@ -209,7 +234,7 @@ async def async_setup_entry(hass, config_entry): async def decorator(call): """Decorate.""" system_id = int(call.data[ATTR_SYSTEM_ID]) - if system_id not in systems: + if system_id not in simplisafe.systems: _LOGGER.error("Unknown system ID in service call: %s", system_id) return await coro(call) @@ -222,7 +247,7 @@ async def async_setup_entry(hass, config_entry): async def decorator(call): """Decorate.""" - system = systems[int(call.data[ATTR_SYSTEM_ID])] + system = simplisafe.systems[int(call.data[ATTR_SYSTEM_ID])] if system.version != 3: _LOGGER.error("Service only available on V3 systems") return @@ -234,7 +259,7 @@ async def async_setup_entry(hass, config_entry): @_verify_domain_control async def remove_pin(call): """Remove a PIN.""" - system = systems[call.data[ATTR_SYSTEM_ID]] + system = simplisafe.systems[call.data[ATTR_SYSTEM_ID]] try: await system.remove_pin(call.data[ATTR_PIN_LABEL_OR_VALUE]) except SimplipyError as err: @@ -245,7 +270,7 @@ async def async_setup_entry(hass, config_entry): @_verify_domain_control async def set_pin(call): """Set a PIN.""" - system = systems[call.data[ATTR_SYSTEM_ID]] + system = simplisafe.systems[call.data[ATTR_SYSTEM_ID]] try: await system.set_pin(call.data[ATTR_PIN_LABEL], call.data[ATTR_PIN_VALUE]) except SimplipyError as err: @@ -257,7 +282,7 @@ async def async_setup_entry(hass, config_entry): @_verify_domain_control async def set_system_properties(call): """Set one or more system parameters.""" - system = systems[call.data[ATTR_SYSTEM_ID]] + system = simplisafe.systems[call.data[ATTR_SYSTEM_ID]] try: await system.set_properties( { @@ -300,90 +325,278 @@ async def async_unload_entry(hass, entry): return True -class SimpliSafe: - """Define a SimpliSafe API object.""" +class SimpliSafeWebsocket: + """Define a SimpliSafe websocket "manager" object.""" - def __init__(self, hass, api, systems, config_entry): + def __init__(self, hass, websocket): + """Initialize.""" + self._hass = hass + self._websocket = websocket + self._websocket_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY + self._websocket_reconnect_underway = False + self._websocket_watchdog_listener = None + self.last_events = {} + + async def _async_attempt_websocket_connect(self): + """Attempt to connect to the websocket (retrying later on fail).""" + self._websocket_reconnect_underway = True + + try: + await self._websocket.async_connect() + except WebsocketError as err: + _LOGGER.error("Error with the websocket connection: %s", err) + self._websocket_reconnect_delay = min( + 2 * self._websocket_reconnect_delay, 480 + ) + async_call_later( + self._hass, + self._websocket_reconnect_delay, + self.async_websocket_connect, + ) + else: + self._websocket_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY + self._websocket_reconnect_underway = False + + async def _async_websocket_reconnect(self, event_time): + """Forcibly disconnect from and reconnect to the websocket.""" + _LOGGER.debug("Websocket watchdog expired; forcing socket reconnection") + await self.async_websocket_disconnect() + await self._async_attempt_websocket_connect() + + def _on_connect(self): + """Define a handler to fire when the websocket is connected.""" + _LOGGER.info("Connected to websocket") + _LOGGER.debug("Websocket watchdog starting") + if self._websocket_watchdog_listener is not None: + self._websocket_watchdog_listener() + self._websocket_watchdog_listener = async_call_later( + self._hass, DEFAULT_WATCHDOG_SECONDS, self._async_websocket_reconnect + ) + + @staticmethod + def _on_disconnect(): + """Define a handler to fire when the websocket is disconnected.""" + _LOGGER.info("Disconnected from websocket") + + def _on_event(self, event): + """Define a handler to fire when a new SimpliSafe event arrives.""" + _LOGGER.debug("New websocket event: %s", event) + self.last_events[event.system_id] = event + async_dispatcher_send(self._hass, TOPIC_UPDATE.format(event.system_id)) + + _LOGGER.debug("Resetting websocket watchdog") + self._websocket_watchdog_listener() + self._websocket_watchdog_listener = async_call_later( + self._hass, DEFAULT_WATCHDOG_SECONDS, self._async_websocket_reconnect + ) + self._websocket_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY + + if event.event_type not in WEBSOCKET_EVENTS_TO_TRIGGER_HASS_EVENT: + return + + if event.sensor_type: + sensor_type = event.sensor_type.name + else: + sensor_type = None + + self._hass.bus.async_fire( + EVENT_SIMPLISAFE_EVENT, + event_data={ + ATTR_LAST_EVENT_CHANGED_BY: event.changed_by, + ATTR_LAST_EVENT_TYPE: event.event_type, + ATTR_LAST_EVENT_INFO: event.info, + ATTR_LAST_EVENT_SENSOR_NAME: event.sensor_name, + ATTR_LAST_EVENT_SENSOR_SERIAL: event.sensor_serial, + ATTR_LAST_EVENT_SENSOR_TYPE: sensor_type, + ATTR_SYSTEM_ID: event.system_id, + ATTR_LAST_EVENT_TIMESTAMP: event.timestamp, + }, + ) + + async def async_websocket_connect(self): + """Register handlers and connect to the websocket.""" + if self._websocket_reconnect_underway: + return + + self._websocket.on_connect(self._on_connect) + self._websocket.on_disconnect(self._on_disconnect) + self._websocket.on_event(self._on_event) + + await self._async_attempt_websocket_connect() + + async def async_websocket_disconnect(self): + """Disconnect from the websocket.""" + await self._websocket.async_disconnect() + + +class SimpliSafe: + """Define a SimpliSafe data object.""" + + def __init__(self, hass, api, config_entry): """Initialize.""" self._api = api self._config_entry = config_entry self._emergency_refresh_token_used = False self._hass = hass - self.last_event_data = {} - self.systems = systems + self._system_notifications = {} + self.initial_event_to_use = {} + self.systems = {} + self.websocket = SimpliSafeWebsocket(hass, api.websocket) - async def _update_system(self, system): - """Update a system.""" - try: + @callback + def _async_process_new_notifications(self, system): + """Act on any new system notifications.""" + old_notifications = self._system_notifications.get(system.system_id, []) + latest_notifications = system.notifications + + # Save the latest notifications: + self._system_notifications[system.system_id] = latest_notifications + + # Process any notifications that are new: + to_add = set(latest_notifications) - set(old_notifications) + + if not to_add: + return + + _LOGGER.debug("New system notifications: %s", to_add) + + for notification in to_add: + text = notification.text + if notification.link: + text = f"{text} For more information: {notification.link}" + + self._hass.bus.async_fire( + EVENT_SIMPLISAFE_NOTIFICATION, + event_data={ + ATTR_CATEGORY: notification.category, + ATTR_CODE: notification.code, + ATTR_MESSAGE: text, + ATTR_TIMESTAMP: notification.timestamp, + }, + ) + + async def async_init(self): + """Initialize the data class.""" + asyncio.create_task(self.websocket.async_websocket_connect()) + + self.systems = await self._api.get_systems() + for system in self.systems.values(): + self._hass.async_create_task( + async_register_base_station( + self._hass, system, self._config_entry.entry_id + ) + ) + + # Future events will come from the websocket, but since subscription to the + # websocket doesn't provide the most recent event, we grab it from the REST + # API to ensure event-related attributes aren't empty on startup: + try: + self.initial_event_to_use[ + system.system_id + ] = await system.get_latest_event() + except SimplipyError as err: + _LOGGER.error("Error while fetching initial event: %s", err) + self.initial_event_to_use[system.system_id] = {} + + async def refresh(event_time): + """Refresh data from the SimpliSafe account.""" + await self.async_update() + + self._hass.data[DOMAIN][DATA_LISTENER][ + self._config_entry.entry_id + ] = async_track_time_interval(self._hass, refresh, DEFAULT_SCAN_INTERVAL) + + await self.async_update() + + async def async_update(self): + """Get updated data from SimpliSafe.""" + + async def update_system(system): + """Update a system.""" await system.update() - except InvalidCredentialsError: - # SimpliSafe's cloud is a little shaky. At times, a 500 or 502 will - # seemingly harm simplisafe-python's existing access token _and_ refresh - # token, thus preventing the integration from recovering. However, the - # refresh token stored in the config entry escapes unscathed (again, - # apparently); so, if we detect that we're in such a situation, try a last- - # ditch effort by re-authenticating with the stored token: - if self._emergency_refresh_token_used: - # If we've already tried this, log the error, suggest a HASS restart, - # and stop the time tracker: - _LOGGER.error( - "SimpliSafe authentication disconnected. Please restart HASS." + self._async_process_new_notifications(system) + _LOGGER.debug('Updated REST API data for "%s"', system.address) + async_dispatcher_send(self._hass, TOPIC_UPDATE.format(system.system_id)) + + tasks = [update_system(system) for system in self.systems.values()] + + results = await asyncio.gather(*tasks, return_exceptions=True) + for result in results: + if isinstance(result, InvalidCredentialsError): + if self._emergency_refresh_token_used: + _LOGGER.error( + "SimpliSafe authentication disconnected. Please restart HASS." + ) + remove_listener = self._hass.data[DOMAIN][DATA_LISTENER].pop( + self._config_entry.entry_id + ) + remove_listener() + return + + _LOGGER.warning("SimpliSafe cloud error; trying stored refresh token") + self._emergency_refresh_token_used = True + return await self._api.refresh_access_token( + self._config_entry.data[CONF_TOKEN] ) - remove_listener = self._hass.data[DOMAIN][DATA_LISTENER].pop( - self._config_entry.entry_id - ) - remove_listener() + + if isinstance(result, SimplipyError): + _LOGGER.error("SimpliSafe error while updating: %s", result) return - _LOGGER.warning("SimpliSafe cloud error; trying stored refresh token") - self._emergency_refresh_token_used = True - return await self._api.refresh_access_token( - self._config_entry.data[CONF_TOKEN] - ) - except SimplipyError as err: - _LOGGER.error( - 'SimpliSafe error while updating "%s": %s', system.address, err - ) - return - except Exception as err: # pylint: disable=broad-except - _LOGGER.error('Unknown error while updating "%s": %s', system.address, err) - return + if isinstance(result, SimplipyError): + _LOGGER.error("Unknown error while updating: %s", result) + return - self.last_event_data[system.system_id] = await system.get_latest_event() + if self._api.refresh_token_dirty: + # Reconnect the websocket: + await self._api.websocket.async_disconnect() + await self._api.websocket.async_connect() + + # Save the new refresh token: + _async_save_refresh_token( + self._hass, self._config_entry, self._api.refresh_token + ) # If we've reached this point using an emergency refresh token, we're in the # clear and we can discard it: if self._emergency_refresh_token_used: self._emergency_refresh_token_used = False - async def async_update(self): - """Get updated data from SimpliSafe.""" - tasks = [self._update_system(system) for system in self.systems.values()] - - await asyncio.gather(*tasks) - - if self._api.refresh_token_dirty: - _async_save_refresh_token( - self._hass, self._config_entry, self._api.refresh_token - ) - class SimpliSafeEntity(Entity): """Define a base SimpliSafe entity.""" - def __init__(self, system, name, *, serial=None): + def __init__(self, simplisafe, system, name, *, serial=None): """Initialize.""" self._async_unsub_dispatcher_connect = None - self._attrs = {ATTR_SYSTEM_ID: system.system_id} + self._last_processed_websocket_event = None self._name = name self._online = True + self._simplisafe = simplisafe self._system = system + self.websocket_events_to_listen_for = [] if serial: self._serial = serial else: self._serial = system.serial + self._attrs = { + ATTR_LAST_EVENT_INFO: simplisafe.initial_event_to_use[system.system_id].get( + "info" + ), + ATTR_LAST_EVENT_SENSOR_NAME: simplisafe.initial_event_to_use[ + system.system_id + ].get("sensorName"), + ATTR_LAST_EVENT_SENSOR_TYPE: simplisafe.initial_event_to_use[ + system.system_id + ].get("sensorType"), + ATTR_LAST_EVENT_TIMESTAMP: simplisafe.initial_event_to_use[ + system.system_id + ].get("eventTimestamp"), + ATTR_SYSTEM_ID: system.system_id, + } + @property def available(self): """Return whether the entity is available.""" @@ -420,6 +633,36 @@ class SimpliSafeEntity(Entity): """Return the unique ID of the entity.""" return self._serial + @callback + def _async_should_ignore_websocket_event(self, event): + """Return whether this entity should ignore a particular websocket event. + + Note that we can't check for a final condition – whether the event belongs to + a particular entity, like a lock – because some events (like arming the system + from a keypad _or_ from the website) should impact the same entity. + """ + # We've already processed this event: + if self._last_processed_websocket_event == event: + return True + + # This is an event for a system other than the one this entity belongs to: + if event.system_id != self._system.system_id: + return True + + # This isn't an event that this entity cares about: + if event.event_type not in self.websocket_events_to_listen_for: + return True + + # This event is targeted at a specific entity whose serial number is different + # from this one's: + if ( + event.event_type in WEBSOCKET_EVENTS_REQUIRING_SERIAL + and event.sensor_serial != self._serial + ): + return True + + return False + async def async_added_to_hass(self): """Register callbacks.""" @@ -429,9 +672,47 @@ class SimpliSafeEntity(Entity): self.async_schedule_update_ha_state(True) self._async_unsub_dispatcher_connect = async_dispatcher_connect( - self.hass, TOPIC_UPDATE, update + self.hass, TOPIC_UPDATE.format(self._system.system_id), update ) + async def async_update(self): + """Update the entity.""" + self.async_update_from_rest_api() + + last_websocket_event = self._simplisafe.websocket.last_events.get( + self._system.system_id + ) + + if self._async_should_ignore_websocket_event(last_websocket_event): + return + + self._last_processed_websocket_event = last_websocket_event + + if last_websocket_event.sensor_type: + sensor_type = last_websocket_event.sensor_type.name + else: + sensor_type = None + + self._attrs.update( + { + ATTR_LAST_EVENT_INFO: last_websocket_event.info, + ATTR_LAST_EVENT_SENSOR_NAME: last_websocket_event.sensor_name, + ATTR_LAST_EVENT_SENSOR_TYPE: sensor_type, + ATTR_LAST_EVENT_TIMESTAMP: last_websocket_event.timestamp, + } + ) + self.async_update_from_websocket_event(last_websocket_event) + + @callback + def async_update_from_rest_api(self): + """Update the entity with the provided REST API data.""" + pass + + @callback + def async_update_from_websocket_event(self, event): + """Update the entity with the provided websocket API data.""" + pass + async def async_will_remove_from_hass(self) -> None: """Disconnect dispatcher listener when removed.""" if self._async_unsub_dispatcher_connect: diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 362c0244749..c675f9c2748 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -2,9 +2,21 @@ import logging import re -from simplipy.entity import EntityTypes +from simplipy.errors import SimplipyError from simplipy.system import SystemStates -from simplipy.system.v3 import VOLUME_HIGH, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_OFF +from simplipy.websocket import ( + EVENT_ALARM_CANCELED, + EVENT_ALARM_TRIGGERED, + EVENT_ARMED_AWAY, + EVENT_ARMED_AWAY_BY_KEYPAD, + EVENT_ARMED_AWAY_BY_REMOTE, + EVENT_ARMED_HOME, + EVENT_AWAY_EXIT_DELAY_BY_KEYPAD, + EVENT_AWAY_EXIT_DELAY_BY_REMOTE, + EVENT_DISARMED_BY_MASTER_PIN, + EVENT_DISARMED_BY_REMOTE, + EVENT_HOME_EXIT_DELAY, +) from homeassistant.components.alarm_control_panel import ( FORMAT_NUMBER, @@ -23,40 +35,33 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) -from homeassistant.util.dt import utc_from_timestamp +from homeassistant.core import callback from . import SimpliSafeEntity -from .const import DATA_CLIENT, DOMAIN +from .const import ( + ATTR_ALARM_DURATION, + ATTR_ALARM_VOLUME, + ATTR_CHIME_VOLUME, + ATTR_ENTRY_DELAY_AWAY, + ATTR_ENTRY_DELAY_HOME, + ATTR_EXIT_DELAY_AWAY, + ATTR_EXIT_DELAY_HOME, + ATTR_LIGHT, + ATTR_VOICE_PROMPT_VOLUME, + DATA_CLIENT, + DOMAIN, + VOLUME_STRING_MAP, +) _LOGGER = logging.getLogger(__name__) -ATTR_ALARM_DURATION = "alarm_duration" -ATTR_ALARM_VOLUME = "alarm_volume" ATTR_BATTERY_BACKUP_POWER_LEVEL = "battery_backup_power_level" -ATTR_CHIME_VOLUME = "chime_volume" -ATTR_ENTRY_DELAY_AWAY = "entry_delay_away" -ATTR_ENTRY_DELAY_HOME = "entry_delay_home" -ATTR_EXIT_DELAY_AWAY = "exit_delay_away" -ATTR_EXIT_DELAY_HOME = "exit_delay_home" ATTR_GSM_STRENGTH = "gsm_strength" -ATTR_LAST_EVENT_INFO = "last_event_info" -ATTR_LAST_EVENT_SENSOR_NAME = "last_event_sensor_name" -ATTR_LAST_EVENT_SENSOR_TYPE = "last_event_sensor_type" -ATTR_LAST_EVENT_TIMESTAMP = "last_event_timestamp" -ATTR_LAST_EVENT_TYPE = "last_event_type" -ATTR_LIGHT = "light" +ATTR_PIN_NAME = "pin_name" ATTR_RF_JAMMING = "rf_jamming" -ATTR_VOICE_PROMPT_VOLUME = "voice_prompt_volume" ATTR_WALL_POWER_LEVEL = "wall_power_level" ATTR_WIFI_STRENGTH = "wifi_strength" -VOLUME_STRING_MAP = { - VOLUME_HIGH: "high", - VOLUME_LOW: "low", - VOLUME_MEDIUM: "medium", - VOLUME_OFF: "off", -} - async def async_setup_entry(hass, entry, async_add_entities): """Set up a SimpliSafe alarm control panel based on a config entry.""" @@ -75,33 +80,42 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel): def __init__(self, simplisafe, system, code): """Initialize the SimpliSafe alarm.""" - super().__init__(system, "Alarm Control Panel") + super().__init__(simplisafe, system, "Alarm Control Panel") self._changed_by = None self._code = code - self._simplisafe = simplisafe - self._state = None + self._last_event = None - if self._system.version == 3: - self._attrs.update( - { - ATTR_ALARM_DURATION: self._system.alarm_duration, - ATTR_ALARM_VOLUME: VOLUME_STRING_MAP[self._system.alarm_volume], - ATTR_BATTERY_BACKUP_POWER_LEVEL: self._system.battery_backup_power_level, - ATTR_CHIME_VOLUME: VOLUME_STRING_MAP[self._system.chime_volume], - ATTR_ENTRY_DELAY_AWAY: self._system.entry_delay_away, - ATTR_ENTRY_DELAY_HOME: self._system.entry_delay_home, - ATTR_EXIT_DELAY_AWAY: self._system.exit_delay_away, - ATTR_EXIT_DELAY_HOME: self._system.exit_delay_home, - ATTR_GSM_STRENGTH: self._system.gsm_strength, - ATTR_LIGHT: self._system.light, - ATTR_RF_JAMMING: self._system.rf_jamming, - ATTR_VOICE_PROMPT_VOLUME: VOLUME_STRING_MAP[ - self._system.voice_prompt_volume - ], - ATTR_WALL_POWER_LEVEL: self._system.wall_power_level, - ATTR_WIFI_STRENGTH: self._system.wifi_strength, - } - ) + if system.alarm_going_off: + self._state = STATE_ALARM_TRIGGERED + elif system.state == SystemStates.away: + self._state = STATE_ALARM_ARMED_AWAY + elif system.state in ( + SystemStates.away_count, + SystemStates.exit_delay, + SystemStates.home_count, + ): + self._state = STATE_ALARM_ARMING + elif system.state == SystemStates.home: + self._state = STATE_ALARM_ARMED_HOME + elif system.state == SystemStates.off: + self._state = STATE_ALARM_DISARMED + else: + self._state = None + + for event_type in ( + EVENT_ALARM_CANCELED, + EVENT_ALARM_TRIGGERED, + EVENT_ARMED_AWAY, + EVENT_ARMED_AWAY_BY_KEYPAD, + EVENT_ARMED_AWAY_BY_REMOTE, + EVENT_ARMED_HOME, + EVENT_AWAY_EXIT_DELAY_BY_KEYPAD, + EVENT_AWAY_EXIT_DELAY_BY_REMOTE, + EVENT_DISARMED_BY_MASTER_PIN, + EVENT_DISARMED_BY_REMOTE, + EVENT_HOME_EXIT_DELAY, + ): + self.websocket_events_to_listen_for.append(event_type) @property def changed_by(self): @@ -139,71 +153,96 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel): if not self._validate_code(code, "disarming"): return - await self._system.set_off() + try: + await self._system.set_off() + except SimplipyError as err: + _LOGGER.error('Error while disarming "%s": %s', self._system.name, err) + return + + self._state = STATE_ALARM_DISARMED async def async_alarm_arm_home(self, code=None): """Send arm home command.""" if not self._validate_code(code, "arming home"): return - await self._system.set_home() + try: + await self._system.set_home() + except SimplipyError as err: + _LOGGER.error('Error while arming "%s" (home): %s', self._system.name, err) + return + + self._state = STATE_ALARM_ARMED_HOME async def async_alarm_arm_away(self, code=None): """Send arm away command.""" if not self._validate_code(code, "arming away"): return - await self._system.set_away() + try: + await self._system.set_away() + except SimplipyError as err: + _LOGGER.error('Error while arming "%s" (away): %s', self._system.name, err) + return - async def async_update(self): - """Update alarm status.""" - last_event = self._simplisafe.last_event_data[self._system.system_id] - - if last_event.get("pinName"): - self._changed_by = last_event["pinName"] + self._state = STATE_ALARM_ARMING + @callback + def async_update_from_rest_api(self): + """Update the entity with the provided REST API data.""" if self._system.state == SystemStates.error: self._online = False return - self._online = True - if self._system.alarm_going_off: + if self._system.version == 3: + self._attrs.update( + { + ATTR_ALARM_DURATION: self._system.alarm_duration, + ATTR_ALARM_VOLUME: VOLUME_STRING_MAP[self._system.alarm_volume], + ATTR_BATTERY_BACKUP_POWER_LEVEL: self._system.battery_backup_power_level, + ATTR_CHIME_VOLUME: VOLUME_STRING_MAP[self._system.chime_volume], + ATTR_ENTRY_DELAY_AWAY: self._system.entry_delay_away, + ATTR_ENTRY_DELAY_HOME: self._system.entry_delay_home, + ATTR_EXIT_DELAY_AWAY: self._system.exit_delay_away, + ATTR_EXIT_DELAY_HOME: self._system.exit_delay_home, + ATTR_GSM_STRENGTH: self._system.gsm_strength, + ATTR_LIGHT: self._system.light, + ATTR_RF_JAMMING: self._system.rf_jamming, + ATTR_VOICE_PROMPT_VOLUME: VOLUME_STRING_MAP[ + self._system.voice_prompt_volume + ], + ATTR_WALL_POWER_LEVEL: self._system.wall_power_level, + ATTR_WIFI_STRENGTH: self._system.wifi_strength, + } + ) + + @callback + def async_update_from_websocket_event(self, event): + """Update the entity with the provided websocket API event data.""" + if event.event_type in ( + EVENT_ALARM_CANCELED, + EVENT_DISARMED_BY_MASTER_PIN, + EVENT_DISARMED_BY_REMOTE, + ): + self._state = STATE_ALARM_DISARMED + elif event.event_type == EVENT_ALARM_TRIGGERED: self._state = STATE_ALARM_TRIGGERED - elif self._system.state == SystemStates.away: + elif event.event_type in ( + EVENT_ARMED_AWAY, + EVENT_ARMED_AWAY_BY_KEYPAD, + EVENT_ARMED_AWAY_BY_REMOTE, + ): self._state = STATE_ALARM_ARMED_AWAY - elif self._system.state in ( - SystemStates.away_count, - SystemStates.exit_delay, - SystemStates.home_count, + elif event.event_type == EVENT_ARMED_HOME: + self._state = STATE_ALARM_ARMED_HOME + elif event.event_type in ( + EVENT_AWAY_EXIT_DELAY_BY_KEYPAD, + EVENT_AWAY_EXIT_DELAY_BY_REMOTE, + EVENT_HOME_EXIT_DELAY, ): self._state = STATE_ALARM_ARMING - elif self._system.state == SystemStates.home: - self._state = STATE_ALARM_ARMED_HOME - elif self._system.state == SystemStates.off: - self._state = STATE_ALARM_DISARMED else: self._state = None - try: - last_event_sensor_type = EntityTypes(last_event["sensorType"]).name - except ValueError: - _LOGGER.warning( - 'Encountered unknown entity type: %s ("%s"). Please report it at' - "https://github.com/home-assistant/home-assistant/issues.", - last_event["sensorType"], - last_event["sensorName"], - ) - last_event_sensor_type = None - - self._attrs.update( - { - ATTR_LAST_EVENT_INFO: last_event["info"], - ATTR_LAST_EVENT_SENSOR_NAME: last_event["sensorName"], - ATTR_LAST_EVENT_SENSOR_TYPE: last_event_sensor_type, - ATTR_LAST_EVENT_TIMESTAMP: utc_from_timestamp( - last_event["eventTimestamp"] - ), - ATTR_LAST_EVENT_TYPE: last_event["eventType"], - } - ) + self._changed_by = event.changed_by diff --git a/homeassistant/components/simplisafe/const.py b/homeassistant/components/simplisafe/const.py index 4dfef39de46..6ca5f8323a7 100644 --- a/homeassistant/components/simplisafe/const.py +++ b/homeassistant/components/simplisafe/const.py @@ -1,10 +1,28 @@ """Define constants for the SimpliSafe component.""" from datetime import timedelta +from simplipy.system.v3 import VOLUME_HIGH, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_OFF + DOMAIN = "simplisafe" DATA_CLIENT = "client" DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) -TOPIC_UPDATE = "update" +ATTR_ALARM_DURATION = "alarm_duration" +ATTR_ALARM_VOLUME = "alarm_volume" +ATTR_CHIME_VOLUME = "chime_volume" +ATTR_ENTRY_DELAY_AWAY = "entry_delay_away" +ATTR_ENTRY_DELAY_HOME = "entry_delay_home" +ATTR_EXIT_DELAY_AWAY = "exit_delay_away" +ATTR_EXIT_DELAY_HOME = "exit_delay_home" +ATTR_LIGHT = "light" +ATTR_VOICE_PROMPT_VOLUME = "voice_prompt_volume" + +VOLUMES = [VOLUME_OFF, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_HIGH] +VOLUME_STRING_MAP = { + VOLUME_HIGH: "high", + VOLUME_LOW: "low", + VOLUME_MEDIUM: "medium", + VOLUME_OFF: "off", +} diff --git a/homeassistant/components/simplisafe/lock.py b/homeassistant/components/simplisafe/lock.py index 10c5d310e73..58448ec4599 100644 --- a/homeassistant/components/simplisafe/lock.py +++ b/homeassistant/components/simplisafe/lock.py @@ -1,10 +1,12 @@ """Support for SimpliSafe locks.""" import logging +from simplipy.errors import SimplipyError from simplipy.lock import LockStates +from simplipy.websocket import EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED from homeassistant.components.lock import LockDevice -from homeassistant.const import STATE_LOCKED, STATE_UNKNOWN, STATE_UNLOCKED +from homeassistant.core import callback from . import SimpliSafeEntity from .const import DATA_CLIENT, DOMAIN @@ -15,19 +17,13 @@ ATTR_LOCK_LOW_BATTERY = "lock_low_battery" ATTR_JAMMED = "jammed" ATTR_PIN_PAD_LOW_BATTERY = "pin_pad_low_battery" -STATE_MAP = { - LockStates.locked: STATE_LOCKED, - LockStates.unknown: STATE_UNKNOWN, - LockStates.unlocked: STATE_UNLOCKED, -} - async def async_setup_entry(hass, entry, async_add_entities): """Set up SimpliSafe locks based on a config entry.""" simplisafe = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] async_add_entities( [ - SimpliSafeLock(system, lock) + SimpliSafeLock(simplisafe, system, lock) for system in simplisafe.systems.values() for lock in system.locks.values() ] @@ -37,32 +33,48 @@ async def async_setup_entry(hass, entry, async_add_entities): class SimpliSafeLock(SimpliSafeEntity, LockDevice): """Define a SimpliSafe lock.""" - def __init__(self, system, lock): + def __init__(self, simplisafe, system, lock): """Initialize.""" - super().__init__(system, lock.name, serial=lock.serial) + super().__init__(simplisafe, system, lock.name, serial=lock.serial) + self._is_locked = False self._lock = lock + for event_type in (EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED): + self.websocket_events_to_listen_for.append(event_type) + @property def is_locked(self): """Return true if the lock is locked.""" - return STATE_MAP.get(self._lock.state) == STATE_LOCKED + return self._is_locked async def async_lock(self, **kwargs): """Lock the lock.""" - await self._lock.lock() + try: + await self._lock.lock() + except SimplipyError as err: + _LOGGER.error('Error while locking "%s": %s', self._lock.name, err) + return + + self._is_locked = True async def async_unlock(self, **kwargs): """Unlock the lock.""" - await self._lock.unlock() + try: + await self._lock.unlock() + except SimplipyError as err: + _LOGGER.error('Error while unlocking "%s": %s', self._lock.name, err) + return - async def async_update(self): - """Update lock status.""" + self._is_locked = False + + @callback + def async_update_from_rest_api(self): + """Update the entity with the provided REST API data.""" if self._lock.offline or self._lock.disabled: self._online = False return self._online = True - self._attrs.update( { ATTR_LOCK_LOW_BATTERY: self._lock.lock_low_battery, @@ -70,3 +82,11 @@ class SimpliSafeLock(SimpliSafeEntity, LockDevice): ATTR_PIN_PAD_LOW_BATTERY: self._lock.pin_pad_low_battery, } ) + + @callback + def async_update_from_websocket_event(self, event): + """Update the entity with the provided websocket event data.""" + if event.event_type == EVENT_LOCK_LOCKED: + self._is_locked = True + else: + self._is_locked = False diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index f95db72d45a..e44f39265cb 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,7 +3,7 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==6.1.0"], + "requirements": ["simplisafe-python==8.1.1"], "dependencies": [], "codeowners": ["@bachya"] } diff --git a/homeassistant/components/smappee/__init__.py b/homeassistant/components/smappee/__init__.py index d34653e60e7..c31ab97cb95 100644 --- a/homeassistant/components/smappee/__init__.py +++ b/homeassistant/components/smappee/__init__.py @@ -199,7 +199,7 @@ class Smappee: try: return self._smappy.get_consumption(location_id, start, end, aggregation) except RequestException as error: - _LOGGER.error("Error getting comsumption from Smappee cloud. (%s)", error) + _LOGGER.error("Error getting consumption from Smappee cloud. (%s)", error) def get_sensor_consumption(self, location_id, sensor_id, aggregation, delta): """Update data from Smappee.""" @@ -221,7 +221,7 @@ class Smappee: location_id, sensor_id, start, end, aggregation ) except RequestException as error: - _LOGGER.error("Error getting comsumption from Smappee cloud. (%s)", error) + _LOGGER.error("Error getting consumption from Smappee cloud. (%s)", error) def actuator_on(self, location_id, actuator_id, is_remote_switch, duration=None): """Turn on actuator.""" diff --git a/homeassistant/components/smartthings/.translations/sv.json b/homeassistant/components/smartthings/.translations/sv.json index 6da4624fa39..725957682ad 100644 --- a/homeassistant/components/smartthings/.translations/sv.json +++ b/homeassistant/components/smartthings/.translations/sv.json @@ -13,7 +13,7 @@ "step": { "user": { "data": { - "access_token": "\u00c5tkomsttoken" + "access_token": "\u00c5tkomstnyckel" }, "description": "V\u00e4nligen ange en [personlig \u00e5tkomsttoken]({token_url}) f\u00f6r SmartThings som har skapats enligt [instruktionerna]({component_url}).", "title": "Ange personlig \u00e5tkomsttoken" diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 33f9558023d..1539fa076e4 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -146,7 +146,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): except ClientResponseError as ex: if ex.status in (401, 403): _LOGGER.exception( - "Unable to setup config entry '%s' - please reconfigure the integration", + "Unable to setup configuration entry '%s' - please reconfigure the integration", entry.title, ) remove_entry = True @@ -183,7 +183,7 @@ async def async_get_entry_scenes(entry: ConfigEntry, api): except ClientResponseError as ex: if ex.status == 403: _LOGGER.exception( - "Unable to load scenes for config entry '%s' because the access token does not have the required access", + "Unable to load scenes for configuration entry '%s' because the access token does not have the required access", entry.title, ) else: @@ -230,7 +230,7 @@ async def async_remove_entry(hass: HomeAssistantType, entry: ConfigEntry) -> Non app_count = sum(1 for entry in all_entries if entry.data[CONF_APP_ID] == app_id) if app_count > 1: _LOGGER.debug( - "App %s was not removed because it is in use by other config entries", + "App %s was not removed because it is in use by other configuration entries", app_id, ) return diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index 19a9e20cd6b..232540ee47b 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -406,7 +406,7 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateDevice): self._device.device_id, mode, ) - self._hvac_modes = modes + self._hvac_modes = list(modes) @property def current_temperature(self): diff --git a/homeassistant/components/smhi/.translations/pl.json b/homeassistant/components/smhi/.translations/pl.json index 21973cd54b6..818f27853ff 100644 --- a/homeassistant/components/smhi/.translations/pl.json +++ b/homeassistant/components/smhi/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Nazwa ju\u017c istnieje", + "name_exists": "Nazwa ju\u017c istnieje.", "wrong_location": "Lokalizacja w Szwecji" }, "step": { diff --git a/homeassistant/components/socialblade/manifest.json b/homeassistant/components/socialblade/manifest.json index 2ce7fbabf0f..540febe7f2e 100644 --- a/homeassistant/components/socialblade/manifest.json +++ b/homeassistant/components/socialblade/manifest.json @@ -2,7 +2,7 @@ "domain": "socialblade", "name": "Social Blade", "documentation": "https://www.home-assistant.io/integrations/socialblade", - "requirements": ["socialbladeclient==0.2"], + "requirements": ["socialbladeclient==0.5"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/solaredge/.translations/hu.json b/homeassistant/components/solaredge/.translations/hu.json new file mode 100644 index 00000000000..ae8f51983ea --- /dev/null +++ b/homeassistant/components/solaredge/.translations/hu.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Ennek az install\u00e1ci\u00f3nak a neve" + }, + "title": "Az API param\u00e9terek megad\u00e1sa ehhez a telep\u00edt\u00e9shez" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/pl.json b/homeassistant/components/solaredge/.translations/pl.json index 376a81219b0..5e80c1563f4 100644 --- a/homeassistant/components/solaredge/.translations/pl.json +++ b/homeassistant/components/solaredge/.translations/pl.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "site_exists": "Ten site_id jest ju\u017c skonfigurowany" + "site_exists": "To site_id jest ju\u017c skonfigurowane." }, "error": { - "site_exists": "Ten site_id jest ju\u017c skonfigurowany" + "site_exists": "To site_id jest ju\u017c skonfigurowane." }, "step": { "user": { diff --git a/homeassistant/components/solaredge/.translations/sv.json b/homeassistant/components/solaredge/.translations/sv.json new file mode 100644 index 00000000000..25bb0f325a1 --- /dev/null +++ b/homeassistant/components/solaredge/.translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "site_exists": "Denna site_id \u00e4r redan konfigurerad" + }, + "error": { + "site_exists": "Denna site_id \u00e4r redan konfigurerad" + }, + "step": { + "user": { + "data": { + "api_key": "API-nyckeln f\u00f6r den h\u00e4r webbplatsen", + "name": "Namnet p\u00e5 den h\u00e4r installationen", + "site_id": "SolarEdge webbplats-id" + }, + "title": "Definiera API-parametrarna f\u00f6r den h\u00e4r installationen" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/config_flow.py b/homeassistant/components/solaredge/config_flow.py index 7c8c9380522..62bf99ab383 100644 --- a/homeassistant/components/solaredge/config_flow.py +++ b/homeassistant/components/solaredge/config_flow.py @@ -54,7 +54,7 @@ class SolarEdgeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return True async def async_step_user(self, user_input=None): - """Step when user intializes a integration.""" + """Step when user initializes a integration.""" self._errors = {} if user_input is not None: name = slugify(user_input.get(CONF_NAME, DEFAULT_NAME)) diff --git a/homeassistant/components/solarlog/.translations/hu.json b/homeassistant/components/solarlog/.translations/hu.json new file mode 100644 index 00000000000..e52cebefda6 --- /dev/null +++ b/homeassistant/components/solarlog/.translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6zt m\u00e1r konfigur\u00e1ltuk" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/pl.json b/homeassistant/components/solarlog/.translations/pl.json index 251d183b361..fdbf21feb92 100644 --- a/homeassistant/components/solarlog/.translations/pl.json +++ b/homeassistant/components/solarlog/.translations/pl.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." }, "error": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, sprawd\u017a adres hosta" }, "step": { diff --git a/homeassistant/components/solarlog/.translations/ru.json b/homeassistant/components/solarlog/.translations/ru.json index b64496c4591..3333d5c0d5f 100644 --- a/homeassistant/components/solarlog/.translations/ru.json +++ b/homeassistant/components/solarlog/.translations/ru.json @@ -11,7 +11,7 @@ "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" + "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 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432 Solar-Log" }, "title": "Solar-Log" } diff --git a/homeassistant/components/solarlog/.translations/sv.json b/homeassistant/components/solarlog/.translations/sv.json new file mode 100644 index 00000000000..981bd9fb167 --- /dev/null +++ b/homeassistant/components/solarlog/.translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta, kontrollera v\u00e4rdadressen" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rdnamnet eller ip-adressen f\u00f6r din Solar-Log-enhet", + "name": "Prefixet som ska anv\u00e4ndas f\u00f6r dina Solar-Log sensorer" + }, + "title": "Definiera din Solar-Log-anslutning" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/config_flow.py b/homeassistant/components/solarlog/config_flow.py index 5cb2d5deec1..111155b27b6 100644 --- a/homeassistant/components/solarlog/config_flow.py +++ b/homeassistant/components/solarlog/config_flow.py @@ -54,7 +54,7 @@ class SolarLogConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return False async def async_step_user(self, user_input=None): - """Step when user intializes a integration.""" + """Step when user initializes a integration.""" self._errors = {} if user_input is not None: # set some defaults in case we need to return to the form diff --git a/homeassistant/components/soma/.translations/hu.json b/homeassistant/components/soma/.translations/hu.json new file mode 100644 index 00000000000..797cfa1b2d8 --- /dev/null +++ b/homeassistant/components/soma/.translations/hu.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "connection_error": "Nem siker\u00fclt csatlakozni a SOMA Connecthez.", + "missing_configuration": "A Soma \u00f6sszetev\u0151 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t.", + "result_error": "A SOMA Connect hiba\u00e1llapottal v\u00e1laszolt." + }, + "create_entry": { + "default": "Soma sikeresen hiteles\u00edtett." + }, + "step": { + "user": { + "data": { + "host": "Kiszolg\u00e1l\u00f3", + "port": "Port" + }, + "description": "K\u00e9rj\u00fck, adja meg a SOMA Connect csatlakoz\u00e1si be\u00e1ll\u00edt\u00e1sait.", + "title": "SOMA csatlakoz\u00e1s" + } + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/sv.json b/homeassistant/components/soma/.translations/sv.json new file mode 100644 index 00000000000..bb3ce895fd5 --- /dev/null +++ b/homeassistant/components/soma/.translations/sv.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan bara konfigurera ett Soma-konto.", + "authorize_url_timeout": "Timeout vid generering av auktoriserings-url.", + "connection_error": "Det gick inte att ansluta till SOMA Connect.", + "missing_configuration": "Soma-komponenten \u00e4r inte konfigurerad. F\u00f6lj dokumentationen.", + "result_error": "SOMA Connect svarade med felstatus." + }, + "create_entry": { + "default": "Lyckad autentisering med Soma." + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "port": "Port" + }, + "description": "Ange anslutningsinst\u00e4llningar f\u00f6r din SOMA Connect.", + "title": "SOMA Connect" + } + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/hu.json b/homeassistant/components/somfy/.translations/hu.json new file mode 100644 index 00000000000..3df2fb30477 --- /dev/null +++ b/homeassistant/components/somfy/.translations/hu.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "pick_implementation": { + "title": "V\u00e1lassza ki a hiteles\u00edt\u00e9si m\u00f3dszert" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/sv.json b/homeassistant/components/somfy/.translations/sv.json index 390cd1f4d80..982b32a90a1 100644 --- a/homeassistant/components/somfy/.translations/sv.json +++ b/homeassistant/components/somfy/.translations/sv.json @@ -8,6 +8,11 @@ "create_entry": { "default": "Lyckad autentisering med Somfy." }, + "step": { + "pick_implementation": { + "title": "V\u00e4lj autentiseringsmetod" + } + }, "title": "Somfy" } } \ No newline at end of file diff --git a/homeassistant/components/sonarr/sensor.py b/homeassistant/components/sonarr/sensor.py index 82bcdad6ef4..c0781b37603 100644 --- a/homeassistant/components/sonarr/sensor.py +++ b/homeassistant/components/sonarr/sensor.py @@ -14,6 +14,15 @@ from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_PORT, CONF_SSL, + DATA_BYTES, + DATA_EXABYTES, + DATA_GIGABYTES, + DATA_KILOBYTES, + DATA_MEGABYTES, + DATA_PETABYTES, + DATA_TERABYTES, + DATA_YOTTABYTES, + DATA_ZETTABYTES, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -29,10 +38,10 @@ DEFAULT_HOST = "localhost" DEFAULT_PORT = 8989 DEFAULT_URLBASE = "" DEFAULT_DAYS = "1" -DEFAULT_UNIT = "GB" +DEFAULT_UNIT = DATA_GIGABYTES SENSOR_TYPES = { - "diskspace": ["Disk Space", "GB", "mdi:harddisk"], + "diskspace": ["Disk Space", DATA_GIGABYTES, "mdi:harddisk"], "queue": ["Queue", "Episodes", "mdi:download"], "upcoming": ["Upcoming", "Episodes", "mdi:television"], "wanted": ["Wanted", "Episodes", "mdi:television"], @@ -52,7 +61,17 @@ ENDPOINTS = { } # Support to Yottabytes for the future, why not -BYTE_SIZES = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] +BYTE_SIZES = [ + DATA_BYTES, + DATA_KILOBYTES, + DATA_MEGABYTES, + DATA_GIGABYTES, + DATA_TERABYTES, + DATA_PETABYTES, + DATA_EXABYTES, + DATA_ZETTABYTES, + DATA_YOTTABYTES, +] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, diff --git a/homeassistant/components/sonos/.translations/zh-Hans.json b/homeassistant/components/sonos/.translations/zh-Hans.json index 17c1e78d3e8..de2609f4a71 100644 --- a/homeassistant/components/sonos/.translations/zh-Hans.json +++ b/homeassistant/components/sonos/.translations/zh-Hans.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u6ca1\u6709\u5728\u7f51\u7edc\u4e0a\u627e\u5230 Sonos \u8bbe\u5907\u3002", - "single_instance_allowed": "\u53ea\u6709\u4e00\u6b21 Sonos \u914d\u7f6e\u662f\u5fc5\u8981\u7684\u3002" + "single_instance_allowed": "\u53ea\u9700\u8bbe\u7f6e\u4e00\u6b21 Sonos \u5373\u53ef\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 97d03e2116e..37b479a90b1 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -107,7 +107,7 @@ class SonosData: async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Sonos platform. Obsolete.""" _LOGGER.error( - "Loading Sonos by media_player platform config is no longer supported" + "Loading Sonos by media_player platform configuration is no longer supported" ) @@ -762,6 +762,7 @@ class SonosEntity(MediaPlayerDevice): return await self.hass.async_add_executor_job(_get_soco_group) + @callback def _async_regroup(group): """Rebuild internal group layout.""" sonos_group = [] diff --git a/homeassistant/components/soundtouch/media_player.py b/homeassistant/components/soundtouch/media_player.py index 72677995a9d..71592e92c17 100644 --- a/homeassistant/components/soundtouch/media_player.py +++ b/homeassistant/components/soundtouch/media_player.py @@ -241,57 +241,46 @@ class SoundTouchDevice(MediaPlayerDevice): def turn_off(self): """Turn off media player.""" self._device.power_off() - self._status = self._device.status() def turn_on(self): """Turn on media player.""" self._device.power_on() - self._status = self._device.status() def volume_up(self): """Volume up the media player.""" self._device.volume_up() - self._volume = self._device.volume() def volume_down(self): """Volume down media player.""" self._device.volume_down() - self._volume = self._device.volume() def set_volume_level(self, volume): """Set volume level, range 0..1.""" self._device.set_volume(int(volume * 100)) - self._volume = self._device.volume() def mute_volume(self, mute): """Send mute command.""" self._device.mute() - self._volume = self._device.volume() def media_play_pause(self): """Simulate play pause media player.""" self._device.play_pause() - self._status = self._device.status() def media_play(self): """Send play command.""" self._device.play() - self._status = self._device.status() def media_pause(self): """Send media pause command to media player.""" self._device.pause() - self._status = self._device.status() def media_next_track(self): """Send next track command.""" self._device.next_track() - self._status = self._device.status() def media_previous_track(self): """Send the previous track command.""" self._device.previous_track() - self._status = self._device.status() @property def media_image_url(self): diff --git a/homeassistant/components/spc/binary_sensor.py b/homeassistant/components/spc/binary_sensor.py index b5ff14ce01d..34689c4dccf 100644 --- a/homeassistant/components/spc/binary_sensor.py +++ b/homeassistant/components/spc/binary_sensor.py @@ -19,6 +19,7 @@ def _get_device_class(zone_type): ZoneType.ALARM: "motion", ZoneType.ENTRY_EXIT: "opening", ZoneType.FIRE: "smoke", + ZoneType.TECHNICAL: "power", }.get(zone_type) diff --git a/homeassistant/components/speedtestdotnet/const.py b/homeassistant/components/speedtestdotnet/const.py index 69aadb7ac6c..a08c9421c76 100644 --- a/homeassistant/components/speedtestdotnet/const.py +++ b/homeassistant/components/speedtestdotnet/const.py @@ -1,10 +1,12 @@ """Consts used by Speedtest.net.""" +from homeassistant.const import DATA_RATE_MEGABITS_PER_SECOND + DOMAIN = "speedtestdotnet" DATA_UPDATED = f"{DOMAIN}_data_updated" SENSOR_TYPES = { "ping": ["Ping", "ms"], - "download": ["Download", "Mbit/s"], - "upload": ["Upload", "Mbit/s"], + "download": ["Download", DATA_RATE_MEGABITS_PER_SECOND], + "upload": ["Upload", DATA_RATE_MEGABITS_PER_SECOND], } diff --git a/homeassistant/components/spotify/.translations/nl.json b/homeassistant/components/spotify/.translations/nl.json new file mode 100644 index 00000000000..abe59854044 --- /dev/null +++ b/homeassistant/components/spotify/.translations/nl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "U kunt slechts \u00e9\u00e9n Spotify-account configureren.", + "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", + "missing_configuration": "De Spotify integratie is niet geconfigureerd. Gelieve de documentatie te volgen." + }, + "create_entry": { + "default": "Succesvol geauthenticeerd met Spotify." + }, + "step": { + "pick_implementation": { + "title": "Kies Authenticatiemethode" + } + }, + "title": "Spotify" + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/.translations/sl.json b/homeassistant/components/spotify/.translations/sl.json new file mode 100644 index 00000000000..6ab0b0a40a6 --- /dev/null +++ b/homeassistant/components/spotify/.translations/sl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "Konfigurirate lahko samo en ra\u010dun Spotify.", + "authorize_url_timeout": "\u010casovna omejitev za generiranje potrditvenega URL-ja je potekla.", + "missing_configuration": "Integracija Spotify ni konfigurirana. Prosimo, upo\u0161tevajte dokumentacijo." + }, + "create_entry": { + "default": "Uspe\u0161no overjena s Spotify." + }, + "step": { + "pick_implementation": { + "title": "Izberite na\u010din preverjanja pristnosti" + } + }, + "title": "Spotify" + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/.translations/tr.json b/homeassistant/components/spotify/.translations/tr.json new file mode 100644 index 00000000000..88755b800f4 --- /dev/null +++ b/homeassistant/components/spotify/.translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "Yaln\u0131zca bir Spotify hesab\u0131 ayarlayabilirsin.", + "authorize_url_timeout": "Kimlik do\u011frulama URL'sini olu\u015ftururken zaman a\u015f\u0131m\u0131 ger\u00e7ekle\u015fti.", + "missing_configuration": "Spotify entegrasyonu ayarlanmam\u0131\u015f. L\u00fctfen dok\u00fcmentasyonu takip et." + }, + "create_entry": { + "default": "Spotify ile kimlik ba\u015far\u0131yla do\u011fruland\u0131." + }, + "step": { + "pick_implementation": { + "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" + } + }, + "title": "Spotify" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/hu.json b/homeassistant/components/starline/.translations/hu.json index c45d9ac871e..ccc5b7983d0 100644 --- a/homeassistant/components/starline/.translations/hu.json +++ b/homeassistant/components/starline/.translations/hu.json @@ -8,9 +8,35 @@ "step": { "auth_app": { "data": { - "app_id": "App ID" - } + "app_id": "App ID", + "app_secret": "Titok" + }, + "description": "Alkalmaz\u00e1s azonos\u00edt\u00f3ja \u00e9s titkos k\u00f3dja a StarLine fejleszt\u0151i fi\u00f3kb\u00f3l ", + "title": "Alkalmaz\u00e1si hiteles\u00edt\u0151 adatok" + }, + "auth_captcha": { + "data": { + "captcha_code": "K\u00f3d a k\u00e9pr\u0151l" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "SMS k\u00f3d" + }, + "description": "Adja meg a {phone_number} telefonra k\u00fcld\u00f6tt k\u00f3dot.", + "title": "K\u00e9tfaktoros hiteles\u00edt\u00e9s" + }, + "auth_user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "A StarLine fi\u00f3k e-mail c\u00edme \u00e9s jelszava", + "title": "Felhaszn\u00e1l\u00f3i hiteles\u00edt\u0151 adatok" } - } + }, + "title": "Starline" } } \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/sv.json b/homeassistant/components/starline/.translations/sv.json index 42d01b56753..83f2300892d 100644 --- a/homeassistant/components/starline/.translations/sv.json +++ b/homeassistant/components/starline/.translations/sv.json @@ -1,11 +1,42 @@ { "config": { + "error": { + "error_auth_app": "Fel applikations-id eller hemlighet", + "error_auth_mfa": "Felaktig kod", + "error_auth_user": "Felaktigt anv\u00e4ndarnamn eller l\u00f6senord" + }, "step": { "auth_app": { "data": { + "app_id": "App-ID", "app_secret": "Hemlighet" - } + }, + "description": "Applikations-ID och hemlig kod fr\u00e5n StarLine-utvecklarkonto", + "title": "Autentiseringsuppgifter f\u00f6r applikation" + }, + "auth_captcha": { + "data": { + "captcha_code": "Kod fr\u00e5n bild" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "SMS-kod" + }, + "description": "Ange koden som skickas till telefonen {phone_number}", + "title": "Tv\u00e5faktorautentisering" + }, + "auth_user": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + }, + "description": "StarLine-kontots e-postadress och l\u00f6senord", + "title": "Anv\u00e4ndaruppgifter" } - } + }, + "title": "StarLine" } } \ No newline at end of file diff --git a/homeassistant/components/starline/config_flow.py b/homeassistant/components/starline/config_flow.py index fa559f62913..34415e9dca4 100644 --- a/homeassistant/components/starline/config_flow.py +++ b/homeassistant/components/starline/config_flow.py @@ -4,7 +4,7 @@ from typing import Optional from starline import StarlineAuth import voluptuous as vol -from homeassistant import config_entries +from homeassistant import config_entries, core from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from .const import ( # pylint: disable=unused-import @@ -85,6 +85,7 @@ class StarlineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self._async_authenticate_user(error) return self._async_form_auth_captcha(error) + @core.callback def _async_form_auth_app(self, error=None): """Authenticate application form.""" errors = {} @@ -106,6 +107,7 @@ class StarlineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) + @core.callback def _async_form_auth_user(self, error=None): """Authenticate user form.""" errors = {} @@ -127,6 +129,7 @@ class StarlineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) + @core.callback def _async_form_auth_mfa(self, error=None): """Authenticate mfa form.""" errors = {} @@ -146,6 +149,7 @@ class StarlineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): description_placeholders={"phone_number": self._phone_number}, ) + @core.callback def _async_form_auth_captcha(self, error=None): """Captcha verification form.""" errors = {} diff --git a/homeassistant/components/startca/sensor.py b/homeassistant/components/startca/sensor.py index e07f21e5d60..82106c2da57 100644 --- a/homeassistant/components/startca/sensor.py +++ b/homeassistant/components/startca/sensor.py @@ -8,7 +8,12 @@ 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 +from homeassistant.const import ( + CONF_API_KEY, + CONF_MONITORED_VARIABLES, + CONF_NAME, + DATA_GIGABYTES, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -19,7 +24,6 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Start.ca" CONF_TOTAL_BANDWIDTH = "total_bandwidth" -GIGABYTES = "GB" PERCENT = "%" MIN_TIME_BETWEEN_UPDATES = timedelta(hours=1) @@ -27,17 +31,17 @@ REQUEST_TIMEOUT = 5 # seconds SENSOR_TYPES = { "usage": ["Usage Ratio", PERCENT, "mdi:percent"], - "usage_gb": ["Usage", GIGABYTES, "mdi:download"], - "limit": ["Data limit", GIGABYTES, "mdi:download"], - "used_download": ["Used Download", GIGABYTES, "mdi:download"], - "used_upload": ["Used Upload", GIGABYTES, "mdi:upload"], - "used_total": ["Used Total", GIGABYTES, "mdi:download"], - "grace_download": ["Grace Download", GIGABYTES, "mdi:download"], - "grace_upload": ["Grace Upload", GIGABYTES, "mdi:upload"], - "grace_total": ["Grace Total", GIGABYTES, "mdi:download"], - "total_download": ["Total Download", GIGABYTES, "mdi:download"], - "total_upload": ["Total Upload", GIGABYTES, "mdi:download"], - "used_remaining": ["Remaining", GIGABYTES, "mdi:download"], + "usage_gb": ["Usage", DATA_GIGABYTES, "mdi:download"], + "limit": ["Data limit", DATA_GIGABYTES, "mdi:download"], + "used_download": ["Used Download", DATA_GIGABYTES, "mdi:download"], + "used_upload": ["Used Upload", DATA_GIGABYTES, "mdi:upload"], + "used_total": ["Used Total", DATA_GIGABYTES, "mdi:download"], + "grace_download": ["Grace Download", DATA_GIGABYTES, "mdi:download"], + "grace_upload": ["Grace Upload", DATA_GIGABYTES, "mdi:upload"], + "grace_total": ["Grace Total", DATA_GIGABYTES, "mdi:download"], + "total_download": ["Total Download", DATA_GIGABYTES, "mdi:download"], + "total_upload": ["Total Upload", DATA_GIGABYTES, "mdi:download"], + "used_remaining": ["Remaining", DATA_GIGABYTES, "mdi:download"], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 865fda93a3e..d85b6b079ae 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -294,6 +294,7 @@ class StatisticsSensor(Entity): """Timer callback for sensor update.""" _LOGGER.debug("%s: executing scheduled update", self.entity_id) self.async_schedule_update_ha_state(True) + self._update_listener = None self._update_listener = async_track_point_in_utc_time( self.hass, _scheduled_update, next_to_purge_timestamp diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 99ffd833eb3..6cd07c7f926 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -95,7 +95,7 @@ def stream_worker(hass, stream, quit_event): if packet.is_keyframe: # Calculate the segment duration by multiplying the presentation # timestamp by the time base, which gets us total seconds. - # By then dividing by the seqence, we can calculate how long + # By then dividing by the sequence, we can calculate how long # each segment is, assuming the stream starts from 0. segment_duration = (packet.pts * packet.time_base) / sequence # Save segment to outputs diff --git a/homeassistant/components/surepetcare/__init__.py b/homeassistant/components/surepetcare/__init__.py index 450d7eb9a15..a22ba4a1335 100644 --- a/homeassistant/components/surepetcare/__init__.py +++ b/homeassistant/components/surepetcare/__init__.py @@ -1,17 +1,17 @@ """Support for Sure Petcare cat/pet flaps.""" import logging +from typing import Any, Dict, List from surepy import ( SurePetcare, SurePetcareAuthenticationError, SurePetcareError, - SureThingID, + SureProductID, ) import voluptuous as vol from homeassistant.const import ( CONF_ID, - CONF_NAME, CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_TYPE, @@ -23,9 +23,11 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval from .const import ( + CONF_FEEDERS, CONF_FLAPS, - CONF_HOUSEHOLD_ID, + CONF_PARENT, CONF_PETS, + CONF_PRODUCT_ID, DATA_SURE_PETCARE, DEFAULT_SCAN_INTERVAL, DOMAIN, @@ -36,23 +38,19 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -FLAP_SCHEMA = vol.Schema( - {vol.Required(CONF_ID): cv.positive_int, vol.Required(CONF_NAME): cv.string} -) - -PET_SCHEMA = vol.Schema( - {vol.Required(CONF_ID): cv.positive_int, vol.Required(CONF_NAME): cv.string} -) - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_HOUSEHOLD_ID): cv.positive_int, - vol.Required(CONF_FLAPS): vol.All(cv.ensure_list, [FLAP_SCHEMA]), - vol.Required(CONF_PETS): vol.All(cv.ensure_list, [PET_SCHEMA]), + vol.Optional(CONF_FEEDERS, default=[]): vol.All( + cv.ensure_list, [cv.positive_int] + ), + vol.Optional(CONF_FLAPS, default=[]): vol.All( + cv.ensure_list, [cv.positive_int] + ), + vol.Optional(CONF_PETS): vol.All(cv.ensure_list, [cv.positive_int]), vol.Optional( CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL ): cv.time_period, @@ -63,7 +61,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass, config): +async def async_setup(hass, config) -> bool: """Initialize the Sure Petcare component.""" conf = config[DOMAIN] @@ -78,11 +76,10 @@ async def async_setup(hass, config): surepy = SurePetcare( conf[CONF_USERNAME], conf[CONF_PASSWORD], - conf[CONF_HOUSEHOLD_ID], hass.loop, async_get_clientsession(hass), ) - await surepy.refresh_token() + await surepy.get_data() except SurePetcareAuthenticationError: _LOGGER.error("Unable to connect to surepetcare.io: Wrong credentials!") return False @@ -90,32 +87,44 @@ async def async_setup(hass, config): _LOGGER.error("Unable to connect to surepetcare.io: Wrong %s!", error) return False - # add flaps + # add feeders things = [ - { - CONF_NAME: flap[CONF_NAME], - CONF_ID: flap[CONF_ID], - CONF_TYPE: SureThingID.FLAP.name, - } - for flap in conf[CONF_FLAPS] + {CONF_ID: feeder, CONF_TYPE: SureProductID.FEEDER} + for feeder in conf[CONF_FEEDERS] ] - # add pets + # add flaps (don't differentiate between CAT and PET for now) things.extend( [ - { - CONF_NAME: pet[CONF_NAME], - CONF_ID: pet[CONF_ID], - CONF_TYPE: SureThingID.PET.name, - } - for pet in conf[CONF_PETS] + {CONF_ID: flap, CONF_TYPE: SureProductID.PET_FLAP} + for flap in conf[CONF_FLAPS] ] ) - spc = hass.data[DATA_SURE_PETCARE][SPC] = SurePetcareAPI( - hass, surepy, things, conf[CONF_HOUSEHOLD_ID] + # discover hubs the flaps/feeders are connected to + for device in things.copy(): + device_data = await surepy.device(device[CONF_ID]) + if ( + CONF_PARENT in device_data + and device_data[CONF_PARENT][CONF_PRODUCT_ID] == SureProductID.HUB + and device_data[CONF_PARENT][CONF_ID] not in things + ): + things.append( + { + CONF_ID: device_data[CONF_PARENT][CONF_ID], + CONF_TYPE: SureProductID.HUB, + } + ) + + # add pets + things.extend( + [{CONF_ID: pet, CONF_TYPE: SureProductID.PET} for pet in conf[CONF_PETS]] ) + _LOGGER.debug("Devices and Pets to setup: %s", things) + + spc = hass.data[DATA_SURE_PETCARE][SPC] = SurePetcareAPI(hass, surepy, things) + # initial update await spc.async_update() @@ -135,16 +144,18 @@ async def async_setup(hass, config): class SurePetcareAPI: """Define a generic Sure Petcare object.""" - def __init__(self, hass, surepy, ids, household_id): + def __init__(self, hass, surepy: SurePetcare, ids: List[Dict[str, Any]]) -> None: """Initialize the Sure Petcare object.""" self.hass = hass self.surepy = surepy - self.household_id = household_id self.ids = ids - self.states = {} + self.states: Dict[str, Any] = {} - async def async_update(self, args=None): + async def async_update(self, arg: Any = None) -> None: """Refresh Sure Petcare data.""" + + await self.surepy.get_data() + for thing in self.ids: sure_id = thing[CONF_ID] sure_type = thing[CONF_TYPE] @@ -152,10 +163,15 @@ class SurePetcareAPI: try: type_state = self.states.setdefault(sure_type, {}) - if sure_type == SureThingID.FLAP.name: - type_state[sure_id] = await self.surepy.get_flap_data(sure_id) - elif sure_type == SureThingID.PET.name: - type_state[sure_id] = await self.surepy.get_pet_data(sure_id) + if sure_type in [ + SureProductID.CAT_FLAP, + SureProductID.PET_FLAP, + SureProductID.FEEDER, + SureProductID.HUB, + ]: + type_state[sure_id] = await self.surepy.device(sure_id) + elif sure_type == SureProductID.PET: + type_state[sure_id] = await self.surepy.pet(sure_id) except SurePetcareError as error: _LOGGER.error("Unable to retrieve data from surepetcare.io: %s", error) diff --git a/homeassistant/components/surepetcare/binary_sensor.py b/homeassistant/components/surepetcare/binary_sensor.py index 100da5cb790..5b3ac492137 100644 --- a/homeassistant/components/surepetcare/binary_sensor.py +++ b/homeassistant/components/surepetcare/binary_sensor.py @@ -1,23 +1,28 @@ """Support for Sure PetCare Flaps/Pets binary sensors.""" +from datetime import datetime import logging +from typing import Any, Dict, Optional -from surepy import SureLocationID, SureLockStateID, SureThingID +from surepy import SureLocationID, SureProductID from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_LOCK, + DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_PRESENCE, BinarySensorDevice, ) -from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE +from homeassistant.const import CONF_ID, CONF_TYPE from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import DATA_SURE_PETCARE, DEFAULT_DEVICE_CLASS, SPC, TOPIC_UPDATE +from . import SurePetcareAPI +from .const import DATA_SURE_PETCARE, SPC, TOPIC_UPDATE _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None +) -> None: """Set up Sure PetCare Flaps sensors based on a config entry.""" if discovery_info is None: return @@ -30,10 +35,20 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= sure_id = thing[CONF_ID] sure_type = thing[CONF_TYPE] - if sure_type == SureThingID.FLAP.name: - entity = Flap(sure_id, thing[CONF_NAME], spc) - elif sure_type == SureThingID.PET.name: - entity = Pet(sure_id, thing[CONF_NAME], spc) + # connectivity + if sure_type in [ + SureProductID.CAT_FLAP, + SureProductID.PET_FLAP, + SureProductID.FEEDER, + ]: + entities.append(DeviceConnectivity(sure_id, sure_type, spc)) + + if sure_type == SureProductID.PET: + entity = Pet(sure_id, spc) + elif sure_type == SureProductID.HUB: + entity = Hub(sure_id, spc) + else: + continue entities.append(entity) @@ -44,57 +59,67 @@ class SurePetcareBinarySensor(BinarySensorDevice): """A binary sensor implementation for Sure Petcare Entities.""" def __init__( - self, _id: int, name: str, spc, device_class: str, sure_type: SureThingID + self, + _id: int, + spc: SurePetcareAPI, + device_class: str, + sure_type: SureProductID, ): """Initialize a Sure Petcare binary sensor.""" self._id = _id - self._name = name - self._spc = spc - self._device_class = device_class self._sure_type = sure_type - self._state = {} + self._device_class = device_class + + self._spc: SurePetcareAPI = spc + self._spc_data: Dict[str, Any] = self._spc.states[self._sure_type].get(self._id) + self._state: Dict[str, Any] = {} + + # cover special case where a device has no name set + if "name" in self._spc_data: + name = self._spc_data["name"] + else: + name = f"Unnamed {self._sure_type.name.capitalize()}" + + self._name = f"{self._sure_type.name.capitalize()} {name.capitalize()}" self._async_unsub_dispatcher_connect = None @property - def is_on(self): + def is_on(self) -> Optional[bool]: """Return true if entity is on/unlocked.""" return bool(self._state) @property - def should_poll(self): + def should_poll(self) -> bool: """Return true.""" return False @property - def name(self): + def name(self) -> str: """Return the name of the device if any.""" return self._name @property - def device_state_attributes(self): - """Return the state attributes of the device.""" - return self._state - - @property - def device_class(self): + def device_class(self) -> str: """Return the device class.""" - return DEFAULT_DEVICE_CLASS if not self._device_class else self._device_class + return None if not self._device_class else self._device_class @property - def unique_id(self): + def unique_id(self: BinarySensorDevice) -> str: """Return an unique ID.""" - return f"{self._spc.household_id}-{self._id}" + return f"{self._spc_data['household_id']}-{self._id}" - async def async_update(self): + async def async_update(self) -> None: """Get the latest data and update the state.""" - self._state = self._spc.states[self._sure_type][self._id].get("data") + self._spc_data = self._spc.states[self._sure_type].get(self._id) + self._state = self._spc_data.get("status") + _LOGGER.debug("%s -> self._state: %s", self._name, self._state) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" @callback - def update(): + def update() -> None: """Update the state.""" self.async_schedule_update_ha_state(True) @@ -102,54 +127,38 @@ class SurePetcareBinarySensor(BinarySensorDevice): self.hass, TOPIC_UPDATE, update ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Disconnect dispatcher listener when removed.""" if self._async_unsub_dispatcher_connect: self._async_unsub_dispatcher_connect() -class Flap(SurePetcareBinarySensor): - """Sure Petcare Flap.""" +class Hub(SurePetcareBinarySensor): + """Sure Petcare Pet.""" - def __init__(self, _id: int, name: str, spc): - """Initialize a Sure Petcare Flap.""" - super().__init__( - _id, - f"Flap {name.capitalize()}", - spc, - DEVICE_CLASS_LOCK, - SureThingID.FLAP.name, - ) + def __init__(self, _id: int, spc: SurePetcareAPI) -> None: + """Initialize a Sure Petcare Hub.""" + super().__init__(_id, spc, DEVICE_CLASS_CONNECTIVITY, SureProductID.HUB) @property - def is_on(self): - """Return true if entity is on/unlocked.""" - try: - return bool(self._state["locking"]["mode"] == SureLockStateID.UNLOCKED) - except (KeyError, TypeError): - return None + def available(self) -> bool: + """Return true if entity is available.""" + return bool(self._state["online"]) @property - def device_state_attributes(self): + def is_on(self) -> bool: + """Return true if entity is online.""" + return self.available + + @property + def device_state_attributes(self) -> Optional[Dict[str, Any]]: """Return the state attributes of the device.""" attributes = None if self._state: - try: - attributes = { - "battery_voltage": self._state["battery"] / 4, - "locking_mode": self._state["locking"]["mode"], - "device_rssi": self._state["signal"]["device_rssi"], - "hub_rssi": self._state["signal"]["hub_rssi"], - } - - except (KeyError, TypeError) as error: - _LOGGER.error( - "Error getting device state attributes from %s: %s\n\n%s", - self._name, - error, - self._state, - ) - attributes = self._state + attributes = { + "led_mode": int(self._state["led_mode"]), + "pairing_mode": bool(self._state["pairing_mode"]), + } return attributes @@ -157,20 +166,76 @@ class Flap(SurePetcareBinarySensor): class Pet(SurePetcareBinarySensor): """Sure Petcare Pet.""" - def __init__(self, _id: int, name: str, spc): + def __init__(self, _id: int, spc: SurePetcareAPI) -> None: """Initialize a Sure Petcare Pet.""" - super().__init__( - _id, - f"Pet {name.capitalize()}", - spc, - DEVICE_CLASS_PRESENCE, - SureThingID.PET.name, - ) + super().__init__(_id, spc, DEVICE_CLASS_PRESENCE, SureProductID.PET) @property - def is_on(self): + def is_on(self) -> bool: """Return true if entity is at home.""" try: - return bool(self._state["where"] == SureLocationID.INSIDE) + return bool(SureLocationID(self._state["where"]) == SureLocationID.INSIDE) except (KeyError, TypeError): return False + + @property + def device_state_attributes(self) -> Optional[Dict[str, Any]]: + """Return the state attributes of the device.""" + attributes = None + if self._state: + attributes = { + "since": str( + datetime.fromisoformat(self._state["since"]).replace(tzinfo=None) + ), + "where": SureLocationID(self._state["where"]).name.capitalize(), + } + + return attributes + + async def async_update(self) -> None: + """Get the latest data and update the state.""" + self._spc_data = self._spc.states[self._sure_type].get(self._id) + self._state = self._spc_data.get("position") + _LOGGER.debug("%s -> self._state: %s", self._name, self._state) + + +class DeviceConnectivity(SurePetcareBinarySensor): + """Sure Petcare Pet.""" + + def __init__( + self, _id: int, sure_type: SureProductID, spc: SurePetcareAPI, + ) -> None: + """Initialize a Sure Petcare Device.""" + super().__init__(_id, spc, DEVICE_CLASS_CONNECTIVITY, sure_type) + + @property + def name(self) -> str: + """Return the name of the device if any.""" + return f"{self._name}_connectivity" + + @property + def unique_id(self: BinarySensorDevice) -> str: + """Return an unique ID.""" + return f"{self._spc_data['household_id']}-{self._id}-connectivity" + + @property + def available(self) -> bool: + """Return true if entity is available.""" + return bool(self._state) + + @property + def is_on(self) -> bool: + """Return true if entity is online.""" + return self.available + + @property + def device_state_attributes(self) -> Optional[Dict[str, Any]]: + """Return the state attributes of the device.""" + attributes = None + if self._state: + attributes = { + "device_rssi": f'{self._state["signal"]["device_rssi"]:.2f}', + "hub_rssi": f'{self._state["signal"]["hub_rssi"]:.2f}', + } + + return attributes diff --git a/homeassistant/components/surepetcare/const.py b/homeassistant/components/surepetcare/const.py index 731bfba07e6..d534398784f 100644 --- a/homeassistant/components/surepetcare/const.py +++ b/homeassistant/components/surepetcare/const.py @@ -11,8 +11,11 @@ SPC = "spc" SUREPY = "surepy" CONF_HOUSEHOLD_ID = "household_id" +CONF_FEEDERS = "feeders" CONF_FLAPS = "flaps" +CONF_PARENT = "parent" CONF_PETS = "pets" +CONF_PRODUCT_ID = "product_id" CONF_DATA = "data" SURE_IDS = "sure_ids" diff --git a/homeassistant/components/surepetcare/manifest.json b/homeassistant/components/surepetcare/manifest.json index b4879932714..b1efa4ce639 100644 --- a/homeassistant/components/surepetcare/manifest.json +++ b/homeassistant/components/surepetcare/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/surepetcare", "dependencies": [], "codeowners": ["@benleb"], - "requirements": ["surepy==0.1.10"] + "requirements": ["surepy==0.2.3"] } diff --git a/homeassistant/components/surepetcare/sensor.py b/homeassistant/components/surepetcare/sensor.py index dd7fdcb0316..8dc9cf30e3c 100644 --- a/homeassistant/components/surepetcare/sensor.py +++ b/homeassistant/components/surepetcare/sensor.py @@ -1,19 +1,15 @@ """Support for Sure PetCare Flaps/Pets sensors.""" import logging +from typing import Any, Dict, Optional -from surepy import SureThingID +from surepy import SureLockStateID, SureProductID -from homeassistant.const import ( - ATTR_VOLTAGE, - CONF_ID, - CONF_NAME, - CONF_TYPE, - DEVICE_CLASS_BATTERY, -) +from homeassistant.const import ATTR_VOLTAGE, CONF_ID, CONF_TYPE, DEVICE_CLASS_BATTERY from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity +from . import SurePetcareAPI from .const import ( DATA_SURE_PETCARE, SPC, @@ -30,97 +26,82 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if discovery_info is None: return + entities = [] + spc = hass.data[DATA_SURE_PETCARE][SPC] - async_add_entities( - [ - FlapBattery(entity[CONF_ID], entity[CONF_NAME], spc) - for entity in spc.ids - if entity[CONF_TYPE] == SureThingID.FLAP.name - ], - True, - ) + + for entity in spc.ids: + sure_type = entity[CONF_TYPE] + + if sure_type in [ + SureProductID.CAT_FLAP, + SureProductID.PET_FLAP, + SureProductID.FEEDER, + ]: + entities.append(SureBattery(entity[CONF_ID], sure_type, spc)) + + if sure_type in [ + SureProductID.CAT_FLAP, + SureProductID.PET_FLAP, + ]: + entities.append(Flap(entity[CONF_ID], sure_type, spc)) + + async_add_entities(entities, True) -class FlapBattery(Entity): - """Sure Petcare Flap.""" +class SurePetcareSensor(Entity): + """A binary sensor implementation for Sure Petcare Entities.""" + + def __init__( + self, _id: int, sure_type: SureProductID, spc: SurePetcareAPI, + ): + """Initialize a Sure Petcare sensor.""" - def __init__(self, _id: int, name: str, spc): - """Initialize a Sure Petcare Flap battery sensor.""" self._id = _id - self._name = f"Flap {name.capitalize()} Battery Level" + self._sure_type = sure_type + self._spc = spc - self._state = self._spc.states[SureThingID.FLAP.name][self._id].get("data") + self._spc_data: Dict[str, Any] = self._spc.states[self._sure_type].get(self._id) + self._state: Dict[str, Any] = {} + + self._name = ( + f"{self._sure_type.name.capitalize()} " + f"{self._spc_data['name'].capitalize()}" + ) self._async_unsub_dispatcher_connect = None @property - def should_poll(self): - """Return true.""" - return False - - @property - def name(self): + def name(self) -> str: """Return the name of the device if any.""" return self._name @property - def state(self): - """Return battery level in percent.""" - try: - per_battery_voltage = self._state["battery"] / 4 - voltage_diff = per_battery_voltage - SURE_BATT_VOLTAGE_LOW - battery_percent = int(voltage_diff / SURE_BATT_VOLTAGE_DIFF * 100) - except (KeyError, TypeError): - battery_percent = None - - return battery_percent - - @property - def unique_id(self): + def unique_id(self) -> str: """Return an unique ID.""" - return f"{self._spc.household_id}-{self._id}" + return f"{self._spc_data['household_id']}-{self._id}" @property - def device_class(self): - """Return the device class.""" - return DEVICE_CLASS_BATTERY + def available(self) -> bool: + """Return true if entity is available.""" + return bool(self._state) @property - def device_state_attributes(self): - """Return state attributes.""" - attributes = None - if self._state: - try: - voltage_per_battery = float(self._state["battery"]) / 4 - attributes = { - ATTR_VOLTAGE: f"{float(self._state['battery']):.2f}", - f"{ATTR_VOLTAGE}_per_battery": f"{voltage_per_battery:.2f}", - } - except (KeyError, TypeError) as error: - attributes = self._state - _LOGGER.error( - "Error getting device state attributes from %s: %s\n\n%s", - self._name, - error, - self._state, - ) + def should_poll(self) -> bool: + """Return true.""" + return False - return attributes - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return "%" - - async def async_update(self): + async def async_update(self) -> None: """Get the latest data and update the state.""" - self._state = self._spc.states[SureThingID.FLAP.name][self._id].get("data") + self._spc_data = self._spc.states[self._sure_type].get(self._id) + self._state = self._spc_data.get("status") + _LOGGER.debug("%s -> self._state: %s", self._name, self._state) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" @callback - def update(): + def update() -> None: """Update the state.""" self.async_schedule_update_ha_state(True) @@ -128,7 +109,77 @@ class FlapBattery(Entity): self.hass, TOPIC_UPDATE, update ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Disconnect dispatcher listener when removed.""" if self._async_unsub_dispatcher_connect: self._async_unsub_dispatcher_connect() + + +class Flap(SurePetcareSensor): + """Sure Petcare Flap.""" + + @property + def state(self) -> Optional[int]: + """Return battery level in percent.""" + return SureLockStateID(self._state["locking"]["mode"]).name.capitalize() + + @property + def device_state_attributes(self) -> Optional[Dict[str, Any]]: + """Return the state attributes of the device.""" + attributes = None + if self._state: + attributes = { + "learn_mode": bool(self._state["learn_mode"]), + } + + return attributes + + +class SureBattery(SurePetcareSensor): + """Sure Petcare Flap.""" + + @property + def name(self) -> str: + """Return the name of the device if any.""" + return f"{self._name} Battery Level" + + @property + def state(self) -> Optional[int]: + """Return battery level in percent.""" + battery_percent: Optional[int] + try: + per_battery_voltage = self._state["battery"] / 4 + voltage_diff = per_battery_voltage - SURE_BATT_VOLTAGE_LOW + battery_percent = min(int(voltage_diff / SURE_BATT_VOLTAGE_DIFF * 100), 100) + except (KeyError, TypeError): + battery_percent = None + + return battery_percent + + @property + def unique_id(self) -> str: + """Return an unique ID.""" + return f"{self._spc_data['household_id']}-{self._id}-battery" + + @property + def device_class(self) -> str: + """Return the device class.""" + return DEVICE_CLASS_BATTERY + + @property + def device_state_attributes(self) -> Optional[Dict[str, Any]]: + """Return state attributes.""" + attributes = None + if self._state: + voltage_per_battery = float(self._state["battery"]) / 4 + attributes = { + ATTR_VOLTAGE: f"{float(self._state['battery']):.2f}", + f"{ATTR_VOLTAGE}_per_battery": f"{voltage_per_battery:.2f}", + } + + return attributes + + @property + def unit_of_measurement(self) -> str: + """Return the unit of measurement.""" + return "%" diff --git a/homeassistant/components/switch/.translations/sv.json b/homeassistant/components/switch/.translations/sv.json new file mode 100644 index 00000000000..3ec36265e52 --- /dev/null +++ b/homeassistant/components/switch/.translations/sv.json @@ -0,0 +1,19 @@ +{ + "device_automation": { + "action_type": { + "toggle": "V\u00e4xla {entity_name}", + "turn_off": "St\u00e4ng av {entity_name}", + "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} \u00e4r avst\u00e4ngd", + "is_on": "{entity_name} \u00e4r p\u00e5", + "turn_off": "{entity_name} st\u00e4ngdes av", + "turn_on": "{entity_name} slogs p\u00e5" + }, + "trigger_type": { + "turned_off": "{entity_name} st\u00e4ngdes av", + "turned_on": "{entity_name} slogs p\u00e5" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synologydsm/sensor.py b/homeassistant/components/synologydsm/sensor.py index 3f823331433..d10ecaa15ed 100644 --- a/homeassistant/components/synologydsm/sensor.py +++ b/homeassistant/components/synologydsm/sensor.py @@ -17,6 +17,8 @@ from homeassistant.const import ( CONF_PORT, CONF_SSL, CONF_USERNAME, + DATA_MEGABYTES, + DATA_RATE_KILOBYTES_PER_SECOND, EVENT_HOMEASSISTANT_START, TEMP_CELSIUS, ) @@ -43,14 +45,14 @@ _UTILISATION_MON_COND = { "cpu_5min_load": ["CPU Load (5 min)", "%", "mdi:chip"], "cpu_15min_load": ["CPU Load (15 min)", "%", "mdi:chip"], "memory_real_usage": ["Memory Usage (Real)", "%", "mdi:memory"], - "memory_size": ["Memory Size", "Mb", "mdi:memory"], - "memory_cached": ["Memory Cached", "Mb", "mdi:memory"], - "memory_available_swap": ["Memory Available (Swap)", "Mb", "mdi:memory"], - "memory_available_real": ["Memory Available (Real)", "Mb", "mdi:memory"], - "memory_total_swap": ["Memory Total (Swap)", "Mb", "mdi:memory"], - "memory_total_real": ["Memory Total (Real)", "Mb", "mdi:memory"], - "network_up": ["Network Up", "Kbps", "mdi:upload"], - "network_down": ["Network Down", "Kbps", "mdi:download"], + "memory_size": ["Memory Size", DATA_MEGABYTES, "mdi:memory"], + "memory_cached": ["Memory Cached", DATA_MEGABYTES, "mdi:memory"], + "memory_available_swap": ["Memory Available (Swap)", DATA_MEGABYTES, "mdi:memory"], + "memory_available_real": ["Memory Available (Real)", DATA_MEGABYTES, "mdi:memory"], + "memory_total_swap": ["Memory Total (Swap)", DATA_MEGABYTES, "mdi:memory"], + "memory_total_real": ["Memory Total (Real)", DATA_MEGABYTES, "mdi:memory"], + "network_up": ["Network Up", DATA_RATE_KILOBYTES_PER_SECOND, "mdi:upload"], + "network_down": ["Network Down", DATA_RATE_KILOBYTES_PER_SECOND, "mdi:download"], } _STORAGE_VOL_MON_COND = { "volume_status": ["Status", None, "mdi:checkbox-marked-circle-outline"], diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 44ff9c49a01..0c4270eaeef 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -99,6 +99,7 @@ class LogEntry: def __init__(self, record, stack, source): """Initialize a log entry.""" self.first_occured = self.timestamp = record.created + self.name = record.name self.level = record.levelname self.message = record.getMessage() self.exception = "" @@ -114,7 +115,7 @@ class LogEntry: def hash(self): """Calculate a key for DedupStore.""" - return frozenset([self.message, self.root_cause]) + return frozenset([self.name, self.message, self.root_cause]) def to_dict(self): """Convert object into dict to maintain backward compatibility.""" diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index b1a33736083..1ea8a409052 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -7,7 +7,15 @@ import psutil import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_RESOURCES, CONF_TYPE, STATE_OFF, STATE_ON +from homeassistant.const import ( + CONF_RESOURCES, + CONF_TYPE, + DATA_GIBIBYTES, + DATA_MEBIBYTES, + DATA_RATE_MEGABYTES_PER_SECOND, + STATE_OFF, + STATE_ON, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util @@ -19,8 +27,8 @@ _LOGGER = logging.getLogger(__name__) CONF_ARG = "arg" SENSOR_TYPES = { - "disk_free": ["Disk free", "GiB", "mdi:harddisk", None], - "disk_use": ["Disk use", "GiB", "mdi:harddisk", None], + "disk_free": ["Disk free", DATA_GIBIBYTES, "mdi:harddisk", None], + "disk_use": ["Disk use", DATA_GIBIBYTES, "mdi:harddisk", None], "disk_use_percent": ["Disk use (percent)", "%", "mdi:harddisk", None], "ipv4_address": ["IPv4 address", "", "mdi:server-network", None], "ipv6_address": ["IPv6 address", "", "mdi:server-network", None], @@ -28,29 +36,29 @@ SENSOR_TYPES = { "load_15m": ["Load (15m)", " ", "mdi:memory", None], "load_1m": ["Load (1m)", " ", "mdi:memory", None], "load_5m": ["Load (5m)", " ", "mdi:memory", None], - "memory_free": ["Memory free", "MiB", "mdi:memory", None], - "memory_use": ["Memory use", "MiB", "mdi:memory", None], + "memory_free": ["Memory free", DATA_MEBIBYTES, "mdi:memory", None], + "memory_use": ["Memory use", DATA_MEBIBYTES, "mdi:memory", None], "memory_use_percent": ["Memory use (percent)", "%", "mdi:memory", None], - "network_in": ["Network in", "MiB", "mdi:server-network", None], - "network_out": ["Network out", "MiB", "mdi:server-network", None], + "network_in": ["Network in", DATA_MEBIBYTES, "mdi:server-network", None], + "network_out": ["Network out", DATA_MEBIBYTES, "mdi:server-network", None], "packets_in": ["Packets in", " ", "mdi:server-network", None], "packets_out": ["Packets out", " ", "mdi:server-network", None], "throughput_network_in": [ "Network throughput in", - "MB/s", + DATA_RATE_MEGABYTES_PER_SECOND, "mdi:server-network", None, ], "throughput_network_out": [ "Network throughput out", - "MB/s", + DATA_RATE_MEGABYTES_PER_SECOND, "mdi:server-network", None, ], "process": ["Process", " ", "mdi:memory", None], "processor_use": ["Processor use", "%", "mdi:memory", None], - "swap_free": ["Swap free", "MiB", "mdi:harddisk", None], - "swap_use": ["Swap use", "MiB", "mdi:harddisk", None], + "swap_free": ["Swap free", DATA_MEBIBYTES, "mdi:harddisk", None], + "swap_use": ["Swap use", DATA_MEBIBYTES, "mdi:harddisk", None], "swap_use_percent": ["Swap use (percent)", "%", "mdi:harddisk", None], } diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index ebf605bdc75..727fb868a33 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -12,7 +12,7 @@ from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.util import Throttle -from .const import CONF_FALLBACK +from .const import CONF_FALLBACK, DATA _LOGGER = logging.getLogger(__name__) @@ -20,19 +20,22 @@ DOMAIN = "tado" SIGNAL_TADO_UPDATE_RECEIVED = "tado_update_received_{}_{}" -TADO_COMPONENTS = ["sensor", "climate"] +TADO_COMPONENTS = ["sensor", "climate", "water_heater"] MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) SCAN_INTERVAL = timedelta(seconds=15) CONFIG_SCHEMA = vol.Schema( { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_FALLBACK, default=True): cv.boolean, - } + DOMAIN: vol.All( + cv.ensure_list, + [ + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_FALLBACK, default=True): cv.boolean, + } + ], ) }, extra=vol.ALLOW_EXTRA, @@ -41,45 +44,54 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up of the Tado component.""" - username = config[DOMAIN][CONF_USERNAME] - password = config[DOMAIN][CONF_PASSWORD] + acc_list = config[DOMAIN] - tadoconnector = TadoConnector(hass, username, password) - if not tadoconnector.setup(): - return False + api_data_list = [] - hass.data[DOMAIN] = tadoconnector + for acc in acc_list: + username = acc[CONF_USERNAME] + password = acc[CONF_PASSWORD] + fallback = acc[CONF_FALLBACK] - # Do first update - tadoconnector.update() + tadoconnector = TadoConnector(hass, username, password, fallback) + if not tadoconnector.setup(): + continue + + # Do first update + tadoconnector.update() + + api_data_list.append(tadoconnector) + # Poll for updates in the background + hass.helpers.event.track_time_interval( + # we're using here tadoconnector as a parameter of lambda + # to capture actual value instead of closuring of latest value + lambda now, tc=tadoconnector: tc.update(), + SCAN_INTERVAL, + ) + + hass.data[DOMAIN] = {} + hass.data[DOMAIN][DATA] = api_data_list # Load components for component in TADO_COMPONENTS: load_platform( - hass, - component, - DOMAIN, - {CONF_FALLBACK: config[DOMAIN][CONF_FALLBACK]}, - config, + hass, component, DOMAIN, {}, config, ) - # Poll for updates in the background - hass.helpers.event.track_time_interval( - lambda now: tadoconnector.update(), SCAN_INTERVAL - ) - return True class TadoConnector: """An object to store the Tado data.""" - def __init__(self, hass, username, password): + def __init__(self, hass, username, password, fallback): """Initialize Tado Connector.""" self.hass = hass self._username = username self._password = password + self._fallback = fallback + self.device_id = None self.tado = None self.zones = None self.devices = None @@ -88,6 +100,11 @@ class TadoConnector: "device": {}, } + @property + def fallback(self): + """Return fallback flag to Smart Schedule.""" + return self._fallback + def setup(self): """Connect to Tado and fetch the zones.""" try: @@ -101,7 +118,7 @@ class TadoConnector: # Load zones and devices self.zones = self.tado.getZones() self.devices = self.tado.getMe()["homes"] - + self.device_id = self.devices[0]["id"] return True @Throttle(MIN_TIME_BETWEEN_UPDATES) diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 88433db0991..b92a54edd5e 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -25,13 +25,16 @@ from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, TEMP_CELSIUS from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import CONF_FALLBACK, DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED +from . import DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED from .const import ( CONST_MODE_OFF, CONST_MODE_SMART_SCHEDULE, CONST_OVERLAY_MANUAL, CONST_OVERLAY_TADO_MODE, + CONST_OVERLAY_TIMER, + DATA, TYPE_AIR_CONDITIONING, + TYPE_HEATING, ) _LOGGER = logging.getLogger(__name__) @@ -39,25 +42,25 @@ _LOGGER = logging.getLogger(__name__) FAN_MAP_TADO = {"HIGH": FAN_HIGH, "MIDDLE": FAN_MIDDLE, "LOW": FAN_LOW} HVAC_MAP_TADO_HEAT = { - "MANUAL": HVAC_MODE_HEAT, - "TIMER": HVAC_MODE_HEAT, - "TADO_MODE": HVAC_MODE_HEAT, - "SMART_SCHEDULE": HVAC_MODE_AUTO, - "OFF": HVAC_MODE_OFF, + CONST_OVERLAY_MANUAL: HVAC_MODE_HEAT, + CONST_OVERLAY_TIMER: HVAC_MODE_HEAT, + CONST_OVERLAY_TADO_MODE: HVAC_MODE_HEAT, + CONST_MODE_SMART_SCHEDULE: HVAC_MODE_AUTO, + CONST_MODE_OFF: HVAC_MODE_OFF, } HVAC_MAP_TADO_COOL = { - "MANUAL": HVAC_MODE_COOL, - "TIMER": HVAC_MODE_COOL, - "TADO_MODE": HVAC_MODE_COOL, - "SMART_SCHEDULE": HVAC_MODE_AUTO, - "OFF": HVAC_MODE_OFF, + CONST_OVERLAY_MANUAL: HVAC_MODE_COOL, + CONST_OVERLAY_TIMER: HVAC_MODE_COOL, + CONST_OVERLAY_TADO_MODE: HVAC_MODE_COOL, + CONST_MODE_SMART_SCHEDULE: HVAC_MODE_AUTO, + CONST_MODE_OFF: HVAC_MODE_OFF, } HVAC_MAP_TADO_HEAT_COOL = { - "MANUAL": HVAC_MODE_HEAT_COOL, - "TIMER": HVAC_MODE_HEAT_COOL, - "TADO_MODE": HVAC_MODE_HEAT_COOL, - "SMART_SCHEDULE": HVAC_MODE_AUTO, - "OFF": HVAC_MODE_OFF, + CONST_OVERLAY_MANUAL: HVAC_MODE_HEAT_COOL, + CONST_OVERLAY_TIMER: HVAC_MODE_HEAT_COOL, + CONST_OVERLAY_TADO_MODE: HVAC_MODE_HEAT_COOL, + CONST_MODE_SMART_SCHEDULE: HVAC_MODE_AUTO, + CONST_MODE_OFF: HVAC_MODE_OFF, } SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE @@ -70,21 +73,24 @@ SUPPORT_PRESET = [PRESET_AWAY, PRESET_HOME] def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tado climate platform.""" - tado = hass.data[DOMAIN] + if discovery_info is None: + return + api_list = hass.data[DOMAIN][DATA] entities = [] - for zone in tado.zones: - entity = create_climate_entity( - tado, zone["name"], zone["id"], discovery_info[CONF_FALLBACK] - ) - if entity: - entities.append(entity) + + for tado in api_list: + for zone in tado.zones: + if zone["type"] in [TYPE_HEATING, TYPE_AIR_CONDITIONING]: + entity = create_climate_entity(tado, zone["name"], zone["id"]) + if entity: + entities.append(entity) if entities: add_entities(entities, True) -def create_climate_entity(tado, name: str, zone_id: int, fallback: bool): +def create_climate_entity(tado, name: str, zone_id: int): """Create a Tado climate entity.""" capabilities = tado.get_capabilities(zone_id) _LOGGER.debug("Capabilities for zone %s: %s", zone_id, capabilities) @@ -112,15 +118,7 @@ def create_climate_entity(tado, name: str, zone_id: int, fallback: bool): step = temperatures["celsius"].get("step", PRECISION_TENTHS) entity = TadoClimate( - tado, - name, - zone_id, - zone_type, - min_temp, - max_temp, - step, - ac_support_heat, - fallback, + tado, name, zone_id, zone_type, min_temp, max_temp, step, ac_support_heat, ) return entity @@ -138,7 +136,6 @@ class TadoClimate(ClimateDevice): max_temp, step, ac_support_heat, - fallback, ): """Initialize of Tado climate entity.""" self._tado = tado @@ -146,6 +143,7 @@ class TadoClimate(ClimateDevice): self.zone_name = zone_name self.zone_id = zone_id self.zone_type = zone_type + self._unique_id = f"{zone_type} {zone_id} {tado.device_id}" self._ac_device = zone_type == TYPE_AIR_CONDITIONING self._ac_support_heat = ac_support_heat @@ -162,12 +160,10 @@ class TadoClimate(ClimateDevice): self._step = step self._target_temp = None - if fallback: - _LOGGER.debug("Default overlay is set to TADO MODE") + if tado.fallback: # Fallback to Smart Schedule at next Schedule switch self._default_overlay = CONST_OVERLAY_TADO_MODE else: - _LOGGER.debug("Default overlay is set to MANUAL MODE") # Don't fallback to Smart Schedule, but keep in manual mode self._default_overlay = CONST_OVERLAY_MANUAL @@ -199,6 +195,11 @@ class TadoClimate(ClimateDevice): """Return the name of the entity.""" return self.zone_name + @property + def unique_id(self): + """Return the unique id.""" + return self._unique_id + @property def should_poll(self) -> bool: """Do not poll.""" @@ -358,11 +359,7 @@ class TadoClimate(ClimateDevice): def update(self): """Handle update callbacks.""" _LOGGER.debug("Updating climate platform for zone %d", self.zone_id) - try: - data = self._tado.data["zone"][self.zone_id] - except KeyError: - _LOGGER.debug("No data") - return + data = self._tado.data["zone"][self.zone_id] if "sensorDataPoints" in data: sensor_data = data["sensorDataPoints"] @@ -375,13 +372,13 @@ class TadoClimate(ClimateDevice): humidity = float(sensor_data["humidity"]["percentage"]) self._cur_humidity = humidity - # temperature setting will not exist when device is off - if ( - "temperature" in data["setting"] - and data["setting"]["temperature"] is not None - ): - setting = float(data["setting"]["temperature"]["celsius"]) - self._target_temp = setting + # temperature setting will not exist when device is off + if ( + "temperature" in data["setting"] + and data["setting"]["temperature"] is not None + ): + setting = float(data["setting"]["temperature"]["celsius"]) + self._target_temp = setting if "tadoMode" in data: mode = data["tadoMode"] diff --git a/homeassistant/components/tado/const.py b/homeassistant/components/tado/const.py index 3c0232c8ba2..8d67e3bf9f8 100644 --- a/homeassistant/components/tado/const.py +++ b/homeassistant/components/tado/const.py @@ -2,6 +2,7 @@ # Configuration CONF_FALLBACK = "fallback" +DATA = "data" # Types TYPE_AIR_CONDITIONING = "AIR_CONDITIONING" diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index 7539988d42e..e51cc53caa5 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -2,7 +2,11 @@ "domain": "tado", "name": "Tado", "documentation": "https://www.home-assistant.io/integrations/tado", - "requirements": ["python-tado==0.2.9"], + "requirements": [ + "python-tado==0.3.0" + ], "dependencies": [], - "codeowners": ["@michaelarnauts"] + "codeowners": [ + "@michaelarnauts" + ] } diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index a928b61a508..f5f32a6ed1a 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/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 . import DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED +from . import DATA, DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED from .const import TYPE_AIR_CONDITIONING, TYPE_HEATING, TYPE_HOT_WATER _LOGGER = logging.getLogger(__name__) @@ -40,26 +40,29 @@ DEVICE_SENSORS = ["tado bridge status"] def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the sensor platform.""" - tado = hass.data[DOMAIN] + api_list = hass.data[DOMAIN][DATA] - # Create zone sensors entities = [] - for zone in tado.zones: - entities.extend( - [ - create_zone_sensor(tado, zone["name"], zone["id"], variable) - for variable in ZONE_SENSORS.get(zone["type"]) - ] - ) - # Create device sensors - for home in tado.devices: - entities.extend( - [ - create_device_sensor(tado, home["name"], home["id"], variable) - for variable in DEVICE_SENSORS - ] - ) + for tado in api_list: + # Create zone sensors + + for zone in tado.zones: + entities.extend( + [ + create_zone_sensor(tado, zone["name"], zone["id"], variable) + for variable in ZONE_SENSORS.get(zone["type"]) + ] + ) + + # Create device sensors + for home in tado.devices: + entities.extend( + [ + create_device_sensor(tado, home["name"], home["id"], variable) + for variable in DEVICE_SENSORS + ] + ) add_entities(entities, True) @@ -86,7 +89,7 @@ class TadoSensor(Entity): self.zone_variable = zone_variable self.sensor_type = sensor_type - self._unique_id = f"{zone_variable} {zone_id}" + self._unique_id = f"{zone_variable} {zone_id} {tado.device_id}" self._state = None self._state_attributes = None @@ -227,23 +230,16 @@ class TadoSensor(Entity): self._state = data["tadoMode"] elif self.zone_variable == "overlay": - if "overlay" in data and data["overlay"] is not None: - self._state = True - self._state_attributes = { - "termination": data["overlay"]["termination"]["type"] - } - else: - self._state = False - self._state_attributes = {} + self._state = "overlay" in data and data["overlay"] is not None + self._state_attributes = ( + {"termination": data["overlay"]["termination"]["type"]} + if self._state + else {} + ) elif self.zone_variable == "early start": - if "preparation" in data and data["preparation"] is not None: - self._state = True - else: - self._state = False + self._state = "preparation" in data and data["preparation"] is not None elif self.zone_variable == "open window": - if "openWindowDetected" in data: - self._state = data["openWindowDetected"] - else: - self._state = False + self._state = "openWindow" in data and data["openWindow"] is not None + self._state_attributes = data["openWindow"] if self._state else {} diff --git a/homeassistant/components/tado/water_heater.py b/homeassistant/components/tado/water_heater.py new file mode 100644 index 00000000000..fc3a9ce9cf4 --- /dev/null +++ b/homeassistant/components/tado/water_heater.py @@ -0,0 +1,302 @@ +"""Support for Tado hot water zones.""" +import logging + +from homeassistant.components.water_heater import ( + SUPPORT_OPERATION_MODE, + SUPPORT_TARGET_TEMPERATURE, + WaterHeaterDevice, +) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from . import DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED +from .const import ( + CONST_MODE_OFF, + CONST_MODE_SMART_SCHEDULE, + CONST_OVERLAY_MANUAL, + CONST_OVERLAY_TADO_MODE, + CONST_OVERLAY_TIMER, + DATA, + TYPE_HOT_WATER, +) + +_LOGGER = logging.getLogger(__name__) + +MODE_AUTO = "auto" +MODE_HEAT = "heat" +MODE_OFF = "off" + +OPERATION_MODES = [MODE_AUTO, MODE_HEAT, MODE_OFF] + +WATER_HEATER_MAP_TADO = { + CONST_OVERLAY_MANUAL: MODE_HEAT, + CONST_OVERLAY_TIMER: MODE_HEAT, + CONST_OVERLAY_TADO_MODE: MODE_HEAT, + CONST_MODE_SMART_SCHEDULE: MODE_AUTO, + CONST_MODE_OFF: MODE_OFF, +} + +SUPPORT_FLAGS_HEATER = SUPPORT_OPERATION_MODE + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Tado water heater platform.""" + if discovery_info is None: + return + + api_list = hass.data[DOMAIN][DATA] + entities = [] + + for tado in api_list: + for zone in tado.zones: + if zone["type"] in [TYPE_HOT_WATER]: + entity = create_water_heater_entity(tado, zone["name"], zone["id"]) + entities.append(entity) + + if entities: + add_entities(entities, True) + + +def create_water_heater_entity(tado, name: str, zone_id: int): + """Create a Tado water heater device.""" + capabilities = tado.get_capabilities(zone_id) + supports_temperature_control = capabilities["canSetTemperature"] + + if supports_temperature_control and "temperatures" in capabilities: + temperatures = capabilities["temperatures"] + min_temp = float(temperatures["celsius"]["min"]) + max_temp = float(temperatures["celsius"]["max"]) + else: + min_temp = None + max_temp = None + + entity = TadoWaterHeater( + tado, name, zone_id, supports_temperature_control, min_temp, max_temp + ) + + return entity + + +class TadoWaterHeater(WaterHeaterDevice): + """Representation of a Tado water heater.""" + + def __init__( + self, + tado, + zone_name, + zone_id, + supports_temperature_control, + min_temp, + max_temp, + ): + """Initialize of Tado water heater entity.""" + self._tado = tado + + self.zone_name = zone_name + self.zone_id = zone_id + self._unique_id = f"{zone_id} {tado.device_id}" + + self._device_is_active = False + self._is_away = False + + self._supports_temperature_control = supports_temperature_control + self._min_temperature = min_temp + self._max_temperature = max_temp + + self._target_temp = None + + self._supported_features = SUPPORT_FLAGS_HEATER + if self._supports_temperature_control: + self._supported_features |= SUPPORT_TARGET_TEMPERATURE + + if tado.fallback: + # Fallback to Smart Schedule at next Schedule switch + self._default_overlay = CONST_OVERLAY_TADO_MODE + else: + # Don't fallback to Smart Schedule, but keep in manual mode + self._default_overlay = CONST_OVERLAY_MANUAL + + self._current_operation = CONST_MODE_SMART_SCHEDULE + self._overlay_mode = CONST_MODE_SMART_SCHEDULE + + async def async_added_to_hass(self): + """Register for sensor updates.""" + + @callback + def async_update_callback(): + """Schedule an entity update.""" + self.async_schedule_update_ha_state(True) + + async_dispatcher_connect( + self.hass, + SIGNAL_TADO_UPDATE_RECEIVED.format("zone", self.zone_id), + async_update_callback, + ) + + @property + def supported_features(self): + """Return the list of supported features.""" + return self._supported_features + + @property + def name(self): + """Return the name of the entity.""" + return self.zone_name + + @property + def unique_id(self): + """Return the unique id.""" + return self._unique_id + + @property + def should_poll(self) -> bool: + """Do not poll.""" + return False + + @property + def current_operation(self): + """Return current readable operation mode.""" + return WATER_HEATER_MAP_TADO.get(self._current_operation) + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._target_temp + + @property + def is_away_mode_on(self): + """Return true if away mode is on.""" + return self._is_away + + @property + def operation_list(self): + """Return the list of available operation modes (readable).""" + return OPERATION_MODES + + @property + def temperature_unit(self): + """Return the unit of measurement used by the platform.""" + return TEMP_CELSIUS + + @property + def min_temp(self): + """Return the minimum temperature.""" + return self._min_temperature + + @property + def max_temp(self): + """Return the maximum temperature.""" + return self._max_temperature + + def set_operation_mode(self, operation_mode): + """Set new operation mode.""" + mode = None + + if operation_mode == MODE_OFF: + mode = CONST_MODE_OFF + elif operation_mode == MODE_AUTO: + mode = CONST_MODE_SMART_SCHEDULE + elif operation_mode == MODE_HEAT: + mode = self._default_overlay + + self._current_operation = mode + self._overlay_mode = None + + # Set a target temperature if we don't have any + if mode == CONST_OVERLAY_TADO_MODE and self._target_temp is None: + self._target_temp = self.min_temp + + self._control_heater() + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + if not self._supports_temperature_control or temperature is None: + return + + self._current_operation = self._default_overlay + self._overlay_mode = None + self._target_temp = temperature + self._control_heater() + + def update(self): + """Handle update callbacks.""" + _LOGGER.debug("Updating water_heater platform for zone %d", self.zone_id) + data = self._tado.data["zone"][self.zone_id] + + if "tadoMode" in data: + mode = data["tadoMode"] + self._is_away = mode == "AWAY" + + if "setting" in data: + power = data["setting"]["power"] + if power == "OFF": + self._current_operation = CONST_MODE_OFF + # There is no overlay, the mode will always be + # "SMART_SCHEDULE" + self._overlay_mode = CONST_MODE_SMART_SCHEDULE + self._device_is_active = False + else: + self._device_is_active = True + + # temperature setting will not exist when device is off + if ( + "temperature" in data["setting"] + and data["setting"]["temperature"] is not None + ): + setting = float(data["setting"]["temperature"]["celsius"]) + self._target_temp = setting + + overlay = False + overlay_data = None + termination = CONST_MODE_SMART_SCHEDULE + + if "overlay" in data: + overlay_data = data["overlay"] + overlay = overlay_data is not None + + if overlay: + termination = overlay_data["termination"]["type"] + + if self._device_is_active: + # If you set mode manually to off, there will be an overlay + # and a termination, but we want to see the mode "OFF" + self._overlay_mode = termination + self._current_operation = termination + + def _control_heater(self): + """Send new target temperature.""" + if self._current_operation == CONST_MODE_SMART_SCHEDULE: + _LOGGER.debug( + "Switching to SMART_SCHEDULE for zone %s (%d)", + self.zone_name, + self.zone_id, + ) + self._tado.reset_zone_overlay(self.zone_id) + self._overlay_mode = self._current_operation + return + + if self._current_operation == CONST_MODE_OFF: + _LOGGER.debug( + "Switching to OFF for zone %s (%d)", self.zone_name, self.zone_id + ) + self._tado.set_zone_off(self.zone_id, CONST_OVERLAY_MANUAL, TYPE_HOT_WATER) + self._overlay_mode = self._current_operation + return + + _LOGGER.debug( + "Switching to %s for zone %s (%d) with temperature %s", + self._current_operation, + self.zone_name, + self.zone_id, + self._target_temp, + ) + self._tado.set_zone_overlay( + self.zone_id, + self._current_operation, + self._target_temp, + None, + TYPE_HOT_WATER, + ) + self._overlay_mode = self._current_operation diff --git a/homeassistant/components/tahoma/__init__.py b/homeassistant/components/tahoma/__init__.py index 0d74d6018a5..f14e3019ac0 100644 --- a/homeassistant/components/tahoma/__init__.py +++ b/homeassistant/components/tahoma/__init__.py @@ -31,7 +31,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -TAHOMA_COMPONENTS = ["scene", "sensor", "cover", "switch", "binary_sensor"] +TAHOMA_COMPONENTS = ["binary_sensor", "cover", "lock", "scene", "sensor", "switch"] TAHOMA_TYPES = { "io:AwningValanceIOComponent": "cover", @@ -52,6 +52,7 @@ TAHOMA_TYPES = { "io:VerticalExteriorAwningIOComponent": "cover", "io:VerticalInteriorBlindVeluxIOComponent": "cover", "io:WindowOpenerVeluxIOComponent": "cover", + "opendoors:OpenDoorsSmartLockComponent": "lock", "rtds:RTDSContactSensor": "sensor", "rtds:RTDSMotionSensor": "sensor", "rtds:RTDSSmokeSensor": "smoke", diff --git a/homeassistant/components/tahoma/binary_sensor.py b/homeassistant/components/tahoma/binary_sensor.py index 81078ab480b..7621a542838 100644 --- a/homeassistant/components/tahoma/binary_sensor.py +++ b/homeassistant/components/tahoma/binary_sensor.py @@ -14,6 +14,8 @@ SCAN_INTERVAL = timedelta(seconds=120) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Tahoma controller devices.""" + if discovery_info is None: + return _LOGGER.debug("Setup Tahoma Binary sensor platform") controller = hass.data[TAHOMA_DOMAIN]["controller"] devices = [] diff --git a/homeassistant/components/tahoma/cover.py b/homeassistant/components/tahoma/cover.py index fb2bedc746c..7692e9bedf7 100644 --- a/homeassistant/components/tahoma/cover.py +++ b/homeassistant/components/tahoma/cover.py @@ -51,6 +51,8 @@ TAHOMA_DEVICE_CLASSES = { def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tahoma covers.""" + if discovery_info is None: + return controller = hass.data[TAHOMA_DOMAIN]["controller"] devices = [] for device in hass.data[TAHOMA_DOMAIN]["devices"]["cover"]: diff --git a/homeassistant/components/tahoma/lock.py b/homeassistant/components/tahoma/lock.py new file mode 100644 index 00000000000..0b02975fc7e --- /dev/null +++ b/homeassistant/components/tahoma/lock.py @@ -0,0 +1,89 @@ +"""Support for Tahoma lock.""" +from datetime import timedelta +import logging + +from homeassistant.components.lock import LockDevice +from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_LOCKED, STATE_UNLOCKED + +from . import DOMAIN as TAHOMA_DOMAIN, TahomaDevice + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=120) +TAHOMA_STATE_LOCKED = "locked" + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Tahoma lock.""" + if discovery_info is None: + return + controller = hass.data[TAHOMA_DOMAIN]["controller"] + devices = [] + for device in hass.data[TAHOMA_DOMAIN]["devices"]["lock"]: + devices.append(TahomaLock(device, controller)) + add_entities(devices, True) + + +class TahomaLock(TahomaDevice, LockDevice): + """Representation a Tahoma lock.""" + + def __init__(self, tahoma_device, controller): + """Initialize the device.""" + super().__init__(tahoma_device, controller) + self._lock_status = None + self._available = False + self._battery_level = None + self._name = None + + def update(self): + """Update method.""" + self.controller.get_states([self.tahoma_device]) + self._battery_level = self.tahoma_device.active_states["core:BatteryState"] + self._name = self.tahoma_device.active_states["core:NameState"] + if ( + self.tahoma_device.active_states.get("core:LockedUnlockedState") + == TAHOMA_STATE_LOCKED + ): + self._lock_status = STATE_LOCKED + else: + self._lock_status = STATE_UNLOCKED + self._available = ( + self.tahoma_device.active_states.get("core:AvailabilityState") + == "available" + ) + + def unlock(self, **kwargs): + """Unlock method.""" + _LOGGER.debug("Unlocking %s", self._name) + self.apply_action("unlock") + + def lock(self, **kwargs): + """Lock method.""" + _LOGGER.debug("Locking %s", self._name) + self.apply_action("lock") + + @property + def name(self): + """Return the name of the lock.""" + return self._name + + @property + def available(self): + """Return True if the lock is available.""" + return self._available + + @property + def is_locked(self): + """Return True if the lock is locked.""" + return self._lock_status == STATE_LOCKED + + @property + def device_state_attributes(self): + """Return the lock state attributes.""" + attr = { + ATTR_BATTERY_LEVEL: self._battery_level, + } + super_attr = super().device_state_attributes + if super_attr is not None: + attr.update(super_attr) + return attr diff --git a/homeassistant/components/tahoma/scene.py b/homeassistant/components/tahoma/scene.py index e54ff91a0f6..c60f245fc50 100644 --- a/homeassistant/components/tahoma/scene.py +++ b/homeassistant/components/tahoma/scene.py @@ -10,6 +10,8 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tahoma scenes.""" + if discovery_info is None: + return controller = hass.data[TAHOMA_DOMAIN]["controller"] scenes = [] for scene in hass.data[TAHOMA_DOMAIN]["scenes"]: diff --git a/homeassistant/components/tahoma/sensor.py b/homeassistant/components/tahoma/sensor.py index 85ccb55761d..fb8c61607c7 100644 --- a/homeassistant/components/tahoma/sensor.py +++ b/homeassistant/components/tahoma/sensor.py @@ -16,6 +16,8 @@ ATTR_RSSI_LEVEL = "rssi_level" def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Tahoma controller devices.""" + if discovery_info is None: + return controller = hass.data[TAHOMA_DOMAIN]["controller"] devices = [] for device in hass.data[TAHOMA_DOMAIN]["devices"]["sensor"]: diff --git a/homeassistant/components/tahoma/switch.py b/homeassistant/components/tahoma/switch.py index 1612120f313..9f98e711ac9 100644 --- a/homeassistant/components/tahoma/switch.py +++ b/homeassistant/components/tahoma/switch.py @@ -13,6 +13,8 @@ ATTR_RSSI_LEVEL = "rssi_level" def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Tahoma switches.""" + if discovery_info is None: + return controller = hass.data[TAHOMA_DOMAIN]["controller"] devices = [] for switch in hass.data[TAHOMA_DOMAIN]["devices"]["switch"]: diff --git a/homeassistant/components/teksavvy/sensor.py b/homeassistant/components/teksavvy/sensor.py index fe183129eaa..f340f4a3971 100644 --- a/homeassistant/components/teksavvy/sensor.py +++ b/homeassistant/components/teksavvy/sensor.py @@ -6,7 +6,12 @@ import async_timeout import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_API_KEY, CONF_MONITORED_VARIABLES, CONF_NAME +from homeassistant.const import ( + CONF_API_KEY, + CONF_MONITORED_VARIABLES, + CONF_NAME, + DATA_GIGABYTES, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -17,7 +22,6 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "TekSavvy" CONF_TOTAL_BANDWIDTH = "total_bandwidth" -GIGABYTES = "GB" PERCENT = "%" MIN_TIME_BETWEEN_UPDATES = timedelta(hours=1) @@ -25,15 +29,15 @@ REQUEST_TIMEOUT = 5 # seconds SENSOR_TYPES = { "usage": ["Usage Ratio", PERCENT, "mdi:percent"], - "usage_gb": ["Usage", GIGABYTES, "mdi:download"], - "limit": ["Data limit", GIGABYTES, "mdi:download"], - "onpeak_download": ["On Peak Download", GIGABYTES, "mdi:download"], - "onpeak_upload": ["On Peak Upload", GIGABYTES, "mdi:upload"], - "onpeak_total": ["On Peak Total", GIGABYTES, "mdi:download"], - "offpeak_download": ["Off Peak download", GIGABYTES, "mdi:download"], - "offpeak_upload": ["Off Peak Upload", GIGABYTES, "mdi:upload"], - "offpeak_total": ["Off Peak Total", GIGABYTES, "mdi:download"], - "onpeak_remaining": ["Remaining", GIGABYTES, "mdi:download"], + "usage_gb": ["Usage", DATA_GIGABYTES, "mdi:download"], + "limit": ["Data limit", DATA_GIGABYTES, "mdi:download"], + "onpeak_download": ["On Peak Download", DATA_GIGABYTES, "mdi:download"], + "onpeak_upload": ["On Peak Upload", DATA_GIGABYTES, "mdi:upload"], + "onpeak_total": ["On Peak Total", DATA_GIGABYTES, "mdi:download"], + "offpeak_download": ["Off Peak download", DATA_GIGABYTES, "mdi:download"], + "offpeak_upload": ["Off Peak Upload", DATA_GIGABYTES, "mdi:upload"], + "offpeak_total": ["Off Peak Total", DATA_GIGABYTES, "mdi:download"], + "onpeak_remaining": ["Remaining", DATA_GIGABYTES, "mdi:download"], } API_HA_MAP = ( diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 9b56201f8c7..277f9108663 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -25,7 +25,6 @@ from homeassistant.const import ( ATTR_LONGITUDE, CONF_API_KEY, CONF_PLATFORM, - CONF_TIMEOUT, CONF_URL, HTTP_DIGEST_AUTHENTICATION, ) @@ -67,6 +66,7 @@ ATTR_URL = "url" ATTR_USER_ID = "user_id" ATTR_USERNAME = "username" ATTR_VERIFY_SSL = "verify_ssl" +ATTR_TIMEOUT = "timeout" CONF_ALLOWED_CHAT_IDS = "allowed_chat_ids" CONF_PROXY_URL = "proxy_url" @@ -135,7 +135,7 @@ BASE_SERVICE_SCHEMA = vol.Schema( vol.Optional(ATTR_DISABLE_WEB_PREV): cv.boolean, vol.Optional(ATTR_KEYBOARD): vol.All(cv.ensure_list, [cv.string]), vol.Optional(ATTR_KEYBOARD_INLINE): cv.ensure_list, - vol.Optional(CONF_TIMEOUT): vol.Coerce(float), + vol.Optional(ATTR_TIMEOUT): cv.positive_int, }, extra=vol.ALLOW_EXTRA, ) @@ -499,15 +499,15 @@ class TelegramNotificationService: ATTR_DISABLE_WEB_PREV: None, ATTR_REPLY_TO_MSGID: None, ATTR_REPLYMARKUP: None, - CONF_TIMEOUT: None, + ATTR_TIMEOUT: None, } if data is not None: if ATTR_PARSER in data: params[ATTR_PARSER] = self._parsers.get( data[ATTR_PARSER], self._parse_mode ) - if CONF_TIMEOUT in data: - params[CONF_TIMEOUT] = data[CONF_TIMEOUT] + if ATTR_TIMEOUT in data: + params[ATTR_TIMEOUT] = data[ATTR_TIMEOUT] if ATTR_DISABLE_NOTIF in data: params[ATTR_DISABLE_NOTIF] = data[ATTR_DISABLE_NOTIF] if ATTR_DISABLE_WEB_PREV in data: diff --git a/homeassistant/components/telegram_bot/services.yaml b/homeassistant/components/telegram_bot/services.yaml index ed8720c5877..e3d303a2c52 100644 --- a/homeassistant/components/telegram_bot/services.yaml +++ b/homeassistant/components/telegram_bot/services.yaml @@ -21,6 +21,9 @@ send_message: disable_web_page_preview: description: Disables link previews for links in the message. example: true + timeout: + description: Timeout for send message. Will help with timeout errors (poor internet connection, etc) + example: '1000' keyboard: description: List of rows of commands, comma-separated, to make a custom keyboard. Empty list clears a previously set keyboard. example: '["/command1, /command2", "/command3"]' @@ -55,6 +58,9 @@ send_photo: verify_ssl: description: Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server. example: false + timeout: + description: Timeout for send photo. Will help with timeout errors (poor internet connection, etc) + example: '1000' keyboard: description: List of rows of commands, comma-separated, to make a custom keyboard. example: '["/command1, /command2", "/command3"]' @@ -86,6 +92,9 @@ send_sticker: verify_ssl: description: Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server. example: false + timeout: + description: Timeout for send sticker. Will help with timeout errors (poor internet connection, etc) + example: '1000' keyboard: description: List of rows of commands, comma-separated, to make a custom keyboard. example: '["/command1, /command2", "/command3"]' @@ -120,6 +129,9 @@ send_video: verify_ssl: description: Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server. example: false + timeout: + description: Timeout for send video. Will help with timeout errors (poor internet connection, etc) + example: '1000' keyboard: description: List of rows of commands, comma-separated, to make a custom keyboard. example: '["/command1, /command2", "/command3"]' @@ -154,6 +166,9 @@ send_document: verify_ssl: description: Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server. example: false + timeout: + description: Timeout for send document. Will help with timeout errors (poor internet connection, etc) + example: '1000' keyboard: description: List of rows of commands, comma-separated, to make a custom keyboard. example: '["/command1, /command2", "/command3"]' @@ -176,6 +191,9 @@ send_location: disable_notification: description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. example: true + timeout: + description: Timeout for send photo. Will help with timeout errors (poor internet connection, etc) + example: '1000' keyboard: description: List of rows of commands, comma-separated, to make a custom keyboard. example: '["/command1, /command2", "/command3"]' diff --git a/homeassistant/components/tellduslive/.translations/pl.json b/homeassistant/components/tellduslive/.translations/pl.json index 01d3c7125c3..68e53df57f1 100644 --- a/homeassistant/components/tellduslive/.translations/pl.json +++ b/homeassistant/components/tellduslive/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "TelldusLive jest ju\u017c skonfigurowany", + "already_setup": "TelldusLive jest ju\u017c skonfigurowany.", "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji.", "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", "unknown": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d" diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index c5512461f34..a6855a1654b 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -6,8 +6,10 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, + ATTR_HS_COLOR, ENTITY_ID_FORMAT, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_COLOR_TEMP, Light, ) @@ -42,6 +44,8 @@ CONF_LEVEL_ACTION = "set_level" CONF_LEVEL_TEMPLATE = "level_template" CONF_TEMPERATURE_TEMPLATE = "temperature_template" CONF_TEMPERATURE_ACTION = "set_temperature" +CONF_COLOR_TEMPLATE = "color_template" +CONF_COLOR_ACTION = "set_color" LIGHT_SCHEMA = vol.Schema( { @@ -57,6 +61,8 @@ LIGHT_SCHEMA = vol.Schema( vol.Optional(CONF_ENTITY_ID): cv.entity_ids, vol.Optional(CONF_TEMPERATURE_TEMPLATE): cv.template, vol.Optional(CONF_TEMPERATURE_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_COLOR_TEMPLATE): cv.template, + vol.Optional(CONF_COLOR_ACTION): cv.SCRIPT_SCHEMA, } ) @@ -76,14 +82,19 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= icon_template = device_config.get(CONF_ICON_TEMPLATE) entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE) availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) - level_template = device_config.get(CONF_LEVEL_TEMPLATE) on_action = device_config[CONF_ON_ACTION] off_action = device_config[CONF_OFF_ACTION] + level_action = device_config.get(CONF_LEVEL_ACTION) + level_template = device_config.get(CONF_LEVEL_TEMPLATE) + temperature_action = device_config.get(CONF_TEMPERATURE_ACTION) temperature_template = device_config.get(CONF_TEMPERATURE_TEMPLATE) + color_action = device_config.get(CONF_COLOR_ACTION) + color_template = device_config.get(CONF_COLOR_TEMPLATE) + templates = { CONF_VALUE_TEMPLATE: state_template, CONF_ICON_TEMPLATE: icon_template, @@ -91,6 +102,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= CONF_AVAILABILITY_TEMPLATE: availability_template, CONF_LEVEL_TEMPLATE: level_template, CONF_TEMPERATURE_TEMPLATE: temperature_template, + CONF_COLOR_TEMPLATE: color_template, } initialise_templates(hass, templates) @@ -114,6 +126,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= entity_ids, temperature_action, temperature_template, + color_action, + color_template, ) ) @@ -144,6 +158,8 @@ class LightTemplate(Light): entity_ids, temperature_action, temperature_template, + color_action, + color_template, ): """Initialize the light.""" self.hass = hass @@ -165,12 +181,17 @@ class LightTemplate(Light): if temperature_action is not None: self._temperature_script = Script(hass, temperature_action) self._temperature_template = temperature_template + self._color_script = None + if color_action is not None: + self._color_script = Script(hass, color_action) + self._color_template = color_template self._state = False self._icon = None self._entity_picture = None self._brightness = None self._temperature = None + self._color = None self._entities = entity_ids self._available = True @@ -184,6 +205,11 @@ class LightTemplate(Light): """Return the CT color value in mireds.""" return self._temperature + @property + def hs_color(self): + """Return the hue and saturation color value [float, float].""" + return self._color + @property def name(self): """Return the display name of this light.""" @@ -197,6 +223,8 @@ class LightTemplate(Light): supported_features |= SUPPORT_BRIGHTNESS if self._temperature_script is not None: supported_features |= SUPPORT_COLOR_TEMP + if self._color_script is not None: + supported_features |= SUPPORT_COLOR return supported_features @property @@ -239,6 +267,7 @@ class LightTemplate(Light): self._template is not None or self._level_template is not None or self._temperature_template is not None + or self._color_template is not None or self._availability_template is not None ): async_track_state_change( @@ -282,6 +311,12 @@ class LightTemplate(Light): await self._temperature_script.async_run( {"color_temp": kwargs[ATTR_COLOR_TEMP]}, context=self._context ) + elif ATTR_HS_COLOR in kwargs and self._color_script: + hs_value = kwargs[ATTR_HS_COLOR] + await self._color_script.async_run( + {"hs": hs_value, "h": int(hs_value[0]), "s": int(hs_value[1])}, + context=self._context, + ) else: await self._on_script.async_run() @@ -303,6 +338,8 @@ class LightTemplate(Light): self.update_temperature() + self.update_color() + for property_name, template in ( ("_icon", self._icon_template), ("_entity_picture", self._entity_picture_template), @@ -396,3 +433,34 @@ class LightTemplate(Light): except TemplateError: _LOGGER.error("Cannot evaluate temperature template", exc_info=True) self._temperature = None + + @callback + def update_color(self): + """Update the hs_color from the template.""" + if self._color_template is None: + return + + self._color = None + + try: + render = self._color_template.async_render() + h_str, s_str = map( + float, render.replace("(", "").replace(")", "").split(",", 1) + ) + if ( + h_str is not None + and s_str is not None + and 0 <= h_str <= 360 + and 0 <= s_str <= 100 + ): + self._color = (h_str, s_str) + elif h_str is not None and s_str is not None: + _LOGGER.error( + "Received invalid hs_color : (%s, %s). Expected: (0-360, 0-100)", + h_str, + s_str, + ) + else: + _LOGGER.error("Received invalid hs_color : (%s)", render) + except TemplateError as ex: + _LOGGER.error(ex) diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index e34e9644381..024dc2b7bdd 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -4,10 +4,10 @@ "documentation": "https://www.home-assistant.io/integrations/tensorflow", "requirements": [ "tensorflow==1.13.2", - "numpy==1.17.4", + "numpy==1.18.1", "protobuf==3.6.1", - "pillow==6.2.1" + "pillow==7.0.0" ], "dependencies": [], "codeowners": [] -} +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/pl.json b/homeassistant/components/tesla/.translations/pl.json index 5a8a3d2ebd3..89233646ef0 100644 --- a/homeassistant/components/tesla/.translations/pl.json +++ b/homeassistant/components/tesla/.translations/pl.json @@ -2,7 +2,7 @@ "config": { "error": { "connection_error": "B\u0142\u0105d po\u0142\u0105czenia; sprawd\u017a sie\u0107 i spr\u00f3buj ponownie", - "identifier_exists": "Adres e-mail ju\u017c zarejestrowany", + "identifier_exists": "Adres e-mail jest ju\u017c zarejestrowany.", "invalid_credentials": "Nieprawid\u0142owe po\u015bwiadczenia", "unknown_error": "Nieznany b\u0142\u0105d, prosz\u0119 zg\u0142osi\u0107 dane z loga" }, diff --git a/homeassistant/components/tesla/.translations/sv.json b/homeassistant/components/tesla/.translations/sv.json new file mode 100644 index 00000000000..46263ff64ae --- /dev/null +++ b/homeassistant/components/tesla/.translations/sv.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Fel vid anslutning; kontrollera n\u00e4tverket och f\u00f6rs\u00f6k igen", + "identifier_exists": "E-post redan registrerad", + "invalid_credentials": "Ogiltiga autentiseringsuppgifter", + "unknown_error": "Ok\u00e4nt fel, var god att rapportera logginformation" + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "username": "E-postadress" + }, + "description": "V\u00e4nligen ange din information.", + "title": "Tesla - Konfiguration" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Sekunder mellan skanningar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py index 3c2a22793db..df0664b8f4c 100644 --- a/homeassistant/components/tesla/__init__.py +++ b/homeassistant/components/tesla/__init__.py @@ -27,7 +27,14 @@ from .config_flow import ( configured_instances, validate_input, ) -from .const import DATA_LISTENER, DOMAIN, ICONS, TESLA_COMPONENTS +from .const import ( + DATA_LISTENER, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + ICONS, + MIN_SCAN_INTERVAL, + TESLA_COMPONENTS, +) _LOGGER = logging.getLogger(__name__) @@ -37,9 +44,9 @@ CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_SCAN_INTERVAL, default=300): vol.All( - cv.positive_int, vol.Clamp(min=300) - ), + vol.Optional( + CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL + ): vol.All(cv.positive_int, vol.Clamp(min=MIN_SCAN_INTERVAL)), } ) }, @@ -64,7 +71,7 @@ async def async_setup(hass, base_config): def _update_entry(email, data=None, options=None): data = data or {} - options = options or {CONF_SCAN_INTERVAL: 300} + options = options or {CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL} for entry in hass.config_entries.async_entries(DOMAIN): if email != entry.title: continue @@ -120,7 +127,9 @@ async def async_setup_entry(hass, config_entry): websession, refresh_token=config[CONF_TOKEN], access_token=config[CONF_ACCESS_TOKEN], - update_interval=config_entry.options.get(CONF_SCAN_INTERVAL, 300), + update_interval=config_entry.options.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ), ) (refresh_token, access_token) = await controller.connect() except TeslaException as ex: diff --git a/homeassistant/components/tesla/config_flow.py b/homeassistant/components/tesla/config_flow.py index 2d2bc0158d2..c719807da9f 100644 --- a/homeassistant/components/tesla/config_flow.py +++ b/homeassistant/components/tesla/config_flow.py @@ -15,7 +15,7 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.helpers import aiohttp_client, config_validation as cv -from .const import DOMAIN +from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, MIN_SCAN_INTERVAL _LOGGER = logging.getLogger(__name__) @@ -100,8 +100,10 @@ class OptionsFlowHandler(config_entries.OptionsFlow): { vol.Optional( CONF_SCAN_INTERVAL, - default=self.config_entry.options.get(CONF_SCAN_INTERVAL, 300), - ): vol.All(cv.positive_int, vol.Clamp(min=300)) + default=self.config_entry.options.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ), + ): vol.All(cv.positive_int, vol.Clamp(min=MIN_SCAN_INTERVAL)) } ) return self.async_show_form(step_id="init", data_schema=data_schema) @@ -120,7 +122,7 @@ async def validate_input(hass: core.HomeAssistant, data): websession, email=data[CONF_USERNAME], password=data[CONF_PASSWORD], - update_interval=300, + update_interval=DEFAULT_SCAN_INTERVAL, ) (config[CONF_TOKEN], config[CONF_ACCESS_TOKEN]) = await controller.connect( test_login=True diff --git a/homeassistant/components/tesla/const.py b/homeassistant/components/tesla/const.py index be460a430ac..54cb7a2e071 100644 --- a/homeassistant/components/tesla/const.py +++ b/homeassistant/components/tesla/const.py @@ -1,6 +1,8 @@ """Const file for Tesla cars.""" DOMAIN = "tesla" DATA_LISTENER = "listener" +DEFAULT_SCAN_INTERVAL = 660 +MIN_SCAN_INTERVAL = 60 TESLA_COMPONENTS = [ "sensor", "lock", diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index 23bc76ee6b5..9f8579e3e18 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -2,7 +2,7 @@ "domain": "tibber", "name": "Tibber", "documentation": "https://www.home-assistant.io/integrations/tibber", - "requirements": ["pyTibber==0.12.0"], + "requirements": ["pyTibber==0.12.2"], "dependencies": [], "codeowners": ["@danielhiversen"], "quality_scale": "silver" diff --git a/homeassistant/components/tod/binary_sensor.py b/homeassistant/components/tod/binary_sensor.py index cab57c59ac8..72507b3d148 100644 --- a/homeassistant/components/tod/binary_sensor.py +++ b/homeassistant/components/tod/binary_sensor.py @@ -126,14 +126,14 @@ class TodSensor(BinarySensorDevice): current_local_date = self.current_datetime.astimezone( self.hass.config.time_zone ).date() - # calcuate utc datetime corecponding to local time + # calculate utc datetime corecponding to local time utc_datetime = self.hass.config.time_zone.localize( datetime.combine(current_local_date, naive_time) ).astimezone(tz=pytz.UTC) return utc_datetime def _calculate_initial_boudary_time(self): - """Calculate internal absolute time boudaries.""" + """Calculate internal absolute time boundaries.""" nowutc = self.current_datetime # If after value is a sun event instead of absolute time if is_sun_event(self._after): diff --git a/homeassistant/components/toon/__init__.py b/homeassistant/components/toon/__init__.py index 348826a1264..612561707b1 100644 --- a/homeassistant/components/toon/__init__.py +++ b/homeassistant/components/toon/__init__.py @@ -137,7 +137,7 @@ class ToonData: def update(self, now=None): """Update all Toon data and notify entities.""" - # Ignore the TTL meganism from client library + # Ignore the TTL mechanism from client library # It causes a lots of issues, hence we take control over caching self._toon._clear_cache() # pylint: disable=protected-access diff --git a/homeassistant/components/totalconnect/__init__.py b/homeassistant/components/totalconnect/__init__.py index 020f2d9c07f..e6cfbbc629a 100644 --- a/homeassistant/components/totalconnect/__init__.py +++ b/homeassistant/components/totalconnect/__init__.py @@ -24,7 +24,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -TOTALCONNECT_PLATFORMS = ["alarm_control_panel"] +TOTALCONNECT_PLATFORMS = ["alarm_control_panel", "binary_sensor"] def setup(hass, config): diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py index ed77fc4eea0..b255132a365 100644 --- a/homeassistant/components/totalconnect/alarm_control_panel.py +++ b/homeassistant/components/totalconnect/alarm_control_panel.py @@ -32,10 +32,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): client = hass.data[TOTALCONNECT_DOMAIN].client - for location in client.locations: - location_id = location.get("LocationID") - name = location.get("LocationName") - alarms.append(TotalConnectAlarm(name, location_id, client)) + for location_id, location in client.locations.items(): + location_name = location.location_name + alarms.append(TotalConnectAlarm(location_name, location_id, client)) add_entities(alarms) @@ -72,35 +71,35 @@ class TotalConnectAlarm(alarm.AlarmControlPanel): def update(self): """Return the state of the device.""" - status = self._client.get_armed_status(self._name) + status = self._client.get_armed_status(self._location_id) attr = { "location_name": self._name, "location_id": self._location_id, - "ac_loss": self._client.ac_loss, - "low_battery": self._client.low_battery, + "ac_loss": self._client.locations[self._location_id].ac_loss, + "low_battery": self._client.locations[self._location_id].low_battery, + "cover_tampered": self._client.locations[ + self._location_id + ].is_cover_tampered, "triggered_source": None, "triggered_zone": None, } - if status == self._client.DISARMED: + if status in (self._client.DISARMED, self._client.DISARMED_BYPASS): state = STATE_ALARM_DISARMED - elif status == self._client.DISARMED_BYPASS: - state = STATE_ALARM_DISARMED - elif status == self._client.ARMED_STAY: - state = STATE_ALARM_ARMED_HOME - elif status == self._client.ARMED_STAY_INSTANT: - state = STATE_ALARM_ARMED_HOME - elif status == self._client.ARMED_STAY_INSTANT_BYPASS: + elif status in ( + self._client.ARMED_STAY, + self._client.ARMED_STAY_INSTANT, + self._client.ARMED_STAY_INSTANT_BYPASS, + ): state = STATE_ALARM_ARMED_HOME elif status == self._client.ARMED_STAY_NIGHT: state = STATE_ALARM_ARMED_NIGHT - elif status == self._client.ARMED_AWAY: - state = STATE_ALARM_ARMED_AWAY - elif status == self._client.ARMED_AWAY_BYPASS: - state = STATE_ALARM_ARMED_AWAY - elif status == self._client.ARMED_AWAY_INSTANT: - state = STATE_ALARM_ARMED_AWAY - elif status == self._client.ARMED_AWAY_INSTANT_BYPASS: + elif status in ( + self._client.ARMED_AWAY, + self._client.ARMED_AWAY_BYPASS, + self._client.ARMED_AWAY_INSTANT, + self._client.ARMED_AWAY_INSTANT_BYPASS, + ): state = STATE_ALARM_ARMED_AWAY elif status == self._client.ARMED_CUSTOM_BYPASS: state = STATE_ALARM_ARMED_CUSTOM_BYPASS @@ -128,16 +127,16 @@ class TotalConnectAlarm(alarm.AlarmControlPanel): def alarm_disarm(self, code=None): """Send disarm command.""" - self._client.disarm(self._name) + self._client.disarm(self._location_id) def alarm_arm_home(self, code=None): """Send arm home command.""" - self._client.arm_stay(self._name) + self._client.arm_stay(self._location_id) def alarm_arm_away(self, code=None): """Send arm away command.""" - self._client.arm_away(self._name) + self._client.arm_away(self._location_id) def alarm_arm_night(self, code=None): """Send arm night command.""" - self._client.arm_stay_night(self._name) + self._client.arm_stay_night(self._location_id) diff --git a/homeassistant/components/totalconnect/binary_sensor.py b/homeassistant/components/totalconnect/binary_sensor.py new file mode 100644 index 00000000000..28bd58cfff8 --- /dev/null +++ b/homeassistant/components/totalconnect/binary_sensor.py @@ -0,0 +1,90 @@ +"""Interfaces with TotalConnect sensors.""" +import logging + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_DOOR, + DEVICE_CLASS_GAS, + DEVICE_CLASS_SMOKE, + BinarySensorDevice, +) + +from . import DOMAIN as TOTALCONNECT_DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up a sensor for a TotalConnect device.""" + if discovery_info is None: + return + + sensors = [] + + client_locations = hass.data[TOTALCONNECT_DOMAIN].client.locations + + for location_id, location in client_locations.items(): + for zone_id, zone in location.zones.items(): + sensors.append(TotalConnectBinarySensor(zone_id, location_id, zone)) + add_entities(sensors, True) + + +class TotalConnectBinarySensor(BinarySensorDevice): + """Represent an TotalConnect zone.""" + + def __init__(self, zone_id, location_id, zone): + """Initialize the TotalConnect status.""" + self._zone_id = zone_id + self._location_id = location_id + self._zone = zone + self._name = self._zone.description + self._unique_id = f"{location_id} {zone_id}" + self._is_on = None + self._is_tampered = None + self._is_low_battery = None + + @property + def unique_id(self): + """Return the unique id.""" + return self._unique_id + + @property + def name(self): + """Return the name of the device.""" + return self._name + + def update(self): + """Return the state of the device.""" + self._is_tampered = self._zone.is_tampered() + self._is_low_battery = self._zone.is_low_battery() + + if self._zone.is_faulted() or self._zone.is_triggered(): + self._is_on = True + else: + self._is_on = False + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._is_on + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + if self._zone.is_type_security(): + return DEVICE_CLASS_DOOR + if self._zone.is_type_fire(): + return DEVICE_CLASS_SMOKE + if self._zone.is_type_carbon_monoxide(): + return DEVICE_CLASS_GAS + return None + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attributes = { + "zone_id": self._zone_id, + "location_id": self._location_id, + "low_battery": self._is_low_battery, + "tampered": self._is_tampered, + } + return attributes diff --git a/homeassistant/components/totalconnect/manifest.json b/homeassistant/components/totalconnect/manifest.json index 6b2119f1cf5..967115e721a 100644 --- a/homeassistant/components/totalconnect/manifest.json +++ b/homeassistant/components/totalconnect/manifest.json @@ -2,7 +2,7 @@ "domain": "totalconnect", "name": "Honeywell Total Connect Alarm", "documentation": "https://www.home-assistant.io/integrations/totalconnect", - "requirements": ["total_connect_client==0.28"], + "requirements": ["total_connect_client==0.50"], "dependencies": [], - "codeowners": [] + "codeowners": ["@austinmroczek"] } diff --git a/homeassistant/components/traccar/.translations/pl.json b/homeassistant/components/traccar/.translations/pl.json index 95b7eb1af00..b7eaf7fe16e 100644 --- a/homeassistant/components/traccar/.translations/pl.json +++ b/homeassistant/components/traccar/.translations/pl.json @@ -10,7 +10,7 @@ "step": { "user": { "description": "Na pewno chcesz skonfigurowa\u0107 Traccar?", - "title": "Skonfiguruj Traccar" + "title": "Konfiguracja Traccar" } }, "title": "Traccar" diff --git a/homeassistant/components/traccar/.translations/sv.json b/homeassistant/components/traccar/.translations/sv.json new file mode 100644 index 00000000000..ddd33235e01 --- /dev/null +++ b/homeassistant/components/traccar/.translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Din Home Assistant-instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot meddelanden fr\u00e5n Traccar.", + "one_instance_allowed": "Endast en enda instans \u00e4r n\u00f6dv\u00e4ndig." + }, + "create_entry": { + "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du st\u00e4lla in webhook-funktionen i Traccar.\n\nAnv\u00e4nd f\u00f6ljande url: `{webhook_url}`\n\nMer information finns i [dokumentationen]({docs_url})." + }, + "step": { + "user": { + "description": "\u00c4r du s\u00e4ker p\u00e5 att du vill st\u00e4lla in Traccar?", + "title": "St\u00e4ll in Traccar" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/.translations/de.json b/homeassistant/components/tradfri/.translations/de.json index 5dc2630556e..68165dbb291 100644 --- a/homeassistant/components/tradfri/.translations/de.json +++ b/homeassistant/components/tradfri/.translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Bridge ist bereits konfiguriert", + "already_configured": "Bridge ist bereits konfiguriert.", "already_in_progress": "Der Konfigurationsablauf f\u00fcr die Bridge wird bereits ausgef\u00fchrt." }, "error": { diff --git a/homeassistant/components/tradfri/.translations/pl.json b/homeassistant/components/tradfri/.translations/pl.json index fc115294031..208687839dd 100644 --- a/homeassistant/components/tradfri/.translations/pl.json +++ b/homeassistant/components/tradfri/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Mostek jest ju\u017c skonfigurowany", + "already_configured": "Mostek jest ju\u017c skonfigurowany.", "already_in_progress": "Konfiguracja mostka jest ju\u017c w toku." }, "error": { diff --git a/homeassistant/components/tradfri/light.py b/homeassistant/components/tradfri/light.py index 0fe826be9af..40fe7b01cb0 100644 --- a/homeassistant/components/tradfri/light.py +++ b/homeassistant/components/tradfri/light.py @@ -246,7 +246,7 @@ class TradfriLight(TradfriBaseDevice, Light): color_command = self._device_control.set_hsb(**color_data) transition_time = None - # HSB can always be set, but color temp + brightness is bulb dependant + # HSB can always be set, but color temp + brightness is bulb dependent command = dimmer_command if command is not None: command += color_command diff --git a/homeassistant/components/trafikverket_train/manifest.json b/homeassistant/components/trafikverket_train/manifest.json index 33f634e279f..1458b717fc6 100644 --- a/homeassistant/components/trafikverket_train/manifest.json +++ b/homeassistant/components/trafikverket_train/manifest.json @@ -2,7 +2,7 @@ "domain": "trafikverket_train", "name": "Trafikverket Train", "documentation": "https://www.home-assistant.io/integrations/trafikverket_train", - "requirements": ["pytrafikverket==0.1.5.9"], + "requirements": ["pytrafikverket==0.1.6.1"], "dependencies": [], "codeowners": ["@endor-force"] -} +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_weatherstation/manifest.json b/homeassistant/components/trafikverket_weatherstation/manifest.json index 652cebf6730..3224df25c3f 100644 --- a/homeassistant/components/trafikverket_weatherstation/manifest.json +++ b/homeassistant/components/trafikverket_weatherstation/manifest.json @@ -2,7 +2,7 @@ "domain": "trafikverket_weatherstation", "name": "Trafikverket Weather Station", "documentation": "https://www.home-assistant.io/integrations/trafikverket_weatherstation", - "requirements": ["pytrafikverket==0.1.5.9"], + "requirements": ["pytrafikverket==0.1.6.1"], "dependencies": [], "codeowners": [] -} +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/hu.json b/homeassistant/components/transmission/.translations/hu.json new file mode 100644 index 00000000000..14bf5c28bdf --- /dev/null +++ b/homeassistant/components/transmission/.translations/hu.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Csak egyetlen p\u00e9ld\u00e1nyra van sz\u00fcks\u00e9g." + }, + "error": { + "cannot_connect": "Nem lehet csatlakozni az \u00e1llom\u00e1shoz", + "name_exists": "A n\u00e9v m\u00e1r l\u00e9tezik", + "wrong_credentials": "Rossz felhaszn\u00e1l\u00f3n\u00e9v vagy jelsz\u00f3" + }, + "step": { + "options": { + "data": { + "scan_interval": "Friss\u00edt\u00e9si gyakoris\u00e1g" + }, + "title": "Be\u00e1ll\u00edt\u00e1sok konfigur\u00e1l\u00e1sa" + }, + "user": { + "data": { + "host": "Kiszolg\u00e1l\u00f3", + "name": "N\u00e9v", + "password": "Jelsz\u00f3", + "port": "Port", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "title": "\u00c1tviteli \u00fcgyf\u00e9l be\u00e1ll\u00edt\u00e1sa" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/pl.json b/homeassistant/components/transmission/.translations/pl.json index a85a3f9b006..5aac538766b 100644 --- a/homeassistant/components/transmission/.translations/pl.json +++ b/homeassistant/components/transmission/.translations/pl.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z hostem", - "name_exists": "Nazwa ju\u017c istnieje", + "name_exists": "Nazwa ju\u017c istnieje.", "wrong_credentials": "Nieprawid\u0142owa nazwa u\u017cytkownika lub has\u0142o" }, "step": { diff --git a/homeassistant/components/transmission/.translations/sv.json b/homeassistant/components/transmission/.translations/sv.json index 30004af17db..b2a00771e85 100644 --- a/homeassistant/components/transmission/.translations/sv.json +++ b/homeassistant/components/transmission/.translations/sv.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "V\u00e4rden \u00e4r redan konfigurerad.", "one_instance_allowed": "Endast en enda instans \u00e4r n\u00f6dv\u00e4ndig." }, "error": { "cannot_connect": "Det g\u00e5r inte att ansluta till v\u00e4rden", + "name_exists": "Namnet finns redan", "wrong_credentials": "Fel anv\u00e4ndarnamn eller l\u00f6senord" }, "step": { @@ -21,16 +23,20 @@ "password": "L\u00f6senord", "port": "Port", "username": "Anv\u00e4ndarnamn" - } + }, + "title": "St\u00e4ll in Transmission-klienten" } - } + }, + "title": "Transmission" }, "options": { "step": { "init": { "data": { "scan_interval": "Uppdateringsfrekvens" - } + }, + "description": "Konfigurera alternativ f\u00f6r Transmission", + "title": "Konfigurera alternativ f\u00f6r Transmission" } } } diff --git a/homeassistant/components/transmission/const.py b/homeassistant/components/transmission/const.py index 9a9250dbed6..659ef97d9de 100644 --- a/homeassistant/components/transmission/const.py +++ b/homeassistant/components/transmission/const.py @@ -1,13 +1,16 @@ """Constants for the Transmission Bittorent Client component.""" + +from homeassistant.const import DATA_RATE_MEGABYTES_PER_SECOND + DOMAIN = "transmission" SENSOR_TYPES = { "active_torrents": ["Active Torrents", "Torrents"], "current_status": ["Status", None], - "download_speed": ["Down Speed", "MB/s"], + "download_speed": ["Down Speed", DATA_RATE_MEGABYTES_PER_SECOND], "paused_torrents": ["Paused Torrents", "Torrents"], "total_torrents": ["Total Torrents", "Torrents"], - "upload_speed": ["Up Speed", "MB/s"], + "upload_speed": ["Up Speed", DATA_RATE_MEGABYTES_PER_SECOND], "completed_torrents": ["Completed Torrents", "Torrents"], "started_torrents": ["Started Torrents", "Torrents"], } diff --git a/homeassistant/components/transmission/switch.py b/homeassistant/components/transmission/switch.py index 1756df7baee..3d85a76f2bd 100644 --- a/homeassistant/components/transmission/switch.py +++ b/homeassistant/components/transmission/switch.py @@ -80,7 +80,7 @@ class TransmissionSwitch(ToggleEntity): def turn_off(self, **kwargs): """Turn the device off.""" if self.type == "on_off": - _LOGGING.debug("Stoping all torrents") + _LOGGING.debug("Stopping all torrents") self._tm_client.api.stop_torrents() if self.type == "turtle_mode": _LOGGING.debug("Turning Turtle Mode of Transmission off") diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index 2b9e7a4eccf..2026816c090 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -2,7 +2,7 @@ "domain": "trend", "name": "Trend", "documentation": "https://www.home-assistant.io/integrations/trend", - "requirements": ["numpy==1.17.4"], + "requirements": ["numpy==1.18.1"], "dependencies": [], "codeowners": [], "quality_scale": "internal" diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 318101605e8..3a456dec531 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -22,7 +22,7 @@ from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SERVICE_PLAY_MEDIA, ) -from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM, ENTITY_MATCH_ALL +from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery @@ -90,7 +90,7 @@ SCHEMA_SERVICE_SAY = vol.Schema( { vol.Required(ATTR_MESSAGE): cv.string, vol.Optional(ATTR_CACHE): cv.boolean, - vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, + vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids, vol.Optional(ATTR_LANGUAGE): cv.string, vol.Optional(ATTR_OPTIONS): dict, } @@ -148,7 +148,7 @@ async def async_setup(hass, config): async def async_say_handle(service): """Service handle for say.""" - entity_ids = service.data.get(ATTR_ENTITY_ID, ENTITY_MATCH_ALL) + entity_ids = service.data[ATTR_ENTITY_ID] message = service.data.get(ATTR_MESSAGE) cache = service.data.get(ATTR_CACHE) language = service.data.get(ATTR_LANGUAGE) diff --git a/homeassistant/components/twentemilieu/.translations/pl.json b/homeassistant/components/twentemilieu/.translations/pl.json index 042fcf0dda6..130672906ef 100644 --- a/homeassistant/components/twentemilieu/.translations/pl.json +++ b/homeassistant/components/twentemilieu/.translations/pl.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "address_exists": "Adres ju\u017c skonfigurowany." + "address_exists": "Adres jest ju\u017c skonfigurowany." }, "error": { - "connection_error": "Po\u0142\u0105czenie nieudane.", + "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", "invalid_address": "Nie znaleziono adresu w obszarze us\u0142ugi Twente Milieu." }, "step": { "user": { "data": { - "house_letter": "List domowy / dodatkowy", + "house_letter": "List domowy/dodatkowy", "house_number": "Numer domu", "post_code": "Kod pocztowy" }, diff --git a/homeassistant/components/twentemilieu/.translations/sv.json b/homeassistant/components/twentemilieu/.translations/sv.json new file mode 100644 index 00000000000..ba2d8743681 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/sv.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_exists": "Adressen har redan st\u00e4llts in." + }, + "error": { + "connection_error": "Det gick inte att ansluta.", + "invalid_address": "Adress hittades inte i serviceomr\u00e5det Twente Milieu." + }, + "step": { + "user": { + "data": { + "house_letter": "Husbrev/till\u00e4gg", + "house_number": "Husnummer", + "post_code": "Postnummer" + }, + "description": "St\u00e4ll in Twente Milieu som ger information om avfallshantering p\u00e5 din adress.", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/twitch/sensor.py b/homeassistant/components/twitch/sensor.py index f4276160d6c..1bf66810e5b 100644 --- a/homeassistant/components/twitch/sensor.py +++ b/homeassistant/components/twitch/sensor.py @@ -6,6 +6,7 @@ from twitch import TwitchClient import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ATTR_FRIENDLY_NAME, CONF_TOKEN import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -13,6 +14,13 @@ _LOGGER = logging.getLogger(__name__) ATTR_GAME = "game" ATTR_TITLE = "title" +ATTR_SUBSCRIPTION = "subscribed" +ATTR_SUBSCRIPTION_SINCE = "subscribed_since" +ATTR_SUBSCRIPTION_GIFTED = "subscription_is_gifted" +ATTR_FOLLOW = "following" +ATTR_FOLLOW_SINCE = "following_since" +ATTR_FOLLOWING = "followers" +ATTR_VIEWS = "views" CONF_CHANNELS = "channels" CONF_CLIENT_ID = "client_id" @@ -26,6 +34,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_CLIENT_ID): cv.string, vol.Required(CONF_CHANNELS): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_TOKEN): cv.string, } ) @@ -34,29 +43,35 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Twitch platform.""" channels = config[CONF_CHANNELS] client_id = config[CONF_CLIENT_ID] - client = TwitchClient(client_id=client_id) + oauth_token = config.get(CONF_TOKEN) + client = TwitchClient(client_id, oauth_token) try: client.ingests.get_server_list() except HTTPError: - _LOGGER.error("Client ID is not valid") + _LOGGER.error("Client ID or OAuth token is not valid") return - users = client.users.translate_usernames_to_ids(channels) + channel_ids = client.users.translate_usernames_to_ids(channels) - add_entities([TwitchSensor(user, client) for user in users], True) + add_entities([TwitchSensor(channel_id, client) for channel_id in channel_ids], True) class TwitchSensor(Entity): """Representation of an Twitch channel.""" - def __init__(self, user, client): + def __init__(self, channel, client): """Initialize the sensor.""" self._client = client - self._user = user - self._channel = self._user.name - self._id = self._user.id - self._state = self._preview = self._game = self._title = None + self._channel = channel + self._oauth_enabled = client._oauth_token is not None + self._state = None + self._preview = None + self._game = None + self._title = None + self._subscription = None + self._follow = None + self._statistics = None @property def should_poll(self): @@ -66,7 +81,7 @@ class TwitchSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return self._channel + return self._channel.display_name @property def state(self): @@ -81,28 +96,67 @@ class TwitchSensor(Entity): @property def device_state_attributes(self): """Return the state attributes.""" + attr = { + ATTR_FRIENDLY_NAME: self._channel.display_name, + } + attr.update(self._statistics) + + if self._oauth_enabled: + attr.update(self._subscription) + attr.update(self._follow) + if self._state == STATE_STREAMING: - return {ATTR_GAME: self._game, ATTR_TITLE: self._title} + attr.update({ATTR_GAME: self._game, ATTR_TITLE: self._title}) + return attr @property def unique_id(self): """Return unique ID for this sensor.""" - return self._id + return self._channel.id @property def icon(self): """Icon to use in the frontend, if any.""" return ICON - # pylint: disable=no-member def update(self): """Update device state.""" - stream = self._client.streams.get_stream_by_user(self._id) + + channel = self._client.channels.get_by_id(self._channel.id) + + self._statistics = { + ATTR_FOLLOWING: channel.followers, + ATTR_VIEWS: channel.views, + } + if self._oauth_enabled: + user = self._client.users.get() + + try: + sub = self._client.users.check_subscribed_to_channel( + user.id, self._channel.id + ) + self._subscription = { + ATTR_SUBSCRIPTION: True, + ATTR_SUBSCRIPTION_SINCE: sub.created_at, + ATTR_SUBSCRIPTION_GIFTED: sub.is_gift, + } + except HTTPError: + self._subscription = {ATTR_SUBSCRIPTION: False} + + try: + follow = self._client.users.check_follows_channel( + user.id, self._channel.id + ) + self._follow = {ATTR_FOLLOW: True, ATTR_FOLLOW_SINCE: follow.created_at} + except HTTPError: + self._follow = {ATTR_FOLLOW: False} + + stream = self._client.streams.get_stream_by_user(self._channel.id) if stream: - self._game = stream.get("channel").get("game") - self._title = stream.get("channel").get("status") - self._preview = stream.get("preview").get("medium") + self._game = stream.channel.get("game") + self._title = stream.channel.get("status") + self._preview = stream.preview.get("medium") self._state = STATE_STREAMING else: - self._preview = self._client.users.get_by_id(self._id).get("logo") + self._preview = self._channel.logo self._state = STATE_OFFLINE diff --git a/homeassistant/components/unifi/.translations/ca.json b/homeassistant/components/unifi/.translations/ca.json index 899b532290e..89d299a2857 100644 --- a/homeassistant/components/unifi/.translations/ca.json +++ b/homeassistant/components/unifi/.translations/ca.json @@ -28,10 +28,13 @@ "device_tracker": { "data": { "detection_time": "Temps (en segons) des de s'ha vist per \u00faltima vegada fins que es considera a fora", + "ssid_filter": "Selecciona els SSID's on fer-hi el seguiment de clients", "track_clients": "Segueix clients de la xarxa", "track_devices": "Segueix dispositius de la xarxa (dispositius Ubiquiti)", "track_wired_clients": "Inclou clients de xarxa per cable" - } + }, + "description": "Configuraci\u00f3 de seguiment de dispositius", + "title": "Opcions d'UniFi" }, "init": { "data": { @@ -42,7 +45,9 @@ "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Crea sensors d'\u00fas d'ample de banda per a clients de la xarxa" - } + }, + "description": "Configuraci\u00f3 dels sensors d\u2019estad\u00edstiques", + "title": "Opcions d'UniFi" } } } diff --git a/homeassistant/components/unifi/.translations/da.json b/homeassistant/components/unifi/.translations/da.json index 46a94cc4047..1afd1ca96ce 100644 --- a/homeassistant/components/unifi/.translations/da.json +++ b/homeassistant/components/unifi/.translations/da.json @@ -28,10 +28,13 @@ "device_tracker": { "data": { "detection_time": "Tid i sekunder fra sidst set indtil betragtet som v\u00e6k", + "ssid_filter": "V\u00e6lg SSIDer, der skal spores tr\u00e5dl\u00f8se klienter p\u00e5", "track_clients": "Spor netv\u00e6rksklienter", "track_devices": "Spor netv\u00e6rksenheder (Ubiquiti-enheder)", "track_wired_clients": "Inkluder kablede netv\u00e6rksklienter" - } + }, + "description": "Konfigurer enhedssporing", + "title": "UniFi-indstillinger" }, "init": { "data": { @@ -41,8 +44,10 @@ }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "Opret b\u00e5ndbredde-forbrugssensorer for netv\u00e6rksklienter" - } + "allow_bandwidth_sensors": "B\u00e5ndbreddeforbrugssensorer for netv\u00e6rksklienter" + }, + "description": "Konfigurer statistiksensorer", + "title": "UniFi-indstillinger" } } } diff --git a/homeassistant/components/unifi/.translations/de.json b/homeassistant/components/unifi/.translations/de.json index 32a378b7c00..2f3db9d9b89 100644 --- a/homeassistant/components/unifi/.translations/de.json +++ b/homeassistant/components/unifi/.translations/de.json @@ -28,10 +28,13 @@ "device_tracker": { "data": { "detection_time": "Zeit in Sekunden vom letzten Gesehenen bis zur Entfernung", + "ssid_filter": "W\u00e4hlen Sie SSIDs zur Verfolgung von drahtlosen Clients aus", "track_clients": "Nachverfolgen von Netzwerkclients", "track_devices": "Verfolgen von Netzwerkger\u00e4ten (Ubiquiti-Ger\u00e4te)", "track_wired_clients": "Einbinden von kabelgebundenen Netzwerk-Clients" - } + }, + "description": "Konfigurieren Sie die Ger\u00e4teverfolgung", + "title": "UniFi-Optionen" }, "init": { "data": { @@ -42,7 +45,9 @@ "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Erstellen von Bandbreiten-Nutzungssensoren f\u00fcr Netzwerk-Clients" - } + }, + "description": "Konfigurieren Sie Statistiksensoren", + "title": "UniFi-Optionen" } } } diff --git a/homeassistant/components/unifi/.translations/en.json b/homeassistant/components/unifi/.translations/en.json index d9b65b6d1da..f1f96b3c363 100644 --- a/homeassistant/components/unifi/.translations/en.json +++ b/homeassistant/components/unifi/.translations/en.json @@ -28,15 +28,20 @@ "device_tracker": { "data": { "detection_time": "Time in seconds from last seen until considered away", + "ssid_filter": "Select SSIDs to track wireless clients on", "track_clients": "Track network clients", "track_devices": "Track network devices (Ubiquiti devices)", "track_wired_clients": "Include wired network clients" - } + }, + "description": "Configure device tracking", + "title": "UniFi options" }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "Create bandwidth usage sensors for network clients" - } + "allow_bandwidth_sensors": "Bandwidth usage sensors for network clients" + }, + "description": "Configure statistics sensors", + "title": "UniFi options" } } } diff --git a/homeassistant/components/unifi/.translations/es.json b/homeassistant/components/unifi/.translations/es.json index 677899c0958..6c5e9d677c2 100644 --- a/homeassistant/components/unifi/.translations/es.json +++ b/homeassistant/components/unifi/.translations/es.json @@ -28,10 +28,13 @@ "device_tracker": { "data": { "detection_time": "Tiempo en segundos desde la \u00faltima vez que se vio hasta considerarlo desconectado", + "ssid_filter": "Seleccione los SSIDs para realizar seguimiento de clientes inal\u00e1mbricos", "track_clients": "Seguimiento de los clientes de red", "track_devices": "Rastree dispositivos de red (dispositivos Ubiquiti)", "track_wired_clients": "Incluir clientes de red cableada" - } + }, + "description": "Configurar dispositivo de seguimiento", + "title": "Opciones UniFi" }, "init": { "data": { @@ -42,7 +45,9 @@ "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Crear sensores para monitorizar uso de ancho de banda de clientes de red" - } + }, + "description": "Configurar estad\u00edsticas de los sensores", + "title": "Opciones UniFi" } } } diff --git a/homeassistant/components/unifi/.translations/hu.json b/homeassistant/components/unifi/.translations/hu.json index b927e652ba7..f6919f985dc 100644 --- a/homeassistant/components/unifi/.translations/hu.json +++ b/homeassistant/components/unifi/.translations/hu.json @@ -21,5 +21,14 @@ } }, "title": "UniFi Vez\u00e9rl\u0151" + }, + "options": { + "step": { + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "S\u00e1vsz\u00e9less\u00e9g-haszn\u00e1lati \u00e9rz\u00e9kel\u0151k l\u00e9trehoz\u00e1sa a h\u00e1l\u00f3zati \u00fcgyfelek sz\u00e1m\u00e1ra" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/it.json b/homeassistant/components/unifi/.translations/it.json index 80b546ebcf8..c1aa9afe54f 100644 --- a/homeassistant/components/unifi/.translations/it.json +++ b/homeassistant/components/unifi/.translations/it.json @@ -28,10 +28,13 @@ "device_tracker": { "data": { "detection_time": "Tempo in secondi dall'ultima volta che viene visto fino a quando non \u00e8 considerato lontano", + "ssid_filter": "Selezionare gli SSID su cui tracciare i client wireless", "track_clients": "Traccia i client di rete", "track_devices": "Tracciare i dispositivi di rete (dispositivi Ubiquiti)", "track_wired_clients": "Includi i client di rete cablata" - } + }, + "description": "Configurare il tracciamento del dispositivo", + "title": "Opzioni UniFi" }, "init": { "data": { @@ -41,8 +44,10 @@ }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "Creare sensori di utilizzo della larghezza di banda per i client di rete" - } + "allow_bandwidth_sensors": "Sensori di utilizzo della larghezza di banda per i client di rete" + }, + "description": "Configurare i sensori delle statistiche", + "title": "Opzioni UniFi" } } } diff --git a/homeassistant/components/unifi/.translations/ko.json b/homeassistant/components/unifi/.translations/ko.json index 295430b7284..dbcd4d7feee 100644 --- a/homeassistant/components/unifi/.translations/ko.json +++ b/homeassistant/components/unifi/.translations/ko.json @@ -28,15 +28,20 @@ "device_tracker": { "data": { "detection_time": "\ub9c8\uc9c0\ub9c9\uc73c\ub85c \ud655\uc778\ub41c \uc2dc\uac04\ubd80\ud130 \uc678\ucd9c \uc0c1\ud0dc\ub85c \uac04\uc8fc\ub418\ub294 \uc2dc\uac04 (\ucd08)", + "ssid_filter": "\ubb34\uc120 \ud074\ub77c\uc774\uc5b8\ud2b8\ub97c \ucd94\uc801\ud558\ub824\uba74 SSID\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694", "track_clients": "\ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8 \ucd94\uc801 \ub300\uc0c1", "track_devices": "\ub124\ud2b8\uc6cc\ud06c \uae30\uae30 \ucd94\uc801 (Ubiquiti \uae30\uae30)", "track_wired_clients": "\uc720\uc120 \ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8 \ud3ec\ud568" - } + }, + "description": "\uc7a5\uce58 \ucd94\uc801 \uad6c\uc131", + "title": "UniFi \uc635\uc158" }, "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" - } + "allow_bandwidth_sensors": "\ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8 \ub300\uc5ed\ud3ed \uc0ac\uc6a9\ub7c9 \uc13c\uc11c" + }, + "description": "\ud1b5\uacc4 \uc13c\uc11c \uad6c\uc131", + "title": "UniFi \uc635\uc158" } } } diff --git a/homeassistant/components/unifi/.translations/lb.json b/homeassistant/components/unifi/.translations/lb.json index 4fa1f62c602..9707432540d 100644 --- a/homeassistant/components/unifi/.translations/lb.json +++ b/homeassistant/components/unifi/.translations/lb.json @@ -31,7 +31,8 @@ "track_clients": "Netzwierk Cliente verfollegen", "track_devices": "Netzwierk Apparater (Ubiquiti Apparater) verfollegen", "track_wired_clients": "Kabel Netzwierk Cliente abez\u00e9ien" - } + }, + "title": "UniFi Optiounen" }, "init": { "data": { @@ -42,7 +43,9 @@ "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Bandbreet Benotzung Sensore fir Netzwierk Cliente erstellen" - } + }, + "description": "Statistik Sensoren konfigur\u00e9ieren", + "title": "UniFi Optiounen" } } } diff --git a/homeassistant/components/unifi/.translations/no.json b/homeassistant/components/unifi/.translations/no.json index 9041f018423..65730c7ab8b 100644 --- a/homeassistant/components/unifi/.translations/no.json +++ b/homeassistant/components/unifi/.translations/no.json @@ -28,21 +28,20 @@ "device_tracker": { "data": { "detection_time": "Tid i sekunder fra sist sett til den ble ansett borte", + "ssid_filter": "Velg SSID-er for \u00e5 spore tr\u00e5dl\u00f8se klienter p\u00e5", "track_clients": "Spor nettverksklienter", "track_devices": "Spore nettverksenheter (Ubiquiti-enheter)", "track_wired_clients": "Inkluder kablede nettverksklienter" - } - }, - "init": { - "data": { - "one": "en", - "other": "andre" - } + }, + "description": "Konfigurere enhetssporing", + "title": "UniFi-alternativer" }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "Opprett b\u00e5ndbreddesensorer for nettverksklienter" - } + "allow_bandwidth_sensors": "B\u00e5ndbreddebrukssensorer for nettverksklienter" + }, + "description": "Konfigurer statistikk sensorer", + "title": "UniFi-alternativer" } } } diff --git a/homeassistant/components/unifi/.translations/pl.json b/homeassistant/components/unifi/.translations/pl.json index 5887460a8a5..e016fbc7cce 100644 --- a/homeassistant/components/unifi/.translations/pl.json +++ b/homeassistant/components/unifi/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Witryna kontrolera jest ju\u017c skonfigurowana", + "already_configured": "Witryna kontrolera jest ju\u017c skonfigurowana.", "user_privilege": "U\u017cytkownik musi by\u0107 administratorem" }, "error": { @@ -28,10 +28,13 @@ "device_tracker": { "data": { "detection_time": "Czas w sekundach od momentu, kiedy ostatnio widziano, a\u017c do momentu, kiedy uznano go za nieobecny.", + "ssid_filter": "Wybierz SSIDy do \u015bledzenia klient\u00f3w bezprzewodowych", "track_clients": "\u015aled\u017a klient\u00f3w sieciowych", "track_devices": "\u015aled\u017a urz\u0105dzenia sieciowe (urz\u0105dzenia Ubiquiti)", "track_wired_clients": "Uwzgl\u0119dnij klient\u00f3w sieci przewodowej" - } + }, + "description": "Konfiguracja \u015bledzenia urz\u0105dze\u0144", + "title": "Opcje UniFi" }, "init": { "data": { @@ -44,7 +47,9 @@ "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Stw\u00f3rz sensory wykorzystania przepustowo\u015bci przez klient\u00f3w sieciowych" - } + }, + "description": "Konfiguracja sensora statystyk", + "title": "Opcje UniFi" } } } diff --git a/homeassistant/components/unifi/.translations/ru.json b/homeassistant/components/unifi/.translations/ru.json index 3a67d483c0c..0080474cf64 100644 --- a/homeassistant/components/unifi/.translations/ru.json +++ b/homeassistant/components/unifi/.translations/ru.json @@ -28,10 +28,13 @@ "device_tracker": { "data": { "detection_time": "\u0412\u0440\u0435\u043c\u044f \u043e\u0442 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0433\u043e \u0441\u0435\u0430\u043d\u0441\u0430 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c (\u0441\u0435\u043a.), \u043f\u043e \u0438\u0441\u0442\u0435\u0447\u0435\u043d\u0438\u044e \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u0442\u0430\u0442\u0443\u0441 \"\u041d\u0435 \u0434\u043e\u043c\u0430\".", + "ssid_filter": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 SSID \u0434\u043b\u044f \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u044f \u0431\u0435\u0441\u043f\u0440\u043e\u0432\u043e\u0434\u043d\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432", "track_clients": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u0441\u0435\u0442\u0438", "track_devices": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u0435 \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 (\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Ubiquiti)", "track_wired_clients": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043f\u0440\u043e\u0432\u043e\u0434\u043d\u043e\u0439 \u0441\u0435\u0442\u0438" - } + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 UniFi" }, "init": { "data": { @@ -43,8 +46,10 @@ }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "\u0421\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043f\u043e\u043b\u043e\u0441\u044b \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u043d\u0438\u044f \u0434\u043b\u044f \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432" - } + "allow_bandwidth_sensors": "\u0421\u0435\u043d\u0441\u043e\u0440\u044b \u043f\u043e\u043b\u043e\u0441\u044b \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u043d\u0438\u044f \u0434\u043b\u044f \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432 \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0438", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 UniFi" } } } diff --git a/homeassistant/components/unifi/.translations/sv.json b/homeassistant/components/unifi/.translations/sv.json index bc1d9f8cb72..dbf5373aa9a 100644 --- a/homeassistant/components/unifi/.translations/sv.json +++ b/homeassistant/components/unifi/.translations/sv.json @@ -25,11 +25,24 @@ }, "options": { "step": { + "device_tracker": { + "data": { + "detection_time": "Tid i sekunder fr\u00e5n senast sett tills den anses borta", + "track_clients": "Sp\u00e5ra n\u00e4tverksklienter", + "track_devices": "Sp\u00e5ra n\u00e4tverksenheter (Ubiquiti-enheter)", + "track_wired_clients": "Inkludera tr\u00e5dbundna n\u00e4tverksklienter" + } + }, "init": { "data": { "one": "Tom", "other": "Tomma" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Skapa bandbreddsanv\u00e4ndningssensorer f\u00f6r n\u00e4tverksklienter" + } } } } diff --git a/homeassistant/components/unifi/.translations/zh-Hans.json b/homeassistant/components/unifi/.translations/zh-Hans.json index 2bc6bda37e4..ebed653732f 100644 --- a/homeassistant/components/unifi/.translations/zh-Hans.json +++ b/homeassistant/components/unifi/.translations/zh-Hans.json @@ -28,10 +28,17 @@ "device_tracker": { "data": { "detection_time": "\u8ddd\u79bb\u4e0a\u6b21\u53d1\u73b0\u591a\u5c11\u79d2\u540e\u8ba4\u4e3a\u79bb\u5f00", + "ssid_filter": "\u9009\u62e9\u6240\u8981\u8ffd\u8e2a\u7684\u65e0\u7ebf\u7f51\u7edcSSID", "track_clients": "\u8ddf\u8e2a\u7f51\u7edc\u5ba2\u6237\u7aef", "track_devices": "\u8ddf\u8e2a\u7f51\u7edc\u8bbe\u5907\uff08Ubiquiti \u8bbe\u5907\uff09", "track_wired_clients": "\u5305\u62ec\u6709\u7ebf\u7f51\u7edc\u5ba2\u6237\u7aef" - } + }, + "description": "\u914d\u7f6e\u8bbe\u5907\u8ddf\u8e2a", + "title": "UniFi \u9009\u9879" + }, + "statistics_sensors": { + "description": "\u914d\u7f6e\u7edf\u8ba1\u4f20\u611f\u5668", + "title": "UniFi \u9009\u9879" } } } diff --git a/homeassistant/components/unifi/.translations/zh-Hant.json b/homeassistant/components/unifi/.translations/zh-Hant.json index 5e0b881af15..cce150a6765 100644 --- a/homeassistant/components/unifi/.translations/zh-Hant.json +++ b/homeassistant/components/unifi/.translations/zh-Hant.json @@ -28,15 +28,20 @@ "device_tracker": { "data": { "detection_time": "\u6700\u7d42\u51fa\u73fe\u5f8c\u8996\u70ba\u96e2\u958b\u7684\u6642\u9593\uff08\u4ee5\u79d2\u70ba\u55ae\u4f4d\uff09", + "ssid_filter": "\u9078\u64c7\u6240\u8981\u8ffd\u8e64\u7684\u7121\u7dda\u7db2\u8def", "track_clients": "\u8ffd\u8e64\u7db2\u8def\u5ba2\u6236\u7aef", "track_devices": "\u8ffd\u8e64\u7db2\u8def\u8a2d\u5099\uff08Ubiquiti \u8a2d\u5099\uff09", "track_wired_clients": "\u5305\u542b\u6709\u7dda\u7db2\u8def\u5ba2\u6236\u7aef" - } + }, + "description": "\u8a2d\u5b9a\u8a2d\u5099\u8ffd\u8e64", + "title": "UniFi \u9078\u9805" }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "\u65b0\u589e\u7db2\u8def\u5ba2\u6236\u7aef\u983b\u5bec\u7528\u91cf\u611f\u61c9\u5668" - } + "allow_bandwidth_sensors": "\u7db2\u8def\u5ba2\u6236\u7aef\u983b\u5bec\u7528\u91cf\u611f\u61c9\u5668" + }, + "description": "\u8a2d\u5b9a\u7d71\u8a08\u6578\u64da\u611f\u61c9\u5668", + "title": "UniFi \u9078\u9805" } } } diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 65015b357a7..a21ae4ed508 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -1,7 +1,7 @@ """Support for devices connected to UniFi POE.""" import voluptuous as vol -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC @@ -96,6 +96,8 @@ async def async_setup_entry(hass, config_entry): # sw_version=config.raw['swversion'], ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, controller.shutdown) + return True diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 52ecab08856..36fa7489e81 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -1,4 +1,6 @@ """Config flow for UniFi.""" +import socket + import voluptuous as vol from homeassistant import config_entries @@ -10,21 +12,18 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from .const import ( CONF_ALLOW_BANDWIDTH_SENSORS, CONF_CONTROLLER, CONF_DETECTION_TIME, CONF_SITE_ID, + CONF_SSID_FILTER, CONF_TRACK_CLIENTS, CONF_TRACK_DEVICES, CONF_TRACK_WIRED_CLIENTS, CONTROLLER_ID, - DEFAULT_ALLOW_BANDWIDTH_SENSORS, - DEFAULT_DETECTION_TIME, - DEFAULT_TRACK_CLIENTS, - DEFAULT_TRACK_DEVICES, - DEFAULT_TRACK_WIRED_CLIENTS, DOMAIN, LOGGER, ) @@ -104,11 +103,15 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_abort(reason="unknown") + host = "" + if await async_discover_unifi(self.hass): + host = "unifi" + return self.async_show_form( step_id="user", data_schema=vol.Schema( { - vol.Required(CONF_HOST): str, + vol.Required(CONF_HOST, default=host): str, vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, vol.Optional(CONF_PORT, default=DEFAULT_PORT): int, @@ -179,33 +182,30 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): self.options.update(user_input) return await self.async_step_statistics_sensors() + controller = get_controller_from_config_entry(self.hass, self.config_entry) + + ssid_filter = {wlan: wlan for wlan in controller.api.wlans} + return self.async_show_form( step_id="device_tracker", data_schema=vol.Schema( { vol.Optional( - CONF_TRACK_CLIENTS, - default=self.config_entry.options.get( - CONF_TRACK_CLIENTS, DEFAULT_TRACK_CLIENTS - ), + CONF_TRACK_CLIENTS, default=controller.option_track_clients, ): bool, vol.Optional( CONF_TRACK_WIRED_CLIENTS, - default=self.config_entry.options.get( - CONF_TRACK_WIRED_CLIENTS, DEFAULT_TRACK_WIRED_CLIENTS - ), + default=controller.option_track_wired_clients, ): bool, vol.Optional( - CONF_TRACK_DEVICES, - default=self.config_entry.options.get( - CONF_TRACK_DEVICES, DEFAULT_TRACK_DEVICES - ), + CONF_TRACK_DEVICES, default=controller.option_track_devices, ): bool, + vol.Optional( + CONF_SSID_FILTER, default=controller.option_ssid_filter + ): cv.multi_select(ssid_filter), vol.Optional( CONF_DETECTION_TIME, - default=self.config_entry.options.get( - CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME - ), + default=int(controller.option_detection_time.total_seconds()), ): int, } ), @@ -217,16 +217,15 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): self.options.update(user_input) return await self._update_options() + controller = get_controller_from_config_entry(self.hass, self.config_entry) + 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, - ), + default=controller.option_allow_bandwidth_sensors, ): bool } ), @@ -235,3 +234,11 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): async def _update_options(self): """Update config entry options.""" return self.async_create_entry(title="", data=self.options) + + +async def async_discover_unifi(hass): + """Discover UniFi address.""" + try: + return await hass.async_add_executor_job(socket.gethostbyname, "unifi") + except socket.gaierror: + return None diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 826491f6ba6..b7cd8e8b6a1 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -5,9 +5,13 @@ import ssl from aiohttp import CookieJar import aiounifi +from aiounifi.controller import SIGNAL_CONNECTION_STATE +from aiounifi.events import WIRELESS_CLIENT_CONNECTED, WIRELESS_GUEST_CONNECTED +from aiounifi.websocket import STATE_DISCONNECTED, STATE_RUNNING import async_timeout from homeassistant.const import CONF_HOST +from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -40,6 +44,7 @@ from .const import ( ) from .errors import AuthenticationRequired, CannotConnect +RETRY_TIMER = 15 SUPPORTED_PLATFORMS = ["device_tracker", "sensor", "switch"] @@ -59,6 +64,11 @@ class UniFiController: self._site_name = None self._site_role = None + @property + def controller_id(self): + """Return the controller ID.""" + return CONTROLLER_ID.format(host=self.host, site=self.site) + @property def host(self): """Return the host of this controller.""" @@ -130,15 +140,47 @@ class UniFiController: return client.mac return None + @callback + def async_unifi_signalling_callback(self, signal, data): + """Handle messages back from UniFi library.""" + if signal == SIGNAL_CONNECTION_STATE: + + if data == STATE_DISCONNECTED and self.available: + LOGGER.error("Lost connection to UniFi") + + if (data == STATE_RUNNING and not self.available) or ( + data == STATE_DISCONNECTED and self.available + ): + self.available = data == STATE_RUNNING + async_dispatcher_send(self.hass, self.signal_reachable) + + if not self.available: + self.hass.loop.call_later(RETRY_TIMER, self.reconnect) + + elif signal == "new_data" and data: + if "event" in data: + if data["event"].event in ( + WIRELESS_CLIENT_CONNECTED, + WIRELESS_GUEST_CONNECTED, + ): + self.update_wireless_clients() + elif data.get("clients") or data.get("devices"): + async_dispatcher_send(self.hass, self.signal_update) + + @property + def signal_reachable(self) -> str: + """Integration specific event to signal a change in connection status.""" + return f"unifi-reachable-{self.controller_id}" + @property def signal_update(self): """Event specific per UniFi entry to signal new data.""" - return f"unifi-update-{CONTROLLER_ID.format(host=self.host, site=self.site)}" + return f"unifi-update-{self.controller_id}" @property def signal_options_update(self): """Event specific per UniFi entry to signal new options.""" - return f"unifi-options-{CONTROLLER_ID.format(host=self.host, site=self.site)}" + return f"unifi-options-{self.controller_id}" def update_wireless_clients(self): """Update set of known to be wireless clients.""" @@ -156,59 +198,13 @@ class UniFiController: unifi_wireless_clients = self.hass.data[UNIFI_WIRELESS_CLIENTS] unifi_wireless_clients.update_data(self.wireless_clients, self.config_entry) - async def request_update(self): - """Request an update.""" - if self.progress is not None: - return await self.progress - - self.progress = self.hass.async_create_task(self.async_update()) - await self.progress - - self.progress = None - - async def async_update(self): - """Update UniFi controller information.""" - failed = False - - try: - with async_timeout.timeout(10): - await self.api.clients.update() - await self.api.devices.update() - if self.option_block_clients: - await self.api.clients_all.update() - - except aiounifi.LoginRequired: - try: - with async_timeout.timeout(5): - await self.api.login() - - except (asyncio.TimeoutError, aiounifi.AiounifiException): - failed = True - if self.available: - LOGGER.error("Unable to reach controller %s", self.host) - self.available = False - - except (asyncio.TimeoutError, aiounifi.AiounifiException): - failed = True - if self.available: - LOGGER.error("Unable to reach controller %s", self.host) - self.available = False - - if not failed and not self.available: - LOGGER.info("Reconnected to controller %s", self.host) - self.available = True - - self.update_wireless_clients() - - async_dispatcher_send(self.hass, self.signal_update) - async def async_setup(self): """Set up a UniFi controller.""" - hass = self.hass - try: self.api = await get_controller( - self.hass, **self.config_entry.data[CONF_CONTROLLER] + self.hass, + **self.config_entry.data[CONF_CONTROLLER], + async_callback=self.async_unifi_signalling_callback, ) await self.api.initialize() @@ -227,26 +223,28 @@ class UniFiController: LOGGER.error("Unknown error connecting with UniFi controller: %s", err) return False - wireless_clients = hass.data[UNIFI_WIRELESS_CLIENTS] + wireless_clients = self.hass.data[UNIFI_WIRELESS_CLIENTS] self.wireless_clients = wireless_clients.get_data(self.config_entry) self.update_wireless_clients() self.import_configuration() - self.config_entry.add_update_listener(self.async_options_updated) - for platform in SUPPORTED_PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup( + self.hass.async_create_task( + self.hass.config_entries.async_forward_entry_setup( self.config_entry, platform ) ) + self.api.start_websocket() + + self.config_entry.add_update_listener(self.async_config_entry_updated) + return True @staticmethod - async def async_options_updated(hass, entry): - """Triggered by config entry options updates.""" + async def async_config_entry_updated(hass, entry) -> None: + """Handle signals of config entry being updated.""" controller_id = CONTROLLER_ID.format( host=entry.data[CONF_CONTROLLER][CONF_HOST], site=entry.data[CONF_CONTROLLER][CONF_SITE_ID], @@ -279,7 +277,6 @@ class UniFiController: (CONF_SSID_FILTER, CONF_SSID_FILTER), ): if config in import_config: - print(config) if config == option and import_config[ config ] != self.config_entry.options.get(option): @@ -296,12 +293,38 @@ class UniFiController: self.config_entry, options=options ) + @callback + def reconnect(self) -> None: + """Prepare to reconnect UniFi session.""" + LOGGER.debug("Reconnecting to UniFi in %i", RETRY_TIMER) + self.hass.loop.create_task(self.async_reconnect()) + + async def async_reconnect(self) -> None: + """Try to reconnect UniFi session.""" + try: + with async_timeout.timeout(5): + await self.api.login() + self.api.start_websocket() + + except (asyncio.TimeoutError, aiounifi.AiounifiException): + self.hass.loop.call_later(RETRY_TIMER, self.reconnect) + + @callback + def shutdown(self, event) -> None: + """Wrap the call to unifi.close. + + Used as an argument to EventBus.async_listen_once. + """ + self.api.stop_websocket() + async def async_reset(self): """Reset this controller to default state. Will cancel any scheduled setup retry and will unload the config entry. """ + self.api.stop_websocket() + for platform in SUPPORTED_PLATFORMS: await self.hass.config_entries.async_forward_entry_unload( self.config_entry, platform @@ -314,7 +337,9 @@ class UniFiController: return True -async def get_controller(hass, host, username, password, port, site, verify_ssl): +async def get_controller( + hass, host, username, password, port, site, verify_ssl, async_callback=None +): """Create a controller object and verify authentication.""" sslcontext = None @@ -335,6 +360,7 @@ async def get_controller(hass, host, username, password, port, site, verify_ssl) site=site, websession=session, sslcontext=sslcontext, + callback=async_callback, ) try: diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 8b45a0f227b..5dd5f0c83ae 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -1,19 +1,17 @@ """Track devices using UniFi controllers.""" import logging -from pprint import pformat from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.components.device_tracker.const import SOURCE_TYPE_ROUTER from homeassistant.components.unifi.config_flow import get_controller_from_config_entry from homeassistant.core import callback -from homeassistant.helpers import entity_registry from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_registry import DISABLED_CONFIG_ENTRY import homeassistant.util.dt as dt_util from .const import ATTR_MANUFACTURER +from .unifi_client import UniFiClient LOGGER = logging.getLogger(__name__) @@ -43,7 +41,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): controller = get_controller_from_config_entry(hass, config_entry) tracked = {} - registry = await entity_registry.async_get_registry(hass) + option_track_clients = controller.option_track_clients + option_track_devices = controller.option_track_devices + option_track_wired_clients = controller.option_track_wired_clients + + registry = await hass.helpers.entity_registry.async_get_registry() # Restore clients that is not a part of active clients list. for entity in registry.entities.values(): @@ -65,31 +67,72 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def update_controller(): """Update the values of the controller.""" - update_items(controller, async_add_entities, tracked) + nonlocal option_track_clients + nonlocal option_track_devices + + if not option_track_clients and not option_track_devices: + return + + add_entities(controller, async_add_entities, tracked) 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 tracked.values(): + def options_updated(): + """Manage entities affected by config entry options.""" + nonlocal option_track_clients + nonlocal option_track_devices + nonlocal option_track_wired_clients - if entity.entity_registry_enabled_default == entity.enabled: + update = False + remove = set() + + for current_option, config_entry_option, tracker_class in ( + (option_track_clients, controller.option_track_clients, UniFiClientTracker), + (option_track_devices, controller.option_track_devices, UniFiDeviceTracker), + ): + if current_option == config_entry_option: continue - disabled_by = None - if not entity.entity_registry_enabled_default and entity.enabled: - disabled_by = DISABLED_CONFIG_ENTRY + if config_entry_option: + update = True + else: + for mac, entity in tracked.items(): + if isinstance(entity, tracker_class): + remove.add(mac) - registry.async_update_entity( - entity.registry_entry.entity_id, disabled_by=disabled_by - ) + if ( + controller.option_track_clients + and option_track_wired_clients != controller.option_track_wired_clients + ): + + if controller.option_track_wired_clients: + update = True + else: + for mac, entity in tracked.items(): + if isinstance(entity, UniFiClientTracker) and entity.is_wired: + remove.add(mac) + + option_track_clients = controller.option_track_clients + option_track_devices = controller.option_track_devices + option_track_wired_clients = controller.option_track_wired_clients + + for mac in remove: + entity = tracked.pop(mac) + + if registry.async_is_registered(entity.entity_id): + registry.async_remove(entity.entity_id) + + hass.async_create_task(entity.async_remove()) + + if update: + update_controller() controller.listeners.append( async_dispatcher_connect( - hass, controller.signal_options_update, update_disable_on_entities + hass, controller.signal_options_update, options_updated ) ) @@ -97,20 +140,25 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback -def update_items(controller, async_add_entities, tracked): - """Update tracked device state from the controller.""" +def add_entities(controller, async_add_entities, tracked): + """Add new tracker entities from the controller.""" new_tracked = [] - for items, tracker_class in ( - (controller.api.clients, UniFiClientTracker), - (controller.api.devices, UniFiDeviceTracker), + for items, tracker_class, track in ( + (controller.api.clients, UniFiClientTracker, controller.option_track_clients), + (controller.api.devices, UniFiDeviceTracker, controller.option_track_devices), ): + if not track: + continue for item_id in items: if item_id in tracked: - if tracked[item_id].enabled: - tracked[item_id].async_schedule_update_ha_state() + continue + + if tracker_class is UniFiClientTracker and ( + not controller.option_track_wired_clients and items[item_id].is_wired + ): continue tracked[item_id] = tracker_class(items[item_id], controller) @@ -120,25 +168,24 @@ def update_items(controller, async_add_entities, tracked): async_add_entities(new_tracked) -class UniFiClientTracker(ScannerEntity): +class UniFiClientTracker(UniFiClient, ScannerEntity): """Representation of a network client.""" def __init__(self, client, controller): """Set up tracked client.""" - self.client = client - self.controller = controller - self.is_wired = self.client.mac not in controller.wireless_clients - self.wired_bug = None + super().__init__(client, controller) + self.wired_bug = None if self.is_wired != self.client.is_wired: self.wired_bug = dt_util.utcnow() - self.controller.option_detection_time @property - def entity_registry_enabled_default(self): - """Return if the entity should be enabled when first added to the entity registry.""" - if not self.controller.option_track_clients: - return False + def is_connected(self): + """Return true if the client is connected to the network. + If connected to unwanted ssid return False. + If is_wired and client.is_wired differ it means that the device is offline and UniFi bug shows device as wired. + """ if ( not self.is_wired and self.controller.option_ssid_filter @@ -146,37 +193,6 @@ class UniFiClientTracker(ScannerEntity): ): return False - if not self.controller.option_track_wired_clients and self.is_wired: - return False - - return True - - async def async_added_to_hass(self): - """Client entity created.""" - LOGGER.debug("New UniFi client tracker %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. - """ - await self.controller.request_update() - - if self.is_wired and self.client.mac in self.controller.wireless_clients: - self.is_wired = False - - LOGGER.debug( - "Updating UniFi tracked client %s\n%s", - self.entity_id, - pformat(self.client.raw), - ) - - @property - def is_connected(self): - """Return true if the client is connected to the network. - - If is_wired and client.is_wired differ it means that the device is offline and UniFi bug shows device as wired. - """ if self.is_wired != self.client.is_wired: if not self.wired_bug: self.wired_bug = dt_util.utcnow() @@ -198,26 +214,11 @@ class UniFiClientTracker(ScannerEntity): """Return the source type of the client.""" return SOURCE_TYPE_ROUTER - @property - def name(self) -> str: - """Return the name of the client.""" - return self.client.name or self.client.hostname - @property def unique_id(self) -> str: """Return a unique identifier for this client.""" return f"{self.client.mac}-{self.controller.site}" - @property - def available(self) -> bool: - """Return if controller is available.""" - return self.controller.available - - @property - def device_info(self): - """Return a client description for device registry.""" - return {"connections": {(CONNECTION_NETWORK_MAC, self.client.mac)}} - @property def device_state_attributes(self): """Return the client state attributes.""" @@ -239,28 +240,31 @@ class UniFiDeviceTracker(ScannerEntity): """Set up tracked device.""" self.device = device self.controller = controller - - @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_track_devices: - return True - return False + self.listeners = [] async def async_added_to_hass(self): """Subscribe to device events.""" LOGGER.debug("New UniFi device tracker %s (%s)", self.name, self.device.mac) - - async def async_update(self): - """Synchronize state with controller.""" - await self.controller.request_update() - - LOGGER.debug( - "Updating UniFi tracked device %s\n%s", - self.entity_id, - pformat(self.device.raw), + self.device.register_callback(self.async_update_callback) + self.listeners.append( + async_dispatcher_connect( + self.hass, self.controller.signal_reachable, self.async_update_callback + ) ) + async def async_will_remove_from_hass(self) -> None: + """Disconnect device object when removed.""" + self.device.remove_callback(self.async_update_callback) + for unsub_dispatcher in self.listeners: + unsub_dispatcher() + + @callback + def async_update_callback(self): + """Update the sensor's state.""" + LOGGER.debug("Updating UniFi tracked device %s", self.entity_id) + + self.async_schedule_update_ha_state() + @property def is_connected(self): """Return true if the device is connected to the network.""" @@ -325,3 +329,8 @@ class UniFiDeviceTracker(ScannerEntity): attributes["upgradable"] = self.device.upgradable return attributes + + @property + def should_poll(self): + """No polling needed.""" + return False diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index e2bcd5b68a5..a42b136e665 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -3,8 +3,12 @@ "name": "Ubiquiti UniFi", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifi", - "requirements": ["aiounifi==11"], + "requirements": [ + "aiounifi==13" + ], "dependencies": [], - "codeowners": ["@kane610"], + "codeowners": [ + "@kane610" + ], "quality_scale": "platinum" -} +} \ No newline at end of file diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 9145fd8e00f..942b0ef6779 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -3,11 +3,9 @@ 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 + +from .unifi_client import UniFiClient LOGGER = logging.getLogger(__name__) @@ -24,36 +22,48 @@ async def async_setup_entry(hass, config_entry, async_add_entities): controller = get_controller_from_config_entry(hass, config_entry) sensors = {} - registry = await entity_registry.async_get_registry(hass) + option_allow_bandwidth_sensors = controller.option_allow_bandwidth_sensors + + entity_registry = await hass.helpers.entity_registry.async_get_registry() @callback def update_controller(): """Update the values of the controller.""" - update_items(controller, async_add_entities, sensors) + nonlocal option_allow_bandwidth_sensors + + if not option_allow_bandwidth_sensors: + return + + add_entities(controller, async_add_entities, sensors) controller.listeners.append( async_dispatcher_connect(hass, controller.signal_update, update_controller) ) @callback - def update_disable_on_entities(): + def options_updated(): """Update the values of the controller.""" - for entity in sensors.values(): + nonlocal option_allow_bandwidth_sensors - if entity.entity_registry_enabled_default == entity.enabled: - continue + if option_allow_bandwidth_sensors != controller.option_allow_bandwidth_sensors: + option_allow_bandwidth_sensors = controller.option_allow_bandwidth_sensors - disabled_by = None - if not entity.entity_registry_enabled_default and entity.enabled: - disabled_by = DISABLED_CONFIG_ENTRY + if option_allow_bandwidth_sensors: + update_controller() - registry.async_update_entity( - entity.registry_entry.entity_id, disabled_by=disabled_by - ) + else: + for sensor in sensors.values(): + + if entity_registry.async_is_registered(sensor.entity_id): + entity_registry.async_remove(sensor.entity_id) + + hass.async_create_task(sensor.async_remove()) + + sensors.clear() controller.listeners.append( async_dispatcher_connect( - hass, controller.signal_options_update, update_disable_on_entities + hass, controller.signal_options_update, options_updated ) ) @@ -61,8 +71,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback -def update_items(controller, async_add_entities, sensors): - """Update sensors from the controller.""" +def add_entities(controller, async_add_entities, sensors): + """Add new sensor entities from the controller.""" new_sensors = [] for client_id in controller.api.clients: @@ -73,9 +83,6 @@ def update_items(controller, async_add_entities, sensors): 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( @@ -87,51 +94,7 @@ def update_items(controller, async_add_entities, 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): +class UniFiRxBandwidthSensor(UniFiClient): """Receiving bandwidth sensor.""" @property @@ -153,7 +116,7 @@ class UniFiRxBandwidthSensor(UniFiBandwidthSensor): return f"rx-{self.client.mac}" -class UniFiTxBandwidthSensor(UniFiBandwidthSensor): +class UniFiTxBandwidthSensor(UniFiRxBandwidthSensor): """Transmitting bandwidth sensor.""" @property diff --git a/homeassistant/components/unifi/strings.json b/homeassistant/components/unifi/strings.json index ce2f2345917..e652b60ee32 100644 --- a/homeassistant/components/unifi/strings.json +++ b/homeassistant/components/unifi/strings.json @@ -31,15 +31,20 @@ "device_tracker": { "data": { "detection_time": "Time in seconds from last seen until considered away", + "ssid_filter": "Select SSIDs to track wireless clients on", "track_clients": "Track network clients", "track_devices": "Track network devices (Ubiquiti devices)", "track_wired_clients": "Include wired network clients" - } + }, + "description": "Configure device tracking", + "title": "UniFi options" }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "Create bandwidth usage sensors for network clients" - } + "allow_bandwidth_sensors": "Bandwidth usage sensors for network clients" + }, + "description": "Configure statistics sensors", + "title": "UniFi options" } } } diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index b1f62131eb4..941f4f8ab84 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -1,15 +1,15 @@ """Support for devices connected to UniFi POE.""" import logging -from pprint import pformat from homeassistant.components.switch import SwitchDevice from homeassistant.components.unifi.config_flow import get_controller_from_config_entry from homeassistant.core import callback from homeassistant.helpers import entity_registry -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.restore_state import RestoreEntity +from .unifi_client import UniFiClient + LOGGER = logging.getLogger(__name__) @@ -20,7 +20,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 switches for UniFi component. - Switches are controlling network switch ports with Poe. + Switches are controlling network access and switch ports with POE. """ controller = get_controller_from_config_entry(hass, config_entry) @@ -55,7 +55,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def update_controller(): """Update the values of the controller.""" - update_items(controller, async_add_entities, switches, switches_off) + add_entities(controller, async_add_entities, switches, switches_off) controller.listeners.append( async_dispatcher_connect(hass, controller.signal_update, update_controller) @@ -66,8 +66,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback -def update_items(controller, async_add_entities, switches, switches_off): - """Update POE port state from the controller.""" +def add_entities(controller, async_add_entities, switches, switches_off): + """Add new switch entities from the controller.""" new_switches = [] devices = controller.api.devices @@ -77,13 +77,6 @@ def update_items(controller, async_add_entities, switches, switches_off): block_client_id = f"block-{client_id}" if block_client_id in switches: - if switches[block_client_id].enabled: - LOGGER.debug( - "Updating UniFi block switch %s (%s)", - switches[block_client_id].entity_id, - switches[block_client_id].client.mac, - ) - switches[block_client_id].async_schedule_update_ha_state() continue if client_id not in controller.api.clients_all: @@ -99,13 +92,6 @@ def update_items(controller, async_add_entities, switches, switches_off): poe_client_id = f"poe-{client_id}" if poe_client_id in switches: - if switches[poe_client_id].enabled: - LOGGER.debug( - "Updating UniFi POE switch %s (%s)", - switches[poe_client_id].entity_id, - switches[poe_client_id].client.mac, - ) - switches[poe_client_id].async_schedule_update_ha_state() continue client = controller.api.clients[client_id] @@ -148,42 +134,21 @@ def update_items(controller, async_add_entities, switches, switches_off): async_add_entities(new_switches) -class UniFiClient: - """Base class for UniFi switches.""" - - def __init__(self, client, controller): - """Set up switch.""" - self.client = client - self.controller = controller - - async def async_update(self): - """Synchronize state with controller.""" - await self.controller.request_update() - - @property - def name(self): - """Return the name of the client.""" - return self.client.name or self.client.hostname - - @property - def device_info(self): - """Return a device description for device registry.""" - return {"connections": {(CONNECTION_NETWORK_MAC, self.client.mac)}} - - class UniFiPOEClientSwitch(UniFiClient, SwitchDevice, RestoreEntity): """Representation of a client that uses POE.""" def __init__(self, client, controller): """Set up POE switch.""" super().__init__(client, controller) + self.poe_mode = None if self.client.sw_port and self.port.poe_mode != "off": self.poe_mode = self.port.poe_mode async def async_added_to_hass(self): """Call when entity about to be added to Home Assistant.""" - LOGGER.debug("New UniFi POE switch %s (%s)", self.name, self.client.mac) + await super().async_added_to_hass() + state = await self.async_get_last_state() if state is None: @@ -198,16 +163,6 @@ class UniFiPOEClientSwitch(UniFiClient, SwitchDevice, RestoreEntity): if not self.client.sw_port: self.client.raw["sw_port"] = state.attributes["port"] - async def async_update(self): - """Log client information after update.""" - await super().async_update() - - LOGGER.debug( - "Updating UniFi POE controlled client %s\n%s", - self.entity_id, - pformat(self.client.raw), - ) - @property def unique_id(self): """Return a unique identifier for this switch.""" @@ -261,16 +216,20 @@ class UniFiPOEClientSwitch(UniFiClient, SwitchDevice, RestoreEntity): @property def port(self): """Shortcut to the switch port that client is connected to.""" - return self.device.ports[self.client.sw_port] + try: + return self.device.ports[self.client.sw_port] + except TypeError: + LOGGER.warning( + "Entity %s reports faulty device %s or port %s", + self.entity_id, + self.client.sw_mac, + self.client.sw_port, + ) class UniFiBlockClientSwitch(UniFiClient, SwitchDevice): """Representation of a blockable client.""" - async def async_added_to_hass(self): - """Call when entity about to be added to Home Assistant.""" - LOGGER.debug("New UniFi Block switch %s (%s)", self.name, self.client.mac) - @property def unique_id(self): """Return a unique identifier for this switch.""" @@ -281,11 +240,6 @@ class UniFiBlockClientSwitch(UniFiClient, SwitchDevice): """Return true if client is allowed to connect.""" return not self.client.blocked - @property - def available(self): - """Return if controller is available.""" - return self.controller.available - async def async_turn_on(self, **kwargs): """Turn on connectivity for client.""" await self.controller.api.clients.async_unblock(self.client.mac) diff --git a/homeassistant/components/unifi/unifi_client.py b/homeassistant/components/unifi/unifi_client.py new file mode 100644 index 00000000000..2e18f55a57b --- /dev/null +++ b/homeassistant/components/unifi/unifi_client.py @@ -0,0 +1,65 @@ +"""Base class for UniFi clients.""" + +import logging + +from homeassistant.core import callback +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity + +LOGGER = logging.getLogger(__name__) + + +class UniFiClient(Entity): + """Base class for UniFi clients.""" + + def __init__(self, client, controller) -> None: + """Set up client.""" + self.client = client + self.controller = controller + self.listeners = [] + self.is_wired = self.client.mac not in controller.wireless_clients + + async def async_added_to_hass(self) -> None: + """Client entity created.""" + LOGGER.debug("New UniFi client %s (%s)", self.name, self.client.mac) + self.client.register_callback(self.async_update_callback) + self.listeners.append( + async_dispatcher_connect( + self.hass, self.controller.signal_reachable, self.async_update_callback + ) + ) + + async def async_will_remove_from_hass(self) -> None: + """Disconnect client object when removed.""" + self.client.remove_callback(self.async_update_callback) + for unsub_dispatcher in self.listeners: + unsub_dispatcher() + + @callback + def async_update_callback(self) -> None: + """Update the clients state.""" + if self.is_wired and self.client.mac in self.controller.wireless_clients: + self.is_wired = False + LOGGER.debug("Updating client %s %s", self.entity_id, self.client.mac) + self.async_schedule_update_ha_state() + + @property + def name(self) -> str: + """Return the name of the client.""" + return self.client.name or self.client.hostname + + @property + def available(self) -> bool: + """Return if controller is available.""" + return self.controller.available + + @property + def device_info(self) -> dict: + """Return a client description for device registry.""" + return {"connections": {(CONNECTION_NETWORK_MAC, self.client.mac)}} + + @property + def should_poll(self) -> bool: + """No polling needed.""" + return False diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py index 42eb988ed56..0a2c6697f69 100644 --- a/homeassistant/components/updater/__init__.py +++ b/homeassistant/components/updater/__init__.py @@ -12,11 +12,9 @@ from distro import linux_distribution # pylint: disable=import-error import voluptuous as vol from homeassistant.const import __version__ as current_version -from homeassistant.helpers import discovery, event +from homeassistant.helpers import discovery, update_coordinator from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_send -import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -28,8 +26,6 @@ CONF_COMPONENT_REPORTING = "include_used_components" DOMAIN = "updater" -DISPATCHER_REMOTE_UPDATE = "updater_remote_update" - UPDATER_URL = "https://updater.home-assistant.io/" UPDATER_UUID_FILE = ".uuid" @@ -84,30 +80,25 @@ async def async_setup(hass, config): # This component only makes sense in release versions _LOGGER.info("Running on 'dev', only analytics will be submitted") - hass.async_create_task( - discovery.async_load_platform(hass, "binary_sensor", DOMAIN, {}, config) - ) - - config = config.get(DOMAIN, {}) - if config.get(CONF_REPORTING): + conf = config.get(DOMAIN, {}) + if conf.get(CONF_REPORTING): huuid = await hass.async_add_job(_load_uuid, hass) else: huuid = None - include_components = config.get(CONF_COMPONENT_REPORTING) + include_components = conf.get(CONF_COMPONENT_REPORTING) - async def check_new_version(now): + async def check_new_version(): """Check if a new version is available and report if one is.""" - result = await get_newest_version(hass, huuid, include_components) + newest, release_notes = await get_newest_version( + hass, huuid, include_components + ) - if result is None: - return - - newest, release_notes = result + _LOGGER.debug("Fetched version %s: %s", newest, release_notes) # Skip on dev - if newest is None or "dev" in current_version: - return + if "dev" in current_version: + return Updater(False, "", "") # Load data from supervisor on Hass.io if hass.components.hassio.is_hassio(): @@ -116,20 +107,33 @@ async def async_setup(hass, config): # Validate version update_available = False if StrictVersion(newest) > StrictVersion(current_version): - _LOGGER.info("The latest available version of Home Assistant is %s", newest) + _LOGGER.debug( + "The latest available version of Home Assistant is %s", newest + ) update_available = True elif StrictVersion(newest) == StrictVersion(current_version): - _LOGGER.info("You are on the latest version (%s) of Home Assistant", newest) + _LOGGER.debug( + "You are on the latest version (%s) of Home Assistant", newest + ) elif StrictVersion(newest) < StrictVersion(current_version): _LOGGER.debug("Local version is newer than the latest version (%s)", newest) - updater = Updater(update_available, newest, release_notes) - async_dispatcher_send(hass, DISPATCHER_REMOTE_UPDATE, updater) + _LOGGER.debug("Update available: %s", update_available) - # Update daily, start 1 hour after startup - _dt = dt_util.utcnow() + timedelta(hours=1) - event.async_track_utc_time_change( - hass, check_new_version, hour=_dt.hour, minute=_dt.minute, second=_dt.second + return Updater(update_available, newest, release_notes) + + coordinator = hass.data[DOMAIN] = update_coordinator.DataUpdateCoordinator( + hass, + _LOGGER, + name="Home Assistant update", + update_method=check_new_version, + update_interval=timedelta(days=1), + ) + + await coordinator.async_refresh() + + hass.async_create_task( + discovery.async_load_platform(hass, "binary_sensor", DOMAIN, {}, config) ) return True @@ -164,17 +168,17 @@ async def get_newest_version(hass, huuid, include_components): ) except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Could not contact Home Assistant Update to check for updates") - return None + raise update_coordinator.UpdateFailed try: res = await req.json() except ValueError: _LOGGER.error("Received invalid JSON from Home Assistant Update") - return None + raise update_coordinator.UpdateFailed try: res = RESPONSE_SCHEMA(res) return res["version"], res["release-notes"] except vol.Invalid: _LOGGER.error("Got unexpected response: %s", res) - return None + raise update_coordinator.UpdateFailed diff --git a/homeassistant/components/updater/binary_sensor.py b/homeassistant/components/updater/binary_sensor.py index 3e026a87d4d..7abab616d5c 100644 --- a/homeassistant/components/updater/binary_sensor.py +++ b/homeassistant/components/updater/binary_sensor.py @@ -1,26 +1,24 @@ """Support for Home Assistant Updater binary sensors.""" from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import ATTR_NEWEST_VERSION, ATTR_RELEASE_NOTES, DISPATCHER_REMOTE_UPDATE, Updater +from . import ATTR_NEWEST_VERSION, ATTR_RELEASE_NOTES, DOMAIN as UPDATER_DOMAIN async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the updater binary sensors.""" - async_add_entities([UpdaterBinary()]) + if discovery_info is None: + return + + async_add_entities([UpdaterBinary(hass.data[UPDATER_DOMAIN])]) class UpdaterBinary(BinarySensorDevice): """Representation of an updater binary sensor.""" - def __init__(self): + def __init__(self, coordinator): """Initialize the binary sensor.""" - self._update_available = None - self._release_notes = None - self._newest_version = None - self._unsub_dispatcher = None + self.coordinator = coordinator @property def name(self) -> str: @@ -35,12 +33,12 @@ class UpdaterBinary(BinarySensorDevice): @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" - return self._update_available + return self.coordinator.data.update_available @property def available(self) -> bool: """Return True if entity is available.""" - return self._update_available is not None + return self.coordinator.last_update_success @property def should_poll(self) -> bool: @@ -50,32 +48,24 @@ class UpdaterBinary(BinarySensorDevice): @property def device_state_attributes(self) -> dict: """Return the optional state attributes.""" - data = super().device_state_attributes - if data is None: - data = {} - if self._release_notes: - data[ATTR_RELEASE_NOTES] = self._release_notes - if self._newest_version: - data[ATTR_NEWEST_VERSION] = self._newest_version + data = {} + if self.coordinator.data.release_notes: + data[ATTR_RELEASE_NOTES] = self.coordinator.data.release_notes + if self.coordinator.data.newest_version: + data[ATTR_NEWEST_VERSION] = self.coordinator.data.newest_version return data async def async_added_to_hass(self): """Register update dispatcher.""" - - @callback - def async_state_update(updater: Updater): - """Update callback.""" - self._newest_version = updater.newest_version - self._release_notes = updater.release_notes - self._update_available = updater.update_available - self.async_schedule_update_ha_state() - - self._unsub_dispatcher = async_dispatcher_connect( - self.hass, DISPATCHER_REMOTE_UPDATE, async_state_update - ) + self.coordinator.async_add_listener(self.async_write_ha_state) async def async_will_remove_from_hass(self): - """Register update dispatcher.""" - if self._unsub_dispatcher is not None: - self._unsub_dispatcher() - self._unsub_dispatcher = None + """When removed from hass.""" + self.coordinator.async_remove_listener(self.async_write_ha_state) + + async def async_update(self): + """Update the entity. + + Only used by the generic entity update service. + """ + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/upnp/.translations/pl.json b/homeassistant/components/upnp/.translations/pl.json index d7ede44d22d..964e5a6818d 100644 --- a/homeassistant/components/upnp/.translations/pl.json +++ b/homeassistant/components/upnp/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "UPnP/IGD jest ju\u017c skonfigurowane", + "already_configured": "UPnP/IGD jest ju\u017c skonfigurowane.", "incomplete_device": "Ignorowanie niekompletnego urz\u0105dzenia UPnP", "no_devices_discovered": "Nie wykryto urz\u0105dze\u0144 UPnP/IGD", "no_devices_found": "Nie znaleziono w sieci urz\u0105dze\u0144 UPnP/IGD.", diff --git a/homeassistant/components/upnp/.translations/ru.json b/homeassistant/components/upnp/.translations/ru.json index 6dce1b3d76c..b0a7b7e7b65 100644 --- a/homeassistant/components/upnp/.translations/ru.json +++ b/homeassistant/components/upnp/.translations/ru.json @@ -5,7 +5,7 @@ "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 \u0441\u0435\u043d\u0441\u043e\u0440\u044b \u0438\u043b\u0438 \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u043f\u043e\u0440\u0442\u043e\u0432.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "error": { @@ -25,7 +25,7 @@ "user": { "data": { "enable_port_mapping": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u043f\u043e\u0440\u0442\u043e\u0432 \u0434\u043b\u044f Home Assistant", - "enable_sensors": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0441\u0435\u0442\u0435\u0432\u043e\u0433\u043e \u0442\u0440\u0430\u0444\u0438\u043a\u0430", + "enable_sensors": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0441\u0435\u043d\u0441\u043e\u0440\u044b \u0441\u0435\u0442\u0435\u0432\u043e\u0433\u043e \u0442\u0440\u0430\u0444\u0438\u043a\u0430", "igd": "UPnP / IGD" }, "title": "UPnP / IGD" diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index 81fd5c025b9..db121678d93 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -170,7 +170,7 @@ class PerSecondUPnPIGDSensor(UpnpSensor): """Get unit we are measuring in.""" raise NotImplementedError() - def _async_fetch_value(self): + async def _async_fetch_value(self): """Fetch a value from the IGD.""" raise NotImplementedError() diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 3dab92b89f8..8c47e716b80 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -140,7 +140,7 @@ class UtilityMeterSensor(RestoreEntity): diff = Decimal(new_state.state) - Decimal(old_state.state) if (not self._sensor_net_consumption) and diff < 0: - # Source sensor just rolled over for unknow reasons, + # Source sensor just rolled over for unknown reasons, return self._state += diff diff --git a/homeassistant/components/vacuum/.translations/sv.json b/homeassistant/components/vacuum/.translations/sv.json new file mode 100644 index 00000000000..38b7f72ab9b --- /dev/null +++ b/homeassistant/components/vacuum/.translations/sv.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "clean": "L\u00e5t {entity_name} st\u00e4da", + "dock": "L\u00e5t {entity_name} \u00e5terg\u00e5 till dockan" + }, + "condition_type": { + "is_cleaning": "{entity_name} st\u00e4dar", + "is_docked": "{entity_name} \u00e4r dockad" + }, + "trigger_type": { + "cleaning": "{entity_name} b\u00f6rjade st\u00e4da", + "docked": "{entity_name} dockad" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 225a6ed72bc..3cd2de600e3 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -248,7 +248,7 @@ class VacuumDevice(_BaseVacuum, ToggleEntity): @property def capability_attributes(self): - """Return capabilitiy attributes.""" + """Return capability attributes.""" if self.fan_speed is not None: return {ATTR_FAN_SPEED_LIST: self.fan_speed_list} @@ -330,7 +330,7 @@ class StateVacuumDevice(_BaseVacuum): @property def capability_attributes(self): - """Return capabilitiy attributes.""" + """Return capability attributes.""" if self.fan_speed is not None: return {ATTR_FAN_SPEED_LIST: self.fan_speed_list} diff --git a/homeassistant/components/vallox/manifest.json b/homeassistant/components/vallox/manifest.json index 875cc6f8787..7a082200740 100644 --- a/homeassistant/components/vallox/manifest.json +++ b/homeassistant/components/vallox/manifest.json @@ -2,7 +2,7 @@ "domain": "vallox", "name": "Valloxs", "documentation": "https://www.home-assistant.io/integrations/vallox", - "requirements": ["vallox-websocket-api==2.2.0"], + "requirements": ["vallox-websocket-api==2.4.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/velbus/.translations/pl.json b/homeassistant/components/velbus/.translations/pl.json index 72e18b0e2c8..0856d142bef 100644 --- a/homeassistant/components/velbus/.translations/pl.json +++ b/homeassistant/components/velbus/.translations/pl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "port_exists": "Ten port jest ju\u017c skonfigurowany" + "port_exists": "Ten port jest ju\u017c skonfigurowany." }, "error": { "connection_failed": "Po\u0142\u0105czenie Velbus nie powiod\u0142o si\u0119", - "port_exists": "Ten port jest ju\u017c skonfigurowany" + "port_exists": "Ten port jest ju\u017c skonfigurowany." }, "step": { "user": { diff --git a/homeassistant/components/velbus/.translations/sv.json b/homeassistant/components/velbus/.translations/sv.json new file mode 100644 index 00000000000..5a864439423 --- /dev/null +++ b/homeassistant/components/velbus/.translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "port_exists": "Den h\u00e4r porten \u00e4r redan konfigurerad" + }, + "error": { + "connection_failed": "Velbus-anslutningen misslyckades", + "port_exists": "Den h\u00e4r porten \u00e4r redan konfigurerad" + }, + "step": { + "user": { + "data": { + "name": "Namnet p\u00e5 den h\u00e4r velbus-anslutningen", + "port": "Anslutningsstr\u00e4ng" + }, + "title": "Definiera velbus-anslutningstypen" + } + }, + "title": "Velbus-gr\u00e4nssnitt" + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index d8f9dae13de..b4fe49a88e7 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -140,11 +140,11 @@ class VelbusEntity(Entity): "identifiers": { (DOMAIN, self._module.get_module_address(), self._module.serial) }, - "name": "{} {}".format( - self._module.get_module_address(), self._module.get_module_name() + "name": "{} ({})".format( + self._module.get_module_name(), self._module.get_module_address() ), "manufacturer": "Velleman", - "model": self._module.get_module_name(), + "model": self._module.get_module_type_name(), "sw_version": "{}.{}-{}".format( self._module.memory_map_version, self._module.build_year, diff --git a/homeassistant/components/velbus/config_flow.py b/homeassistant/components/velbus/config_flow.py index 9325acf0608..1d081b711a8 100644 --- a/homeassistant/components/velbus/config_flow.py +++ b/homeassistant/components/velbus/config_flow.py @@ -49,7 +49,7 @@ class VelbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return False async def async_step_user(self, user_input=None): - """Step when user intializes a integration.""" + """Step when user initializes a integration.""" self._errors = {} if user_input is not None: name = slugify(user_input[CONF_NAME]) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 258b367fa5b..3063c4445bd 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -2,7 +2,7 @@ "domain": "velbus", "name": "Velbus", "documentation": "https://www.home-assistant.io/integrations/velbus", - "requirements": ["python-velbus==2.0.36"], + "requirements": ["python-velbus==2.0.41"], "config_flow": true, "dependencies": [], "codeowners": ["@Cereal2nd", "@brefra"] diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py index 01eb5faf897..5b5d50347ac 100644 --- a/homeassistant/components/verisure/lock.py +++ b/homeassistant/components/verisure/lock.py @@ -1,6 +1,6 @@ """Support for Verisure locks.""" import logging -from time import sleep, time +from time import monotonic, sleep from homeassistant.components.lock import LockDevice from homeassistant.const import ATTR_CODE, STATE_LOCKED, STATE_UNLOCKED @@ -71,7 +71,7 @@ class VerisureDoorlock(LockDevice): def update(self): """Update lock status.""" - if time() - self._change_timestamp < 10: + if monotonic() - self._change_timestamp < 10: return hub.update_overview() status = hub.get_first( @@ -131,4 +131,4 @@ class VerisureDoorlock(LockDevice): transaction = hub.session.get_lock_state_transaction(transaction_id) if transaction["result"] == "OK": self._state = state - self._change_timestamp = time() + self._change_timestamp = monotonic() diff --git a/homeassistant/components/verisure/switch.py b/homeassistant/components/verisure/switch.py index 32e1c1364a3..2df250303c5 100644 --- a/homeassistant/components/verisure/switch.py +++ b/homeassistant/components/verisure/switch.py @@ -1,6 +1,6 @@ """Support for Verisure Smartplugs.""" import logging -from time import time +from time import monotonic from homeassistant.components.switch import SwitchDevice @@ -44,7 +44,7 @@ class VerisureSmartplug(SwitchDevice): @property def is_on(self): """Return true if on.""" - if time() - self._change_timestamp < 10: + if monotonic() - self._change_timestamp < 10: return self._state self._state = ( hub.get_first( @@ -67,13 +67,13 @@ class VerisureSmartplug(SwitchDevice): """Set smartplug status on.""" hub.session.set_smartplug_state(self._device_label, True) self._state = True - self._change_timestamp = time() + self._change_timestamp = monotonic() def turn_off(self, **kwargs): """Set smartplug status off.""" hub.session.set_smartplug_state(self._device_label, False) self._state = False - self._change_timestamp = time() + self._change_timestamp = monotonic() # pylint: disable=no-self-use def update(self): diff --git a/homeassistant/components/vesync/.translations/sv.json b/homeassistant/components/vesync/.translations/sv.json new file mode 100644 index 00000000000..a477ca6e5da --- /dev/null +++ b/homeassistant/components/vesync/.translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_setup": "Endast en Vesync-instans \u00e4r till\u00e5ten" + }, + "error": { + "invalid_login": "Ogiltigt anv\u00e4ndarnamn eller l\u00f6senord" + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "username": "E-postadress" + }, + "title": "Ange anv\u00e4ndarnamn och l\u00f6senord" + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json index cefc244e5b8..66fd15d3a90 100644 --- a/homeassistant/components/vicare/manifest.json +++ b/homeassistant/components/vicare/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/vicare", "dependencies": [], "codeowners": ["@oischinger"], - "requirements": ["PyViCare==0.1.2"] + "requirements": ["PyViCare==0.1.7"] } diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index f31e4f65170..eea3d81faf6 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -122,7 +122,8 @@ class ViCareWater(WaterHeaterDevice): """Set new target temperatures.""" temp = kwargs.get(ATTR_TEMPERATURE) if temp is not None: - self._api.setDomesticHotWaterTemperature(self._target_temperature) + self._api.setDomesticHotWaterTemperature(temp) + self._target_temperature = temp @property def min_temp(self): diff --git a/homeassistant/components/vilfo/.translations/ca.json b/homeassistant/components/vilfo/.translations/ca.json new file mode 100644 index 00000000000..07d9ddafb51 --- /dev/null +++ b/homeassistant/components/vilfo/.translations/ca.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "L'encaminador Vilfo ja est\u00e0 configurat." + }, + "error": { + "cannot_connect": "No s'ha pogut connectar. Verifica la informaci\u00f3 proporcionada i torna-ho a provar.", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida. Comprova el testimoni d'acc\u00e9s i torna-ho a provar.", + "unknown": "S'ha produ\u00eft un error inesperat durant la configuraci\u00f3 de la integraci\u00f3." + }, + "step": { + "user": { + "data": { + "access_token": "Testimoni d'acc\u00e9s per l'API de l'encaminador Vilfo", + "host": "Nom d'amfitri\u00f3 o IP de l'encaminador" + }, + "description": "Configura la integraci\u00f3 de l'encaminador Vilfo. Necessites la seva IP o nom d'amfitri\u00f3 i el testimoni d'acc\u00e9s de l'API (token). Per a m\u00e9s informaci\u00f3, visita: https://www.home-assistant.io/integrations/vilfo", + "title": "Connexi\u00f3 amb l'encaminador Vilfo" + } + }, + "title": "Encaminador Vilfo" + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/.translations/da.json b/homeassistant/components/vilfo/.translations/da.json new file mode 100644 index 00000000000..f233b4cb7b9 --- /dev/null +++ b/homeassistant/components/vilfo/.translations/da.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Denne Vilfo-router er allerede konfigureret." + }, + "error": { + "cannot_connect": "Forbindelsen kunne ikke oprettes. Tjek de oplysninger, du har angivet, og pr\u00f8v igen.", + "invalid_auth": "Ugyldig godkendelse. Kontroller adgangstoken og pr\u00f8v igen.", + "unknown": "Der opstod en uventet fejl under konfiguration af integrationen." + }, + "step": { + "user": { + "data": { + "access_token": "Adgangstoken til Vilfo-router-API", + "host": "Router-v\u00e6rtsnavn eller IP" + }, + "description": "Indstil Vilfo-routerintegration. Du har brug for dit Vilfo-routerv\u00e6rtsnavn/IP og et API-adgangstoken. For yderligere information om denne integration og hvordan du f\u00e5r disse detaljer, kan du bes\u00f8ge: https://www.home-assistant.io/integrations/vilfo", + "title": "Opret forbindelse til Vilfo-router" + } + }, + "title": "Vilfo-router" + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/.translations/de.json b/homeassistant/components/vilfo/.translations/de.json new file mode 100644 index 00000000000..9c0f938b679 --- /dev/null +++ b/homeassistant/components/vilfo/.translations/de.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Dieser Vilfo Router ist bereits konfiguriert." + }, + "error": { + "cannot_connect": "Verbindung nicht m\u00f6glich. Bitte \u00fcberpr\u00fcfen Sie die von Ihnen angegebenen Informationen und versuchen Sie es erneut.", + "invalid_auth": "Ung\u00fcltige Authentifizierung. Bitte \u00fcberpr\u00fcfen Sie den Zugriffstoken und versuchen Sie es erneut.", + "unknown": "Beim Einrichten der Integration ist ein unerwarteter Fehler aufgetreten." + }, + "step": { + "user": { + "data": { + "access_token": "Zugriffstoken f\u00fcr die Vilfo Router-API", + "host": "Router-Hostname oder IP" + }, + "title": "Stellen Sie eine Verbindung zum Vilfo Router her" + } + }, + "title": "Vilfo Router" + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/.translations/en.json b/homeassistant/components/vilfo/.translations/en.json new file mode 100644 index 00000000000..e6b9817f5a8 --- /dev/null +++ b/homeassistant/components/vilfo/.translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "This Vilfo Router is already configured." + }, + "error": { + "cannot_connect": "Failed to connect. Please check the information you provided and try again.", + "invalid_auth": "Invalid authentication. Please check the access token and try again.", + "unknown": "An unexpected error occurred while setting up the integration." + }, + "step": { + "user": { + "data": { + "access_token": "Access token for the Vilfo Router API", + "host": "Router hostname or IP" + }, + "description": "Set up the Vilfo Router integration. You need your Vilfo Router hostname/IP and an API access token. For additional information on this integration and how to get those details, visit: https://www.home-assistant.io/integrations/vilfo", + "title": "Connect to the Vilfo Router" + } + }, + "title": "Vilfo Router" + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/.translations/es.json b/homeassistant/components/vilfo/.translations/es.json new file mode 100644 index 00000000000..170faa197da --- /dev/null +++ b/homeassistant/components/vilfo/.translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Este router Vilfo ya est\u00e1 configurado." + }, + "error": { + "cannot_connect": "No se pudo conectar. Compruebe la informaci\u00f3n que proporcion\u00f3 e int\u00e9ntelo de nuevo.", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida. Compruebe el token de acceso e int\u00e9ntelo de nuevo.", + "unknown": "Se ha producido un error inesperado al configurar la integraci\u00f3n." + }, + "step": { + "user": { + "data": { + "access_token": "Token de acceso para la API del Router Vilfo", + "host": "Nombre de host o IP del router" + }, + "description": "Configure la integraci\u00f3n del Router Vilfo. Necesita su nombre de host/IP del Router Vilfo y un token de acceso a la API. Para obtener informaci\u00f3n adicional sobre esta integraci\u00f3n y c\u00f3mo obtener esos detalles, visite: https://www.home-assistant.io/integrations/vilfo", + "title": "Conectar con el Router Vilfo" + } + }, + "title": "Router Vilfo" + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/.translations/fr.json b/homeassistant/components/vilfo/.translations/fr.json new file mode 100644 index 00000000000..6abeb789f23 --- /dev/null +++ b/homeassistant/components/vilfo/.translations/fr.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Routeur Vilfo" + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/.translations/hu.json b/homeassistant/components/vilfo/.translations/hu.json new file mode 100644 index 00000000000..5ae11707c19 --- /dev/null +++ b/homeassistant/components/vilfo/.translations/hu.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Router hostname vagy IP" + }, + "title": "Csatlakoz\u00e1s a Vilfo routerhez" + } + }, + "title": "Vilfo Router" + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/.translations/it.json b/homeassistant/components/vilfo/.translations/it.json new file mode 100644 index 00000000000..5523dcc0c09 --- /dev/null +++ b/homeassistant/components/vilfo/.translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Questo Vilfo Router \u00e8 gi\u00e0 configurato." + }, + "error": { + "cannot_connect": "Impossibile connettersi. Controllare le informazioni fornite e riprovare.", + "invalid_auth": "Autenticazione non valida. Controllare il token di accesso e riprovare.", + "unknown": "Si \u00e8 verificato un errore imprevisto durante l'impostazione dell'integrazione." + }, + "step": { + "user": { + "data": { + "access_token": "Token di accesso per il Vilfo Router API", + "host": "Nome host o IP del router" + }, + "description": "Configurare l'integrazione del Vilfo Router. \u00c8 necessario il vostro hostname/IP del Vilfo Router e un token di accesso API. Per ulteriori informazioni su questa integrazione e su come ottenere tali dettagli, visitare il sito: https://www.home-assistant.io/integrations/vilfo", + "title": "Collegamento al Vilfo Router" + } + }, + "title": "Vilfo Router" + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/.translations/ko.json b/homeassistant/components/vilfo/.translations/ko.json new file mode 100644 index 00000000000..85cb147ff6c --- /dev/null +++ b/homeassistant/components/vilfo/.translations/ko.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\uc774 Vilfo \ub77c\uc6b0\ud130\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \uc785\ub825\ud558\uc2e0 \ub0b4\uc6a9\uc744 \ud655\uc778\ud558\uc2e0 \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uc561\uc138\uc2a4 \ud1a0\ud070\uc744 \ud655\uc778\ud558\uc2e0 \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "unknown": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud558\ub294 \uc911 \uc608\uae30\uce58 \uc54a\uc740 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4." + }, + "step": { + "user": { + "data": { + "access_token": "Vilfo \ub77c\uc6b0\ud130 API \uc6a9 \uc561\uc138\uc2a4 \ud1a0\ud070", + "host": "\ub77c\uc6b0\ud130 \ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c" + }, + "description": "Vilfo \ub77c\uc6b0\ud130 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. Vilfo \ub77c\uc6b0\ud130 \ud638\uc2a4\ud2b8 \uc774\ub984 / IP \uc640 API \uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc5d0 \ub300\ud55c \ucd94\uac00 \uc815\ubcf4\uc640 \uc138\ubd80 \uc0ac\ud56d\uc740 https://www.home-assistant.io/integrations/vilfo \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694", + "title": "Vilfo \ub77c\uc6b0\ud130\uc5d0 \uc5f0\uacb0\ud558\uae30" + } + }, + "title": "Vilfo \ub77c\uc6b0\ud130" + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/.translations/lb.json b/homeassistant/components/vilfo/.translations/lb.json new file mode 100644 index 00000000000..7b88bd31d17 --- /dev/null +++ b/homeassistant/components/vilfo/.translations/lb.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "D\u00ebse Vilfo Router ass scho konfigur\u00e9iert." + }, + "error": { + "cannot_connect": "Feeler beim verbannen. Iwwerpr\u00e9ift \u00e4r Informatiounen an prob\u00e9iert nach emol.", + "invalid_auth": "Ong\u00eblteg Authentifikatioun. Iwwerpr\u00e9ift den Acc\u00e8s jeton an prob\u00e9iert nach emol.", + "unknown": "Onerwaarte Feeler beim ariichten vun der Integratioun." + }, + "step": { + "user": { + "data": { + "access_token": "Acc\u00e8s Jeton fir Vilfo Router API", + "host": "Router Numm oder IP" + }, + "description": "Vilfo Router Integratioun ariichten. Dir braucht \u00e4re Vilfo Router Numm/IP an een API Acc\u00e8s Jeton. Fir weider Informatiounen zu d\u00ebser Integratioun a w\u00e9i een zu d\u00ebsen n\u00e9idegen Informatioune k\u00ebnnt, gitt op: https://www.home-assistant.io/integrations/vilfo", + "title": "Mam Vilfo Router verbannen" + } + }, + "title": "Vilfo Router" + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/.translations/nl.json b/homeassistant/components/vilfo/.translations/nl.json new file mode 100644 index 00000000000..db2691d3eeb --- /dev/null +++ b/homeassistant/components/vilfo/.translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Deze Vilfo Router is al geconfigureerd." + }, + "error": { + "cannot_connect": "Kon niet verbinden. Controleer de door u verstrekte informatie en probeer het opnieuw.", + "unknown": "Er is een onverwachte fout opgetreden tijdens het instellen van de integratie." + }, + "step": { + "user": { + "data": { + "access_token": "Toegangstoken voor de Vilfo Router API", + "host": "Router hostnaam of IP-adres" + }, + "title": "Maak verbinding met de Vilfo Router" + } + }, + "title": "Vilfo Router" + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/.translations/no.json b/homeassistant/components/vilfo/.translations/no.json new file mode 100644 index 00000000000..af72a4bd7b0 --- /dev/null +++ b/homeassistant/components/vilfo/.translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Denne Vilfo Ruteren er allerede konfigurert." + }, + "error": { + "cannot_connect": "Tilkobling mislyktes. Vennligst sjekk informasjonen du oppga, og pr\u00f8v igjen.", + "invalid_auth": "Ugyldig godkjenning. Vennligst sjekk access token, og pr\u00f8v p\u00e5 nytt.", + "unknown": "Det oppstod en uventet feil under installasjonen av integrasjonen." + }, + "step": { + "user": { + "data": { + "access_token": "Tilgangstoken for Vilfo Router API", + "host": "Ruter vertsnavn eller IP" + }, + "description": "Konfigurer Vilfo Router-integreringen. Du trenger ditt Vilfo Router vertsnavn/IP og et API-tilgangstoken. Hvis du vil ha mer informasjon om denne integreringen og hvordan du f\u00e5r disse detaljene, kan du g\u00e5 til: https://www.home-assistant.io/integrations/vilfo", + "title": "Koble til Vilfo Ruteren" + } + }, + "title": "Vilfo Router" + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/.translations/pl.json b/homeassistant/components/vilfo/.translations/pl.json new file mode 100644 index 00000000000..aef0c14703f --- /dev/null +++ b/homeassistant/components/vilfo/.translations/pl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Ten router Vilfo jest ju\u017c skonfigurowany." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia. Sprawd\u017a wprowadzone dane i spr\u00f3buj ponownie.", + "invalid_auth": "Nieudane uwierzytelnienie. Sprawd\u017a token dost\u0119pu i spr\u00f3buj ponownie.", + "unknown": "Wyst\u0105pi\u0142 nieoczekiwany b\u0142\u0105d podczas konfiguracji integracji." + }, + "step": { + "user": { + "data": { + "access_token": "Token dost\u0119pu do interfejsu API routera Vilfo", + "host": "Nazwa hosta lub adres IP routera" + }, + "description": "Skonfiguruj integracj\u0119 routera Vilfo. Potrzebujesz nazwy hosta/adresu IP routera Vilfo i tokena dost\u0119pu do interfejsu API. Aby uzyska\u0107 dodatkowe informacje na temat tej integracji i sposobu uzyskania niezb\u0119dnych danych do konfiguracji, odwied\u017a: https://www.home-assistant.io/integrations/vilfo", + "title": "Po\u0142\u0105cz si\u0119 z routerem Vilfo" + } + }, + "title": "Router Vilfo" + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/.translations/ru.json b/homeassistant/components/vilfo/.translations/ru.json new file mode 100644 index 00000000000..ce8f325e0ea --- /dev/null +++ b/homeassistant/components/vilfo/.translations/ru.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\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, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438 \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0438 \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438." + }, + "step": { + "user": { + "data": { + "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a API \u0440\u043e\u0443\u0442\u0435\u0440\u0430", + "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430 Vilfo. \u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441 \u0440\u043e\u0443\u0442\u0435\u0440\u0430 \u0438 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 API. \u0414\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 \u043f\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u044d\u0442\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438, \u043f\u043e\u0441\u0435\u0442\u0438\u0442\u0435 \u0432\u0435\u0431-\u0441\u0430\u0439\u0442: https://www.home-assistant.io/integrations/vilfo.", + "title": "Vilfo Router" + } + }, + "title": "Vilfo Router" + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/.translations/sl.json b/homeassistant/components/vilfo/.translations/sl.json new file mode 100644 index 00000000000..a7d683e793c --- /dev/null +++ b/homeassistant/components/vilfo/.translations/sl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Ta usmerjevalnik Vilfo je \u017ee konfiguriran." + }, + "error": { + "cannot_connect": "Povezava ni uspela. Prosimo, preverite informacije, ki ste jih vnesli in poskusite znova.", + "invalid_auth": "Neveljavna avtentikacija. Preverite dostopni \u017eeton in poskusite znova.", + "unknown": "Med nastavitvijo integracije je pri\u0161lo do nepri\u010dakovane napake." + }, + "step": { + "user": { + "data": { + "access_token": "Dostopni \u017eeton za API Vilfo Router", + "host": "Ime gostitelja usmerjevalnika ali IP" + }, + "description": "Nastavite integracijo Vilfo Router. Potrebujete ime gostitelja ali IP Vilfo usmerjevalnika in dostopni \u017eeton API. Za dodatne informacije o tej integraciji in kako do teh podrobnosti obi\u0161\u010dite: https://www.home-assistant.io/integrations/vilfo", + "title": "Pove\u017eite se z usmerjevalnikom Vilfo" + } + }, + "title": "Vilfo Router" + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/.translations/sv.json b/homeassistant/components/vilfo/.translations/sv.json new file mode 100644 index 00000000000..69edce6b9d8 --- /dev/null +++ b/homeassistant/components/vilfo/.translations/sv.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Den h\u00e4r Vilfo-routern \u00e4r redan konfigurerad." + }, + "error": { + "cannot_connect": "Kunde inte ansluta. V\u00e4nligen kontrollera informationen du angav och f\u00f6rs\u00f6k igen.", + "invalid_auth": "Ogiltig autentisering. V\u00e4nligen kontrollera \u00e5tkomstnyckeln och f\u00f6rs\u00f6k igen.", + "unknown": "Ett ov\u00e4ntat fel intr\u00e4ffade n\u00e4r integrationen skulle konfigureras." + }, + "step": { + "user": { + "data": { + "access_token": "\u00c5tkomstnyckel f\u00f6r Vilfo-routerns API", + "host": "Routerns v\u00e4rdnamn eller IP-adress" + }, + "description": "St\u00e4ll in Vilfo Router-integrationen. Du beh\u00f6ver din Vilfo-routers v\u00e4rdnamn eller IP-adress och en \u00e5tkomstnyckel till dess API. F\u00f6r ytterligare information om den h\u00e4r integrationen och hur du f\u00e5r fram den n\u00f6dv\u00e4ndiga informationen, bes\u00f6k: https://www.home-assistant.io/integrations/vilfo", + "title": "Anslut till Vilfo-routern" + } + }, + "title": "Vilfo Router" + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/.translations/zh-Hans.json b/homeassistant/components/vilfo/.translations/zh-Hans.json new file mode 100644 index 00000000000..788f85b9382 --- /dev/null +++ b/homeassistant/components/vilfo/.translations/zh-Hans.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25\u3002\u8bf7\u68c0\u67e5\u8f93\u5165\u4fe1\u606f\u540e\uff0c\u518d\u8bd5\u4e00\u6b21\u3002", + "unknown": "\u8bbe\u7f6e\u6574\u5408\u65f6\u53d1\u751f\u610f\u5916\u9519\u8bef\u3002" + }, + "step": { + "user": { + "data": { + "access_token": "Vilfo \u8def\u7531\u5668 API \u5b58\u53d6\u5bc6\u94a5", + "host": "\u8def\u7531\u5668\u4e3b\u673a\u540d\u6216 IP \u5730\u5740" + }, + "description": "\u8bbe\u7f6e Vilfo \u8def\u7531\u5668\u6574\u5408\u3002\u60a8\u9700\u8981\u8f93\u5165 Vilfo \u8def\u7531\u5668\u4e3b\u673a\u540d/IP \u5730\u5740\u3001API\u5b58\u53d6\u5bc6\u94a5\u3002\u5176\u4ed6\u6574\u5408\u7684\u76f8\u5173\u4fe1\u606f\uff0c\u8bf7\u8bbf\u95ee\uff1ahttps://www.home-assistant.io/integrations/vilfo", + "title": "\u8fde\u63a5\u5230 Vilfo \u8def\u7531\u5668" + } + }, + "title": "Vilfo \u8def\u7531\u5668" + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/.translations/zh-Hant.json b/homeassistant/components/vilfo/.translations/zh-Hant.json new file mode 100644 index 00000000000..7553cc683cd --- /dev/null +++ b/homeassistant/components/vilfo/.translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Vilfo \u8def\u7531\u5668\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557\u3002\u8acb\u6aa2\u67e5\u8f38\u5165\u8cc7\u6599\u5f8c\uff0c\u518d\u8a66\u4e00\u6b21\u3002", + "invalid_auth": "\u9a57\u8b49\u7121\u6548\uff0c\u8acb\u6aa2\u67e5\u5b58\u53d6\u5bc6\u9470\u5f8c\u518d\u8a66\u4e00\u6b21\u3002", + "unknown": "\u8a2d\u5b9a\u6574\u5408\u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002" + }, + "step": { + "user": { + "data": { + "access_token": "Vilfo \u8def\u7531\u5668 API \u5b58\u53d6\u5bc6\u9470", + "host": "\u8def\u7531\u5668\u4e3b\u6a5f\u7aef\u6216 IP \u4f4d\u5740" + }, + "description": "\u8a2d\u5b9a Vilfo \u8def\u7531\u5668\u6574\u5408\u3002\u9700\u8981\u8f38\u5165 Vilfo \u8def\u7531\u5668\u4e3b\u6a5f\u540d\u7a31/IP \u4f4d\u5740\u3001API \u5b58\u53d6\u5bc6\u9470\u3002\u5176\u4ed6\u6574\u5408\u76f8\u95dc\u8cc7\u8a0a\uff0c\u8acb\u53c3\u8003\uff1ahttps://www.home-assistant.io/integrations/vilfo", + "title": "\u9023\u7dda\u81f3 Vilfo \u8def\u7531\u5668" + } + }, + "title": "Vilfo \u8def\u7531\u5668" + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/__init__.py b/homeassistant/components/vilfo/__init__.py new file mode 100644 index 00000000000..ffa628d6db2 --- /dev/null +++ b/homeassistant/components/vilfo/__init__.py @@ -0,0 +1,125 @@ +"""The Vilfo Router integration.""" +import asyncio +from datetime import timedelta +import logging + +from vilfo import Client as VilfoClient +from vilfo.exceptions import VilfoException + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.util import Throttle + +from .const import ATTR_BOOT_TIME, ATTR_LOAD, DOMAIN, ROUTER_DEFAULT_HOST + +PLATFORMS = ["sensor"] + +DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistantType, config: ConfigType): + """Set up the Vilfo Router component.""" + hass.data.setdefault(DOMAIN, {}) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Vilfo Router from a config entry.""" + host = entry.data[CONF_HOST] + access_token = entry.data[CONF_ACCESS_TOKEN] + + vilfo_router = VilfoRouterData(hass, host, access_token) + + await vilfo_router.async_update() + + if not vilfo_router.available: + raise ConfigEntryNotReady + + hass.data[DOMAIN][entry.entry_id] = vilfo_router + + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +class VilfoRouterData: + """Define an object to hold sensor data.""" + + def __init__(self, hass, host, access_token): + """Initialize.""" + self._vilfo = VilfoClient(host, access_token) + self.hass = hass + self.host = host + self.available = False + self.firmware_version = None + self.mac_address = self._vilfo.mac + self.data = {} + self._unavailable_logged = False + + @property + def unique_id(self): + """Get the unique_id for the Vilfo Router.""" + if self.mac_address: + return self.mac_address + + if self.host == ROUTER_DEFAULT_HOST: + return self.host + + return self.host + + def _fetch_data(self): + board_information = self._vilfo.get_board_information() + load = self._vilfo.get_load() + + return { + "board_information": board_information, + "load": load, + } + + @Throttle(DEFAULT_SCAN_INTERVAL) + async def async_update(self): + """Update data using calls to VilfoClient library.""" + try: + data = await self.hass.async_add_executor_job(self._fetch_data) + + self.firmware_version = data["board_information"]["version"] + self.data[ATTR_BOOT_TIME] = data["board_information"]["bootTime"] + self.data[ATTR_LOAD] = data["load"] + + self.available = True + except VilfoException as error: + if not self._unavailable_logged: + _LOGGER.error( + "Could not fetch data from %s, error: %s", self.host, error + ) + self._unavailable_logged = True + self.available = False + return + + if self.available and self._unavailable_logged: + _LOGGER.info("Vilfo Router %s is available again", self.host) + self._unavailable_logged = False diff --git a/homeassistant/components/vilfo/config_flow.py b/homeassistant/components/vilfo/config_flow.py new file mode 100644 index 00000000000..2b9df3d9195 --- /dev/null +++ b/homeassistant/components/vilfo/config_flow.py @@ -0,0 +1,147 @@ +"""Config flow for Vilfo Router integration.""" +import ipaddress +import logging +import re + +from vilfo import Client as VilfoClient +from vilfo.exceptions import ( + AuthenticationException as VilfoAuthenticationException, + VilfoException, +) +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_ID, CONF_MAC + +from .const import DOMAIN # pylint:disable=unused-import +from .const import ROUTER_DEFAULT_HOST + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST, default=ROUTER_DEFAULT_HOST): str, + vol.Required(CONF_ACCESS_TOKEN, default=""): str, + } +) + +RESULT_SUCCESS = "success" +RESULT_CANNOT_CONNECT = "cannot_connect" +RESULT_INVALID_AUTH = "invalid_auth" + + +def host_valid(host): + """Return True if hostname or IP address is valid.""" + try: + if ipaddress.ip_address(host).version == (4 or 6): + return True + except ValueError: + disallowed = re.compile(r"[^a-zA-Z\d\-]") + return all(x and not disallowed.search(x) for x in host.split(".")) + + +def _try_connect_and_fetch_basic_info(host, token): + """Attempt to connect and call the ping endpoint and, if successful, fetch basic information.""" + + # Perform the ping. This doesn't validate authentication. + controller = VilfoClient(host=host, token=token) + result = {"type": None, "data": {}} + + try: + controller.ping() + except VilfoException: + result["type"] = RESULT_CANNOT_CONNECT + result["data"] = CannotConnect + return result + + # Perform a call that requires authentication. + try: + controller.get_board_information() + except VilfoAuthenticationException: + result["type"] = RESULT_INVALID_AUTH + result["data"] = InvalidAuth + return result + + if controller.mac: + result["data"][CONF_ID] = controller.mac + result["data"][CONF_MAC] = controller.mac + else: + result["data"][CONF_ID] = host + result["data"][CONF_MAC] = None + + result["type"] = RESULT_SUCCESS + + return result + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + + # Validate the host before doing anything else. + if not host_valid(data[CONF_HOST]): + raise InvalidHost + + config = {} + + result = await hass.async_add_executor_job( + _try_connect_and_fetch_basic_info, data[CONF_HOST], data[CONF_ACCESS_TOKEN] + ) + + if result["type"] != RESULT_SUCCESS: + raise result["data"] + + # Return some info we want to store in the config entry. + result_data = result["data"] + config["title"] = f"{data[CONF_HOST]}" + config[CONF_MAC] = result_data[CONF_MAC] + config[CONF_HOST] = data[CONF_HOST] + config[CONF_ID] = result_data[CONF_ID] + + return config + + +class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Vilfo Router.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + info = await validate_input(self.hass, user_input) + except InvalidHost: + errors[CONF_HOST] = "wrong_host" + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception as err: # pylint: disable=broad-except + _LOGGER.error("Unexpected exception: %s", err) + errors["base"] = "unknown" + else: + await self.async_set_unique_id(info[CONF_ID]) + self._abort_if_unique_id_configured() + + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" + + +class InvalidHost(exceptions.HomeAssistantError): + """Error to indicate that hostname/IP address is invalid.""" diff --git a/homeassistant/components/vilfo/const.py b/homeassistant/components/vilfo/const.py new file mode 100644 index 00000000000..1a40b8430d7 --- /dev/null +++ b/homeassistant/components/vilfo/const.py @@ -0,0 +1,36 @@ +"""Constants for the Vilfo Router integration.""" +from homeassistant.const import DEVICE_CLASS_TIMESTAMP + +DOMAIN = "vilfo" + +ATTR_API_DATA_FIELD = "api_data_field" +ATTR_API_DATA_FIELD_LOAD = "load" +ATTR_API_DATA_FIELD_BOOT_TIME = "boot_time" +ATTR_DEVICE_CLASS = "device_class" +ATTR_ICON = "icon" +ATTR_LABEL = "label" +ATTR_LOAD = "load" +ATTR_UNIT = "unit" +ATTR_BOOT_TIME = "boot_time" + +ROUTER_DEFAULT_HOST = "admin.vilfo.com" +ROUTER_DEFAULT_MODEL = "Vilfo Router" +ROUTER_DEFAULT_NAME = "Vilfo Router" +ROUTER_MANUFACTURER = "Vilfo AB" + +UNIT_PERCENT = "%" + +SENSOR_TYPES = { + ATTR_LOAD: { + ATTR_LABEL: "Load", + ATTR_UNIT: UNIT_PERCENT, + ATTR_ICON: "mdi:memory", + ATTR_API_DATA_FIELD: ATTR_API_DATA_FIELD_LOAD, + }, + ATTR_BOOT_TIME: { + ATTR_LABEL: "Boot time", + ATTR_ICON: "mdi:timer", + ATTR_API_DATA_FIELD: ATTR_API_DATA_FIELD_BOOT_TIME, + ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP, + }, +} diff --git a/homeassistant/components/vilfo/manifest.json b/homeassistant/components/vilfo/manifest.json new file mode 100644 index 00000000000..cedb485fab3 --- /dev/null +++ b/homeassistant/components/vilfo/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "vilfo", + "name": "Vilfo Router", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/vilfo", + "requirements": ["vilfo-api-client==0.3.2"], + "dependencies": [], + "codeowners": ["@ManneW"] +} diff --git a/homeassistant/components/vilfo/sensor.py b/homeassistant/components/vilfo/sensor.py new file mode 100644 index 00000000000..e2909647c2d --- /dev/null +++ b/homeassistant/components/vilfo/sensor.py @@ -0,0 +1,94 @@ +"""Support for Vilfo Router sensors.""" +from homeassistant.helpers.entity import Entity + +from .const import ( + ATTR_API_DATA_FIELD, + ATTR_DEVICE_CLASS, + ATTR_ICON, + ATTR_LABEL, + ATTR_UNIT, + DOMAIN, + ROUTER_DEFAULT_MODEL, + ROUTER_DEFAULT_NAME, + ROUTER_MANUFACTURER, + SENSOR_TYPES, +) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add Vilfo Router entities from a config_entry.""" + vilfo = hass.data[DOMAIN][config_entry.entry_id] + + sensors = [] + + for sensor_type in SENSOR_TYPES: + sensors.append(VilfoRouterSensor(sensor_type, vilfo)) + + async_add_entities(sensors, True) + + +class VilfoRouterSensor(Entity): + """Define a Vilfo Router Sensor.""" + + def __init__(self, sensor_type, api): + """Initialize.""" + self.api = api + self.sensor_type = sensor_type + self._device_info = { + "identifiers": {(DOMAIN, api.host, api.mac_address)}, + "name": ROUTER_DEFAULT_NAME, + "manufacturer": ROUTER_MANUFACTURER, + "model": ROUTER_DEFAULT_MODEL, + "sw_version": api.firmware_version, + } + self._unique_id = f"{self.api.unique_id}_{self.sensor_type}" + self._state = None + + @property + def available(self): + """Return whether the sensor is available or not.""" + return self.api.available + + @property + def device_info(self): + """Return the device info.""" + return self._device_info + + @property + def device_class(self): + """Return the device class.""" + return SENSOR_TYPES[self.sensor_type].get(ATTR_DEVICE_CLASS) + + @property + def icon(self): + """Return the icon for the sensor.""" + return SENSOR_TYPES[self.sensor_type][ATTR_ICON] + + @property + def name(self): + """Return the name of the sensor.""" + parent_device_name = self._device_info["name"] + sensor_name = SENSOR_TYPES[self.sensor_type][ATTR_LABEL] + return f"{parent_device_name} {sensor_name}" + + @property + def state(self): + """Return the state.""" + return self._state + + @property + def unique_id(self): + """Return a unique_id for this entity.""" + return self._unique_id + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity.""" + return SENSOR_TYPES[self.sensor_type].get(ATTR_UNIT) + + async def async_update(self): + """Update the router data.""" + await self.api.async_update() + self._state = self.api.data.get( + SENSOR_TYPES[self.sensor_type][ATTR_API_DATA_FIELD] + ) diff --git a/homeassistant/components/vilfo/strings.json b/homeassistant/components/vilfo/strings.json new file mode 100644 index 00000000000..e7a55c55f1f --- /dev/null +++ b/homeassistant/components/vilfo/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "title": "Vilfo Router", + "step": { + "user": { + "title": "Connect to the Vilfo Router", + "description": "Set up the Vilfo Router integration. You need your Vilfo Router hostname/IP and an API access token. For additional information on this integration and how to get those details, visit: https://www.home-assistant.io/integrations/vilfo", + "data": { + "host": "Router hostname or IP", + "access_token": "Access token for the Vilfo Router API" + } + } + }, + "error": { + "cannot_connect": "Failed to connect. Please check the information you provided and try again.", + "invalid_auth": "Invalid authentication. Please check the access token and try again.", + "unknown": "An unexpected error occurred while setting up the integration." + }, + "abort": { + "already_configured": "This Vilfo Router is already configured." + } + } +} diff --git a/homeassistant/components/vivotek/camera.py b/homeassistant/components/vivotek/camera.py index f4a195f5b0c..6bf9fdace5a 100644 --- a/homeassistant/components/vivotek/camera.py +++ b/homeassistant/components/vivotek/camera.py @@ -25,8 +25,8 @@ CONF_FRAMERATE = "framerate" CONF_SECURITY_LEVEL = "security_level" CONF_STREAM_PATH = "stream_path" -DEFAULT_CAMERA_BRAND = "Vivotek" -DEFAULT_NAME = "Vivotek Camera" +DEFAULT_CAMERA_BRAND = "VIVOTEK" +DEFAULT_NAME = "VIVOTEK Camera" DEFAULT_EVENT_0_KEY = "event_i0_enable" DEFAULT_SECURITY_LEVEL = "admin" DEFAULT_STREAM_SOURCE = "live.sdp" diff --git a/homeassistant/components/vivotek/manifest.json b/homeassistant/components/vivotek/manifest.json index 9246bc4c89b..3b4a4211f34 100644 --- a/homeassistant/components/vivotek/manifest.json +++ b/homeassistant/components/vivotek/manifest.json @@ -1,6 +1,6 @@ { "domain": "vivotek", - "name": "Vivotek", + "name": "VIVOTEK", "documentation": "https://www.home-assistant.io/integrations/vivotek", "requirements": ["libpyvivotek==0.4.0"], "dependencies": [], diff --git a/homeassistant/components/vizio/.translations/ca.json b/homeassistant/components/vizio/.translations/ca.json index 6901ffc1736..834138e9221 100644 --- a/homeassistant/components/vizio/.translations/ca.json +++ b/homeassistant/components/vizio/.translations/ca.json @@ -1,18 +1,19 @@ { "config": { "abort": { - "already_in_progress": "El flux de configuraci\u00f3 pel component Vizio ja est\u00e0 en curs.", + "already_in_progress": "El flux de dades de configuraci\u00f3 pel component Vizio ja est\u00e0 en curs.", "already_setup": "Aquesta entrada ja ha estat configurada.", "already_setup_with_diff_host_and_name": "Sembla que aquesta entrada ja s'ha configurat amb un amfitri\u00f3 i nom diferents a partir del n\u00famero de s\u00e8rie. Elimina les entrades antigues de configuraction.yaml i del men\u00fa d'integracions abans de provar d'afegir el dispositiu novament.", "host_exists": "Ja existeix un component Vizio configurat amb el host.", "name_exists": "Ja existeix un component Vizio configurat amb el nom.", + "updated_entry": "Aquesta entrada ja s'ha configurat per\u00f2 el nom i les opcions definides a la configuraci\u00f3 no coincideixen amb els valors importats anteriorment, en conseq\u00fc\u00e8ncia, s'han actualitzat.", "updated_options": "Aquesta entrada ja s'ha configurat per\u00f2 les opcions definides a la configuraci\u00f3 no coincideixen amb els valors importats anteriorment, en conseq\u00fc\u00e8ncia, s'han actualitzat.", "updated_volume_step": "Aquesta entrada ja s'ha configurat per\u00f2 la mida de l'increment de volum definit a la configuraci\u00f3 no coincideix, en conseq\u00fc\u00e8ncia, s'ha actualitzat." }, "error": { "cant_connect": "No s'ha pogut connectar amb el dispositiu. [Comprova la documentaci\u00f3](https://www.home-assistant.io/integrations/vizio/) i torna a verificar que: \n - El dispositiu est\u00e0 engegat \n - El dispositiu est\u00e0 connectat a la xarxa \n - Els valors que has intridu\u00eft s\u00f3n correctes\n abans d\u2019intentar tornar a presentar.", - "host_exists": "L'amfitri\u00f3 ja est\u00e0 configurat.", - "name_exists": "El nom ja est\u00e0 configurat.", + "host_exists": "Dispositiu Vizio amb aquest nom d'amfitri\u00f3 ja configurat.", + "name_exists": "Dispositiu Vizio amb aquest nom ja configurat.", "tv_needs_token": "Si el tipus de dispositiu \u00e9s 'tv', cal un testimoni d'acc\u00e9s v\u00e0lid (token)." }, "step": { diff --git a/homeassistant/components/vizio/.translations/da.json b/homeassistant/components/vizio/.translations/da.json index 9ec9c4122ee..9bfd5864025 100644 --- a/homeassistant/components/vizio/.translations/da.json +++ b/homeassistant/components/vizio/.translations/da.json @@ -24,7 +24,7 @@ "host": ":", "name": "Navn" }, - "title": "Ops\u00e6tning af Vizio SmartCast-klient" + "title": "Ops\u00e6t Vizio SmartCast-enhed" } }, "title": "Vizio SmartCast" diff --git a/homeassistant/components/vizio/.translations/en.json b/homeassistant/components/vizio/.translations/en.json index 80d8f500615..cee436c9647 100644 --- a/homeassistant/components/vizio/.translations/en.json +++ b/homeassistant/components/vizio/.translations/en.json @@ -6,7 +6,7 @@ "already_setup_with_diff_host_and_name": "This entry appears to have already been setup with a different host and name based on its serial number. Please remove any old entries from your configuration.yaml and from the Integrations menu before reattempting to add this device.", "host_exists": "Vizio component with host already configured.", "name_exists": "Vizio component with name already configured.", - "updated_entry": "This entry has already been setup but the name and/or options defined in the config do not match the previously imported config so the config entry has been updated accordingly.", + "updated_entry": "This entry has already been setup but the name and/or options defined in the configuration do not match the previously imported configuration, so the configuration entry has been updated accordingly.", "updated_options": "This entry has already been setup but the options defined in the config do not match the previously imported options values so the config entry has been updated accordingly.", "updated_volume_step": "This entry has already been setup but the volume step size in the config does not match the config entry so the config entry has been updated accordingly." }, @@ -24,7 +24,7 @@ "host": ":", "name": "Name" }, - "title": "Setup Vizio SmartCast Client" + "title": "Setup Vizio SmartCast Device" } }, "title": "Vizio SmartCast" diff --git a/homeassistant/components/vizio/.translations/es.json b/homeassistant/components/vizio/.translations/es.json index 009f93a50c6..408d94825f1 100644 --- a/homeassistant/components/vizio/.translations/es.json +++ b/homeassistant/components/vizio/.translations/es.json @@ -6,6 +6,7 @@ "already_setup_with_diff_host_and_name": "Esta entrada parece haber sido ya configurada con un host y un nombre diferentes basados en su n\u00famero de serie. Elimine las entradas antiguas de su archivo configuration.yaml y del men\u00fa Integraciones antes de volver a intentar agregar este dispositivo.", "host_exists": "Host ya configurado del componente de Vizio", "name_exists": "Nombre ya configurado del componente de Vizio", + "updated_entry": "Esta entrada ya ha sido configurada pero el nombre y/o las opciones definidas en la configuraci\u00f3n no coinciden con la configuraci\u00f3n previamente importada, por lo que la entrada de la configuraci\u00f3n ha sido actualizada en consecuencia.", "updated_options": "Esta entrada ya ha sido configurada pero las opciones definidas en la configuraci\u00f3n no coinciden con los valores de las opciones importadas previamente, por lo que la entrada de la configuraci\u00f3n ha sido actualizada en consecuencia.", "updated_volume_step": "Esta entrada ya ha sido configurada pero el tama\u00f1o del paso de volumen en la configuraci\u00f3n no coincide con la entrada de la configuraci\u00f3n, por lo que la entrada de la configuraci\u00f3n ha sido actualizada en consecuencia." }, diff --git a/homeassistant/components/vizio/.translations/fr.json b/homeassistant/components/vizio/.translations/fr.json index 78d2347bfac..cf0cdea787f 100644 --- a/homeassistant/components/vizio/.translations/fr.json +++ b/homeassistant/components/vizio/.translations/fr.json @@ -6,6 +6,7 @@ "already_setup_with_diff_host_and_name": "Cette entr\u00e9e semble avoir d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9e avec un h\u00f4te et un nom diff\u00e9rents en fonction de son num\u00e9ro de s\u00e9rie. Veuillez supprimer toutes les anciennes entr\u00e9es de votre configuration.yaml et du menu Int\u00e9grations avant de r\u00e9essayer d'ajouter ce p\u00e9riph\u00e9rique.", "host_exists": "Composant Vizio avec h\u00f4te d\u00e9j\u00e0 configur\u00e9.", "name_exists": "Composant Vizio dont le nom est d\u00e9j\u00e0 configur\u00e9.", + "updated_entry": "Cette entr\u00e9e a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9e mais le nom et/ou les options d\u00e9finis dans la configuration ne correspondent pas \u00e0 la configuration pr\u00e9c\u00e9demment import\u00e9e, de sorte que l'entr\u00e9e de configuration a \u00e9t\u00e9 mise \u00e0 jour en cons\u00e9quence.", "updated_options": "Cette entr\u00e9e a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9e mais les options d\u00e9finies dans la configuration ne correspondent pas aux valeurs des options pr\u00e9c\u00e9demment import\u00e9es, de sorte que l'entr\u00e9e de configuration a \u00e9t\u00e9 mise \u00e0 jour en cons\u00e9quence.", "updated_volume_step": "Cette entr\u00e9e a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9e, mais la taille du pas du volume dans la configuration ne correspond pas \u00e0 l'entr\u00e9e de configuration, de sorte que l'entr\u00e9e de configuration a \u00e9t\u00e9 mise \u00e0 jour en cons\u00e9quence." }, @@ -32,7 +33,8 @@ "step": { "init": { "data": { - "timeout": "D\u00e9lai d'expiration de la demande d'API (secondes)" + "timeout": "D\u00e9lai d'expiration de la demande d'API (secondes)", + "volume_step": "Taille du pas de volume" }, "title": "Mettre \u00e0 jour les options de Vizo SmartCast" } diff --git a/homeassistant/components/vizio/.translations/hu.json b/homeassistant/components/vizio/.translations/hu.json new file mode 100644 index 00000000000..650d5133dbd --- /dev/null +++ b/homeassistant/components/vizio/.translations/hu.json @@ -0,0 +1,43 @@ +{ + "config": { + "abort": { + "already_in_progress": "A vizio komponens konfigur\u00e1ci\u00f3s folyamata m\u00e1r folyamatban van.", + "already_setup": "Ez a bejegyz\u00e9s m\u00e1r be van \u00e1ll\u00edtva.", + "already_setup_with_diff_host_and_name": "\u00dagy t\u0171nik, hogy ez a bejegyz\u00e9s m\u00e1r be van \u00e1ll\u00edtva egy m\u00e1sik \u00e1llom\u00e1ssal \u00e9s n\u00e9vvel a sorozatsz\u00e1ma alapj\u00e1n. T\u00e1vol\u00edtsa el a r\u00e9gi bejegyz\u00e9seket a configuration.yaml \u00e9s az Integr\u00e1ci\u00f3k men\u00fcb\u0151l, miel\u0151tt \u00fajra megpr\u00f3b\u00e1ln\u00e1 hozz\u00e1adni ezt az eszk\u00f6zt.", + "host_exists": "Vizio-\u00f6sszetev\u0151, amelynek az kiszolg\u00e1l\u00f3neve m\u00e1r konfigur\u00e1lva van.", + "name_exists": "Vizio-\u00f6sszetev\u0151, amelynek neve m\u00e1r konfigur\u00e1lva van.", + "updated_entry": "Ez a bejegyz\u00e9s m\u00e1r be van \u00e1ll\u00edtva, de a konfigur\u00e1ci\u00f3ban defini\u00e1lt n\u00e9v \u00e9s/vagy be\u00e1ll\u00edt\u00e1sok nem egyeznek meg a kor\u00e1bban import\u00e1lt konfigur\u00e1ci\u00f3val, \u00edgy a konfigur\u00e1ci\u00f3s bejegyz\u00e9s ennek megfelel\u0151en friss\u00fclt.", + "updated_options": "Ez a bejegyz\u00e9s m\u00e1r be van \u00e1ll\u00edtva, de a konfigur\u00e1ci\u00f3ban megadott be\u00e1ll\u00edt\u00e1sok nem egyeznek meg a kor\u00e1bban import\u00e1lt be\u00e1ll\u00edt\u00e1si \u00e9rt\u00e9kekkel, \u00edgy a konfigur\u00e1ci\u00f3s bejegyz\u00e9s ennek megfelel\u0151en friss\u00fclt.", + "updated_volume_step": "Ez a bejegyz\u00e9s m\u00e1r be van \u00e1ll\u00edtva, de a konfigur\u00e1ci\u00f3ban l\u00e9v\u0151 henger\u0151l\u00e9p\u00e9s m\u00e9rete nem egyezik meg a konfigur\u00e1ci\u00f3s bejegyz\u00e9ssel, \u00edgy a konfigur\u00e1ci\u00f3s bejegyz\u00e9s ennek megfelel\u0151en friss\u00fclt." + }, + "error": { + "cant_connect": "Nem lehetett csatlakozni az eszk\u00f6zh\u00f6z. [Tekintsd \u00e1t a dokumentumokat] (https://www.home-assistant.io/integrations/vizio/) \u00e9s \u00fajra ellen\u0151rizd, hogy:\n- A k\u00e9sz\u00fcl\u00e9k be van kapcsolva\n- A k\u00e9sz\u00fcl\u00e9k csatlakozik a h\u00e1l\u00f3zathoz\n- A kit\u00f6lt\u00f6tt \u00e9rt\u00e9kek pontosak\nmiel\u0151tt \u00fajra elk\u00fclden\u00e9d.", + "host_exists": "A megadott kiszolg\u00e1l\u00f3n\u00e9vvel rendelkez\u0151 Vizio-eszk\u00f6z m\u00e1r konfigur\u00e1lva van.", + "name_exists": "A megadott n\u00e9vvel rendelkez\u0151 Vizio-eszk\u00f6z m\u00e1r konfigur\u00e1lva van.", + "tv_needs_token": "Ha az eszk\u00f6z t\u00edpusa \"tv\", akkor \u00e9rv\u00e9nyes hozz\u00e1f\u00e9r\u00e9si tokenre van sz\u00fcks\u00e9g." + }, + "step": { + "user": { + "data": { + "access_token": "Hozz\u00e1f\u00e9r\u00e9si token", + "device_class": "Eszk\u00f6zt\u00edpus", + "name": "N\u00e9v" + }, + "title": "A Vizio SmartCast Client be\u00e1ll\u00edt\u00e1sa" + } + }, + "title": "Vizio SmartCast" + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "API-k\u00e9r\u00e9s id\u0151t\u00fall\u00e9p\u00e9se (m\u00e1sodpercben)", + "volume_step": "Hanger\u0151 l\u00e9p\u00e9s nagys\u00e1ga" + }, + "title": "Friss\u00edtse a Vizo SmartCast be\u00e1ll\u00edt\u00e1sokat" + } + }, + "title": "Friss\u00edtse a Vizo SmartCast be\u00e1ll\u00edt\u00e1sokat" + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/.translations/it.json b/homeassistant/components/vizio/.translations/it.json index f3cb7b83026..dd27133453e 100644 --- a/homeassistant/components/vizio/.translations/it.json +++ b/homeassistant/components/vizio/.translations/it.json @@ -6,6 +6,7 @@ "already_setup_with_diff_host_and_name": "Sembra che questa voce sia gi\u00e0 stata configurata con un host e un nome diversi in base al suo numero seriale. Rimuovere eventuali voci precedenti da configuration.yaml e dal menu Integrazioni prima di tentare nuovamente di aggiungere questo dispositivo.", "host_exists": "Componente Vizio con host gi\u00e0 configurato.", "name_exists": "Componente Vizio con nome gi\u00e0 configurato.", + "updated_entry": "Questa voce \u00e8 gi\u00e0 stata configurata, ma il nome e/o le opzioni definite nella configurazione non corrispondono alla configurazione importata in precedenza, pertanto la voce di configurazione \u00e8 stata aggiornata di conseguenza.", "updated_options": "Questa voce \u00e8 gi\u00e0 stata impostata, ma le opzioni definite nella configurazione non corrispondono ai valori delle opzioni importate in precedenza, quindi la voce di configurazione \u00e8 stata aggiornata di conseguenza.", "updated_volume_step": "Questa voce \u00e8 gi\u00e0 stata impostata, ma la dimensione del passo del volume nella configurazione non corrisponde alla voce di configurazione, quindi \u00e8 stata aggiornata di conseguenza." }, diff --git a/homeassistant/components/vizio/.translations/ko.json b/homeassistant/components/vizio/.translations/ko.json index 4c0460ec0e1..64c0887b3f8 100644 --- a/homeassistant/components/vizio/.translations/ko.json +++ b/homeassistant/components/vizio/.translations/ko.json @@ -24,7 +24,7 @@ "host": "<\ud638\uc2a4\ud2b8/ip>:", "name": "\uc774\ub984" }, - "title": "Vizio SmartCast \ud074\ub77c\uc774\uc5b8\ud2b8 \uc124\uc815" + "title": "Vizio SmartCast \uae30\uae30 \uc124\uc815" } }, "title": "Vizio SmartCast" diff --git a/homeassistant/components/vizio/.translations/nl.json b/homeassistant/components/vizio/.translations/nl.json new file mode 100644 index 00000000000..bbc95d73bbc --- /dev/null +++ b/homeassistant/components/vizio/.translations/nl.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_in_progress": "Configuratie stroom voor vizio component al in uitvoering.", + "already_setup": "Dit item is al ingesteld.", + "already_setup_with_diff_host_and_name": "Dit item lijkt al te zijn ingesteld met een andere host en naam op basis van het serienummer. Verwijder alle oude vermeldingen uit uw configuratie.yaml en uit het menu Integraties voordat u opnieuw probeert dit apparaat toe te voegen.", + "host_exists": "Vizio apparaat met opgegeven host al geconfigureerd.", + "name_exists": "Vizio apparaat met opgegeven naam al geconfigureerd.", + "updated_entry": "Dit item is al ingesteld, maar de naam en/of opties die zijn gedefinieerd in de configuratie komen niet overeen met de eerder ge\u00efmporteerde configuratie, dus het configuratie-item is dienovereenkomstig bijgewerkt.", + "updated_options": "Dit item is al ingesteld, maar de opties die in de configuratie zijn gedefinieerd komen niet overeen met de eerder ge\u00efmporteerde optiewaarden, dus de configuratie-invoer is dienovereenkomstig bijgewerkt.", + "updated_volume_step": "Dit item is al ingesteld, maar de volumestapgrootte in de configuratie komt niet overeen met het configuratie-item, dus het configuratie-item is dienovereenkomstig bijgewerkt." + }, + "error": { + "cant_connect": "Kan geen verbinding maken met het apparaat. [Bekijk de documenten] (https://www.home-assistant.io/integrations/vizio/) en controleer of:\n- Het apparaat is ingeschakeld\n- Het apparaat is aangesloten op het netwerk\n- De waarden die u ingevuld correct zijn\nvoordat u weer probeert om opnieuw in te dienen.", + "host_exists": "Vizio apparaat met opgegeven host al geconfigureerd.", + "name_exists": "Vizio apparaat met opgegeven naam al geconfigureerd.", + "tv_needs_token": "Wanneer het apparaattype `tv` is, dan is er een geldig toegangstoken nodig." + }, + "step": { + "user": { + "data": { + "access_token": "Toegangstoken", + "device_class": "Apparaattype", + "host": ":", + "name": "Naam" + }, + "title": "Vizio SmartCast Client instellen" + } + }, + "title": "Vizio SmartCast" + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Time-out van API-aanvragen (seconden)", + "volume_step": "Volume Stapgrootte" + }, + "title": "Update Vizo SmartCast Opties" + } + }, + "title": "Update Vizo SmartCast Opties" + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/.translations/no.json b/homeassistant/components/vizio/.translations/no.json index be1cae7aaf1..0b92497a5e7 100644 --- a/homeassistant/components/vizio/.translations/no.json +++ b/homeassistant/components/vizio/.translations/no.json @@ -6,7 +6,7 @@ "already_setup_with_diff_host_and_name": "Denne oppf\u00f8ringen ser ut til \u00e5 allerede v\u00e6re konfigurert med en annen vert og navn basert p\u00e5 serienummeret. Fjern den gamle oppf\u00f8ringer fra konfigurasjonen.yaml og fra integrasjonsmenyen f\u00f8r du pr\u00f8ver ut \u00e5 legge til denne enheten p\u00e5 nytt.", "host_exists": "Vizio komponent med vert allerede konfigurert.", "name_exists": "Vizio-komponent med navn som allerede er konfigurert.", - "updated_entry": "Denne oppf\u00f8ringen er allerede konfigurert, men navnet og / eller alternativene som er definert i konfigurasjonen, stemmer ikke overens med den tidligere importerte konfigurasjonen, s\u00e5 konfigurasjonsoppf\u00f8ringen er oppdatert deretter.", + "updated_entry": "Denne oppf\u00f8ringen er allerede konfigurert, men navnet og / eller alternativene som er definert i konfigurasjonen samsvarer ikke med den tidligere importerte konfigurasjonen, s\u00e5 konfigurasjonsoppf\u00f8ringen er oppdatert deretter.", "updated_options": "Denne oppf\u00f8ringen er allerede konfigurert, men alternativene som er definert i konfigurasjonen samsvarer ikke med de tidligere importerte alternativverdiene, s\u00e5 konfigurasjonsoppf\u00f8ringen er oppdatert deretter.", "updated_volume_step": "Denne oppf\u00f8ringen er allerede konfigurert, men volumstrinnst\u00f8rrelsen i konfigurasjonen samsvarer ikke med konfigurasjonsoppf\u00f8ringen, s\u00e5 konfigurasjonsoppf\u00f8ringen er oppdatert deretter." }, @@ -24,7 +24,7 @@ "host": ":", "name": "Navn" }, - "title": "Oppsett Vizio SmartCast Client" + "title": "Sett opp Vizio SmartCast-enhet" } }, "title": "Vizio SmartCast" diff --git a/homeassistant/components/vizio/.translations/sl.json b/homeassistant/components/vizio/.translations/sl.json new file mode 100644 index 00000000000..55faaaf26a8 --- /dev/null +++ b/homeassistant/components/vizio/.translations/sl.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfiguracijski tok za komponento vizio je \u017ee v teku.", + "already_setup": "Ta vnos je \u017ee nastavljen.", + "already_setup_with_diff_host_and_name": "Zdi se, da je bil ta vnos \u017ee nastavljen z drugim gostiteljem in imenom glede na njegovo serijsko \u0161tevilko. Pred ponovnim poskusom dodajanja te naprave, odstranite vse stare vnose iz config.yaml in iz menija Integrations.", + "host_exists": "VIZIO komponenta z gostiteljem \u017ee nastavljen.", + "name_exists": "Vizio komponenta z imenom je \u017ee konfigurirana.", + "updated_entry": "Ta vnos je \u017ee nastavljen, vendar se ime in / ali mo\u017enosti, opredeljene v config, ne ujemajo s predhodno uvo\u017eenim configom, zato je bil vnos konfiguracije ustrezno posodobljen.", + "updated_options": "Ta vnos je \u017ee nastavljen, vendar se mo\u017enosti, definirane v config-u, ne ujemajo s predhodno uvo\u017eenimi vrednostmi, zato je bil vnos konfiguracije ustrezno posodobljen.", + "updated_volume_step": "Ta vnos je \u017ee nastavljen, vendar velikost koraka glasnosti v config-u ne ustreza vnosu konfiguracije, zato je bil vnos konfiguracije ustrezno posodobljen." + }, + "error": { + "cant_connect": "Ni bilo mogo\u010de povezati z napravo. [Preglejte dokumente] (https://www.home-assistant.io/integrations/vizio/) in ponovno preverite, ali: \n \u2013 Naprava je vklopljena \n \u2013 Naprava je povezana z omre\u017ejem \n \u2013 Vrednosti, ki ste jih izpolnili, so to\u010dne \nnato poskusite ponovno.", + "host_exists": "Naprava Vizio z dolo\u010denim gostiteljem je \u017ee konfigurirana.", + "name_exists": "Naprava Vizio z navedenim imenom je \u017ee konfigurirana.", + "tv_needs_token": "Ko je vrsta naprave\u00bb TV \u00ab, je potreben veljaven \u017eeton za dostop." + }, + "step": { + "user": { + "data": { + "access_token": "\u017deton za dostop", + "device_class": "Vrsta naprave", + "host": ":", + "name": "Ime" + }, + "title": "Nastavite odjemalec Vizio SmartCast" + } + }, + "title": "Vizio SmartCast" + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "\u010casovna omejitev zahteve za API (sekunde)", + "volume_step": "Velikost koraka glasnosti" + }, + "title": "Posodobite mo\u017enosti Vizo SmartCast" + } + }, + "title": "Posodobite mo\u017enosti Vizo SmartCast" + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/.translations/sv.json b/homeassistant/components/vizio/.translations/sv.json index 2c127f602ce..072b441a071 100644 --- a/homeassistant/components/vizio/.translations/sv.json +++ b/homeassistant/components/vizio/.translations/sv.json @@ -1,31 +1,44 @@ { "config": { "abort": { - "host_exists": "Vizio-komponenten med v\u00e4rdnamnet \u00e4r redan konfigurerad." + "already_in_progress": "Konfigurationsfl\u00f6de f\u00f6r vizio-komponenten p\u00e5g\u00e5r\nredan.", + "already_setup": "Den h\u00e4r posten har redan st\u00e4llts in.", + "already_setup_with_diff_host_and_name": "Den h\u00e4r posten verkar redan ha st\u00e4llts in med en annan v\u00e4rd och ett annat namn baserat p\u00e5 dess serienummer. Ta bort alla gamla poster fr\u00e5n configuration.yaml och fr\u00e5n menyn Integrationer innan du f\u00f6rs\u00f6ker l\u00e4gga till den h\u00e4r enheten igen.", + "host_exists": "Vizio-komponenten med v\u00e4rdnamnet \u00e4r redan konfigurerad.", + "name_exists": "Vizio-komponent med namn redan konfigurerad.", + "updated_entry": "Den h\u00e4r posten har redan konfigurerats, men namnet och/eller alternativen som definierats i konfigurationen matchar inte den tidigare importerade konfigurationen och d\u00e4rf\u00f6r har konfigureringsposten uppdaterats i enlighet med detta.", + "updated_options": "Den h\u00e4r posten har redan st\u00e4llts in men de alternativ som definierats i konfigurationen matchar inte de tidigare importerade alternativv\u00e4rdena s\u00e5 konfigurationsposten har uppdaterats i enlighet med detta.", + "updated_volume_step": "Den h\u00e4r posten har redan st\u00e4llts in men volymstegstorleken i konfigurationen matchar inte konfigurationsposten s\u00e5 konfigurationsposten har uppdaterats i enlighet med detta." }, "error": { + "cant_connect": "Det gick inte att ansluta till enheten. [Granska dokumentationen] (https://www.home-assistant.io/integrations/vizio/) och p\u00e5 nytt kontrollera att\n- Enheten \u00e4r p\u00e5slagen\n- Enheten \u00e4r ansluten till n\u00e4tverket\n- De v\u00e4rden du fyllt i \u00e4r korrekta\ninnan du f\u00f6rs\u00f6ker skicka in igen.", "host_exists": "Vizio-enheten med angivet v\u00e4rdnamn \u00e4r redan konfigurerad.", - "name_exists": "Vizio-enheten med angivet namn \u00e4r redan konfigurerad." + "name_exists": "Vizio-enheten med angivet namn \u00e4r redan konfigurerad.", + "tv_needs_token": "N\u00e4r Enhetstyp \u00e4r 'tv' beh\u00f6vs en giltig \u00e5tkomsttoken." }, "step": { "user": { "data": { "access_token": "\u00c5tkomstnyckel", "device_class": "Enhetstyp", + "host": ":", "name": "Namn" }, "title": "St\u00e4ll in Vizio SmartCast-klient" } }, - "title": "" + "title": "Vizio SmartCast" }, "options": { "step": { "init": { "data": { - "timeout": "Timeout f\u00f6r API-anrop (sekunder)" - } + "timeout": "Timeout f\u00f6r API-anrop (sekunder)", + "volume_step": "Storlek p\u00e5 volymsteg" + }, + "title": "Uppdatera Vizo SmartCast-alternativ" } - } + }, + "title": "Uppdatera Vizo SmartCast-alternativ" } } \ No newline at end of file diff --git a/homeassistant/components/vizio/.translations/zh-Hant.json b/homeassistant/components/vizio/.translations/zh-Hant.json index cd859977551..24128bb1b9e 100644 --- a/homeassistant/components/vizio/.translations/zh-Hant.json +++ b/homeassistant/components/vizio/.translations/zh-Hant.json @@ -24,7 +24,7 @@ "host": "<\u4e3b\u6a5f\u7aef/IP>:", "name": "\u540d\u7a31" }, - "title": "\u8a2d\u5b9a Vizio SmartCast \u5ba2\u6236\u7aef" + "title": "\u8a2d\u5b9a Vizio SmartCast \u8a2d\u5099" } }, "title": "Vizio SmartCast" diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index 04f70da4a8c..ea0e9ede237 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -177,7 +177,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if entry.data[CONF_NAME] != import_config[CONF_NAME]: updated_name[CONF_NAME] = import_config[CONF_NAME] - if entry.data[CONF_VOLUME_STEP] != import_config[CONF_VOLUME_STEP]: + if entry.data.get(CONF_VOLUME_STEP) != import_config[CONF_VOLUME_STEP]: updated_options[CONF_VOLUME_STEP] = import_config[CONF_VOLUME_STEP] if updated_options or updated_name: @@ -205,6 +205,11 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> Dict[str, Any]: """Handle zeroconf discovery.""" + # Set unique ID early to prevent device from getting rediscovered multiple times + await self.async_set_unique_id( + unique_id=discovery_info[CONF_HOST].split(":")[0], raise_on_progress=True + ) + discovery_info[ CONF_HOST ] = f"{discovery_info[CONF_HOST]}:{discovery_info[CONF_PORT]}" diff --git a/homeassistant/components/vizio/const.py b/homeassistant/components/vizio/const.py index 92fb37c153e..e3ac66e05c3 100644 --- a/homeassistant/components/vizio/const.py +++ b/homeassistant/components/vizio/const.py @@ -1,6 +1,4 @@ """Constants used by vizio component.""" -from datetime import timedelta - from pyvizio.const import ( DEVICE_CLASS_SPEAKER as VIZIO_DEVICE_CLASS_SPEAKER, DEVICE_CLASS_TV as VIZIO_DEVICE_CLASS_TV, @@ -72,6 +70,3 @@ VIZIO_SCHEMA = { vol.Coerce(int), vol.Range(min=1, max=10) ), } - -MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) diff --git a/homeassistant/components/vizio/manifest.json b/homeassistant/components/vizio/manifest.json index ea1162540cf..bf88ed9f437 100644 --- a/homeassistant/components/vizio/manifest.json +++ b/homeassistant/components/vizio/manifest.json @@ -2,9 +2,10 @@ "domain": "vizio", "name": "Vizio SmartCast TV", "documentation": "https://www.home-assistant.io/integrations/vizio", - "requirements": ["pyvizio==0.1.4"], + "requirements": ["pyvizio==0.1.21"], "dependencies": [], "codeowners": ["@raman325"], "config_flow": true, - "zeroconf": ["_viziocast._tcp.local."] + "zeroconf": ["_viziocast._tcp.local."], + "quality_scale": "platinum" } diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index b2f529bce10..7d76505a457 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -1,10 +1,10 @@ """Vizio SmartCast Device support.""" +from datetime import timedelta import logging from typing import Callable, List from pyvizio import VizioAsync -from homeassistant import util from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -31,15 +31,13 @@ from .const import ( DEVICE_ID, DOMAIN, ICON, - MIN_TIME_BETWEEN_FORCED_SCANS, - MIN_TIME_BETWEEN_SCANS, SUPPORTED_COMMANDS, VIZIO_DEVICE_CLASSES, ) _LOGGER = logging.getLogger(__name__) - +SCAN_INTERVAL = timedelta(seconds=10) PARALLEL_UPDATES = 0 @@ -55,13 +53,21 @@ async def async_setup_entry( device_class = config_entry.data[CONF_DEVICE_CLASS] # If config entry options not set up, set them up, otherwise assign values managed in options + volume_step = config_entry.options.get( + CONF_VOLUME_STEP, config_entry.data.get(CONF_VOLUME_STEP, DEFAULT_VOLUME_STEP), + ) + + params = {} if not config_entry.options: - volume_step = config_entry.data.get(CONF_VOLUME_STEP, DEFAULT_VOLUME_STEP) - hass.config_entries.async_update_entry( - config_entry, options={CONF_VOLUME_STEP: volume_step} - ) - else: - volume_step = config_entry.options[CONF_VOLUME_STEP] + params["options"] = {CONF_VOLUME_STEP: volume_step} + + if not config_entry.data.get(CONF_VOLUME_STEP): + new_data = config_entry.data.copy() + new_data.update({CONF_VOLUME_STEP: volume_step}) + params["data"] = new_data + + if params: + hass.config_entries.async_update_entry(config_entry, **params) device = VizioAsync( DEVICE_ID, @@ -74,18 +80,7 @@ async def async_setup_entry( ) if not await device.can_connect(): - fail_auth_msg = "" - if token: - fail_auth_msg = f"and auth token '{token}' are correct." - else: - fail_auth_msg = "is correct." - _LOGGER.warning( - "Failed to connect to Vizio device, please check if host '%s' " - "is valid and available. Also check if device class '%s' %s", - host, - device_class, - fail_auth_msg, - ) + _LOGGER.warning("Failed to connect to %s", host) raise PlatformNotReady entity = VizioDevice(config_entry, device, name, volume_step, device_class) @@ -120,17 +115,32 @@ class VizioDevice(MediaPlayerDevice): self._max_volume = float(self._device.get_max_volume()) self._icon = ICON[device_class] self._available = True + self._model = None + self._sw_version = None - @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) async def async_update(self) -> None: """Retrieve latest state of the device.""" + if not self._model: + self._model = await self._device.get_model() + + if not self._sw_version: + self._sw_version = await self._device.get_version() + is_on = await self._device.get_power_state(log_api_exception=False) if is_on is None: - self._available = False + if self._available: + _LOGGER.warning( + "Lost connection to %s", self._config_entry.data[CONF_HOST] + ) + self._available = False return - self._available = True + if not self._available: + _LOGGER.info( + "Restored connection to %s", self._config_entry.data[CONF_HOST] + ) + self._available = True if not is_on: self._state = STATE_OFF @@ -147,9 +157,9 @@ class VizioDevice(MediaPlayerDevice): input_ = await self._device.get_current_input(log_api_exception=False) if input_ is not None: - self._current_input = input_.meta_name + self._current_input = input_ - inputs = await self._device.get_inputs(log_api_exception=False) + inputs = await self._device.get_inputs_list(log_api_exception=False) if inputs is not None: self._available_inputs = [input_.name for input_ in inputs] @@ -157,7 +167,7 @@ class VizioDevice(MediaPlayerDevice): async def _async_send_update_options_signal( hass: HomeAssistantType, config_entry: ConfigEntry ) -> None: - """Send update event when when Vizio config entry is updated.""" + """Send update event when Vizio config entry is updated.""" # Move this method to component level if another entity ever gets added for a single config entry. # See here: https://github.com/home-assistant/home-assistant/pull/30653#discussion_r366426121 async_dispatcher_send(hass, config_entry.entry_id, config_entry) @@ -241,6 +251,8 @@ class VizioDevice(MediaPlayerDevice): "identifiers": {(DOMAIN, self._config_entry.unique_id)}, "name": self.name, "manufacturer": "VIZIO", + "model": self._model, + "sw_version": self._sw_version, } @property @@ -273,10 +285,10 @@ class VizioDevice(MediaPlayerDevice): async def async_select_source(self, source: str) -> None: """Select input source.""" - await self._device.input_switch(source) + await self._device.set_input(source) async def async_volume_up(self) -> None: - """Increasing volume of the device.""" + """Increase volume of the device.""" await self._device.vol_up(num=self._volume_step) if self._volume_level is not None: @@ -285,7 +297,7 @@ class VizioDevice(MediaPlayerDevice): ) async def async_volume_down(self) -> None: - """Decreasing volume of the device.""" + """Decrease volume of the device.""" await self._device.vol_down(num=self._volume_step) if self._volume_level is not None: diff --git a/homeassistant/components/vizio/strings.json b/homeassistant/components/vizio/strings.json index 64b2fb5f936..5a554b7e3db 100644 --- a/homeassistant/components/vizio/strings.json +++ b/homeassistant/components/vizio/strings.json @@ -21,7 +21,7 @@ "abort": { "already_setup": "This entry has already been setup.", "already_setup_with_diff_host_and_name": "This entry appears to have already been setup with a different host and name based on its serial number. Please remove any old entries from your configuration.yaml and from the Integrations menu before reattempting to add this device.", - "updated_entry": "This entry has already been setup but the name and/or options defined in the config do not match the previously imported config so the config entry has been updated accordingly." + "updated_entry": "This entry has already been setup but the name and/or options defined in the configuration do not match the previously imported configuration, so the configuration entry has been updated accordingly." } }, "options": { diff --git a/homeassistant/components/vultr/sensor.py b/homeassistant/components/vultr/sensor.py index fec912f00d8..0bcdcf9d4c1 100644 --- a/homeassistant/components/vultr/sensor.py +++ b/homeassistant/components/vultr/sensor.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME +from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME, DATA_GIGABYTES import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -21,7 +21,7 @@ DEFAULT_NAME = "Vultr {} {}" MONITORED_CONDITIONS = { ATTR_CURRENT_BANDWIDTH_USED: [ "Current Bandwidth Used", - "GB", + DATA_GIGABYTES, "mdi:chart-histogram", ], ATTR_PENDING_CHARGES: ["Pending Charges", "US$", "mdi:currency-usd"], diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index ecff3105ae0..4de0a58a881 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -145,7 +145,7 @@ class WaterHeaterDevice(Entity): @property def capability_attributes(self): - """Return capabilitiy attributes.""" + """Return capability attributes.""" supported_features = self.supported_features or 0 data = { diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 99df9fd17ce..f4d9f97fe42 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -29,6 +29,7 @@ from homeassistant.const import ( CONF_HOST, CONF_NAME, ENTITY_MATCH_ALL, + ENTITY_MATCH_NONE, STATE_OFF, STATE_ON, ) @@ -137,6 +138,9 @@ class LgWebOSMediaPlayerEntity(MediaPlayerDevice): async def async_signal_handler(self, data): """Handle domain-specific signal by calling appropriate method.""" entity_ids = data[ATTR_ENTITY_ID] + if entity_ids == ENTITY_MATCH_NONE: + return + if entity_ids == ENTITY_MATCH_ALL or self.entity_id in entity_ids: params = { key: value diff --git a/homeassistant/components/websocket_api/permissions.py b/homeassistant/components/websocket_api/permissions.py index c270c0f0ccc..bd6013aac0a 100644 --- a/homeassistant/components/websocket_api/permissions.py +++ b/homeassistant/components/websocket_api/permissions.py @@ -7,6 +7,7 @@ from homeassistant.components.lovelace import EVENT_LOVELACE_UPDATED from homeassistant.components.persistent_notification import ( EVENT_PERSISTENT_NOTIFICATIONS_UPDATED, ) +from homeassistant.components.shopping_list import EVENT as EVENT_SHOPPING_LIST_UPDATED from homeassistant.const import ( EVENT_COMPONENT_LOADED, EVENT_CORE_CONFIG_UPDATE, @@ -22,16 +23,17 @@ from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED # These are events that do not contain any sensitive data # Except for state_changed, which is handled accordingly. SUBSCRIBE_WHITELIST = { + EVENT_AREA_REGISTRY_UPDATED, EVENT_COMPONENT_LOADED, EVENT_CORE_CONFIG_UPDATE, + EVENT_DEVICE_REGISTRY_UPDATED, + EVENT_ENTITY_REGISTRY_UPDATED, + EVENT_LOVELACE_UPDATED, EVENT_PANELS_UPDATED, EVENT_PERSISTENT_NOTIFICATIONS_UPDATED, EVENT_SERVICE_REGISTERED, EVENT_SERVICE_REMOVED, + EVENT_SHOPPING_LIST_UPDATED, EVENT_STATE_CHANGED, EVENT_THEMES_UPDATED, - EVENT_AREA_REGISTRY_UPDATED, - EVENT_DEVICE_REGISTRY_UPDATED, - EVENT_ENTITY_REGISTRY_UPDATED, - EVENT_LOVELACE_UPDATED, } diff --git a/homeassistant/components/wink/services.yaml b/homeassistant/components/wink/services.yaml index 93d53159702..500f1fd3f2a 100644 --- a/homeassistant/components/wink/services.yaml +++ b/homeassistant/components/wink/services.yaml @@ -131,7 +131,7 @@ set_nimbus_dial_configuration: description: The minimum value allowed to be set example: 0 max_value: - description: The maximum value allowd to be set + description: The maximum value allowed to be set example: 500 min_position: description: The minimum position the dial hand can rotate to generally [0-360] @@ -141,10 +141,10 @@ set_nimbus_dial_configuration: example: 360 set_nimbus_dial_state: - description: Set the value and lables of an individual nimbus dial + description: Set the value and labels of an individual nimbus dial fields: entity_id: - description: Name fo the entity to set. + description: Name of the entity to set. example: 'wink.nimbus_dial_3' value: description: The value that should be set (Should be between min_value and max_value) diff --git a/homeassistant/components/withings/.translations/de.json b/homeassistant/components/withings/.translations/de.json index 067fa97ebdc..ae8ab679593 100644 --- a/homeassistant/components/withings/.translations/de.json +++ b/homeassistant/components/withings/.translations/de.json @@ -6,7 +6,7 @@ "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." + "default": "Erfolgreiche Authentifizierung mit Withings." }, "step": { "pick_implementation": { diff --git a/homeassistant/components/withings/.translations/fr.json b/homeassistant/components/withings/.translations/fr.json index bd0ec740421..0ad55e7eaa7 100644 --- a/homeassistant/components/withings/.translations/fr.json +++ b/homeassistant/components/withings/.translations/fr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "missing_configuration": "L'int\u00e9gration Withings n'est pas configur\u00e9e. Veuillez suivre la documentation.", "no_flows": "Vous devez configurer Withings avant de pouvoir vous authentifier avec celui-ci. Veuillez lire la documentation." }, "create_entry": { diff --git a/homeassistant/components/withings/.translations/hu.json b/homeassistant/components/withings/.translations/hu.json index 000e19c2067..503013e402f 100644 --- a/homeassistant/components/withings/.translations/hu.json +++ b/homeassistant/components/withings/.translations/hu.json @@ -2,12 +2,31 @@ "config": { "abort": { "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n.", - "missing_configuration": "A Withings integr\u00e1ci\u00f3 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t." + "missing_configuration": "A Withings integr\u00e1ci\u00f3 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t.", + "no_flows": "Konfigur\u00e1lnia kell a Withings-et, miel\u0151tt hiteles\u00edtheti mag\u00e1t vele. K\u00e9rj\u00fck, olvassa el a dokument\u00e1ci\u00f3t." + }, + "create_entry": { + "default": "A Withings sikeresen hiteles\u00edtett." }, "step": { "pick_implementation": { "title": "V\u00e1lassza ki a hiteles\u00edt\u00e9si m\u00f3dszert" + }, + "profile": { + "data": { + "profile": "Profil" + }, + "description": "Melyik profilt v\u00e1lasztottad ki a Withings weboldalon? Fontos, hogy a profilok egyeznek, k\u00fcl\u00f6nben az adatok helytelen c\u00edmk\u00e9vel lesznek ell\u00e1tva.", + "title": "Felhaszn\u00e1l\u00f3i profil." + }, + "user": { + "data": { + "profile": "Profil" + }, + "description": "V\u00e1lasszon egy felhaszn\u00e1l\u00f3i profilt, amelyet szeretn\u00e9, hogy a Home Assistant hozz\u00e1rendeljen a Withings profilhoz. \u00dcgyeljen arra, hogy ugyanazt a felhaszn\u00e1l\u00f3t v\u00e1lassza a Withings oldalon, k\u00fcl\u00f6nben az adatok nem lesznek megfelel\u0151en felcimk\u00e9zve.", + "title": "Felhaszn\u00e1l\u00f3i profil." } - } + }, + "title": "Withings" } } \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/nl.json b/homeassistant/components/withings/.translations/nl.json index c831561a439..0b01fc8c16a 100644 --- a/homeassistant/components/withings/.translations/nl.json +++ b/homeassistant/components/withings/.translations/nl.json @@ -1,12 +1,17 @@ { "config": { "abort": { + "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", + "missing_configuration": "De Withings integratie is niet geconfigureerd. Gelieve de documentatie te volgen.", "no_flows": "U moet Withings configureren voordat u zich ermee kunt verifi\u00ebren. [Gelieve de documentatie te lezen]" }, "create_entry": { "default": "Succesvol geverifieerd met Withings voor het geselecteerde profiel." }, "step": { + "pick_implementation": { + "title": "Kies Authenticatiemethode" + }, "profile": { "data": { "profile": "Profiel" diff --git a/homeassistant/components/withings/.translations/sl.json b/homeassistant/components/withings/.translations/sl.json index 2ee52b29b2d..600b2dbf450 100644 --- a/homeassistant/components/withings/.translations/sl.json +++ b/homeassistant/components/withings/.translations/sl.json @@ -1,12 +1,17 @@ { "config": { "abort": { + "authorize_url_timeout": "\u010casovna omejitev za generiranje potrditvenega URL-ja je potekla.", + "missing_configuration": "Integracija Withings ni konfigurirana. Prosimo, upo\u0161tevajte dokumentacijo.", "no_flows": "Withings morate prvo konfigurirati, preden ga boste lahko uporabili za overitev. Prosimo, preberite dokumentacijo." }, "create_entry": { "default": "Uspe\u0161no overjen z Withings za izbrani profil." }, "step": { + "pick_implementation": { + "title": "Izberite na\u010din preverjanja pristnosti" + }, "profile": { "data": { "profile": "Profil" diff --git a/homeassistant/components/withings/.translations/sv.json b/homeassistant/components/withings/.translations/sv.json index e2493e9afa7..dc8954af2c7 100644 --- a/homeassistant/components/withings/.translations/sv.json +++ b/homeassistant/components/withings/.translations/sv.json @@ -2,12 +2,31 @@ "config": { "abort": { "authorize_url_timeout": "Skapandet av en auktoriseringsadress \u00f6verskred tidsgr\u00e4nsen.", - "missing_configuration": "Withings-integrationen \u00e4r inte konfigurerad. V\u00e4nligen f\u00f6lj dokumentationen." + "missing_configuration": "Withings-integrationen \u00e4r inte konfigurerad. V\u00e4nligen f\u00f6lj dokumentationen.", + "no_flows": "Du m\u00e5ste konfigurera Withings innan du kan autentisera med den. L\u00e4s dokumentationen." + }, + "create_entry": { + "default": "Lyckad autentisering med Withings." }, "step": { "pick_implementation": { "title": "V\u00e4lj autentiseringsmetod" + }, + "profile": { + "data": { + "profile": "Profil" + }, + "description": "Vilken profil valde du p\u00e5 Withings webbplats? Det \u00e4r viktigt att profilerna matchar, annars kommer data att vara felm\u00e4rkta.", + "title": "Anv\u00e4ndarprofil." + }, + "user": { + "data": { + "profile": "Profil" + }, + "description": "V\u00e4lj en anv\u00e4ndarprofil som du vill att Home Assistant ska kartl\u00e4gga med en Withings-profil. Var noga med att v\u00e4lja samma anv\u00e4ndare p\u00e5 visningssidan eller s\u00e5 kommer inte data att betecknas korrekt.", + "title": "Anv\u00e4ndarprofil." } - } + }, + "title": "Withings" } } \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/hu.json b/homeassistant/components/wled/.translations/hu.json new file mode 100644 index 00000000000..644b61ceb73 --- /dev/null +++ b/homeassistant/components/wled/.translations/hu.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Ez a WLED eszk\u00f6z m\u00e1r konfigur\u00e1lva van.", + "connection_error": "Nem siker\u00fclt csatlakozni a WLED eszk\u00f6zh\u00f6z." + }, + "error": { + "connection_error": "Nem siker\u00fclt csatlakozni a WLED eszk\u00f6zh\u00f6z." + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "Hosztn\u00e9v vagy IP c\u00edm" + }, + "description": "\u00c1ll\u00edtsa be a WLED-et, hogy integr\u00e1l\u00f3djon a Home Assistant alkalmaz\u00e1sba.", + "title": "Csatlakoztassa a WLED-t" + }, + "zeroconf_confirm": { + "description": "Hozz\u00e1 akarja adni a {name} `nev\u0171 WLED-et a Home Assistant-hez?", + "title": "Felfedezett WLED eszk\u00f6z" + } + }, + "title": "WLED" + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/pl.json b/homeassistant/components/wled/.translations/pl.json index c10c8ab34d6..6080336c44f 100644 --- a/homeassistant/components/wled/.translations/pl.json +++ b/homeassistant/components/wled/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "To urz\u0105dzenie WLED jest ju\u017c skonfigurowane", + "already_configured": "To urz\u0105dzenie WLED jest ju\u017c skonfigurowane.", "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z urz\u0105dzeniem WLED." }, "error": { diff --git a/homeassistant/components/wled/.translations/sv.json b/homeassistant/components/wled/.translations/sv.json new file mode 100644 index 00000000000..980c023118e --- /dev/null +++ b/homeassistant/components/wled/.translations/sv.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Den h\u00e4r WLED-enheten \u00e4r redan konfigurerad.", + "connection_error": "Det gick inte att ansluta till WLED-enheten." + }, + "error": { + "connection_error": "Det gick inte att ansluta till WLED-enheten." + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "V\u00e4rd eller IP-adress" + }, + "description": "St\u00e4ll in din WLED f\u00f6r att integrera med Home Assistant.", + "title": "L\u00e4nka din WLED" + }, + "zeroconf_confirm": { + "description": "Vill du l\u00e4gga till WLED med namnet `{name}` till Home Assistant?", + "title": "Uppt\u00e4ckt WLED-enhet" + } + }, + "title": "WLED" + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/config_flow.py b/homeassistant/components/wled/config_flow.py index 155cd022fd7..dbcd55a7b17 100644 --- a/homeassistant/components/wled/config_flow.py +++ b/homeassistant/components/wled/config_flow.py @@ -11,8 +11,8 @@ from homeassistant.config_entries import ( ConfigFlow, ) from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME -from homeassistant.helpers import ConfigType from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import ConfigType from .const import DOMAIN # pylint: disable=unused-import diff --git a/homeassistant/components/wled/const.py b/homeassistant/components/wled/const.py index dcfdad963a7..94ee513f134 100644 --- a/homeassistant/components/wled/const.py +++ b/homeassistant/components/wled/const.py @@ -30,4 +30,3 @@ ATTR_UDP_PORT = "udp_port" # Units of measurement CURRENT_MA = "mA" -DATA_BYTES = "bytes" diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index c3fc2d4e6c2..41e03d8c728 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -4,20 +4,13 @@ import logging from typing import Callable, List, Optional, Union from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DEVICE_CLASS_TIMESTAMP +from homeassistant.const import DATA_BYTES, DEVICE_CLASS_TIMESTAMP from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.dt import utcnow from . import WLED, WLEDDeviceEntity -from .const import ( - ATTR_LED_COUNT, - ATTR_MAX_POWER, - CURRENT_MA, - DATA_BYTES, - DATA_WLED_CLIENT, - DOMAIN, -) +from .const import ATTR_LED_COUNT, ATTR_MAX_POWER, CURRENT_MA, DATA_WLED_CLIENT, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index e888b0c5614..21b84d87cbb 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -2,7 +2,7 @@ "domain": "workday", "name": "Workday", "documentation": "https://www.home-assistant.io/integrations/workday", - "requirements": ["holidays==0.9.12"], + "requirements": ["holidays==0.10.1"], "dependencies": [], "codeowners": ["@fabaff"], "quality_scale": "internal" diff --git a/homeassistant/components/wwlln/.translations/pl.json b/homeassistant/components/wwlln/.translations/pl.json index 652d580644f..658dbebbe45 100644 --- a/homeassistant/components/wwlln/.translations/pl.json +++ b/homeassistant/components/wwlln/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "Lokalizacja ju\u017c zarejestrowana" + "identifier_exists": "Lokalizacja jest ju\u017c zarejestrowana." }, "step": { "user": { diff --git a/homeassistant/components/wwlln/geo_location.py b/homeassistant/components/wwlln/geo_location.py index e8dd7ec08c7..e1ca47664d5 100644 --- a/homeassistant/components/wwlln/geo_location.py +++ b/homeassistant/components/wwlln/geo_location.py @@ -35,7 +35,7 @@ DEFAULT_EVENT_NAME = "Lightning Strike: {0}" DEFAULT_ICON = "mdi:flash" DEFAULT_UPDATE_INTERVAL = timedelta(minutes=10) -SIGNAL_DELETE_ENTITY = "delete_entity_{0}" +SIGNAL_DELETE_ENTITY = "wwlln_delete_entity_{0}" async def async_setup_entry(hass, entry, async_add_entities): diff --git a/homeassistant/components/xiaomi_aqara/manifest.json b/homeassistant/components/xiaomi_aqara/manifest.json index 4568f67dbf5..fade5e1a51b 100644 --- a/homeassistant/components/xiaomi_aqara/manifest.json +++ b/homeassistant/components/xiaomi_aqara/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/xiaomi_aqara", "requirements": ["PyXiaomiGateway==0.12.4"], "dependencies": [], + "after_dependencies": ["discovery"], "codeowners": ["@danielhiversen", "@syssi"] } diff --git a/homeassistant/components/xiaomi_miio/air_quality.py b/homeassistant/components/xiaomi_miio/air_quality.py index 50de263fb15..110ca7cff49 100644 --- a/homeassistant/components/xiaomi_miio/air_quality.py +++ b/homeassistant/components/xiaomi_miio/air_quality.py @@ -21,6 +21,8 @@ DEFAULT_NAME = "Xiaomi Miio Air Quality Monitor" ATTR_CO2E = "carbon_dioxide_equivalent" ATTR_TVOC = "total_volatile_organic_compounds" +ATTR_TEMP = "temperature" +ATTR_HUM = "humidity" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -33,6 +35,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( PROP_TO_ATTR = { "carbon_dioxide_equivalent": ATTR_CO2E, "total_volatile_organic_compounds": ATTR_TVOC, + "temperature": ATTR_TEMP, + "humidity": ATTR_HUM, } @@ -91,6 +95,8 @@ class AirMonitorB1(AirQualityEntity): self._carbon_dioxide_equivalent = None self._particulate_matter_2_5 = None self._total_volatile_organic_compounds = None + self._temperature = None + self._humidity = None async def async_update(self): """Fetch state from the miio device.""" @@ -100,6 +106,8 @@ class AirMonitorB1(AirQualityEntity): self._carbon_dioxide_equivalent = state.co2e self._particulate_matter_2_5 = round(state.pm25, 1) self._total_volatile_organic_compounds = round(state.tvoc, 3) + self._temperature = round(state.temperature, 2) + self._humidity = round(state.humidity, 2) self._available = True except DeviceException as ex: self._available = False @@ -150,6 +158,16 @@ class AirMonitorB1(AirQualityEntity): """Return the total volatile organic compounds.""" return self._total_volatile_organic_compounds + @property + def temperature(self): + """Return the current temperature.""" + return self._temperature + + @property + def humidity(self): + """Return the current humidity.""" + return self._humidity + @property def device_state_attributes(self): """Return the state attributes.""" @@ -179,6 +197,8 @@ class AirMonitorS1(AirMonitorB1): self._carbon_dioxide = state.co2 self._particulate_matter_2_5 = state.pm25 self._total_volatile_organic_compounds = state.tvoc + self._temperature = state.temperature + self._humidity = state.humidity self._available = True except DeviceException as ex: self._available = False diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index bcc83bae454..61462bcdbc0 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -19,7 +19,6 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, - ATTR_ENTITY_ID, ATTR_HS_COLOR, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, @@ -27,7 +26,7 @@ from homeassistant.components.light import ( SUPPORT_COLOR_TEMP, Light, ) -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN +from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.util import color, dt diff --git a/homeassistant/components/yamaha/services.yaml b/homeassistant/components/yamaha/services.yaml index 592a1d1342e..c92522008be 100644 --- a/homeassistant/components/yamaha/services.yaml +++ b/homeassistant/components/yamaha/services.yaml @@ -2,7 +2,7 @@ enable_output: description: Enable or disable an output port fields: entity_id: - description: Name(s) of entites to enable/disable port on. + description: Name(s) of entities to enable/disable port on. example: 'media_player.yamaha' port: description: Name of port to enable/disable. diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 35c2a8ddfac..1a181536d0b 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/yeelight", "requirements": ["yeelight==0.5.0"], "dependencies": [], + "after_dependencies": ["discovery"], "codeowners": ["@rytilahti", "@zewelor"] } diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index b4dbbda51f1..206f529344f 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -108,7 +108,6 @@ def setup(hass, config): def stop_zeroconf(_): """Stop Zeroconf.""" - zeroconf.unregister_service(info) zeroconf.close() hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_zeroconf) diff --git a/homeassistant/components/zha/.translations/ca.json b/homeassistant/components/zha/.translations/ca.json index 2b8230ad689..e5181fb5106 100644 --- a/homeassistant/components/zha/.translations/ca.json +++ b/homeassistant/components/zha/.translations/ca.json @@ -54,14 +54,14 @@ "device_shaken": "Dispositiu sacsejat", "device_slid": "Dispositiu lliscat a \"{subtype}\"", "device_tilted": "Dispositiu inclinat", - "remote_button_double_press": "Bot\u00f3 \"{subtype}\" clicat dues vegades consecutives", - "remote_button_long_press": "Bot\u00f3 \"{subtype}\" premut continuament", + "remote_button_double_press": "Bot\u00f3 \"{subtype}\" clicat dues vegades", + "remote_button_long_press": "Bot\u00f3 \"{subtype}\" premut cont\u00ednuament", "remote_button_long_release": "Bot\u00f3 \"{subtype}\" alliberat despr\u00e9s d'una estona premut", - "remote_button_quadruple_press": "Bot\u00f3 \"{subtype}\" clicat quatre vegades consecutives", - "remote_button_quintuple_press": "Bot\u00f3 \"{subtype}\" clicat cinc vegades consecutives", + "remote_button_quadruple_press": "Bot\u00f3 \"{subtype}\" clicat quatre vegades", + "remote_button_quintuple_press": "Bot\u00f3 \"{subtype}\" clicat cinc vegades", "remote_button_short_press": "Bot\u00f3 \"{subtype}\" premut", "remote_button_short_release": "Bot\u00f3 \"{subtype}\" alliberat", - "remote_button_triple_press": "Bot\u00f3 \"{subtype}\" clicat tres vegades consecutives" + "remote_button_triple_press": "Bot\u00f3 \"{subtype}\" clicat tres vegades" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/pl.json b/homeassistant/components/zha/.translations/pl.json index 4189ea6d9be..4698a0a37ef 100644 --- a/homeassistant/components/zha/.translations/pl.json +++ b/homeassistant/components/zha/.translations/pl.json @@ -54,14 +54,14 @@ "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}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty", - "remote_button_short_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty", - "remote_button_short_release": "przycisk \"{subtype}\" zostanie zwolniony", - "remote_button_triple_press": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty" + "remote_button_double_press": "\"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty", + "remote_button_long_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", + "remote_button_long_release": "\"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", + "remote_button_quadruple_press": "\"{subtype}\" czterokrotnie naci\u015bni\u0119ty", + "remote_button_quintuple_press": "\"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty", + "remote_button_short_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty", + "remote_button_short_release": "\"{subtype}\" zostanie zwolniony", + "remote_button_triple_press": "\"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/ru.json b/homeassistant/components/zha/.translations/ru.json index 8850fdfc07a..38b0aa8359c 100644 --- a/homeassistant/components/zha/.translations/ru.json +++ b/homeassistant/components/zha/.translations/ru.json @@ -12,10 +12,10 @@ "radio_type": "\u0422\u0438\u043f \u0420\u0430\u0434\u0438\u043e", "usb_path": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" }, - "title": "Zigbee Home Automation (ZHA)" + "title": "Zigbee Home Automation" } }, - "title": "Zigbee Home Automation" + "title": "Zigbee Home Automation (ZHA)" }, "device_automation": { "action_type": { diff --git a/homeassistant/components/zha/.translations/sv.json b/homeassistant/components/zha/.translations/sv.json index 2762adc0fba..473cf1cd2a9 100644 --- a/homeassistant/components/zha/.translations/sv.json +++ b/homeassistant/components/zha/.translations/sv.json @@ -18,15 +18,50 @@ "title": "ZHA" }, "device_automation": { + "action_type": { + "squawk": "Kraxa", + "warn": "Varna" + }, "trigger_subtype": { + "both_buttons": "B\u00e5da knapparna", + "button_1": "F\u00f6rsta knappen", + "button_2": "Andra knappen", + "button_3": "Tredje knappen", + "button_4": "Fj\u00e4rde knappen", + "button_5": "Femte knappen", + "button_6": "Sj\u00e4tte knappen", "close": "St\u00e4ng", "dim_down": "Dimma ned", "dim_up": "Dimma upp", + "face_1": "med bildsida 1 aktiverat", + "face_2": "med bildsida 2 aktiverat", + "face_3": "med bildsida 3 aktiverat", + "face_4": "med bildsida 4 aktiverat", + "face_5": "med bildsida 5 aktiverat", + "face_6": "med bildsida 6 aktiverat", + "face_any": "Med valfri/specificerad bildsida(or) aktiverat", "left": "V\u00e4nster", "open": "\u00d6ppen", "right": "H\u00f6ger", "turn_off": "St\u00e4ng av", "turn_on": "Starta" + }, + "trigger_type": { + "device_dropped": "Enheten tappades", + "device_flipped": "Enheten v\u00e4nd \"{subtype}\"", + "device_knocked": "Enheten knackad \"{subtype}\"", + "device_rotated": "Enheten roterade \"{subtype}\"", + "device_shaken": "Enheten skakad", + "device_slid": "Enheten gled \"{subtype}\"", + "device_tilted": "Enheten lutad", + "remote_button_double_press": "\"{subtype}\"-knappen dubbelklickades", + "remote_button_long_press": "\"{subtype}\"-knappen kontinuerligt nedtryckt", + "remote_button_long_release": "\"{subtype}\"-knappen sl\u00e4pptes efter ett l\u00e5ngttryck", + "remote_button_quadruple_press": "\"{subtype}\"-knappen klickades \nfyrfaldigt", + "remote_button_quintuple_press": "\"{subtype}\"-knappen klickades \nfemfaldigt", + "remote_button_short_press": "\"{subtype}\"-knappen trycktes in", + "remote_button_short_release": "\"{subtype}\"-knappen sl\u00e4ppt", + "remote_button_triple_press": "\"{subtype}\"-knappen trippelklickades" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index a5ecf21e0c3..d899f51b487 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -194,8 +194,7 @@ class ZigbeeChannel(LogMixin): async def async_configure(self): """Set cluster binding and attribute reporting.""" - # Xiaomi devices don't need this and it disrupts pairing - if self._zha_device.manufacturer != "LUMI": + if not self._zha_device.skip_configuration: await self.bind() if self.cluster.is_server: for report_config in self._report_config: @@ -203,8 +202,9 @@ class ZigbeeChannel(LogMixin): report_config["attr"], report_config["config"] ) await asyncio.sleep(uniform(0.1, 0.5)) - - self.debug("finished channel configuration") + self.debug("finished channel configuration") + else: + self.debug("skipping channel configuration") self._status = ChannelStatus.CONFIGURED async def async_initialize(self, from_cache): @@ -264,7 +264,7 @@ class ZigbeeChannel(LogMixin): def log(self, level, msg, *args): """Log a message.""" msg = f"[%s:%s]: {msg}" - args = (self.device.nwk, self._id,) + args + args = (self.device.nwk, self._id) + args _LOGGER.log(level, msg, *args) def __getattr__(self, name): diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index 69e4ea1a27a..781738fc048 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -146,9 +146,8 @@ class IASZoneChannel(ZigbeeChannel): async def async_configure(self): """Configure IAS device.""" - # Xiaomi devices don't need this and it disrupts pairing - if self._zha_device.manufacturer == "LUMI": - self.debug("finished IASZoneChannel configuration") + if self._zha_device.skip_configuration: + self.debug("skipping IASZoneChannel configuration") return self.debug("started IASZoneChannel configuration") diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index b8782101cd4..f4cccfa4e52 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -96,6 +96,7 @@ DATA_ZHA_GATEWAY = "zha_gateway" DEBUG_COMP_BELLOWS = "bellows" DEBUG_COMP_ZHA = "homeassistant.components.zha" DEBUG_COMP_ZIGPY = "zigpy" +DEBUG_COMP_ZIGPY_CC = "zigpy_cc" DEBUG_COMP_ZIGPY_DECONZ = "zigpy_deconz" DEBUG_COMP_ZIGPY_XBEE = "zigpy_xbee" DEBUG_COMP_ZIGPY_ZIGATE = "zigpy_zigate" @@ -105,8 +106,9 @@ DEBUG_LEVELS = { DEBUG_COMP_BELLOWS: logging.DEBUG, DEBUG_COMP_ZHA: logging.DEBUG, DEBUG_COMP_ZIGPY: logging.DEBUG, - DEBUG_COMP_ZIGPY_XBEE: logging.DEBUG, + DEBUG_COMP_ZIGPY_CC: logging.DEBUG, DEBUG_COMP_ZIGPY_DECONZ: logging.DEBUG, + DEBUG_COMP_ZIGPY_XBEE: logging.DEBUG, DEBUG_COMP_ZIGPY_ZIGATE: logging.DEBUG, } DEBUG_RELAY_LOGGERS = [DEBUG_COMP_ZHA, DEBUG_COMP_ZIGPY] @@ -131,9 +133,10 @@ POWER_BATTERY_OR_UNKNOWN = "Battery or Unknown" class RadioType(enum.Enum): """Possible options for radio type.""" - ezsp = "ezsp" - xbee = "xbee" deconz = "deconz" + ezsp = "ezsp" + ti_cc = "ti_cc" + xbee = "xbee" zigate = "zigate" @classmethod diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 8810fd77fe7..2e7c48c639f 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -213,6 +213,11 @@ class ZHADevice(LogMixin): if Groups.cluster_id in clusters: return True + @property + def skip_configuration(self): + """Return true if the device should not issue configuration related commands.""" + return self._zigpy_device.skip_configuration + @property def gateway(self): """Return the gateway for this device.""" diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index e5a199c5bbd..33faaa334cb 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -41,6 +41,7 @@ from .const import ( DEBUG_COMP_BELLOWS, DEBUG_COMP_ZHA, DEBUG_COMP_ZIGPY, + DEBUG_COMP_ZIGPY_CC, DEBUG_COMP_ZIGPY_DECONZ, DEBUG_COMP_ZIGPY_XBEE, DEBUG_COMP_ZIGPY_ZIGATE, @@ -424,7 +425,7 @@ class ZHAGateway: # ZHA already has an initialized device so either the device was assigned a # new nwk or device was physically reset and added again without being removed _LOGGER.debug( - "device - %s has been reset and readded or its nwk address changed", + "device - %s has been reset and re-added or its nwk address changed", f"0x{device.nwk:04x}:{device.ieee}", ) await self._async_device_rejoined(zha_device) @@ -555,12 +556,13 @@ def async_capture_log_levels(): DEBUG_COMP_BELLOWS: logging.getLogger(DEBUG_COMP_BELLOWS).getEffectiveLevel(), DEBUG_COMP_ZHA: logging.getLogger(DEBUG_COMP_ZHA).getEffectiveLevel(), DEBUG_COMP_ZIGPY: logging.getLogger(DEBUG_COMP_ZIGPY).getEffectiveLevel(), - DEBUG_COMP_ZIGPY_XBEE: logging.getLogger( - DEBUG_COMP_ZIGPY_XBEE - ).getEffectiveLevel(), + DEBUG_COMP_ZIGPY_CC: logging.getLogger(DEBUG_COMP_ZIGPY_CC).getEffectiveLevel(), DEBUG_COMP_ZIGPY_DECONZ: logging.getLogger( DEBUG_COMP_ZIGPY_DECONZ ).getEffectiveLevel(), + DEBUG_COMP_ZIGPY_XBEE: logging.getLogger( + DEBUG_COMP_ZIGPY_XBEE + ).getEffectiveLevel(), DEBUG_COMP_ZIGPY_ZIGATE: logging.getLogger( DEBUG_COMP_ZIGPY_ZIGATE ).getEffectiveLevel(), @@ -573,8 +575,9 @@ def async_set_logger_levels(levels): logging.getLogger(DEBUG_COMP_BELLOWS).setLevel(levels[DEBUG_COMP_BELLOWS]) logging.getLogger(DEBUG_COMP_ZHA).setLevel(levels[DEBUG_COMP_ZHA]) logging.getLogger(DEBUG_COMP_ZIGPY).setLevel(levels[DEBUG_COMP_ZIGPY]) - logging.getLogger(DEBUG_COMP_ZIGPY_XBEE).setLevel(levels[DEBUG_COMP_ZIGPY_XBEE]) + logging.getLogger(DEBUG_COMP_ZIGPY_CC).setLevel(levels[DEBUG_COMP_ZIGPY_CC]) logging.getLogger(DEBUG_COMP_ZIGPY_DECONZ).setLevel(levels[DEBUG_COMP_ZIGPY_DECONZ]) + logging.getLogger(DEBUG_COMP_ZIGPY_XBEE).setLevel(levels[DEBUG_COMP_ZIGPY_XBEE]) logging.getLogger(DEBUG_COMP_ZIGPY_ZIGATE).setLevel(levels[DEBUG_COMP_ZIGPY_ZIGATE]) diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 311f8fa275f..4f5c6fc5c6b 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -13,6 +13,8 @@ import bellows.zigbee.application import zigpy.profiles.zha import zigpy.profiles.zll import zigpy.zcl as zcl +import zigpy_cc.api +import zigpy_cc.zigbee.application import zigpy_deconz.api import zigpy_deconz.zigbee.application import zigpy_xbee.api @@ -127,15 +129,20 @@ LIGHT_CLUSTERS = SetRegistry() OUTPUT_CHANNEL_ONLY_CLUSTERS = SetRegistry() RADIO_TYPES = { + RadioType.deconz.name: { + ZHA_GW_RADIO: zigpy_deconz.api.Deconz, + CONTROLLER: zigpy_deconz.zigbee.application.ControllerApplication, + ZHA_GW_RADIO_DESCRIPTION: "Deconz", + }, RadioType.ezsp.name: { ZHA_GW_RADIO: bellows.ezsp.EZSP, CONTROLLER: bellows.zigbee.application.ControllerApplication, ZHA_GW_RADIO_DESCRIPTION: "EZSP", }, - RadioType.deconz.name: { - ZHA_GW_RADIO: zigpy_deconz.api.Deconz, - CONTROLLER: zigpy_deconz.zigbee.application.ControllerApplication, - ZHA_GW_RADIO_DESCRIPTION: "Deconz", + RadioType.ti_cc.name: { + ZHA_GW_RADIO: zigpy_cc.api.API, + CONTROLLER: zigpy_cc.zigbee.application.ControllerApplication, + ZHA_GW_RADIO_DESCRIPTION: "TI CC", }, RadioType.xbee.name: { ZHA_GW_RADIO: zigpy_xbee.api.XBee, diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index f5ec96690bc..16c5604587d 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -5,7 +5,8 @@ "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ "bellows-homeassistant==0.13.2", - "zha-quirks==0.0.32", + "zha-quirks==0.0.33", + "zigpy-cc==0.1.0", "zigpy-deconz==0.7.0", "zigpy-homeassistant==0.13.2", "zigpy-xbee-homeassistant==0.9.0", diff --git a/homeassistant/components/zhong_hong/climate.py b/homeassistant/components/zhong_hong/climate.py index 203131afdb1..62f5b9acbaf 100644 --- a/homeassistant/components/zhong_hong/climate.py +++ b/homeassistant/components/zhong_hong/climate.py @@ -88,7 +88,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): hub_is_initialized = False async def startup(): - """Start hub socket after all climate entity is setted up.""" + """Start hub socket after all climate entity is set up.""" nonlocal hub_is_initialized if not all([device.is_initialized for device in devices]): return diff --git a/homeassistant/components/zone/.translations/pl.json b/homeassistant/components/zone/.translations/pl.json index e649de4c75e..5c013d5da8f 100644 --- a/homeassistant/components/zone/.translations/pl.json +++ b/homeassistant/components/zone/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Nazwa ju\u017c istnieje" + "name_exists": "Nazwa ju\u017c istnieje." }, "step": { "init": { diff --git a/homeassistant/components/zone/.translations/zh-Hans.json b/homeassistant/components/zone/.translations/zh-Hans.json index 6d06b68dad8..6972b2946e4 100644 --- a/homeassistant/components/zone/.translations/zh-Hans.json +++ b/homeassistant/components/zone/.translations/zh-Hans.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "\u540d\u79f0\u5df2\u5b58\u5728" + "name_exists": "\u8be5\u540d\u79f0\u5df2\u5b58\u5728" }, "step": { "init": { @@ -13,7 +13,7 @@ "passive": "\u88ab\u52a8", "radius": "\u534a\u5f84" }, - "title": "\u5b9a\u4e49\u533a\u57df\u76f8\u5173\u53d8\u91cf" + "title": "\u5b9a\u4e49\u533a\u57df\u53c2\u6570" } }, "title": "\u533a\u57df" diff --git a/homeassistant/components/zwave/.translations/pl.json b/homeassistant/components/zwave/.translations/pl.json index 254008ddb4c..a985405c009 100644 --- a/homeassistant/components/zwave/.translations/pl.json +++ b/homeassistant/components/zwave/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Z-Wave jest ju\u017c skonfigurowany", + "already_configured": "Z-Wave jest ju\u017c skonfigurowany.", "one_instance_only": "Komponent obs\u0142uguje tylko jedn\u0105 instancj\u0119 Z-Wave" }, "error": { diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 9b9236de1c2..ba7e26ee58c 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -678,7 +678,7 @@ async def async_setup_entry(hass, config_entry): if value.type == const.TYPE_BOOL: value.data = int(selection == "True") _LOGGER.info( - "Setting config parameter %s on Node %s with bool selection %s", + "Setting configuration parameter %s on Node %s with bool selection %s", param, node_id, str(selection), @@ -687,7 +687,7 @@ async def async_setup_entry(hass, config_entry): if value.type == const.TYPE_LIST: value.data = str(selection) _LOGGER.info( - "Setting config parameter %s on Node %s with list selection %s", + "Setting configuration parameter %s on Node %s with list selection %s", param, node_id, str(selection), @@ -697,7 +697,7 @@ async def async_setup_entry(hass, config_entry): network.manager.pressButton(value.value_id) network.manager.releaseButton(value.value_id) _LOGGER.info( - "Setting config parameter %s on Node %s " + "Setting configuration parameter %s on Node %s " "with button selection %s", param, node_id, @@ -706,7 +706,7 @@ async def async_setup_entry(hass, config_entry): return value.data = int(selection) _LOGGER.info( - "Setting config parameter %s on Node %s with selection %s", + "Setting configuration parameter %s on Node %s with selection %s", param, node_id, selection, @@ -714,7 +714,7 @@ async def async_setup_entry(hass, config_entry): return node.set_config_param(param, selection, size) _LOGGER.info( - "Setting unknown config parameter %s on Node %s with selection %s", + "Setting unknown configuration parameter %s on Node %s with selection %s", param, node_id, selection, diff --git a/homeassistant/components/zwave/climate.py b/homeassistant/components/zwave/climate.py index 840418fb063..4ee9b8b9cc9 100644 --- a/homeassistant/components/zwave/climate.py +++ b/homeassistant/components/zwave/climate.py @@ -529,7 +529,7 @@ class ZWaveClimateBase(ZWaveDeviceEntity, ClimateDevice): self._mode().data = operation_mode def turn_aux_heat_on(self): - """Turn auxillary heater on.""" + """Turn auxiliary heater on.""" if not self._aux_heat: return operation_mode = AUX_HEAT_ZWAVE_MODE @@ -537,7 +537,7 @@ class ZWaveClimateBase(ZWaveDeviceEntity, ClimateDevice): self._mode().data = operation_mode def turn_aux_heat_off(self): - """Turn auxillary heater off.""" + """Turn auxiliary heater off.""" if not self._aux_heat: return if HVAC_MODE_HEAT in self._hvac_mapping: diff --git a/homeassistant/components/zwave/lock.py b/homeassistant/components/zwave/lock.py index 44e73da320f..382d2c4dbf2 100644 --- a/homeassistant/components/zwave/lock.py +++ b/homeassistant/components/zwave/lock.py @@ -180,7 +180,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if len(str(usercode)) < 4: _LOGGER.error( "Invalid code provided: (%s) " - "usercode must be atleast 4 and at most" + "usercode must be at least 4 and at most" " %s digits", usercode, len(value.data), diff --git a/homeassistant/config.py b/homeassistant/config.py index f5870d683a0..6ff571f0d6b 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -697,6 +697,9 @@ async def async_process_component_config( except (vol.Invalid, HomeAssistantError) as ex: async_log_exception(ex, domain, config, hass, integration.documentation) return None + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unknown error calling %s config validator", domain) + return None # No custom config validator, proceed with schema validation if hasattr(component, "CONFIG_SCHEMA"): @@ -705,6 +708,9 @@ async def async_process_component_config( except vol.Invalid as ex: async_log_exception(ex, domain, config, hass, integration.documentation) return None + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unknown error calling %s CONFIG_SCHEMA", domain) + return None component_platform_schema = getattr( component, "PLATFORM_SCHEMA_BASE", getattr(component, "PLATFORM_SCHEMA", None) @@ -721,6 +727,13 @@ async def async_process_component_config( except vol.Invalid as ex: async_log_exception(ex, domain, p_config, hass, integration.documentation) continue + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Unknown error validating %s platform config with %s component platform schema", + p_name, + domain, + ) + continue # Not all platform components follow same pattern for platforms # So if p_name is None we are not going to validate platform @@ -756,6 +769,13 @@ async def async_process_component_config( p_integration.documentation, ) continue + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Unknown error validating config for %s platform for %s component with PLATFORM_SCHEMA", + p_name, + domain, + ) + continue platforms.append(p_validated) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 793e8be0045..1cec1e75fe9 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -183,7 +183,7 @@ class ConfigEntry: component = integration.get_component() except ImportError as err: _LOGGER.error( - "Error importing integration %s to set up %s config entry: %s", + "Error importing integration %s to set up %s configuration entry: %s", integration.domain, self.domain, err, @@ -197,7 +197,7 @@ class ConfigEntry: integration.get_platform("config_flow") except ImportError as err: _LOGGER.error( - "Error importing platform config_flow from integration %s to set up %s config entry: %s", + "Error importing platform config_flow from integration %s to set up %s configuration entry: %s", integration.domain, self.domain, err, @@ -503,7 +503,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): integration.get_platform("config_flow") except ImportError as err: _LOGGER.error( - "Error occurred loading config flow for integration %s: %s", + "Error occurred loading configuration flow for integration %s: %s", handler_key, err, ) @@ -775,6 +775,7 @@ class ConfigEntries: return await entry.async_unload(self.hass, integration=integration) + @callback def _async_schedule_save(self) -> None: """Save the entity registry to a file.""" self._store.async_delay_save(self._data_to_save, SAVE_DELAY) @@ -947,7 +948,7 @@ class SystemOptions: self.disable_new_entities = disable_new_entities def as_dict(self) -> Dict[str, Any]: - """Return dictionary version of this config entrys system options.""" + """Return dictionary version of this config entries system options.""" return {"disable_new_entities": self.disable_new_entities} @@ -1024,7 +1025,7 @@ class EntityRegistryDisabledHandler: self.changed = set() _LOGGER.info( - "Reloading config entries because disabled_by changed in entity registry: %s", + "Reloading configuration entries because disabled_by changed in entity registry: %s", ", ".join(self.changed), ) diff --git a/homeassistant/const.py b/homeassistant/const.py index 9db5a72292c..ff8e6bf8a9e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 105 -PATCH_VERSION = "5" +MINOR_VERSION = 106 +PATCH_VERSION = "0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) @@ -16,6 +16,7 @@ PLATFORM_FORMAT = "{platform}.{domain}" MATCH_ALL = "*" # Entity target all constant +ENTITY_MATCH_NONE = "none" ENTITY_MATCH_ALL = "all" # If no name is specified @@ -377,6 +378,40 @@ MASS_POUNDS: str = "lb" # UV Index units UNIT_UV_INDEX: str = "UV index" +# Data units +DATA_BITS = "bit" +DATA_KILOBITS = "kbit" +DATA_MEGABITS = "Mbit" +DATA_GIGABITS = "Gbit" +DATA_BYTES = "B" +DATA_KILOBYTES = "kB" +DATA_MEGABYTES = "MB" +DATA_GIGABYTES = "GB" +DATA_TERABYTES = "TB" +DATA_PETABYTES = "PB" +DATA_EXABYTES = "EB" +DATA_ZETTABYTES = "ZB" +DATA_YOTTABYTES = "YB" +DATA_KIBIBYTES = "KiB" +DATA_MEBIBYTES = "MiB" +DATA_GIBIBYTES = "GiB" +DATA_TEBIBYTES = "TiB" +DATA_PEBIBYTES = "PiB" +DATA_EXBIBYTES = "EiB" +DATA_ZEBIBYTES = "ZiB" +DATA_YOBIBYTES = "YiB" +DATA_RATE_BITS_PER_SECOND = f"{DATA_BITS}/s" +DATA_RATE_KILOBITS_PER_SECOND = f"{DATA_KILOBITS}/s" +DATA_RATE_MEGABITS_PER_SECOND = f"{DATA_MEGABITS}/s" +DATA_RATE_GIGABITS_PER_SECOND = f"{DATA_GIGABITS}/s" +DATA_RATE_BYTES_PER_SECOND = f"{DATA_BYTES}/s" +DATA_RATE_KILOBYTES_PER_SECOND = f"{DATA_KILOBYTES}/s" +DATA_RATE_MEGABYTES_PER_SECOND = f"{DATA_MEGABYTES}/s" +DATA_RATE_GIGABYTES_PER_SECOND = f"{DATA_GIGABYTES}/s" +DATA_RATE_KIBIBYTES_PER_SECOND = f"{DATA_KIBIBYTES}/s" +DATA_RATE_MEBIBYTES_PER_SECOND = f"{DATA_MEBIBYTES}/s" +DATA_RATE_GIBIBYTES_PER_SECOND = f"{DATA_GIBIBYTES}/s" + # #### SERVICES #### SERVICE_HOMEASSISTANT_STOP = "stop" SERVICE_HOMEASSISTANT_RESTART = "restart" diff --git a/homeassistant/core.py b/homeassistant/core.py index 3f561cdfab8..c17c1f698ce 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -298,10 +298,10 @@ class HomeAssistant: if asyncio.iscoroutine(check_target): task = self.loop.create_task(target) # type: ignore - elif is_callback(check_target): - self.loop.call_soon(target, *args) elif asyncio.iscoroutinefunction(check_target): task = self.loop.create_task(target(*args)) + elif is_callback(check_target): + self.loop.call_soon(target, *args) else: task = self.loop.run_in_executor( # type: ignore None, target, *args @@ -360,7 +360,11 @@ class HomeAssistant: target: target to call. args: parameters for method to call. """ - if not asyncio.iscoroutine(target) and is_callback(target): + if ( + not asyncio.iscoroutine(target) + and not asyncio.iscoroutinefunction(target) + and is_callback(target) + ): target(*args) else: self.async_add_job(target, *args) @@ -1245,10 +1249,10 @@ class ServiceRegistry: self, handler: Service, service_call: ServiceCall ) -> None: """Execute a service.""" - if handler.is_callback: - handler.func(service_call) - elif handler.is_coroutinefunction: + if handler.is_coroutinefunction: await handler.func(service_call) + elif handler.is_callback: + handler.func(service_call) else: await self._hass.async_add_executor_job(handler.func, service_call) @@ -1284,6 +1288,9 @@ class Config: # List of allowed external dirs to access self.whitelist_external_dirs: Set[str] = set() + # If Home Assistant is running in safe mode + self.safe_mode: bool = False + def distance(self, lat: float, lon: float) -> Optional[float]: """Calculate distance from Home Assistant. @@ -1346,6 +1353,7 @@ class Config: "whitelist_external_dirs": self.whitelist_external_dirs, "version": __version__, "config_source": self.config_source, + "safe_mode": self.safe_mode, } def set_time_zone(self, time_zone_str: str) -> None: diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 4dd1c7acf50..4a115762be4 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -30,7 +30,7 @@ class UnknownHandler(FlowError): class UnknownFlow(FlowError): - """Uknown flow specified.""" + """Unknown flow specified.""" class UnknownStep(FlowError): diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index cf77dae7fb2..39a9bccf607 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -20,11 +20,13 @@ FLOWS = [ "daikin", "deconz", "dialogflow", + "dynalite", "ecobee", "elgato", "emulated_roku", "esphome", "garmin_connect", + "gdacs", "geofency", "geonetnz_quakes", "geonetnz_volcano", @@ -45,6 +47,7 @@ FLOWS = [ "ipma", "iqvia", "izone", + "konnected", "life360", "lifx", "linky", @@ -53,8 +56,11 @@ FLOWS = [ "logi_circle", "luftdaten", "mailgun", + "melcloud", "met", + "meteo_france", "mikrotik", + "minecraft_server", "mobile_app", "mqtt", "neato", @@ -95,6 +101,7 @@ FLOWS = [ "upnp", "velbus", "vesync", + "vilfo", "vizio", "wemo", "withings", diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index bea04484b11..0eb9af0231d 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -36,6 +36,11 @@ SSDP = { "modelName": "Philips hue bridge 2015" } ], + "konnected": [ + { + "manufacturer": "konnected.io" + } + ], "samsungtv": [ { "st": "urn:samsung.com:device:RemoteControlReceiver:1" diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py index ad97456968b..7189f519724 100644 --- a/homeassistant/helpers/__init__.py +++ b/homeassistant/helpers/__init__.py @@ -1,10 +1,10 @@ """Helper methods for components within Home Assistant.""" import re -from typing import Any, Dict, Iterable, Sequence, Tuple +from typing import Any, Iterable, Sequence, Tuple from homeassistant.const import CONF_PLATFORM -ConfigType = Dict[str, Any] +from .typing import ConfigType def config_per_platform(config: ConfigType, domain: str) -> Iterable[Tuple[Any, Any]]: diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 852948220de..1ff2644fa58 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -52,6 +52,7 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM_METRIC, CONF_VALUE_TEMPLATE, ENTITY_MATCH_ALL, + ENTITY_MATCH_NONE, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, TEMP_CELSIUS, @@ -231,7 +232,9 @@ def entity_ids(value: Union[str, List]) -> List[str]: return [entity_id(ent_id) for ent_id in value] -comp_entity_ids = vol.Any(vol.All(vol.Lower, ENTITY_MATCH_ALL), entity_ids) +comp_entity_ids = vol.Any( + vol.All(vol.Lower, vol.Any(ENTITY_MATCH_ALL, ENTITY_MATCH_NONE)), entity_ids +) def entity_domain(domain: str) -> Callable[[Any], str]: @@ -585,6 +588,25 @@ def ensure_list_csv(value: Any) -> List: return ensure_list(value) +class multi_select: + """Multi select validator returning list of selected values.""" + + def __init__(self, options: dict) -> None: + """Initialize multi select.""" + self.options = options + + def __call__(self, selected: list) -> list: + """Validate input.""" + if not isinstance(selected, list): + raise vol.Invalid("Not a list") + + for value in selected: + if value not in self.options: + raise vol.Invalid(f"{value} is not a valid option") + + return selected + + def deprecated( key: str, replacement_key: Optional[str] = None, @@ -710,6 +732,9 @@ def custom_serializer(schema: Any) -> Any: if schema is positive_time_period_dict: return {"type": "positive_time_period_dict"} + if isinstance(schema, multi_select): + return {"type": "multi_select", "options": schema.options} + return voluptuous_serialize.UNSUPPORTED @@ -736,7 +761,9 @@ def make_entity_service_schema( { **schema, vol.Optional(ATTR_ENTITY_ID): comp_entity_ids, - vol.Optional(ATTR_AREA_ID): vol.All(ensure_list, [str]), + vol.Optional(ATTR_AREA_ID): vol.Any( + ENTITY_MATCH_NONE, vol.All(ensure_list, [str]) + ), }, extra=extra, ), diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index ac5fb608675..05f49cd9f53 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -5,6 +5,7 @@ import voluptuous as vol from homeassistant import config_entries, data_entry_flow from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator +import homeassistant.helpers.config_validation as cv # mypy: allow-untyped-calls, allow-untyped-defs @@ -36,7 +37,9 @@ class _BaseFlowManagerView(HomeAssistantView): if schema is None: data["data_schema"] = [] else: - data["data_schema"] = voluptuous_serialize.convert(schema) + data["data_schema"] = voluptuous_serialize.convert( + schema, custom_serializer=cv.custom_serializer + ) return data diff --git a/homeassistant/helpers/debounce.py b/homeassistant/helpers/debounce.py index 5bacbdb7d11..bbaf6dacfeb 100644 --- a/homeassistant/helpers/debounce.py +++ b/homeassistant/helpers/debounce.py @@ -13,6 +13,7 @@ class Debouncer: self, hass: HomeAssistant, logger: Logger, + *, cooldown: float, immediate: bool, function: Optional[Callable[..., Awaitable[Any]]] = None, diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index a2a0ae840e0..49ed0f4a567 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -337,7 +337,7 @@ class Entity(ABC): if name is not None: attr[ATTR_FRIENDLY_NAME] = name - icon = self.icon + icon = (entry and entry.icon) or self.icon if icon is not None: attr[ATTR_ICON] = icon @@ -365,13 +365,25 @@ class Entity(ABC): if end - start > 0.4 and not self._slow_reported: self._slow_reported = True + extra = "" + if "custom_components" in type(self).__module__: + extra = "Please report it to the custom component author." + else: + extra = ( + "Please create a bug report at " + "https://github.com/home-assistant/home-assistant/issues?q=is%3Aopen+is%3Aissue" + ) + if self.platform: + extra += ( + f"+label%3A%22integration%3A+{self.platform.platform_name}%22" + ) + _LOGGER.warning( - "Updating state for %s (%s) took %.3f seconds. " - "Please report platform to the developers at " - "https://goo.gl/Nvioub", + "Updating state for %s (%s) took %.3f seconds. %s", self.entity_id, type(self), end - start, + extra, ) # Overwrite properties that have been set in the config file. @@ -429,7 +441,10 @@ class Entity(ABC): If state is changed more than once before the ha state change task has been executed, the intermediate state transitions will be missed. """ - self.hass.async_create_task(self.async_update_ha_state(force_refresh)) + if force_refresh: + self.hass.async_create_task(self.async_update_ha_state(force_refresh)) + else: + self.async_write_ha_state() async def async_device_update(self, warning=True): """Process 'update' or 'async_update' from entity. @@ -568,7 +583,6 @@ class Entity(ABC): # call an requests async def async_request_call(self, coro): """Process request batched.""" - if self.parallel_updates: await self.parallel_updates.acquire() diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index e26dc5dfbea..f6c473dd418 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -273,6 +273,7 @@ class EntityComponent: return processed_conf + @callback def _async_init_entity_platform( self, platform_type: str, diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 8fedc198fe2..e1e046eaa6d 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -62,22 +62,42 @@ class EntityPlatform: # Platform is None for the EntityComponent "catch-all" EntityPlatform # which powers entity_component.add_entities if platform is None: - self.parallel_updates = None - self.parallel_updates_semaphore: Optional[asyncio.Semaphore] = None + self.parallel_updates_created = True + self.parallel_updates: Optional[asyncio.Semaphore] = None return - self.parallel_updates = getattr(platform, "PARALLEL_UPDATES", None) - # semaphore will be created on demand - self.parallel_updates_semaphore = None + self.parallel_updates_created = False + self.parallel_updates = None - def _get_parallel_updates_semaphore(self) -> asyncio.Semaphore: - """Get or create a semaphore for parallel updates.""" - if self.parallel_updates_semaphore is None: - self.parallel_updates_semaphore = asyncio.Semaphore( - self.parallel_updates if self.parallel_updates else 1, - loop=self.hass.loop, - ) - return self.parallel_updates_semaphore + @callback + def _get_parallel_updates_semaphore( + self, entity_has_async_update: bool + ) -> Optional[asyncio.Semaphore]: + """Get or create a semaphore for parallel updates. + + Semaphore will be created on demand because we base it off if update method is async or not. + + If parallel updates is set to 0, we skip the semaphore. + If parallel updates is set to a number, we initialize the semaphore to that number. + Default for entities with `async_update` method is 1. Otherwise it's 0. + """ + if self.parallel_updates_created: + return self.parallel_updates + + self.parallel_updates_created = True + + parallel_updates = getattr(self.platform, "PARALLEL_UPDATES", None) + + if parallel_updates is None and not entity_has_async_update: + parallel_updates = 1 + + if parallel_updates == 0: + parallel_updates = None + + if parallel_updates is not None: + self.parallel_updates = asyncio.Semaphore(parallel_updates) + + return self.parallel_updates async def async_setup(self, platform_config, discovery_info=None): """Set up the platform from a config file.""" @@ -282,21 +302,9 @@ class EntityPlatform: entity.hass = self.hass entity.platform = self - - # Async entity - # PARALLEL_UPDATES == None: entity.parallel_updates = None - # PARALLEL_UPDATES == 0: entity.parallel_updates = None - # PARALLEL_UPDATES > 0: entity.parallel_updates = Semaphore(p) - # Sync entity - # PARALLEL_UPDATES == None: entity.parallel_updates = Semaphore(1) - # PARALLEL_UPDATES == 0: entity.parallel_updates = None - # PARALLEL_UPDATES > 0: entity.parallel_updates = Semaphore(p) - if hasattr(entity, "async_update") and not self.parallel_updates: - entity.parallel_updates = None - elif not hasattr(entity, "async_update") and self.parallel_updates == 0: - entity.parallel_updates = None - else: - entity.parallel_updates = self._get_parallel_updates_semaphore() + entity.parallel_updates = self._get_parallel_updates_semaphore( + hasattr(entity, "async_update") + ) # Update properties before we generate the entity_id if update_before_add: @@ -361,6 +369,8 @@ class EntityPlatform: supported_features=entity.supported_features, device_class=entity.device_class, unit_of_measurement=entity.unit_of_measurement, + original_name=entity.name, + original_icon=entity.icon, ) entity.registry_entry = entry diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 635f7feba13..5996fb6eaf7 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -17,6 +17,8 @@ import attr from homeassistant.const import ( ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + ATTR_ICON, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, EVENT_HOMEASSISTANT_START, @@ -60,6 +62,7 @@ class RegistryEntry: unique_id = attr.ib(type=str) platform = attr.ib(type=str) name = attr.ib(type=str, default=None) + icon = attr.ib(type=str, default=None) device_id: Optional[str] = attr.ib(default=None) config_entry_id: Optional[str] = attr.ib(default=None) disabled_by = attr.ib( @@ -79,6 +82,9 @@ class RegistryEntry: supported_features: int = attr.ib(default=0) device_class: Optional[str] = attr.ib(default=None) unit_of_measurement: Optional[str] = attr.ib(default=None) + # As set by integration + original_name: Optional[str] = attr.ib(default=None) + original_icon: Optional[str] = attr.ib(default=None) domain = attr.ib(type=str, init=False, repr=False) @domain.default @@ -167,6 +173,8 @@ class EntityRegistry: supported_features: Optional[int] = None, device_class: Optional[str] = None, unit_of_measurement: Optional[str] = None, + original_name: Optional[str] = None, + original_icon: Optional[str] = None, ) -> RegistryEntry: """Get entity. Create if it doesn't exist.""" config_entry_id = None @@ -184,6 +192,8 @@ class EntityRegistry: supported_features=supported_features or _UNDEF, device_class=device_class or _UNDEF, unit_of_measurement=unit_of_measurement or _UNDEF, + original_name=original_name or _UNDEF, + original_icon=original_icon or _UNDEF, # When we changed our slugify algorithm, we invalidated some # stored entity IDs with either a __ or ending in _. # Fix introduced in 0.86 (Jan 23, 2019). Next line can be @@ -215,6 +225,8 @@ class EntityRegistry: supported_features=supported_features or 0, device_class=device_class, unit_of_measurement=unit_of_measurement, + original_name=original_name, + original_icon=original_icon, ) self.entities[entity_id] = entity _LOGGER.info("Registered new %s.%s entity: %s", domain, platform, entity_id) @@ -254,6 +266,7 @@ class EntityRegistry: entity_id, *, name=_UNDEF, + icon=_UNDEF, new_entity_id=_UNDEF, new_unique_id=_UNDEF, disabled_by=_UNDEF, @@ -264,6 +277,7 @@ class EntityRegistry: self._async_update_entity( entity_id, name=name, + icon=icon, new_entity_id=new_entity_id, new_unique_id=new_unique_id, disabled_by=disabled_by, @@ -276,6 +290,7 @@ class EntityRegistry: entity_id, *, name=_UNDEF, + icon=_UNDEF, config_entry_id=_UNDEF, new_entity_id=_UNDEF, device_id=_UNDEF, @@ -285,6 +300,8 @@ class EntityRegistry: supported_features=_UNDEF, device_class=_UNDEF, unit_of_measurement=_UNDEF, + original_name=_UNDEF, + original_icon=_UNDEF, ): """Private facing update properties method.""" old = self.entities[entity_id] @@ -293,6 +310,7 @@ class EntityRegistry: for attr_name, value in ( ("name", name), + ("icon", icon), ("config_entry_id", config_entry_id), ("device_id", device_id), ("disabled_by", disabled_by), @@ -300,6 +318,8 @@ class EntityRegistry: ("supported_features", supported_features), ("device_class", device_class), ("unit_of_measurement", unit_of_measurement), + ("original_name", original_name), + ("original_icon", original_icon), ): if value is not _UNDEF and value != getattr(old, attr_name): changes[attr_name] = value @@ -372,11 +392,14 @@ class EntityRegistry: unique_id=entity["unique_id"], platform=entity["platform"], name=entity.get("name"), + icon=entity.get("icon"), disabled_by=entity.get("disabled_by"), capabilities=entity.get("capabilities") or {}, supported_features=entity.get("supported_features", 0), device_class=entity.get("device_class"), unit_of_measurement=entity.get("unit_of_measurement"), + original_name=entity.get("original_name"), + original_icon=entity.get("original_icon"), ) self.entities = entities @@ -399,11 +422,14 @@ class EntityRegistry: "unique_id": entry.unique_id, "platform": entry.platform, "name": entry.name, + "icon": entry.icon, "disabled_by": entry.disabled_by, "capabilities": entry.capabilities, "supported_features": entry.supported_features, "device_class": entry.device_class, "unit_of_measurement": entry.unit_of_measurement, + "original_name": entry.original_name, + "original_icon": entry.original_icon, } for entry in self.entities.values() ] @@ -523,6 +549,14 @@ def async_setup_entity_restore( if entry.unit_of_measurement is not None: attrs[ATTR_UNIT_OF_MEASUREMENT] = entry.unit_of_measurement + name = entry.name or entry.original_name + if name is not None: + attrs[ATTR_FRIENDLY_NAME] = name + + icon = entry.icon or entry.original_icon + if icon is not None: + attrs[ATTR_ICON] = icon + states.async_set(entry.entity_id, STATE_UNAVAILABLE, attrs) hass.bus.async_listen(EVENT_HOMEASSISTANT_START, _write_unavailable_states) diff --git a/homeassistant/helpers/logging.py b/homeassistant/helpers/logging.py index 0b274458045..2e3270879f0 100644 --- a/homeassistant/helpers/logging.py +++ b/homeassistant/helpers/logging.py @@ -42,7 +42,7 @@ class KeywordStyleAdapter(logging.LoggerAdapter): def process( self, msg: Any, kwargs: MutableMapping[str, Any] ) -> Tuple[Any, MutableMapping[str, Any]]: - """Process the keyward args in preparation for logging.""" + """Process the keyword args in preparation for logging.""" return ( msg, { diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index e8f0f9a6bac..d57d3ad9920 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -2,7 +2,7 @@ import asyncio from datetime import datetime, timedelta import logging -from typing import Any, Dict, List, Optional, Set +from typing import Any, Awaitable, Dict, List, Optional, Set, cast from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.core import ( @@ -20,9 +20,6 @@ from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.storage import Store import homeassistant.util.dt as dt_util -# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs -# mypy: no-warn-return-any - DATA_RESTORE_STATE_TASK = "restore_state_task" _LOGGER = logging.getLogger(__name__) @@ -45,7 +42,7 @@ class StoredState: self.state = state self.last_seen = last_seen - def as_dict(self) -> Dict: + def as_dict(self) -> Dict[str, Any]: """Return a dict representation of the stored state.""" return {"state": self.state.as_dict(), "last_seen": self.last_seen} @@ -104,7 +101,7 @@ class RestoreStateData: load_instance(hass) ) - return await task + return await cast(Awaitable["RestoreStateData"], task) def __init__(self, hass: HomeAssistant) -> None: """Initialize the restore state data class.""" @@ -174,6 +171,7 @@ class RestoreStateData: def async_setup_dump(self, *args: Any) -> None: """Set up the restore state listeners.""" + @callback def _async_dump_states(*_: Any) -> None: self.hass.async_create_task(self.async_dump_states()) @@ -210,15 +208,18 @@ class RestoreStateData: self.entity_ids.remove(entity_id) -def _encode(value): +def _encode(value: Any) -> Any: """Little helper to JSON encode a value.""" try: - return JSONEncoder.default(None, value) + return JSONEncoder.default( + None, # type: ignore + value, + ) except TypeError: return value -def _encode_complex(value): +def _encode_complex(value: Any) -> Any: """Recursively encode all values with the JSONEncoder.""" if isinstance(value, dict): return {_encode(key): _encode_complex(value) for key, value in value.items()} diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index b30cab3fbd4..9085c929651 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -7,7 +7,12 @@ from typing import Callable import voluptuous as vol from homeassistant.auth.permissions.const import CAT_ENTITIES, POLICY_CONTROL -from homeassistant.const import ATTR_AREA_ID, ATTR_ENTITY_ID, ENTITY_MATCH_ALL +from homeassistant.const import ( + ATTR_AREA_ID, + ATTR_ENTITY_ID, + ENTITY_MATCH_ALL, + ENTITY_MATCH_NONE, +) import homeassistant.core as ha from homeassistant.exceptions import ( HomeAssistantError, @@ -121,11 +126,25 @@ async def async_extract_entities(hass, entities, service_call, expand_group=True entity_ids = await async_extract_entity_ids(hass, service_call, expand_group) - return [ - entity - for entity in entities - if entity.available and entity.entity_id in entity_ids - ] + found = [] + + for entity in entities: + if entity.entity_id not in entity_ids: + continue + + entity_ids.remove(entity.entity_id) + + if not entity.available: + continue + + found.append(entity) + + if entity_ids: + _LOGGER.warning( + "Unable to find referenced entities %s", ", ".join(sorted(entity_ids)) + ) + + return found @bind_hass @@ -137,12 +156,15 @@ async def async_extract_entity_ids(hass, service_call, expand_group=True): entity_ids = service_call.data.get(ATTR_ENTITY_ID) area_ids = service_call.data.get(ATTR_AREA_ID) - if not entity_ids and not area_ids: - return [] - extracted = set() - if entity_ids: + if entity_ids in (None, ENTITY_MATCH_NONE) and area_ids in ( + None, + ENTITY_MATCH_NONE, + ): + return extracted + + if entity_ids and entity_ids != ENTITY_MATCH_NONE: # Entity ID attr can be a list or a string if isinstance(entity_ids, str): entity_ids = [entity_ids] @@ -152,7 +174,7 @@ async def async_extract_entity_ids(hass, service_call, expand_group=True): extracted.update(entity_ids) - if area_ids: + if area_ids and area_ids != ENTITY_MATCH_NONE: if isinstance(area_ids, str): area_ids = [area_ids] @@ -294,16 +316,15 @@ async def entity_service_call(hass, platforms, func, call, required_features=Non # Check the permissions - # A list with for each platform in platforms a list of entities to call - # the service on. - platforms_entities = [] + # A list with entities to call the service on. + entity_candidates = [] if entity_perms is None: for platform in platforms: if target_all_entities: - platforms_entities.append(list(platform.entities.values())) + entity_candidates.extend(platform.entities.values()) else: - platforms_entities.append( + entity_candidates.extend( [ entity for entity in platform.entities.values() @@ -315,7 +336,7 @@ async def entity_service_call(hass, platforms, func, call, required_features=Non # If we target all entities, we will select all entities the user # is allowed to control. for platform in platforms: - platforms_entities.append( + entity_candidates.extend( [ entity for entity in platform.entities.values() @@ -340,29 +361,20 @@ async def entity_service_call(hass, platforms, func, call, required_features=Non platform_entities.append(entity) - platforms_entities.append(platform_entities) + entity_candidates.extend(platform_entities) - tasks = [ - _handle_service_platform_call( - hass, func, data, entities, call.context, required_features - ) - for platform, entities in zip(platforms, platforms_entities) - ] + if not target_all_entities: + for entity in entity_candidates: + entity_ids.remove(entity.entity_id) - if tasks: - done, pending = await asyncio.wait(tasks) - assert not pending - for future in done: - future.result() # pop exception if have + if entity_ids: + _LOGGER.warning( + "Unable to find referenced entities %s", ", ".join(sorted(entity_ids)) + ) + entities = [] -async def _handle_service_platform_call( - hass, func, data, entities, context, required_features -): - """Handle a function call.""" - tasks = [] - - for entity in entities: + for entity in entity_candidates: if not entity.available: continue @@ -372,27 +384,33 @@ async def _handle_service_platform_call( ): continue - entity.async_set_context(context) + entities.append(entity) - if isinstance(func, str): - result = hass.async_add_job(partial(getattr(entity, func), **data)) - else: - result = hass.async_add_job(func, entity, data) + if not entities: + return - # Guard because callback functions do not return a task when passed to async_add_job. - if result is not None: - result = await result - - if asyncio.iscoroutine(result): - _LOGGER.error( - "Service %s for %s incorrectly returns a coroutine object. Await result instead in service handler. Report bug to integration author.", - func, - entity.entity_id, + done, pending = await asyncio.wait( + [ + entity.async_request_call( + _handle_entity_call(hass, entity, func, data, call.context) ) - await result + for entity in entities + ] + ) + assert not pending + for future in done: + future.result() # pop exception if have - if entity.should_poll: - tasks.append(entity.async_update_ha_state(True)) + tasks = [] + + for entity in entities: + if not entity.should_poll: + continue + + # Context expires if the turn on commands took a long time. + # Set context again so it's there when we update + entity.async_set_context(call.context) + tasks.append(entity.async_update_ha_state(True)) if tasks: done, pending = await asyncio.wait(tasks) @@ -401,6 +419,28 @@ async def _handle_service_platform_call( future.result() # pop exception if have +async def _handle_entity_call(hass, entity, func, data, context): + """Handle calling service method.""" + entity.async_set_context(context) + + if isinstance(func, str): + result = hass.async_add_job(partial(getattr(entity, func), **data)) + else: + result = hass.async_add_job(func, entity, data) + + # Guard because callback functions do not return a task when passed to async_add_job. + if result is not None: + await result + + if asyncio.iscoroutine(result): + _LOGGER.error( + "Service %s for %s incorrectly returns a coroutine object. Await result instead in service handler. Report bug to integration author.", + func, + entity.entity_id, + ) + await result + + @bind_hass @ha.callback def async_register_admin_service( @@ -421,7 +461,9 @@ def async_register_admin_service( if not user.is_admin: raise Unauthorized(context=call.context) - await hass.async_add_job(service_func, call) + result = hass.async_add_job(service_func, call) + if result is not None: + await result hass.services.async_register(domain, service, admin_handler, schema) @@ -442,6 +484,7 @@ def verify_domain_control(hass: HomeAssistantType, domain: str) -> Callable: return await service_handler(call) user = await hass.auth.async_get_user(call.context.user_id) + if user is None: raise UnknownUser( context=call.context, @@ -450,14 +493,12 @@ def verify_domain_control(hass: HomeAssistantType, domain: str) -> Callable: ) reg = await hass.helpers.entity_registry.async_get_registry() - entities = [ - entity.entity_id - for entity in reg.entities.values() - if entity.platform == domain - ] - for entity_id in entities: - if user.permissions.check_entity(entity_id, POLICY_CONTROL): + for entity in reg.entities.values(): + if entity.platform != domain: + continue + + if user.permissions.check_entity(entity.entity_id, POLICY_CONTROL): return await service_handler(call) raise Unauthorized( diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 8565315f87f..e7f89b482e2 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -378,7 +378,7 @@ class DomainStates: raise TemplateError(f"Invalid entity ID '{entity_id}'") return _get_state(self._hass, entity_id) - def _collect_domain(self): + def _collect_domain(self) -> None: entity_collect = self._hass.data.get(_RENDER_INFO) if entity_collect is not None: # pylint: disable=protected-access @@ -398,12 +398,12 @@ class DomainStates: ) ) - def __len__(self): + def __len__(self) -> int: """Return number of states.""" self._collect_domain() return len(self._hass.states.async_entity_ids(self._domain)) - def __repr__(self): + def __repr__(self) -> str: """Representation of Domain States.""" return f"