diff --git a/.coveragerc b/.coveragerc index 0240e24b74a..de92c1c23e8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -10,6 +10,14 @@ omit = # omit pieces of code that rely on external devices being present homeassistant/components/acer_projector/switch.py homeassistant/components/actiontec/device_tracker.py + homeassistant/components/acmeda/__init__.py + homeassistant/components/acmeda/base.py + homeassistant/components/acmeda/const.py + homeassistant/components/acmeda/cover.py + homeassistant/components/acmeda/errors.py + homeassistant/components/acmeda/helpers.py + homeassistant/components/acmeda/hub.py + homeassistant/components/acmeda/sensor.py homeassistant/components/adguard/__init__.py homeassistant/components/adguard/const.py homeassistant/components/adguard/sensor.py @@ -55,14 +63,9 @@ omit = homeassistant/components/arwn/sensor.py homeassistant/components/asterisk_cdr/mailbox.py homeassistant/components/asterisk_mbox/* - homeassistant/components/atag/__init__.py - homeassistant/components/atag/climate.py - homeassistant/components/atag/sensor.py - homeassistant/components/atag/water_heater.py homeassistant/components/aten_pe/* homeassistant/components/atome/* homeassistant/components/aurora_abb_powerone/sensor.py - homeassistant/components/automatic/* homeassistant/components/avea/light.py homeassistant/components/avion/light.py homeassistant/components/avri/sensor.py @@ -115,6 +118,7 @@ omit = homeassistant/components/cast/* homeassistant/components/cert_expiry/helper.py homeassistant/components/channels/* + homeassistant/components/circuit/* homeassistant/components/cisco_ios/device_tracker.py homeassistant/components/cisco_mobility_express/device_tracker.py homeassistant/components/cisco_webex_teams/notify.py @@ -172,6 +176,8 @@ omit = homeassistant/components/dsmr_reader/* homeassistant/components/dte_energy_bridge/sensor.py homeassistant/components/dublin_bus_transport/sensor.py + homeassistant/components/dunehd/__init__.py + homeassistant/components/dunehd/const.py homeassistant/components/dunehd/media_player.py homeassistant/components/dwd_weather_warnings/sensor.py homeassistant/components/dweet/* @@ -271,7 +277,6 @@ omit = homeassistant/components/garmin_connect/sensor.py homeassistant/components/gc100/* homeassistant/components/geniushub/* - homeassistant/components/gearbest/sensor.py homeassistant/components/geizhals/sensor.py homeassistant/components/gios/__init__.py homeassistant/components/gios/air_quality.py @@ -282,7 +287,6 @@ omit = homeassistant/components/glances/sensor.py homeassistant/components/gntp/notify.py homeassistant/components/goalfeed/* - homeassistant/components/gogogate2/cover.py homeassistant/components/google/* homeassistant/components/google_cloud/tts.py homeassistant/components/google_maps/device_tracker.py @@ -296,6 +300,10 @@ omit = homeassistant/components/growatt_server/sensor.py homeassistant/components/gstreamer/media_player.py homeassistant/components/gtfs/sensor.py + homeassistant/components/guardian/__init__.py + homeassistant/components/guardian/binary_sensor.py + homeassistant/components/guardian/sensor.py + homeassistant/components/guardian/switch.py homeassistant/components/habitica/* homeassistant/components/hangouts/* homeassistant/components/hangouts/__init__.py @@ -557,7 +565,9 @@ omit = homeassistant/components/openevse/sensor.py homeassistant/components/openexchangerates/sensor.py homeassistant/components/opengarage/cover.py + homeassistant/components/openhome/__init__.py homeassistant/components/openhome/media_player.py + homeassistant/components/openhome/const.py homeassistant/components/opensensemap/air_quality.py homeassistant/components/opensky/sensor.py homeassistant/components/opentherm_gw/__init__.py @@ -595,7 +605,11 @@ omit = homeassistant/components/plaato/* homeassistant/components/plex/media_player.py homeassistant/components/plex/sensor.py - homeassistant/components/plugwise/* + homeassistant/components/plugwise/__init__.py + homeassistant/components/plugwise/binary_sensor.py + homeassistant/components/plugwise/climate.py + homeassistant/components/plugwise/sensor.py + homeassistant/components/plugwise/switch.py homeassistant/components/plum_lightpad/* homeassistant/components/pocketcasts/sensor.py homeassistant/components/point/* @@ -718,7 +732,6 @@ omit = homeassistant/components/soma/__init__.py homeassistant/components/somfy/* homeassistant/components/somfy_mylink/* - homeassistant/components/sonarr/sensor.py homeassistant/components/sonos/* homeassistant/components/sony_projector/switch.py homeassistant/components/spc/* @@ -748,6 +761,7 @@ omit = homeassistant/components/synology/camera.py homeassistant/components/synology_chat/notify.py homeassistant/components/synology_dsm/__init__.py + homeassistant/components/synology_dsm/binary_sensor.py homeassistant/components/synology_dsm/sensor.py homeassistant/components/synology_srm/device_tracker.py homeassistant/components/syslog/notify.py @@ -872,9 +886,6 @@ omit = homeassistant/components/wirelesstag/* homeassistant/components/worldtidesinfo/sensor.py homeassistant/components/worxlandroid/sensor.py - homeassistant/components/wunderlist/* - homeassistant/components/wwlln/__init__.py - homeassistant/components/wwlln/geo_location.py homeassistant/components/x10/light.py homeassistant/components/xbox_live/sensor.py homeassistant/components/xeoma/camera.py @@ -922,7 +933,7 @@ omit = homeassistant/components/zha/light.py homeassistant/components/zha/sensor.py homeassistant/components/zhong_hong/climate.py - homeassistant/components/zigbee/* + homeassistant/components/xbee/* homeassistant/components/ziggo_mediabox_xl/media_player.py homeassistant/components/zoneminder/* homeassistant/components/supla/* diff --git a/.gitignore b/.gitignore index 2473aeb4bf6..75d63d8bacb 100644 --- a/.gitignore +++ b/.gitignore @@ -106,6 +106,7 @@ virtualization/vagrant/config !.vscode/cSpell.json !.vscode/extensions.json !.vscode/tasks.json +.env # Built docs docs/build diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 491cfc05d8a..42856451494 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,7 +22,7 @@ repos: - --quiet-level=2 exclude_types: [csv, json] - repo: https://gitlab.com/pycqa/flake8 - rev: 3.7.9 + rev: 3.8.1 hooks: - id: flake8 additional_dependencies: @@ -80,8 +80,8 @@ repos: entry: script/run-in-env.sh python3 -m script.gen_requirements_all pass_filenames: false language: script - types: [json] - files: ^homeassistant/.+/manifest\.json$ + types: [text] + files: ^(homeassistant/.+/manifest\.json|\.pre-commit-config\.yaml)$ - id: hassfest name: hassfest entry: script/run-in-env.sh python3 -m script.hassfest diff --git a/CODEOWNERS b/CODEOWNERS index fd224174c90..82e3e388026 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -14,6 +14,7 @@ homeassistant/scripts/check_config.py @kellerza # Integrations homeassistant/components/abode/* @shred86 +homeassistant/components/acmeda/* @atmurray homeassistant/components/adguard/* @frenck homeassistant/components/agent_dvr/* @ispysoftware homeassistant/components/airly/* @bieniu @@ -42,7 +43,6 @@ homeassistant/components/atome/* @baqs homeassistant/components/august/* @bdraco homeassistant/components/aurora_abb_powerone/* @davet2001 homeassistant/components/auth/* @home-assistant/core -homeassistant/components/automatic/* @armills homeassistant/components/automation/* @home-assistant/core homeassistant/components/avea/* @pattyland homeassistant/components/avri/* @timvancann @@ -68,6 +68,7 @@ homeassistant/components/bt_smarthub/* @jxwolstenholme homeassistant/components/buienradar/* @mjj4791 @ties homeassistant/components/cast/* @emontnemery homeassistant/components/cert_expiry/* @Cereal2nd @jjlawren +homeassistant/components/circuit/* @braam homeassistant/components/cisco_ios/* @fbradyirl homeassistant/components/cisco_mobility_express/* @fbradyirl homeassistant/components/cisco_webex_teams/* @fbradyirl @@ -86,7 +87,7 @@ homeassistant/components/cups/* @fabaff homeassistant/components/daikin/* @fredrike homeassistant/components/darksky/* @fabaff homeassistant/components/deconz/* @Kane610 -homeassistant/components/delijn/* @bollewolle +homeassistant/components/delijn/* @bollewolle @Emilv2 homeassistant/components/demo/* @home-assistant/core homeassistant/components/denonavr/* @scarface-4711 @starkillerOG homeassistant/components/derivative/* @afaucogney @@ -97,6 +98,7 @@ homeassistant/components/directv/* @ctalkington homeassistant/components/discogs/* @thibmaek homeassistant/components/doorbird/* @oblogic7 @bdraco homeassistant/components/dsmr_reader/* @depl0y +homeassistant/components/dunehd/* @bieniu homeassistant/components/dweet/* @fabaff homeassistant/components/dynalite/* @ziv1234 homeassistant/components/dyson/* @etheralm @@ -140,7 +142,6 @@ 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 homeassistant/components/geonetnz_quakes/* @exxamalte @@ -149,6 +150,7 @@ homeassistant/components/gios/* @bieniu homeassistant/components/gitter/* @fabaff homeassistant/components/glances/* @fabaff @engrbm87 homeassistant/components/gntp/* @robbiet480 +homeassistant/components/gogogate2/* @vangorra homeassistant/components/google_assistant/* @home-assistant/cloud homeassistant/components/google_cloud/* @lufton homeassistant/components/google_translate/* @awarecan @@ -159,6 +161,7 @@ homeassistant/components/griddy/* @bdraco homeassistant/components/group/* @home-assistant/core homeassistant/components/growatt_server/* @indykoning homeassistant/components/gtfs/* @robbiet480 +homeassistant/components/guardian/* @bachya homeassistant/components/harmony/* @ehendrix23 @bramkragten @bdraco homeassistant/components/hassio/* @home-assistant/hass-io homeassistant/components/heatmiser/* @andylockran @@ -178,7 +181,7 @@ homeassistant/components/homematicip_cloud/* @SukramJ homeassistant/components/honeywell/* @zxdavb homeassistant/components/html5/* @robbiet480 homeassistant/components/http/* @home-assistant/core -homeassistant/components/huawei_lte/* @scop +homeassistant/components/huawei_lte/* @scop @fphammerle homeassistant/components/huawei_router/* @abmantis homeassistant/components/hue/* @balloob homeassistant/components/hunterdouglas_powerview/* @bdraco @@ -193,6 +196,7 @@ homeassistant/components/input_datetime/* @home-assistant/core homeassistant/components/input_number/* @home-assistant/core homeassistant/components/input_select/* @home-assistant/core homeassistant/components/input_text/* @home-assistant/core +homeassistant/components/insteon/* @teharris1 homeassistant/components/integration/* @dgomes homeassistant/components/intent/* @home-assistant/core homeassistant/components/intesishome/* @jnimmo @@ -306,11 +310,11 @@ homeassistant/components/pilight/* @trekky12 homeassistant/components/plaato/* @JohNan homeassistant/components/plant/* @ChristianKuehnel homeassistant/components/plex/* @jjlawren -homeassistant/components/plugwise/* @laetificat @CoMPaTech @bouwew +homeassistant/components/plugwise/* @CoMPaTech @bouwew homeassistant/components/plum_lightpad/* @ColinHarrington homeassistant/components/point/* @fredrike homeassistant/components/powerwall/* @bdraco @jrester -homeassistant/components/proxmoxve/* @k4ds3 +homeassistant/components/proxmoxve/* @k4ds3 @jhollowe homeassistant/components/ps4/* @ktnrg45 homeassistant/components/ptvsd/* @swamp-ig homeassistant/components/push/* @dgomes @@ -370,7 +374,6 @@ homeassistant/components/soma/* @ratsept homeassistant/components/somfy/* @tetienne homeassistant/components/sonarr/* @ctalkington homeassistant/components/songpal/* @rytilahti @shenxn -homeassistant/components/sonos/* @amelchio homeassistant/components/spaceapi/* @fabaff homeassistant/components/speedtestdotnet/* @rohankapoorcom homeassistant/components/spider/* @peternijssen @@ -456,7 +459,6 @@ homeassistant/components/withings/* @vangorra homeassistant/components/wled/* @frenck homeassistant/components/workday/* @fabaff homeassistant/components/worldclock/* @fabaff -homeassistant/components/wwlln/* @bachya homeassistant/components/xbox_live/* @MartinHjelmare homeassistant/components/xfinity/* @cisasteelersfan homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 5d2149dce05..f9b1ea79314 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -2,79 +2,139 @@ ## Our Pledge -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, gender identity and expression, level of experience, -nationality, personal appearance, race, religion, or sexual identity and -orientation. +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. ## Our Standards -Examples of behavior that contributes to creating a positive environment -include: +Examples of behavior that contributes to a positive environment for our +community include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community -Examples of unacceptable behavior by participants include: +Examples of unacceptable behavior include: -* The use of sexualized language or imagery and unwelcome sexual attention or -advances -* Trolling, insulting/derogatory comments, and personal or political attacks +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission +* Publishing others' private information, such as a physical or email + address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting -## Our Responsibilities +## Enforcement Responsibilities -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. ## Scope -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at [safety@home-assistant.io][email]. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. +reported to the community leaders responsible for enforcement at +[safety@home-assistant.io][email] or by using the report/flag feature of +the medium used. All complaints will be reviewed and investigated promptly and +fairly. -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available [here][version]. +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available [here][version]. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder][mozilla]. ## Adoption -This Code of Conduct was first adopted January 21st, 2017 and announced in [this][coc-blog] blog post. +This Code of Conduct was first adopted January 21st, 2017 and announced in +[this][coc-blog] blog post and has been updated on May 25th, 2020 to version +2.0 of the [Contributor Covenant][homepage] as announced in [this][coc2-blog] +blog post. -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ +For answers to common questions about this code of conduct, see the FAQ at +. Translations are available at +. + +[coc-blog]: /blog/2017/01/21/home-assistant-governance/ +[coc2-blog]: /blog/2020/05/25/code-of-conduct-updated/ [email]: mailto:safety@home-assistant.io -[coc-blog]: https://home-assistant.io/blog/2017/01/21/home-assistant-governance/ +[homepage]: http://contributor-covenant.org +[mozilla]: https://github.com/mozilla/diversity +[version]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html diff --git a/Dockerfile.dev b/Dockerfile.dev index be8e2223390..4c2a7ebe9e3 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,4 +1,4 @@ -FROM python:3.8 +FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.8 RUN \ apt-get update && apt-get install -y --no-install-recommends \ diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index da60db941da..975899d3113 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -158,7 +158,7 @@ stages: steps: - template: templates/azp-step-cache.yaml@azure parameters: - keyfile: "requirements_test_all.txt | homeassistant/package_constraints.txt" + keyfile: "requirements_test_all.txt | requirements_test.txt | homeassistant/package_constraints.txt" build: | set -e python -m venv venv diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index d53d86f528c..086416b7d35 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -14,15 +14,14 @@ 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.helpers.typing import ConfigType -from homeassistant.setup import DATA_SETUP, async_setup_component -from homeassistant.util.logging import AsyncHandler +from homeassistant.setup import DATA_SETUP, DATA_SETUP_STARTED, async_setup_component +from homeassistant.util.logging import async_activate_log_queue_handler from homeassistant.util.package import async_get_user_site, is_virtual_env from homeassistant.util.yaml import clear_secret_cache @@ -33,6 +32,8 @@ ERROR_LOG_FILENAME = "home-assistant.log" # hass.data key for logging information. DATA_LOGGING = "logging" +LOG_SLOW_STARTUP_INTERVAL = 60 + DEBUGGER_INTEGRATIONS = {"ptvsd"} CORE_INTEGRATIONS = ("homeassistant", "persistent_notification") LOGGING_INTEGRATIONS = {"logger", "system_log", "sentry"} @@ -43,6 +44,13 @@ STAGE_1_INTEGRATIONS = { "mqtt_eventstream", # To provide account link implementations "cloud", + # Ensure supervisor is available + "hassio", + # Get the frontend up and running as soon + # as possible so problem integrations can + # be removed + "frontend", + "config", } @@ -278,24 +286,17 @@ def async_enable_logging( err_handler.setLevel(logging.INFO if verbose else logging.WARNING) err_handler.setFormatter(logging.Formatter(fmt, datefmt=datefmt)) - async_handler = AsyncHandler(hass.loop, err_handler) - - async def async_stop_async_handler(_: Any) -> None: - """Cleanup async handler.""" - logging.getLogger("").removeHandler(async_handler) # type: ignore - await async_handler.async_close(blocking=True) - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler) - logger = logging.getLogger("") - logger.addHandler(async_handler) # type: ignore - logger.setLevel(logging.INFO) + logger.addHandler(err_handler) + logger.setLevel(logging.INFO if verbose else logging.WARNING) # Save the log file location for access by other components. hass.data[DATA_LOGGING] = err_log_path else: _LOGGER.error("Unable to set up error log %s (access denied)", err_log_path) + async_activate_log_queue_handler(hass) + async def async_mount_local_lib_path(config_dir: str) -> str: """Add local library to Python Path. @@ -331,13 +332,30 @@ async def _async_set_up_integrations( ) -> None: """Set up all the integrations.""" + setup_started = hass.data[DATA_SETUP_STARTED] = {} + async def async_setup_multi_components(domains: Set[str]) -> None: """Set up multiple domains. Log on failure.""" + + async def _async_log_pending_setups() -> None: + """Periodic log of setups that are pending for longer than LOG_SLOW_STARTUP_INTERVAL.""" + while True: + await asyncio.sleep(LOG_SLOW_STARTUP_INTERVAL) + remaining = [domain for domain in domains if domain in setup_started] + + if remaining: + _LOGGER.info( + "Waiting on integrations to complete setup: %s", + ", ".join(remaining), + ) + futures = { domain: hass.async_create_task(async_setup_component(hass, domain, config)) for domain in domains } + log_task = asyncio.create_task(_async_log_pending_setups()) await asyncio.wait(futures.values()) + log_task.cancel() errors = [domain for domain in domains if futures[domain].exception()] for domain in errors: exception = futures[domain].exception() @@ -388,6 +406,8 @@ async def _async_set_up_integrations( ) if stage_1_domains: + _LOGGER.info("Setting up %s", stage_1_domains) + await async_setup_multi_components(stage_1_domains) # Load all integrations @@ -430,4 +450,5 @@ async def _async_set_up_integrations( await async_setup_multi_components(stage_2_domains) # Wrap up startup + _LOGGER.debug("Waiting for startup to wrap up") await hass.async_block_till_done() diff --git a/homeassistant/components/abode/translations/hu.json b/homeassistant/components/abode/translations/hu.json index 89b695da7d9..90ca7cafb34 100644 --- a/homeassistant/components/abode/translations/hu.json +++ b/homeassistant/components/abode/translations/hu.json @@ -12,7 +12,7 @@ "user": { "data": { "password": "Jelsz\u00f3", - "username": "Email c\u00edm" + "username": "E-mail" }, "title": "T\u00f6ltse ki az Abode bejelentkez\u00e9si adatait" } diff --git a/homeassistant/components/abode/translations/it.json b/homeassistant/components/abode/translations/it.json index 414ffb92ef4..97b5d562283 100644 --- a/homeassistant/components/abode/translations/it.json +++ b/homeassistant/components/abode/translations/it.json @@ -12,7 +12,7 @@ "user": { "data": { "password": "Password", - "username": "Indirizzo email" + "username": "E-mail" }, "title": "Inserisci le tue informazioni di accesso Abode" } diff --git a/homeassistant/components/abode/translations/pl.json b/homeassistant/components/abode/translations/pl.json index 6efd1ae885c..d7a25bb20b7 100644 --- a/homeassistant/components/abode/translations/pl.json +++ b/homeassistant/components/abode/translations/pl.json @@ -11,8 +11,8 @@ "step": { "user": { "data": { - "password": "[%key_id:common::config_flow::data::password%]", - "username": "[%key_id:common::config_flow::data::email%]" + "password": "Has\u0142o", + "username": "Adres e-mail" }, "title": "Wprowad\u017a informacje logowania Abode" } diff --git a/homeassistant/components/acmeda/__init__.py b/homeassistant/components/acmeda/__init__.py new file mode 100644 index 00000000000..3b4f135a6fd --- /dev/null +++ b/homeassistant/components/acmeda/__init__.py @@ -0,0 +1,59 @@ +"""The Rollease Acmeda Automate integration.""" +import asyncio + +from homeassistant import config_entries, core + +from .const import DOMAIN +from .hub import PulseHub + +CONF_HUBS = "hubs" + +PLATFORMS = ["cover", "sensor"] + + +async def async_setup(hass: core.HomeAssistant, config: dict): + """Set up the Rollease Acmeda Automate component.""" + return True + + +async def async_setup_entry( + hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry +): + """Set up Rollease Acmeda Automate hub from a config entry.""" + hub = PulseHub(hass, config_entry) + + if not await hub.async_setup(): + return False + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][config_entry.entry_id] = hub + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, component) + ) + + return True + + +async def async_unload_entry( + hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry +): + """Unload a config entry.""" + hub = hass.data[DOMAIN][config_entry.entry_id] + + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(config_entry, component) + for component in PLATFORMS + ] + ) + ) + if not await hub.async_reset(): + return False + + if unload_ok: + hass.data[DOMAIN].pop(config_entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/acmeda/base.py b/homeassistant/components/acmeda/base.py new file mode 100644 index 00000000000..c467fe17ba3 --- /dev/null +++ b/homeassistant/components/acmeda/base.py @@ -0,0 +1,89 @@ +"""Base class for Acmeda Roller Blinds.""" +import aiopulse + +from homeassistant.core import callback +from homeassistant.helpers import entity +from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg + +from .const import ACMEDA_ENTITY_REMOVE, DOMAIN, LOGGER + + +class AcmedaBase(entity.Entity): + """Base representation of an Acmeda roller.""" + + def __init__(self, roller: aiopulse.Roller): + """Initialize the roller.""" + self.roller = roller + + async def async_remove_and_unregister(self): + """Unregister from entity and device registry and call entity remove function.""" + LOGGER.error("Removing %s %s", self.__class__.__name__, self.unique_id) + + ent_registry = await get_ent_reg(self.hass) + if self.entity_id in ent_registry.entities: + ent_registry.async_remove(self.entity_id) + + dev_registry = await get_dev_reg(self.hass) + device = dev_registry.async_get_device( + identifiers={(DOMAIN, self.unique_id)}, connections=set() + ) + if device is not None: + dev_registry.async_update_device( + device.id, remove_config_entry_id=self.registry_entry.config_entry_id + ) + + await self.async_remove() + + async def async_added_to_hass(self): + """Entity has been added to hass.""" + self.roller.callback_subscribe(self.notify_update) + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + ACMEDA_ENTITY_REMOVE.format(self.roller.id), + self.async_remove_and_unregister, + ) + ) + + async def async_will_remove_from_hass(self): + """Entity being removed from hass.""" + self.roller.callback_unsubscribe(self.notify_update) + + @callback + def notify_update(self): + """Write updated device state information.""" + LOGGER.debug("Device update notification received: %s", self.name) + self.async_write_ha_state() + + @property + def should_poll(self): + """Report that Acmeda entities do not need polling.""" + return False + + @property + def unique_id(self): + """Return the unique ID of this roller.""" + return self.roller.id + + @property + def device_id(self): + """Return the ID of this roller.""" + return self.roller.id + + @property + def name(self): + """Return the name of roller.""" + return self.roller.name + + @property + def device_info(self): + """Return the device info.""" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.roller.name, + "manufacturer": "Rollease Acmeda", + "via_device": (DOMAIN, self.roller.hub.id), + } diff --git a/homeassistant/components/acmeda/config_flow.py b/homeassistant/components/acmeda/config_flow.py new file mode 100644 index 00000000000..33dac4814e5 --- /dev/null +++ b/homeassistant/components/acmeda/config_flow.py @@ -0,0 +1,71 @@ +"""Config flow for Rollease Acmeda Automate Pulse Hub.""" +import asyncio +from typing import Dict, Optional + +import aiopulse +import async_timeout +import voluptuous as vol + +from homeassistant import config_entries + +from .const import DOMAIN # pylint: disable=unused-import + + +class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Acmeda config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize the config flow.""" + self.discovered_hubs: Optional[Dict[str, aiopulse.Hub]] = None + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + if ( + user_input is not None + and self.discovered_hubs is not None + # pylint: disable=unsupported-membership-test + and user_input["id"] in self.discovered_hubs + ): + # pylint: disable=unsubscriptable-object + return await self.async_create(self.discovered_hubs[user_input["id"]]) + + # Already configured hosts + already_configured = { + entry.unique_id for entry in self._async_current_entries() + } + + hubs = [] + try: + with async_timeout.timeout(5): + async for hub in aiopulse.Hub.discover(): + if hub.id not in already_configured: + hubs.append(hub) + except asyncio.TimeoutError: + pass + + if len(hubs) == 0: + return self.async_abort(reason="all_configured") + + if len(hubs) == 1: + return await self.async_create(hubs[0]) + + self.discovered_hubs = {hub.id: hub for hub in hubs} + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required("id"): vol.In( + {hub.id: f"{hub.id} {hub.host}" for hub in hubs} + ) + } + ), + ) + + async def async_create(self, hub): + """Create the Acmeda Hub entry.""" + await self.async_set_unique_id(hub.id, raise_on_progress=False) + return self.async_create_entry(title=hub.id, data={"host": hub.host}) diff --git a/homeassistant/components/acmeda/const.py b/homeassistant/components/acmeda/const.py new file mode 100644 index 00000000000..b8712fee4ba --- /dev/null +++ b/homeassistant/components/acmeda/const.py @@ -0,0 +1,8 @@ +"""Constants for the Rollease Acmeda Automate integration.""" +import logging + +LOGGER = logging.getLogger(__package__) +DOMAIN = "acmeda" + +ACMEDA_HUB_UPDATE = "acmeda_hub_update_{}" +ACMEDA_ENTITY_REMOVE = "acmeda_entity_remove_{}" diff --git a/homeassistant/components/acmeda/cover.py b/homeassistant/components/acmeda/cover.py new file mode 100644 index 00000000000..0a4436bc073 --- /dev/null +++ b/homeassistant/components/acmeda/cover.py @@ -0,0 +1,122 @@ +"""Support for Acmeda Roller Blinds.""" +from homeassistant.components.cover import ( + ATTR_POSITION, + SUPPORT_CLOSE, + SUPPORT_CLOSE_TILT, + SUPPORT_OPEN, + SUPPORT_OPEN_TILT, + SUPPORT_SET_POSITION, + SUPPORT_SET_TILT_POSITION, + SUPPORT_STOP, + SUPPORT_STOP_TILT, + CoverEntity, +) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .base import AcmedaBase +from .const import ACMEDA_HUB_UPDATE, DOMAIN +from .helpers import async_add_acmeda_entities + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Acmeda Rollers from a config entry.""" + hub = hass.data[DOMAIN][config_entry.entry_id] + + current = set() + + @callback + def async_add_acmeda_covers(): + async_add_acmeda_entities( + hass, AcmedaCover, config_entry, current, async_add_entities + ) + + hub.cleanup_callbacks.append( + async_dispatcher_connect( + hass, + ACMEDA_HUB_UPDATE.format(config_entry.entry_id), + async_add_acmeda_covers, + ) + ) + + +class AcmedaCover(AcmedaBase, CoverEntity): + """Representation of a Acmeda cover device.""" + + @property + def current_cover_position(self): + """Return the current position of the roller blind. + + None is unknown, 0 is closed, 100 is fully open. + """ + position = None + if self.roller.type != 7: + position = 100 - self.roller.closed_percent + return position + + @property + def current_cover_tilt_position(self): + """Return the current tilt of the roller blind. + + None is unknown, 0 is closed, 100 is fully open. + """ + position = None + if self.roller.type == 7 or self.roller.type == 10: + position = 100 - self.roller.closed_percent + return position + + @property + def supported_features(self): + """Flag supported features.""" + supported_features = 0 + if self.current_cover_position is not None: + supported_features |= ( + SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP | SUPPORT_SET_POSITION + ) + if self.current_cover_tilt_position is not None: + supported_features |= ( + SUPPORT_OPEN_TILT + | SUPPORT_CLOSE_TILT + | SUPPORT_STOP_TILT + | SUPPORT_SET_TILT_POSITION + ) + + return supported_features + + @property + def is_closed(self): + """Return if the cover is closed.""" + is_closed = self.roller.closed_percent == 100 + return is_closed + + async def close_cover(self, **kwargs): + """Close the roller.""" + await self.roller.move_down() + + async def open_cover(self, **kwargs): + """Open the roller.""" + await self.roller.move_up() + + async def stop_cover(self, **kwargs): + """Stop the roller.""" + await self.roller.move_stop() + + async def set_cover_position(self, **kwargs): + """Move the roller shutter to a specific position.""" + await self.roller.move_to(100 - kwargs[ATTR_POSITION]) + + async def close_cover_tilt(self, **kwargs): + """Close the roller.""" + await self.roller.move_down() + + async def open_cover_tilt(self, **kwargs): + """Open the roller.""" + await self.roller.move_up() + + async def stop_cover_tilt(self, **kwargs): + """Stop the roller.""" + await self.roller.move_stop() + + async def set_cover_tilt(self, **kwargs): + """Tilt the roller shutter to a specific position.""" + await self.roller.move_to(100 - kwargs[ATTR_POSITION]) diff --git a/homeassistant/components/acmeda/errors.py b/homeassistant/components/acmeda/errors.py new file mode 100644 index 00000000000..f26090df03d --- /dev/null +++ b/homeassistant/components/acmeda/errors.py @@ -0,0 +1,10 @@ +"""Errors for the Acmeda Pulse component.""" +from homeassistant.exceptions import HomeAssistantError + + +class PulseException(HomeAssistantError): + """Base class for Acmeda Pulse exceptions.""" + + +class CannotConnect(PulseException): + """Unable to connect to the bridge.""" diff --git a/homeassistant/components/acmeda/helpers.py b/homeassistant/components/acmeda/helpers.py new file mode 100644 index 00000000000..f8ea744be77 --- /dev/null +++ b/homeassistant/components/acmeda/helpers.py @@ -0,0 +1,41 @@ +"""Helper functions for Acmeda Pulse.""" +from homeassistant.core import callback +from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg + +from .const import DOMAIN, LOGGER + + +@callback +def async_add_acmeda_entities( + hass, entity_class, config_entry, current, async_add_entities +): + """Add any new entities.""" + hub = hass.data[DOMAIN][config_entry.entry_id] + LOGGER.debug("Looking for new %s on: %s", entity_class.__name__, hub.host) + + api = hub.api.rollers + + new_items = [] + for unique_id, roller in api.items(): + if unique_id not in current: + LOGGER.debug("New %s %s", entity_class.__name__, unique_id) + new_item = entity_class(roller) + current.add(unique_id) + new_items.append(new_item) + + async_add_entities(new_items) + + +async def update_devices(hass, config_entry, api): + """Tell hass that device info has been updated.""" + dev_registry = await get_dev_reg(hass) + + for api_item in api.values(): + # Update Device name + device = dev_registry.async_get_device( + identifiers={(DOMAIN, api_item.id)}, connections=set() + ) + if device is not None: + dev_registry.async_update_device( + device.id, name=api_item.name, + ) diff --git a/homeassistant/components/acmeda/hub.py b/homeassistant/components/acmeda/hub.py new file mode 100644 index 00000000000..0b74b874dcc --- /dev/null +++ b/homeassistant/components/acmeda/hub.py @@ -0,0 +1,88 @@ +"""Code to handle a Pulse Hub.""" +import asyncio +from typing import Optional + +import aiopulse + +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from .const import ACMEDA_ENTITY_REMOVE, ACMEDA_HUB_UPDATE, LOGGER +from .helpers import update_devices + + +class PulseHub: + """Manages a single Pulse Hub.""" + + def __init__(self, hass, config_entry): + """Initialize the system.""" + self.config_entry = config_entry + self.hass = hass + self.api: Optional[aiopulse.Hub] = None + self.tasks = [] + self.current_rollers = {} + self.cleanup_callbacks = [] + + @property + def title(self): + """Return the title of the hub shown in the integrations list.""" + return f"{self.api.id} ({self.api.host})" + + @property + def host(self): + """Return the host of this hub.""" + return self.config_entry.data["host"] + + async def async_setup(self, tries=0): + """Set up a hub based on host parameter.""" + host = self.host + + hub = aiopulse.Hub(host) + self.api = hub + + hub.callback_subscribe(self.async_notify_update) + self.tasks.append(asyncio.create_task(hub.run())) + + LOGGER.debug("Hub setup complete") + return True + + async def async_reset(self): + """Reset this hub to default state.""" + + for cleanup_callback in self.cleanup_callbacks: + cleanup_callback() + + # If not setup + if self.api is None: + return False + + self.api.callback_unsubscribe(self.async_notify_update) + await self.api.stop() + del self.api + self.api = None + + # Wait for any running tasks to complete + await asyncio.wait(self.tasks) + + return True + + async def async_notify_update(self, update_type): + """Evaluate entities when hub reports that update has occurred.""" + LOGGER.debug("Hub {update_type.name} updated") + + if update_type == aiopulse.UpdateType.rollers: + await update_devices(self.hass, self.config_entry, self.api.rollers) + self.hass.config_entries.async_update_entry( + self.config_entry, title=self.title + ) + + async_dispatcher_send( + self.hass, ACMEDA_HUB_UPDATE.format(self.config_entry.entry_id) + ) + + for unique_id in list(self.current_rollers): + if unique_id not in self.api.rollers: + LOGGER.debug("Notifying remove of %s", unique_id) + self.current_rollers.pop(unique_id) + async_dispatcher_send( + self.hass, ACMEDA_ENTITY_REMOVE.format(unique_id) + ) diff --git a/homeassistant/components/acmeda/manifest.json b/homeassistant/components/acmeda/manifest.json new file mode 100644 index 00000000000..8b76af0c57e --- /dev/null +++ b/homeassistant/components/acmeda/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "acmeda", + "name": "Rollease Acmeda Automate", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/acmeda", + "requirements": ["aiopulse==0.4.0"], + "codeowners": [ + "@atmurray" + ] +} \ No newline at end of file diff --git a/homeassistant/components/acmeda/sensor.py b/homeassistant/components/acmeda/sensor.py new file mode 100644 index 00000000000..e549160fbdd --- /dev/null +++ b/homeassistant/components/acmeda/sensor.py @@ -0,0 +1,46 @@ +"""Support for Acmeda Roller Blind Batteries.""" +from homeassistant.const import DEVICE_CLASS_BATTERY, UNIT_PERCENTAGE +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .base import AcmedaBase +from .const import ACMEDA_HUB_UPDATE, DOMAIN +from .helpers import async_add_acmeda_entities + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Acmeda Rollers from a config entry.""" + hub = hass.data[DOMAIN][config_entry.entry_id] + + current = set() + + @callback + def async_add_acmeda_sensors(): + async_add_acmeda_entities( + hass, AcmedaBattery, config_entry, current, async_add_entities + ) + + hub.cleanup_callbacks.append( + async_dispatcher_connect( + hass, + ACMEDA_HUB_UPDATE.format(config_entry.entry_id), + async_add_acmeda_sensors, + ) + ) + + +class AcmedaBattery(AcmedaBase): + """Representation of a Acmeda cover device.""" + + device_class = DEVICE_CLASS_BATTERY + unit_of_measurement = UNIT_PERCENTAGE + + @property + def name(self): + """Return the name of roller.""" + return f"{super().name} Battery" + + @property + def state(self): + """Return the state of the device.""" + return self.roller.battery diff --git a/homeassistant/components/acmeda/strings.json b/homeassistant/components/acmeda/strings.json new file mode 100644 index 00000000000..eb7ed44999b --- /dev/null +++ b/homeassistant/components/acmeda/strings.json @@ -0,0 +1,16 @@ +{ + "title": "Rollease Acmeda Automate", + "config": { + "step": { + "user": { + "title": "Pick a hub to add", + "data": { + "id": "Host ID" + } + } + }, + "abort": { + "all_configured": "No new Pulse hubs discovered." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/acmeda/translations/ca.json b/homeassistant/components/acmeda/translations/ca.json new file mode 100644 index 00000000000..0812387ab7f --- /dev/null +++ b/homeassistant/components/acmeda/translations/ca.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "all_configured": "No s'han descobert nous hubs de Pulse." + }, + "step": { + "user": { + "data": { + "id": "ID d'amfitri\u00f3" + }, + "title": "Selecci\u00f3 del Hub a afegir" + } + } + }, + "title": "Rollease Acmeda Automate" +} \ No newline at end of file diff --git a/homeassistant/components/wwlln/translations/pt.json b/homeassistant/components/acmeda/translations/de.json similarity index 59% rename from homeassistant/components/wwlln/translations/pt.json rename to homeassistant/components/acmeda/translations/de.json index c7081cd694a..bef1c5e9e99 100644 --- a/homeassistant/components/wwlln/translations/pt.json +++ b/homeassistant/components/acmeda/translations/de.json @@ -3,8 +3,7 @@ "step": { "user": { "data": { - "latitude": "Latitude", - "longitude": "Longitude" + "id": "Host-ID" } } } diff --git a/homeassistant/components/acmeda/translations/en.json b/homeassistant/components/acmeda/translations/en.json new file mode 100644 index 00000000000..ab20d0b1939 --- /dev/null +++ b/homeassistant/components/acmeda/translations/en.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "all_configured": "No new Pulse hubs discovered." + }, + "step": { + "user": { + "data": { + "id": "Host ID" + }, + "title": "Pick a hub to add" + } + } + }, + "title": "Rollease Acmeda Automate" +} \ No newline at end of file diff --git a/homeassistant/components/acmeda/translations/es.json b/homeassistant/components/acmeda/translations/es.json new file mode 100644 index 00000000000..0ca3dbf6e2f --- /dev/null +++ b/homeassistant/components/acmeda/translations/es.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "all_configured": "No se han descubierto nuevos hubs Pulse." + }, + "step": { + "user": { + "data": { + "id": "ID de host" + }, + "title": "Elige un hub para a\u00f1adir" + } + } + }, + "title": "Rollease Acmeda Automate" +} \ No newline at end of file diff --git a/homeassistant/components/acmeda/translations/fr.json b/homeassistant/components/acmeda/translations/fr.json new file mode 100644 index 00000000000..03798dc33b7 --- /dev/null +++ b/homeassistant/components/acmeda/translations/fr.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "id": "ID de l'h\u00f4te" + }, + "title": "Choisissez un hub \u00e0 ajouter" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/acmeda/translations/it.json b/homeassistant/components/acmeda/translations/it.json new file mode 100644 index 00000000000..a9349d06923 --- /dev/null +++ b/homeassistant/components/acmeda/translations/it.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "all_configured": "Non sono stati scoperti nuovi hub Pulse." + }, + "step": { + "user": { + "data": { + "id": "ID host" + }, + "title": "Scegliere un hub da aggiungere" + } + } + }, + "title": "Rollease Acmeda Automate" +} \ No newline at end of file diff --git a/homeassistant/components/acmeda/translations/ko.json b/homeassistant/components/acmeda/translations/ko.json new file mode 100644 index 00000000000..cc79dada5bd --- /dev/null +++ b/homeassistant/components/acmeda/translations/ko.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "all_configured": "\ubc1c\uacac\ub41c \uc0c8\ub85c\uc6b4 Pulse \ud5c8\ube0c\uac00 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "step": { + "user": { + "data": { + "id": "\ud638\uc2a4\ud2b8 ID" + }, + "title": "\ucd94\uac00\ud560 \ud5c8\ube0c \uc120\ud0dd\ud558\uae30" + } + } + }, + "title": "Rollease Acmeda Automate" +} \ No newline at end of file diff --git a/homeassistant/components/acmeda/translations/lb.json b/homeassistant/components/acmeda/translations/lb.json new file mode 100644 index 00000000000..27ae3072de5 --- /dev/null +++ b/homeassistant/components/acmeda/translations/lb.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "all_configured": "Keng nei Pulse Hubs entdeckt." + }, + "step": { + "user": { + "data": { + "id": "Host ID" + }, + "title": "Wiel den Hub aus dee soll dob\u00e4igesat ginn." + } + } + }, + "title": "Rollease ACmeda Automate" +} \ No newline at end of file diff --git a/homeassistant/components/acmeda/translations/nl.json b/homeassistant/components/acmeda/translations/nl.json new file mode 100644 index 00000000000..76b680a5f8c --- /dev/null +++ b/homeassistant/components/acmeda/translations/nl.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "all_configured": "Geen nieuwe Pulse hubs ontdekt." + }, + "step": { + "user": { + "data": { + "id": "Host ID" + }, + "title": "Kies een hub om toe te voegen" + } + } + }, + "title": "Rollease Acmeda Automate" +} \ No newline at end of file diff --git a/homeassistant/components/acmeda/translations/no.json b/homeassistant/components/acmeda/translations/no.json new file mode 100644 index 00000000000..66335077cfb --- /dev/null +++ b/homeassistant/components/acmeda/translations/no.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "all_configured": "Ingen nye Pulse-hub oppdaget." + }, + "step": { + "user": { + "data": { + "id": "Verts-ID" + }, + "title": "Velg en hub du vil legge til" + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/acmeda/translations/pl.json b/homeassistant/components/acmeda/translations/pl.json new file mode 100644 index 00000000000..4732d44580f --- /dev/null +++ b/homeassistant/components/acmeda/translations/pl.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "all_configured": "Nie wykryto hub\u00f3w Pulse." + }, + "step": { + "user": { + "data": { + "id": "ID hosta" + }, + "title": "Wybierz hub, kt\u00f3ry chcesz doda\u0107" + } + } + }, + "title": "Rollease Acmeda Automate" +} \ No newline at end of file diff --git a/homeassistant/components/acmeda/translations/ru.json b/homeassistant/components/acmeda/translations/ru.json new file mode 100644 index 00000000000..92922fdbb5d --- /dev/null +++ b/homeassistant/components/acmeda/translations/ru.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "all_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u044b." + }, + "step": { + "user": { + "data": { + "id": "ID \u0445\u043e\u0441\u0442\u0430" + }, + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0445\u0430\u0431, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0443\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c" + } + } + }, + "title": "Rollease Acmeda Automate" +} \ No newline at end of file diff --git a/homeassistant/components/acmeda/translations/zh-Hant.json b/homeassistant/components/acmeda/translations/zh-Hant.json new file mode 100644 index 00000000000..5d4263ea5ae --- /dev/null +++ b/homeassistant/components/acmeda/translations/zh-Hant.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "all_configured": "\u672a\u641c\u5c0b\u5230 Pulse hub" + }, + "step": { + "user": { + "data": { + "id": "\u4e3b\u6a5f ID" + }, + "title": "\u9078\u64c7\u6240\u8981\u65b0\u589e\u7684 Hub" + } + } + }, + "title": "Rollease Acmeda Automate" +} \ No newline at end of file diff --git a/homeassistant/components/adguard/translations/bg.json b/homeassistant/components/adguard/translations/bg.json index cc68f3e3b28..bc8dff7fedd 100644 --- a/homeassistant/components/adguard/translations/bg.json +++ b/homeassistant/components/adguard/translations/bg.json @@ -21,8 +21,7 @@ "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435", "verify_ssl": "AdGuard Home \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u043d\u0430\u0434\u0435\u0436\u0434\u0435\u043d \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0412\u0430\u0448\u0438\u044f AdGuard Home, \u0437\u0430 \u0434\u0430 \u043f\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u0435 \u043d\u0430\u0431\u043b\u044e\u0434\u0435\u043d\u0438\u0435 \u0438 \u043a\u043e\u043d\u0442\u0440\u043e\u043b.", - "title": "\u0421\u0432\u044a\u0440\u0436\u0435\u0442\u0435 \u0412\u0430\u0448\u0438\u044f AdGuard Home." + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0412\u0430\u0448\u0438\u044f AdGuard Home, \u0437\u0430 \u0434\u0430 \u043f\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u0435 \u043d\u0430\u0431\u043b\u044e\u0434\u0435\u043d\u0438\u0435 \u0438 \u043a\u043e\u043d\u0442\u0440\u043e\u043b." } } } diff --git a/homeassistant/components/adguard/translations/ca.json b/homeassistant/components/adguard/translations/ca.json index 06b355273fe..ae142c7382c 100644 --- a/homeassistant/components/adguard/translations/ca.json +++ b/homeassistant/components/adguard/translations/ca.json @@ -23,8 +23,7 @@ "username": "[%key::common::config_flow::data::username%]", "verify_ssl": "AdGuard Home utilitza un certificat adequat" }, - "description": "Configuraci\u00f3 de la inst\u00e0ncia d'AdGuard Home, permet el control i la monitoritzaci\u00f3.", - "title": "Enlla\u00e7ar AdGuard Home." + "description": "Configuraci\u00f3 de la inst\u00e0ncia d'AdGuard Home, permet el control i la monitoritzaci\u00f3." } } } diff --git a/homeassistant/components/adguard/translations/da.json b/homeassistant/components/adguard/translations/da.json index 9d3460b1ed2..20fd721eccc 100644 --- a/homeassistant/components/adguard/translations/da.json +++ b/homeassistant/components/adguard/translations/da.json @@ -21,8 +21,7 @@ "username": "Brugernavn", "verify_ssl": "AdGuard Home bruger et korrekt certifikat" }, - "description": "Konfigurer din AdGuard Home-instans for at tillade overv\u00e5gning og kontrol.", - "title": "Forbind din AdGuard Home." + "description": "Konfigurer din AdGuard Home-instans for at tillade overv\u00e5gning og kontrol." } } } diff --git a/homeassistant/components/adguard/translations/de.json b/homeassistant/components/adguard/translations/de.json index 19c1a5ce6fc..2e320d65e39 100644 --- a/homeassistant/components/adguard/translations/de.json +++ b/homeassistant/components/adguard/translations/de.json @@ -23,8 +23,7 @@ "username": "Benutzername", "verify_ssl": "AdGuard Home verwendet ein richtiges Zertifikat" }, - "description": "Richte deine AdGuard Home-Instanz ein um sie zu \u00dcberwachen und zu Steuern.", - "title": "Verkn\u00fcpfe AdGuard Home." + "description": "Richte deine AdGuard Home-Instanz ein um sie zu \u00dcberwachen und zu Steuern." } } } diff --git a/homeassistant/components/adguard/translations/en.json b/homeassistant/components/adguard/translations/en.json index d9b5d81b469..ffeb11af839 100644 --- a/homeassistant/components/adguard/translations/en.json +++ b/homeassistant/components/adguard/translations/en.json @@ -23,8 +23,7 @@ "username": "Username", "verify_ssl": "AdGuard Home uses a proper certificate" }, - "description": "Set up your AdGuard Home instance to allow monitoring and control.", - "title": "Link your AdGuard Home." + "description": "Set up your AdGuard Home instance to allow monitoring and control." } } } diff --git a/homeassistant/components/adguard/translations/es-419.json b/homeassistant/components/adguard/translations/es-419.json index c0ce604fbee..450bb05c886 100644 --- a/homeassistant/components/adguard/translations/es-419.json +++ b/homeassistant/components/adguard/translations/es-419.json @@ -16,13 +16,14 @@ }, "user": { "data": { + "host": "Host", "password": "Contrase\u00f1a", + "port": "Puerto", "ssl": "AdGuard Home utiliza un certificado SSL", "username": "Nombre de usuario", "verify_ssl": "AdGuard Home utiliza un certificado adecuado" }, - "description": "Configure su instancia de AdGuard Home para permitir la supervisi\u00f3n y el control.", - "title": "Enlace su AdGuard Home." + "description": "Configure su instancia de AdGuard Home para permitir la supervisi\u00f3n y el control." } } } diff --git a/homeassistant/components/adguard/translations/es.json b/homeassistant/components/adguard/translations/es.json index 8e4bc821fdf..70900e15eb5 100644 --- a/homeassistant/components/adguard/translations/es.json +++ b/homeassistant/components/adguard/translations/es.json @@ -23,8 +23,7 @@ "username": "Usuario", "verify_ssl": "AdGuard Home utiliza un certificado apropiado" }, - "description": "Configure su instancia de AdGuard Home para permitir la supervisi\u00f3n y el control.", - "title": "Enlace su AdGuard Home." + "description": "Configure su instancia de AdGuard Home para permitir la supervisi\u00f3n y el control." } } } diff --git a/homeassistant/components/adguard/translations/fr.json b/homeassistant/components/adguard/translations/fr.json index 777e1e992af..3819aa9ec76 100644 --- a/homeassistant/components/adguard/translations/fr.json +++ b/homeassistant/components/adguard/translations/fr.json @@ -23,8 +23,7 @@ "username": "Nom d'utilisateur", "verify_ssl": "AdGuard Home utilise un certificat appropri\u00e9" }, - "description": "Configurez votre instance AdGuard Home pour permettre la surveillance et le contr\u00f4le.", - "title": "Liez votre AdGuard Home." + "description": "Configurez votre instance AdGuard Home pour permettre la surveillance et le contr\u00f4le." } } } diff --git a/homeassistant/components/adguard/translations/hu.json b/homeassistant/components/adguard/translations/hu.json index 34b601027c2..1ca56c7684f 100644 --- a/homeassistant/components/adguard/translations/hu.json +++ b/homeassistant/components/adguard/translations/hu.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "host": "Hoszt", "password": "Jelsz\u00f3", "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" diff --git a/homeassistant/components/adguard/translations/it.json b/homeassistant/components/adguard/translations/it.json index c3c9aef22c9..0100f2914b4 100644 --- a/homeassistant/components/adguard/translations/it.json +++ b/homeassistant/components/adguard/translations/it.json @@ -23,8 +23,7 @@ "username": "Nome utente", "verify_ssl": "AdGuard Home utilizza un certificato appropriato" }, - "description": "Configura l'istanza di AdGuard Home per consentire il monitoraggio e il controllo.", - "title": "Collega la tua AdGuard Home." + "description": "Configura l'istanza di AdGuard Home per consentire il monitoraggio e il controllo." } } } diff --git a/homeassistant/components/adguard/translations/ko.json b/homeassistant/components/adguard/translations/ko.json index b5b77e434ca..cdb453b930f 100644 --- a/homeassistant/components/adguard/translations/ko.json +++ b/homeassistant/components/adguard/translations/ko.json @@ -23,8 +23,7 @@ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984", "verify_ssl": "AdGuard Home \uc740 \uc62c\ubc14\ub978 \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4" }, - "description": "\ubaa8\ub2c8\ud130\ub9c1 \ubc0f \uc81c\uc5b4\uac00 \uac00\ub2a5\ud558\ub3c4\ub85d AdGuard Home \uc778\uc2a4\ud134\uc2a4\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694.", - "title": "AdGuard Home \uc5f0\uacb0\ud558\uae30" + "description": "\ubaa8\ub2c8\ud130\ub9c1 \ubc0f \uc81c\uc5b4\uac00 \uac00\ub2a5\ud558\ub3c4\ub85d AdGuard Home \uc778\uc2a4\ud134\uc2a4\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694." } } } diff --git a/homeassistant/components/adguard/translations/lb.json b/homeassistant/components/adguard/translations/lb.json index 76c85138976..97cefb46bc0 100644 --- a/homeassistant/components/adguard/translations/lb.json +++ b/homeassistant/components/adguard/translations/lb.json @@ -23,8 +23,7 @@ "username": "Benotzernumm", "verify_ssl": "AdGuard Home benotzt een eegenen Zertifikat" }, - "description": "Konfigur\u00e9iert \u00e4r AdGuard Home Instanz fir d'Iwwerwaachung an d'Kontroll z'erlaben.", - "title": "Verbannt \u00e4ren AdGuard Home" + "description": "Konfigur\u00e9iert \u00e4r AdGuard Home Instanz fir d'Iwwerwaachung an d'Kontroll z'erlaben." } } } diff --git a/homeassistant/components/adguard/translations/nl.json b/homeassistant/components/adguard/translations/nl.json index 427894eeff4..6d09824a699 100644 --- a/homeassistant/components/adguard/translations/nl.json +++ b/homeassistant/components/adguard/translations/nl.json @@ -21,8 +21,7 @@ "username": "Gebruikersnaam", "verify_ssl": "AdGuard Home maakt gebruik van een goed certificaat" }, - "description": "Stel uw AdGuard Home-instantie in om toezicht en controle mogelijk te maken.", - "title": "Link uw AdGuard Home." + "description": "Stel uw AdGuard Home-instantie in om toezicht en controle mogelijk te maken." } } } diff --git a/homeassistant/components/adguard/translations/no.json b/homeassistant/components/adguard/translations/no.json index 60385c586e2..0633e817db9 100644 --- a/homeassistant/components/adguard/translations/no.json +++ b/homeassistant/components/adguard/translations/no.json @@ -21,8 +21,7 @@ "ssl": "AdGuard Hjem bruker et SSL-sertifikat", "verify_ssl": "AdGuard Home bruker et riktig sertifikat" }, - "description": "Sett opp din AdGuard Hjem instans for \u00e5 tillate overv\u00e5king og kontroll.", - "title": "Koble til ditt AdGuard Hjem." + "description": "Sett opp din AdGuard Hjem instans for \u00e5 tillate overv\u00e5king og kontroll." } } } diff --git a/homeassistant/components/adguard/translations/pl.json b/homeassistant/components/adguard/translations/pl.json index 6cf2de9163d..6451f902642 100644 --- a/homeassistant/components/adguard/translations/pl.json +++ b/homeassistant/components/adguard/translations/pl.json @@ -7,7 +7,7 @@ "single_instance_allowed": "Dozwolona jest tylko jedna konfiguracja AdGuard Home." }, "error": { - "connection_error": "[%key_id:common::config_flow::error::cannot_connect%]" + "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." }, "step": { "hassio_confirm": { @@ -16,15 +16,14 @@ }, "user": { "data": { - "host": "[%key_id:common::config_flow::data::host%]", - "password": "[%key_id:common::config_flow::data::password%]", - "port": "[%key_id:common::config_flow::data::port%]", + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o", + "port": "Port", "ssl": "AdGuard Home u\u017cywa certyfikatu SSL", - "username": "[%key_id:common::config_flow::data::username%]", + "username": "Nazwa u\u017cytkownika", "verify_ssl": "AdGuard Home u\u017cywa odpowiedniego certyfikatu." }, - "description": "Skonfiguruj instancj\u0119 AdGuard Home, aby umo\u017cliwi\u0107 monitorowanie i kontrol\u0119.", - "title": "Po\u0142\u0105cz AdGuard Home" + "description": "Skonfiguruj instancj\u0119 AdGuard Home, aby umo\u017cliwi\u0107 monitorowanie i kontrol\u0119." } } } diff --git a/homeassistant/components/adguard/translations/pt-BR.json b/homeassistant/components/adguard/translations/pt-BR.json index e2dcdf7d312..942c793b9b4 100644 --- a/homeassistant/components/adguard/translations/pt-BR.json +++ b/homeassistant/components/adguard/translations/pt-BR.json @@ -19,8 +19,7 @@ "username": "Nome de usu\u00e1rio", "verify_ssl": "O AdGuard Home usa um certificado apropriado" }, - "description": "Configure sua inst\u00e2ncia do AdGuard Home para permitir o monitoramento e o controle.", - "title": "Vincule o seu AdGuard Home." + "description": "Configure sua inst\u00e2ncia do AdGuard Home para permitir o monitoramento e o controle." } } } diff --git a/homeassistant/components/adguard/translations/ru.json b/homeassistant/components/adguard/translations/ru.json index 1287b408544..e9998587353 100644 --- a/homeassistant/components/adguard/translations/ru.json +++ b/homeassistant/components/adguard/translations/ru.json @@ -23,8 +23,7 @@ "username": "\u041b\u043e\u0433\u0438\u043d", "verify_ssl": "AdGuard Home \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0438 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044f AdGuard Home.", - "title": "AdGuard Home" + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0438 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044f AdGuard Home." } } } diff --git a/homeassistant/components/adguard/translations/sl.json b/homeassistant/components/adguard/translations/sl.json index 7cad7c1ac3a..45f0f2a2d0d 100644 --- a/homeassistant/components/adguard/translations/sl.json +++ b/homeassistant/components/adguard/translations/sl.json @@ -23,8 +23,7 @@ "username": "Uporabni\u0161ko ime", "verify_ssl": "AdGuard Home uporablja ustrezen certifikat" }, - "description": "Nastavite primerek AdGuard Home, da omogo\u010dite spremljanje in nadzor.", - "title": "Pove\u017eite svoj AdGuard Home." + "description": "Nastavite primerek AdGuard Home, da omogo\u010dite spremljanje in nadzor." } } } diff --git a/homeassistant/components/adguard/translations/sv.json b/homeassistant/components/adguard/translations/sv.json index 2a7e4d7a40d..ce501bf459c 100644 --- a/homeassistant/components/adguard/translations/sv.json +++ b/homeassistant/components/adguard/translations/sv.json @@ -21,8 +21,7 @@ "username": "Anv\u00e4ndarnamn", "verify_ssl": "AdGuard Home anv\u00e4nder ett korrekt certifikat" }, - "description": "St\u00e4ll in din AdGuard Home-instans f\u00f6r att till\u00e5ta \u00f6vervakning och kontroll.", - "title": "L\u00e4nka din AdGuard Home." + "description": "St\u00e4ll in din AdGuard Home-instans f\u00f6r att till\u00e5ta \u00f6vervakning och kontroll." } } } diff --git a/homeassistant/components/adguard/translations/zh-Hant.json b/homeassistant/components/adguard/translations/zh-Hant.json index f3473fd3197..3bbe86352b8 100644 --- a/homeassistant/components/adguard/translations/zh-Hant.json +++ b/homeassistant/components/adguard/translations/zh-Hant.json @@ -23,8 +23,7 @@ "username": "\u4f7f\u7528\u8005\u540d\u7a31", "verify_ssl": "AdGuard Home \u4f7f\u7528\u5c0d\u61c9\u8a8d\u8b49" }, - "description": "\u8a2d\u5b9a AdGuard Home \u4ee5\u9032\u884c\u76e3\u63a7\u3002", - "title": "\u9023\u7d50 AdGuard Home\u3002" + "description": "\u8a2d\u5b9a AdGuard Home \u4ee5\u9032\u884c\u76e3\u63a7\u3002" } } } diff --git a/homeassistant/components/agent_dvr/translations/es-419.json b/homeassistant/components/agent_dvr/translations/es-419.json new file mode 100644 index 00000000000..4b3e1211c31 --- /dev/null +++ b/homeassistant/components/agent_dvr/translations/es-419.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "already_in_progress": "El flujo de configuraci\u00f3n para el dispositivo ya est\u00e1 en progreso.", + "device_unavailable": "El dispositivo no est\u00e1 disponible" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Puerto" + }, + "title": "Configurar Agent DVR" + } + } + }, + "title": "Agent DVR" +} \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/translations/fr.json b/homeassistant/components/agent_dvr/translations/fr.json index dc9a372a0c0..89f70da0af1 100644 --- a/homeassistant/components/agent_dvr/translations/fr.json +++ b/homeassistant/components/agent_dvr/translations/fr.json @@ -12,7 +12,8 @@ "data": { "host": "H\u00f4te", "port": "Port" - } + }, + "title": "Configurer l'agent DVR" } } }, diff --git a/homeassistant/components/wwlln/translations/fi.json b/homeassistant/components/agent_dvr/translations/hu.json similarity index 58% rename from homeassistant/components/wwlln/translations/fi.json rename to homeassistant/components/agent_dvr/translations/hu.json index 1b1b454585f..45918735010 100644 --- a/homeassistant/components/wwlln/translations/fi.json +++ b/homeassistant/components/agent_dvr/translations/hu.json @@ -3,8 +3,8 @@ "step": { "user": { "data": { - "latitude": "Leveysaste", - "longitude": "Pituusaste" + "host": "Hoszt", + "port": "Port" } } } diff --git a/homeassistant/components/agent_dvr/translations/pl.json b/homeassistant/components/agent_dvr/translations/pl.json index 6f36cf2d7d4..5045015087f 100644 --- a/homeassistant/components/agent_dvr/translations/pl.json +++ b/homeassistant/components/agent_dvr/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." }, "error": { "already_in_progress": "Konfiguracja urz\u0105dzenia jest ju\u017c w toku.", @@ -10,8 +10,8 @@ "step": { "user": { "data": { - "host": "[%key_id:common::config_flow::data::host%]", - "port": "[%key_id:common::config_flow::data::port%]" + "host": "Nazwa hosta lub adres IP", + "port": "Port" }, "title": "Konfiguracja Agent DVR" } diff --git a/homeassistant/components/agent_dvr/translations/pt-BR.json b/homeassistant/components/agent_dvr/translations/pt-BR.json new file mode 100644 index 00000000000..df74434ffc7 --- /dev/null +++ b/homeassistant/components/agent_dvr/translations/pt-BR.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "device_unavailable": "O dispositivo n\u00e3o est\u00e1 dispon\u00edvel" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/translations/ca.json b/homeassistant/components/airly/translations/ca.json index 3caf870ccdf..9d085481120 100644 --- a/homeassistant/components/airly/translations/ca.json +++ b/homeassistant/components/airly/translations/ca.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "api_key": "Clau API d'Airly", + "api_key": "Clau API", "latitude": "Latitud", "longitude": "Longitud", "name": "Nom de la integraci\u00f3" diff --git a/homeassistant/components/airly/translations/hu.json b/homeassistant/components/airly/translations/hu.json index f91b2de241f..b6b6790c1e0 100644 --- a/homeassistant/components/airly/translations/hu.json +++ b/homeassistant/components/airly/translations/hu.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "api_key": "Airly API kulcs", + "api_key": "API kulcs", "latitude": "Sz\u00e9less\u00e9g", "longitude": "Hossz\u00fas\u00e1g", "name": "Az integr\u00e1ci\u00f3 neve" diff --git a/homeassistant/components/airly/translations/it.json b/homeassistant/components/airly/translations/it.json index c42af14f030..e394b7af8d3 100644 --- a/homeassistant/components/airly/translations/it.json +++ b/homeassistant/components/airly/translations/it.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "api_key": "Chiave API Airly", + "api_key": "Chiave API", "latitude": "Latitudine", "longitude": "Logitudine", "name": "Nome dell'integrazione" diff --git a/homeassistant/components/airly/translations/pl.json b/homeassistant/components/airly/translations/pl.json index 1b4b56c7656..340b8cb4622 100644 --- a/homeassistant/components/airly/translations/pl.json +++ b/homeassistant/components/airly/translations/pl.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "api_key": "[%key_id:common::config_flow::data::api_key%] Airly", + "api_key": "Klucz API", "latitude": "Szeroko\u015b\u0107 geograficzna", "longitude": "D\u0142ugo\u015b\u0107 geograficzna", "name": "Nazwa integracji" diff --git a/homeassistant/components/airvisual/translations/ca.json b/homeassistant/components/airvisual/translations/ca.json index e285e68e186..045be812dc2 100644 --- a/homeassistant/components/airvisual/translations/ca.json +++ b/homeassistant/components/airvisual/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Aquesta clau API ja est\u00e0 sent utilitzada." + "already_configured": "Aquestes coordenades o Node/Pro ID ja estan registrades." }, "error": { "general_error": "S'ha produ\u00eft un error desconegut.", @@ -21,21 +21,18 @@ "node_pro": { "data": { "ip_address": "Adre\u00e7a IP o amfitri\u00f3 de la unitat", - "password": "Contrasenya de la unitat" + "password": "Contrasenya" }, "description": "Monitoritza una unitat personal d'AirVisual. Pots obtenir la contrasenya des de la interf\u00edcie d'usuari (UI) de la unitat.", "title": "Configuraci\u00f3 d'AirVisual Node/Pro" }, "user": { "data": { - "api_key": "Clau API", "cloud_api": "Ubicaci\u00f3 geogr\u00e0fica", - "latitude": "Latitud", - "longitude": "Longitud", "node_pro": "AirVisual Node Pro", "type": "Tipus d'integraci\u00f3" }, - "description": "Monitoritzaci\u00f3 de la qualitat de l'aire per ubicaci\u00f3 geogr\u00e0fica.", + "description": "Tria quin tipus de dades d'AirVisual vols monitoritzar.", "title": "Configura AirVisual" } } @@ -46,7 +43,6 @@ "data": { "show_on_map": "Mostra al mapa l'\u00e0rea geogr\u00e0fica monitoritzada" }, - "description": "Estableix les diferents opcions de la integraci\u00f3 AirVisual.", "title": "Configuraci\u00f3 d'AirVisual" } } diff --git a/homeassistant/components/airvisual/translations/de.json b/homeassistant/components/airvisual/translations/de.json index 5e66daa3919..e25cfe805ed 100644 --- a/homeassistant/components/airvisual/translations/de.json +++ b/homeassistant/components/airvisual/translations/de.json @@ -27,10 +27,7 @@ }, "user": { "data": { - "api_key": "API-Schl\u00fcssel", "cloud_api": "Geografische Position", - "latitude": "Breitengrad", - "longitude": "L\u00e4ngengrad", "node_pro": "AirVisual Node Pro", "type": "Integrationstyp" }, @@ -45,7 +42,6 @@ "data": { "show_on_map": "Zeigen Sie die \u00fcberwachte Geografie auf der Karte an" }, - "description": "Legen Sie verschiedene Optionen f\u00fcr die AirVisual-Integration fest.", "title": "Konfigurieren Sie AirVisual" } } diff --git a/homeassistant/components/airvisual/translations/en.json b/homeassistant/components/airvisual/translations/en.json index 6298193f59b..a85c2ba75ce 100644 --- a/homeassistant/components/airvisual/translations/en.json +++ b/homeassistant/components/airvisual/translations/en.json @@ -28,10 +28,7 @@ }, "user": { "data": { - "api_key": "API Key", "cloud_api": "Geographical Location", - "latitude": "Latitude", - "longitude": "Longitude", "node_pro": "AirVisual Node Pro", "type": "Integration Type" }, @@ -46,7 +43,6 @@ "data": { "show_on_map": "Show monitored geography on the map" }, - "description": "Set various options for the AirVisual integration.", "title": "Configure AirVisual" } } diff --git a/homeassistant/components/airvisual/translations/es-419.json b/homeassistant/components/airvisual/translations/es-419.json index 77c7ed8dd71..e552deb2242 100644 --- a/homeassistant/components/airvisual/translations/es-419.json +++ b/homeassistant/components/airvisual/translations/es-419.json @@ -5,11 +5,13 @@ }, "error": { "general_error": "Se ha producido un error desconocido.", + "invalid_api_key": "Se proporciona una clave de API no v\u00e1lida.", "unable_to_connect": "No se puede conectar a la unidad Node/Pro." }, "step": { "geography": { "data": { + "api_key": "Clave API", "latitude": "Latitud", "longitude": "Longitud" }, @@ -26,10 +28,7 @@ }, "user": { "data": { - "api_key": "Clave API", "cloud_api": "Localizaci\u00f3n geogr\u00e1fica", - "latitude": "Latitud", - "longitude": "Longitud", "node_pro": "AirVisual Node Pro", "type": "Tipo de integraci\u00f3n" }, @@ -44,7 +43,6 @@ "data": { "show_on_map": "Mostrar geograf\u00eda monitoreada en el mapa" }, - "description": "Establezca varias opciones para la integraci\u00f3n de AirVisual.", "title": "Configurar AirVisual" } } diff --git a/homeassistant/components/airvisual/translations/es.json b/homeassistant/components/airvisual/translations/es.json index 0bd06eaf7bd..dbb44ed4abe 100644 --- a/homeassistant/components/airvisual/translations/es.json +++ b/homeassistant/components/airvisual/translations/es.json @@ -21,21 +21,18 @@ "node_pro": { "data": { "ip_address": "Direcci\u00f3n IP/Nombre de host de la Unidad", - "password": "Contrase\u00f1a de la Unidad" + "password": "Contrase\u00f1a" }, "description": "Monitorizar una unidad personal AirVisual. La contrase\u00f1a puede ser recuperada desde la interfaz de la unidad.", "title": "Configurar un AirVisual Node/Pro" }, "user": { "data": { - "api_key": "Clave API", "cloud_api": "Ubicaci\u00f3n Geogr\u00e1fica", - "latitude": "Latitud", - "longitude": "Longitud", "node_pro": "AirVisual Node Pro", "type": "Tipo de Integraci\u00f3n" }, - "description": "Elige qu\u00e9 tipo de datos de AirVisual quieres monitorear.", + "description": "Elige qu\u00e9 tipo de datos de AirVisual quieres monitorizar.", "title": "Configurar AirVisual" } } @@ -46,7 +43,6 @@ "data": { "show_on_map": "Mostrar geograf\u00eda monitorizada en el mapa" }, - "description": "Ajustar varias opciones para la integraci\u00f3n de AirVisual.", "title": "Configurar AirVisual" } } diff --git a/homeassistant/components/airvisual/translations/fr.json b/homeassistant/components/airvisual/translations/fr.json index 73efe479b2c..8d9eef019e0 100644 --- a/homeassistant/components/airvisual/translations/fr.json +++ b/homeassistant/components/airvisual/translations/fr.json @@ -28,10 +28,7 @@ }, "user": { "data": { - "api_key": "Cl\u00e9 API", "cloud_api": "Localisation g\u00e9ographique", - "latitude": "Latitude", - "longitude": "Longitude", "node_pro": "AirVisual Node Pro", "type": "Type d'int\u00e9gration" }, @@ -46,7 +43,6 @@ "data": { "show_on_map": "Afficher la g\u00e9ographie surveill\u00e9e sur la carte" }, - "description": "D\u00e9finissez diverses options pour l'int\u00e9gration d'AirVisual.", "title": "Configurer AirVisual" } } diff --git a/homeassistant/components/airvisual/translations/hu.json b/homeassistant/components/airvisual/translations/hu.json index c8a33625953..655060337e1 100644 --- a/homeassistant/components/airvisual/translations/hu.json +++ b/homeassistant/components/airvisual/translations/hu.json @@ -6,10 +6,15 @@ "step": { "geography": { "data": { - "api_key": "API Kulcs", + "api_key": "API kulcs", "latitude": "Sz\u00e9less\u00e9g", "longitude": "Hossz\u00fas\u00e1g" } + }, + "node_pro": { + "data": { + "password": "Jelsz\u00f3" + } } } } diff --git a/homeassistant/components/airvisual/translations/it.json b/homeassistant/components/airvisual/translations/it.json index 9831011c7b2..c22481918a0 100644 --- a/homeassistant/components/airvisual/translations/it.json +++ b/homeassistant/components/airvisual/translations/it.json @@ -21,17 +21,14 @@ "node_pro": { "data": { "ip_address": "Indirizzo IP/Nome host dell'unit\u00e0", - "password": "Password dell'unit\u00e0" + "password": "Password" }, "description": "Monitorare un'unit\u00e0 AirVisual personale. La password pu\u00f2 essere recuperata dall'interfaccia utente dell'unit\u00e0.", "title": "Configurare un AirVisual Node/Pro" }, "user": { "data": { - "api_key": "Chiave API", "cloud_api": "Posizione geografica", - "latitude": "Latitudine", - "longitude": "Logitudine", "node_pro": "AirVisual Node Pro", "type": "Tipo di integrazione" }, @@ -46,7 +43,6 @@ "data": { "show_on_map": "Mostra l'area geografica monitorata sulla mappa" }, - "description": "Impostare varie opzioni per l'integrazione AirVisual.", "title": "Configurare AirVisual" } } diff --git a/homeassistant/components/airvisual/translations/ko.json b/homeassistant/components/airvisual/translations/ko.json index e60e0a7ef1c..19cd136cc0f 100644 --- a/homeassistant/components/airvisual/translations/ko.json +++ b/homeassistant/components/airvisual/translations/ko.json @@ -28,10 +28,7 @@ }, "user": { "data": { - "api_key": "API \ud0a4", "cloud_api": "\uc9c0\ub9ac\uc801 \uc704\uce58", - "latitude": "\uc704\ub3c4", - "longitude": "\uacbd\ub3c4", "node_pro": "AirVisual Node Pro", "type": "\uc5f0\ub3d9 \uc720\ud615" }, @@ -46,7 +43,6 @@ "data": { "show_on_map": "\uc9c0\ub3c4\uc5d0 \ubaa8\ub2c8\ud130\ub9c1\ub41c \uc9c0\ub9ac \uc815\ubcf4 \ud45c\uc2dc" }, - "description": "AirVisual \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc5d0 \ub300\ud55c \ub2e4\uc591\ud55c \uc635\uc158\uc744 \uc124\uc815\ud574\uc8fc\uc138\uc694.", "title": "AirVisual \uad6c\uc131\ud558\uae30" } } diff --git a/homeassistant/components/airvisual/translations/lb.json b/homeassistant/components/airvisual/translations/lb.json index 52e55242a05..a9645c6e3b7 100644 --- a/homeassistant/components/airvisual/translations/lb.json +++ b/homeassistant/components/airvisual/translations/lb.json @@ -28,10 +28,7 @@ }, "user": { "data": { - "api_key": "API Schl\u00ebssel", "cloud_api": "Geografesche Standuert", - "latitude": "Breedegrad", - "longitude": "L\u00e4ngegrad", "node_pro": "Airvisual Node Pro", "type": "Typ vun der Integratioun" }, @@ -46,7 +43,6 @@ "data": { "show_on_map": "Iwwerwaachte Geografie op der Kaart uweisen" }, - "description": "Verschidden Optioune fir d'AirVisual Integratioun d\u00e9fin\u00e9ieren.", "title": "Airvisual ariichten" } } diff --git a/homeassistant/components/airvisual/translations/nl.json b/homeassistant/components/airvisual/translations/nl.json index 102b08cb91e..cfccef38e89 100644 --- a/homeassistant/components/airvisual/translations/nl.json +++ b/homeassistant/components/airvisual/translations/nl.json @@ -26,10 +26,7 @@ }, "user": { "data": { - "api_key": "API-sleutel", "cloud_api": "Geografische ligging", - "latitude": "Breedtegraad", - "longitude": "Lengtegraad", "node_pro": "AirVisual Node Pro", "type": "Integratietype" }, @@ -44,7 +41,6 @@ "data": { "show_on_map": "Toon gecontroleerde geografie op de kaart" }, - "description": "Stel verschillende opties in voor de AirVisual-integratie.", "title": "Configureer AirVisual" } } diff --git a/homeassistant/components/airvisual/translations/no.json b/homeassistant/components/airvisual/translations/no.json index 888e1f58e35..28cf8c9a5bb 100644 --- a/homeassistant/components/airvisual/translations/no.json +++ b/homeassistant/components/airvisual/translations/no.json @@ -28,10 +28,7 @@ }, "user": { "data": { - "api_key": "API-n\u00f8kkel", "cloud_api": "Geografisk plassering", - "latitude": "Breddegrad", - "longitude": "Lengdegrad", "node_pro": "", "type": "Integrasjonstype" }, @@ -46,7 +43,6 @@ "data": { "show_on_map": "Vis overv\u00e5ket geografi p\u00e5 kartet" }, - "description": "Angi forskjellige alternativer for AirVisual-integrasjonen.", "title": "Konfigurer AirVisual" } } diff --git a/homeassistant/components/airvisual/translations/pl.json b/homeassistant/components/airvisual/translations/pl.json index 7873a18bc7e..dea77a233aa 100644 --- a/homeassistant/components/airvisual/translations/pl.json +++ b/homeassistant/components/airvisual/translations/pl.json @@ -4,14 +4,14 @@ "already_configured": "Ten klucz API jest ju\u017c w u\u017cyciu." }, "error": { - "general_error": "[%key_id:common::config_flow::error::unknown%]", + "general_error": "Nieoczekiwany b\u0142\u0105d.", "invalid_api_key": "Nieprawid\u0142owy klucz API.", "unable_to_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z jednostk\u0105 Node/Pro." }, "step": { "geography": { "data": { - "api_key": "[%key_id:common::config_flow::data::api_key%]", + "api_key": "Klucz API", "latitude": "Szeroko\u015b\u0107 geograficzna", "longitude": "D\u0142ugo\u015b\u0107 geograficzna" }, @@ -20,18 +20,15 @@ }, "node_pro": { "data": { - "ip_address": "[%key_id:common::config_flow::data::host%]", - "password": "[%key_id:common::config_flow::data::password%] jednostki" + "ip_address": "Nazwa hosta lub adres IP", + "password": "Has\u0142o" }, "description": "Monitoruj jednostk\u0119 AirVisual. Has\u0142o mo\u017cna odzyska\u0107 z interfejsu u\u017cytkownika urz\u0105dzenia.", "title": "Konfiguracja AirVisual Node/Pro" }, "user": { "data": { - "api_key": "[%key_id:common::config_flow::data::api_key%]", "cloud_api": "Lokalizacja geograficzna", - "latitude": "Szeroko\u015b\u0107 geograficzna", - "longitude": "D\u0142ugo\u015b\u0107 geograficzna", "node_pro": "AirVisual Node Pro", "type": "Typ integracji" }, @@ -46,7 +43,6 @@ "data": { "show_on_map": "Wy\u015bwietlaj encje na mapie" }, - "description": "Konfiguracja opcji integracji AirVisual.", "title": "Konfiguracja AirVisual" } } diff --git a/homeassistant/components/airvisual/translations/pt-BR.json b/homeassistant/components/airvisual/translations/pt-BR.json new file mode 100644 index 00000000000..035782cb320 --- /dev/null +++ b/homeassistant/components/airvisual/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "general_error": "Ocorreu um erro desconhecido.", + "invalid_api_key": "Chave de API fornecida \u00e9 inv\u00e1lida." + }, + "step": { + "geography": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude" + } + }, + "user": { + "data": { + "type": "Tipo de Integra\u00e7\u00e3o" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/ru.json b/homeassistant/components/airvisual/translations/ru.json index ecc8999fd18..67af449c9b0 100644 --- a/homeassistant/components/airvisual/translations/ru.json +++ b/homeassistant/components/airvisual/translations/ru.json @@ -28,10 +28,7 @@ }, "user": { "data": { - "api_key": "\u041a\u043b\u044e\u0447 API", "cloud_api": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435", - "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", - "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", "node_pro": "AirVisual Node Pro", "type": "\u0422\u0438\u043f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438" }, @@ -46,7 +43,6 @@ "data": { "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0435\u043c\u0443\u044e \u043e\u0431\u043b\u0430\u0441\u0442\u044c \u043d\u0430 \u043a\u0430\u0440\u0442\u0435" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 AirVisual.", "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 AirVisual" } } diff --git a/homeassistant/components/airvisual/translations/sl.json b/homeassistant/components/airvisual/translations/sl.json index 18765467fd9..f9121852d62 100644 --- a/homeassistant/components/airvisual/translations/sl.json +++ b/homeassistant/components/airvisual/translations/sl.json @@ -28,10 +28,7 @@ }, "user": { "data": { - "api_key": "API Klju\u010d", "cloud_api": "Geografska lokacija", - "latitude": "Zemljepisna \u0161irina", - "longitude": "Zemljepisna dol\u017eina", "node_pro": "AirVisual Node Pro", "type": "Vrsta integracije" }, @@ -46,7 +43,6 @@ "data": { "show_on_map": "Prika\u017ei nadzorovano obmo\u010dje na zemljevidu" }, - "description": "Nastavite razli\u010dne mo\u017enosti za integracijo AirVisual.", "title": "Nastavite AirVisual" } } diff --git a/homeassistant/components/airvisual/translations/sv.json b/homeassistant/components/airvisual/translations/sv.json index 4c54fcf002c..4c4e1271d72 100644 --- a/homeassistant/components/airvisual/translations/sv.json +++ b/homeassistant/components/airvisual/translations/sv.json @@ -18,10 +18,7 @@ }, "user": { "data": { - "api_key": "API-nyckel", "cloud_api": "Geografisk Plats", - "latitude": "Latitud", - "longitude": "Longitud", "type": "Integrationstyp" } } diff --git a/homeassistant/components/airvisual/translations/zh-Hant.json b/homeassistant/components/airvisual/translations/zh-Hant.json index 36dd078d7b5..80e8372f3f6 100644 --- a/homeassistant/components/airvisual/translations/zh-Hant.json +++ b/homeassistant/components/airvisual/translations/zh-Hant.json @@ -5,7 +5,7 @@ }, "error": { "general_error": "\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002", - "invalid_api_key": "API \u5bc6\u78bc\u7121\u6548\u3002", + "invalid_api_key": "API \u5bc6\u9470\u7121\u6548\u3002", "unable_to_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Node/Pro \u8a2d\u5099\u3002" }, "step": { @@ -28,10 +28,7 @@ }, "user": { "data": { - "api_key": "API \u5bc6\u9470", "cloud_api": "\u5730\u7406\u5ea7\u6a19", - "latitude": "\u7def\u5ea6", - "longitude": "\u7d93\u5ea6", "node_pro": "AirVisual Node Pro", "type": "\u6574\u5408\u985e\u578b" }, @@ -46,7 +43,6 @@ "data": { "show_on_map": "\u65bc\u5730\u5716\u4e0a\u986f\u793a\u76e3\u63a7\u4f4d\u7f6e\u3002" }, - "description": "\u8a2d\u5b9a AirVisual \u6574\u5408\u9078\u9805\u3002", "title": "\u8a2d\u5b9a AirVisual" } } diff --git a/homeassistant/components/alarm_control_panel/translations/es.json b/homeassistant/components/alarm_control_panel/translations/es.json index fc76102d0fe..4002c26cd29 100644 --- a/homeassistant/components/alarm_control_panel/translations/es.json +++ b/homeassistant/components/alarm_control_panel/translations/es.json @@ -24,13 +24,13 @@ }, "state": { "_": { - "armed": "Armado", + "armed": "Armada", "armed_away": "Armada ausente", "armed_custom_bypass": "Armada personalizada", "armed_home": "Armada en casa", "armed_night": "Armada noche", "arming": "Armando", - "disarmed": "Desarmado", + "disarmed": "Desarmada", "disarming": "Desarmando", "pending": "Pendiente", "triggered": "Disparada" diff --git a/homeassistant/components/alarm_control_panel/translations/pt-BR.json b/homeassistant/components/alarm_control_panel/translations/pt-BR.json index a056e1f4187..07e005cba03 100644 --- a/homeassistant/components/alarm_control_panel/translations/pt-BR.json +++ b/homeassistant/components/alarm_control_panel/translations/pt-BR.json @@ -7,6 +7,13 @@ "disarm": "Desarmar {entity_name}", "trigger": "Disparar {entidade_nome}" }, + "condition_type": { + "is_armed_away": "{entity_name} est\u00e1 armado modo longe", + "is_armed_home": "{entity_name} est\u00e1 armadado modo casa", + "is_armed_night": "{entity_name} est\u00e1 armadado modo noite", + "is_disarmed": "{entity_name} est\u00e1 desarmado", + "is_triggered": "{entity_name} est\u00e1 acionado" + }, "trigger_type": { "armed_away": "{entity_name} armado modo longe", "armed_home": "{entity_name} armadado modo casa", diff --git a/homeassistant/components/alexa/__init__.py b/homeassistant/components/alexa/__init__.py index de5a67087ca..81b0f670058 100644 --- a/homeassistant/components/alexa/__init__.py +++ b/homeassistant/components/alexa/__init__.py @@ -3,15 +3,13 @@ import logging import voluptuous as vol -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_NAME from homeassistant.core import callback from homeassistant.helpers import config_validation as cv, entityfilter from . import flash_briefings, intent, smart_home_http from .const import ( CONF_AUDIO, - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES, CONF_DISPLAY_URL, diff --git a/homeassistant/components/alexa/auth.py b/homeassistant/components/alexa/auth.py index 3b7984f56d3..090481876da 100644 --- a/homeassistant/components/alexa/auth.py +++ b/homeassistant/components/alexa/auth.py @@ -7,7 +7,7 @@ import logging import aiohttp import async_timeout -from homeassistant.const import HTTP_OK +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, HTTP_OK from homeassistant.core import callback from homeassistant.helpers import aiohttp_client from homeassistant.util import dt @@ -48,8 +48,8 @@ class Auth: lwa_params = { "grant_type": "authorization_code", "code": accept_grant_code, - "client_id": self.client_id, - "client_secret": self.client_secret, + CONF_CLIENT_ID: self.client_id, + CONF_CLIENT_SECRET: self.client_secret, } _LOGGER.debug( "Calling LWA to get the access token (first time), with: %s", @@ -80,8 +80,8 @@ class Auth: lwa_params = { "grant_type": "refresh_token", "refresh_token": self._prefs[STORAGE_REFRESH_TOKEN], - "client_id": self.client_id, - "client_secret": self.client_secret, + CONF_CLIENT_ID: self.client_id, + CONF_CLIENT_SECRET: self.client_secret, } _LOGGER.debug("Calling LWA to refresh the access token.") diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 7451d15eb1c..c11e974310c 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1661,7 +1661,21 @@ class AlexaDoorbellEventSource(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-doorbelleventsource.html """ - supported_locales = {"en-US"} + supported_locales = { + "en-US", + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "es-MX", + "fr-CA", + "fr-FR", + "it-IT", + "ja-JP", + } def name(self): """Return the Alexa API name of this interface.""" @@ -1789,6 +1803,13 @@ class AlexaEqualizerController(AlexaCapability): """ supported_locales = {"en-US"} + VALID_SOUND_MODES = { + "MOVIE", + "MUSIC", + "NIGHT", + "SPORT", + "TV", + } def name(self): """Return the Alexa API name of this interface.""" @@ -1807,35 +1828,34 @@ class AlexaEqualizerController(AlexaCapability): raise UnsupportedProperty(name) sound_mode = self.entity.attributes.get(media_player.ATTR_SOUND_MODE) - if sound_mode and sound_mode.upper() in ( - "MOVIE", - "MUSIC", - "NIGHT", - "SPORT", - "TV", - ): + if sound_mode and sound_mode.upper() in self.VALID_SOUND_MODES: return sound_mode.upper() return None def configurations(self): - """Return the sound modes supported in the configurations object. - - Valid Values for modes are: MOVIE, MUSIC, NIGHT, SPORT, TV. - """ + """Return the sound modes supported in the configurations object.""" configurations = None - sound_mode_list = self.entity.attributes.get(media_player.ATTR_SOUND_MODE_LIST) - if sound_mode_list: - supported_sound_modes = [ - {"name": sound_mode.upper()} - for sound_mode in sound_mode_list - if sound_mode.upper() in ("MOVIE", "MUSIC", "NIGHT", "SPORT", "TV") - ] - + supported_sound_modes = self.get_valid_inputs( + self.entity.attributes.get(media_player.ATTR_SOUND_MODE_LIST, []) + ) + if supported_sound_modes: configurations = {"modes": {"supported": supported_sound_modes}} return configurations + @classmethod + def get_valid_inputs(cls, sound_mode_list): + """Return list of supported inputs.""" + input_list = [] + for sound_mode in sound_mode_list: + sound_mode = sound_mode.upper() + + if sound_mode in cls.VALID_SOUND_MODES: + input_list.append({"name": sound_mode}) + + return input_list + class AlexaTimeHoldController(AlexaCapability): """Implements Alexa.TimeHoldController. diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index ca1c6236fe6..50e3edb475c 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -18,8 +18,6 @@ CONF_DISPLAY_URL = "display_url" CONF_FILTER = "filter" CONF_ENTITY_CONFIG = "entity_config" CONF_ENDPOINT = "endpoint" -CONF_CLIENT_ID = "client_id" -CONF_CLIENT_SECRET = "client_secret" CONF_LOCALE = "locale" ATTR_UID = "uid" @@ -126,6 +124,8 @@ class Inputs: """ VALID_SOURCE_NAME_MAP = { + "antenna": "TUNER", + "antennatv": "TUNER", "aux": "AUX 1", "aux1": "AUX 1", "aux2": "AUX 2", @@ -135,6 +135,7 @@ class Inputs: "aux6": "AUX 6", "aux7": "AUX 7", "bluray": "BLURAY", + "blurayplayer": "BLURAY", "cable": "CABLE", "cd": "CD", "coax": "COAX 1", @@ -186,6 +187,7 @@ class Inputs: "playstation": "PLAYSTATION", "playstation3": "PLAYSTATION 3", "playstation4": "PLAYSTATION 4", + "rokumediaplayer": "MEDIA PLAYER", "satellite": "SATELLITE", "satellitetv": "SATELLITE", "smartcast": "SMARTCAST", diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index e74722b89e8..972df08bd1e 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -547,7 +547,11 @@ class MediaPlayerCapabilities(AlexaEntity): yield AlexaChannelController(self.entity) if supported & media_player.const.SUPPORT_SELECT_SOUND_MODE: - yield AlexaEqualizerController(self.entity) + inputs = AlexaInputController.get_valid_inputs( + self.entity.attributes.get(media_player.const.ATTR_SOUND_MODE_LIST, []) + ) + if len(inputs) > 0: + yield AlexaEqualizerController(self.entity) yield AlexaEndpointHealth(self.hass, self.entity) yield Alexa(self.hass) diff --git a/homeassistant/components/alexa/intent.py b/homeassistant/components/alexa/intent.py index f879b66268b..c04b493beec 100644 --- a/homeassistant/components/alexa/intent.py +++ b/homeassistant/components/alexa/intent.py @@ -40,6 +40,16 @@ def async_setup(hass): hass.http.register_view(AlexaIntentsView) +async def async_setup_intents(hass): + """ + Do intents setup. + + Right now this module does not expose any, but the intent component breaks + without it. + """ + pass # pylint: disable=unnecessary-pass + + class UnknownRequest(HomeAssistantError): """When an unknown Alexa request is passed in.""" diff --git a/homeassistant/components/alexa/smart_home_http.py b/homeassistant/components/alexa/smart_home_http.py index 7c745f8afdd..41ebfb340eb 100644 --- a/homeassistant/components/alexa/smart_home_http.py +++ b/homeassistant/components/alexa/smart_home_http.py @@ -3,17 +3,11 @@ import logging from homeassistant import core from homeassistant.components.http.view import HomeAssistantView +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from .auth import Auth from .config import AbstractConfig -from .const import ( - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, - CONF_ENDPOINT, - CONF_ENTITY_CONFIG, - CONF_FILTER, - CONF_LOCALE, -) +from .const import CONF_ENDPOINT, CONF_ENTITY_CONFIG, CONF_FILTER, CONF_LOCALE from .smart_home import async_handle_message from .state_report import async_enable_proactive_mode diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py index 07daa3e4781..3710ff14b1a 100644 --- a/homeassistant/components/almond/__init__.py +++ b/homeassistant/components/almond/__init__.py @@ -13,7 +13,13 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components import conversation -from homeassistant.const import CONF_HOST, CONF_TYPE, EVENT_HOMEASSISTANT_START +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_HOST, + CONF_TYPE, + EVENT_HOMEASSISTANT_START, +) from homeassistant.core import Context, CoreState, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import ( @@ -29,9 +35,6 @@ from homeassistant.helpers import ( from . import config_flow from .const import DOMAIN, TYPE_LOCAL, TYPE_OAUTH2 -CONF_CLIENT_ID = "client_id" -CONF_CLIENT_SECRET = "client_secret" - STORAGE_VERSION = 1 STORAGE_KEY = DOMAIN diff --git a/homeassistant/components/almond/translations/pl.json b/homeassistant/components/almond/translations/pl.json index ed60372d5b3..b9fc05c1b42 100644 --- a/homeassistant/components/almond/translations/pl.json +++ b/homeassistant/components/almond/translations/pl.json @@ -7,11 +7,11 @@ }, "step": { "hassio_confirm": { - "description": "Czy chcesz skonfigurowa\u0107 Home Assistant'a, aby \u0142\u0105czy\u0142 si\u0119 z Almond dostarczonym przez dodatek Hass.io: {addon}?", + "description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby \u0142\u0105czy\u0142 si\u0119 z Almond dostarczonym przez dodatek Hass.io: {addon}?", "title": "Almond poprzez dodatek Hass.io" }, "pick_implementation": { - "title": "[%key_id:common::config_flow::title::oauth2_pick_implementation%]" + "title": "Wybierz metod\u0119 uwierzytelniania" } } } diff --git a/homeassistant/components/ambiclimate/__init__.py b/homeassistant/components/ambiclimate/__init__.py index e15f6dea2ec..490c41255bf 100644 --- a/homeassistant/components/ambiclimate/__init__.py +++ b/homeassistant/components/ambiclimate/__init__.py @@ -3,10 +3,11 @@ import logging import voluptuous as vol +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_validation as cv from . import config_flow -from .const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, DOMAIN +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ambiclimate/climate.py b/homeassistant/components/ambiclimate/climate.py index cb19d1329ca..93b38974464 100644 --- a/homeassistant/components/ambiclimate/climate.py +++ b/homeassistant/components/ambiclimate/climate.py @@ -11,14 +11,18 @@ from homeassistant.components.climate.const import ( HVAC_MODE_OFF, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.const import ATTR_NAME, ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.const import ( + ATTR_NAME, + ATTR_TEMPERATURE, + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + TEMP_CELSIUS, +) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( ATTR_VALUE, - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, DOMAIN, SERVICE_COMFORT_FEEDBACK, SERVICE_COMFORT_MODE, diff --git a/homeassistant/components/ambiclimate/config_flow.py b/homeassistant/components/ambiclimate/config_flow.py index 99d4aa3c944..2b88e7ab91e 100644 --- a/homeassistant/components/ambiclimate/config_flow.py +++ b/homeassistant/components/ambiclimate/config_flow.py @@ -5,6 +5,7 @@ import ambiclimate from homeassistant import config_entries from homeassistant.components.http import HomeAssistantView +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.network import get_url @@ -12,8 +13,6 @@ from homeassistant.helpers.network import get_url from .const import ( AUTH_CALLBACK_NAME, AUTH_CALLBACK_PATH, - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, DOMAIN, STORAGE_KEY, STORAGE_VERSION, diff --git a/homeassistant/components/ambiclimate/const.py b/homeassistant/components/ambiclimate/const.py index 833fef303f5..6393e97569a 100644 --- a/homeassistant/components/ambiclimate/const.py +++ b/homeassistant/components/ambiclimate/const.py @@ -1,12 +1,13 @@ """Constants used by the Ambiclimate component.""" -ATTR_VALUE = "value" -CONF_CLIENT_ID = "client_id" -CONF_CLIENT_SECRET = "client_secret" DOMAIN = "ambiclimate" + +ATTR_VALUE = "value" + SERVICE_COMFORT_FEEDBACK = "send_comfort_feedback" SERVICE_COMFORT_MODE = "set_comfort_mode" SERVICE_TEMPERATURE_MODE = "set_temperature_mode" + STORAGE_KEY = "ambiclimate_auth" STORAGE_VERSION = 1 diff --git a/homeassistant/components/ambiclimate/translations/bg.json b/homeassistant/components/ambiclimate/translations/bg.json index e76a714d5b0..0472cfd33f1 100644 --- a/homeassistant/components/ambiclimate/translations/bg.json +++ b/homeassistant/components/ambiclimate/translations/bg.json @@ -14,7 +14,7 @@ }, "step": { "auth": { - "description": "\u041c\u043e\u043b\u044f, \u043f\u043e\u0441\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0442\u043e\u0437\u0438 [link]({authorization_url}) \u0438 \u0420\u0430\u0437\u0440\u0435\u0448\u0435\u0442\u0435 \u0434\u043e\u0441\u0442\u044a\u043f\u0430 \u0434\u043e \u043f\u0440\u043e\u0444\u0438\u043b\u0430 \u0441\u0438 \u0432 Ambiclimate, \u0441\u043b\u0435\u0434 \u0442\u043e\u0432\u0430 \u0441\u0435 \u0432\u044a\u0440\u043d\u0435\u0442\u0435 \u0438 \u043d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0418\u0437\u043f\u0440\u0430\u0449\u0430\u043d\u0435 \u043f\u043e-\u0434\u043e\u043b\u0443. \n (\u0423\u0432\u0435\u0440\u0435\u0442\u0435 \u0441\u0435, \u0447\u0435 \u043f\u043e\u0441\u043e\u0447\u0435\u043d\u0438\u044f\u0442 url \u0437\u0430 \u043e\u0431\u0440\u0430\u0442\u043d\u0430 \u043f\u043e\u0432\u0438\u043a\u0432\u0430\u043d\u0435 \u0435 {cb_url})", + "description": "\u041c\u043e\u043b\u044f, \u043f\u043e\u0441\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0442\u043e\u0437\u0438 [link]({authorization_url}) \u0438 **\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u0442\u0435** \u0434\u043e\u0441\u0442\u044a\u043f\u0430 \u0434\u043e \u043f\u0440\u043e\u0444\u0438\u043b\u0430 \u0441\u0438 \u0432 Ambiclimate, \u0441\u043b\u0435\u0434 \u0442\u043e\u0432\u0430 \u0441\u0435 \u0432\u044a\u0440\u043d\u0435\u0442\u0435 \u0438 \u043d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 **\u0418\u0437\u043f\u0440\u0430\u0449\u0430\u043d\u0435** \u043f\u043e-\u0434\u043e\u043b\u0443. \n (\u0423\u0432\u0435\u0440\u0435\u0442\u0435 \u0441\u0435, \u0447\u0435 \u043f\u043e\u0441\u043e\u0447\u0435\u043d\u0438\u044f\u0442 url \u0437\u0430 \u043e\u0431\u0440\u0430\u0442\u043d\u0430 \u043f\u043e\u0432\u0438\u043a\u0432\u0430\u043d\u0435 \u0435 {cb_url})", "title": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0438\u0440\u0430\u043d\u0435 \u0441 Ambiclimate" } } diff --git a/homeassistant/components/ambiclimate/translations/ca.json b/homeassistant/components/ambiclimate/translations/ca.json index 0b8ca963813..64ea45410d3 100644 --- a/homeassistant/components/ambiclimate/translations/ca.json +++ b/homeassistant/components/ambiclimate/translations/ca.json @@ -14,7 +14,7 @@ }, "step": { "auth": { - "description": "V\u00e9s a l'[enlla\u00e7]({authorization_url}) i Permet l'acc\u00e9s al teu compte de Ambiclimate, despr\u00e9s torna i prem Envia (a sota).\n(Assegura't que l'enlla\u00e7 de retorn \u00e9s el seg\u00fcent {cb_url})", + "description": "V\u00e9s a l'[enlla\u00e7]({authorization_url}) i **Permet** l'acc\u00e9s al teu compte de Ambiclimate, despr\u00e9s torna i prem **Envia** (a sota).\n(Assegura't que l'enlla\u00e7 de retorn \u00e9s el seg\u00fcent {cb_url})", "title": "Autenticaci\u00f3 amb Ambi Climate" } } diff --git a/homeassistant/components/ambiclimate/translations/cs.json b/homeassistant/components/ambiclimate/translations/cs.json index da0430346a7..2f2369429fe 100644 --- a/homeassistant/components/ambiclimate/translations/cs.json +++ b/homeassistant/components/ambiclimate/translations/cs.json @@ -6,7 +6,7 @@ }, "step": { "auth": { - "description": "N\u00e1sledujte tento [odkaz]({authorization_url}) a Povolit p\u0159\u00edstup k va\u0161emu \u00fa\u010dtu Ambiclimate, pot\u00e9 se vra\u0165te a stiskn\u011bte Odeslat n\u00ed\u017ee. \n (Ujist\u011bte se, \u017ee zadan\u00e1 adresa URL zp\u011btn\u00e9ho vol\u00e1n\u00ed je {cb_url} )", + "description": "N\u00e1sledujte tento [odkaz]({authorization_url}) a **Povolit** p\u0159\u00edstup k va\u0161emu \u00fa\u010dtu Ambiclimate, pot\u00e9 se vra\u0165te a stiskn\u011bte **Odeslat** n\u00ed\u017ee. \n (Ujist\u011bte se, \u017ee zadan\u00e1 adresa URL zp\u011btn\u00e9ho vol\u00e1n\u00ed je {cb_url} )", "title": "Ov\u011b\u0159it Ambiclimate" } } diff --git a/homeassistant/components/ambiclimate/translations/de.json b/homeassistant/components/ambiclimate/translations/de.json index 6fba5772a10..43618a8ab99 100644 --- a/homeassistant/components/ambiclimate/translations/de.json +++ b/homeassistant/components/ambiclimate/translations/de.json @@ -14,7 +14,7 @@ }, "step": { "auth": { - "description": "Bitte folge diesem [link] ({authorization_url}) und Erlaube Zugriff auf dein Ambiclimate-Konto, komme dann zur\u00fcck und dr\u00fccke Senden darunter.\n (Pr\u00fcfe, dass die Callback-URL {cb_url} ist.)", + "description": "Bitte folge diesem [link] ({authorization_url}) und **Erlaube** Zugriff auf dein Ambiclimate-Konto, komme dann zur\u00fcck und dr\u00fccke **Senden** darunter.\n (Pr\u00fcfe, dass die Callback-URL {cb_url} ist.)", "title": "Ambiclimate authentifizieren" } } diff --git a/homeassistant/components/ambiclimate/translations/en.json b/homeassistant/components/ambiclimate/translations/en.json index 509b801fa62..177ecd2907f 100644 --- a/homeassistant/components/ambiclimate/translations/en.json +++ b/homeassistant/components/ambiclimate/translations/en.json @@ -14,7 +14,7 @@ }, "step": { "auth": { - "description": "Please follow this [link]({authorization_url}) and Allow access to your Ambiclimate account, then come back and press Submit below.\n(Make sure the specified callback url is {cb_url})", + "description": "Please follow this [link]({authorization_url}) and **Allow** access to your Ambiclimate account, then come back and press **Submit** below.\n(Make sure the specified callback url is {cb_url})", "title": "Authenticate Ambiclimate" } } diff --git a/homeassistant/components/ambiclimate/translations/fr.json b/homeassistant/components/ambiclimate/translations/fr.json index c16b0c10266..9c5864fcb0f 100644 --- a/homeassistant/components/ambiclimate/translations/fr.json +++ b/homeassistant/components/ambiclimate/translations/fr.json @@ -14,7 +14,7 @@ }, "step": { "auth": { - "description": "Suivez ce [lien] ( {authorization_url} ) et Autorisez l'acc\u00e8s \u00e0 votre compte Ambiclimate, puis revenez et appuyez sur Envoyer ci-dessous. \n (Assurez-vous que l'URL de rappel sp\u00e9cifi\u00e9 est {cb_url} )", + "description": "Suivez ce [lien]({authorization_url}) et **Autorisez** l'acc\u00e8s \u00e0 votre compte Ambiclimate, puis revenez et appuyez sur **Envoyer** ci-dessous. \n (Assurez-vous que l'URL de rappel sp\u00e9cifi\u00e9 est {cb_url})", "title": "Authentifier Ambiclimate" } } diff --git a/homeassistant/components/ambiclimate/translations/it.json b/homeassistant/components/ambiclimate/translations/it.json index 2da7a0ee4c8..427aa0ab445 100644 --- a/homeassistant/components/ambiclimate/translations/it.json +++ b/homeassistant/components/ambiclimate/translations/it.json @@ -3,7 +3,7 @@ "abort": { "access_token": "Errore sconosciuto durante la generazione di un token di accesso.", "already_setup": "L'account Ambiclimate \u00e8 configurato.", - "no_config": "\u00c8 necessario configurare Ambiclimate prima di poter eseguire l'autenticazione con esso. [Leggere le istruzioni] (https://www.home-assistant.io/components/ambiclimate/)." + "no_config": "\u00c8 necessario configurare Ambiclimate prima di poter eseguire l'autenticazione con esso. [Leggere le istruzioni](https://www.home-assistant.io/components/ambiclimate/)." }, "create_entry": { "default": "Autenticato con successo con Ambiclimate" @@ -14,7 +14,7 @@ }, "step": { "auth": { - "description": "Segui questo [link]({authorization_url}) e Consenti accesso al tuo account Ambiclimate, quindi torna indietro e premi Invia qui sotto. \n (Assicurati che l'URL di richiamata specificato sia {cb_url})", + "description": "Segui questo [link]({authorization_url}) e **Consenti** l'accesso al tuo account Ambiclimate, quindi torna indietro e premi **Invia** qui sotto. \n(Assicurati che l'URL di richiamata specificato sia {cb_url})", "title": "Autenticare Ambiclimate" } } diff --git a/homeassistant/components/ambiclimate/translations/ko.json b/homeassistant/components/ambiclimate/translations/ko.json index 2717d4c4b79..fd55f75bf28 100644 --- a/homeassistant/components/ambiclimate/translations/ko.json +++ b/homeassistant/components/ambiclimate/translations/ko.json @@ -9,12 +9,12 @@ "default": "Ambi Climate \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, "error": { - "follow_link": "Submit \ubc84\ud2bc\uc744 \ub204\ub974\uae30 \uc804\uc5d0 \ub9c1\ud06c\ub97c \ub530\ub77c \uc778\uc99d\uc744 \ubc1b\uc544\uc8fc\uc138\uc694", + "follow_link": "\ud655\uc778\uc744 \ud074\ub9ad\ud558\uae30 \uc804\uc5d0 \ub9c1\ud06c\ub97c \ub530\ub77c \uc778\uc99d\uc744 \ubc1b\uc544\uc8fc\uc138\uc694", "no_token": "Ambi Climate \ub85c \uc778\uc99d\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4" }, "step": { "auth": { - "description": "[\ub9c1\ud06c]({authorization_url}) \ub97c \ud074\ub9ad\ud558\uc5ec Ambi Climate \uacc4\uc815\uc5d0 \ub300\ud574 \ud5c8\uc6a9 \ud55c \ub2e4\uc74c, \ub2e4\uc2dc \ub3cc\uc544\uc640\uc11c \ud558\ub2e8\uc758 Submit \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694. \n(\ucf5c\ubc31 url \uc744 {cb_url} \ub85c \uad6c\uc131\ud588\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694)", + "description": "[\ub9c1\ud06c]({authorization_url}) \ub97c \ud074\ub9ad\ud558\uc5ec Ambi Climate \uacc4\uc815\uc5d0 \ub300\ud574 **\ud5c8\uc6a9**\ud55c \ub2e4\uc74c, \ub2e4\uc2dc \ub3cc\uc544\uc640\uc11c \ud558\ub2e8\uc758 **\ud655\uc778**\uc744 \ud074\ub9ad\ud574\uc8fc\uc138\uc694. \n(\ucf5c\ubc31 url \uc744 {cb_url} \ub85c \uad6c\uc131\ud588\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694)", "title": "Ambi Climate \uc778\uc99d\ud558\uae30" } } diff --git a/homeassistant/components/ambiclimate/translations/nl.json b/homeassistant/components/ambiclimate/translations/nl.json index 17e6dfa9c82..e65688af89d 100644 --- a/homeassistant/components/ambiclimate/translations/nl.json +++ b/homeassistant/components/ambiclimate/translations/nl.json @@ -14,7 +14,7 @@ }, "step": { "auth": { - "description": "Volg deze [link] ( {authorization_url} ) en Toestaan toegang tot uw Ambiclimate-account, kom dan terug en druk hieronder op Verzenden . \n (Zorg ervoor dat de opgegeven callback-URL {cb_url} )", + "description": "Volg deze [link]({authorization_url}) en klik op **Toestaan** om toegang te geven tot uw Ambiclimate-account, kom dan terug en druk hieronder op **Verzenden**. \n (Zorg ervoor dat de opgegeven callback-URL {cb_url})", "title": "Authenticatie Ambiclimate" } } diff --git a/homeassistant/components/ambiclimate/translations/no.json b/homeassistant/components/ambiclimate/translations/no.json index 13529d2e1af..3cc3f4617e7 100644 --- a/homeassistant/components/ambiclimate/translations/no.json +++ b/homeassistant/components/ambiclimate/translations/no.json @@ -14,7 +14,7 @@ }, "step": { "auth": { - "description": "Vennligst f\u00f8lg denne [linken]({authorization_url}) og Tillat tilgang til din Ambiclimate konto, kom deretter tilbake og trykk Send nedenfor.\n(Kontroller at den angitte URL-adressen for tilbakeringing er {cb_url})", + "description": "Vennligst f\u00f8lg denne [linken]({authorization_url}) og **Tillat** tilgang til din Ambiclimate konto, kom deretter tilbake og trykk **Send** nedenfor.\n(Kontroller at den angitte URL-adressen for tilbakeringing er {cb_url})", "title": "Godkjenn Ambiclimate" } } diff --git a/homeassistant/components/ambiclimate/translations/ru.json b/homeassistant/components/ambiclimate/translations/ru.json index a1eefc78575..5712c99df22 100644 --- a/homeassistant/components/ambiclimate/translations/ru.json +++ b/homeassistant/components/ambiclimate/translations/ru.json @@ -14,7 +14,7 @@ }, "step": { "auth": { - "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e [\u0441\u0441\u044b\u043b\u043a\u0435]({authorization_url}) \u0438 \u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0435 \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Ambi Climate, \u0437\u0430\u0442\u0435\u043c \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c. \n(\u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 URL \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0433\u043e \u0432\u044b\u0437\u043e\u0432\u0430 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 {cb_url})", + "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e [\u0441\u0441\u044b\u043b\u043a\u0435]({authorization_url}) \u0438 **\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0435** \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Ambi Climate, \u0437\u0430\u0442\u0435\u043c \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **\u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c**. \n(\u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 URL \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0433\u043e \u0432\u044b\u0437\u043e\u0432\u0430 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 {cb_url})", "title": "Ambi Climate" } } diff --git a/homeassistant/components/ambiclimate/translations/zh-Hant.json b/homeassistant/components/ambiclimate/translations/zh-Hant.json index 2efd9f13549..7b995d09944 100644 --- a/homeassistant/components/ambiclimate/translations/zh-Hant.json +++ b/homeassistant/components/ambiclimate/translations/zh-Hant.json @@ -14,7 +14,7 @@ }, "step": { "auth": { - "description": "\u8acb\u4f7f\u7528\u6b64[\u9023\u7d50]\uff08{authorization_url}\uff09\u4e26\u9ede\u9078\u5141\u8a31\u4ee5\u5b58\u53d6 Ambiclimate \u5e33\u865f\uff0c\u7136\u5f8c\u8fd4\u56de\u6b64\u9801\u9762\u4e26\u9ede\u9078\u4e0b\u65b9\u7684\u50b3\u9001\u3002\n\uff08\u78ba\u5b9a Callback url \u70ba {cb_url}\uff09", + "description": "\u8acb\u4f7f\u7528\u6b64[\u9023\u7d50]\uff08{authorization_url}\uff09\u4e26\u9ede\u9078 **\u5141\u8a31** \u4ee5\u5b58\u53d6 Ambiclimate \u5e33\u865f\uff0c\u7136\u5f8c\u8fd4\u56de\u6b64\u9801\u9762\u4e26\u9ede\u9078\u4e0b\u65b9\u7684 **\u50b3\u9001**\u3002\n\uff08\u78ba\u5b9a Callback url \u70ba {cb_url}\uff09", "title": "\u8a8d\u8b49 Ambiclimate" } } diff --git a/homeassistant/components/ambient_station/translations/it.json b/homeassistant/components/ambient_station/translations/it.json index 1991d053f6c..ceecb5363c8 100644 --- a/homeassistant/components/ambient_station/translations/it.json +++ b/homeassistant/components/ambient_station/translations/it.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "api_key": "API Key", + "api_key": "Chiave API", "app_key": "Application Key" }, "title": "Inserisci i tuoi dati" diff --git a/homeassistant/components/ambient_station/translations/pl.json b/homeassistant/components/ambient_station/translations/pl.json index 01a0c83bd28..bb597971b0c 100644 --- a/homeassistant/components/ambient_station/translations/pl.json +++ b/homeassistant/components/ambient_station/translations/pl.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "api_key": "[%key_id:common::config_flow::data::api_key%]", + "api_key": "Klucz API", "app_key": "Klucz aplikacji" }, "title": "Wprowad\u017a dane" diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index 44c1c498c28..3a8619a092f 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -7,6 +7,8 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_RESOURCES, + ELECTRICAL_CURRENT_AMPERE, + ELECTRICAL_VOLT_AMPERE, FREQUENCY_HERTZ, POWER_WATT, TEMP_CELSIUS, @@ -67,9 +69,9 @@ SENSOR_TYPES = { "nominv": ["Nominal Input Voltage", VOLT, "mdi:flash"], "nomoutv": ["Nominal Output Voltage", VOLT, "mdi:flash"], "nompower": ["Nominal Output Power", POWER_WATT, "mdi:flash"], - "nomapnt": ["Nominal Apparent Power", "VA", "mdi:flash"], + "nomapnt": ["Nominal Apparent Power", ELECTRICAL_VOLT_AMPERE, "mdi:flash"], "numxfers": ["Transfer Count", "", "mdi:counter"], - "outcurnt": ["Output Current", "A", "mdi:flash"], + "outcurnt": ["Output Current", ELECTRICAL_CURRENT_AMPERE, "mdi:flash"], "outputv": ["Output Voltage", VOLT, "mdi:flash"], "reg1": ["Register 1 Fault", "", "mdi:information-outline"], "reg2": ["Register 2 Fault", "", "mdi:information-outline"], @@ -98,8 +100,8 @@ INFERRED_UNITS = { " Seconds": TIME_SECONDS, " Percent": UNIT_PERCENTAGE, " Volts": VOLT, - " Ampere": "A", - " Volt-Ampere": "VA", + " Ampere": ELECTRICAL_CURRENT_AMPERE, + " Volt-Ampere": ELECTRICAL_VOLT_AMPERE, " Watts": POWER_WATT, " Hz": FREQUENCY_HERTZ, " C": TEMP_CELSIUS, diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index 8d0cd44070c..001ce5d2a4e 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -35,14 +35,18 @@ import homeassistant.core as ha from homeassistant.exceptions import ServiceNotFound, TemplateError, Unauthorized from homeassistant.helpers import template from homeassistant.helpers.json import JSONEncoder +from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.helpers.state import AsyncTrackStates _LOGGER = logging.getLogger(__name__) ATTR_BASE_URL = "base_url" +ATTR_EXTERNAL_URL = "external_url" +ATTR_INTERNAL_URL = "internal_url" ATTR_LOCATION_NAME = "location_name" ATTR_REQUIRES_API_PASSWORD = "requires_api_password" +ATTR_UUID = "uuid" ATTR_VERSION = "version" DOMAIN = "api" @@ -173,19 +177,36 @@ class APIDiscoveryView(HomeAssistantView): url = URL_API_DISCOVERY_INFO name = "api:discovery" - @ha.callback - def get(self, request): + async def get(self, request): """Get discovery information.""" hass = request.app["hass"] - return self.json( - { - ATTR_BASE_URL: hass.config.api.base_url, - ATTR_LOCATION_NAME: hass.config.location_name, - # always needs authentication - ATTR_REQUIRES_API_PASSWORD: True, - ATTR_VERSION: __version__, - } - ) + uuid = await hass.helpers.instance_id.async_get() + + data = { + ATTR_UUID: uuid, + ATTR_BASE_URL: None, + ATTR_EXTERNAL_URL: None, + ATTR_INTERNAL_URL: None, + ATTR_LOCATION_NAME: hass.config.location_name, + # always needs authentication + ATTR_REQUIRES_API_PASSWORD: True, + ATTR_VERSION: __version__, + } + + try: + data["external_url"] = get_url(hass, allow_internal=False) + except NoURLAvailableError: + pass + + try: + data["internal_url"] = get_url(hass, allow_external=False) + except NoURLAvailableError: + pass + + # Set old base URL based on external or internal + data["base_url"] = data["external_url"] or data["internal_url"] + + return self.json(data) class APIStatesView(HomeAssistantView): diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index aa11e66d49c..008266e5a45 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -27,6 +27,7 @@ from .const import ( DOMAIN, DOMAIN_DATA_CONFIG, DOMAIN_DATA_ENTRIES, + DOMAIN_DATA_TASKS, SIGNAL_CLIENT_DATA, SIGNAL_CLIENT_STARTED, SIGNAL_CLIENT_STOPPED, @@ -73,6 +74,15 @@ DEVICE_SCHEMA = vol.Schema( ) ) + +async def _await_cancel(task): + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + + CONFIG_SCHEMA = vol.Schema( {DOMAIN: vol.All(cv.ensure_list, [DEVICE_SCHEMA])}, extra=vol.ALLOW_EXTRA ) @@ -81,6 +91,7 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistantType, config: ConfigType): """Set up the component.""" hass.data[DOMAIN_DATA_ENTRIES] = {} + hass.data[DOMAIN_DATA_TASKS] = {} hass.data[DOMAIN_DATA_CONFIG] = {} for device in config[DOMAIN]: @@ -94,6 +105,13 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): ) ) + async def _stop(_): + asyncio.gather( + *[_await_cancel(task) for task in hass.data[DOMAIN_DATA_TASKS].values()] + ) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop) + return True @@ -107,13 +125,15 @@ async def async_setup_entry(hass: HomeAssistantType, entry: config_entries.Confi {CONF_HOST: entry.data[CONF_HOST], CONF_PORT: entry.data[CONF_PORT]} ), ) + tasks = hass.data.setdefault(DOMAIN_DATA_TASKS, {}) hass.data[DOMAIN_DATA_ENTRIES][entry.entry_id] = { "client": client, "config": config, } - asyncio.ensure_future(_run_client(hass, client, config[CONF_SCAN_INTERVAL])) + task = asyncio.create_task(_run_client(hass, client, DEFAULT_SCAN_INTERVAL)) + tasks[entry.entry_id] = task hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, "media_player") @@ -122,22 +142,23 @@ async def async_setup_entry(hass: HomeAssistantType, entry: config_entries.Confi return True +async def async_unload_entry(hass, entry): + """Cleanup before removing config entry.""" + await hass.config_entries.async_forward_entry_unload(entry, "media_player") + + task = hass.data[DOMAIN_DATA_TASKS].pop(entry.entry_id) + await _await_cancel(task) + + hass.data[DOMAIN_DATA_ENTRIES].pop(entry.entry_id) + + return True + + async def _run_client(hass, client, interval): - task = asyncio.Task.current_task() - run = True - - async def _stop(_): - nonlocal run - run = False - task.cancel() - await task - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop) - def _listen(_): hass.helpers.dispatcher.async_dispatcher_send(SIGNAL_CLIENT_DATA, client.host) - while run: + while True: try: with async_timeout.timeout(interval): await client.start() @@ -163,7 +184,7 @@ async def _run_client(hass, client, interval): except asyncio.TimeoutError: continue except asyncio.CancelledError: - return + raise except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception, aborting arcam client") return diff --git a/homeassistant/components/arcam_fmj/const.py b/homeassistant/components/arcam_fmj/const.py index dc5a576acec..180abf2c960 100644 --- a/homeassistant/components/arcam_fmj/const.py +++ b/homeassistant/components/arcam_fmj/const.py @@ -5,9 +5,12 @@ SIGNAL_CLIENT_STARTED = "arcam.client_started" SIGNAL_CLIENT_STOPPED = "arcam.client_stopped" SIGNAL_CLIENT_DATA = "arcam.client_data" +EVENT_TURN_ON = "arcam_fmj.turn_on" + DEFAULT_PORT = 50000 DEFAULT_NAME = "Arcam FMJ" DEFAULT_SCAN_INTERVAL = 5 DOMAIN_DATA_ENTRIES = f"{DOMAIN}.entries" +DOMAIN_DATA_TASKS = f"{DOMAIN}.tasks" DOMAIN_DATA_CONFIG = f"{DOMAIN}.config" diff --git a/homeassistant/components/arcam_fmj/device_trigger.py b/homeassistant/components/arcam_fmj/device_trigger.py new file mode 100644 index 00000000000..549b4cf4f82 --- /dev/null +++ b/homeassistant/components/arcam_fmj/device_trigger.py @@ -0,0 +1,70 @@ +"""Provides device automations for Arcam FMJ Receiver control.""" +from typing import List + +import voluptuous as vol + +from homeassistant.components.automation import AutomationActionType +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_PLATFORM, + CONF_TYPE, +) +from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback +from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType + +from .const import DOMAIN, EVENT_TURN_ON + +TRIGGER_TYPES = {"turn_on"} +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), + } +) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device triggers for Arcam FMJ Receiver control devices.""" + registry = await entity_registry.async_get_registry(hass) + triggers = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain == "media_player": + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "turn_on", + } + ) + + return triggers + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + config = TRIGGER_SCHEMA(config) + + if config[CONF_TYPE] == "turn_on": + + @callback + def _handle_event(event: Event): + if event.data[ATTR_ENTITY_ID] == config[CONF_ENTITY_ID]: + hass.async_run_job(action({"trigger": config}, context=event.context)) + + return hass.bus.async_listen(EVENT_TURN_ON, _handle_event) + + return lambda: None diff --git a/homeassistant/components/arcam_fmj/manifest.json b/homeassistant/components/arcam_fmj/manifest.json index c304d7bf351..ff89641667a 100644 --- a/homeassistant/components/arcam_fmj/manifest.json +++ b/homeassistant/components/arcam_fmj/manifest.json @@ -3,6 +3,6 @@ "name": "Arcam FMJ Receivers", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/arcam_fmj", - "requirements": ["arcam-fmj==0.4.4"], + "requirements": ["arcam-fmj==0.4.6"], "codeowners": ["@elupus"] } diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 125b3bf96b1..27e1497a32d 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -18,6 +18,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_STEP, ) from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_NAME, CONF_ZONE, SERVICE_TURN_ON, @@ -31,6 +32,7 @@ from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .const import ( DOMAIN, DOMAIN_DATA_ENTRIES, + EVENT_TURN_ON, SIGNAL_CLIENT_DATA, SIGNAL_CLIENT_STARTED, SIGNAL_CLIENT_STOPPED, @@ -53,6 +55,7 @@ async def async_setup_entry( [ ArcamFmj( State(client, zone), + config_entry.unique_id or config_entry.entry_id, zone_config[CONF_NAME], zone_config.get(SERVICE_TURN_ON), ) @@ -67,9 +70,12 @@ async def async_setup_entry( class ArcamFmj(MediaPlayerEntity): """Representation of a media device.""" - def __init__(self, state: State, name: str, turn_on: Optional[ConfigType]): + def __init__( + self, state: State, uuid: str, name: str, turn_on: Optional[ConfigType] + ): """Initialize device.""" self._state = state + self._uuid = uuid self._name = name self._turn_on = turn_on self._support = ( @@ -78,6 +84,7 @@ class ArcamFmj(MediaPlayerEntity): | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_STEP | SUPPORT_TURN_OFF + | SUPPORT_TURN_ON ) if state.zn == 1: self._support |= SUPPORT_SELECT_SOUND_MODE @@ -95,6 +102,11 @@ class ArcamFmj(MediaPlayerEntity): ) ) + @property + def unique_id(self): + """Return unique identifier if known.""" + return f"{self._uuid}-{self._state.zn}" + @property def device_info(self): """Return a device description for device registry.""" @@ -124,10 +136,7 @@ class ArcamFmj(MediaPlayerEntity): @property def supported_features(self): """Flag media player features that are supported.""" - support = self._support - if self._state.get_power() is not None or self._turn_on: - support |= SUPPORT_TURN_ON - return support + return self._support async def async_added_to_hass(self): """Once registered, add listener for events.""" @@ -230,7 +239,8 @@ class ArcamFmj(MediaPlayerEntity): validate_config=False, ) else: - _LOGGER.error("Unable to turn on") + _LOGGER.debug("Firing event to turn on device") + self.hass.bus.async_fire(EVENT_TURN_ON, {ATTR_ENTITY_ID: self.entity_id}) async def async_turn_off(self): """Turn the media player off.""" diff --git a/homeassistant/components/arcam_fmj/strings.json b/homeassistant/components/arcam_fmj/strings.json new file mode 100644 index 00000000000..6f60c9e2471 --- /dev/null +++ b/homeassistant/components/arcam_fmj/strings.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "trigger_type": { + "turn_on": "{entity_name} was requested to turn on" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/ca.json b/homeassistant/components/arcam_fmj/translations/ca.json index b78b8cbaa7b..33af7b119be 100644 --- a/homeassistant/components/arcam_fmj/translations/ca.json +++ b/homeassistant/components/arcam_fmj/translations/ca.json @@ -1,3 +1,7 @@ { - "title": "Arcam FMJ" + "device_automation": { + "trigger_type": { + "turn_on": "S'ha sol\u00b7licitat l'activaci\u00f3 de {entity_name}" + } + } } \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/de.json b/homeassistant/components/arcam_fmj/translations/de.json index b78b8cbaa7b..85032362472 100644 --- a/homeassistant/components/arcam_fmj/translations/de.json +++ b/homeassistant/components/arcam_fmj/translations/de.json @@ -1,3 +1,7 @@ { - "title": "Arcam FMJ" + "device_automation": { + "trigger_type": { + "turn_on": "{entity_name} wurde zum Einschalten aufgefordert" + } + } } \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/en.json b/homeassistant/components/arcam_fmj/translations/en.json index b78b8cbaa7b..6f60c9e2471 100644 --- a/homeassistant/components/arcam_fmj/translations/en.json +++ b/homeassistant/components/arcam_fmj/translations/en.json @@ -1,3 +1,7 @@ { - "title": "Arcam FMJ" + "device_automation": { + "trigger_type": { + "turn_on": "{entity_name} was requested to turn on" + } + } } \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/es.json b/homeassistant/components/arcam_fmj/translations/es.json index b78b8cbaa7b..87a165511cc 100644 --- a/homeassistant/components/arcam_fmj/translations/es.json +++ b/homeassistant/components/arcam_fmj/translations/es.json @@ -1,3 +1,7 @@ { - "title": "Arcam FMJ" + "device_automation": { + "trigger_type": { + "turn_on": "Se solicit\u00f3 encender {entity_name}" + } + } } \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/it.json b/homeassistant/components/arcam_fmj/translations/it.json index b78b8cbaa7b..71deb04fd1e 100644 --- a/homeassistant/components/arcam_fmj/translations/it.json +++ b/homeassistant/components/arcam_fmj/translations/it.json @@ -1,3 +1,7 @@ { - "title": "Arcam FMJ" + "device_automation": { + "trigger_type": { + "turn_on": "\u00c8 stato richiesto di attivare {entity_name}" + } + } } \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/ko.json b/homeassistant/components/arcam_fmj/translations/ko.json index b78b8cbaa7b..7a2005017af 100644 --- a/homeassistant/components/arcam_fmj/translations/ko.json +++ b/homeassistant/components/arcam_fmj/translations/ko.json @@ -1,3 +1,7 @@ { - "title": "Arcam FMJ" + "device_automation": { + "trigger_type": { + "turn_on": "{entity_name} \uc774(\uac00) \ucf1c\uc9c0\ub3c4\ub85d \uc694\uccad\ub418\uc5c8\uc744 \ub54c" + } + } } \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/lb.json b/homeassistant/components/arcam_fmj/translations/lb.json index b78b8cbaa7b..a057827bb6f 100644 --- a/homeassistant/components/arcam_fmj/translations/lb.json +++ b/homeassistant/components/arcam_fmj/translations/lb.json @@ -1,3 +1,7 @@ { - "title": "Arcam FMJ" + "device_automation": { + "trigger_type": { + "turn_on": "{entity_name} soll ugeschalt ginn" + } + } } \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/nl.json b/homeassistant/components/arcam_fmj/translations/nl.json index b78b8cbaa7b..761d57a48cb 100644 --- a/homeassistant/components/arcam_fmj/translations/nl.json +++ b/homeassistant/components/arcam_fmj/translations/nl.json @@ -1,3 +1,7 @@ { - "title": "Arcam FMJ" + "device_automation": { + "trigger_type": { + "turn_on": "{entity_name} is gevraagd in te schakelen" + } + } } \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/no.json b/homeassistant/components/arcam_fmj/translations/no.json index d8a4c453015..81404be2ace 100644 --- a/homeassistant/components/arcam_fmj/translations/no.json +++ b/homeassistant/components/arcam_fmj/translations/no.json @@ -1,3 +1,7 @@ { - "title": "" + "device_automation": { + "trigger_type": { + "turn_on": "{entity_name} ble bedt om \u00e5 sl\u00e5 p\u00e5" + } + } } \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/pl.json b/homeassistant/components/arcam_fmj/translations/pl.json index b78b8cbaa7b..cad2b4adb25 100644 --- a/homeassistant/components/arcam_fmj/translations/pl.json +++ b/homeassistant/components/arcam_fmj/translations/pl.json @@ -1,3 +1,7 @@ { - "title": "Arcam FMJ" + "device_automation": { + "trigger_type": { + "turn_on": "{entity_name} zostanie poproszony o w\u0142\u0105czenie" + } + } } \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/pt-BR.json b/homeassistant/components/arcam_fmj/translations/pt-BR.json index b78b8cbaa7b..8071efb001f 100644 --- a/homeassistant/components/arcam_fmj/translations/pt-BR.json +++ b/homeassistant/components/arcam_fmj/translations/pt-BR.json @@ -1,3 +1,7 @@ { - "title": "Arcam FMJ" + "device_automation": { + "trigger_type": { + "turn_on": "Foi solicitado que {entity_name} ligue" + } + } } \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/ru.json b/homeassistant/components/arcam_fmj/translations/ru.json index b78b8cbaa7b..58f4fd5ea3b 100644 --- a/homeassistant/components/arcam_fmj/translations/ru.json +++ b/homeassistant/components/arcam_fmj/translations/ru.json @@ -1,3 +1,7 @@ { - "title": "Arcam FMJ" + "device_automation": { + "trigger_type": { + "turn_on": "\u0437\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 {entity_name}" + } + } } \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/zh-Hant.json b/homeassistant/components/arcam_fmj/translations/zh-Hant.json index b78b8cbaa7b..18ea7d22eb1 100644 --- a/homeassistant/components/arcam_fmj/translations/zh-Hant.json +++ b/homeassistant/components/arcam_fmj/translations/zh-Hant.json @@ -1,3 +1,7 @@ { - "title": "Arcam FMJ" + "device_automation": { + "trigger_type": { + "turn_on": "{entity_name} \u4f9d\u9700\u6c42\u958b\u555f" + } + } } \ No newline at end of file diff --git a/homeassistant/components/arwn/sensor.py b/homeassistant/components/arwn/sensor.py index 5da860d8a50..7c947be61bf 100644 --- a/homeassistant/components/arwn/sensor.py +++ b/homeassistant/components/arwn/sensor.py @@ -39,6 +39,10 @@ def discover_sensors(topic, payload): return ArwnSensor( "Rain Since Midnight", "since_midnight", "in", "mdi:water" ) + return ( + ArwnSensor("Total Rainfall", "total", unit, "mdi:water"), + ArwnSensor("Rainfall Rate", "rate", unit, "mdi:water"), + ) if domain == "barometer": return ArwnSensor("Barometer", "pressure", unit, "mdi:thermometer-lines") if domain == "wind": diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index 631e6e9d70f..cbe32a1ec43 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -10,6 +10,9 @@ from . import DATA_ASUSWRT _LOGGER = logging.getLogger(__name__) +UPLOAD_ICON = "mdi:upload-network" +DOWNLOAD_ICON = "mdi:download-network" + async def async_setup_platform(hass, config, add_entities, discovery_info=None): """Set up the asuswrt sensors.""" @@ -82,6 +85,11 @@ class AsuswrtRXSensor(AsuswrtSensor): _name = "Asuswrt Download Speed" _unit = DATA_RATE_MEGABITS_PER_SECOND + @property + def icon(self): + """Return the icon.""" + return DOWNLOAD_ICON + @property def unit_of_measurement(self): """Return the unit of measurement.""" @@ -100,6 +108,11 @@ class AsuswrtTXSensor(AsuswrtSensor): _name = "Asuswrt Upload Speed" _unit = DATA_RATE_MEGABITS_PER_SECOND + @property + def icon(self): + """Return the icon.""" + return UPLOAD_ICON + @property def unit_of_measurement(self): """Return the unit of measurement.""" @@ -118,6 +131,11 @@ class AsuswrtTotalRXSensor(AsuswrtSensor): _name = "Asuswrt Download" _unit = DATA_GIGABYTES + @property + def icon(self): + """Return the icon.""" + return DOWNLOAD_ICON + @property def unit_of_measurement(self): """Return the unit of measurement.""" @@ -136,6 +154,11 @@ class AsuswrtTotalTXSensor(AsuswrtSensor): _name = "Asuswrt Upload" _unit = DATA_GIGABYTES + @property + def icon(self): + """Return the icon.""" + return UPLOAD_ICON + @property def unit_of_measurement(self): """Return the unit of measurement.""" diff --git a/homeassistant/components/atag/__init__.py b/homeassistant/components/atag/__init__.py index e80ae0cc79e..237a82f207a 100644 --- a/homeassistant/components/atag/__init__.py +++ b/homeassistant/components/atag/__init__.py @@ -31,16 +31,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): session = async_get_clientsession(hass) coordinator = AtagDataUpdateCoordinator(hass, session, entry) - try: - await coordinator.async_refresh() - except AtagException: - raise ConfigEntryNotReady - + await coordinator.async_refresh() if not coordinator.last_update_success: raise ConfigEntryNotReady hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator + if entry.unique_id is None: + hass.config_entries.async_update_entry(entry, unique_id=coordinator.atag.id) for platform in PLATFORMS: hass.async_create_task( @@ -65,9 +63,8 @@ class AtagDataUpdateCoordinator(DataUpdateCoordinator): """Update data via library.""" with async_timeout.timeout(20): try: - await self.atag.update() - if not self.atag.report: - raise UpdateFailed("No data") + if not await self.atag.update(): + raise UpdateFailed("No data received") except AtagException as error: raise UpdateFailed(error) return self.atag.report @@ -121,11 +118,6 @@ class AtagEntity(Entity): """Return the polling requirement of the entity.""" return False - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self.coordinator.atag.climate.temp_unit - @property def available(self): """Return True if entity is available.""" diff --git a/homeassistant/components/atag/climate.py b/homeassistant/components/atag/climate.py index 4c39b2ea8f8..ad46fefe8c2 100644 --- a/homeassistant/components/atag/climate.py +++ b/homeassistant/components/atag/climate.py @@ -12,7 +12,7 @@ from homeassistant.components.climate.const import ( SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.const import ATTR_TEMPERATURE from . import CLIMATE, DOMAIN, AtagEntity @@ -66,9 +66,7 @@ class AtagThermostat(AtagEntity, ClimateEntity): @property def temperature_unit(self): """Return the unit of measurement.""" - if self.coordinator.atag.climate.temp_unit in [TEMP_CELSIUS, TEMP_FAHRENHEIT]: - return self.coordinator.atag.climate.temp_unit - return None + return self.coordinator.atag.climate.temp_unit @property def current_temperature(self) -> Optional[float]: diff --git a/homeassistant/components/atag/config_flow.py b/homeassistant/components/atag/config_flow.py index 369f4b98587..7ad05087304 100644 --- a/homeassistant/components/atag/config_flow.py +++ b/homeassistant/components/atag/config_flow.py @@ -1,9 +1,9 @@ """Config flow for the Atag component.""" -from pyatag import DEFAULT_PORT, AtagException, AtagOne +import pyatag import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_DEVICE, CONF_EMAIL, CONF_HOST, CONF_PORT +from homeassistant.const import CONF_EMAIL, CONF_HOST, CONF_PORT from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -12,7 +12,7 @@ from . import DOMAIN # pylint: disable=unused-import DATA_SCHEMA = { vol.Required(CONF_HOST): str, vol.Optional(CONF_EMAIL): str, - vol.Required(CONF_PORT, default=DEFAULT_PORT): vol.Coerce(int), + vol.Required(CONF_PORT, default=pyatag.const.DEFAULT_PORT): vol.Coerce(int), } @@ -25,21 +25,22 @@ class AtagConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" - if self._async_current_entries(): - return self.async_abort(reason="already_configured") - if not user_input: return await self._show_form() session = async_get_clientsession(self.hass) try: - atag = AtagOne(session=session, **user_input) + atag = pyatag.AtagOne(session=session, **user_input) await atag.authorize() await atag.update(force=True) - except AtagException: + except pyatag.errors.Unauthorized: + return await self._show_form({"base": "unauthorized"}) + except pyatag.errors.AtagException: return await self._show_form({"base": "connection_error"}) - user_input.update({CONF_DEVICE: atag.id}) + await self.async_set_unique_id(atag.id) + self._abort_if_unique_id_configured(updates=user_input) + return self.async_create_entry(title=atag.id, data=user_input) @callback diff --git a/homeassistant/components/atag/manifest.json b/homeassistant/components/atag/manifest.json index 5fd77ee5155..9f8a5d2c6eb 100644 --- a/homeassistant/components/atag/manifest.json +++ b/homeassistant/components/atag/manifest.json @@ -3,6 +3,6 @@ "name": "Atag", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/atag/", - "requirements": ["pyatag==0.3.1.2"], + "requirements": ["pyatag==0.3.3.4"], "codeowners": ["@MatsNL"] } diff --git a/homeassistant/components/atag/sensor.py b/homeassistant/components/atag/sensor.py index d5ff0b7bbde..1d647eb4764 100644 --- a/homeassistant/components/atag/sensor.py +++ b/homeassistant/components/atag/sensor.py @@ -5,6 +5,8 @@ from homeassistant.const import ( PRESSURE_BAR, TEMP_CELSIUS, TEMP_FAHRENHEIT, + TIME_HOURS, + UNIT_PERCENTAGE, ) from . import DOMAIN, AtagEntity @@ -65,6 +67,8 @@ class AtagSensor(AtagEntity): PRESSURE_BAR, TEMP_CELSIUS, TEMP_FAHRENHEIT, + UNIT_PERCENTAGE, + TIME_HOURS, ]: return self.coordinator.data[self._id].measure return None diff --git a/homeassistant/components/atag/strings.json b/homeassistant/components/atag/strings.json index 859f0531c5a..85e22c10c1b 100644 --- a/homeassistant/components/atag/strings.json +++ b/homeassistant/components/atag/strings.json @@ -12,10 +12,11 @@ } }, "error": { + "unauthorized": "Pairing denied, check device for auth request", "connection_error": "Failed to connect, please try again" }, "abort": { - "already_configured": "Only one Atag device can be added to Home Assistant" + "already_configured": "This device has already been added to HomeAssistant" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/atag/translations/ca.json b/homeassistant/components/atag/translations/ca.json index 4208873e675..ac9959bca39 100644 --- a/homeassistant/components/atag/translations/ca.json +++ b/homeassistant/components/atag/translations/ca.json @@ -1,17 +1,18 @@ { "config": { "abort": { - "already_configured": "Nom\u00e9s es pot afegir un sol dispositiu Atag a Home Assistant" + "already_configured": "Aquest dispositiu ja ha estat afegit a Home Assistant" }, "error": { - "connection_error": "No s'ha pogut connectar, torna-ho a provar" + "connection_error": "No s'ha pogut connectar, torna-ho a provar", + "unauthorized": "L'emparellament s'ha denegat, comprova si hi ha una sol\u00b7licitud d'autenticaci\u00f3 al dispositiu" }, "step": { "user": { "data": { "email": "Correu electr\u00f2nic (opcional)", "host": "Amfitri\u00f3", - "port": "Port (10000)" + "port": "Port" }, "title": "Connexi\u00f3 amb el dispositiu" } diff --git a/homeassistant/components/atag/translations/de.json b/homeassistant/components/atag/translations/de.json index f9d40a035a3..cf133aef758 100644 --- a/homeassistant/components/atag/translations/de.json +++ b/homeassistant/components/atag/translations/de.json @@ -9,6 +9,7 @@ "step": { "user": { "data": { + "email": "Email (Optional)", "host": "Host", "port": "Port (10000)" }, diff --git a/homeassistant/components/atag/translations/en.json b/homeassistant/components/atag/translations/en.json index 978f3a15453..8901a417065 100644 --- a/homeassistant/components/atag/translations/en.json +++ b/homeassistant/components/atag/translations/en.json @@ -1,10 +1,11 @@ { "config": { "abort": { - "already_configured": "Only one Atag device can be added to Home Assistant" + "already_configured": "This device has already been added to HomeAssistant" }, "error": { - "connection_error": "Failed to connect, please try again" + "connection_error": "Failed to connect, please try again", + "unauthorized": "Pairing denied, check device for auth request" }, "step": { "user": { diff --git a/homeassistant/components/atag/translations/es-419.json b/homeassistant/components/atag/translations/es-419.json index 214dd0e9004..f837491b330 100644 --- a/homeassistant/components/atag/translations/es-419.json +++ b/homeassistant/components/atag/translations/es-419.json @@ -9,6 +9,7 @@ "step": { "user": { "data": { + "email": "Correo electr\u00f3nico (opcional)", "host": "Host", "port": "Puerto (10000)" }, diff --git a/homeassistant/components/atag/translations/es.json b/homeassistant/components/atag/translations/es.json index b02a20e09a1..c60a8476bdf 100644 --- a/homeassistant/components/atag/translations/es.json +++ b/homeassistant/components/atag/translations/es.json @@ -1,16 +1,18 @@ { "config": { "abort": { - "already_configured": "S\u00f3lo se puede a\u00f1adir un dispositivo Atag a Home Assistant" + "already_configured": "Este dispositivo ya ha sido a\u00f1adido a HomeAssistant" }, "error": { - "connection_error": "No se pudo conectar, por favor, int\u00e9ntalo de nuevo" + "connection_error": "No se pudo conectar, por favor, int\u00e9ntalo de nuevo", + "unauthorized": "Emparejamiento denegado, comprobar el dispositivo para la solicitud de autorizaci\u00f3n" }, "step": { "user": { "data": { + "email": "Correo electr\u00f3nico (Opcional)", "host": "Host", - "port": "Puerto (10000)" + "port": "Puerto" }, "title": "Conectarse al dispositivo" } diff --git a/homeassistant/components/atag/translations/hu.json b/homeassistant/components/atag/translations/hu.json index 22687b6944a..45918735010 100644 --- a/homeassistant/components/atag/translations/hu.json +++ b/homeassistant/components/atag/translations/hu.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "port": "Port (10000)" + "host": "Hoszt", + "port": "Port" } } } diff --git a/homeassistant/components/atag/translations/it.json b/homeassistant/components/atag/translations/it.json index 190da0f14d7..cbef4fab268 100644 --- a/homeassistant/components/atag/translations/it.json +++ b/homeassistant/components/atag/translations/it.json @@ -1,16 +1,18 @@ { "config": { "abort": { - "already_configured": "\u00c8 possibile aggiungere un solo dispositivo Atag ad Home Assistant" + "already_configured": "Questo dispositivo \u00e8 gi\u00e0 stato aggiunto a HomeAssistant" }, "error": { - "connection_error": "Impossibile connettersi, si prega di riprovare" + "connection_error": "Impossibile connettersi, si prega di riprovare", + "unauthorized": "Associazione negata, controllare il dispositivo per la richiesta di autenticazione" }, "step": { "user": { "data": { + "email": "E-mail (Opzionale)", "host": "Host", - "port": "Porta (10000)" + "port": "Porta" }, "title": "Connettersi al dispositivo" } diff --git a/homeassistant/components/atag/translations/ko.json b/homeassistant/components/atag/translations/ko.json index 97558185815..3ca689a488c 100644 --- a/homeassistant/components/atag/translations/ko.json +++ b/homeassistant/components/atag/translations/ko.json @@ -4,7 +4,8 @@ "already_configured": "Home Assistant \uc5d0\ub294 \ud558\ub098\uc758 Atag \uae30\uae30\ub9cc \ucd94\uac00\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4" }, "error": { - "connection_error": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." + "connection_error": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "unauthorized": "\ud398\uc5b4\ub9c1\uc774 \uac70\ubd80\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uc778\uc99d \uc694\uccad \uae30\uae30\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694" }, "step": { "user": { diff --git a/homeassistant/components/atag/translations/lb.json b/homeassistant/components/atag/translations/lb.json index 3850ab33419..72f5820e2d3 100644 --- a/homeassistant/components/atag/translations/lb.json +++ b/homeassistant/components/atag/translations/lb.json @@ -4,7 +4,8 @@ "already_configured": "N\u00ebmmen 1 Atag Apparat kann am Home Assistant dob\u00e4igesat ginn" }, "error": { - "connection_error": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol." + "connection_error": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol.", + "unauthorized": "Kopplung verweigert, iwwerpr\u00e9if den Apparat fir auth request" }, "step": { "user": { diff --git a/homeassistant/components/atag/translations/nl.json b/homeassistant/components/atag/translations/nl.json index 049e363cf92..077beb65871 100644 --- a/homeassistant/components/atag/translations/nl.json +++ b/homeassistant/components/atag/translations/nl.json @@ -4,7 +4,8 @@ "already_configured": "Er kan slechts \u00e9\u00e9n Atag-apparaat worden toegevoegd aan Home Assistant " }, "error": { - "connection_error": "Verbinding mislukt, probeer het opnieuw" + "connection_error": "Verbinding mislukt, probeer het opnieuw", + "unauthorized": "Koppelen geweigerd, controleer apparaat op autorisatieverzoek" }, "step": { "user": { diff --git a/homeassistant/components/atag/translations/no.json b/homeassistant/components/atag/translations/no.json index ee9811c581f..3f446a5f21b 100644 --- a/homeassistant/components/atag/translations/no.json +++ b/homeassistant/components/atag/translations/no.json @@ -1,10 +1,11 @@ { "config": { "abort": { - "already_configured": "Bare en Atag-enhet kan legges til Home Assistant" + "already_configured": "Denne enheten er allerede lagt til i HomeAssistant" }, "error": { - "connection_error": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen" + "connection_error": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen", + "unauthorized": "Parring nektet, sjekk enheten for autorisasjonsforesp\u00f8rsel" }, "step": { "user": { diff --git a/homeassistant/components/atag/translations/pl.json b/homeassistant/components/atag/translations/pl.json index a1d7c281dc9..971ff7221b9 100644 --- a/homeassistant/components/atag/translations/pl.json +++ b/homeassistant/components/atag/translations/pl.json @@ -9,11 +9,11 @@ "step": { "user": { "data": { - "email": "[%key_id:common::config_flow::data::email%] (opcjonalnie)", - "host": "[%key_id:common::config_flow::data::host%]", - "port": "[%key_id:common::config_flow::data::port%] (10000)" + "email": "Adres e-mail (opcjonalnie)", + "host": "Nazwa hosta lub adres IP", + "port": "Port" }, - "title": "Po\u0142\u0105cz z urz\u0105dzeniem" + "title": "Po\u0142\u0105czenie z urz\u0105dzeniem" } } }, diff --git a/homeassistant/components/atag/translations/pt-BR.json b/homeassistant/components/atag/translations/pt-BR.json new file mode 100644 index 00000000000..eda0a2cc041 --- /dev/null +++ b/homeassistant/components/atag/translations/pt-BR.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "connection_error": "Falha ao conectar, tente novamente" + }, + "step": { + "user": { + "data": { + "email": "E-mail (Opcional)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/atag/translations/ru.json b/homeassistant/components/atag/translations/ru.json index daaff9d9dda..caf80b415e6 100644 --- a/homeassistant/components/atag/translations/ru.json +++ b/homeassistant/components/atag/translations/ru.json @@ -1,10 +1,11 @@ { "config": { "abort": { - "already_configured": "\u041c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e." + "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": { - "connection_error": "\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." + "connection_error": "\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.", + "unauthorized": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0437\u0430\u043f\u0440\u0435\u0449\u0435\u043d\u043e, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0430 \u0437\u0430\u043f\u0440\u043e\u0441 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/atag/translations/zh-Hant.json b/homeassistant/components/atag/translations/zh-Hant.json index 46d39b186ac..6fc2e520d17 100644 --- a/homeassistant/components/atag/translations/zh-Hant.json +++ b/homeassistant/components/atag/translations/zh-Hant.json @@ -1,10 +1,11 @@ { "config": { "abort": { - "already_configured": "\u50c5\u80fd\u65b0\u589e\u4e00\u7d44 Atag \u8a2d\u5099\u81f3 Home Assistant" + "already_configured": "\u6b64\u8a2d\u5099\u5df2\u7d93\u65b0\u589e\u81f3 Home Assistant" }, "error": { - "connection_error": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21" + "connection_error": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", + "unauthorized": "\u914d\u5c0d\u906d\u62d2\uff0c\u8acb\u6aa2\u67e5\u8a2d\u5099\u8a8d\u8b49\u8acb\u6c42" }, "step": { "user": { diff --git a/homeassistant/components/atag/water_heater.py b/homeassistant/components/atag/water_heater.py index 311ff56985b..f9c2a4625bb 100644 --- a/homeassistant/components/atag/water_heater.py +++ b/homeassistant/components/atag/water_heater.py @@ -40,9 +40,8 @@ class AtagWaterHeater(AtagEntity, WaterHeaterEntity): @property def current_operation(self): """Return current operation.""" - if self.coordinator.atag.dhw.status: - return STATE_PERFORMANCE - return STATE_OFF + operation = self.coordinator.atag.dhw.current_operation + return operation if operation in self.operation_list else STATE_OFF @property def operation_list(self): diff --git a/homeassistant/components/august/translations/fr.json b/homeassistant/components/august/translations/fr.json index da2df2461a1..752b7dc3712 100644 --- a/homeassistant/components/august/translations/fr.json +++ b/homeassistant/components/august/translations/fr.json @@ -16,12 +16,14 @@ "timeout": "D\u00e9lai d'expiration (secondes)", "username": "Nom d'utilisateur" }, + "description": "Si la m\u00e9thode de connexion est \u00abe-mail\u00bb, le nom d'utilisateur est l'adresse e-mail. Si la m\u00e9thode de connexion est \u00abt\u00e9l\u00e9phone\u00bb, le nom d'utilisateur est le num\u00e9ro de t\u00e9l\u00e9phone au format \u00ab+ NNNNNNNNN\u00bb.", "title": "Configurer un compte August" }, "validation": { "data": { "code": "Code de v\u00e9rification" }, + "description": "Veuillez v\u00e9rifier votre {login_method} ( {username} ) et entrez le code de v\u00e9rification ci-dessous", "title": "Authentification \u00e0 deux facteurs" } } diff --git a/homeassistant/components/august/translations/hu.json b/homeassistant/components/august/translations/hu.json new file mode 100644 index 00000000000..dee4ed9ee0f --- /dev/null +++ b/homeassistant/components/august/translations/hu.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/august/translations/ko.json b/homeassistant/components/august/translations/ko.json index dce916bb788..52f939c45a0 100644 --- a/homeassistant/components/august/translations/ko.json +++ b/homeassistant/components/august/translations/ko.json @@ -16,7 +16,7 @@ "timeout": "\uc81c\ud55c \uc2dc\uac04 (\ucd08)", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, - "description": "\ub85c\uadf8\uc778 \ubc29\ubc95\uc774 '\uc774\uba54\uc77c'\uc778 \uacbd\uc6b0, \uc0ac\uc6a9\uc790 \uc774\ub984\uc740 \uc774\uba54\uc77c \uc8fc\uc18c\uc785\ub2c8\ub2e4. \ub85c\uadf8\uc778 \ubc29\ubc95\uc774 'phone'\uc778 \uacbd\uc6b0, \uc0ac\uc6a9\uc790 \uc774\ub984\uc740 '+NNNNNNNNN' \ud615\uc2dd\uc758 \uc804\ud654\ubc88\ud638\uc785\ub2c8\ub2e4.", + "description": "\ub85c\uadf8\uc778 \ubc29\ubc95\uc774 '\uc774\uba54\uc77c'\uc778 \uacbd\uc6b0, \uc0ac\uc6a9\uc790 \uc774\ub984\uc740 \uc774\uba54\uc77c \uc8fc\uc18c\uc785\ub2c8\ub2e4. \ub85c\uadf8\uc778 \ubc29\ubc95\uc774 '\uc804\ud654\ubc88\ud638'\uc778 \uacbd\uc6b0, \uc0ac\uc6a9\uc790 \uc774\ub984\uc740 '+NNNNNNNNN' \ud615\uc2dd\uc758 \uc804\ud654\ubc88\ud638\uc785\ub2c8\ub2e4.", "title": "August \uacc4\uc815 \uc124\uc815\ud558\uae30" }, "validation": { diff --git a/homeassistant/components/august/translations/pl.json b/homeassistant/components/august/translations/pl.json index 33ef6431792..eeaa5269da4 100644 --- a/homeassistant/components/august/translations/pl.json +++ b/homeassistant/components/august/translations/pl.json @@ -1,20 +1,20 @@ { "config": { "abort": { - "already_configured": "[%key_id:common::config_flow::abort::already_configured_account%]" + "already_configured": "Konto jest ju\u017c skonfigurowane." }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "[%key_id:common::config_flow::error::invalid_auth%]", - "unknown": "[%key_id:common::config_flow::error::unknown%]" + "invalid_auth": "Niepoprawne uwierzytelnienie.", + "unknown": "Nieoczekiwany b\u0142\u0105d." }, "step": { "user": { "data": { "login_method": "Metoda logowania", - "password": "[%key_id:common::config_flow::data::password%]", + "password": "Has\u0142o", "timeout": "Limit czasu (sekundy)", - "username": "[%key_id:common::config_flow::data::username%]" + "username": "Nazwa u\u017cytkownika" }, "description": "Je\u015bli metod\u0105 logowania jest 'e-mail', nazw\u0105 u\u017cytkownika b\u0119dzie adres e-mail. Je\u015bli metod\u0105 logowania jest 'telefon', nazw\u0105 u\u017cytkownika b\u0119dzie numer telefonu w formacie '+NNNNNNNNN'.", "title": "Konfiguracja konta August" diff --git a/homeassistant/components/august/translations/pt-BR.json b/homeassistant/components/august/translations/pt-BR.json new file mode 100644 index 00000000000..efb4b3db35f --- /dev/null +++ b/homeassistant/components/august/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "timeout": "Tempo limite (segundos)" + }, + "description": "Se o m\u00e9todo de login for 'email', Nome de usu\u00e1rio \u00e9 o endere\u00e7o de email. Se o m\u00e9todo de login for 'telefone', Nome de usu\u00e1rio ser\u00e1 o n\u00famero de telefone no formato '+NNNNNNNNN'.", + "title": "Configurar uma conta de August" + }, + "validation": { + "data": { + "code": "C\u00f3digo de verifica\u00e7\u00e3o" + }, + "description": "Por favor, verifique o seu {login_method} ({username}) e digite o c\u00f3digo de verifica\u00e7\u00e3o abaixo", + "title": "Autentica\u00e7\u00e3o de dois fatores" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/translations/fi.json b/homeassistant/components/auth/translations/fi.json index b73c7e194f9..92e4f03c0f9 100644 --- a/homeassistant/components/auth/translations/fi.json +++ b/homeassistant/components/auth/translations/fi.json @@ -1,6 +1,9 @@ { "mfa_setup": { "notify": { + "error": { + "invalid_code": "Virheellinen koodi. Yrit\u00e4 uudelleen." + }, "step": { "setup": { "title": "Varmista asetukset" diff --git a/homeassistant/components/auth/translations/it.json b/homeassistant/components/auth/translations/it.json index dbfe4acd615..34d404f0bc6 100644 --- a/homeassistant/components/auth/translations/it.json +++ b/homeassistant/components/auth/translations/it.json @@ -25,7 +25,7 @@ }, "step": { "init": { - "description": "Per attivare l'autenticazione a due fattori utilizzando le password monouso basate sul tempo, eseguire la scansione del codice QR con l'app di autenticazione. Se non ne hai uno, ti consigliamo [Google Authenticator] (https://support.google.com/accounts/answer/1066447) o [Authy] (https://authy.com/). \n\n {qr_code} \n \nDopo la scansione, inserisci il codice a sei cifre dalla tua app per verificare la configurazione. Se riscontri problemi con la scansione del codice QR, esegui una configurazione manuale con il codice ** ` {code} ` **.", + "description": "Per attivare l'autenticazione a due fattori utilizzando le password monouso basate sul tempo, eseguire la scansione del codice QR con l'app di autenticazione. Se non ne hai uno, ti consigliamo [Google Authenticator](https://support.google.com/accounts/answer/1066447) o [Authy](https://authy.com/). \n\n {qr_code} \n \nDopo la scansione, inserisci il codice a sei cifre dalla tua app per verificare la configurazione. Se riscontri problemi con la scansione del codice QR, esegui una configurazione manuale con il codice **`{code}`**.", "title": "Imposta l'autenticazione a due fattori usando TOTP" } }, diff --git a/homeassistant/components/auth/translations/ko.json b/homeassistant/components/auth/translations/ko.json index 563c141587f..80850bb58b4 100644 --- a/homeassistant/components/auth/translations/ko.json +++ b/homeassistant/components/auth/translations/ko.json @@ -21,11 +21,11 @@ }, "totp": { "error": { - "invalid_code": "\uc798\ubabb\ub41c \ucf54\ub4dc \uc785\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694. \uc774 \uc624\ub958\uac00 \uc9c0\uc18d\uc801\uc73c\ub85c \ubc1c\uc0dd\ud55c\ub2e4\uba74 Home Assistant \uc758 \uc2dc\uac04\uc124\uc815\uc774 \uc62c\ubc14\ub978\uc9c0 \ud655\uc778\ud574\ubcf4\uc138\uc694." + "invalid_code": "\uc798\ubabb\ub41c \ucf54\ub4dc\uc785\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694. \uc774 \uc624\ub958\uac00 \uc9c0\uc18d\uc801\uc73c\ub85c \ubc1c\uc0dd\ud55c\ub2e4\uba74 Home Assistant \uc758 \uc2dc\uac04\uc124\uc815\uc774 \uc62c\ubc14\ub978\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694." }, "step": { "init": { - "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \uad6c\uc131\ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574\uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.", + "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \uad6c\uc131\ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574\uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud558\uc5ec \uc124\uc815\uc744 \ud655\uc778\ud574\uc8fc\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\uc8fc\uc138\uc694.", "title": "TOTP 2\ub2e8\uacc4 \uc778\uc99d \uad6c\uc131\ud558\uae30" } }, diff --git a/homeassistant/components/automatic/__init__.py b/homeassistant/components/automatic/__init__.py deleted file mode 100644 index 8a1cae16f1e..00000000000 --- a/homeassistant/components/automatic/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The automatic component.""" diff --git a/homeassistant/components/automatic/device_tracker.py b/homeassistant/components/automatic/device_tracker.py deleted file mode 100644 index 0f48ef6376d..00000000000 --- a/homeassistant/components/automatic/device_tracker.py +++ /dev/null @@ -1,361 +0,0 @@ -"""Support for the Automatic platform.""" -import asyncio -from datetime import timedelta -import json -import logging -import os - -import aioautomatic -from aiohttp import web -import voluptuous as vol - -from homeassistant.components.device_tracker import ( - ATTR_ATTRIBUTES, - ATTR_DEV_ID, - ATTR_GPS, - ATTR_GPS_ACCURACY, - ATTR_HOST_NAME, - ATTR_MAC, - PLATFORM_SCHEMA, -) -from homeassistant.components.http import HomeAssistantView -from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import callback -from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_track_time_interval - -_LOGGER = logging.getLogger(__name__) - -ATTR_FUEL_LEVEL = "fuel_level" - -CONF_CLIENT_ID = "client_id" -CONF_CURRENT_LOCATION = "current_location" -CONF_DEVICES = "devices" -CONF_SECRET = "secret" - -DATA_CONFIGURING = "automatic_configurator_clients" -DATA_REFRESH_TOKEN = "refresh_token" -DEFAULT_SCOPE = ["location", "trip", "vehicle:events", "vehicle:profile"] -DEFAULT_TIMEOUT = 5 -EVENT_AUTOMATIC_UPDATE = "automatic_update" - -FULL_SCOPE = DEFAULT_SCOPE + ["current_location"] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_SECRET): cv.string, - vol.Optional(CONF_CURRENT_LOCATION, default=False): cv.boolean, - vol.Optional(CONF_DEVICES): vol.All(cv.ensure_list, [cv.string]), - } -) - - -def _get_refresh_token_from_file(hass, filename): - """Attempt to load session data from file.""" - path = hass.config.path(filename) - - if not os.path.isfile(path): - return None - - try: - with open(path) as data_file: - data = json.load(data_file) - if data is None: - return None - - return data.get(DATA_REFRESH_TOKEN) - except ValueError: - return None - - -def _write_refresh_token_to_file(hass, filename, refresh_token): - """Attempt to store session data to file.""" - path = hass.config.path(filename) - - os.makedirs(os.path.dirname(path), exist_ok=True) - with open(path, "w+") as data_file: - json.dump({DATA_REFRESH_TOKEN: refresh_token}, data_file) - - -@asyncio.coroutine -def async_setup_scanner(hass, config, async_see, discovery_info=None): - """Validate the configuration and return an Automatic scanner.""" - - hass.http.register_view(AutomaticAuthCallbackView()) - - scope = FULL_SCOPE if config.get(CONF_CURRENT_LOCATION) else DEFAULT_SCOPE - - client = aioautomatic.Client( - client_id=config[CONF_CLIENT_ID], - client_secret=config[CONF_SECRET], - client_session=async_get_clientsession(hass), - request_kwargs={"timeout": DEFAULT_TIMEOUT}, - ) - - filename = f".automatic/session-{config[CONF_CLIENT_ID]}.json" - refresh_token = yield from hass.async_add_job( - _get_refresh_token_from_file, hass, filename - ) - - @asyncio.coroutine - def initialize_data(session): - """Initialize the AutomaticData object from the created session.""" - hass.async_add_job( - _write_refresh_token_to_file, hass, filename, session.refresh_token - ) - data = AutomaticData(hass, client, session, config.get(CONF_DEVICES), async_see) - - # Load the initial vehicle data - vehicles = yield from session.get_vehicles() - for vehicle in vehicles: - hass.async_create_task(data.load_vehicle(vehicle)) - - # Create a task instead of adding a tracking job, since this task will - # run until the websocket connection is closed. - hass.loop.create_task(data.ws_connect()) - - if refresh_token is not None: - try: - session = yield from client.create_session_from_refresh_token(refresh_token) - yield from initialize_data(session) - return True - except aioautomatic.exceptions.AutomaticError as err: - _LOGGER.error(str(err)) - - configurator = hass.components.configurator - request_id = configurator.async_request_config( - "Automatic", - description=("Authorization required for Automatic device tracker."), - link_name="Click here to authorize Home Assistant.", - link_url=client.generate_oauth_url(scope), - entity_picture="/static/images/logo_automatic.png", - ) - - @asyncio.coroutine - def initialize_callback(code, state): - """Call after OAuth2 response is returned.""" - try: - session = yield from client.create_session_from_oauth_code(code, state) - yield from initialize_data(session) - configurator.async_request_done(request_id) - except aioautomatic.exceptions.AutomaticError as err: - _LOGGER.error(str(err)) - configurator.async_notify_errors(request_id, str(err)) - return False - - if DATA_CONFIGURING not in hass.data: - hass.data[DATA_CONFIGURING] = {} - - hass.data[DATA_CONFIGURING][client.state] = initialize_callback - return True - - -class AutomaticAuthCallbackView(HomeAssistantView): - """Handle OAuth finish callback requests.""" - - requires_auth = False - url = "/api/automatic/callback" - name = "api:automatic:callback" - - @callback - def get(self, request): # pylint: disable=no-self-use - """Finish OAuth callback request.""" - hass = request.app["hass"] - params = request.query - response = web.HTTPFound("/lovelace") - - if "state" not in params or "code" not in params: - if "error" in params: - _LOGGER.error("Error authorizing Automatic: %s", params["error"]) - return response - _LOGGER.error("Error authorizing Automatic. Invalid response returned") - return response - - if ( - DATA_CONFIGURING not in hass.data - or params["state"] not in hass.data[DATA_CONFIGURING] - ): - _LOGGER.error("Automatic configuration request not found") - return response - - code = params["code"] - state = params["state"] - initialize_callback = hass.data[DATA_CONFIGURING][state] - hass.async_create_task(initialize_callback(code, state)) - - return response - - -class AutomaticData: - """A class representing an Automatic cloud service connection.""" - - def __init__(self, hass, client, session, devices, async_see): - """Initialize the automatic device scanner.""" - self.hass = hass - self.devices = devices - self.vehicle_info = {} - self.vehicle_seen = {} - self.client = client - self.session = session - self.async_see = async_see - self.ws_reconnect_handle = None - self.ws_close_requested = False - - self.client.on_app_event( - lambda name, event: self.hass.async_create_task( - self.handle_event(name, event) - ) - ) - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.ws_close()) - - @asyncio.coroutine - def handle_event(self, name, event): - """Coroutine to update state for a real time event.""" - - self.hass.bus.async_fire(EVENT_AUTOMATIC_UPDATE, event.data) - - if event.vehicle.id not in self.vehicle_info: - # If vehicle hasn't been seen yet, request the detailed - # info for this vehicle. - _LOGGER.info("New vehicle found") - try: - vehicle = yield from event.get_vehicle() - except aioautomatic.exceptions.AutomaticError as err: - _LOGGER.error(str(err)) - return - yield from self.get_vehicle_info(vehicle) - - if event.created_at < self.vehicle_seen[event.vehicle.id]: - # Skip events received out of order - _LOGGER.debug( - "Skipping out of order event. Event Created %s. Last seen event: %s", - event.created_at, - self.vehicle_seen[event.vehicle.id], - ) - return - self.vehicle_seen[event.vehicle.id] = event.created_at - - kwargs = self.vehicle_info[event.vehicle.id] - if kwargs is None: - # Ignored device - return - - # If this is a vehicle status report, update the fuel level - if name == "vehicle:status_report": - fuel_level = event.vehicle.fuel_level_percent - if fuel_level is not None: - kwargs[ATTR_ATTRIBUTES][ATTR_FUEL_LEVEL] = fuel_level - - # Send the device seen notification - if event.location is not None: - kwargs[ATTR_GPS] = (event.location.lat, event.location.lon) - kwargs[ATTR_GPS_ACCURACY] = event.location.accuracy_m - - yield from self.async_see(**kwargs) - - @asyncio.coroutine - def ws_connect(self, now=None): - """Open the websocket connection.""" - - self.ws_close_requested = False - - if self.ws_reconnect_handle is not None: - _LOGGER.debug("Retrying websocket connection") - try: - ws_loop_future = yield from self.client.ws_connect() - except aioautomatic.exceptions.UnauthorizedClientError: - _LOGGER.error( - "Client unauthorized for websocket connection. " - "Ensure Websocket is selected in the Automatic " - "developer application event delivery preferences" - ) - return - except aioautomatic.exceptions.AutomaticError as err: - if self.ws_reconnect_handle is None: - # Show log error and retry connection every 5 minutes - _LOGGER.error("Error opening websocket connection: %s", err) - self.ws_reconnect_handle = async_track_time_interval( - self.hass, self.ws_connect, timedelta(minutes=5) - ) - return - - if self.ws_reconnect_handle is not None: - self.ws_reconnect_handle() - self.ws_reconnect_handle = None - - _LOGGER.info("Websocket connected") - - try: - yield from ws_loop_future - except aioautomatic.exceptions.AutomaticError as err: - _LOGGER.error(str(err)) - - _LOGGER.info("Websocket closed") - - # If websocket was close was not requested, attempt to reconnect - if not self.ws_close_requested: - self.hass.loop.create_task(self.ws_connect()) - - @asyncio.coroutine - def ws_close(self): - """Close the websocket connection.""" - self.ws_close_requested = True - if self.ws_reconnect_handle is not None: - self.ws_reconnect_handle() - self.ws_reconnect_handle = None - - yield from self.client.ws_close() - - @asyncio.coroutine - def load_vehicle(self, vehicle): - """Load the vehicle's initial state and update hass.""" - kwargs = yield from self.get_vehicle_info(vehicle) - yield from self.async_see(**kwargs) - - @asyncio.coroutine - def get_vehicle_info(self, vehicle): - """Fetch the latest vehicle info from automatic.""" - - name = vehicle.display_name - if name is None: - name = " ".join( - filter(None, (str(vehicle.year), vehicle.make, vehicle.model)) - ) - - if self.devices is not None and name not in self.devices: - self.vehicle_info[vehicle.id] = None - return - - self.vehicle_info[vehicle.id] = kwargs = { - ATTR_DEV_ID: vehicle.id, - ATTR_HOST_NAME: name, - ATTR_MAC: vehicle.id, - ATTR_ATTRIBUTES: {ATTR_FUEL_LEVEL: vehicle.fuel_level_percent}, - } - self.vehicle_seen[vehicle.id] = vehicle.updated_at or vehicle.created_at - - if vehicle.latest_location is not None: - location = vehicle.latest_location - kwargs[ATTR_GPS] = (location.lat, location.lon) - kwargs[ATTR_GPS_ACCURACY] = location.accuracy_m - return kwargs - - trips = [] - try: - # Get the most recent trip for this vehicle - trips = yield from self.session.get_trips(vehicle=vehicle.id, limit=1) - except aioautomatic.exceptions.AutomaticError as err: - _LOGGER.error(str(err)) - - if trips: - location = trips[0].end_location - kwargs[ATTR_GPS] = (location.lat, location.lon) - kwargs[ATTR_GPS_ACCURACY] = location.accuracy_m - - if trips[0].ended_at >= self.vehicle_seen[vehicle.id]: - self.vehicle_seen[vehicle.id] = trips[0].ended_at - - return kwargs diff --git a/homeassistant/components/automatic/manifest.json b/homeassistant/components/automatic/manifest.json deleted file mode 100644 index e0d06ff0f1f..00000000000 --- a/homeassistant/components/automatic/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "automatic", - "name": "Automatic", - "documentation": "https://www.home-assistant.io/integrations/automatic", - "requirements": ["aioautomatic==0.6.5"], - "dependencies": ["configurator", "http"], - "codeowners": ["@armills"] -} diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index e5b66594d2f..8b2c036034b 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -14,7 +14,6 @@ from homeassistant.const import ( CONF_ID, CONF_PLATFORM, CONF_ZONE, - EVENT_AUTOMATION_TRIGGERED, EVENT_HOMEASSISTANT_STARTED, SERVICE_RELOAD, SERVICE_TOGGLE, @@ -61,6 +60,9 @@ CONDITION_TYPE_OR = "or" DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND DEFAULT_INITIAL_STATE = True +EVENT_AUTOMATION_RELOADED = "automation_reloaded" +EVENT_AUTOMATION_TRIGGERED = "automation_triggered" + ATTR_LAST_TRIGGERED = "last_triggered" ATTR_VARIABLES = "variables" SERVICE_TRIGGER = "trigger" @@ -214,11 +216,25 @@ async def async_setup(hass, config): if conf is None: return await _async_process_config(hass, conf, component) + hass.bus.async_fire(EVENT_AUTOMATION_RELOADED, context=service_call.context) async_register_admin_service( hass, DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=vol.Schema({}) ) + @callback + def async_describe_logbook_event(event): + """Describe a logbook event.""" + return { + "name": event.data.get(ATTR_NAME), + "message": "has been triggered", + "entity_id": event.data.get(ATTR_ENTITY_ID), + } + + hass.components.logbook.async_describe_event( + DOMAIN, EVENT_AUTOMATION_TRIGGERED, async_describe_logbook_event + ) + return True @@ -373,6 +389,8 @@ class AutomationEntity(ToggleEntity, RestoreEntity): trigger_context = Context(parent_id=parent_id) self.async_set_context(trigger_context) + self._last_triggered = utcnow() + self.async_write_ha_state() self.hass.bus.async_fire( EVENT_AUTOMATION_TRIGGERED, {ATTR_NAME: self._name, ATTR_ENTITY_ID: self.entity_id}, @@ -386,9 +404,6 @@ class AutomationEntity(ToggleEntity, RestoreEntity): except Exception: # pylint: disable=broad-except pass - self._last_triggered = utcnow() - self.async_write_ha_state() - async def async_will_remove_from_hass(self): """Remove listeners when removing automation from Home Assistant.""" await super().async_will_remove_from_hass() diff --git a/homeassistant/components/automation/manifest.json b/homeassistant/components/automation/manifest.json index 1b5fad1b588..a93baa0528a 100644 --- a/homeassistant/components/automation/manifest.json +++ b/homeassistant/components/automation/manifest.json @@ -2,7 +2,7 @@ "domain": "automation", "name": "Automation", "documentation": "https://www.home-assistant.io/integrations/automation", - "after_dependencies": ["device_automation", "webhook"], + "after_dependencies": ["device_automation", "logbook", "webhook"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/automation/translations/ca.json b/homeassistant/components/automation/translations/ca.json index d138d6da6e5..c1d35331e2b 100644 --- a/homeassistant/components/automation/translations/ca.json +++ b/homeassistant/components/automation/translations/ca.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "Desactivat", - "on": "Activat" + "off": "OFF", + "on": "ON" } }, "title": "Automatitzaci\u00f3" diff --git a/homeassistant/components/axis/__init__.py b/homeassistant/components/axis/__init__.py index 5294e30ed6f..eedeac01366 100644 --- a/homeassistant/components/axis/__init__.py +++ b/homeassistant/components/axis/__init__.py @@ -2,19 +2,10 @@ 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 homeassistant.const import CONF_DEVICE, EVENT_HOMEASSISTANT_STOP -from .const import CONF_CAMERA, CONF_EVENTS, DEFAULT_TRIGGER_TIME, DOMAIN -from .device import AxisNetworkDevice, get_device +from .const import DOMAIN as AXIS_DOMAIN +from .device import AxisNetworkDevice LOGGER = logging.getLogger(__name__) @@ -26,11 +17,7 @@ async def async_setup(hass, config): async def async_setup_entry(hass, config_entry): """Set up the Axis component.""" - if DOMAIN not in hass.data: - hass.data[DOMAIN] = {} - - if not config_entry.options: - await async_populate_options(hass, config_entry) + hass.data.setdefault(AXIS_DOMAIN, {}) device = AxisNetworkDevice(hass, config_entry) @@ -40,10 +27,10 @@ async def async_setup_entry(hass, config_entry): # 0.104 introduced config entry unique id, this makes upgrading possible if config_entry.unique_id is None: hass.config_entries.async_update_entry( - config_entry, unique_id=device.api.vapix.params.system_serialnumber + config_entry, unique_id=device.api.vapix.serial_number ) - hass.data[DOMAIN][config_entry.unique_id] = device + hass.data[AXIS_DOMAIN][config_entry.unique_id] = device await device.async_update_device_registry() @@ -54,32 +41,10 @@ async def async_setup_entry(hass, config_entry): async def async_unload_entry(hass, config_entry): """Unload Axis device config entry.""" - device = hass.data[DOMAIN].pop(config_entry.data[CONF_MAC]) + device = hass.data[AXIS_DOMAIN].pop(config_entry.unique_id) return await device.async_reset() -async def async_populate_options(hass, config_entry): - """Populate default options for 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) - - options = { - CONF_CAMERA: camera, - CONF_EVENTS: True, - CONF_TRIGGER_TIME: DEFAULT_TRIGGER_TIME, - } - - 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) diff --git a/homeassistant/components/axis/axis_base.py b/homeassistant/components/axis/axis_base.py index 2e848168b49..976e779c20e 100644 --- a/homeassistant/components/axis/axis_base.py +++ b/homeassistant/components/axis/axis_base.py @@ -18,7 +18,7 @@ class AxisEntityBase(Entity): """Subscribe device events.""" self.async_on_remove( async_dispatcher_connect( - self.hass, self.device.event_reachable, self.update_callback + self.hass, self.device.signal_reachable, self.update_callback ) ) @@ -49,15 +49,12 @@ class AxisEventBase(AxisEntityBase): async def async_added_to_hass(self) -> None: """Subscribe sensors events.""" self.event.register_callback(self.update_callback) - await super().async_added_to_hass() async def async_will_remove_from_hass(self) -> None: """Disconnect device object when removed.""" self.event.remove_callback(self.update_callback) - await super().async_will_remove_from_hass() - @property def device_class(self): """Return the class of the event.""" diff --git a/homeassistant/components/axis/binary_sensor.py b/homeassistant/components/axis/binary_sensor.py index 4709d706ad0..c9e8436fdeb 100644 --- a/homeassistant/components/axis/binary_sensor.py +++ b/homeassistant/components/axis/binary_sensor.py @@ -2,10 +2,21 @@ from datetime import timedelta -from axis.event_stream import CLASS_INPUT, CLASS_OUTPUT +from axis.event_stream import ( + CLASS_INPUT, + CLASS_LIGHT, + CLASS_MOTION, + CLASS_OUTPUT, + CLASS_SOUND, +) -from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.const import CONF_TRIGGER_TIME +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_LIGHT, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_SOUND, + BinarySensorEntity, +) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import async_track_point_in_utc_time @@ -14,6 +25,13 @@ from homeassistant.util.dt import utcnow from .axis_base import AxisEventBase from .const import DOMAIN as AXIS_DOMAIN +DEVICE_CLASS = { + CLASS_INPUT: DEVICE_CLASS_CONNECTIVITY, + CLASS_LIGHT: DEVICE_CLASS_LIGHT, + CLASS_MOTION: DEVICE_CLASS_MOTION, + CLASS_SOUND: DEVICE_CLASS_SOUND, +} + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a Axis binary sensor.""" @@ -22,13 +40,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def async_add_sensor(event_id): """Add binary sensor from Axis device.""" - event = device.api.event.events[event_id] + event = device.api.event[event_id] if event.CLASS != CLASS_OUTPUT: async_add_entities([AxisBinarySensor(event, device)], True) device.listeners.append( - async_dispatcher_connect(hass, device.event_new_sensor, async_add_sensor) + async_dispatcher_connect(hass, device.signal_new_event, async_add_sensor) ) @@ -38,7 +56,7 @@ class AxisBinarySensor(AxisEventBase, BinarySensorEntity): def __init__(self, event, device): """Initialize the Axis binary sensor.""" super().__init__(event, device) - self.remove_timer = None + self.cancel_scheduled_update = None @callback def update_callback(self, no_delay=False): @@ -46,24 +64,25 @@ class AxisBinarySensor(AxisEventBase, BinarySensorEntity): Parameter no_delay is True when device_event_reachable is sent. """ - delay = self.device.config_entry.options[CONF_TRIGGER_TIME] - if self.remove_timer is not None: - self.remove_timer() - self.remove_timer = None + @callback + def scheduled_update(now): + """Timer callback for sensor update.""" + self.cancel_scheduled_update = None + self.async_write_ha_state() - if self.is_on or delay == 0 or no_delay: + if self.cancel_scheduled_update is not None: + self.cancel_scheduled_update() + self.cancel_scheduled_update = None + + if self.is_on or self.device.option_trigger_time == 0 or no_delay: self.async_write_ha_state() return - @callback - def _delay_update(now): - """Timer callback for sensor update.""" - self.async_write_ha_state() - self.remove_timer = None - - self.remove_timer = async_track_point_in_utc_time( - self.hass, _delay_update, utcnow() + timedelta(seconds=delay) + self.cancel_scheduled_update = async_track_point_in_utc_time( + self.hass, + scheduled_update, + utcnow() + timedelta(seconds=self.device.option_trigger_time), ) @property @@ -84,3 +103,8 @@ class AxisBinarySensor(AxisEventBase, BinarySensorEntity): ) return super().name + + @property + def device_class(self): + """Return the class of the sensor.""" + return DEVICE_CLASS.get(self.event.CLASS) diff --git a/homeassistant/components/axis/camera.py b/homeassistant/components/axis/camera.py index ca76552a4cc..8e7e4592cb6 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_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, @@ -19,11 +18,7 @@ from homeassistant.const import ( from homeassistant.helpers.dispatcher import async_dispatcher_connect from .axis_base import AxisEntityBase -from .const import DOMAIN as AXIS_DOMAIN - -AXIS_IMAGE = "http://{host}:{port}/axis-cgi/jpg/image.cgi" -AXIS_VIDEO = "http://{host}:{port}/axis-cgi/mjpg/video.cgi" -AXIS_STREAM = "rtsp://{user}:{password}@{host}/axis-media/media.amp?videocodec=h264" +from .const import DEFAULT_STREAM_PROFILE, DOMAIN as AXIS_DOMAIN async def async_setup_entry(hass, config_entry, async_add_entities): @@ -32,34 +27,34 @@ async def async_setup_entry(hass, config_entry, async_add_entities): device = hass.data[AXIS_DOMAIN][config_entry.unique_id] - config = { - CONF_NAME: config_entry.data[CONF_NAME], - CONF_USERNAME: config_entry.data[CONF_USERNAME], - CONF_PASSWORD: config_entry.data[CONF_PASSWORD], - CONF_MJPEG_URL: AXIS_VIDEO.format( - host=config_entry.data[CONF_HOST], port=config_entry.data[CONF_PORT], - ), - CONF_STILL_IMAGE_URL: AXIS_IMAGE.format( - host=config_entry.data[CONF_HOST], port=config_entry.data[CONF_PORT], - ), - CONF_AUTHENTICATION: HTTP_DIGEST_AUTHENTICATION, - } - async_add_entities([AxisCamera(config, device)]) + if not device.option_camera: + return + + async_add_entities([AxisCamera(device)]) class AxisCamera(AxisEntityBase, MjpegCamera): """Representation of a Axis camera.""" - def __init__(self, config, device): + def __init__(self, device): """Initialize Axis Communications camera component.""" AxisEntityBase.__init__(self, device) + + config = { + CONF_NAME: device.config_entry.data[CONF_NAME], + CONF_USERNAME: device.config_entry.data[CONF_USERNAME], + CONF_PASSWORD: device.config_entry.data[CONF_PASSWORD], + CONF_MJPEG_URL: self.mjpeg_source, + CONF_STILL_IMAGE_URL: self.image_source, + CONF_AUTHENTICATION: HTTP_DIGEST_AUTHENTICATION, + } MjpegCamera.__init__(self, config) async def async_added_to_hass(self): """Subscribe camera events.""" self.async_on_remove( async_dispatcher_connect( - self.hass, self.device.event_new_address, self._new_address + self.hass, self.device.signal_new_address, self._new_address ) ) @@ -70,21 +65,34 @@ class AxisCamera(AxisEntityBase, MjpegCamera): """Return supported features.""" return SUPPORT_STREAM - async def stream_source(self): - """Return the stream source.""" - return AXIS_STREAM.format( - user=self.device.config_entry.data[CONF_USERNAME], - password=self.device.config_entry.data[CONF_PASSWORD], - host=self.device.host, - ) - def _new_address(self): """Set new device address for video stream.""" - port = self.device.config_entry.data[CONF_PORT] - self._mjpeg_url = AXIS_VIDEO.format(host=self.device.host, port=port) - self._still_image_url = AXIS_IMAGE.format(host=self.device.host, port=port) + self._mjpeg_url = self.mjpeg_source + self._still_image_url = self.image_source @property def unique_id(self): """Return a unique identifier for this device.""" return f"{self.device.serial}-camera" + + @property + def image_source(self): + """Return still image URL for device.""" + return f"http://{self.device.host}:{self.device.config_entry.data[CONF_PORT]}/axis-cgi/jpg/image.cgi" + + @property + def mjpeg_source(self): + """Return mjpeg URL for device.""" + options = "" + if self.device.option_stream_profile != DEFAULT_STREAM_PROFILE: + options = f"?&streamprofile={self.device.option_stream_profile}" + + return f"http://{self.device.host}:{self.device.config_entry.data[CONF_PORT]}/axis-cgi/mjpg/video.cgi{options}" + + async def stream_source(self): + """Return the stream source.""" + options = "" + if self.device.option_stream_profile != DEFAULT_STREAM_PROFILE: + options = f"&streamprofile={self.device.option_stream_profile}" + + return f"rtsp://{self.device.config_entry.data[CONF_USERNAME]}:{self.device.config_entry.data[CONF_PASSWORD]}@{self.device.host}/axis-media/media.amp?videocodec=h264{options}" diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 37141d6017a..5b300fe323b 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -13,9 +13,15 @@ from homeassistant.const import ( CONF_PORT, CONF_USERNAME, ) +from homeassistant.core import callback from homeassistant.util.network import is_link_local -from .const import CONF_MODEL, DOMAIN +from .const import ( + CONF_MODEL, + CONF_STREAM_PROFILE, + DEFAULT_STREAM_PROFILE, + DOMAIN as AXIS_DOMAIN, +) from .device import get_device from .errors import AuthenticationRequired, CannotConnect @@ -32,12 +38,18 @@ AXIS_INCLUDE = EVENT_TYPES + PLATFORMS DEFAULT_PORT = 80 -class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): """Handle a Axis config flow.""" VERSION = 2 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return AxisOptionsFlowHandler(config_entry) + def __init__(self): """Initialize the Axis config flow.""" self.device_config = {} @@ -61,8 +73,7 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): password=user_input[CONF_PASSWORD], ) - serial_number = device.vapix.params.system_serialnumber - await self.async_set_unique_id(serial_number) + await self.async_set_unique_id(device.vapix.serial_number) self._abort_if_unique_id_configured( updates={ @@ -76,8 +87,8 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): 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, + CONF_MAC: device.vapix.serial_number, + CONF_MODEL: device.vapix.product_number, } return await self._create_entry() @@ -110,7 +121,7 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): model = self.device_config[CONF_MODEL] same_model = [ entry.data[CONF_NAME] - for entry in self.hass.config_entries.async_entries(DOMAIN) + for entry in self.hass.config_entries.async_entries(AXIS_DOMAIN) if entry.data[CONF_MODEL] == model ] @@ -158,3 +169,39 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): } return await self.async_step_user() + + +class AxisOptionsFlowHandler(config_entries.OptionsFlow): + """Handle Axis device options.""" + + def __init__(self, config_entry): + """Initialize Axis device options flow.""" + self.config_entry = config_entry + self.options = dict(config_entry.options) + self.device = None + + async def async_step_init(self, user_input=None): + """Manage the Axis device options.""" + self.device = self.hass.data[AXIS_DOMAIN][self.config_entry.unique_id] + return await self.async_step_configure_stream() + + async def async_step_configure_stream(self, user_input=None): + """Manage the Axis device options.""" + if user_input is not None: + self.options.update(user_input) + return self.async_create_entry(title="", data=self.options) + + profiles = [DEFAULT_STREAM_PROFILE] + for profile in self.device.api.vapix.streaming_profiles: + profiles.append(profile.name) + + return self.async_show_form( + step_id="configure_stream", + data_schema=vol.Schema( + { + vol.Optional( + CONF_STREAM_PROFILE, default=self.device.option_stream_profile + ): vol.In(profiles) + } + ), + ) diff --git a/homeassistant/components/axis/const.py b/homeassistant/components/axis/const.py index 7f0fd9c8947..203bbdf94c7 100644 --- a/homeassistant/components/axis/const.py +++ b/homeassistant/components/axis/const.py @@ -1,12 +1,23 @@ """Constants for the Axis component.""" import logging +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN + LOGGER = logging.getLogger(__package__) DOMAIN = "axis" +ATTR_MANUFACTURER = "Axis Communications AB" + CONF_CAMERA = "camera" CONF_EVENTS = "events" CONF_MODEL = "model" +CONF_STREAM_PROFILE = "stream_profile" +DEFAULT_EVENTS = True +DEFAULT_STREAM_PROFILE = "No stream profile" DEFAULT_TRIGGER_TIME = 0 + +PLATFORMS = [BINARY_SENSOR_DOMAIN, CAMERA_DOMAIN, SWITCH_DOMAIN] diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index a204136e018..69cab856516 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -4,21 +4,41 @@ import asyncio import async_timeout import axis -from axis.streammanager import SIGNAL_PLAYING +from axis.configuration import Configuration +from axis.event_stream import OPERATION_INITIALIZED +from axis.mqtt import mqtt_json_to_event +from axis.streammanager import SIGNAL_PLAYING, STATE_STOPPED +from homeassistant.components import mqtt +from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN +from homeassistant.components.mqtt.models import Message from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, + CONF_TRIGGER_TIME, CONF_USERNAME, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.setup import async_when_setup -from .const import CONF_CAMERA, CONF_EVENTS, CONF_MODEL, DOMAIN, LOGGER +from .const import ( + ATTR_MANUFACTURER, + CONF_CAMERA, + CONF_EVENTS, + CONF_MODEL, + CONF_STREAM_PROFILE, + DEFAULT_EVENTS, + DEFAULT_STREAM_PROFILE, + DEFAULT_TRIGGER_TIME, + DOMAIN as AXIS_DOMAIN, + LOGGER, + PLATFORMS, +) from .errors import AuthenticationRequired, CannotConnect @@ -57,19 +77,106 @@ class AxisNetworkDevice: """Return the serial number of this device.""" return self.config_entry.unique_id + @property + def option_camera(self): + """Config entry option defining if camera should be used.""" + supported_formats = self.api.vapix.params.image_format + return self.config_entry.options.get(CONF_CAMERA, bool(supported_formats)) + + @property + def option_events(self): + """Config entry option defining if platforms based on events should be created.""" + return self.config_entry.options.get(CONF_EVENTS, DEFAULT_EVENTS) + + @property + def option_stream_profile(self): + """Config entry option defining what stream profile camera platform should use.""" + return self.config_entry.options.get( + CONF_STREAM_PROFILE, DEFAULT_STREAM_PROFILE + ) + + @property + def option_trigger_time(self): + """Config entry option defining minimum number of seconds to keep trigger high.""" + return self.config_entry.options.get(CONF_TRIGGER_TIME, DEFAULT_TRIGGER_TIME) + + @property + def signal_reachable(self): + """Device specific event to signal a change in connection status.""" + return f"axis_reachable_{self.serial}" + + @property + def signal_new_event(self): + """Device specific event to signal new device event available.""" + return f"axis_new_event_{self.serial}" + + @property + def signal_new_address(self): + """Device specific event to signal a change in device address.""" + return f"axis_new_address_{self.serial}" + + @callback + def async_connection_status_callback(self, status): + """Handle signals of device connection status. + + This is called on every RTSP keep-alive message. + Only signal state change if state change is true. + """ + + if self.available != (status == SIGNAL_PLAYING): + self.available = not self.available + async_dispatcher_send(self.hass, self.signal_reachable, True) + + @callback + def async_event_callback(self, action, event_id): + """Call to configure events when initialized on event stream.""" + if action == OPERATION_INITIALIZED: + async_dispatcher_send(self.hass, self.signal_new_event, event_id) + + @staticmethod + async def async_new_address_callback(hass, entry): + """Handle signals of device getting new address. + + Called when config entry is updated. + This is a static method because a class method (bound method), + can not be used with weak references. + """ + device = hass.data[AXIS_DOMAIN][entry.unique_id] + device.api.config.host = device.host + async_dispatcher_send(hass, device.signal_new_address) + async def async_update_device_registry(self): """Update device registry.""" device_registry = await self.hass.helpers.device_registry.async_get_registry() device_registry.async_get_or_create( config_entry_id=self.config_entry.entry_id, connections={(CONNECTION_NETWORK_MAC, self.serial)}, - identifiers={(DOMAIN, self.serial)}, - manufacturer="Axis Communications AB", + identifiers={(AXIS_DOMAIN, self.serial)}, + manufacturer=ATTR_MANUFACTURER, model=f"{self.model} {self.product_type}", name=self.name, sw_version=self.fw_version, ) + async def use_mqtt(self, hass: HomeAssistant, component: str) -> None: + """Set up to use MQTT.""" + status = await hass.async_add_executor_job( + self.api.vapix.mqtt.get_client_status + ) + + if status.get("data", {}).get("status", {}).get("state") == "active": + self.listeners.append( + await mqtt.async_subscribe(hass, f"{self.serial}/#", self.mqtt_message) + ) + + @callback + def mqtt_message(self, message: Message) -> None: + """Receive Axis MQTT message.""" + self.disconnect_from_stream() + + event = mqtt_json_to_event(message.payload) + self.api.event.process_event(event) + async def async_setup(self): """Set up the device.""" try: @@ -88,115 +195,67 @@ class AxisNetworkDevice: LOGGER.error("Unknown error connecting with Axis device on %s", self.host) return False - self.fw_version = self.api.vapix.params.firmware_version - self.product_type = self.api.vapix.params.prodtype + self.fw_version = self.api.vapix.firmware_version + self.product_type = self.api.vapix.product_type - if self.config_entry.options[CONF_CAMERA]: - - self.hass.async_create_task( - self.hass.config_entries.async_forward_entry_setup( - self.config_entry, "camera" - ) + async def start_platforms(): + await asyncio.gather( + *[ + self.hass.config_entries.async_forward_entry_setup( + self.config_entry, platform + ) + for platform in PLATFORMS + ] ) - - if self.config_entry.options[CONF_EVENTS]: - - self.api.stream.connection_status_callback = ( - self.async_connection_status_callback - ) - self.api.enable_events(event_callback=self.async_event_callback) - - platform_tasks = [ - self.hass.config_entries.async_forward_entry_setup( - self.config_entry, platform + if self.option_events: + self.api.stream.connection_status_callback.append( + self.async_connection_status_callback ) - for platform in ["binary_sensor", "switch"] - ] - self.hass.async_create_task(self.start(platform_tasks)) + self.api.enable_events(event_callback=self.async_event_callback) + self.api.stream.start() + + if self.api.vapix.mqtt: + async_when_setup(self.hass, MQTT_DOMAIN, self.use_mqtt) + + self.hass.async_create_task(start_platforms()) self.config_entry.add_update_listener(self.async_new_address_callback) return True - @property - def event_new_address(self): - """Device specific event to signal new device address.""" - return f"axis_new_address_{self.serial}" - - @staticmethod - async def async_new_address_callback(hass, entry): - """Handle signals of device getting new address. - - This is a static method because a class method (bound method), - can not be used with weak references. - """ - device = hass.data[DOMAIN][entry.unique_id] - device.api.config.host = device.host - async_dispatcher_send(hass, device.event_new_address) - - @property - def event_reachable(self): - """Device specific event to signal a change in connection status.""" - return f"axis_reachable_{self.serial}" - @callback - def async_connection_status_callback(self, status): - """Handle signals of device connection status. - - This is called on every RTSP keep-alive message. - Only signal state change if state change is true. - """ - - if self.available != (status == SIGNAL_PLAYING): - self.available = not self.available - async_dispatcher_send(self.hass, self.event_reachable, True) - - @property - def event_new_sensor(self): - """Device specific event to signal new sensor available.""" - return f"axis_add_sensor_{self.serial}" - - @callback - def async_event_callback(self, action, event_id): - """Call to configure events when initialized on event stream.""" - if action == "add": - async_dispatcher_send(self.hass, self.event_new_sensor, event_id) - - async def start(self, platform_tasks): - """Start the event stream when all platforms are loaded.""" - await asyncio.gather(*platform_tasks) - self.api.start() + def disconnect_from_stream(self): + """Stop stream.""" + if self.api.stream.state != STATE_STOPPED: + self.api.stream.connection_status_callback.remove( + self.async_connection_status_callback + ) + self.api.stream.stop() @callback def shutdown(self, event): """Stop the event stream.""" - self.api.stop() + self.disconnect_from_stream() async def async_reset(self): """Reset this device to default state.""" - platform_tasks = [] + self.disconnect_from_stream() - if self.config_entry.options[CONF_CAMERA]: - platform_tasks.append( - self.hass.config_entries.async_forward_entry_unload( - self.config_entry, "camera" - ) + unload_ok = all( + await asyncio.gather( + *[ + self.hass.config_entries.async_forward_entry_unload( + self.config_entry, platform + ) + for platform in PLATFORMS + ] ) + ) + if not unload_ok: + return False - if self.config_entry.options[CONF_EVENTS]: - self.api.stop() - platform_tasks += [ - self.hass.config_entries.async_forward_entry_unload( - self.config_entry, platform - ) - for platform in ["binary_sensor", "switch"] - ] - - await asyncio.gather(*platform_tasks) - - for unsub_dispatcher in self.listeners: - unsub_dispatcher() - self.listeners = [] + for unsubscribe_listener in self.listeners: + unsubscribe_listener() return True @@ -205,25 +264,12 @@ async def get_device(hass, host, port, username, password): """Create a Axis device.""" device = axis.AxisDevice( - loop=hass.loop, - host=host, - port=port, - username=username, - password=password, - web_proto="http", + Configuration(host, port=port, username=username, password=password) ) - device.vapix.initialize_params(preload_data=False) - device.vapix.initialize_ports() - try: with async_timeout.timeout(15): - - await asyncio.gather( - hass.async_add_executor_job(device.vapix.params.update_brand), - hass.async_add_executor_job(device.vapix.params.update_properties), - hass.async_add_executor_job(device.vapix.ports.update), - ) + await hass.async_add_executor_job(device.vapix.initialize) return device diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index 8a2530b2022..f0d33fb4159 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -3,7 +3,8 @@ "name": "Axis", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/axis", - "requirements": ["axis==25"], + "requirements": ["axis==29"], "zeroconf": ["_axis-video._tcp.local."], + "after_dependencies": ["mqtt"], "codeowners": ["@Kane610"] } diff --git a/homeassistant/components/axis/strings.json b/homeassistant/components/axis/strings.json index 88b117a9802..672bfe141b9 100644 --- a/homeassistant/components/axis/strings.json +++ b/homeassistant/components/axis/strings.json @@ -24,5 +24,15 @@ "link_local_address": "Link local addresses are not supported", "not_axis_device": "Discovered device not an Axis device" } + }, + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "Select stream profile to use" + }, + "title": "Axis device video stream options" + } + } } -} \ No newline at end of file +} diff --git a/homeassistant/components/axis/switch.py b/homeassistant/components/axis/switch.py index be048a510ed..256a22db114 100644 --- a/homeassistant/components/axis/switch.py +++ b/homeassistant/components/axis/switch.py @@ -17,13 +17,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def async_add_switch(event_id): """Add switch from Axis device.""" - event = device.api.event.events[event_id] + event = device.api.event[event_id] if event.CLASS == CLASS_OUTPUT: async_add_entities([AxisSwitch(event, device)], True) device.listeners.append( - async_dispatcher_connect(hass, device.event_new_sensor, async_add_switch) + async_dispatcher_connect(hass, device.signal_new_event, async_add_switch) ) @@ -37,16 +37,14 @@ class AxisSwitch(AxisEventBase, SwitchEntity): async def async_turn_on(self, **kwargs): """Turn on switch.""" - action = "/" await self.hass.async_add_executor_job( - self.device.api.vapix.ports[self.event.id].action, action + self.device.api.vapix.ports[self.event.id].close ) async def async_turn_off(self, **kwargs): """Turn off switch.""" - action = "\\" await self.hass.async_add_executor_job( - self.device.api.vapix.ports[self.event.id].action, action + self.device.api.vapix.ports[self.event.id].open ) @property diff --git a/homeassistant/components/axis/translations/bg.json b/homeassistant/components/axis/translations/bg.json index 83fbfa0118c..d5bf9373112 100644 --- a/homeassistant/components/axis/translations/bg.json +++ b/homeassistant/components/axis/translations/bg.json @@ -24,6 +24,5 @@ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0432\u0430\u043d\u0435 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043e\u0442 Axis" } } - }, - "title": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Axis" + } } \ No newline at end of file diff --git a/homeassistant/components/axis/translations/ca.json b/homeassistant/components/axis/translations/ca.json index 5ca4c474bc9..8cbbe638d24 100644 --- a/homeassistant/components/axis/translations/ca.json +++ b/homeassistant/components/axis/translations/ca.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", - "bad_config_file": "Dades incorrectes del fitxer de configuraci\u00f3", + "bad_config_file": "Dades del fitxer de configuraci\u00f3 incorrectes", "link_local_address": "L'enlla\u00e7 d'adreces locals no est\u00e0 disponible", "not_axis_device": "El dispositiu descobert no \u00e9s un dispositiu Axis" }, @@ -25,5 +25,14 @@ } } }, - "title": "Dispositiu Axis" + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "Selecciona el perfil de transmissi\u00f3 de v\u00eddeo a utilitzar" + }, + "title": "Opcions de transmissi\u00f3 de v\u00eddeo del dispositiu Axis" + } + } + } } \ No newline at end of file diff --git a/homeassistant/components/axis/translations/da.json b/homeassistant/components/axis/translations/da.json index 78e4e200082..3f2130238a2 100644 --- a/homeassistant/components/axis/translations/da.json +++ b/homeassistant/components/axis/translations/da.json @@ -24,6 +24,5 @@ "title": "Indstil Axis-enhed" } } - }, - "title": "Axis-enhed" + } } \ No newline at end of file diff --git a/homeassistant/components/axis/translations/de.json b/homeassistant/components/axis/translations/de.json index 7410a79bdc9..7753e5ad1e8 100644 --- a/homeassistant/components/axis/translations/de.json +++ b/homeassistant/components/axis/translations/de.json @@ -24,6 +24,5 @@ "title": "Axis Ger\u00e4t einrichten" } } - }, - "title": "Axis Ger\u00e4t" + } } \ No newline at end of file diff --git a/homeassistant/components/axis/translations/en.json b/homeassistant/components/axis/translations/en.json index cc76571b01b..29461ee0612 100644 --- a/homeassistant/components/axis/translations/en.json +++ b/homeassistant/components/axis/translations/en.json @@ -25,5 +25,14 @@ } } }, - "title": "Axis device" + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "Select stream profile to use" + }, + "title": "Axis device video stream options" + } + } + } } \ No newline at end of file diff --git a/homeassistant/components/axis/translations/es-419.json b/homeassistant/components/axis/translations/es-419.json index b5d1cb4ca7b..151ef346be2 100644 --- a/homeassistant/components/axis/translations/es-419.json +++ b/homeassistant/components/axis/translations/es-419.json @@ -24,6 +24,5 @@ "title": "Configurar dispositivo Axis" } } - }, - "title": "Dispositivo Axis" + } } \ No newline at end of file diff --git a/homeassistant/components/axis/translations/es.json b/homeassistant/components/axis/translations/es.json index 19a774fe170..38a33309145 100644 --- a/homeassistant/components/axis/translations/es.json +++ b/homeassistant/components/axis/translations/es.json @@ -25,5 +25,14 @@ } } }, - "title": "Dispositivo Axis" + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "Selecciona el perfil de transmisi\u00f3n a usar" + }, + "title": "Opciones de transmisi\u00f3n de v\u00eddeo del dispositivo Axis" + } + } + } } \ No newline at end of file diff --git a/homeassistant/components/axis/translations/fi.json b/homeassistant/components/axis/translations/fi.json index b81b9858cd0..a3740dd8bcf 100644 --- a/homeassistant/components/axis/translations/fi.json +++ b/homeassistant/components/axis/translations/fi.json @@ -1,3 +1,9 @@ { - "title": "Axis-laite" + "config": { + "step": { + "user": { + "title": "Asenna Axis-laite" + } + } + } } \ No newline at end of file diff --git a/homeassistant/components/axis/translations/fr.json b/homeassistant/components/axis/translations/fr.json index 4fb4f928768..2b5d1b24ef7 100644 --- a/homeassistant/components/axis/translations/fr.json +++ b/homeassistant/components/axis/translations/fr.json @@ -24,6 +24,5 @@ "title": "Configurer l'appareil Axis" } } - }, - "title": "Appareil Axis" + } } \ No newline at end of file diff --git a/homeassistant/components/axis/translations/hu.json b/homeassistant/components/axis/translations/hu.json index ac8538c6943..d749df6a783 100644 --- a/homeassistant/components/axis/translations/hu.json +++ b/homeassistant/components/axis/translations/hu.json @@ -16,6 +16,5 @@ } } } - }, - "title": "Axis eszk\u00f6z" + } } \ No newline at end of file diff --git a/homeassistant/components/axis/translations/it.json b/homeassistant/components/axis/translations/it.json index e3083687b07..b771c6069bf 100644 --- a/homeassistant/components/axis/translations/it.json +++ b/homeassistant/components/axis/translations/it.json @@ -24,6 +24,5 @@ "title": "Impostazione del dispositivo Axis" } } - }, - "title": "Dispositivo Axis" + } } \ No newline at end of file diff --git a/homeassistant/components/axis/translations/ko.json b/homeassistant/components/axis/translations/ko.json index b336a290f9e..45adcca576f 100644 --- a/homeassistant/components/axis/translations/ko.json +++ b/homeassistant/components/axis/translations/ko.json @@ -24,6 +24,5 @@ "title": "Axis \uae30\uae30 \uc124\uc815\ud558\uae30" } } - }, - "title": "Axis \uae30\uae30" + } } \ No newline at end of file diff --git a/homeassistant/components/axis/translations/lb.json b/homeassistant/components/axis/translations/lb.json index c19c50381fc..d73489f6a43 100644 --- a/homeassistant/components/axis/translations/lb.json +++ b/homeassistant/components/axis/translations/lb.json @@ -24,6 +24,5 @@ "title": "Axis Apparat ariichten" } } - }, - "title": "Axis Apparat" + } } \ No newline at end of file diff --git a/homeassistant/components/axis/translations/nl.json b/homeassistant/components/axis/translations/nl.json index 852933c6204..8919514e44a 100644 --- a/homeassistant/components/axis/translations/nl.json +++ b/homeassistant/components/axis/translations/nl.json @@ -24,6 +24,5 @@ "title": "Stel het Axis-apparaat in" } } - }, - "title": "Axis-apparaat" + } } \ No newline at end of file diff --git a/homeassistant/components/axis/translations/no.json b/homeassistant/components/axis/translations/no.json index a316ca726b5..891b4b6d972 100644 --- a/homeassistant/components/axis/translations/no.json +++ b/homeassistant/components/axis/translations/no.json @@ -25,5 +25,14 @@ } } }, - "title": "Axis enhet" + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "Velg str\u00f8mprofil som skal brukes" + }, + "title": "Axis videostr\u00f8m alternativer" + } + } + } } \ No newline at end of file diff --git a/homeassistant/components/axis/translations/pl.json b/homeassistant/components/axis/translations/pl.json index 4d4961bddb2..9b4d27f6205 100644 --- a/homeassistant/components/axis/translations/pl.json +++ b/homeassistant/components/axis/translations/pl.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]", + "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" }, "error": { - "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]", + "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" @@ -16,14 +16,13 @@ "step": { "user": { "data": { - "host": "[%key_id:common::config_flow::data::host%]", - "password": "[%key_id:common::config_flow::data::password%]", - "port": "[%key_id:common::config_flow::data::port%]", - "username": "[%key_id:common::config_flow::data::username%]" + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o", + "port": "Port", + "username": "Nazwa u\u017cytkownika" }, "title": "Konfiguracja urz\u0105dzenia Axis" } } - }, - "title": "Urz\u0105dzenie Axis" + } } \ No newline at end of file diff --git a/homeassistant/components/axis/translations/pt-BR.json b/homeassistant/components/axis/translations/pt-BR.json index 10e9fb563ce..c63bbf7ed0a 100644 --- a/homeassistant/components/axis/translations/pt-BR.json +++ b/homeassistant/components/axis/translations/pt-BR.json @@ -24,6 +24,5 @@ "title": "Configurar o dispositivo Axis" } } - }, - "title": "Dispositivo Axis" + } } \ No newline at end of file diff --git a/homeassistant/components/axis/translations/pt.json b/homeassistant/components/axis/translations/pt.json index 2dc5a14249f..77ce7025f70 100644 --- a/homeassistant/components/axis/translations/pt.json +++ b/homeassistant/components/axis/translations/pt.json @@ -10,6 +10,5 @@ } } } - }, - "title": "Dispositivo Axis" + } } \ No newline at end of file diff --git a/homeassistant/components/axis/translations/ru.json b/homeassistant/components/axis/translations/ru.json index 578e642d4d2..4f8aea64e78 100644 --- a/homeassistant/components/axis/translations/ru.json +++ b/homeassistant/components/axis/translations/ru.json @@ -25,5 +25,14 @@ } } }, - "title": "Axis" + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0440\u043e\u0444\u0438\u043b\u044c \u043f\u043e\u0442\u043e\u043a\u0430 \u0434\u043b\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f" + }, + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0432\u0438\u0434\u0435\u043e \u043f\u043e\u0442\u043e\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Axis" + } + } + } } \ No newline at end of file diff --git a/homeassistant/components/axis/translations/sl.json b/homeassistant/components/axis/translations/sl.json index a41e5ddd652..ac7b359b19e 100644 --- a/homeassistant/components/axis/translations/sl.json +++ b/homeassistant/components/axis/translations/sl.json @@ -24,6 +24,5 @@ "title": "Nastavite plo\u0161\u010dek" } } - }, - "title": "Plo\u0161\u010dek" + } } \ No newline at end of file diff --git a/homeassistant/components/axis/translations/sv.json b/homeassistant/components/axis/translations/sv.json index c208838ed1a..f7c2e8902f7 100644 --- a/homeassistant/components/axis/translations/sv.json +++ b/homeassistant/components/axis/translations/sv.json @@ -24,6 +24,5 @@ "title": "Konfigurera Axis-enhet" } } - }, - "title": "Axis enhet" + } } \ No newline at end of file diff --git a/homeassistant/components/axis/translations/zh-Hant.json b/homeassistant/components/axis/translations/zh-Hant.json index e24c29a86bc..8e90d449f11 100644 --- a/homeassistant/components/axis/translations/zh-Hant.json +++ b/homeassistant/components/axis/translations/zh-Hant.json @@ -25,5 +25,14 @@ } } }, - "title": "Axis \u8a2d\u5099" + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "\u9078\u64c7\u6240\u8981\u4f7f\u7528\u7684\u4e32\u6d41\u8a2d\u5b9a" + }, + "title": "Axis \u8a2d\u5099\u5f71\u50cf\u4e32\u6d41\u9078\u9805" + } + } + } } \ No newline at end of file diff --git a/homeassistant/components/azure_event_hub/__init__.py b/homeassistant/components/azure_event_hub/__init__.py index cc59790b646..3b44c6423be 100644 --- a/homeassistant/components/azure_event_hub/__init__.py +++ b/homeassistant/components/azure_event_hub/__init__.py @@ -1,89 +1,223 @@ """Support for Azure Event Hubs.""" +import asyncio import json import logging +import time from typing import Any, Dict -from azure.eventhub import EventData, EventHubClientAsync +from azure.eventhub import EventData +from azure.eventhub.aio import EventHubProducerClient, EventHubSharedKeyCredential +from azure.eventhub.exceptions import EventHubError import voluptuous as vol from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, - EVENT_STATE_CHANGED, + MATCH_ALL, STATE_UNAVAILABLE, STATE_UNKNOWN, ) from homeassistant.core import Event, HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA +from homeassistant.helpers.event import async_call_later from homeassistant.helpers.json import JSONEncoder +from .const import ( + ADDITIONAL_ARGS, + CONF_EVENT_HUB_CON_STRING, + CONF_EVENT_HUB_INSTANCE_NAME, + CONF_EVENT_HUB_NAMESPACE, + CONF_EVENT_HUB_SAS_KEY, + CONF_EVENT_HUB_SAS_POLICY, + CONF_FILTER, + CONF_MAX_DELAY, + CONF_SEND_INTERVAL, + DOMAIN, +) + _LOGGER = logging.getLogger(__name__) -DOMAIN = "azure_event_hub" - -CONF_EVENT_HUB_NAMESPACE = "event_hub_namespace" -CONF_EVENT_HUB_INSTANCE_NAME = "event_hub_instance_name" -CONF_EVENT_HUB_SAS_POLICY = "event_hub_sas_policy" -CONF_EVENT_HUB_SAS_KEY = "event_hub_sas_key" -CONF_FILTER = "filter" - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( { - vol.Required(CONF_EVENT_HUB_NAMESPACE): cv.string, - vol.Required(CONF_EVENT_HUB_INSTANCE_NAME): cv.string, - vol.Required(CONF_EVENT_HUB_SAS_POLICY): cv.string, - vol.Required(CONF_EVENT_HUB_SAS_KEY): cv.string, - vol.Required(CONF_FILTER): FILTER_SCHEMA, - } + vol.Exclusive(CONF_EVENT_HUB_CON_STRING, "setup_methods"): cv.string, + vol.Exclusive(CONF_EVENT_HUB_NAMESPACE, "setup_methods"): cv.string, + vol.Optional(CONF_EVENT_HUB_INSTANCE_NAME): cv.string, + vol.Optional(CONF_EVENT_HUB_SAS_POLICY): cv.string, + vol.Optional(CONF_EVENT_HUB_SAS_KEY): cv.string, + vol.Optional(CONF_SEND_INTERVAL, default=5): cv.positive_int, + vol.Optional(CONF_MAX_DELAY, default=30): cv.positive_int, + vol.Optional(CONF_FILTER, default={}): FILTER_SCHEMA, + }, + cv.has_at_least_one_key( + CONF_EVENT_HUB_CON_STRING, CONF_EVENT_HUB_NAMESPACE + ), ) }, extra=vol.ALLOW_EXTRA, ) -async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): +async def async_setup(hass, yaml_config): """Activate Azure EH component.""" config = yaml_config[DOMAIN] + if config.get(CONF_EVENT_HUB_CON_STRING): + client_args = {"conn_str": config[CONF_EVENT_HUB_CON_STRING]} + conn_str_client = True + else: + client_args = { + "fully_qualified_namespace": f"{config[CONF_EVENT_HUB_NAMESPACE]}.servicebus.windows.net", + "credential": EventHubSharedKeyCredential( + policy=config[CONF_EVENT_HUB_SAS_POLICY], + key=config[CONF_EVENT_HUB_SAS_KEY], + ), + "eventhub_name": config[CONF_EVENT_HUB_INSTANCE_NAME], + } + conn_str_client = False - event_hub_address = ( - f"amqps://{config[CONF_EVENT_HUB_NAMESPACE]}" - f".servicebus.windows.net/{config[CONF_EVENT_HUB_INSTANCE_NAME]}" + instance = hass.data[DOMAIN] = AzureEventHub( + hass, + client_args, + conn_str_client, + config[CONF_FILTER], + config[CONF_SEND_INTERVAL], + config[CONF_MAX_DELAY], ) - entities_filter = config[CONF_FILTER] - client = EventHubClientAsync( - event_hub_address, - debug=True, - username=config[CONF_EVENT_HUB_SAS_POLICY], - password=config[CONF_EVENT_HUB_SAS_KEY], - ) - async_sender = client.add_async_sender() - await client.run_async() + hass.async_create_task(instance.async_start()) + return True - encoder = JSONEncoder() - async def async_send_to_event_hub(event: Event): - """Send states to Event Hub.""" +class AzureEventHub: + """A event handler class for Azure Event Hub.""" + + def __init__( + self, + hass: HomeAssistant, + client_args: Dict[str, Any], + conn_str_client: bool, + entities_filter: vol.Schema, + send_interval: int, + max_delay: int, + ): + """Initialize the listener.""" + self.hass = hass + self.queue = asyncio.PriorityQueue() + self._client_args = client_args + self._conn_str_client = conn_str_client + self._entities_filter = entities_filter + self._send_interval = send_interval + self._max_delay = max_delay + send_interval + self._listener_remover = None + self._next_send_remover = None + self.shutdown = False + + async def async_start(self): + """Start the recorder, suppress logging and register the callbacks and do the first send after five seconds, to capture the startup events.""" + # suppress the INFO and below logging on the underlying packages, they are very verbose, even at INFO + logging.getLogger("uamqp").setLevel(logging.WARNING) + logging.getLogger("azure.eventhub").setLevel(logging.WARNING) + + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_shutdown) + self._listener_remover = self.hass.bus.async_listen( + MATCH_ALL, self.async_listen + ) + # schedule the first send after 10 seconds to capture startup events, after that each send will schedule the next after the interval. + self._next_send_remover = async_call_later(self.hass, 10, self.async_send) + + async def async_shutdown(self, _: Event): + """Shut down the AEH by queueing None and calling send.""" + if self._next_send_remover: + self._next_send_remover() + if self._listener_remover: + self._listener_remover() + await self.queue.put((3, (time.monotonic(), None))) + await self.async_send(None) + + async def async_listen(self, event: Event): + """Listen for new messages on the bus and queue them for AEH.""" + await self.queue.put((2, (time.monotonic(), event))) + + async def async_send(self, _): + """Write preprocessed events to eventhub, with retry.""" + client = self._get_client() + async with client: + while not self.queue.empty(): + data_batch, dequeue_count = await self.fill_batch(client) + _LOGGER.debug( + "Sending %d event(s), out of %d events in the queue", + len(data_batch), + dequeue_count, + ) + if data_batch: + try: + await client.send_batch(data_batch) + except EventHubError as exc: + _LOGGER.error("Error in sending events to Event Hub: %s", exc) + finally: + for _ in range(dequeue_count): + self.queue.task_done() + await client.close() + + if not self.shutdown: + self._next_send_remover = async_call_later( + self.hass, self._send_interval, self.async_send + ) + + async def fill_batch(self, client): + """Return a batch of events formatted for writing. + + Uses get_nowait instead of await get, because the functions batches and doesn't wait for each single event, the send function is called. + + Throws ValueError on add to batch when the EventDataBatch object reaches max_size. Put the item back in the queue and the next batch will include it. + """ + event_batch = await client.create_batch() + dequeue_count = 0 + dropped = 0 + while not self.shutdown: + try: + _, (timestamp, event) = self.queue.get_nowait() + except asyncio.QueueEmpty: + break + dequeue_count += 1 + if not event: + self.shutdown = True + break + event_data = self._event_to_filtered_event_data(event) + if not event_data: + continue + if time.monotonic() - timestamp <= self._max_delay: + try: + event_batch.add(event_data) + except ValueError: + self.queue.put_nowait((1, (timestamp, event))) + break + else: + dropped += 1 + + if dropped: + _LOGGER.warning( + "Dropped %d old events, consider increasing the max_delay", dropped + ) + + return event_batch, dequeue_count + + def _event_to_filtered_event_data(self, event: Event): + """Filter event states and create EventData object.""" state = event.data.get("new_state") if ( state is None or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE) - or not entities_filter(state.entity_id) + or not self._entities_filter(state.entity_id) ): - return + return None + return EventData(json.dumps(obj=state, cls=JSONEncoder).encode("utf-8")) - event_data = EventData( - json.dumps(obj=state.as_dict(), default=encoder.encode).encode("utf-8") - ) - await async_sender.send(event_data) - - async def async_shutdown(event: Event): - """Shut down the client.""" - await client.stop_async() - - hass.bus.async_listen(EVENT_STATE_CHANGED, async_send_to_event_hub) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_shutdown) - - return True + def _get_client(self): + """Get a Event Producer Client.""" + if self._conn_str_client: + return EventHubProducerClient.from_connection_string( + **self._client_args, **ADDITIONAL_ARGS + ) + return EventHubProducerClient(**self._client_args, **ADDITIONAL_ARGS) diff --git a/homeassistant/components/azure_event_hub/const.py b/homeassistant/components/azure_event_hub/const.py new file mode 100644 index 00000000000..1786bb5cbf2 --- /dev/null +++ b/homeassistant/components/azure_event_hub/const.py @@ -0,0 +1,13 @@ +"""Constants and shared schema for the Azure Event Hub integration.""" +DOMAIN = "azure_event_hub" + +CONF_EVENT_HUB_NAMESPACE = "event_hub_namespace" +CONF_EVENT_HUB_INSTANCE_NAME = "event_hub_instance_name" +CONF_EVENT_HUB_SAS_POLICY = "event_hub_sas_policy" +CONF_EVENT_HUB_SAS_KEY = "event_hub_sas_key" +CONF_EVENT_HUB_CON_STRING = "event_hub_connection_string" +CONF_SEND_INTERVAL = "send_interval" +CONF_MAX_DELAY = "max_delay" +CONF_FILTER = "filter" + +ADDITIONAL_ARGS = {"logging_enable": False} diff --git a/homeassistant/components/azure_event_hub/manifest.json b/homeassistant/components/azure_event_hub/manifest.json index f9d4cf09e04..08bae34d731 100644 --- a/homeassistant/components/azure_event_hub/manifest.json +++ b/homeassistant/components/azure_event_hub/manifest.json @@ -2,6 +2,6 @@ "domain": "azure_event_hub", "name": "Azure Event Hub", "documentation": "https://www.home-assistant.io/integrations/azure_event_hub", - "requirements": ["azure-eventhub==1.3.1"], + "requirements": ["azure-eventhub==5.1.0"], "codeowners": ["@eavanvalkenburg"] } diff --git a/homeassistant/components/binary_sensor/translations/ca.json b/homeassistant/components/binary_sensor/translations/ca.json index 995b5906c53..bf16523251e 100644 --- a/homeassistant/components/binary_sensor/translations/ca.json +++ b/homeassistant/components/binary_sensor/translations/ca.json @@ -91,8 +91,8 @@ }, "state": { "_": { - "off": "Desactivat", - "on": "Activat" + "off": "OFF", + "on": "ON" }, "battery": { "off": "Normal", @@ -107,12 +107,12 @@ "on": "Connectat" }, "door": { - "off": "Tancada", - "on": "Oberta" + "off": "Tancat/da", + "on": "Obert/a" }, "garage_door": { - "off": "Tancada", - "on": "Oberta" + "off": "Tancat/da", + "on": "Obert/a" }, "gas": { "off": "Lliure", @@ -139,15 +139,15 @@ "on": "Detectat" }, "opening": { - "off": "Tancat", - "on": "Obert" + "off": "Tancat/da", + "on": "Obert/a" }, "presence": { - "off": "Lliure", - "on": "Detectat" + "off": "Fora", + "on": "A casa" }, "problem": { - "off": "Correcte", + "off": "OK", "on": "Problema" }, "safety": { @@ -167,8 +167,8 @@ "on": "Detectat" }, "window": { - "off": "Tancada", - "on": "Oberta" + "off": "Tancat/da", + "on": "Obert/a" } }, "title": "Sensor binari" diff --git a/homeassistant/components/binary_sensor/translations/it.json b/homeassistant/components/binary_sensor/translations/it.json index 95a9778345e..590ab87f142 100644 --- a/homeassistant/components/binary_sensor/translations/it.json +++ b/homeassistant/components/binary_sensor/translations/it.json @@ -128,7 +128,7 @@ }, "moisture": { "off": "Asciutto", - "on": "Bagnato" + "on": "Umido" }, "motion": { "off": "Assenza", diff --git a/homeassistant/components/blebox/__init__.py b/homeassistant/components/blebox/__init__.py index 1196deb27b7..3d3c997596a 100644 --- a/homeassistant/components/blebox/__init__.py +++ b/homeassistant/components/blebox/__init__.py @@ -17,7 +17,7 @@ from .const import DEFAULT_SETUP_TIMEOUT, DOMAIN, PRODUCT _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["cover", "sensor"] +PLATFORMS = ["cover", "sensor", "switch", "air_quality", "light", "climate"] PARALLEL_UPDATES = 0 @@ -74,15 +74,19 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): @callback -def create_blebox_entities(product, async_add, entity_klass, entity_type): +def create_blebox_entities( + hass, config_entry, async_add_entities, entity_klass, entity_type +): """Create entities from a BleBox product's features.""" + product = hass.data[DOMAIN][config_entry.entry_id][PRODUCT] + entities = [] if entity_type in product.features: for feature in product.features[entity_type]: entities.append(entity_klass(feature)) - async_add(entities, True) + async_add_entities(entities, True) class BleBoxEntity(Entity): diff --git a/homeassistant/components/blebox/air_quality.py b/homeassistant/components/blebox/air_quality.py new file mode 100644 index 00000000000..e7e9bac1f97 --- /dev/null +++ b/homeassistant/components/blebox/air_quality.py @@ -0,0 +1,36 @@ +"""BleBox air quality entity.""" + +from homeassistant.components.air_quality import AirQualityEntity + +from . import BleBoxEntity, create_blebox_entities + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up a BleBox air quality entity.""" + create_blebox_entities( + hass, config_entry, async_add_entities, BleBoxAirQualityEntity, "air_qualities" + ) + + +class BleBoxAirQualityEntity(BleBoxEntity, AirQualityEntity): + """Representation of a BleBox air quality feature.""" + + @property + def icon(self): + """Return the icon.""" + return "mdi:blur" + + @property + def particulate_matter_0_1(self): + """Return the particulate matter 0.1 level.""" + return self._feature.pm1 + + @property + def particulate_matter_2_5(self): + """Return the particulate matter 2.5 level.""" + return self._feature.pm2_5 + + @property + def particulate_matter_10(self): + """Return the particulate matter 10 level.""" + return self._feature.pm10 diff --git a/homeassistant/components/blebox/climate.py b/homeassistant/components/blebox/climate.py new file mode 100644 index 00000000000..4ee8cf9be76 --- /dev/null +++ b/homeassistant/components/blebox/climate.py @@ -0,0 +1,92 @@ +"""BleBox climate entity.""" + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS + +from . import BleBoxEntity, create_blebox_entities + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up a BleBox climate entity.""" + + create_blebox_entities( + hass, config_entry, async_add_entities, BleBoxClimateEntity, "climates" + ) + + +class BleBoxClimateEntity(BleBoxEntity, ClimateEntity): + """Representation of a BleBox climate feature (saunaBox).""" + + @property + def supported_features(self): + """Return the supported climate features.""" + return SUPPORT_TARGET_TEMPERATURE + + @property + def hvac_mode(self): + """Return the desired HVAC mode.""" + if self._feature.is_on is None: + return None + + return HVAC_MODE_HEAT if self._feature.is_on else HVAC_MODE_OFF + + @property + def hvac_action(self): + """Return the actual current HVAC action.""" + is_on = self._feature.is_on + if not is_on: + return None if is_on is None else CURRENT_HVAC_OFF + + # NOTE: In practice, there's no need to handle case when is_heating is None + return CURRENT_HVAC_HEAT if self._feature.is_heating else CURRENT_HVAC_IDLE + + @property + def hvac_modes(self): + """Return a list of possible HVAC modes.""" + return [HVAC_MODE_OFF, HVAC_MODE_HEAT] + + @property + def temperature_unit(self): + """Return the temperature unit.""" + return TEMP_CELSIUS + + @property + def max_temp(self): + """Return the maximum temperature supported.""" + return self._feature.max_temp + + @property + def min_temp(self): + """Return the maximum temperature supported.""" + return self._feature.min_temp + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._feature.current + + @property + def target_temperature(self): + """Return the desired thermostat temperature.""" + return self._feature.desired + + async def async_set_hvac_mode(self, hvac_mode): + """Set the climate entity mode.""" + if hvac_mode == HVAC_MODE_HEAT: + await self._feature.async_on() + return + + await self._feature.async_off() + + async def async_set_temperature(self, **kwargs): + """Set the thermostat temperature.""" + value = kwargs[ATTR_TEMPERATURE] + await self._feature.async_set_temperature(value) diff --git a/homeassistant/components/blebox/const.py b/homeassistant/components/blebox/const.py index 71d2193f904..f5eba403c75 100644 --- a/homeassistant/components/blebox/const.py +++ b/homeassistant/components/blebox/const.py @@ -9,6 +9,7 @@ from homeassistant.components.cover import ( STATE_OPEN, STATE_OPENING, ) +from homeassistant.components.switch import DEVICE_CLASS_SWITCH from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS DOMAIN = "blebox" @@ -26,6 +27,8 @@ BLEBOX_TO_HASS_DEVICE_CLASSES = { "shutter": DEVICE_CLASS_SHUTTER, "gatebox": DEVICE_CLASS_DOOR, "gate": DEVICE_CLASS_GATE, + "relay": DEVICE_CLASS_SWITCH, + "temperature": DEVICE_CLASS_TEMPERATURE, } BLEBOX_TO_HASS_COVER_STATES = { @@ -43,7 +46,6 @@ BLEBOX_TO_HASS_COVER_STATES = { } BLEBOX_TO_UNIT_MAP = {"celsius": TEMP_CELSIUS} -BLEBOX_DEV_CLASS_MAP = {"temperature": DEVICE_CLASS_TEMPERATURE} DEFAULT_HOST = "192.168.0.2" DEFAULT_PORT = 80 diff --git a/homeassistant/components/blebox/cover.py b/homeassistant/components/blebox/cover.py index 2a8f0219267..620adacf3f6 100644 --- a/homeassistant/components/blebox/cover.py +++ b/homeassistant/components/blebox/cover.py @@ -13,20 +13,15 @@ from homeassistant.components.cover import ( ) from . import BleBoxEntity, create_blebox_entities -from .const import ( - BLEBOX_TO_HASS_COVER_STATES, - BLEBOX_TO_HASS_DEVICE_CLASSES, - DOMAIN, - PRODUCT, -) +from .const import BLEBOX_TO_HASS_COVER_STATES, BLEBOX_TO_HASS_DEVICE_CLASSES -async def async_setup_entry(hass, config_entry, async_add): +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a BleBox entry.""" - product = hass.data[DOMAIN][config_entry.entry_id][PRODUCT] - create_blebox_entities(product, async_add, BleBoxCoverEntity, "covers") - return True + create_blebox_entities( + hass, config_entry, async_add_entities, BleBoxCoverEntity, "covers" + ) class BleBoxCoverEntity(BleBoxEntity, CoverEntity): diff --git a/homeassistant/components/blebox/light.py b/homeassistant/components/blebox/light.py new file mode 100644 index 00000000000..a825d102717 --- /dev/null +++ b/homeassistant/components/blebox/light.py @@ -0,0 +1,100 @@ +"""BleBox light entities implementation.""" +import logging + +from blebox_uniapi.error import BadOnValueError + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_HS_COLOR, + ATTR_WHITE_VALUE, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_WHITE_VALUE, + LightEntity, +) +from homeassistant.util.color import ( + color_hs_to_RGB, + color_rgb_to_hex, + color_RGB_to_hs, + rgb_hex_to_rgb_list, +) + +from . import BleBoxEntity, create_blebox_entities + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up a BleBox entry.""" + + create_blebox_entities( + hass, config_entry, async_add_entities, BleBoxLightEntity, "lights" + ) + + +class BleBoxLightEntity(BleBoxEntity, LightEntity): + """Representation of BleBox lights.""" + + @property + def supported_features(self): + """Return supported features.""" + white = SUPPORT_WHITE_VALUE if self._feature.supports_white else 0 + color = SUPPORT_COLOR if self._feature.supports_color else 0 + brightness = SUPPORT_BRIGHTNESS if self._feature.supports_brightness else 0 + return white | color | brightness + + @property + def is_on(self): + """Return if light is on.""" + return self._feature.is_on + + @property + def brightness(self): + """Return the name.""" + return self._feature.brightness + + @property + def white_value(self): + """Return the white value.""" + return self._feature.white_value + + @property + def hs_color(self): + """Return the hue and saturation.""" + rgbw_hex = self._feature.rgbw_hex + if rgbw_hex is None: + return None + + rgb = rgb_hex_to_rgb_list(rgbw_hex)[0:3] + return color_RGB_to_hs(*rgb) + + async def async_turn_on(self, **kwargs): + """Turn the light on.""" + + white = kwargs.get(ATTR_WHITE_VALUE) + hs_color = kwargs.get(ATTR_HS_COLOR) + brightness = kwargs.get(ATTR_BRIGHTNESS) + + feature = self._feature + value = feature.sensible_on_value + + if brightness is not None: + value = feature.apply_brightness(value, brightness) + + if white is not None: + value = feature.apply_white(value, white) + + if hs_color is not None: + raw_rgb = color_rgb_to_hex(*color_hs_to_RGB(*hs_color)) + value = feature.apply_color(value, raw_rgb) + + try: + await self._feature.async_on(value) + except BadOnValueError as ex: + _LOGGER.error( + "Turning on '%s' failed: Bad value %s (%s)", self.name, value, ex + ) + + async def async_turn_off(self, **kwargs): + """Turn the light off.""" + await self._feature.async_off() diff --git a/homeassistant/components/blebox/sensor.py b/homeassistant/components/blebox/sensor.py index b43b4f21da5..84f4c19371d 100644 --- a/homeassistant/components/blebox/sensor.py +++ b/homeassistant/components/blebox/sensor.py @@ -3,15 +3,15 @@ from homeassistant.helpers.entity import Entity from . import BleBoxEntity, create_blebox_entities -from .const import BLEBOX_DEV_CLASS_MAP, BLEBOX_TO_UNIT_MAP, DOMAIN, PRODUCT +from .const import BLEBOX_TO_HASS_DEVICE_CLASSES, BLEBOX_TO_UNIT_MAP -async def async_setup_entry(hass, config_entry, async_add): +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a BleBox entry.""" - product = hass.data[DOMAIN][config_entry.entry_id][PRODUCT] - create_blebox_entities(product, async_add, BleBoxSensorEntity, "sensors") - return True + create_blebox_entities( + hass, config_entry, async_add_entities, BleBoxSensorEntity, "sensors" + ) class BleBoxSensorEntity(BleBoxEntity, Entity): @@ -30,4 +30,4 @@ class BleBoxSensorEntity(BleBoxEntity, Entity): @property def device_class(self): """Return the device class.""" - return BLEBOX_DEV_CLASS_MAP[self._feature.device_class] + return BLEBOX_TO_HASS_DEVICE_CLASSES[self._feature.device_class] diff --git a/homeassistant/components/blebox/switch.py b/homeassistant/components/blebox/switch.py new file mode 100644 index 00000000000..e88773db639 --- /dev/null +++ b/homeassistant/components/blebox/switch.py @@ -0,0 +1,34 @@ +"""BleBox switch implementation.""" +from homeassistant.components.switch import SwitchEntity + +from . import BleBoxEntity, create_blebox_entities +from .const import BLEBOX_TO_HASS_DEVICE_CLASSES + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up a BleBox switch entity.""" + create_blebox_entities( + hass, config_entry, async_add_entities, BleBoxSwitchEntity, "switches" + ) + + +class BleBoxSwitchEntity(BleBoxEntity, SwitchEntity): + """Representation of a BleBox switch feature.""" + + @property + def device_class(self): + """Return the device class.""" + return BLEBOX_TO_HASS_DEVICE_CLASSES[self._feature.device_class] + + @property + def is_on(self): + """Return whether switch is on.""" + return self._feature.is_on + + async def async_turn_on(self, **kwargs): + """Turn on the switch.""" + await self._feature.async_turn_on() + + async def async_turn_off(self, **kwargs): + """Turn off the switch.""" + await self._feature.async_turn_off() diff --git a/homeassistant/components/blebox/translations/de.json b/homeassistant/components/blebox/translations/de.json index 501b1335244..baf14ba4897 100644 --- a/homeassistant/components/blebox/translations/de.json +++ b/homeassistant/components/blebox/translations/de.json @@ -13,6 +13,7 @@ "step": { "user": { "data": { + "host": "IP Adresse", "port": "Port" }, "description": "Richten Sie Ihre BleBox f\u00fcr die Integration mit dem Home Assistant ein.", diff --git a/homeassistant/components/blebox/translations/es-419.json b/homeassistant/components/blebox/translations/es-419.json new file mode 100644 index 00000000000..eb0545e4fa4 --- /dev/null +++ b/homeassistant/components/blebox/translations/es-419.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_already_configured": "Un dispositivo BleBox ya est\u00e1 configurado en {address} .", + "already_configured": "Este dispositivo BleBox ya est\u00e1 configurado." + }, + "error": { + "cannot_connect": "No se puede conectar al dispositivo BleBox. (Verifique los registros en busca de errores).", + "unknown": "Error desconocido al conectarse al dispositivo BleBox. (Verifique los registros en busca de errores).", + "unsupported_version": "El dispositivo BleBox tiene un firmware desactualizado. Por favor, actual\u00edcelo primero." + }, + "flow_title": "Dispositivo BleBox: {name} ({host})", + "step": { + "user": { + "data": { + "host": "Direcci\u00f3n IP", + "port": "Puerto" + }, + "description": "Configure su BleBox para integrarse con Home Assistant." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blebox/translations/es.json b/homeassistant/components/blebox/translations/es.json index ceed1592992..991553a2074 100644 --- a/homeassistant/components/blebox/translations/es.json +++ b/homeassistant/components/blebox/translations/es.json @@ -13,6 +13,7 @@ "step": { "user": { "data": { + "host": "Direcci\u00f3n IP", "port": "Puerto" }, "description": "Configura tu BleBox para integrarse con Home Assistant.", diff --git a/homeassistant/components/blebox/translations/hu.json b/homeassistant/components/blebox/translations/hu.json index a4c3e1bd2d3..9b0bf1c0ddf 100644 --- a/homeassistant/components/blebox/translations/hu.json +++ b/homeassistant/components/blebox/translations/hu.json @@ -7,6 +7,7 @@ "step": { "user": { "data": { + "host": "IP c\u00edm", "port": "Port" } } diff --git a/homeassistant/components/blebox/translations/it.json b/homeassistant/components/blebox/translations/it.json index 6b36ef97a51..73f7b9276e9 100644 --- a/homeassistant/components/blebox/translations/it.json +++ b/homeassistant/components/blebox/translations/it.json @@ -13,6 +13,7 @@ "step": { "user": { "data": { + "host": "Indirizzo IP", "port": "Porta" }, "description": "Configura BleBox per l'integrazione con Home Assistant.", diff --git a/homeassistant/components/blebox/translations/lb.json b/homeassistant/components/blebox/translations/lb.json index dae53828764..ffe37b1f907 100644 --- a/homeassistant/components/blebox/translations/lb.json +++ b/homeassistant/components/blebox/translations/lb.json @@ -13,6 +13,7 @@ "step": { "user": { "data": { + "host": "IP Adresse", "port": "Port" }, "description": "BleBox ariichten fir d'Integratioun mam Home Assistant.", diff --git a/homeassistant/components/blebox/translations/pl.json b/homeassistant/components/blebox/translations/pl.json index f158ad2c75d..5b0f124014e 100644 --- a/homeassistant/components/blebox/translations/pl.json +++ b/homeassistant/components/blebox/translations/pl.json @@ -13,8 +13,8 @@ "step": { "user": { "data": { - "host": "[%key_id:common::config_flow::data::ip%]", - "port": "[%key_id:common::config_flow::data::port%]" + "host": "Adres IP", + "port": "Port" }, "description": "Skonfiguruj BleBox, aby zintegrowa\u0107 si\u0119 z Home Assistant.", "title": "Skonfiguruj urz\u0105dzenie BleBox" diff --git a/homeassistant/components/blebox/translations/pt-BR.json b/homeassistant/components/blebox/translations/pt-BR.json new file mode 100644 index 00000000000..f7dc708a2d6 --- /dev/null +++ b/homeassistant/components/blebox/translations/pt-BR.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Endere\u00e7o IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blebox/translations/ru.json b/homeassistant/components/blebox/translations/ru.json index b9374cfb11f..b82261be7f7 100644 --- a/homeassistant/components/blebox/translations/ru.json +++ b/homeassistant/components/blebox/translations/ru.json @@ -17,7 +17,7 @@ "port": "\u041f\u043e\u0440\u0442" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 BleBox.", - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 BleBox" + "title": "BleBox" } } } diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py index 3576574c357..04f9652bcb5 100644 --- a/homeassistant/components/blink/__init__.py +++ b/homeassistant/components/blink/__init__.py @@ -25,12 +25,10 @@ from .const import ( SERVICE_REFRESH, SERVICE_SAVE_VIDEO, SERVICE_SEND_PIN, - SERVICE_TRIGGER, ) _LOGGER = logging.getLogger(__name__) -SERVICE_TRIGGER_SCHEMA = vol.Schema({vol.Required(CONF_NAME): cv.string}) SERVICE_SAVE_VIDEO_SCHEMA = vol.Schema( {vol.Required(CONF_NAME): cv.string, vol.Required(CONF_FILENAME): cv.string} ) @@ -106,14 +104,6 @@ async def async_setup_entry(hass, entry): hass.config_entries.async_forward_entry_setup(entry, component) ) - def trigger_camera(call): - """Trigger a camera.""" - cameras = hass.data[DOMAIN][entry.entry_id].cameras - name = call.data[CONF_NAME] - if name in cameras: - cameras[name].snap_picture() - blink_refresh() - def blink_refresh(event_time=None): """Call blink to refresh info.""" hass.data[DOMAIN][entry.entry_id].refresh(force_cache=True) @@ -130,9 +120,6 @@ async def async_setup_entry(hass, entry): ) hass.services.async_register(DOMAIN, SERVICE_REFRESH, blink_refresh) - hass.services.async_register( - DOMAIN, SERVICE_TRIGGER, trigger_camera, schema=SERVICE_TRIGGER_SCHEMA - ) hass.services.async_register( DOMAIN, SERVICE_SAVE_VIDEO, async_save_video, schema=SERVICE_SAVE_VIDEO_SCHEMA ) @@ -163,7 +150,6 @@ async def async_unload_entry(hass, entry): return True hass.services.async_remove(DOMAIN, SERVICE_REFRESH) - hass.services.async_remove(DOMAIN, SERVICE_TRIGGER) hass.services.async_remove(DOMAIN, SERVICE_SAVE_VIDEO_SCHEMA) hass.services.async_remove(DOMAIN, SERVICE_SEND_PIN) diff --git a/homeassistant/components/blink/binary_sensor.py b/homeassistant/components/blink/binary_sensor.py index 8c86622b74e..1841dbbc438 100644 --- a/homeassistant/components/blink/binary_sensor.py +++ b/homeassistant/components/blink/binary_sensor.py @@ -1,11 +1,16 @@ """Support for Blink system camera control.""" -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_MOTION, + BinarySensorEntity, +) -from .const import DOMAIN, TYPE_CAMERA_ARMED, TYPE_MOTION_DETECTED +from .const import DOMAIN, TYPE_BATTERY, TYPE_CAMERA_ARMED, TYPE_MOTION_DETECTED BINARY_SENSORS = { - TYPE_CAMERA_ARMED: ["Camera Armed", "mdi:verified"], - TYPE_MOTION_DETECTED: ["Motion Detected", "mdi:run-fast"], + TYPE_BATTERY: ["Battery", DEVICE_CLASS_BATTERY], + TYPE_CAMERA_ARMED: ["Camera Armed", None], + TYPE_MOTION_DETECTED: ["Motion Detected", DEVICE_CLASS_MOTION], } @@ -27,9 +32,9 @@ class BlinkBinarySensor(BinarySensorEntity): """Initialize the sensor.""" self.data = data self._type = sensor_type - name, icon = BINARY_SENSORS[sensor_type] + name, device_class = BINARY_SENSORS[sensor_type] self._name = f"{DOMAIN} {camera} {name}" - self._icon = icon + self._device_class = device_class self._camera = data.cameras[camera] self._state = None self._unique_id = f"{self._camera.serial}-{self._type}" @@ -39,6 +44,11 @@ class BlinkBinarySensor(BinarySensorEntity): """Return the name of the blink sensor.""" return self._name + @property + def device_class(self): + """Return the class of this device.""" + return self._device_class + @property def is_on(self): """Return the status of the sensor.""" @@ -47,4 +57,7 @@ class BlinkBinarySensor(BinarySensorEntity): def update(self): """Update sensor state.""" self.data.refresh() - self._state = self._camera.attributes[self._type] + state = self._camera.attributes[self._type] + if self._type == TYPE_BATTERY: + state = state != "ok" + self._state = state diff --git a/homeassistant/components/blink/camera.py b/homeassistant/components/blink/camera.py index c675d4dda56..d4282bed606 100644 --- a/homeassistant/components/blink/camera.py +++ b/homeassistant/components/blink/camera.py @@ -1,15 +1,22 @@ """Support for Blink system camera.""" import logging -from homeassistant.components.camera import Camera +import voluptuous as vol -from .const import DEFAULT_BRAND, DOMAIN +from homeassistant.components.camera import Camera +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.helpers import entity_platform +import homeassistant.helpers.config_validation as cv + +from .const import DEFAULT_BRAND, DOMAIN, SERVICE_TRIGGER _LOGGER = logging.getLogger(__name__) ATTR_VIDEO_CLIP = "video" ATTR_IMAGE = "image" +SERVICE_TRIGGER_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids}) + async def async_setup_entry(hass, config, async_add_entities): """Set up a Blink Camera.""" @@ -20,6 +27,12 @@ async def async_setup_entry(hass, config, async_add_entities): async_add_entities(entities) + platform = entity_platform.current_platform.get() + + platform.async_register_entity_service( + SERVICE_TRIGGER, SERVICE_TRIGGER_SCHEMA, "trigger_camera" + ) + class BlinkCamera(Camera): """An implementation of a Blink Camera.""" @@ -69,6 +82,11 @@ class BlinkCamera(Camera): """Return the camera brand.""" return DEFAULT_BRAND + def trigger_camera(self): + """Trigger camera to take a snapshot.""" + self._camera.snap_picture() + self.data.refresh() + def camera_image(self): """Return a still image response from the camera.""" return self._camera.image_from_cache.content diff --git a/homeassistant/components/blink/sensor.py b/homeassistant/components/blink/sensor.py index eb9e309fc65..35cc2d0d5a5 100644 --- a/homeassistant/components/blink/sensor.py +++ b/homeassistant/components/blink/sensor.py @@ -1,17 +1,20 @@ """Support for Blink system camera sensors.""" import logging -from homeassistant.const import TEMP_FAHRENHEIT +from homeassistant.const import ( + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_TEMPERATURE, + TEMP_FAHRENHEIT, +) from homeassistant.helpers.entity import Entity -from .const import DOMAIN, TYPE_BATTERY, TYPE_TEMPERATURE, TYPE_WIFI_STRENGTH +from .const import DOMAIN, TYPE_TEMPERATURE, TYPE_WIFI_STRENGTH _LOGGER = logging.getLogger(__name__) SENSORS = { - TYPE_TEMPERATURE: ["Temperature", TEMP_FAHRENHEIT, "mdi:thermometer"], - TYPE_BATTERY: ["Battery", "", "mdi:battery-80"], - TYPE_WIFI_STRENGTH: ["Wifi Signal", "dBm", "mdi:wifi-strength-2"], + TYPE_TEMPERATURE: ["Temperature", TEMP_FAHRENHEIT, DEVICE_CLASS_TEMPERATURE], + TYPE_WIFI_STRENGTH: ["Wifi Signal", "dBm", DEVICE_CLASS_SIGNAL_STRENGTH], } @@ -31,15 +34,15 @@ class BlinkSensor(Entity): def __init__(self, data, camera, sensor_type): """Initialize sensors from Blink camera.""" - name, units, icon = SENSORS[sensor_type] + name, units, device_class = SENSORS[sensor_type] self._name = f"{DOMAIN} {camera} {name}" self._camera_name = name self._type = sensor_type + self._device_class = device_class self.data = data self._camera = data.cameras[camera] self._state = None self._unit_of_measurement = units - self._icon = icon self._unique_id = f"{self._camera.serial}-{self._type}" self._sensor_key = self._type if self._type == "temperature": @@ -55,16 +58,16 @@ class BlinkSensor(Entity): """Return the unique id for the camera sensor.""" return self._unique_id - @property - def icon(self): - """Return the icon of the sensor.""" - return self._icon - @property def state(self): """Return the camera's current state.""" return self._state + @property + def device_class(self): + """Return the device's class.""" + return self._device_class + @property def unit_of_measurement(self): """Return the unit of measurement.""" diff --git a/homeassistant/components/blink/services.yaml b/homeassistant/components/blink/services.yaml index 9a4d00ee0b8..dc6491e2139 100644 --- a/homeassistant/components/blink/services.yaml +++ b/homeassistant/components/blink/services.yaml @@ -4,11 +4,11 @@ blink_update: description: Force a refresh. trigger_camera: - description: Request named camera to take new image. + description: Request camera to take new image. fields: - name: - description: Name of camera to take new image. - example: "Living Room" + entity_id: + description: Name(s) of camera entities to take new image. + example: "camera.living_room_camera" save_video: description: Save last recorded video clip to local file. diff --git a/homeassistant/components/blink/translations/ca.json b/homeassistant/components/blink/translations/ca.json index 68607b0dbcd..afaacd63793 100644 --- a/homeassistant/components/blink/translations/ca.json +++ b/homeassistant/components/blink/translations/ca.json @@ -8,11 +8,19 @@ "unknown": "Error inesperat" }, "step": { + "2fa": { + "data": { + "2fa": "Codi de dos factors" + }, + "description": "Introdueix el PIN que has rebut per correu electr\u00f2nic. Si el correu no cont\u00e9 cap PIN, deixa-ho en blanc.", + "title": "Autenticaci\u00f3 de dos factors" + }, "user": { "data": { "password": "Contrasenya", "username": "Nom d'usuari" - } + }, + "title": "Inici de sessi\u00f3 amb Blink" } } } diff --git a/homeassistant/components/blink/translations/de.json b/homeassistant/components/blink/translations/de.json new file mode 100644 index 00000000000..ec5ad6c53ca --- /dev/null +++ b/homeassistant/components/blink/translations/de.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "2fa": { + "data": { + "2fa": "Zwei-Faktor Authentifizierungscode" + }, + "description": "Geben Sie die an Ihre E-Mail gesendete Pin ein. Wenn die E-Mail keine PIN enth\u00e4lt, lassen Sie das Feld leer.", + "title": "Zwei-Faktor-Authentifizierung" + }, + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + }, + "title": "Anmelden mit Blink-Konto" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blink/translations/es.json b/homeassistant/components/blink/translations/es.json new file mode 100644 index 00000000000..0606a28ee1f --- /dev/null +++ b/homeassistant/components/blink/translations/es.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "2fa": { + "data": { + "2fa": "C\u00f3digo de dos factores" + }, + "description": "Introduce el pin enviado a tu correo electr\u00f3nico. Si el correo electr\u00f3nico no contiene un pin, d\u00e9jalo en blanco", + "title": "Autenticaci\u00f3n de dos factores" + }, + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario" + }, + "title": "Iniciar sesi\u00f3n con cuenta Blink" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blink/translations/fr.json b/homeassistant/components/blink/translations/fr.json new file mode 100644 index 00000000000..c0283817e60 --- /dev/null +++ b/homeassistant/components/blink/translations/fr.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "2fa": { + "title": "Authentification \u00e0 deux facteurs" + }, + "user": { + "data": { + "password": "Mot de passe", + "username": "Identifiant" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blink/translations/hu.json b/homeassistant/components/blink/translations/hu.json new file mode 100644 index 00000000000..1150cda9ea9 --- /dev/null +++ b/homeassistant/components/blink/translations/hu.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blink/translations/it.json b/homeassistant/components/blink/translations/it.json new file mode 100644 index 00000000000..47099ed8611 --- /dev/null +++ b/homeassistant/components/blink/translations/it.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "2fa": { + "data": { + "2fa": "Codice a due fattori" + }, + "description": "Inserisci il PIN inviato alla tua e-mail. Se l'e-mail non contiene un PIN, lasciare vuoto", + "title": "Autenticazione a due fattori" + }, + "user": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "title": "Accedi con l'account Blink" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blink/translations/ko.json b/homeassistant/components/blink/translations/ko.json new file mode 100644 index 00000000000..0ac9092c723 --- /dev/null +++ b/homeassistant/components/blink/translations/ko.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "2fa": { + "data": { + "2fa": "2\ub2e8\uacc4 \uc778\uc99d \ucf54\ub4dc" + }, + "description": "\uc774\uba54\uc77c\ub85c \ubcf4\ub0b4\ub4dc\ub9b0 PIN \uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694. \uc774\uba54\uc77c\uc5d0 PIN \uc774 \ud3ec\ud568\ub418\uc9c0 \uc54a\uc740 \uacbd\uc6b0 \ube44\uc6cc \ub461\ub2c8\ub2e4", + "title": "2\ub2e8\uacc4 \uc778\uc99d" + }, + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "title": "Blink \uacc4\uc815\uc73c\ub85c \ub85c\uadf8\uc778\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blink/translations/lb.json b/homeassistant/components/blink/translations/lb.json new file mode 100644 index 00000000000..27ab3e6fd87 --- /dev/null +++ b/homeassistant/components/blink/translations/lb.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "2fa": { + "data": { + "2fa": "2-Faktor Code" + }, + "description": "G\u00ebff de PIn un dee per E-Mail versch\u00e9ckt gouf. Falls an der E-Mail kee PIN steht, einfach eidel loossen", + "title": "2-Faktor-Authentifikatioun" + }, + "user": { + "data": { + "password": "Passwuert", + "username": "Benotzernumm" + }, + "title": "Mam Blink Kont verbannen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blink/translations/nl.json b/homeassistant/components/blink/translations/nl.json new file mode 100644 index 00000000000..2af76f4f414 --- /dev/null +++ b/homeassistant/components/blink/translations/nl.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "unknown": "Onverwachte fout" + }, + "step": { + "2fa": { + "data": { + "2fa": "Twee-factor code" + }, + "description": "Voer de pincode in die naar uw e-mail is gestuurd. Als de e-mail geen pincode bevat, laat u dit leeg", + "title": "Tweestapsverificatie" + }, + "user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "title": "Aanmelden met Blink account" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blink/translations/no.json b/homeassistant/components/blink/translations/no.json new file mode 100644 index 00000000000..910a401b0be --- /dev/null +++ b/homeassistant/components/blink/translations/no.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerde konfigurert" + }, + "error": { + "invalid_auth": "Ugyldig legitimasjon", + "unknown": "Uventet feil" + }, + "step": { + "2fa": { + "data": { + "2fa": "To-faktorskode" + }, + "description": "Skriv inn pinnen som er sendt til din e-post. Hvis e-posten ikke inneholder en pin, m\u00e5 du la den st\u00e5 tom", + "title": "Totrinnsverifisering" + }, + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "title": "Logg p\u00e5 med Blink-konto" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blink/translations/pl.json b/homeassistant/components/blink/translations/pl.json new file mode 100644 index 00000000000..99e3f9aebd9 --- /dev/null +++ b/homeassistant/components/blink/translations/pl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + }, + "error": { + "invalid_auth": "Niepoprawne uwierzytelnienie.", + "unknown": "Nieoczekiwany b\u0142\u0105d." + }, + "step": { + "2fa": { + "data": { + "2fa": "Kod uwierzytelniania dwusk\u0142adnikowego" + }, + "description": "Wpisz kod PIN wys\u0142any na Tw\u00f3j adres e-mail. Je\u015bli wiadomo\u015b\u0107 e-mail nie zawiera kodu PIN, pozostaw pole puste.", + "title": "Uwierzytelnianie dwusk\u0142adnikowe" + }, + "user": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "title": "Zaloguj si\u0119 za pomoc\u0105 konta Blink" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blink/translations/pt-BR.json b/homeassistant/components/blink/translations/pt-BR.json new file mode 100644 index 00000000000..70d8b8620c4 --- /dev/null +++ b/homeassistant/components/blink/translations/pt-BR.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "2fa": { + "data": { + "2fa": "C\u00f3digo de dois fatores" + }, + "description": "Digite o pin enviado para o seu e-mail. Se o e-mail n\u00e3o contiver um pin, deixe em branco", + "title": "Autentica\u00e7\u00e3o de dois fatores" + }, + "user": { + "data": { + "password": "Senha", + "username": "Nome de usu\u00e1rio" + }, + "title": "Entrar com a conta Blink" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/config_flow.py b/homeassistant/components/braviatv/config_flow.py index 660e2e83ea1..d7db38c5c2a 100644 --- a/homeassistant/components/braviatv/config_flow.py +++ b/homeassistant/components/braviatv/config_flow.py @@ -4,6 +4,7 @@ import logging import re from bravia_tv import BraviaRC +from bravia_tv.braviarc import NoIPControl import voluptuous as vol from homeassistant import config_entries, exceptions @@ -51,7 +52,7 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def init_device(self, pin): """Initialize Bravia TV device.""" await self.hass.async_add_executor_job( - self.braviarc.connect, pin, CLIENTID_PREFIX, NICKNAME, + self.braviarc.connect, pin, CLIENTID_PREFIX, NICKNAME ) if not self.braviarc.is_connected(): @@ -85,6 +86,9 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except CannotConnect: _LOGGER.error("Import aborted, cannot connect to %s", self.host) return self.async_abort(reason="cannot_connect") + except NoIPControl: + _LOGGER.error("IP Control is disabled in the TV settings") + return self.async_abort(reason="no_ip_control") except ModelNotSupported: _LOGGER.error("Import aborted, your TV is not supported") return self.async_abort(reason="unsupported_model") @@ -129,9 +133,12 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=self.title, data=user_input) # Connecting with th PIN "0000" to start the pairing process on the TV. - await self.hass.async_add_executor_job( - self.braviarc.connect, "0000", CLIENTID_PREFIX, NICKNAME, - ) + try: + await self.hass.async_add_executor_job( + self.braviarc.connect, "0000", CLIENTID_PREFIX, NICKNAME + ) + except NoIPControl: + return self.async_abort(reason="no_ip_control") return self.async_show_form( step_id="authorize", @@ -156,7 +163,7 @@ class BraviaTVOptionsFlowHandler(config_entries.OptionsFlow): self.braviarc = self.hass.data[DOMAIN][self.config_entry.entry_id][BRAVIARC] if not self.braviarc.is_connected(): await self.hass.async_add_executor_job( - self.braviarc.connect, self.pin, CLIENTID_PREFIX, NICKNAME, + self.braviarc.connect, self.pin, CLIENTID_PREFIX, NICKNAME ) content_mapping = await self.hass.async_add_executor_job( diff --git a/homeassistant/components/braviatv/manifest.json b/homeassistant/components/braviatv/manifest.json index 0936c1f9088..7ed09ee018d 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": ["bravia-tv==1.0.4"], + "requirements": ["bravia-tv==1.0.5"], "codeowners": ["@robbiet480", "@bieniu"], "config_flow": true } diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index eb75542460f..f6c023481c0 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -2,6 +2,7 @@ import asyncio import logging +from bravia_tv.braviarc import NoIPControl import voluptuous as vol from homeassistant.components.media_player import ( @@ -162,9 +163,12 @@ class BraviaTVDevice(MediaPlayerEntity): ) if power_status == "active": if self._need_refresh: - connected = await self.hass.async_add_executor_job( - self._braviarc.connect, self._pin, CLIENTID_PREFIX, NICKNAME - ) + try: + connected = await self.hass.async_add_executor_job( + self._braviarc.connect, self._pin, CLIENTID_PREFIX, NICKNAME + ) + except NoIPControl: + _LOGGER.error("IP Control is disabled in the TV settings") self._need_refresh = False else: connected = self._braviarc.is_connected() diff --git a/homeassistant/components/braviatv/strings.json b/homeassistant/components/braviatv/strings.json index ca432270cbb..c066f91d395 100644 --- a/homeassistant/components/braviatv/strings.json +++ b/homeassistant/components/braviatv/strings.json @@ -22,7 +22,8 @@ "unsupported_model": "Your TV model is not supported." }, "abort": { - "already_configured": "This TV is already configured." + "already_configured": "This TV is already configured.", + "no_ip_control": "IP Control is disabled on your TV or the TV is not supported." } }, "options": { diff --git a/homeassistant/components/braviatv/translations/ca.json b/homeassistant/components/braviatv/translations/ca.json index 5a6d50c5c53..c35dc3ce857 100644 --- a/homeassistant/components/braviatv/translations/ca.json +++ b/homeassistant/components/braviatv/translations/ca.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Aquest televisor ja est\u00e0 configurat." + "already_configured": "Aquest televisor ja est\u00e0 configurat.", + "no_ip_control": "El control IP del teu televisor est\u00e0 desactivat o aquest no \u00e9s compatible." }, "error": { "cannot_connect": "No s'ha pogut connectar, amfitri\u00f3 o codi PIN inv\u00e0lids.", @@ -18,7 +19,7 @@ }, "user": { "data": { - "host": "Nom d'amfitri\u00f3 o adre\u00e7a IP del televisor" + "host": "Amfitri\u00f3" }, "description": "Configura la integraci\u00f3 de televisor Sony Bravia. Si tens problemes durant la configuraci\u00f3, v\u00e9s a: https://www.home-assistant.io/integrations/braviatv\n\nAssegura't que el televisor estigui engegat.", "title": "Televisor Sony Bravia" diff --git a/homeassistant/components/braviatv/translations/en.json b/homeassistant/components/braviatv/translations/en.json index c9bc5a4469b..98a569b3ab1 100644 --- a/homeassistant/components/braviatv/translations/en.json +++ b/homeassistant/components/braviatv/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "This TV is already configured." + "already_configured": "This TV is already configured.", + "no_ip_control": "IP Control is disabled on your TV or the TV is not supported." }, "error": { "cannot_connect": "Failed to connect, invalid host or PIN code.", diff --git a/homeassistant/components/braviatv/translations/es.json b/homeassistant/components/braviatv/translations/es.json index fa858a73079..61dac5c9d62 100644 --- a/homeassistant/components/braviatv/translations/es.json +++ b/homeassistant/components/braviatv/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Este televisor ya est\u00e1 configurado." + "already_configured": "Este televisor ya est\u00e1 configurado.", + "no_ip_control": "El Control de IP est\u00e1 desactivado en tu televisor o el televisor no es compatible." }, "error": { "cannot_connect": "No se pudo conectar, host o c\u00f3digo PIN no v\u00e1lido.", diff --git a/homeassistant/components/wwlln/translations/hu.json b/homeassistant/components/braviatv/translations/hu.json similarity index 54% rename from homeassistant/components/wwlln/translations/hu.json rename to homeassistant/components/braviatv/translations/hu.json index 740fc1a8179..cbf055e2fba 100644 --- a/homeassistant/components/wwlln/translations/hu.json +++ b/homeassistant/components/braviatv/translations/hu.json @@ -3,8 +3,7 @@ "step": { "user": { "data": { - "latitude": "Sz\u00e9less\u00e9g", - "longitude": "Hossz\u00fas\u00e1g" + "host": "Hoszt" } } } diff --git a/homeassistant/components/braviatv/translations/it.json b/homeassistant/components/braviatv/translations/it.json index c6fe7db4439..46cb5fca7a4 100644 --- a/homeassistant/components/braviatv/translations/it.json +++ b/homeassistant/components/braviatv/translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Questo televisore \u00e8 gi\u00e0 configurato." + "already_configured": "Questo televisore \u00e8 gi\u00e0 configurato.", + "no_ip_control": "Il controllo IP \u00e8 disabilitato sulla TV o la TV non \u00e8 supportata." }, "error": { "cannot_connect": "Connessione non riuscita, host o codice PIN non valido.", @@ -18,7 +19,7 @@ }, "user": { "data": { - "host": "Nome host TV o indirizzo IP" + "host": "Host" }, "description": "Configurare l'integrazione TV di Sony Bravia. In caso di problemi con la configurazione visitare: https://www.home-assistant.io/integrations/braviatv\n\nAssicurarsi che il televisore sia acceso.", "title": "Sony Bravia TV" diff --git a/homeassistant/components/braviatv/translations/ko.json b/homeassistant/components/braviatv/translations/ko.json index 593fd997709..e8c433ccbfc 100644 --- a/homeassistant/components/braviatv/translations/ko.json +++ b/homeassistant/components/braviatv/translations/ko.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\uc774 TV \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uc774 TV \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "no_ip_control": "TV \uc5d0\uc11c IP \uc81c\uc5b4\uac00 \ube44\ud65c\uc131\ud654\ub418\uc5c8\uac70\ub098 TV \uac00 \uc9c0\uc6d0\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ud638\uc2a4\ud2b8 \ub610\ub294 PIN \ucf54\ub4dc\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", diff --git a/homeassistant/components/braviatv/translations/lb.json b/homeassistant/components/braviatv/translations/lb.json index 37366eee6dd..7a00464bc3a 100644 --- a/homeassistant/components/braviatv/translations/lb.json +++ b/homeassistant/components/braviatv/translations/lb.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "D\u00ebse Fernseh ass scho konfigur\u00e9iert." + "already_configured": "D\u00ebse Fernseh ass scho konfigur\u00e9iert.", + "no_ip_control": "IP Kontroll ass d\u00e9aktiv\u00e9iert um TV oder den TV g\u00ebtt net \u00ebnnerst\u00ebtzt." }, "error": { "cannot_connect": "Feeler beim verbannen, ong\u00ebltege Numm oder PIN code.", diff --git a/homeassistant/components/braviatv/translations/nl.json b/homeassistant/components/braviatv/translations/nl.json index ba09fbca3a3..b35d7de45cf 100644 --- a/homeassistant/components/braviatv/translations/nl.json +++ b/homeassistant/components/braviatv/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Deze tv is al geconfigureerd." + "already_configured": "Deze tv is al geconfigureerd.", + "no_ip_control": "IP-besturing is uitgeschakeld op uw tv of de tv wordt niet ondersteund." }, "error": { "cannot_connect": "Geen verbinding, ongeldige host of PIN-code.", diff --git a/homeassistant/components/braviatv/translations/no.json b/homeassistant/components/braviatv/translations/no.json index f6100b7843a..ff86974f763 100644 --- a/homeassistant/components/braviatv/translations/no.json +++ b/homeassistant/components/braviatv/translations/no.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Denne TV-en er allerede konfigurert." + "already_configured": "Denne TV-en er allerede konfigurert.", + "no_ip_control": "IP-kontrollen er deaktivert p\u00e5 TVen eller TV-en st\u00f8ttes ikke." }, "error": { "cannot_connect": "Kunne ikke koble til, ugyldig vert eller PIN-kode.", diff --git a/homeassistant/components/braviatv/translations/pl.json b/homeassistant/components/braviatv/translations/pl.json index e20679c4172..3b8262a5559 100644 --- a/homeassistant/components/braviatv/translations/pl.json +++ b/homeassistant/components/braviatv/translations/pl.json @@ -13,12 +13,12 @@ "data": { "pin": "Kod PIN" }, - "description": "Wprowad\u017a kod PIN wy\u015bwietlany na telewizorze Sony Bravia. \n\nJe\u015bli kod PIN nie jest wy\u015bwietlany, musisz wyrejestrowa\u0107 Home Assistant'a na swoim telewizorze, przejd\u017a do Ustawienia -> Sie\u0107 -> Ustawienia urz\u0105dzenia zdalnego -> Wyrejestruj urz\u0105dzenie zdalne.", + "description": "Wprowad\u017a kod PIN wy\u015bwietlany na telewizorze Sony Bravia. \n\nJe\u015bli kod PIN nie jest wy\u015bwietlany, musisz wyrejestrowa\u0107 Home Assistanta na swoim telewizorze, przejd\u017a do Ustawienia -> Sie\u0107 -> Ustawienia urz\u0105dzenia zdalnego -> Wyrejestruj urz\u0105dzenie zdalne.", "title": "Autoryzacja Sony Bravia TV" }, "user": { "data": { - "host": "[%key_id:common::config_flow::data::host%]" + "host": "Nazwa hosta lub adres IP" }, "description": "Konfiguracja integracji telewizora Sony Bravia. Je\u015bli masz problemy z konfiguracj\u0105, przejd\u017a do strony: https://www.home-assistant.io/integrations/braviatv\n\nUpewnij si\u0119, \u017ce telewizor jest w\u0142\u0105czony.", "title": "Sony Bravia TV" diff --git a/homeassistant/components/braviatv/translations/pt-BR.json b/homeassistant/components/braviatv/translations/pt-BR.json new file mode 100644 index 00000000000..1a0fedff9d0 --- /dev/null +++ b/homeassistant/components/braviatv/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "description": "Configure a integra\u00e7\u00e3o do Sony Bravia TV. Se voc\u00ea tiver problemas com a configura\u00e7\u00e3o, acesse: https://www.home-assistant.io/integrations/braviatv \n\n Verifique se a sua TV est\u00e1 ligada.", + "title": "Sony Bravia TV" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "ignored_sources": "Lista de fontes ignoradas" + }, + "title": "Op\u00e7\u00f5es para a Sony Bravia TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/ru.json b/homeassistant/components/braviatv/translations/ru.json index e1cabf3bf4e..a799a29831f 100644 --- a/homeassistant/components/braviatv/translations/ru.json +++ b/homeassistant/components/braviatv/translations/ru.json @@ -1,7 +1,8 @@ { "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_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.", + "no_ip_control": "\u041d\u0430 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u043e IP, \u043b\u0438\u0431\u043e \u044d\u0442\u0430 \u043c\u043e\u0434\u0435\u043b\u044c \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0445\u043e\u0441\u0442 \u0438\u043b\u0438 PIN-\u043a\u043e\u0434.", diff --git a/homeassistant/components/braviatv/translations/zh-Hant.json b/homeassistant/components/braviatv/translations/zh-Hant.json index 5841c550330..027e44abaa4 100644 --- a/homeassistant/components/braviatv/translations/zh-Hant.json +++ b/homeassistant/components/braviatv/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u6b64\u96fb\u8996\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" + "already_configured": "\u6b64\u96fb\u8996\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002", + "no_ip_control": "\u96fb\u8996\u4e0a\u7684 IP \u5df2\u95dc\u9589\u6216\u4e0d\u652f\u63f4\u6b64\u6b3e\u96fb\u8996\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u7121\u6548\u7684\u4e3b\u6a5f\u540d\u7a31\u6216 PIN \u78bc\u3002", diff --git a/homeassistant/components/broadlink/__init__.py b/homeassistant/components/broadlink/__init__.py index 573538f63bb..d8b7f60b5b4 100644 --- a/homeassistant/components/broadlink/__init__.py +++ b/homeassistant/components/broadlink/__init__.py @@ -31,12 +31,12 @@ def data_packet(value): def hostname(value): """Validate a hostname.""" - host = str(value).lower() + host = str(value) if len(host) > 253: raise ValueError if host[-1] == ".": host = host[:-1] - allowed = re.compile(r"(?!-)[a-z\d-]{1,63}(? None: """Initialize calendar view.""" self.component = component @@ -200,11 +200,11 @@ class CalendarListView(http.HomeAssistantView): url = "/api/calendars" name = "api:calendars" - def __init__(self, component): + def __init__(self, component: EntityComponent) -> None: """Initialize calendar view.""" self.component = component - async def get(self, request): + async def get(self, request: web.Request) -> web.Response: """Retrieve calendar list.""" hass = request.app["hass"] calendar_list = [] diff --git a/homeassistant/components/calendar/translations/ca.json b/homeassistant/components/calendar/translations/ca.json index 5e842769c51..f1b3279a4cb 100644 --- a/homeassistant/components/calendar/translations/ca.json +++ b/homeassistant/components/calendar/translations/ca.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "Desactivat", - "on": "Activat" + "off": "OFF", + "on": "ON" } }, "title": "Calendari" diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 0b2c1e77d3f..fb33bef7d52 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -34,6 +34,7 @@ from homeassistant.components.stream.const import ( from homeassistant.const import ( ATTR_ENTITY_ID, CONF_FILENAME, + EVENT_HOMEASSISTANT_START, SERVICE_TURN_OFF, SERVICE_TURN_ON, ) @@ -48,7 +49,6 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.network import get_url from homeassistant.loader import bind_hass -from homeassistant.setup import async_when_setup from .const import DATA_CAMERA_PREFS, DOMAIN from .prefs import CameraPreferences @@ -161,6 +161,14 @@ async def async_get_image(hass, entity_id, timeout=10): raise HomeAssistantError("Unable to get image") +@bind_hass +async def async_get_stream_source(hass, entity_id): + """Fetch the stream source for a camera entity.""" + camera = _get_camera_from_entity_id(hass, entity_id) + + return await camera.stream_source() + + @bind_hass async def async_get_mjpeg_stream(hass, request, entity_id): """Fetch an mjpeg stream from a camera entity.""" @@ -251,7 +259,7 @@ async def async_setup(hass, config): await component.async_setup(config) - async def preload_stream(hass, _): + async def preload_stream(_): for camera in component.entities: camera_prefs = prefs.get(camera.entity_id) if not camera_prefs.preload_stream: @@ -265,7 +273,7 @@ async def async_setup(hass, config): request_stream(hass, source, keepalive=True, options=camera.stream_options) - async_when_setup(hass, DOMAIN_STREAM, preload_stream) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, preload_stream) @callback def update_tokens(time): @@ -465,11 +473,11 @@ class CameraView(HomeAssistantView): requires_auth = False - def __init__(self, component): + def __init__(self, component: EntityComponent) -> None: """Initialize a basic camera view.""" self.component = component - async def get(self, request, entity_id): + async def get(self, request: web.Request, entity_id: str) -> web.Response: """Start a GET request.""" camera = self.component.get_entity(entity_id) @@ -501,7 +509,7 @@ class CameraImageView(CameraView): url = "/api/camera_proxy/{entity_id}" name = "api:camera:image" - async def handle(self, request, camera): + async def handle(self, request: web.Request, camera: Camera) -> web.Response: """Serve camera image.""" with suppress(asyncio.CancelledError, asyncio.TimeoutError): async with async_timeout.timeout(10): @@ -519,7 +527,7 @@ class CameraMjpegStream(CameraView): url = "/api/camera_proxy_stream/{entity_id}" name = "api:camera:stream" - async def handle(self, request, camera): + async def handle(self, request: web.Request, camera: Camera) -> web.Response: """Serve camera stream, possibly with interval.""" interval = request.query.get("interval") if interval is None: diff --git a/homeassistant/components/canary/__init__.py b/homeassistant/components/canary/__init__.py index d6effc7eb80..165787a7f46 100644 --- a/homeassistant/components/canary/__init__.py +++ b/homeassistant/components/canary/__init__.py @@ -71,7 +71,6 @@ class CanaryData: self._locations_by_id = {} self._readings_by_device_id = {} - self._entries_by_location_id = {} self.update() @@ -82,9 +81,6 @@ class CanaryData: location_id = location.location_id self._locations_by_id[location_id] = location - self._entries_by_location_id[location_id] = self._api.get_entries( - location_id, entry_type="motion", limit=1 - ) for device in location.devices: if device.is_online: @@ -97,10 +93,6 @@ class CanaryData: """Return a list of locations.""" return self._locations_by_id.values() - def get_motion_entries(self, location_id): - """Return a list of motion entries based on location_id.""" - return self._entries_by_location_id.get(location_id, []) - def get_location(self, location_id): """Return a location based on location_id.""" return self._locations_by_id.get(location_id, []) diff --git a/homeassistant/components/cast/discovery.py b/homeassistant/components/cast/discovery.py index b15346417de..e7d13ea0a18 100644 --- a/homeassistant/components/cast/discovery.py +++ b/homeassistant/components/cast/discovery.py @@ -84,11 +84,12 @@ def setup_internal_discovery(hass: HomeAssistant) -> None: ) _LOGGER.debug("Starting internal pychromecast discovery.") - listener, browser = pychromecast.start_discovery( + listener = pychromecast.CastListener( internal_add_callback, internal_remove_callback, - ChromeCastZeroconf.get_zeroconf(), + internal_add_callback, # Use internal_add_callback also for updates ) + browser = pychromecast.start_discovery(listener, ChromeCastZeroconf.get_zeroconf()) def stop_discovery(event): """Stop discovery of new chromecasts.""" diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 0be595de549..edf0373dd5d 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==5.3.0"], + "requirements": ["pychromecast==6.0.0"], "after_dependencies": ["cloud","zeroconf"], "zeroconf": ["_googlecast._tcp.local."], "codeowners": ["@emontnemery"] diff --git a/homeassistant/components/cast/translations/bg.json b/homeassistant/components/cast/translations/bg.json index 746278595d0..92a840cc5af 100644 --- a/homeassistant/components/cast/translations/bg.json +++ b/homeassistant/components/cast/translations/bg.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Google Cast?", - "title": "Google Cast" + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Google Cast?" } } } diff --git a/homeassistant/components/cast/translations/ca.json b/homeassistant/components/cast/translations/ca.json index 0b358293304..02a2459fdc0 100644 --- a/homeassistant/components/cast/translations/ca.json +++ b/homeassistant/components/cast/translations/ca.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Vols configurar Google Cast?", - "title": "Google Cast" + "description": "Vols configurar Google Cast?" } } } diff --git a/homeassistant/components/cast/translations/cs.json b/homeassistant/components/cast/translations/cs.json index 79694e427ca..3f67763b48b 100644 --- a/homeassistant/components/cast/translations/cs.json +++ b/homeassistant/components/cast/translations/cs.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Chcete nastavit Google Cast?", - "title": "Google Cast" + "description": "Chcete nastavit Google Cast?" } } } diff --git a/homeassistant/components/cast/translations/da.json b/homeassistant/components/cast/translations/da.json index 3230e215bff..fe6ed03bb5a 100644 --- a/homeassistant/components/cast/translations/da.json +++ b/homeassistant/components/cast/translations/da.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Vil du ops\u00e6tte Google Cast?", - "title": "Google Cast" + "description": "Vil du ops\u00e6tte Google Cast?" } } } diff --git a/homeassistant/components/cast/translations/de.json b/homeassistant/components/cast/translations/de.json index 5a87e714bf6..87f8e7cb2bc 100644 --- a/homeassistant/components/cast/translations/de.json +++ b/homeassistant/components/cast/translations/de.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "M\u00f6chtest du Google Cast einrichten?", - "title": "Google Cast" + "description": "M\u00f6chtest du Google Cast einrichten?" } } } diff --git a/homeassistant/components/cast/translations/en.json b/homeassistant/components/cast/translations/en.json index 81ba0457240..e06a94f700b 100644 --- a/homeassistant/components/cast/translations/en.json +++ b/homeassistant/components/cast/translations/en.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Do you want to set up Google Cast?", - "title": "Google Cast" + "description": "Do you want to set up Google Cast?" } } } diff --git a/homeassistant/components/cast/translations/es-419.json b/homeassistant/components/cast/translations/es-419.json index c4374997d8a..c62ece17721 100644 --- a/homeassistant/components/cast/translations/es-419.json +++ b/homeassistant/components/cast/translations/es-419.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u00bfDesea configurar Google Cast?", - "title": "Google Cast" + "description": "\u00bfDesea configurar Google Cast?" } } } diff --git a/homeassistant/components/cast/translations/es.json b/homeassistant/components/cast/translations/es.json index 3a5f5653237..9b4efee18d5 100644 --- a/homeassistant/components/cast/translations/es.json +++ b/homeassistant/components/cast/translations/es.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u00bfQuieres configurar Google Cast?", - "title": "Google Cast" + "description": "\u00bfQuieres configurar Google Cast?" } } } diff --git a/homeassistant/components/cast/translations/fi.json b/homeassistant/components/cast/translations/fi.json index 21ec8206165..730f3ebf9b3 100644 --- a/homeassistant/components/cast/translations/fi.json +++ b/homeassistant/components/cast/translations/fi.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Haluatko m\u00e4\u00e4ritt\u00e4\u00e4 Google Castin?", - "title": "Google Cast" + "description": "Haluatko m\u00e4\u00e4ritt\u00e4\u00e4 Google Castin?" } } } diff --git a/homeassistant/components/cast/translations/fr.json b/homeassistant/components/cast/translations/fr.json index a4fef5a7c52..afa4b094fad 100644 --- a/homeassistant/components/cast/translations/fr.json +++ b/homeassistant/components/cast/translations/fr.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Voulez-vous configurer Google Cast?", - "title": "Google Cast" + "description": "Voulez-vous configurer Google Cast?" } } } diff --git a/homeassistant/components/cast/translations/he.json b/homeassistant/components/cast/translations/he.json index 019561c2b87..0bc4e834cb7 100644 --- a/homeassistant/components/cast/translations/he.json +++ b/homeassistant/components/cast/translations/he.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea Google Cast?", - "title": "Google Cast" + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea Google Cast?" } } } diff --git a/homeassistant/components/cast/translations/hu.json b/homeassistant/components/cast/translations/hu.json index 83f0f959c71..dc55cd224f8 100644 --- a/homeassistant/components/cast/translations/hu.json +++ b/homeassistant/components/cast/translations/hu.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a Google Cast szolg\u00e1ltat\u00e1st?", - "title": "Google Cast" + "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a Google Cast szolg\u00e1ltat\u00e1st?" } } } diff --git a/homeassistant/components/cast/translations/id.json b/homeassistant/components/cast/translations/id.json index f4d66facce1..d3e2bb5f360 100644 --- a/homeassistant/components/cast/translations/id.json +++ b/homeassistant/components/cast/translations/id.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Apakah Anda ingin menyiapkan Google Cast?", - "title": "Google Cast" + "description": "Apakah Anda ingin menyiapkan Google Cast?" } } } diff --git a/homeassistant/components/cast/translations/it.json b/homeassistant/components/cast/translations/it.json index ba3cdc0645d..6e56218c992 100644 --- a/homeassistant/components/cast/translations/it.json +++ b/homeassistant/components/cast/translations/it.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Vuoi configurare Google Cast?", - "title": "Google Cast" + "description": "Vuoi configurare Google Cast?" } } } diff --git a/homeassistant/components/cast/translations/ja.json b/homeassistant/components/cast/translations/ja.json index f078c7c13e9..fa1d4031562 100644 --- a/homeassistant/components/cast/translations/ja.json +++ b/homeassistant/components/cast/translations/ja.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "Google Cast\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f", - "title": "Google Cast" + "description": "Google Cast\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" } } } diff --git a/homeassistant/components/cast/translations/ko.json b/homeassistant/components/cast/translations/ko.json index 1de8e74ec84..e57fceb7705 100644 --- a/homeassistant/components/cast/translations/ko.json +++ b/homeassistant/components/cast/translations/ko.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Google \uce90\uc2a4\ud2b8\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "Google \uce90\uc2a4\ud2b8" + "description": "Google \uce90\uc2a4\ud2b8\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" } } } diff --git a/homeassistant/components/cast/translations/lb.json b/homeassistant/components/cast/translations/lb.json index 9ea5e36b379..813ffd10072 100644 --- a/homeassistant/components/cast/translations/lb.json +++ b/homeassistant/components/cast/translations/lb.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Soll Google Cast konfigur\u00e9iert ginn?", - "title": "Google Cast" + "description": "Soll Google Cast konfigur\u00e9iert ginn?" } } } diff --git a/homeassistant/components/cast/translations/nl.json b/homeassistant/components/cast/translations/nl.json index b22c25f1292..d42ef3e850c 100644 --- a/homeassistant/components/cast/translations/nl.json +++ b/homeassistant/components/cast/translations/nl.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Wilt u Google Cast instellen?", - "title": "Google Cast" + "description": "Wilt u Google Cast instellen?" } } } diff --git a/homeassistant/components/cast/translations/nn.json b/homeassistant/components/cast/translations/nn.json index 0c7d56d3ad8..44d26792812 100644 --- a/homeassistant/components/cast/translations/nn.json +++ b/homeassistant/components/cast/translations/nn.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Vil du sette opp Google Cast?", - "title": "Google Cast" + "description": "Vil du sette opp Google Cast?" } } } diff --git a/homeassistant/components/cast/translations/no.json b/homeassistant/components/cast/translations/no.json index de041d89f81..b96be399cc8 100644 --- a/homeassistant/components/cast/translations/no.json +++ b/homeassistant/components/cast/translations/no.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u00d8nsker du \u00e5 sette opp Google Cast?", - "title": "" + "description": "\u00d8nsker du \u00e5 sette opp Google Cast?" } } } diff --git a/homeassistant/components/cast/translations/pl.json b/homeassistant/components/cast/translations/pl.json index 26897da5da3..965ee22ee5d 100644 --- a/homeassistant/components/cast/translations/pl.json +++ b/homeassistant/components/cast/translations/pl.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Czy chcesz skonfigurowa\u0107 Google Cast?", - "title": "Google Cast" + "description": "Czy chcesz skonfigurowa\u0107 Google Cast?" } } } diff --git a/homeassistant/components/cast/translations/pt-BR.json b/homeassistant/components/cast/translations/pt-BR.json index 25c0adf866f..000971f9e4c 100644 --- a/homeassistant/components/cast/translations/pt-BR.json +++ b/homeassistant/components/cast/translations/pt-BR.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Deseja configurar o Google Cast?", - "title": "Google Cast" + "description": "Deseja configurar o Google Cast?" } } } diff --git a/homeassistant/components/cast/translations/pt.json b/homeassistant/components/cast/translations/pt.json index dcf0391c420..9dd9a69a94c 100644 --- a/homeassistant/components/cast/translations/pt.json +++ b/homeassistant/components/cast/translations/pt.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Deseja configurar o Google Cast?", - "title": "Google Cast" + "description": "Deseja configurar o Google Cast?" } } } diff --git a/homeassistant/components/cast/translations/ro.json b/homeassistant/components/cast/translations/ro.json index 4dd8c04c381..6e93a0fdb1a 100644 --- a/homeassistant/components/cast/translations/ro.json +++ b/homeassistant/components/cast/translations/ro.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Dori\u021bi s\u0103 configura\u021bi Google Cast?", - "title": "Google Cast" + "description": "Dori\u021bi s\u0103 configura\u021bi Google Cast?" } } } diff --git a/homeassistant/components/cast/translations/ru.json b/homeassistant/components/cast/translations/ru.json index fae0cd417ff..a62f33832e0 100644 --- a/homeassistant/components/cast/translations/ru.json +++ b/homeassistant/components/cast/translations/ru.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Google Cast?", - "title": "Google Cast" + "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 Google Cast?" } } } diff --git a/homeassistant/components/cast/translations/sl.json b/homeassistant/components/cast/translations/sl.json index eb4e930af86..c4d2ba98006 100644 --- a/homeassistant/components/cast/translations/sl.json +++ b/homeassistant/components/cast/translations/sl.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Ali \u017eelite nastaviti Google Cast?", - "title": "Google Cast" + "description": "Ali \u017eelite nastaviti Google Cast?" } } } diff --git a/homeassistant/components/cast/translations/sv.json b/homeassistant/components/cast/translations/sv.json index 937604b1000..056c00b1765 100644 --- a/homeassistant/components/cast/translations/sv.json +++ b/homeassistant/components/cast/translations/sv.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Vill du konfigurera Google Cast?", - "title": "Google Cast" + "description": "Vill du konfigurera Google Cast?" } } } diff --git a/homeassistant/components/cast/translations/th.json b/homeassistant/components/cast/translations/th.json index 9806057716a..64a5eaa3085 100644 --- a/homeassistant/components/cast/translations/th.json +++ b/homeassistant/components/cast/translations/th.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "\u0e04\u0e38\u0e13\u0e15\u0e49\u0e2d\u0e07\u0e01\u0e32\u0e23\u0e15\u0e31\u0e49\u0e07\u0e04\u0e48\u0e32 Google Cast \u0e2b\u0e23\u0e37\u0e2d\u0e44\u0e21\u0e48?", - "title": "Google Cast" + "description": "\u0e04\u0e38\u0e13\u0e15\u0e49\u0e2d\u0e07\u0e01\u0e32\u0e23\u0e15\u0e31\u0e49\u0e07\u0e04\u0e48\u0e32 Google Cast \u0e2b\u0e23\u0e37\u0e2d\u0e44\u0e21\u0e48?" } } } diff --git a/homeassistant/components/cast/translations/vi.json b/homeassistant/components/cast/translations/vi.json index 7e75cfce4fa..f65f3c58ebe 100644 --- a/homeassistant/components/cast/translations/vi.json +++ b/homeassistant/components/cast/translations/vi.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "B\u1ea1n c\u00f3 mu\u1ed1n thi\u1ebft l\u1eadp Google Cast kh\u00f4ng?", - "title": "Google Cast" + "description": "B\u1ea1n c\u00f3 mu\u1ed1n thi\u1ebft l\u1eadp Google Cast kh\u00f4ng?" } } } diff --git a/homeassistant/components/cast/translations/zh-Hans.json b/homeassistant/components/cast/translations/zh-Hans.json index 9f834a98990..1c2024f8b81 100644 --- a/homeassistant/components/cast/translations/zh-Hans.json +++ b/homeassistant/components/cast/translations/zh-Hans.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u60a8\u60f3\u8981\u914d\u7f6e Google Cast \u5417\uff1f", - "title": "Google Cast" + "description": "\u60a8\u60f3\u8981\u914d\u7f6e Google Cast \u5417\uff1f" } } } diff --git a/homeassistant/components/cast/translations/zh-Hant.json b/homeassistant/components/cast/translations/zh-Hant.json index 0b7101b5cc8..3bb3e70d688 100644 --- a/homeassistant/components/cast/translations/zh-Hant.json +++ b/homeassistant/components/cast/translations/zh-Hant.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a Google Cast\uff1f", - "title": "Google Cast" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Google Cast\uff1f" } } } diff --git a/homeassistant/components/cert_expiry/translations/ca.json b/homeassistant/components/cert_expiry/translations/ca.json index 5b9b095acbc..50d761f03c2 100644 --- a/homeassistant/components/cert_expiry/translations/ca.json +++ b/homeassistant/components/cert_expiry/translations/ca.json @@ -12,9 +12,9 @@ "step": { "user": { "data": { - "host": "Nom de l'amfitri\u00f3 del certificat", + "host": "Amfitri\u00f3", "name": "Nom del certificat", - "port": "Port del certificat" + "port": "Port" }, "title": "Configuraci\u00f3 del certificat a provar" } diff --git a/homeassistant/components/cert_expiry/translations/hu.json b/homeassistant/components/cert_expiry/translations/hu.json index 584f4c2b759..22e9312e778 100644 --- a/homeassistant/components/cert_expiry/translations/hu.json +++ b/homeassistant/components/cert_expiry/translations/hu.json @@ -3,8 +3,9 @@ "step": { "user": { "data": { + "host": "Hoszt", "name": "A tan\u00fas\u00edtv\u00e1ny neve", - "port": "A tan\u00fas\u00edtv\u00e1ny portja" + "port": "Port" } } } diff --git a/homeassistant/components/cert_expiry/translations/it.json b/homeassistant/components/cert_expiry/translations/it.json index c5e56bc95a2..29b26710b62 100644 --- a/homeassistant/components/cert_expiry/translations/it.json +++ b/homeassistant/components/cert_expiry/translations/it.json @@ -12,9 +12,9 @@ "step": { "user": { "data": { - "host": "L'hostname del certificato", + "host": "Host", "name": "Il nome del certificato", - "port": "La porta del certificato" + "port": "Porta" }, "title": "Definire il certificato da testare" } diff --git a/homeassistant/components/cert_expiry/translations/pl.json b/homeassistant/components/cert_expiry/translations/pl.json index b41dbdf9622..7f92253507c 100644 --- a/homeassistant/components/cert_expiry/translations/pl.json +++ b/homeassistant/components/cert_expiry/translations/pl.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "Ta kombinacja hosta i portu jest ju\u017c skonfigurowana", - "import_failed": "Import z konfiguracji nie powi\u00f3d\u0142 si\u0119" + "already_configured": "Ta kombinacja hosta i portu jest ju\u017c skonfigurowana.", + "import_failed": "Import z konfiguracji nie powi\u00f3d\u0142 si\u0119." }, "error": { "connection_refused": "Po\u0142\u0105czenie odrzucone podczas \u0142\u0105czenia z hostem", @@ -12,9 +12,9 @@ "step": { "user": { "data": { - "host": "Nazwa hosta certyfikatu", + "host": "Nazwa hosta lub adres IP", "name": "Nazwa certyfikatu", - "port": "[%key_id:common::config_flow::data::port%] certyfikatu" + "port": "Port" }, "title": "Zdefiniuj certyfikat do przetestowania" } diff --git a/homeassistant/components/circuit/__init__.py b/homeassistant/components/circuit/__init__.py new file mode 100644 index 00000000000..3d97e110475 --- /dev/null +++ b/homeassistant/components/circuit/__init__.py @@ -0,0 +1,38 @@ +"""The Unify Circuit component.""" + +import logging + +import voluptuous as vol + +from homeassistant.const import CONF_NAME, CONF_URL +from homeassistant.helpers import config_validation as cv, discovery + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "circuit" +CONF_WEBHOOK = "webhook" + +WEBHOOK_SCHEMA = vol.Schema( + {vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_URL): cv.string} +) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + {vol.Required(CONF_WEBHOOK): vol.All(cv.ensure_list, [WEBHOOK_SCHEMA])} + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Set up the Unify Circuit component.""" + webhooks = config[DOMAIN][CONF_WEBHOOK] + + for webhook_conf in webhooks: + hass.async_create_task( + discovery.async_load_platform(hass, "notify", DOMAIN, webhook_conf, config) + ) + + return True diff --git a/homeassistant/components/circuit/manifest.json b/homeassistant/components/circuit/manifest.json new file mode 100644 index 00000000000..d6c43e18677 --- /dev/null +++ b/homeassistant/components/circuit/manifest.json @@ -0,0 +1,7 @@ +{ + "domain": "circuit", + "name": "Unify Circuit", + "documentation": "https://www.home-assistant.io/integrations/circuit", + "codeowners": ["@braam"], + "requirements": ["circuit-webhook==1.0.1"] +} diff --git a/homeassistant/components/circuit/notify.py b/homeassistant/components/circuit/notify.py new file mode 100644 index 00000000000..634ecb4f859 --- /dev/null +++ b/homeassistant/components/circuit/notify.py @@ -0,0 +1,37 @@ +"""Unify Circuit platform for notify component.""" +import logging + +from circuit_webhook import Circuit + +from homeassistant.components.notify import BaseNotificationService +from homeassistant.const import CONF_URL + +_LOGGER = logging.getLogger(__name__) + + +def get_service(hass, config, discovery_info=None): + """Get the Unify Circuit notification service.""" + if discovery_info is None: + return None + + return CircuitNotificationService(discovery_info) + + +class CircuitNotificationService(BaseNotificationService): + """Implement the notification service for Unify Circuit.""" + + def __init__(self, config): + """Initialize the service.""" + self.webhook_url = config[CONF_URL] + + def send_message(self, message=None, **kwargs): + """Send a message to the webhook.""" + + webhook_url = self.webhook_url + + if webhook_url and message: + try: + circuit_message = Circuit(url=webhook_url) + circuit_message.post(text=message) + except RuntimeError as err: + _LOGGER.error("Could not send notification. Error: %s", err) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index d3241791cf2..32dfaa0e8fb 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -118,29 +118,37 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: SERVICE_SET_PRESET_MODE, {vol.Required(ATTR_PRESET_MODE): cv.string}, "async_set_preset_mode", + [SUPPORT_PRESET_MODE], ) component.async_register_entity_service( SERVICE_SET_AUX_HEAT, {vol.Required(ATTR_AUX_HEAT): cv.boolean}, async_service_aux_heat, + [SUPPORT_AUX_HEAT], ) component.async_register_entity_service( - SERVICE_SET_TEMPERATURE, SET_TEMPERATURE_SCHEMA, async_service_temperature_set, + SERVICE_SET_TEMPERATURE, + SET_TEMPERATURE_SCHEMA, + async_service_temperature_set, + [SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE], ) component.async_register_entity_service( SERVICE_SET_HUMIDITY, {vol.Required(ATTR_HUMIDITY): vol.Coerce(float)}, "async_set_humidity", + [SUPPORT_TARGET_HUMIDITY], ) component.async_register_entity_service( SERVICE_SET_FAN_MODE, {vol.Required(ATTR_FAN_MODE): cv.string}, "async_set_fan_mode", + [SUPPORT_FAN_MODE], ) component.async_register_entity_service( SERVICE_SET_SWING_MODE, {vol.Required(ATTR_SWING_MODE): cv.string}, "async_set_swing_mode", + [SUPPORT_SWING_MODE], ) return True diff --git a/homeassistant/components/climate/const.py b/homeassistant/components/climate/const.py index b489071db57..af6c9364b18 100644 --- a/homeassistant/components/climate/const.py +++ b/homeassistant/components/climate/const.py @@ -84,6 +84,17 @@ CURRENT_HVAC_IDLE = "idle" CURRENT_HVAC_FAN = "fan" +# A list of possible HVAC actions. +CURRENT_HVAC_ACTIONS = [ + CURRENT_HVAC_OFF, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_COOL, + CURRENT_HVAC_DRY, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_FAN, +] + + ATTR_AUX_HEAT = "aux_heat" ATTR_CURRENT_HUMIDITY = "current_humidity" ATTR_CURRENT_TEMPERATURE = "current_temperature" diff --git a/homeassistant/components/climate/translations/ca.json b/homeassistant/components/climate/translations/ca.json index e2f3a58e2eb..3eb99744751 100644 --- a/homeassistant/components/climate/translations/ca.json +++ b/homeassistant/components/climate/translations/ca.json @@ -17,12 +17,12 @@ "state": { "_": { "auto": "Autom\u00e0tic", - "cool": "Refredar", - "dry": "Assecar", + "cool": "Refreda", + "dry": "Asseca", "fan_only": "Nom\u00e9s ventilador", - "heat": "Escalfar", - "heat_cool": "Escalfar/Refredar", - "off": "Apagat" + "heat": "Escalfa", + "heat_cool": "Escalfa/Refreda", + "off": "OFF" } }, "title": "Climatitzaci\u00f3" diff --git a/homeassistant/components/climate/translations/ko.json b/homeassistant/components/climate/translations/ko.json index c707c9c1eba..0923d166040 100644 --- a/homeassistant/components/climate/translations/ko.json +++ b/homeassistant/components/climate/translations/ko.json @@ -2,11 +2,11 @@ "device_automation": { "action_type": { "set_hvac_mode": "{entity_name} \uc758 HVAC \ubaa8\ub4dc \ubcc0\uacbd", - "set_preset_mode": "{entity_name} \uc758 \uc0ac\uc804 \uc124\uc815 \ubcc0\uacbd" + "set_preset_mode": "{entity_name} \uc758 \ud504\ub9ac\uc14b \ubcc0\uacbd" }, "condition_type": { "is_hvac_mode": "{entity_name} \uc774(\uac00) \ud2b9\uc815 HVAC \ubaa8\ub4dc\ub85c \uc124\uc815\ub418\uc5b4\uc788\uc73c\uba74", - "is_preset_mode": "{entity_name} \uc774(\uac00) \ud2b9\uc815 \uc0ac\uc804 \uc124\uc815 \ubaa8\ub4dc\ub85c \uc124\uc815\ub418\uc5b4\uc788\uc73c\uba74" + "is_preset_mode": "{entity_name} \uc774(\uac00) \ud2b9\uc815 \ud504\ub9ac\uc14b \ubaa8\ub4dc\ub85c \uc124\uc815\ub418\uc5b4\uc788\uc73c\uba74" }, "trigger_type": { "current_humidity_changed": "{entity_name} \uc774(\uac00) \uc2b5\ub3c4 \ubcc0\ud654\ub97c \uac10\uc9c0\ud560 \ub54c", diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index fcd6738b77c..b72aec18c34 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.34.3"], + "requirements": ["hass-nabucasa==0.34.6"], "dependencies": ["http", "webhook", "alexa"], "after_dependencies": ["google_assistant"], "codeowners": ["@home-assistant/cloud"] diff --git a/homeassistant/components/configurator/translations/ca.json b/homeassistant/components/configurator/translations/ca.json index 0a4ea1ab6fa..4c3ffe3b5c5 100644 --- a/homeassistant/components/configurator/translations/ca.json +++ b/homeassistant/components/configurator/translations/ca.json @@ -1,7 +1,7 @@ { "state": { "_": { - "configure": "Configurar", + "configure": "Configura", "configured": "Configurat" } }, diff --git a/homeassistant/components/coolmaster/translations/pl.json b/homeassistant/components/coolmaster/translations/pl.json index 0edbd6ed379..9b0e4bc5846 100644 --- a/homeassistant/components/coolmaster/translations/pl.json +++ b/homeassistant/components/coolmaster/translations/pl.json @@ -12,7 +12,7 @@ "fan_only": "Obs\u0142uga trybu \"tylko wentylator\"", "heat": "Obs\u0142uga trybu grzania", "heat_cool": "Obs\u0142uga automatycznego trybu grzanie/ch\u0142odzenie", - "host": "[%key_id:common::config_flow::data::host%]", + "host": "Nazwa hosta lub adres IP", "off": "Mo\u017ce by\u0107 wy\u0142\u0105czone" }, "title": "Skonfiguruj szczeg\u00f3\u0142y po\u0142\u0105czenia CoolMasterNet." diff --git a/homeassistant/components/coronavirus/manifest.json b/homeassistant/components/coronavirus/manifest.json index 5248cf38221..ae5083a5f98 100644 --- a/homeassistant/components/coronavirus/manifest.json +++ b/homeassistant/components/coronavirus/manifest.json @@ -3,6 +3,10 @@ "name": "Coronavirus (COVID-19)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/coronavirus", - "requirements": ["coronavirus==1.1.0"], - "codeowners": ["@home_assistant/core"] + "requirements": [ + "coronavirus==1.1.1" + ], + "codeowners": [ + "@home_assistant/core" + ] } diff --git a/homeassistant/components/coronavirus/translations/pt-BR.json b/homeassistant/components/coronavirus/translations/pt-BR.json new file mode 100644 index 00000000000..ab4a4904857 --- /dev/null +++ b/homeassistant/components/coronavirus/translations/pt-BR.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Este pa\u00eds j\u00e1 est\u00e1 configurado." + }, + "step": { + "user": { + "data": { + "country": "Pa\u00eds" + }, + "title": "Escolha um pa\u00eds para monitorar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/translations/ca.json b/homeassistant/components/cover/translations/ca.json index e54cc563da5..970661be215 100644 --- a/homeassistant/components/cover/translations/ca.json +++ b/homeassistant/components/cover/translations/ca.json @@ -13,8 +13,8 @@ "is_closing": "{entity_name} est\u00e0 tancant-se", "is_open": "{entity_name} est\u00e0 obert/a", "is_opening": "{entity_name} s'est\u00e0 obrint", - "is_position": "La posici\u00f3 de {entity_name} \u00e9s", - "is_tilt_position": "La posici\u00f3 d'inclinaci\u00f3 de {entity_name} \u00e9s" + "is_position": "La posici\u00f3 actual de {entity_name} \u00e9s", + "is_tilt_position": "La inclinaci\u00f3 actual de {entity_name} \u00e9s" }, "trigger_type": { "closed": "{entity_name} tancat/da", @@ -27,9 +27,9 @@ }, "state": { "_": { - "closed": "Tancada", + "closed": "Tancat/da", "closing": "Tancant", - "open": "Oberta", + "open": "Obert/a", "opening": "Obrint", "stopped": "Aturat" } diff --git a/homeassistant/components/cppm_tracker/device_tracker.py b/homeassistant/components/cppm_tracker/device_tracker.py index 1bb723091d4..a784fbd2f89 100644 --- a/homeassistant/components/cppm_tracker/device_tracker.py +++ b/homeassistant/components/cppm_tracker/device_tracker.py @@ -10,19 +10,17 @@ from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA, DeviceScanner, ) -from homeassistant.const import CONF_API_KEY, CONF_HOST +from homeassistant.const import CONF_API_KEY, CONF_CLIENT_ID, CONF_HOST import homeassistant.helpers.config_validation as cv SCAN_INTERVAL = timedelta(seconds=120) -CLIENT_ID = "client_id" - GRANT_TYPE = "client_credentials" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, - vol.Required(CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_ID): cv.string, vol.Required(CONF_API_KEY): cv.string, } ) @@ -37,7 +35,7 @@ def get_scanner(hass, config): "server": config[DOMAIN][CONF_HOST], "grant_type": GRANT_TYPE, "secret": config[DOMAIN][CONF_API_KEY], - "client": config[DOMAIN][CLIENT_ID], + "client": config[DOMAIN][CONF_CLIENT_ID], } cppm = ClearPass(data) if cppm.access_token is None: diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index 06735d7e3b8..35ea9ff6f35 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -12,6 +12,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_HOSTS, CONF_PASSWORD from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import Throttle @@ -28,11 +29,18 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) COMPONENT_TYPES = ["climate", "sensor", "switch"] CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - {vol.Optional(CONF_HOSTS, default=[]): vol.All(cv.ensure_list, [cv.string])} - ) - }, + vol.All( + cv.deprecated(DOMAIN, invalidation_version="0.113.0"), + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_HOSTS, default=[]): vol.All( + cv.ensure_list, [cv.string] + ) + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) @@ -149,7 +157,7 @@ class DaikinApi: """Return a device description for device registry.""" info = self.device.values return { - "identifieres": self.device.mac, + "connections": {(CONNECTION_NETWORK_MAC, self.device.mac)}, "manufacturer": "Daikin", "model": info.get("model"), "name": info.get("name"), diff --git a/homeassistant/components/daikin/config_flow.py b/homeassistant/components/daikin/config_flow.py index cd5be5cef29..5f2a8a0c0b1 100644 --- a/homeassistant/components/daikin/config_flow.py +++ b/homeassistant/components/daikin/config_flow.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_HOST, CONF_PASSWORD -from .const import CONF_KEY, CONF_UUID, KEY_IP, KEY_MAC, TIMEOUT +from .const import CONF_KEY, CONF_UUID, KEY_HOSTNAME, KEY_IP, KEY_MAC, TIMEOUT _LOGGER = logging.getLogger(__name__) @@ -124,3 +124,11 @@ class FlowHandler(config_entries.ConfigFlow): self._abort_if_unique_id_configured() self.host = discovery_info[KEY_IP] return await self.async_step_user() + + async def async_step_zeroconf(self, discovery_info): + """Prepare configuration for a discovered Daikin device.""" + _LOGGER.debug("Zeroconf discovery_info: %s", discovery_info) + await self.async_set_unique_id(discovery_info[KEY_HOSTNAME]) + self._abort_if_unique_id_configured() + self.host = discovery_info[CONF_HOST] + return await self.async_step_user() diff --git a/homeassistant/components/daikin/const.py b/homeassistant/components/daikin/const.py index 30d34b898d3..3e24325e5b1 100644 --- a/homeassistant/components/daikin/const.py +++ b/homeassistant/components/daikin/const.py @@ -64,5 +64,6 @@ CONF_UUID = "uuid" KEY_MAC = "mac" KEY_IP = "ip" +KEY_HOSTNAME = "hostname" TIMEOUT = 60 diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index 9b4e76e5eb1..f555174494b 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -3,7 +3,8 @@ "name": "Daikin AC", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/daikin", - "requirements": ["pydaikin==2.0.4"], + "requirements": ["pydaikin==2.1.1"], "codeowners": ["@fredrike"], + "zeroconf": ["_dkapi._tcp.local."], "quality_scale": "platinum" } diff --git a/homeassistant/components/daikin/sensor.py b/homeassistant/components/daikin/sensor.py index eeaa162c2d8..7ff79338a79 100644 --- a/homeassistant/components/daikin/sensor.py +++ b/homeassistant/components/daikin/sensor.py @@ -129,7 +129,7 @@ class DaikinPowerSensor(DaikinSensor): if self._device_attribute == ATTR_TOTAL_POWER: return round(self._api.device.current_total_power_consumption, 3) if self._device_attribute == ATTR_COOL_ENERGY: - return round(self._api.device.last_hour_cool_power_consumption, 3) + return round(self._api.device.last_hour_cool_energy_consumption, 3) if self._device_attribute == ATTR_HEAT_ENERGY: - return round(self._api.device.last_hour_heat_power_consumption, 3) + return round(self._api.device.last_hour_heat_energy_consumption, 3) return None diff --git a/homeassistant/components/daikin/translations/bg.json b/homeassistant/components/daikin/translations/bg.json index dd80874adaa..a1f8209b2bf 100644 --- a/homeassistant/components/daikin/translations/bg.json +++ b/homeassistant/components/daikin/translations/bg.json @@ -1,9 +1,7 @@ { "config": { "abort": { - "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "device_fail": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0441\u044a\u0437\u0434\u0430\u0432\u0430\u043d\u0435\u0442\u043e \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e.", - "device_timeout": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u0441\u0432\u043e\u0435\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e." + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" }, "step": { "user": { diff --git a/homeassistant/components/daikin/translations/ca.json b/homeassistant/components/daikin/translations/ca.json index 9497a57ea49..825c637610a 100644 --- a/homeassistant/components/daikin/translations/ca.json +++ b/homeassistant/components/daikin/translations/ca.json @@ -1,9 +1,7 @@ { "config": { "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat", - "device_fail": "S'ha produ\u00eft un error inesperat al crear el dispositiu.", - "device_timeout": "S'ha acabat el temps d'espera en la connexi\u00f3 amb el dispositiu." + "already_configured": "El dispositiu ja est\u00e0 configurat" }, "error": { "device_fail": "Error inesperat", @@ -17,7 +15,7 @@ "key": "Clau API", "password": "Contrasenya" }, - "description": "Introdueix l'adre\u00e7a IP del teu Daikin AC.", + "description": "Introdueix l'adre\u00e7a IP del teu AC Daikin.\n\nTingues en compte que la Clau API i la Contrasenya s'utilitzen als dispositius BRP072Cxx i SKYFi respectivament.", "title": "Configuraci\u00f3 de Daikin AC" } } diff --git a/homeassistant/components/daikin/translations/da.json b/homeassistant/components/daikin/translations/da.json index 230bd7ecbd8..6502ced4751 100644 --- a/homeassistant/components/daikin/translations/da.json +++ b/homeassistant/components/daikin/translations/da.json @@ -1,9 +1,7 @@ { "config": { "abort": { - "already_configured": "Enheden er allerede konfigureret", - "device_fail": "Uventet fejl ved oprettelse af enhed.", - "device_timeout": "Timeout ved tilslutning til enheden." + "already_configured": "Enheden er allerede konfigureret" }, "step": { "user": { diff --git a/homeassistant/components/daikin/translations/de.json b/homeassistant/components/daikin/translations/de.json index f3f55ea2ecb..e8e3cd0bf6a 100644 --- a/homeassistant/components/daikin/translations/de.json +++ b/homeassistant/components/daikin/translations/de.json @@ -1,9 +1,10 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "device_fail": "Unerwarteter Fehler beim Erstellen des Ger\u00e4ts.", - "device_timeout": "Zeit\u00fcberschreitung beim Verbinden mit dem Ger\u00e4t." + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "device_timeout": "Verbindung fehlgeschlagen" }, "step": { "user": { diff --git a/homeassistant/components/daikin/translations/en.json b/homeassistant/components/daikin/translations/en.json index 30ba908e04f..cf0f679c7ca 100644 --- a/homeassistant/components/daikin/translations/en.json +++ b/homeassistant/components/daikin/translations/en.json @@ -1,9 +1,7 @@ { "config": { "abort": { - "already_configured": "Device is already configured", - "device_fail": "Unexpected error creating device.", - "device_timeout": "Timeout connecting to the device." + "already_configured": "Device is already configured" }, "error": { "device_fail": "Unexpected error", @@ -17,7 +15,7 @@ "key": "API Key", "password": "Password" }, - "description": "Enter IP address of your Daikin AC.", + "description": "Enter IP address of your Daikin AC.\n\nNote that API Key and Password are used by BRP072Cxx and SKYFi devices respectively.", "title": "Configure Daikin AC" } } diff --git a/homeassistant/components/daikin/translations/es-419.json b/homeassistant/components/daikin/translations/es-419.json index 3facdce66d4..8667011e2e4 100644 --- a/homeassistant/components/daikin/translations/es-419.json +++ b/homeassistant/components/daikin/translations/es-419.json @@ -1,9 +1,7 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado", - "device_fail": "Error inesperado al crear el dispositivo.", - "device_timeout": "Tiempo de espera de conexi\u00f3n al dispositivo." + "already_configured": "El dispositivo ya est\u00e1 configurado" }, "step": { "user": { diff --git a/homeassistant/components/daikin/translations/es.json b/homeassistant/components/daikin/translations/es.json index ae3d205e15d..42b58e38438 100644 --- a/homeassistant/components/daikin/translations/es.json +++ b/homeassistant/components/daikin/translations/es.json @@ -1,21 +1,19 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado", - "device_fail": "Error inesperado al crear el dispositivo.", - "device_timeout": "Tiempo de espera agotado al conectar con el dispositivo." + "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { "device_fail": "Error inesperado", - "device_timeout": "Error al conectar", + "device_timeout": "No se pudo conectar", "forbidden": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { "user": { "data": { "host": "Host", - "key": "Clave de autenticaci\u00f3n (s\u00f3lo utilizada por dispositivos BRP072C/Zena)", - "password": "Contrase\u00f1a del dispositivo (s\u00f3lo utilizada por dispositivos SKYFi)" + "key": "Clave API", + "password": "Contrase\u00f1a" }, "description": "Introduce la IP de tu aire acondicionado Daikin", "title": "Configurar aire acondicionado Daikin" diff --git a/homeassistant/components/daikin/translations/fr.json b/homeassistant/components/daikin/translations/fr.json index 3a3f08ae1f3..f3351631fa8 100644 --- a/homeassistant/components/daikin/translations/fr.json +++ b/homeassistant/components/daikin/translations/fr.json @@ -1,9 +1,7 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", - "device_fail": "Erreur inattendue lors de la cr\u00e9ation du p\u00e9riph\u00e9rique.", - "device_timeout": "D\u00e9lai de connexion au p\u00e9riph\u00e9rique expir\u00e9." + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, "step": { "user": { diff --git a/homeassistant/components/daikin/translations/hu.json b/homeassistant/components/daikin/translations/hu.json index eef3afdc5ee..b11dba9028e 100644 --- a/homeassistant/components/daikin/translations/hu.json +++ b/homeassistant/components/daikin/translations/hu.json @@ -1,14 +1,19 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6zt m\u00e1r konfigur\u00e1ltuk", - "device_fail": "Az eszk\u00f6z l\u00e9trehoz\u00e1sakor v\u00e1ratlan hiba l\u00e9pett fel.", - "device_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a k\u00e9sz\u00fcl\u00e9k csatlakoz\u00e1sakor." + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "device_fail": "V\u00e1ratlan hiba", + "device_timeout": "Sikertelen csatlakoz\u00e1s", + "forbidden": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, "step": { "user": { "data": { - "host": "Hoszt" + "host": "Hoszt", + "key": "API kulcs", + "password": "Jelsz\u00f3" }, "description": "Add meg a Daikin l\u00e9gkond\u00edcion\u00e1l\u00f3 IP-c\u00edm\u00e9t.", "title": "A Daikin l\u00e9gkond\u00edcion\u00e1l\u00f3 konfigur\u00e1l\u00e1sa" diff --git a/homeassistant/components/daikin/translations/it.json b/homeassistant/components/daikin/translations/it.json index 634d500ef1e..373dbcd53d2 100644 --- a/homeassistant/components/daikin/translations/it.json +++ b/homeassistant/components/daikin/translations/it.json @@ -1,9 +1,7 @@ { "config": { "abort": { - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", - "device_fail": "Errore inatteso durante la creazione del dispositivo.", - "device_timeout": "Tempo scaduto per la connessione al dispositivo." + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" }, "error": { "device_fail": "Errore imprevisto", @@ -17,7 +15,7 @@ "key": "Chiave API", "password": "Password" }, - "description": "Inserisci l'indirizzo IP del tuo Daikin AC.", + "description": "Inserisci l'indirizzo IP del tuo condizionatore d'aria Daikin. \n\nSi noti che Chiave API e Password sono usati rispettivamente dai dispositivi BRP072Cxx e SKYFi.", "title": "Configura Daikin AC" } } diff --git a/homeassistant/components/daikin/translations/ko.json b/homeassistant/components/daikin/translations/ko.json index 2ce657c8e06..ff79df2de84 100644 --- a/homeassistant/components/daikin/translations/ko.json +++ b/homeassistant/components/daikin/translations/ko.json @@ -1,9 +1,7 @@ { "config": { "abort": { - "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "device_fail": "\uae30\uae30\ub97c \uad6c\uc131\ud558\ub294 \ub3c4\uc911 \uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", - "device_timeout": "\uae30\uae30 \uc5f0\uacb0 \uc2dc\uac04\uc774 \ucd08\uacfc\ud588\uc2b5\ub2c8\ub2e4." + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { "device_fail": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4", @@ -17,7 +15,7 @@ "key": "API \ud0a4", "password": "\ube44\ubc00\ubc88\ud638" }, - "description": "\ub2e4\uc774\ud0a8 \uc5d0\uc5b4\ucee8\uc758 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "description": "\ub2e4\uc774\ud0a8 \uc5d0\uc5b4\ucee8\uc758 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.\n\nAPI \ud0a4 \ubc0f \ube44\ubc00\ubc88\ud638\ub294 BRP072Cxx \uc640 SKYFi \uae30\uae30\uc5d0\uc11c \uc0ac\uc6a9\ub41c\ub2e4\ub294 \uc810\uc5d0 \uc720\uc758\ud558\uc138\uc694.", "title": "\ub2e4\uc774\ud0a8 \uc5d0\uc5b4\ucee8 \uad6c\uc131\ud558\uae30" } } diff --git a/homeassistant/components/daikin/translations/lb.json b/homeassistant/components/daikin/translations/lb.json index 25039f5af80..16d7caf4b37 100644 --- a/homeassistant/components/daikin/translations/lb.json +++ b/homeassistant/components/daikin/translations/lb.json @@ -1,9 +1,7 @@ { "config": { "abort": { - "already_configured": "Apparat ass scho konfigur\u00e9iert", - "device_fail": "Onerwaarte Feeler beim erstelle vum Apparat.", - "device_timeout": "Z\u00e4it Iwwerschreidung beim verbannen mam Apparat." + "already_configured": "Apparat ass scho konfigur\u00e9iert" }, "error": { "device_fail": "Onerwaarte Feeler", diff --git a/homeassistant/components/daikin/translations/nl.json b/homeassistant/components/daikin/translations/nl.json index 0e0db0c907c..6fa2362ee59 100644 --- a/homeassistant/components/daikin/translations/nl.json +++ b/homeassistant/components/daikin/translations/nl.json @@ -1,9 +1,7 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd", - "device_fail": "Onverwachte fout bij het aanmaken van een apparaat.", - "device_timeout": "Time-out voor verbinding met het apparaat." + "already_configured": "Apparaat is al geconfigureerd" }, "step": { "user": { diff --git a/homeassistant/components/daikin/translations/no.json b/homeassistant/components/daikin/translations/no.json index 2bfddeb9973..98d93a29952 100644 --- a/homeassistant/components/daikin/translations/no.json +++ b/homeassistant/components/daikin/translations/no.json @@ -1,16 +1,12 @@ { "config": { "abort": { - "already_configured": "Enheten er allerede konfigurert", - "device_fail": "Uventet feil under oppretting av enheten.", - "device_timeout": "Tidsavbrudd for tilkobling til enheten." + "already_configured": "Enheten er allerede konfigurert" }, "step": { "user": { "data": { - "host": "Vert", - "key": "Godkjenningsn\u00f8kkel (brukes bare av BRP072C/Zena enheter)", - "password": "Enhetspassord (brukes bare av SKYFi-enheter)" + "host": "Vert" }, "description": "Fyll inn IP-adressen til din Daikin AC.", "title": "Konfigurer Daikin AC" diff --git a/homeassistant/components/daikin/translations/pl.json b/homeassistant/components/daikin/translations/pl.json index 76999ad718c..fc39f78c2d0 100644 --- a/homeassistant/components/daikin/translations/pl.json +++ b/homeassistant/components/daikin/translations/pl.json @@ -1,21 +1,19 @@ { "config": { "abort": { - "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]", - "device_fail": "Nieoczekiwany b\u0142\u0105d tworzenia urz\u0105dzenia.", - "device_timeout": "Przekroczono limit czasu \u0142\u0105czenia z urz\u0105dzeniem." + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." }, "error": { - "device_fail": "[%key_id:common::config_flow::error::unknown%]", - "device_timeout": "[%key_id:common::config_flow::error::cannot_connect%]", - "forbidden": "[%key_id:common::config_flow::error::invalid_auth%]" + "device_fail": "Nieoczekiwany b\u0142\u0105d.", + "device_timeout": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "forbidden": "Niepoprawne uwierzytelnienie." }, "step": { "user": { "data": { - "host": "[%key_id:common::config_flow::data::host%]", - "key": "Klucz uwierzytelniania (u\u017cywany tylko przez urz\u0105dzenia BRP072C/Zena)", - "password": "Has\u0142o urz\u0105dzenia (u\u017cywane tylko przez urz\u0105dzenia SKYFi)" + "host": "Nazwa hosta lub adres IP", + "key": "Klucz API", + "password": "Has\u0142o" }, "description": "Wprowad\u017a adres IP Daikin AC.", "title": "Konfiguracja Daikin AC" diff --git a/homeassistant/components/daikin/translations/pt-BR.json b/homeassistant/components/daikin/translations/pt-BR.json index 294e14b1071..7853563a53c 100644 --- a/homeassistant/components/daikin/translations/pt-BR.json +++ b/homeassistant/components/daikin/translations/pt-BR.json @@ -1,9 +1,12 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "device_fail": "Erro inesperado ao criar dispositivo.", - "device_timeout": "Excedido tempo limite conectando ao dispositivo" + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "device_fail": "Erro inesperado", + "device_timeout": "Falha ao conectar", + "forbidden": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { "user": { diff --git a/homeassistant/components/daikin/translations/pt.json b/homeassistant/components/daikin/translations/pt.json index 7e8f0086194..2c9dc8fab29 100644 --- a/homeassistant/components/daikin/translations/pt.json +++ b/homeassistant/components/daikin/translations/pt.json @@ -1,9 +1,7 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "device_fail": "Erro inesperado ao criar dispositivo.", - "device_timeout": "Tempo excedido a tentar ligar ao dispositivo." + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" }, "step": { "user": { diff --git a/homeassistant/components/daikin/translations/ru.json b/homeassistant/components/daikin/translations/ru.json index 780945c0537..5332ac046e1 100644 --- a/homeassistant/components/daikin/translations/ru.json +++ b/homeassistant/components/daikin/translations/ru.json @@ -1,9 +1,7 @@ { "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.", - "device_fail": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", - "device_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443." + "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": { "device_fail": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", @@ -17,7 +15,7 @@ "key": "\u041a\u043b\u044e\u0447 API", "password": "\u041f\u0430\u0440\u043e\u043b\u044c" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 IP-\u0430\u0434\u0440\u0435\u0441 \u0412\u0430\u0448\u0435\u0433\u043e Daikin AC.", + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 IP-\u0430\u0434\u0440\u0435\u0441 \u0432\u0430\u0448\u0435\u0433\u043e Daikin AC. \n\n\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e \u041a\u043b\u044e\u0447 API \u0438 \u041f\u0430\u0440\u043e\u043b\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c\u0438 BRP072Cxx \u0438 SKYFi \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043d\u043d\u043e.", "title": "Daikin AC" } } diff --git a/homeassistant/components/daikin/translations/sl.json b/homeassistant/components/daikin/translations/sl.json index f48d729b83c..a9f8514146f 100644 --- a/homeassistant/components/daikin/translations/sl.json +++ b/homeassistant/components/daikin/translations/sl.json @@ -1,9 +1,7 @@ { "config": { "abort": { - "already_configured": "Naprava je \u017ee konfigurirana", - "device_fail": "Nepri\u010dakovana napaka pri ustvarjanju naprave.", - "device_timeout": "\u010casovna omejitev za priklop na napravo je potekla." + "already_configured": "Naprava je \u017ee konfigurirana" }, "step": { "user": { diff --git a/homeassistant/components/daikin/translations/sv.json b/homeassistant/components/daikin/translations/sv.json index db785feab0b..a704822f0b7 100644 --- a/homeassistant/components/daikin/translations/sv.json +++ b/homeassistant/components/daikin/translations/sv.json @@ -1,9 +1,7 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad", - "device_fail": "Ov\u00e4ntat fel vid skapande av enhet.", - "device_timeout": "Timeout f\u00f6r anslutning till enheten." + "already_configured": "Enheten \u00e4r redan konfigurerad" }, "step": { "user": { diff --git a/homeassistant/components/daikin/translations/zh-Hans.json b/homeassistant/components/daikin/translations/zh-Hans.json index 57b891d1adf..d27301c2f20 100644 --- a/homeassistant/components/daikin/translations/zh-Hans.json +++ b/homeassistant/components/daikin/translations/zh-Hans.json @@ -1,9 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8bbe\u5907\u5df2\u914d\u7f6e\u5b8c\u6210", - "device_fail": "\u521b\u5efa\u8bbe\u5907\u65f6\u51fa\u73b0\u610f\u5916\u9519\u8bef\u3002", - "device_timeout": "\u8fde\u63a5\u8bbe\u5907\u8d85\u65f6\u3002" + "already_configured": "\u8bbe\u5907\u5df2\u914d\u7f6e\u5b8c\u6210" }, "step": { "user": { diff --git a/homeassistant/components/daikin/translations/zh-Hant.json b/homeassistant/components/daikin/translations/zh-Hant.json index c5f367684d5..20121842a37 100644 --- a/homeassistant/components/daikin/translations/zh-Hant.json +++ b/homeassistant/components/daikin/translations/zh-Hant.json @@ -1,9 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "device_fail": "\u5275\u5efa\u8a2d\u5099\u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002", - "device_timeout": "\u9023\u7dda\u81f3\u8a2d\u5099\u903e\u6642\u3002" + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "device_fail": "\u672a\u9810\u671f\u932f\u8aa4", @@ -17,7 +15,7 @@ "key": "API \u5bc6\u9470", "password": "\u5bc6\u78bc" }, - "description": "\u8f38\u5165\u60a8\u7684\u5927\u91d1\u7a7a\u8abf IP \u4f4d\u5740\u3002", + "description": "\u8f38\u5165\u60a8\u7684\u5927\u91d1\u7a7a\u8abf IP \u4f4d\u5740\u3002\n\n\u8acb\u6ce8\u610f\uff1aBRP072Cxx \u8207 SKYFi \u8a2d\u5099\u4e4b API \u5bc6\u9470\u8207\u5bc6\u78bc\u70ba\u5206\u958b\u4f7f\u7528\u3002", "title": "\u8a2d\u5b9a\u5927\u91d1\u7a7a\u8abf" } } diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 95fa223c697..c8917934dd7 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -1,7 +1,15 @@ """Support for deCONZ binary sensors.""" -from pydeconz.sensor import Presence, Vibration +from pydeconz.sensor import CarbonMonoxide, Fire, OpenClose, Presence, Vibration, Water -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_GAS, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_OPENING, + DEVICE_CLASS_SMOKE, + DEVICE_CLASS_VIBRATION, + BinarySensorEntity, +) from homeassistant.const import ATTR_TEMPERATURE from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -14,6 +22,15 @@ ATTR_ORIENTATION = "orientation" ATTR_TILTANGLE = "tiltangle" ATTR_VIBRATIONSTRENGTH = "vibrationstrength" +DEVICE_CLASS = { + CarbonMonoxide: DEVICE_CLASS_GAS, + Fire: DEVICE_CLASS_SMOKE, + OpenClose: DEVICE_CLASS_OPENING, + Presence: DEVICE_CLASS_MOTION, + Vibration: DEVICE_CLASS_VIBRATION, + Water: DEVICE_CLASS_MOISTURE, +} + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ platforms.""" @@ -74,12 +91,7 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorEntity): @property def device_class(self): """Return the class of the sensor.""" - return self._device.SENSOR_CLASS - - @property - def icon(self): - """Return the icon to use in the frontend.""" - return self._device.SENSOR_ICON + return DEVICE_CLASS.get(type(self._device)) @property def device_state_attributes(self): diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index cd125613f21..c2190321fdf 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -37,7 +37,7 @@ ATTR_ON = "on" ATTR_VALVE = "valve" DAMPERS = ["Level controllable output"] -WINDOW_COVERS = ["Window covering device"] +WINDOW_COVERS = ["Window covering device", "Window covering controller"] COVER_TYPES = DAMPERS + WINDOW_COVERS POWER_PLUGS = ["On/Off light", "On/Off plug-in unit", "Smart plug"] diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 48d286266e4..fc0c01b30df 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -82,7 +82,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_group(gateway.api.groups.values()) -class DeconzLight(DeconzDevice, LightEntity): +class DeconzBaseLight(DeconzDevice, LightEntity): """Representation of a deCONZ light.""" def __init__(self, device, gateway): @@ -176,6 +176,9 @@ class DeconzLight(DeconzDevice, LightEntity): async def async_turn_off(self, **kwargs): """Turn off light.""" + if not self._device.state: + return + data = {"on": False} if ATTR_TRANSITION in kwargs: @@ -201,7 +204,21 @@ class DeconzLight(DeconzDevice, LightEntity): return attributes -class DeconzGroup(DeconzLight): +class DeconzLight(DeconzBaseLight): + """Representation of a deCONZ light.""" + + @property + def max_mireds(self): + """Return the warmest color_temp that this light supports.""" + return self._device.ctmax or super().max_mireds + + @property + def min_mireds(self): + """Return the coldest color_temp that this light supports.""" + return self._device.ctmin or super().min_mireds + + +class DeconzGroup(DeconzBaseLight): """Representation of a deCONZ group.""" def __init__(self, device, gateway): diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index e33af57099d..149ea5f1bc5 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==70"], + "requirements": ["pydeconz==71"], "ssdp": [ { "manufacturer": "Royal Philips Electronics" diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index ae0e55ae51f..330c262110a 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -3,9 +3,12 @@ from pydeconz.sensor import ( Battery, Consumption, Daylight, + Humidity, LightLevel, Power, + Pressure, Switch, + Temperature, Thermostat, ) @@ -13,6 +16,15 @@ from homeassistant.const import ( ATTR_TEMPERATURE, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + ENERGY_KILO_WATT_HOUR, + POWER_WATT, + PRESSURE_HPA, + TEMP_CELSIUS, UNIT_PERCENTAGE, ) from homeassistant.core import callback @@ -31,6 +43,29 @@ ATTR_POWER = "power" ATTR_DAYLIGHT = "daylight" ATTR_EVENT_ID = "event_id" +DEVICE_CLASS = { + Humidity: DEVICE_CLASS_HUMIDITY, + LightLevel: DEVICE_CLASS_ILLUMINANCE, + Power: DEVICE_CLASS_POWER, + Pressure: DEVICE_CLASS_PRESSURE, + Temperature: DEVICE_CLASS_TEMPERATURE, +} + +ICON = { + Daylight: "mdi:white-balance-sunny", + Pressure: "mdi:gauge", + Temperature: "mdi:thermometer", +} + +UNIT_OF_MEASUREMENT = { + Consumption: ENERGY_KILO_WATT_HOUR, + Humidity: UNIT_PERCENTAGE, + LightLevel: "lx", + Power: POWER_WATT, + Pressure: PRESSURE_HPA, + Temperature: TEMP_CELSIUS, +} + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ platforms.""" @@ -119,17 +154,17 @@ class DeconzSensor(DeconzDevice): @property def device_class(self): """Return the class of the sensor.""" - return self._device.SENSOR_CLASS + return DEVICE_CLASS.get(type(self._device)) @property def icon(self): """Return the icon to use in the frontend.""" - return self._device.SENSOR_ICON + return ICON.get(type(self._device)) @property def unit_of_measurement(self): """Return the unit of measurement of this sensor.""" - return self._device.SENSOR_UNIT + return UNIT_OF_MEASUREMENT.get(type(self._device)) @property def device_state_attributes(self): diff --git a/homeassistant/components/deconz/translations/bg.json b/homeassistant/components/deconz/translations/bg.json index ad79cb9d584..11a91d859a3 100644 --- a/homeassistant/components/deconz/translations/bg.json +++ b/homeassistant/components/deconz/translations/bg.json @@ -17,18 +17,9 @@ "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 Home Assistant \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0437\u0432\u0430 \u0441 deCONZ \u0431\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f, \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0435\u043d \u043e\u0442 \u0434\u043e\u0431\u0430\u0432\u043a\u0430\u0442\u0430 \u0437\u0430 hass.io {addon}?", "title": "deCONZ Zigbee \u0431\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f \u0447\u0440\u0435\u0437 Hass.io \u0434\u043e\u0431\u0430\u0432\u043a\u0430" }, - "init": { - "title": "\u0414\u0435\u0444\u0438\u043d\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 deCONZ \u0448\u043b\u044e\u0437" - }, "link": { "description": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438 deCONZ \u0448\u043b\u044e\u0437\u0430 \u0437\u0430 \u0434\u0430 \u0441\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430 \u0441 Home Assistant.\n\n1. \u041e\u0442\u0438\u0434\u0435\u0442\u0435 \u043d\u0430 deCONZ Settings -> Gateway -> Advanced\n2. \u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0431\u0443\u0442\u043e\u043d\u0430 \"Authenticate app\"", "title": "\u0412\u0440\u044a\u0437\u043a\u0430 \u0441 deCONZ" - }, - "manual_confirm": { - "data": { - "host": "\u0410\u0434\u0440\u0435\u0441", - "port": "\u041f\u043e\u0440\u0442" - } } } }, diff --git a/homeassistant/components/deconz/translations/ca.json b/homeassistant/components/deconz/translations/ca.json index 9559dcfb211..1c8500b3cc1 100644 --- a/homeassistant/components/deconz/translations/ca.json +++ b/homeassistant/components/deconz/translations/ca.json @@ -17,31 +17,20 @@ "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb la passarel\u00b7la deCONZ proporcionada pel complement de Hass.io: {addon}?", "title": "Passarel\u00b7la d'enlla\u00e7 deCONZ Zigbee (complement de Hass.io)" }, - "init": { - "title": "Definici\u00f3 de la passarel\u00b7la deCONZ" - }, "link": { "description": "Desbloqueja la teva passarel\u00b7la d'enlla\u00e7 deCONZ per a registrar-te amb Home Assistant.\n\n1. V\u00e9s a la configuraci\u00f3 del sistema deCONZ -> Passarel\u00b7la -> Avan\u00e7at\n2. Prem el bot\u00f3 \"Autenticar applicaci\u00f3\"", "title": "Vincular amb deCONZ" }, - "manual_confirm": { - "data": { - "host": "Amfitri\u00f3", - "port": "Port" - } - }, "manual_input": { "data": { "host": "Amfitri\u00f3", "port": "Port" - }, - "title": "Configuraci\u00f3 de la passarel\u00b7la deCONZ" + } }, "user": { "data": { "host": "Selecciona la passarel\u00b7la deCONZ descoberta" - }, - "title": "Selecci\u00f3 de la passarel\u00b7la deCONZ" + } } } }, diff --git a/homeassistant/components/deconz/translations/cs.json b/homeassistant/components/deconz/translations/cs.json index 360cc9e113f..79c173d692a 100644 --- a/homeassistant/components/deconz/translations/cs.json +++ b/homeassistant/components/deconz/translations/cs.json @@ -14,18 +14,9 @@ "description": "Chcete nakonfigurovat slu\u017ebu Home Assistant pro p\u0159ipojen\u00ed k deCONZ br\u00e1n\u011b pomoc\u00ed hass.io {addon}?", "title": "deCONZ Zigbee br\u00e1na prost\u0159ednictv\u00edm dopl\u0148ku Hass.io" }, - "init": { - "title": "Definujte br\u00e1nu deCONZ" - }, "link": { "description": "Odemkn\u011bte br\u00e1nu deCONZ, pro registraci v Home Assistant. \n\n 1. P\u0159ejd\u011bte do nastaven\u00ed syst\u00e9mu deCONZ \n 2. Stiskn\u011bte tla\u010d\u00edtko \"Unlock Gateway\"", "title": "Propojit s deCONZ" - }, - "manual_confirm": { - "data": { - "host": "Hostitel", - "port": "Port" - } } } } diff --git a/homeassistant/components/deconz/translations/cy.json b/homeassistant/components/deconz/translations/cy.json index 594ea26ee6f..6119486f841 100644 --- a/homeassistant/components/deconz/translations/cy.json +++ b/homeassistant/components/deconz/translations/cy.json @@ -9,18 +9,9 @@ "no_key": "Methu cael allwedd API" }, "step": { - "init": { - "title": "Diffiniwch porth dad-adeiladu" - }, "link": { "description": "Datgloi eich porth deCONZ i gofrestru gyda Cynorthwydd Cartref.\n\n1. Ewch i osodiadau system deCONZ \n2. Bwyso botwm \"Datgloi porth\"", "title": "Cysylltu \u00e2 deCONZ" - }, - "manual_confirm": { - "data": { - "host": "Gwesteiwr", - "port": "Port (gwerth diofyn: '80')" - } } } } diff --git a/homeassistant/components/deconz/translations/da.json b/homeassistant/components/deconz/translations/da.json index 348eba18ae3..e4f57869779 100644 --- a/homeassistant/components/deconz/translations/da.json +++ b/homeassistant/components/deconz/translations/da.json @@ -17,18 +17,9 @@ "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til deCONZ-gateway'en leveret af Hass.io-tilf\u00f8jelsen {addon}?", "title": "deCONZ Zigbee-gateway via Hass.io-tilf\u00f8jelse" }, - "init": { - "title": "Definer deCONZ-gateway" - }, "link": { "description": "L\u00e5s din deCONZ-gateway op for at registrere dig med Home Assistant. \n\n 1. G\u00e5 til deCONZ settings -> Gateway -> Advanced\n 2. Tryk p\u00e5 knappen \"Authenticate app\"", "title": "Forbind med deCONZ" - }, - "manual_confirm": { - "data": { - "host": "V\u00e6rt", - "port": "Port" - } } } }, diff --git a/homeassistant/components/deconz/translations/de.json b/homeassistant/components/deconz/translations/de.json index be359a00ca8..6b9fcc0d3a6 100644 --- a/homeassistant/components/deconz/translations/de.json +++ b/homeassistant/components/deconz/translations/de.json @@ -17,31 +17,20 @@ "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass er eine Verbindung mit dem deCONZ Gateway herstellt, der vom Add-on hass.io {addon} bereitgestellt wird?", "title": "deCONZ Zigbee Gateway \u00fcber das Hass.io Add-on" }, - "init": { - "title": "Definiere das deCONZ-Gateway" - }, "link": { "description": "Entsperre dein deCONZ-Gateway, um es bei Home Assistant zu registrieren. \n\n 1. Gehe in die deCONZ-Systemeinstellungen \n 2. Dr\u00fccke die Taste \"Gateway entsperren\"", "title": "Mit deCONZ verbinden" }, - "manual_confirm": { - "data": { - "host": "Host", - "port": "Port" - } - }, "manual_input": { "data": { "host": "Host", "port": "Port" - }, - "title": "Konfigurieren Sie das deCONZ-Gateway" + } }, "user": { "data": { "host": "W\u00e4hlen Sie das erkannte deCONZ-Gateway aus" - }, - "title": "W\u00e4hlen Sie das deCONZ-Gateway" + } } } }, diff --git a/homeassistant/components/deconz/translations/en.json b/homeassistant/components/deconz/translations/en.json index 159171a65d2..fa329d2e1b3 100644 --- a/homeassistant/components/deconz/translations/en.json +++ b/homeassistant/components/deconz/translations/en.json @@ -17,31 +17,20 @@ "description": "Do you want to configure Home Assistant to connect to the deCONZ gateway provided by the Hass.io add-on {addon}?", "title": "deCONZ Zigbee gateway via Hass.io add-on" }, - "init": { - "title": "Define deCONZ gateway" - }, "link": { "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ Settings -> Gateway -> Advanced\n2. Press \"Authenticate app\" button", "title": "Link with deCONZ" }, - "manual_confirm": { - "data": { - "host": "Host", - "port": "Port" - } - }, "manual_input": { "data": { "host": "Host", "port": "Port" - }, - "title": "Configure deCONZ gateway" + } }, "user": { "data": { "host": "Select discovered deCONZ gateway" - }, - "title": "Select deCONZ gateway" + } } } }, diff --git a/homeassistant/components/deconz/translations/es-419.json b/homeassistant/components/deconz/translations/es-419.json index 208616b7ebe..79a43c76b50 100644 --- a/homeassistant/components/deconz/translations/es-419.json +++ b/homeassistant/components/deconz/translations/es-419.json @@ -17,31 +17,20 @@ "description": "\u00bfDesea configurar Home Assistant para conectarse a la puerta de enlace deCONZ proporcionada por el complemento hass.io {addon}?", "title": "deCONZ Zigbee gateway a trav\u00e9s del complemento Hass.io" }, - "init": { - "title": "Definir el gateway deCONZ" - }, "link": { "description": "Desbloquee su puerta de enlace deCONZ para registrarse con Home Assistant. \n\n 1. Vaya a Configuraci\u00f3n deCONZ - > Gateway - > Avanzado \n 2. Presione el bot\u00f3n \"Autenticar aplicaci\u00f3n\"", "title": "Enlazar con deCONZ" }, - "manual_confirm": { - "data": { - "host": "Host", - "port": "Puerto" - } - }, "manual_input": { "data": { "host": "Host", "port": "Puerto" - }, - "title": "Configurar la puerta de enlace deCONZ" + } }, "user": { "data": { "host": "Seleccione la puerta de enlace descubierta deCONZ" - }, - "title": "Seleccione la puerta de enlace deCONZ" + } } } }, diff --git a/homeassistant/components/deconz/translations/es.json b/homeassistant/components/deconz/translations/es.json index 3299ecbdc55..5a4a3f29258 100644 --- a/homeassistant/components/deconz/translations/es.json +++ b/homeassistant/components/deconz/translations/es.json @@ -17,31 +17,20 @@ "description": "\u00bfQuieres configurar Home Assistant para que se conecte al gateway de deCONZ proporcionado por el add-on {addon} de hass.io?", "title": "Add-on deCONZ Zigbee v\u00eda Hass.io" }, - "init": { - "title": "Definir pasarela deCONZ" - }, "link": { "description": "Desbloquea tu gateway de deCONZ para registrarte con Home Assistant.\n\n1. Dir\u00edgete a deCONZ Settings -> Gateway -> Advanced\n2. Pulsa el bot\u00f3n \"Authenticate app\"", "title": "Enlazar con deCONZ" }, - "manual_confirm": { - "data": { - "host": "Host", - "port": "Puerto" - } - }, "manual_input": { "data": { "host": "Host", "port": "Puerto" - }, - "title": "Configurar la puerta de enlace deCONZ" + } }, "user": { "data": { "host": "Seleccione la puerta de enlace descubierta deCONZ" - }, - "title": "Seleccione la puerta de enlace deCONZ" + } } } }, diff --git a/homeassistant/components/deconz/translations/fi.json b/homeassistant/components/deconz/translations/fi.json index 2a3882a0957..f1829e7bfee 100644 --- a/homeassistant/components/deconz/translations/fi.json +++ b/homeassistant/components/deconz/translations/fi.json @@ -8,17 +8,8 @@ "no_key": "API-avainta ei voitu saada" }, "step": { - "init": { - "title": "M\u00e4\u00e4rit\u00e4 deCONZ-yhdysk\u00e4yt\u00e4v\u00e4" - }, "link": { "title": "Linkit\u00e4 deCONZiin" - }, - "manual_confirm": { - "data": { - "host": "Palvelin", - "port": "Portti" - } } } } diff --git a/homeassistant/components/deconz/translations/fr.json b/homeassistant/components/deconz/translations/fr.json index 9a1c4120149..33ca4889894 100644 --- a/homeassistant/components/deconz/translations/fr.json +++ b/homeassistant/components/deconz/translations/fr.json @@ -17,25 +17,15 @@ "description": "Voulez-vous configurer Home Assistant pour qu'il se connecte \u00e0 la passerelle deCONZ fournie par l'add-on hass.io {addon} ?", "title": "Passerelle deCONZ Zigbee via l'add-on Hass.io" }, - "init": { - "title": "Initialiser la passerelle deCONZ" - }, "link": { "description": "D\u00e9verrouillez votre passerelle deCONZ pour vous enregistrer avec Home Assistant. \n\n 1. Acc\u00e9dez aux param\u00e8tres avanc\u00e9s du syst\u00e8me deCONZ \n 2. Cliquez sur \"D\u00e9verrouiller la passerelle\"", "title": "Lien vers deCONZ" }, - "manual_confirm": { - "data": { - "host": "H\u00f4te", - "port": "Port" - } - }, "manual_input": { "data": { "host": "H\u00f4te", "port": "Port" - }, - "title": "Configurer la passerelle deCONZ" + } } } }, diff --git a/homeassistant/components/deconz/translations/he.json b/homeassistant/components/deconz/translations/he.json index 3a6dff48933..2011ebdde0d 100644 --- a/homeassistant/components/deconz/translations/he.json +++ b/homeassistant/components/deconz/translations/he.json @@ -9,18 +9,9 @@ "no_key": "\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05d4\u05d9\u05d4 \u05dc\u05e7\u05d1\u05dc \u05de\u05e4\u05ea\u05d7 API" }, "step": { - "init": { - "title": "\u05d4\u05d2\u05d3\u05e8 \u05de\u05d2\u05e9\u05e8 deCONZ Zigbee" - }, "link": { "description": "\u05d1\u05d8\u05dc \u05d0\u05ea \u05e0\u05e2\u05d9\u05dc\u05ea \u05d4\u05de\u05e9\u05e8 deCONZ \u05e9\u05dc\u05da \u05db\u05d3\u05d9 \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05e2\u05dd Home Assistant.\n\n 1. \u05e2\u05d1\u05d5\u05e8 \u05d0\u05dc \u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05de\u05e2\u05e8\u05db\u05ea deCONZ \n .2 \u05dc\u05d7\u05e5 \u05e2\u05dc \"Unlock Gateway\"", "title": "\u05e7\u05e9\u05e8 \u05e2\u05dd deCONZ" - }, - "manual_confirm": { - "data": { - "host": "\u05de\u05d0\u05e8\u05d7", - "port": "\u05e4\u05d5\u05e8\u05d8 (\u05d1\u05e8\u05d9\u05e8\u05ea \u05de\u05d7\u05d3\u05dc: '80')" - } } } } diff --git a/homeassistant/components/deconz/translations/hu.json b/homeassistant/components/deconz/translations/hu.json index 216d7ddf1a0..bc07dd2cf97 100644 --- a/homeassistant/components/deconz/translations/hu.json +++ b/homeassistant/components/deconz/translations/hu.json @@ -16,14 +16,11 @@ "hassio_confirm": { "title": "deCONZ Zigbee \u00e1tj\u00e1r\u00f3 a Hass.io kieg\u00e9sz\u00edt\u0151 seg\u00edts\u00e9g\u00e9vel" }, - "init": { - "title": "deCONZ \u00e1tj\u00e1r\u00f3 megad\u00e1sa" - }, "link": { "description": "Oldja fel a deCONZ \u00e1tj\u00e1r\u00f3t a Home Assistant-ban val\u00f3 regisztr\u00e1l\u00e1shoz.\n\n1. Menjen a deCONZ rendszer be\u00e1ll\u00edt\u00e1sokhoz\n2. Nyomja meg az \"\u00c1tj\u00e1r\u00f3 felold\u00e1sa\" gombot", "title": "Kapcsol\u00f3d\u00e1s a deCONZ-hoz" }, - "manual_confirm": { + "manual_input": { "data": { "host": "Hoszt", "port": "Port" diff --git a/homeassistant/components/deconz/translations/id.json b/homeassistant/components/deconz/translations/id.json index ba8b5d76869..9c57eadb0ca 100644 --- a/homeassistant/components/deconz/translations/id.json +++ b/homeassistant/components/deconz/translations/id.json @@ -9,18 +9,9 @@ "no_key": "Tidak bisa mendapatkan kunci API" }, "step": { - "init": { - "title": "Tentukan deCONZ gateway" - }, "link": { "description": "Buka gerbang deCONZ Anda untuk mendaftar dengan Home Assistant. \n\n 1. Pergi ke pengaturan sistem deCONZ \n 2. Tekan tombol \"Buka Kunci Gateway\"", "title": "Tautan dengan deCONZ" - }, - "manual_confirm": { - "data": { - "host": "Host", - "port": "Port (nilai default: '80')" - } } } } diff --git a/homeassistant/components/deconz/translations/it.json b/homeassistant/components/deconz/translations/it.json index 55f20056020..26a1a14e32a 100644 --- a/homeassistant/components/deconz/translations/it.json +++ b/homeassistant/components/deconz/translations/it.json @@ -17,31 +17,20 @@ "description": "Vuoi configurare Home Assistant per connettersi al gateway deCONZ fornito dal componente aggiuntivo di Hass.io: {addon}?", "title": "Gateway Pigmee deCONZ tramite il componente aggiuntivo di Hass.io" }, - "init": { - "title": "Definisci il gateway deCONZ" - }, "link": { "description": "Sblocca il tuo gateway deCONZ per registrarti con Home Assistant.\n\n1. Vai a Impostazioni deCONZ -> Gateway -> Avanzate\n2. Premere il pulsante \"Autentica app\"", "title": "Collega con deCONZ" }, - "manual_confirm": { - "data": { - "host": "Host", - "port": "Porta" - } - }, "manual_input": { "data": { "host": "Host", "port": "Porta" - }, - "title": "Configurare il gateway deCONZ" + } }, "user": { "data": { "host": "Selezionare il gateway deCONZ rilevato" - }, - "title": "Selezionare il gateway deCONZ" + } } } }, diff --git a/homeassistant/components/deconz/translations/ja.json b/homeassistant/components/deconz/translations/ja.json index a1d40f49d34..240e04262e4 100644 --- a/homeassistant/components/deconz/translations/ja.json +++ b/homeassistant/components/deconz/translations/ja.json @@ -2,14 +2,6 @@ "config": { "error": { "no_key": "API\u30ad\u30fc\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f" - }, - "step": { - "manual_confirm": { - "data": { - "host": "\u30db\u30b9\u30c8", - "port": "\u30dd\u30fc\u30c8\uff08\u30c7\u30d5\u30a9\u30eb\u30c8\u5024\uff1a'80'\uff09" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/translations/ko.json b/homeassistant/components/deconz/translations/ko.json index ba7d0b271b9..74905c7dacc 100644 --- a/homeassistant/components/deconz/translations/ko.json +++ b/homeassistant/components/deconz/translations/ko.json @@ -17,31 +17,20 @@ "description": "Hass.io {addon} \uc560\ub4dc\uc628\uc5d0\uc11c \uc81c\uacf5\ub41c deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant \ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Hass.io \uc560\ub4dc\uc628\uc758 deCONZ Zigbee \uac8c\uc774\ud2b8\uc6e8\uc774" }, - "init": { - "title": "deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774 \uc815\uc758\ud558\uae30" - }, "link": { "description": "deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \uc5b8\ub77d\ud558\uc5ec Home Assistant \uc5d0 \uc5f0\uacb0\ud558\uae30.\n\n1. deCONZ \uc2dc\uc2a4\ud15c \uc124\uc815\uc73c\ub85c \uc774\ub3d9\ud558\uc138\uc694\n2. \"Authenticate app\" \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694", "title": "deCONZ \uc5f0\uacb0\ud558\uae30" }, - "manual_confirm": { - "data": { - "host": "\ud638\uc2a4\ud2b8", - "port": "\ud3ec\ud2b8" - } - }, "manual_input": { "data": { "host": "\ud638\uc2a4\ud2b8", "port": "\ud3ec\ud2b8" - }, - "title": "deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774 \uad6c\uc131\ud558\uae30" + } }, "user": { "data": { "host": "\ubc1c\uacac\ub41c deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774 \uc120\ud0dd" - }, - "title": "deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774 \uc120\ud0dd\ud558\uae30" + } } } }, @@ -82,7 +71,7 @@ "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc5d0\uc11c \uc190\uc744 \ub5c4 \ub54c", "remote_button_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uc138 \ubc88 \ub20c\ub9b4 \ub54c", "remote_double_tap": "\uae30\uae30\uc758 \"{subtype}\" \uac00 \ub354\ube14 \ud0ed \ub420 \ub54c", - "remote_double_tap_any_side": "\uae30\uae30\uc758 \uc544\ubb34 \uba74\uc774 \ub354\ube14 \ud0ed \ub420 \ub54c", + "remote_double_tap_any_side": "\uae30\uae30\uc758 \uc544\ubb34 \uba74\uc774\ub098 \ub354\ube14 \ud0ed \ub420 \ub54c", "remote_falling": "\uae30\uae30\uac00 \ub5a8\uc5b4\uc9c8 \ub54c", "remote_flip_180_degrees": "\uae30\uae30\uac00 180\ub3c4\ub85c \ub4a4\uc9d1\uc5b4\uc9c8 \ub54c", "remote_flip_90_degrees": "\uae30\uae30\uac00 90\ub3c4\ub85c \ub4a4\uc9d1\uc5b4\uc9c8 \ub54c", diff --git a/homeassistant/components/deconz/translations/lb.json b/homeassistant/components/deconz/translations/lb.json index c6f2dfbf189..6e80c2393ac 100644 --- a/homeassistant/components/deconz/translations/lb.json +++ b/homeassistant/components/deconz/translations/lb.json @@ -17,31 +17,20 @@ "description": "W\u00ebllt dir Home Assistant konfigur\u00e9iere fir sech mat der deCONZ gateway ze verbannen d\u00e9i vum hass.io add-on {addon} bereet gestallt g\u00ebtt?", "title": "deCONZ Zigbee gateway via Hass.io add-on" }, - "init": { - "title": "deCONZ gateway d\u00e9fin\u00e9ieren" - }, "link": { "description": "Entsperrt \u00e4r deCONZ gateway fir se mat Home Assistant ze registr\u00e9ieren.\n\n1. Gidd op deCONZ System Astellungen\n2. Dr\u00e9ckt \"Unlock\" Gateway Kn\u00e4ppchen", "title": "Link mat deCONZ" }, - "manual_confirm": { - "data": { - "host": "Host", - "port": "Port" - } - }, "manual_input": { "data": { "host": "Apparat", "port": "Port" - }, - "title": "deCONZ Gateway ariichten" + } }, "user": { "data": { "host": "Entdeckte deCONZ Gateway auswielen" - }, - "title": "deCONZ Gateway auswielen" + } } } }, diff --git a/homeassistant/components/deconz/translations/nl.json b/homeassistant/components/deconz/translations/nl.json index 8b0caa869f8..6c64e50caca 100644 --- a/homeassistant/components/deconz/translations/nl.json +++ b/homeassistant/components/deconz/translations/nl.json @@ -3,9 +3,9 @@ "abort": { "already_configured": "Bridge is al geconfigureerd", "already_in_progress": "Configuratiestroom voor bridge wordt al ingesteld.", - "no_bridges": "Geen deCONZ bruggen ontdekt", + "no_bridges": "Geen deCONZ apparaten ontdekt", "not_deconz_bridge": "Dit is geen deCONZ bridge", - "one_instance_only": "Component ondersteunt slechts \u00e9\u00e9n deCONZ instance", + "one_instance_only": "Component ondersteunt slechts \u00e9\u00e9n deCONZ instantie", "updated_instance": "DeCONZ-instantie bijgewerkt met nieuw host-adres" }, "error": { @@ -17,31 +17,20 @@ "description": "Wilt u de Home Assistant configureren om verbinding te maken met de deCONZ gateway van de hass.io add-on {addon}?", "title": "deCONZ Zigbee Gateway via Hass.io add-on" }, - "init": { - "title": "Definieer deCONZ gateway" - }, "link": { "description": "Ontgrendel je deCONZ gateway om te registreren met Home Assistant.\n\n1. Ga naar deCONZ systeeminstellingen (Instellingen -> Gateway -> Geavanceerd)\n2. Druk op de knop \"Gateway ontgrendelen\"", "title": "Koppel met deCONZ" }, - "manual_confirm": { - "data": { - "host": "Host", - "port": "Poort" - } - }, "manual_input": { "data": { "host": "Host", "port": "Poort" - }, - "title": "Configureer deCONZ gateway" + } }, "user": { "data": { "host": "Selecteer gevonden deCONZ gateway" - }, - "title": "Selecteer DeCONZ gateway" + } } } }, diff --git a/homeassistant/components/deconz/translations/nn.json b/homeassistant/components/deconz/translations/nn.json index d6d73478a0b..9b962ae0577 100644 --- a/homeassistant/components/deconz/translations/nn.json +++ b/homeassistant/components/deconz/translations/nn.json @@ -9,18 +9,9 @@ "no_key": "Kunne ikkje f\u00e5 ein API-n\u00f8kkel" }, "step": { - "init": { - "title": "Definer deCONZ-gateway" - }, "link": { "description": "L\u00e5s opp deCONZ-gatewayen din for \u00e5 registrere den med Home Assistant.\n\n1. G\u00e5 til systeminnstillingane til deCONZ\n2. Trykk p\u00e5 \"L\u00e5s opp gateway\"-knappen", "title": "Link med deCONZ" - }, - "manual_confirm": { - "data": { - "host": "Vert", - "port": "Port (standardverdi: '80')" - } } } } diff --git a/homeassistant/components/deconz/translations/no.json b/homeassistant/components/deconz/translations/no.json index 19c6358baf5..f25ad1d5886 100644 --- a/homeassistant/components/deconz/translations/no.json +++ b/homeassistant/components/deconz/translations/no.json @@ -17,31 +17,20 @@ "description": "Vil du konfigurere Home Assistant til \u00e5 koble seg til deCONZ-gateway levert av Hass.io-tillegget {addon} ?", "title": "deCONZ Zigbee gateway via Hass.io tillegg" }, - "init": { - "title": "Definer deCONZ-gatewayen" - }, "link": { "description": "L\u00e5s opp deCONZ-gatewayen din for \u00e5 registrere deg med Home Assistant. \n\n 1. G\u00e5 til deCONZ-systeminnstillinger -> Gateway -> Avansert \n 2. Trykk p\u00e5 \"Autentiser app\" knappen", "title": "Koble til deCONZ" }, - "manual_confirm": { - "data": { - "host": "Vert", - "port": "" - } - }, "manual_input": { "data": { "host": "Vert", "port": "Port" - }, - "title": "Konfigurer deCONZ gateway" + } }, "user": { "data": { "host": "Velg oppdaget deCONZ gateway" - }, - "title": "Velg deCONZ gateway" + } } } }, diff --git a/homeassistant/components/deconz/translations/pl.json b/homeassistant/components/deconz/translations/pl.json index 6477dfa9445..72434f4700f 100644 --- a/homeassistant/components/deconz/translations/pl.json +++ b/homeassistant/components/deconz/translations/pl.json @@ -17,38 +17,27 @@ "description": "Czy chcesz skonfigurowa\u0107 Home Assistant, aby po\u0142\u0105czy\u0142 si\u0119 z bramk\u0105 deCONZ dostarczon\u0105 przez dodatek Hass.io {addon}?", "title": "Bramka deCONZ Zigbee przez dodatek Hass.io" }, - "init": { - "title": "Zdefiniuj bramk\u0119 deCONZ" - }, "link": { "description": "Odblokuj bramk\u0119 deCONZ, aby zarejestrowa\u0107 j\u0105 w Home Assistant. \n\n 1. Przejd\u017a do ustawienia deCONZ > bramka > Zaawansowane\n 2. Naci\u015bnij przycisk \"Uwierzytelnij aplikacj\u0119\"", - "title": "Po\u0142\u0105cz z deCONZ" - }, - "manual_confirm": { - "data": { - "host": "[%key_id:common::config_flow::data::host%]", - "port": "[%key_id:common::config_flow::data::port%]" - } + "title": "Po\u0142\u0105czenie z deCONZ" }, "manual_input": { "data": { "host": "Nazwa hosta lub adres IP", - "port": "[%key_id:common::config_flow::data::port%]" - }, - "title": "Konfiguracja bramki deCONZ" + "port": "Port" + } }, "user": { "data": { "host": "Wybierz znalezion\u0105 bramk\u0119 deCONZ" - }, - "title": "Wybierz bramk\u0119 deCONZ" + } } } }, "device_automation": { "trigger_subtype": { "both_buttons": "oba przyciski", - "bottom_buttons": "Dolne przyciski", + "bottom_buttons": "dolne przyciski", "button_1": "pierwszy przycisk", "button_2": "drugi przycisk", "button_3": "trzeci przycisk", @@ -65,7 +54,7 @@ "side_4": "strona 4", "side_5": "strona 5", "side_6": "strona 6", - "top_buttons": "G\u00f3rne przyciski", + "top_buttons": "g\u00f3rne przyciski", "turn_off": "nast\u0105pi wy\u0142\u0105czenie", "turn_on": "nast\u0105pi w\u0142\u0105czenie" }, diff --git a/homeassistant/components/deconz/translations/pt-BR.json b/homeassistant/components/deconz/translations/pt-BR.json index f548b187103..e8f1f2e39e2 100644 --- a/homeassistant/components/deconz/translations/pt-BR.json +++ b/homeassistant/components/deconz/translations/pt-BR.json @@ -16,19 +16,16 @@ "description": "Deseja configurar o Home Assistant para conectar-se ao gateway deCONZ fornecido pelo add-on hass.io {addon} ?", "title": "Gateway deCONZ Zigbee via add-on Hass.io" }, - "init": { - "title": "Defina o gateway deCONZ" - }, "link": { "description": "Desbloqueie o seu gateway deCONZ para se registar no Home Assistant. \n\n 1. V\u00e1 para as configura\u00e7\u00f5es do sistema deCONZ \n 2. Pressione o bot\u00e3o \"Desbloquear Gateway\"", "title": "Linkar com deCONZ" - }, - "manual_confirm": { - "data": { - "host": "Hospedeiro", - "port": "Porta (valor padr\u00e3o: '80')" - } } } + }, + "device_automation": { + "trigger_subtype": { + "bottom_buttons": "Bot\u00f5es inferiores", + "top_buttons": "Bot\u00f5es superiores" + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/translations/pt.json b/homeassistant/components/deconz/translations/pt.json index b385af86ce5..9cb21e03568 100644 --- a/homeassistant/components/deconz/translations/pt.json +++ b/homeassistant/components/deconz/translations/pt.json @@ -9,19 +9,10 @@ "no_key": "N\u00e3o foi poss\u00edvel obter uma chave de API" }, "step": { - "init": { - "title": "Defina o gateway deCONZ" - }, "link": { "description": "Desbloqueie o seu gateway deCONZ para se registar no Home Assistant. \n\n 1. V\u00e1 para as configura\u00e7\u00f5es do sistema deCONZ \n 2. Pressione o bot\u00e3o \"Desbloquear Gateway\"", "title": "Liga\u00e7\u00e3o com deCONZ" }, - "manual_confirm": { - "data": { - "host": "Servidor", - "port": "Porta" - } - }, "manual_input": { "data": { "host": "Servidor", diff --git a/homeassistant/components/deconz/translations/ro.json b/homeassistant/components/deconz/translations/ro.json index a997db44380..1e7a890b8f1 100644 --- a/homeassistant/components/deconz/translations/ro.json +++ b/homeassistant/components/deconz/translations/ro.json @@ -3,11 +3,6 @@ "step": { "link": { "description": "Debloca\u021bi gateway-ul DECONZ pentru a v\u0103 \u00eenregistra la Home Assistant. \n\n 1. Accesa\u021bi Set\u0103rile deCONZ - > Gateway - > Avansat \n 2. Ap\u0103sa\u021bi butonul \u201eAutentifica\u021bi aplica\u021bia\u201d" - }, - "manual_confirm": { - "data": { - "port": "Port" - } } } } diff --git a/homeassistant/components/deconz/translations/ru.json b/homeassistant/components/deconz/translations/ru.json index 6e42969eb29..32bf2001f44 100644 --- a/homeassistant/components/deconz/translations/ru.json +++ b/homeassistant/components/deconz/translations/ru.json @@ -17,31 +17,20 @@ "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}\")?", "title": "Zigbee \u0448\u043b\u044e\u0437 deCONZ (\u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io)" }, - "init": { - "title": "deCONZ" - }, "link": { "description": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0439\u0442\u0435 \u0448\u043b\u044e\u0437 deCONZ \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0432 Home Assistant:\n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u044b deCONZ -> Gateway -> Advanced.\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u00abAuthenticate app\u00bb.", "title": "\u0421\u0432\u044f\u0437\u044c \u0441 deCONZ" }, - "manual_confirm": { - "data": { - "host": "\u0425\u043e\u0441\u0442", - "port": "\u041f\u043e\u0440\u0442" - } - }, "manual_input": { "data": { "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" - }, - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 deCONZ" + } }, "user": { "data": { "host": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u044b\u0439 \u0448\u043b\u044e\u0437 deCONZ" - }, - "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0448\u043b\u044e\u0437 deCONZ" + } } } }, diff --git a/homeassistant/components/deconz/translations/sl.json b/homeassistant/components/deconz/translations/sl.json index af23635af80..092d65fb4cb 100644 --- a/homeassistant/components/deconz/translations/sl.json +++ b/homeassistant/components/deconz/translations/sl.json @@ -17,31 +17,20 @@ "description": "Ali \u017eelite konfigurirati Home Assistant za povezavo s prehodom deCONZ, ki ga ponuja dodatek Hass.io {addon} ?", "title": "deCONZ Zigbee prehod preko dodatka Hass.io" }, - "init": { - "title": "Dolo\u010dite deCONZ prehod" - }, "link": { "description": "Odklenite va\u0161 deCONZ gateway za registracijo s Home Assistant-om. \n1. Pojdite v deCONZ sistemske nastavitve\n2. Pritisnite tipko \"odkleni prehod\"", "title": "Povezava z deCONZ" }, - "manual_confirm": { - "data": { - "host": "Gostitelj", - "port": "Vrata" - } - }, "manual_input": { "data": { "host": "Gostitelj", "port": "Vrata" - }, - "title": "Konfigurirajte prehod deCONZ" + } }, "user": { "data": { "host": "Izberite odkrit prehod deCONZ" - }, - "title": "Izberite prehod deCONZ" + } } } }, diff --git a/homeassistant/components/deconz/translations/sv.json b/homeassistant/components/deconz/translations/sv.json index e7e0f5d917f..795a2464ad9 100644 --- a/homeassistant/components/deconz/translations/sv.json +++ b/homeassistant/components/deconz/translations/sv.json @@ -17,19 +17,10 @@ "description": "Vill du konfigurera Home Assistant att ansluta till den deCONZ-gateway som tillhandah\u00e5lls av Hass.io-till\u00e4gget {addon}?", "title": "deCONZ Zigbee gateway via Hass.io till\u00e4gg" }, - "init": { - "title": "Definiera deCONZ-gatewaye" - }, "link": { "description": "L\u00e5s upp din deCONZ-gateway f\u00f6r att registrera dig med Home Assistant. \n\n 1. G\u00e5 till deCONZ-systeminst\u00e4llningarna \n 2. Tryck p\u00e5 \"L\u00e5s upp gateway\"-knappen", "title": "L\u00e4nka med deCONZ" }, - "manual_confirm": { - "data": { - "host": "V\u00e4rd", - "port": "Port (standardv\u00e4rde: '80')" - } - }, "manual_input": { "data": { "host": "V\u00e4rd", diff --git a/homeassistant/components/deconz/translations/vi.json b/homeassistant/components/deconz/translations/vi.json index 95880d43e39..ad4ff457a77 100644 --- a/homeassistant/components/deconz/translations/vi.json +++ b/homeassistant/components/deconz/translations/vi.json @@ -7,13 +7,6 @@ }, "error": { "no_key": "Kh\u00f4ng th\u1ec3 l\u1ea5y kh\u00f3a API" - }, - "step": { - "manual_confirm": { - "data": { - "port": "C\u1ed5ng (gi\u00e1 tr\u1ecb m\u1eb7c \u0111\u1ecbnh: '80')" - } - } } } } \ 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 1152d5c394d..42b8067f647 100644 --- a/homeassistant/components/deconz/translations/zh-Hans.json +++ b/homeassistant/components/deconz/translations/zh-Hans.json @@ -9,18 +9,9 @@ "no_key": "\u65e0\u6cd5\u83b7\u53d6 API \u5bc6\u94a5" }, "step": { - "init": { - "title": "\u5b9a\u4e49 deCONZ \u7f51\u5173" - }, "link": { "description": "\u89e3\u9501\u60a8\u7684 deCONZ \u7f51\u5173\u4ee5\u6ce8\u518c\u5230 Home Assistant\u3002 \n\n 1. \u524d\u5f80 deCONZ \u7cfb\u7edf\u8bbe\u7f6e\n 2. \u70b9\u51fb\u201c\u89e3\u9501\u7f51\u5173\u201d\u6309\u94ae", "title": "\u8fde\u63a5 deCONZ" - }, - "manual_confirm": { - "data": { - "host": "\u4e3b\u673a", - "port": "\u7aef\u53e3\uff08\u9ed8\u8ba4\u503c\uff1a'80'\uff09" - } } } }, diff --git a/homeassistant/components/deconz/translations/zh-Hant.json b/homeassistant/components/deconz/translations/zh-Hant.json index e80524ce23f..41711eb81aa 100644 --- a/homeassistant/components/deconz/translations/zh-Hant.json +++ b/homeassistant/components/deconz/translations/zh-Hant.json @@ -17,31 +17,20 @@ "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 Hass.io \u9644\u52a0\u6574\u5408 {addon} \u4e4b deCONZ \u9598\u9053\u5668\uff1f", "title": "\u900f\u904e Hass.io \u9644\u52a0\u7d44\u4ef6 deCONZ Zigbee \u9598\u9053\u5668" }, - "init": { - "title": "\u5b9a\u7fa9 deCONZ \u9598\u9053\u5668" - }, "link": { "description": "\u89e3\u9664 deCONZ \u9598\u9053\u5668\u9396\u5b9a\uff0c\u4ee5\u65bc Home Assistant \u9032\u884c\u8a3b\u518a\u3002\n\n1. \u9032\u5165 deCONZ \u7cfb\u7d71\u8a2d\u5b9a -> \u9598\u9053\u5668 -> \u9032\u968e\u8a2d\u5b9a\n2. \u6309\u4e0b\u300c\u8a8d\u8b49\u7a0b\u5f0f\uff08Authenticate app\uff09\u300d\u6309\u9215", "title": "\u9023\u7d50\u81f3 deCONZ" }, - "manual_confirm": { - "data": { - "host": "\u4e3b\u6a5f\u7aef", - "port": "\u901a\u8a0a\u57e0" - } - }, "manual_input": { "data": { "host": "\u4e3b\u6a5f\u7aef", "port": "\u901a\u8a0a\u57e0" - }, - "title": "\u8a2d\u5b9a deCONZ \u9598\u9053\u5668" + } }, "user": { "data": { "host": "\u9078\u64c7\u6240\u63a2\u7d22\u5230\u7684 deCONZ \u9598\u9053\u5668" - }, - "title": "\u9078\u64c7 deCONZ \u9598\u9053\u5668" + } } } }, diff --git a/homeassistant/components/delijn/manifest.json b/homeassistant/components/delijn/manifest.json index 8727dd25139..e3ab4f29512 100644 --- a/homeassistant/components/delijn/manifest.json +++ b/homeassistant/components/delijn/manifest.json @@ -2,6 +2,6 @@ "domain": "delijn", "name": "De Lijn", "documentation": "https://www.home-assistant.io/integrations/delijn", - "codeowners": ["@bollewolle"], + "codeowners": ["@bollewolle", "@Emilv2"], "requirements": ["pydelijn==0.6.0"] } diff --git a/homeassistant/components/delijn/sensor.py b/homeassistant/components/delijn/sensor.py index 2c7eec1691c..8c73fecf26e 100644 --- a/homeassistant/components/delijn/sensor.py +++ b/homeassistant/components/delijn/sensor.py @@ -81,8 +81,6 @@ class DeLijnPublicTransportSensor(Entity): return self._attributes["stopname"] = self._name - for passage in self.line.passages: - passage["stopname"] = self._name try: first = self.line.passages[0] @@ -96,6 +94,7 @@ class DeLijnPublicTransportSensor(Entity): self._attributes["final_destination"] = first["final_destination"] self._attributes["due_at_schedule"] = first["due_at_schedule"] self._attributes["due_at_realtime"] = first["due_at_realtime"] + self._attributes["is_realtime"] = first["is_realtime"] self._attributes["next_passages"] = self.line.passages self._available = True except (KeyError, IndexError): diff --git a/homeassistant/components/demo/climate.py b/homeassistant/components/demo/climate.py index fd5615c82bd..57feae64a89 100644 --- a/homeassistant/components/demo/climate.py +++ b/homeassistant/components/demo/climate.py @@ -309,12 +309,12 @@ class DemoClimate(ClimateEntity): self._preset = preset_mode self.async_write_ha_state() - def turn_aux_heat_on(self): + async def async_turn_aux_heat_on(self): """Turn auxiliary heater on.""" self._aux = True self.async_write_ha_state() - def turn_aux_heat_off(self): + async def async_turn_aux_heat_off(self): """Turn auxiliary heater off.""" self._aux = False self.async_write_ha_state() diff --git a/homeassistant/components/demo/translations/hu.json b/homeassistant/components/demo/translations/hu.json index 996b9c138ef..0f8f1673d43 100644 --- a/homeassistant/components/demo/translations/hu.json +++ b/homeassistant/components/demo/translations/hu.json @@ -1,3 +1,20 @@ { + "options": { + "step": { + "options_1": { + "data": { + "bool": "Opcion\u00e1lis logikai \u00e9rt\u00e9k", + "int": "Numerikus bemenet" + } + }, + "options_2": { + "data": { + "multi": "T\u00f6bbsz\u00f6r\u00f6s kijel\u00f6l\u00e9s", + "select": "V\u00e1lassz egy lehet\u0151s\u00e9get", + "string": "Karakterl\u00e1nc \u00e9rt\u00e9k" + } + } + } + }, "title": "Dem\u00f3" } \ No newline at end of file diff --git a/homeassistant/components/demo/translations/pt-BR.json b/homeassistant/components/demo/translations/pt-BR.json index 9a07b5ebc50..8364f0bc94b 100644 --- a/homeassistant/components/demo/translations/pt-BR.json +++ b/homeassistant/components/demo/translations/pt-BR.json @@ -1,3 +1,20 @@ { + "options": { + "step": { + "options_1": { + "data": { + "bool": "Booleano opcional", + "int": "Entrada num\u00e9rica" + } + }, + "options_2": { + "data": { + "multi": "Sele\u00e7\u00e3o m\u00faltipla", + "select": "Selecione uma op\u00e7\u00e3o", + "string": "Valor do texto" + } + } + } + }, "title": "Demonstra\u00e7\u00e3o" } \ No newline at end of file diff --git a/homeassistant/components/demo/vacuum.py b/homeassistant/components/demo/vacuum.py index a5d85aa9bd6..528944ae27b 100644 --- a/homeassistant/components/demo/vacuum.py +++ b/homeassistant/components/demo/vacuum.py @@ -126,31 +126,21 @@ class DemoVacuum(VacuumEntity): @property def status(self): """Return the status of the vacuum.""" - if self.supported_features & SUPPORT_STATUS == 0: - return - return self._status @property def fan_speed(self): """Return the status of the vacuum.""" - if self.supported_features & SUPPORT_FAN_SPEED == 0: - return - return self._fan_speed @property def fan_speed_list(self): """Return the status of the vacuum.""" - assert self.supported_features & SUPPORT_FAN_SPEED != 0 return FAN_SPEEDS @property def battery_level(self): """Return the status of the vacuum.""" - if self.supported_features & SUPPORT_BATTERY == 0: - return - return max(0, min(100, self._battery_level)) @property @@ -289,24 +279,16 @@ class StateDemoVacuum(StateVacuumEntity): @property def battery_level(self): """Return the current battery level of the vacuum.""" - if self.supported_features & SUPPORT_BATTERY == 0: - return - return max(0, min(100, self._battery_level)) @property def fan_speed(self): """Return the current fan speed of the vacuum.""" - if self.supported_features & SUPPORT_FAN_SPEED == 0: - return - return self._fan_speed @property def fan_speed_list(self): """Return the list of supported fan speeds.""" - if self.supported_features & SUPPORT_FAN_SPEED == 0: - return return FAN_SPEEDS @property diff --git a/homeassistant/components/device_sun_light_trigger/__init__.py b/homeassistant/components/device_sun_light_trigger/__init__.py index fcf61dfe097..b1fc37e3ae3 100644 --- a/homeassistant/components/device_sun_light_trigger/__init__.py +++ b/homeassistant/components/device_sun_light_trigger/__init__.py @@ -11,6 +11,7 @@ from homeassistant.components.light import ( ) from homeassistant.const import ( ATTR_ENTITY_ID, + EVENT_HOMEASSISTANT_START, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_HOME, @@ -59,17 +60,35 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the triggers to control lights based on device presence.""" + conf = config[DOMAIN] + disable_turn_off = conf[CONF_DISABLE_TURN_OFF] + light_group = conf.get(CONF_LIGHT_GROUP) + light_profile = conf[CONF_LIGHT_PROFILE] + device_group = conf.get(CONF_DEVICE_GROUP) + + async def activate_on_start(_): + """Activate automation.""" + await activate_automation( + hass, device_group, light_group, light_profile, disable_turn_off + ) + + if hass.is_running: + await activate_on_start(None) + else: + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, activate_on_start) + + return True + + +async def activate_automation( + hass, device_group, light_group, light_profile, disable_turn_off +): + """Activate the automation.""" logger = logging.getLogger(__name__) device_tracker = hass.components.device_tracker group = hass.components.group light = hass.components.light person = hass.components.person - conf = config[DOMAIN] - disable_turn_off = conf[CONF_DISABLE_TURN_OFF] - light_group = conf.get(CONF_LIGHT_GROUP) - light_profile = conf[CONF_LIGHT_PROFILE] - - device_group = conf.get(CONF_DEVICE_GROUP) if device_group is None: device_entity_ids = hass.states.async_entity_ids(device_tracker.DOMAIN) @@ -79,7 +98,7 @@ async def async_setup(hass, config): if not device_entity_ids: logger.error("No devices found to track") - return False + return # Get the light IDs from the specified group if light_group is None: @@ -89,7 +108,7 @@ async def async_setup(hass, config): if not light_ids: logger.error("No lights found to turn on") - return False + return @callback def anyone_home(): @@ -219,7 +238,7 @@ async def async_setup(hass, config): ) if disable_turn_off: - return True + return @callback def turn_off_lights_when_all_leave(entity, old_state, new_state): @@ -247,4 +266,4 @@ async def async_setup(hass, config): STATE_NOT_HOME, ) - return True + return diff --git a/homeassistant/components/devolo_home_control/config_flow.py b/homeassistant/components/devolo_home_control/config_flow.py index d104bdde275..93b2cfc11e5 100644 --- a/homeassistant/components/devolo_home_control/config_flow.py +++ b/homeassistant/components/devolo_home_control/config_flow.py @@ -30,12 +30,17 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.data_schema = { vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, - vol.Required(CONF_MYDEVOLO, default=DEFAULT_MYDEVOLO): str, - vol.Required(CONF_HOMECONTROL, default=DEFAULT_MPRM): str, } async def async_step_user(self, user_input=None): """Handle a flow initiated by the user.""" + if self.show_advanced_options: + self.data_schema = { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_MYDEVOLO): str, + vol.Required(CONF_HOMECONTROL): str, + } if user_input is None: return self._show_form(user_input) user = user_input[CONF_USERNAME] @@ -46,8 +51,12 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): mydevolo = Mydevolo() mydevolo.user = user mydevolo.password = password - mydevolo.url = user_input[CONF_MYDEVOLO] - mydevolo.mprm = user_input[CONF_HOMECONTROL] + if self.show_advanced_options: + mydevolo.url = user_input[CONF_MYDEVOLO] + mydevolo.mprm = user_input[CONF_HOMECONTROL] + else: + mydevolo.url = DEFAULT_MYDEVOLO + mydevolo.mprm = DEFAULT_MPRM credentials_valid = await self.hass.async_add_executor_job( mydevolo.credentials_valid ) diff --git a/homeassistant/components/devolo_home_control/switch.py b/homeassistant/components/devolo_home_control/switch.py index e5210262268..e70474a8d5d 100644 --- a/homeassistant/components/devolo_home_control/switch.py +++ b/homeassistant/components/devolo_home_control/switch.py @@ -1,7 +1,7 @@ -"""Platform for light integration.""" +"""Platform for switch integration.""" import logging -from homeassistant.components.switch import SwitchDevice +from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType @@ -29,8 +29,8 @@ async def async_setup_entry( async_add_entities(entities) -class DevoloSwitch(SwitchDevice): - """Representation of an Awesome Light.""" +class DevoloSwitch(SwitchEntity): + """Representation of a switch.""" def __init__(self, homecontrol, device_instance, element_uid): """Initialize an devolo Switch.""" diff --git a/homeassistant/components/devolo_home_control/translations/ca.json b/homeassistant/components/devolo_home_control/translations/ca.json index 920e9a0780e..af0b4eb105a 100644 --- a/homeassistant/components/devolo_home_control/translations/ca.json +++ b/homeassistant/components/devolo_home_control/translations/ca.json @@ -9,14 +9,11 @@ "step": { "user": { "data": { - "Home_Control_URL": "URL de Home Control", - "Mydevolo_URL": "URL de mydevolo", "home_control_url": "URL de Home Control", "mydevolo_url": "URL de mydevolo", "password": "Contrasenya", "username": "Correu electr\u00f2nic / ID de devolo" }, - "description": "Configura el teu Home Control de devolo.", "title": "Home Control devolo" } } diff --git a/homeassistant/components/devolo_home_control/translations/de.json b/homeassistant/components/devolo_home_control/translations/de.json index b8a464d6222..456be47b3d8 100644 --- a/homeassistant/components/devolo_home_control/translations/de.json +++ b/homeassistant/components/devolo_home_control/translations/de.json @@ -9,14 +9,11 @@ "step": { "user": { "data": { - "Home_Control_URL": "Home Control URL", - "Mydevolo_URL": "mydevolo URL", "home_control_url": "Home Control URL", "mydevolo_url": "mydevolo URL", "password": "Passwort", "username": "E-Mail-Adresse / devolo ID" }, - "description": "Richten Sie Ihr devolo Home Control ein.", "title": "devolo Home Control" } } diff --git a/homeassistant/components/devolo_home_control/translations/en.json b/homeassistant/components/devolo_home_control/translations/en.json index 3f1ab4d45e3..ae888d37f4a 100644 --- a/homeassistant/components/devolo_home_control/translations/en.json +++ b/homeassistant/components/devolo_home_control/translations/en.json @@ -9,14 +9,11 @@ "step": { "user": { "data": { - "Home_Control_URL": "Home Control URL", - "Mydevolo_URL": "mydevolo URL", "home_control_url": "Home Control URL", "mydevolo_url": "mydevolo URL", "password": "Password", "username": "E-Mail-Address / devolo ID" }, - "description": "Set up your devolo Home Control.", "title": "devolo Home Control" } } diff --git a/homeassistant/components/devolo_home_control/translations/es.json b/homeassistant/components/devolo_home_control/translations/es.json index 108b36b187e..9eb7f04f923 100644 --- a/homeassistant/components/devolo_home_control/translations/es.json +++ b/homeassistant/components/devolo_home_control/translations/es.json @@ -9,14 +9,11 @@ "step": { "user": { "data": { - "Home_Control_URL": "URL de Home Control", - "Mydevolo_URL": "URL de mydevolo", "home_control_url": "URL de Home Control", "mydevolo_url": "URL de mydevolo", "password": "Contrase\u00f1a", "username": "Direcci\u00f3n de correo electr\u00f3nico / ID de devolo" }, - "description": "Configurar el devolo Home Control.", "title": "devolo Home Control" } } diff --git a/homeassistant/components/devolo_home_control/translations/fi.json b/homeassistant/components/devolo_home_control/translations/fi.json index c2957e7c2b5..51dc72c408a 100644 --- a/homeassistant/components/devolo_home_control/translations/fi.json +++ b/homeassistant/components/devolo_home_control/translations/fi.json @@ -3,7 +3,6 @@ "step": { "user": { "data": { - "Mydevolo_URL": "mydevolo URL", "home_control_url": "Home Control URL", "mydevolo_url": "mydevolo URL", "password": "Salasana" diff --git a/homeassistant/components/devolo_home_control/translations/fr.json b/homeassistant/components/devolo_home_control/translations/fr.json index 34d4c7524e4..7ced4c4840d 100644 --- a/homeassistant/components/devolo_home_control/translations/fr.json +++ b/homeassistant/components/devolo_home_control/translations/fr.json @@ -10,7 +10,6 @@ "password": "Mot de passe", "username": "Adresse e-mail / devolo ID" }, - "description": "Installez votre devolo Home Control.", "title": "devolo Home Control" } } diff --git a/homeassistant/components/devolo_home_control/translations/hu.json b/homeassistant/components/devolo_home_control/translations/hu.json new file mode 100644 index 00000000000..ff2c2fc87b5 --- /dev/null +++ b/homeassistant/components/devolo_home_control/translations/hu.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/it.json b/homeassistant/components/devolo_home_control/translations/it.json index 14997ea7b7a..89bc030a7c8 100644 --- a/homeassistant/components/devolo_home_control/translations/it.json +++ b/homeassistant/components/devolo_home_control/translations/it.json @@ -9,14 +9,11 @@ "step": { "user": { "data": { - "Home_Control_URL": "URL di Home Control", - "Mydevolo_URL": "URL di mydevolo", "home_control_url": "URL di Home Control", "mydevolo_url": "URL di mydevolo", "password": "Password", "username": "Indirizzo e-mail / devolo ID" }, - "description": "Configura devolo Home Control.", "title": "devolo Home Control" } } diff --git a/homeassistant/components/devolo_home_control/translations/ko.json b/homeassistant/components/devolo_home_control/translations/ko.json index cf6ea82353a..6cb8d05f17e 100644 --- a/homeassistant/components/devolo_home_control/translations/ko.json +++ b/homeassistant/components/devolo_home_control/translations/ko.json @@ -9,14 +9,11 @@ "step": { "user": { "data": { - "Home_Control_URL": "Home Control URL", - "Mydevolo_URL": "mydevolo URL", "home_control_url": "Home Control URL", "mydevolo_url": "mydevolo URL", "password": "\ube44\ubc00\ubc88\ud638", "username": "\uc774\uba54\uc77c \uc8fc\uc18c / devolo ID" }, - "description": "devolo Home Control \uc744 \uc124\uc815\ud574\uc8fc\uc138\uc694.", "title": "devolo Home Control" } } diff --git a/homeassistant/components/devolo_home_control/translations/lb.json b/homeassistant/components/devolo_home_control/translations/lb.json index 0f95319fb2c..1976e953f8a 100644 --- a/homeassistant/components/devolo_home_control/translations/lb.json +++ b/homeassistant/components/devolo_home_control/translations/lb.json @@ -9,14 +9,11 @@ "step": { "user": { "data": { - "Home_Control_URL": "Home Control URL", - "Mydevolo_URL": "mydevolo URL", "home_control_url": "Home Control URL", "mydevolo_url": "mydevolo URL", "password": "Passwuert", "username": "Benotzernumm" }, - "description": "devolo Home Control ariichten.", "title": "devolo Home Control" } } diff --git a/homeassistant/components/devolo_home_control/translations/no.json b/homeassistant/components/devolo_home_control/translations/no.json index 950c7b6736f..5bf7990ff59 100644 --- a/homeassistant/components/devolo_home_control/translations/no.json +++ b/homeassistant/components/devolo_home_control/translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Denne Home Control Central er allerede konfigurert." + "already_configured": "Denne hjemmekontrollsentralenheten er allerede i bruk." }, "error": { "invalid_credentials": "Ugyldig brukernavn og/eller passord" @@ -9,14 +9,11 @@ "step": { "user": { "data": { - "Home_Control_URL": "", - "Mydevolo_URL": "", "home_control_url": "Home Control URL", "mydevolo_url": "mydevolo URL", "password": "Passord", "username": "Brukernavn" }, - "description": "Sett opp din devolo Home Control.", "title": "" } } diff --git a/homeassistant/components/devolo_home_control/translations/pl.json b/homeassistant/components/devolo_home_control/translations/pl.json index ef8beb5ff01..0d1c2338e33 100644 --- a/homeassistant/components/devolo_home_control/translations/pl.json +++ b/homeassistant/components/devolo_home_control/translations/pl.json @@ -9,14 +9,11 @@ "step": { "user": { "data": { - "Home_Control_URL": "URL Home Control", - "Mydevolo_URL": "URL mydevolo", "home_control_url": "URL Home Control", "mydevolo_url": "URL mydevolo", - "password": "[%key_id:common::config_flow::data::password%]", - "username": "[%key_id:common::config_flow::data::username%]" + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" }, - "description": "Konfiguracja devolo Home Control", "title": "devolo Home Control" } } diff --git a/homeassistant/components/devolo_home_control/translations/ru.json b/homeassistant/components/devolo_home_control/translations/ru.json index 12760361ff8..b65a45d7227 100644 --- a/homeassistant/components/devolo_home_control/translations/ru.json +++ b/homeassistant/components/devolo_home_control/translations/ru.json @@ -9,14 +9,11 @@ "step": { "user": { "data": { - "Home_Control_URL": "Home Control URL", - "Mydevolo_URL": "mydevolo URL", "home_control_url": "Home Control URL", "mydevolo_url": "mydevolo URL", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b / devolo ID" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 devolo Home Control.", "title": "devolo Home Control" } } diff --git a/homeassistant/components/devolo_home_control/translations/sv.json b/homeassistant/components/devolo_home_control/translations/sv.json index a932043a52f..7ac5f75d405 100644 --- a/homeassistant/components/devolo_home_control/translations/sv.json +++ b/homeassistant/components/devolo_home_control/translations/sv.json @@ -6,14 +6,11 @@ "step": { "user": { "data": { - "Home_Control_URL": "Home Control URL", - "Mydevolo_URL": "mydevolo URL", "home_control_url": "Home Control URL", "mydevolo_url": "mydevolo URL", "password": "L\u00f6senord", "username": "E-postadress / devolo-ID" }, - "description": "St\u00e4ll in din devolo Home Control.", "title": "devolo Home Control" } } diff --git a/homeassistant/components/devolo_home_control/translations/zh-Hant.json b/homeassistant/components/devolo_home_control/translations/zh-Hant.json index 581420cba08..ef2407fe5bd 100644 --- a/homeassistant/components/devolo_home_control/translations/zh-Hant.json +++ b/homeassistant/components/devolo_home_control/translations/zh-Hant.json @@ -9,14 +9,11 @@ "step": { "user": { "data": { - "Home_Control_URL": "Home Control URL", - "Mydevolo_URL": "mydevolo URL", "home_control_url": "Home Control URL", "mydevolo_url": "mydevolo URL", "password": "\u5bc6\u78bc", "username": "E-Mail \u4f4d\u5740 / devolo ID" }, - "description": "\u8a2d\u5b9a devolo Home Control\u3002", "title": "devolo Home Control" } } diff --git a/homeassistant/components/dialogflow/translations/it.json b/homeassistant/components/dialogflow/translations/it.json index fe31d88e5c4..2c933d09a52 100644 --- a/homeassistant/components/dialogflow/translations/it.json +++ b/homeassistant/components/dialogflow/translations/it.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u00c8 necessaria una sola istanza." }, "create_entry": { - "default": "Per inviare eventi a Home Assistant, dovrai configurare [l'integrazione webhook di Dialogflow]({dialogflow_url})\n\n Compila le seguenti informazioni: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n - Content Type: application/json \n\n Vedi [la documentazione]({docs_url}) for ulteriori dettagli." + "default": "Per inviare eventi a Home Assistant, dovrai configurare [l'integrazione webhook di Dialogflow]({dialogflow_url})\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\n Vedi [la documentazione]({docs_url}) for ulteriori dettagli." }, "step": { "user": { diff --git a/homeassistant/components/dialogflow/translations/pl.json b/homeassistant/components/dialogflow/translations/pl.json index 3b939a9f369..ee3f65ef0b1 100644 --- a/homeassistant/components/dialogflow/translations/pl.json +++ b/homeassistant/components/dialogflow/translations/pl.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Wymagana jest tylko jedna instancja." }, "create_entry": { - "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant'a, musisz skonfigurowa\u0107 [Dialogflow Webhook]({dialogflow_url}). \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n - Typ zawarto\u015bci: application/json\n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}), by pozna\u0107 szczeg\u00f3\u0142y." + "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistanta, musisz skonfigurowa\u0107 [Dialogflow Webhook]({dialogflow_url}). \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n - Typ zawarto\u015bci: application/json\n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}), by pozna\u0107 szczeg\u00f3\u0142y." }, "step": { "user": { diff --git a/homeassistant/components/directv/translations/ca.json b/homeassistant/components/directv/translations/ca.json index d906fa2b6fc..be96883d2d0 100644 --- a/homeassistant/components/directv/translations/ca.json +++ b/homeassistant/components/directv/translations/ca.json @@ -10,14 +10,12 @@ "flow_title": "DirecTV: {name}", "step": { "ssdp_confirm": { - "description": "Vols configurar {name}?", - "title": "Connexi\u00f3 amb el receptor DirecTV" + "description": "Vols configurar {name}?" }, "user": { "data": { "host": "Amfitri\u00f3" - }, - "title": "Connexi\u00f3 amb el receptor DirecTV" + } } } } diff --git a/homeassistant/components/directv/translations/de.json b/homeassistant/components/directv/translations/de.json index 0b3fa8f29e8..57d3c41b9d1 100644 --- a/homeassistant/components/directv/translations/de.json +++ b/homeassistant/components/directv/translations/de.json @@ -14,14 +14,12 @@ "one": "eins", "other": "andere" }, - "description": "M\u00f6chten Sie {name} einrichten?", - "title": "Stellen Sie eine Verbindung zum DirecTV-Empf\u00e4nger her" + "description": "M\u00f6chten Sie {name} einrichten?" }, "user": { "data": { "host": "Host oder IP-Adresse" - }, - "title": "Schlie\u00dfen Sie den DirecTV-Empf\u00e4nger an" + } } } } diff --git a/homeassistant/components/directv/translations/en.json b/homeassistant/components/directv/translations/en.json index 8df2c1aec66..0275d50d8fc 100644 --- a/homeassistant/components/directv/translations/en.json +++ b/homeassistant/components/directv/translations/en.json @@ -10,14 +10,12 @@ "flow_title": "DirecTV: {name}", "step": { "ssdp_confirm": { - "description": "Do you want to set up {name}?", - "title": "Connect to the DirecTV receiver" + "description": "Do you want to set up {name}?" }, "user": { "data": { "host": "Host" - }, - "title": "Connect to the DirecTV receiver" + } } } } diff --git a/homeassistant/components/directv/translations/es-419.json b/homeassistant/components/directv/translations/es-419.json index 6db50cd6b5a..618a1d04765 100644 --- a/homeassistant/components/directv/translations/es-419.json +++ b/homeassistant/components/directv/translations/es-419.json @@ -10,14 +10,12 @@ "flow_title": "DirecTV: {name}", "step": { "ssdp_confirm": { - "description": "\u00bfDesea configurar {name}?", - "title": "Conectarse al receptor DirecTV" + "description": "\u00bfDesea configurar {name}?" }, "user": { "data": { "host": "Host o direcci\u00f3n IP" - }, - "title": "Conectarse al receptor DirecTV" + } } } } diff --git a/homeassistant/components/directv/translations/es.json b/homeassistant/components/directv/translations/es.json index cb47b845de6..e6a0d6d07ea 100644 --- a/homeassistant/components/directv/translations/es.json +++ b/homeassistant/components/directv/translations/es.json @@ -10,14 +10,12 @@ "flow_title": "DirecTV: {name}", "step": { "ssdp_confirm": { - "description": "\u00bfQuieres configurar {name}?", - "title": "Conectar con el receptor DirecTV" + "description": "\u00bfQuieres configurar {name}?" }, "user": { "data": { "host": "Host o direcci\u00f3n IP" - }, - "title": "Conectar con el receptor DirecTV" + } } } } diff --git a/homeassistant/components/directv/translations/fr.json b/homeassistant/components/directv/translations/fr.json index 0ff3dd4edb4..b4e19f7150f 100644 --- a/homeassistant/components/directv/translations/fr.json +++ b/homeassistant/components/directv/translations/fr.json @@ -14,14 +14,12 @@ "one": "Vide", "other": "Vide" }, - "description": "Voulez-vous configurer {name} ?", - "title": "Connectez-vous au r\u00e9cepteur DirecTV" + "description": "Voulez-vous configurer {name} ?" }, "user": { "data": { "host": "H\u00f4te ou adresse IP" - }, - "title": "Connectez-vous au r\u00e9cepteur DirecTV" + } } } } diff --git a/homeassistant/components/directv/translations/hu.json b/homeassistant/components/directv/translations/hu.json new file mode 100644 index 00000000000..5d8fc929b92 --- /dev/null +++ b/homeassistant/components/directv/translations/hu.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "host": "Hoszt" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/directv/translations/it.json b/homeassistant/components/directv/translations/it.json index d75f10976d9..cdb2f8b418d 100644 --- a/homeassistant/components/directv/translations/it.json +++ b/homeassistant/components/directv/translations/it.json @@ -14,14 +14,12 @@ "one": "uno", "other": "altri" }, - "description": "Vuoi impostare {name} ?", - "title": "Connettersi al ricevitore DirecTV" + "description": "Vuoi impostare {name} ?" }, "user": { "data": { "host": "Host" - }, - "title": "Collegamento al ricevitore DirecTV" + } } } } diff --git a/homeassistant/components/directv/translations/ko.json b/homeassistant/components/directv/translations/ko.json index 4a1fbd3dbbe..f2526418d08 100644 --- a/homeassistant/components/directv/translations/ko.json +++ b/homeassistant/components/directv/translations/ko.json @@ -10,14 +10,12 @@ "flow_title": "DirecTV: {name}", "step": { "ssdp_confirm": { - "description": "{name} \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "DirecTV \ub9ac\uc2dc\ubc84\uc5d0 \uc5f0\uacb0\ud558\uae30" + "description": "{name} \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" }, "user": { "data": { "host": "\ud638\uc2a4\ud2b8" - }, - "title": "DirecTV \ub9ac\uc2dc\ubc84\uc5d0 \uc5f0\uacb0\ud558\uae30" + } } } } diff --git a/homeassistant/components/directv/translations/lb.json b/homeassistant/components/directv/translations/lb.json index 1fb8b72cde8..cc54cfc9567 100644 --- a/homeassistant/components/directv/translations/lb.json +++ b/homeassistant/components/directv/translations/lb.json @@ -14,14 +14,12 @@ "one": "Een", "other": "Aner" }, - "description": "Soll {name} konfigur\u00e9iert ginn?", - "title": "Mam DirecTV Receiver verbannen" + "description": "Soll {name} konfigur\u00e9iert ginn?" }, "user": { "data": { "host": "Numm oder IP Adresse" - }, - "title": "Mam DirecTV Receiver verbannen" + } } } } diff --git a/homeassistant/components/directv/translations/nl.json b/homeassistant/components/directv/translations/nl.json index b6635311064..26b6e65e811 100644 --- a/homeassistant/components/directv/translations/nl.json +++ b/homeassistant/components/directv/translations/nl.json @@ -10,14 +10,12 @@ "flow_title": "DirecTV: {name}", "step": { "ssdp_confirm": { - "description": "Wilt u {name} instellen?", - "title": "Maak verbinding met de DirecTV-ontvanger" + "description": "Wilt u {name} instellen?" }, "user": { "data": { "host": "Host- of IP-adres" - }, - "title": "Maak verbinding met de DirecTV-ontvanger" + } } } } diff --git a/homeassistant/components/directv/translations/no.json b/homeassistant/components/directv/translations/no.json index 9e0906ea2ac..e0f363d5bd0 100644 --- a/homeassistant/components/directv/translations/no.json +++ b/homeassistant/components/directv/translations/no.json @@ -10,14 +10,12 @@ "flow_title": "", "step": { "ssdp_confirm": { - "description": "Vil du sette opp {name} ?", - "title": "Koble til DirecTV-mottakeren" + "description": "Vil du sette opp {name} ?" }, "user": { "data": { "host": "Vert eller IP-adresse" - }, - "title": "Koble til DirecTV-mottakeren" + } } } } diff --git a/homeassistant/components/directv/translations/pl.json b/homeassistant/components/directv/translations/pl.json index f5f657c75ab..bec0198ca70 100644 --- a/homeassistant/components/directv/translations/pl.json +++ b/homeassistant/components/directv/translations/pl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "Odbiornik DirecTV jest ju\u017c skonfigurowany.", - "unknown": "[%key_id:common::config_flow::error::unknown%]" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "unknown": "Nieoczekiwany b\u0142\u0105d." }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie." + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." }, "flow_title": "DirecTV: {name}", "step": { @@ -16,14 +16,12 @@ "one": "jeden", "other": "inne" }, - "description": "Czy chcesz skonfigurowa\u0107 {name}?", - "title": "Po\u0142\u0105czenie z odbiornikiem DirecTV" + "description": "Czy chcesz skonfigurowa\u0107 {name}?" }, "user": { "data": { - "host": "[%key_id:common::config_flow::data::host%]" - }, - "title": "Po\u0142\u0105czenie z odbiornikiem DirecTV" + "host": "Nazwa hosta lub adres IP" + } } } } diff --git a/homeassistant/components/directv/translations/pt-BR.json b/homeassistant/components/directv/translations/pt-BR.json new file mode 100644 index 00000000000..277606b855b --- /dev/null +++ b/homeassistant/components/directv/translations/pt-BR.json @@ -0,0 +1,10 @@ +{ + "config": { + "flow_title": "DirecTV: {name}", + "step": { + "ssdp_confirm": { + "description": "Voc\u00ea quer configurar o {name}?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/directv/translations/ru.json b/homeassistant/components/directv/translations/ru.json index 5c48aa0be58..e2625644238 100644 --- a/homeassistant/components/directv/translations/ru.json +++ b/homeassistant/components/directv/translations/ru.json @@ -10,14 +10,12 @@ "flow_title": "DirecTV: {name}", "step": { "ssdp_confirm": { - "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?", - "title": "DirecTV" + "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 {name}?" }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" - }, - "title": "DirecTV" + } } } } diff --git a/homeassistant/components/directv/translations/sl.json b/homeassistant/components/directv/translations/sl.json index 19387c68db0..04440e6e2c5 100644 --- a/homeassistant/components/directv/translations/sl.json +++ b/homeassistant/components/directv/translations/sl.json @@ -16,14 +16,12 @@ "other": "drugo", "two": "dva" }, - "description": "Ali \u017eelite nastaviti {name} ?", - "title": "Pove\u017eite se s sprejemnikom DirecTV" + "description": "Ali \u017eelite nastaviti {name} ?" }, "user": { "data": { "host": "Gostitelj ali IP naslov" - }, - "title": "Pove\u017eite se s sprejemnikom DirecTV" + } } } } diff --git a/homeassistant/components/directv/translations/zh-Hant.json b/homeassistant/components/directv/translations/zh-Hant.json index 7668adb6c24..9be7ac31e60 100644 --- a/homeassistant/components/directv/translations/zh-Hant.json +++ b/homeassistant/components/directv/translations/zh-Hant.json @@ -10,14 +10,12 @@ "flow_title": "DirecTV\uff1a{name}", "step": { "ssdp_confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f", - "title": "\u9023\u7dda\u81f3 DirecTV \u63a5\u6536\u5668" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef" - }, - "title": "\u9023\u7dda\u81f3 DirecTV \u63a5\u6536\u5668" + } } } } diff --git a/homeassistant/components/discovery/manifest.json b/homeassistant/components/discovery/manifest.json index 76e4ff701c5..89d800a1c36 100644 --- a/homeassistant/components/discovery/manifest.json +++ b/homeassistant/components/discovery/manifest.json @@ -2,7 +2,8 @@ "domain": "discovery", "name": "Discovery", "documentation": "https://www.home-assistant.io/integrations/discovery", - "requirements": ["netdisco==2.6.0"], + "requirements": ["netdisco==2.7.0"], + "after_dependencies": ["zeroconf"], "codeowners": [], "quality_scale": "internal" } diff --git a/homeassistant/components/doorbird/translations/ca.json b/homeassistant/components/doorbird/translations/ca.json index efc7e92a78d..e01e31e0f0a 100644 --- a/homeassistant/components/doorbird/translations/ca.json +++ b/homeassistant/components/doorbird/translations/ca.json @@ -14,7 +14,7 @@ "step": { "user": { "data": { - "host": "Amfitri\u00f3 (adre\u00e7a IP)", + "host": "Amfitri\u00f3", "name": "Nom del dispositiu", "password": "[%key::common::config_flow::data::password%]", "username": "[%key::common::config_flow::data::username%]" diff --git a/homeassistant/components/doorbird/translations/hu.json b/homeassistant/components/doorbird/translations/hu.json index dee4ed9ee0f..618368433ac 100644 --- a/homeassistant/components/doorbird/translations/hu.json +++ b/homeassistant/components/doorbird/translations/hu.json @@ -1,8 +1,14 @@ { "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba" + }, "step": { "user": { "data": { + "host": "Hoszt", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" } diff --git a/homeassistant/components/doorbird/translations/it.json b/homeassistant/components/doorbird/translations/it.json index 6bfbdbf8401..ee9c603fb13 100644 --- a/homeassistant/components/doorbird/translations/it.json +++ b/homeassistant/components/doorbird/translations/it.json @@ -14,7 +14,7 @@ "step": { "user": { "data": { - "host": "Host (indirizzo IP)", + "host": "Host", "name": "Nome del dispositivo", "password": "Password", "username": "Nome utente" diff --git a/homeassistant/components/doorbird/translations/ko.json b/homeassistant/components/doorbird/translations/ko.json index 852d325403f..74057a94d26 100644 --- a/homeassistant/components/doorbird/translations/ko.json +++ b/homeassistant/components/doorbird/translations/ko.json @@ -10,7 +10,7 @@ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, - "flow_title": "DoorBird {name} ({host})", + "flow_title": "DoorBird: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/doorbird/translations/pl.json b/homeassistant/components/doorbird/translations/pl.json index 8cd30868567..a24febcd94a 100644 --- a/homeassistant/components/doorbird/translations/pl.json +++ b/homeassistant/components/doorbird/translations/pl.json @@ -2,22 +2,22 @@ "config": { "abort": { "already_configured": "BoorBird jest ju\u017c skonfigurowany.", - "link_local_address": "Po\u0142\u0105czenie lokalnego adresu nie jest obs\u0142ugiwane", - "not_doorbird_device": "To urz\u0105dzenie nie jest urz\u0105dzeniem DoorBird" + "link_local_address": "Po\u0142\u0105czenie lokalnego adresu nie jest obs\u0142ugiwane.", + "not_doorbird_device": "To urz\u0105dzenie nie jest urz\u0105dzeniem DoorBird." }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "[%key_id:common::config_flow::error::invalid_auth%]", - "unknown": "[%key_id:common::config_flow::error::unknown%]" + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "invalid_auth": "Niepoprawne uwierzytelnienie.", + "unknown": "Nieoczekiwany b\u0142\u0105d." }, "flow_title": "DoorBird {name} ({host})", "step": { "user": { "data": { - "host": "[%key_id:common::config_flow::data::host%]", + "host": "Nazwa hosta lub adres IP", "name": "Nazwa urz\u0105dzenia", - "password": "[%key_id:common::config_flow::data::password%]", - "username": "[%key_id:common::config_flow::data::username%]" + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" }, "title": "Po\u0142\u0105czenie z DoorBird" } diff --git a/homeassistant/components/doorbird/translations/pt-BR.json b/homeassistant/components/doorbird/translations/pt-BR.json new file mode 100644 index 00000000000..828f6a24e84 --- /dev/null +++ b/homeassistant/components/doorbird/translations/pt-BR.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_configured": "Este DoorBird j\u00e1 est\u00e1 configurado", + "link_local_address": "Link de endere\u00e7os locais n\u00e3o s\u00e3o suportados", + "not_doorbird_device": "Este dispositivo n\u00e3o \u00e9 um DoorBird" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py index b6740655434..0ec67bc97fd 100644 --- a/homeassistant/components/dsmr_reader/definitions.py +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -1,6 +1,11 @@ """Definitions for DSMR Reader sensors added to MQTT.""" -from homeassistant.const import ENERGY_KILO_WATT_HOUR, VOLT, VOLUME_CUBIC_METERS +from homeassistant.const import ( + ELECTRICAL_CURRENT_AMPERE, + ENERGY_KILO_WATT_HOUR, + VOLT, + VOLUME_CUBIC_METERS, +) def dsmr_transform(value): @@ -98,6 +103,21 @@ DEFINITIONS = { "icon": "mdi:flash", "unit": VOLT, }, + "dsmr/reading/phase_power_current_l1": { + "name": "Phase power current L1", + "icon": "mdi:flash", + "unit": ELECTRICAL_CURRENT_AMPERE, + }, + "dsmr/reading/phase_power_current_l2": { + "name": "Phase power current L2", + "icon": "mdi:flash", + "unit": ELECTRICAL_CURRENT_AMPERE, + }, + "dsmr/reading/phase_power_current_l3": { + "name": "Phase power current L3", + "icon": "mdi:flash", + "unit": ELECTRICAL_CURRENT_AMPERE, + }, "dsmr/consumption/gas/delivered": { "name": "Gas usage", "icon": "mdi:fire", diff --git a/homeassistant/components/dunehd/__init__.py b/homeassistant/components/dunehd/__init__.py index a8b8a8cf7af..a1fa456aa09 100644 --- a/homeassistant/components/dunehd/__init__.py +++ b/homeassistant/components/dunehd/__init__.py @@ -1 +1,49 @@ -"""The dunehd component.""" +"""The Dune HD component.""" +import asyncio + +from pdunehd import DuneHDPlayer + +from homeassistant.const import CONF_HOST + +from .const import DOMAIN + +PLATFORMS = ["media_player"] + + +async def async_setup(hass, config): + """Set up the Dune HD component.""" + return True + + +async def async_setup_entry(hass, config_entry): + """Set up a config entry.""" + host = config_entry.data[CONF_HOST] + + player = DuneHDPlayer(host) + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][config_entry.entry_id] = player + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, component) + ) + + return True + + +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(config_entry, component) + for component in PLATFORMS + ] + ) + ) + + if unload_ok: + hass.data[DOMAIN].pop(config_entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/dunehd/config_flow.py b/homeassistant/components/dunehd/config_flow.py new file mode 100644 index 00000000000..1f0efa668f9 --- /dev/null +++ b/homeassistant/components/dunehd/config_flow.py @@ -0,0 +1,101 @@ +"""Adds config flow for Dune HD integration.""" +import ipaddress +import logging +import re + +from pdunehd import DuneHDPlayer +import voluptuous as vol + +from homeassistant import config_entries, exceptions +from homeassistant.const import CONF_HOST + +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +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: + if len(host) > 253: + return False + allowed = re.compile(r"(?!-)[A-Z\d\-\_]{1,63}(?= 27.0." + }, + "flow_title": "Servidor forked-daapd: {name} ({host})", + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "name": "Sobrenom", + "password": "Contrasenya de l'API (deixa-ho en blanc si no t\u00e9 contrasenya)", + "port": "Port de l'API" + }, + "title": "Configuraci\u00f3 del dispositiu forked-daapd" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "librespot_java_port": "Port per al pipe control de librespot-java (si s'utilitza)", + "max_playlists": "Nombre m\u00e0xim de llistes de reproducci\u00f3 utilitzades com a fonts", + "tts_pause_time": "Segons de pausa abans i despr\u00e9s de TTS", + "tts_volume": "Volum TTS (valor 'float' entre [0,1])" + }, + "description": "Configura les diferents opcions de la integraci\u00f3 forked-daapd.", + "title": "Configuraci\u00f3 de les opcions de forked-daapd" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/translations/de.json b/homeassistant/components/forked_daapd/translations/de.json new file mode 100644 index 00000000000..4a82bf666cd --- /dev/null +++ b/homeassistant/components/forked_daapd/translations/de.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "unknown_error": "Unbekannter Fehler", + "wrong_host_or_port": "Verbindung konnte nicht hergestellt werden. Bitte \u00fcberpr\u00fcfen Sie Host und Port.", + "wrong_password": "Ung\u00fcltiges Passwort" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "API-Passwort (leer lassen, wenn kein Passwort vorhanden ist)", + "port": "API Port" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "max_playlists": "Maximale Anzahl der als Quellen verwendeten Wiedergabelisten", + "tts_pause_time": "Sekunden bis zur Pause vor und nach der TTS" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/translations/es.json b/homeassistant/components/forked_daapd/translations/es.json new file mode 100644 index 00000000000..39215f2667d --- /dev/null +++ b/homeassistant/components/forked_daapd/translations/es.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado.", + "not_forked_daapd": "El dispositivo no es un servidor forked-daapd." + }, + "error": { + "unknown_error": "Error desconocido.", + "websocket_not_enabled": "Websocket no activado en servidor forked-daapd.", + "wrong_host_or_port": "No se ha podido conectar. Por favor comprueba host y puerto.", + "wrong_password": "Contrase\u00f1a incorrecta.", + "wrong_server_type": "La integraci\u00f3n forked-daapd requiere un servidor forked-daapd con versi\u00f3n >= 27.0." + }, + "flow_title": "Servidor forked-daapd: {name} ({host})", + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nombre amigable", + "password": "Contrase\u00f1a API (dejar en blanco si no hay contrase\u00f1a)", + "port": "Puerto API" + }, + "title": "Configurar dispositivo forked-daapd" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "librespot_java_port": "Puerto para control de tuber\u00eda librespot-java (si se usa)", + "max_playlists": "N\u00famero m\u00e1ximo de listas de reproducci\u00f3n utilizadas como fuentes", + "tts_pause_time": "Segundos para pausar antes y despu\u00e9s del TTS", + "tts_volume": "Volumen TTS (decimal en el rango [0,1])" + }, + "description": "Ajustar varias opciones para la integraci\u00f3n de forked-daapd", + "title": "Configurar opciones para forked-daapd" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/translations/it.json b/homeassistant/components/forked_daapd/translations/it.json new file mode 100644 index 00000000000..f6d4517c7e7 --- /dev/null +++ b/homeassistant/components/forked_daapd/translations/it.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato.", + "not_forked_daapd": "Il dispositivo non \u00e8 un server forked-daapd." + }, + "error": { + "unknown_error": "Errore sconosciuto.", + "websocket_not_enabled": "websocket del server forked-daapd non abilitato.", + "wrong_host_or_port": "Impossibile connettersi. Si prega di controllare host e porta.", + "wrong_password": "Password errata", + "wrong_server_type": "L'integrazione forked-daapd richiede un server forked-daapd con versione >= 27.0." + }, + "flow_title": "server forked-daapd: {name} ({host})", + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nome descrittivo", + "password": "Password API (lasciare vuota se non c'\u00e8 password)", + "port": "Porta API" + }, + "title": "Configurare il dispositivo forked-daapd" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "librespot_java_port": "Porta per il controllo pipe librespot-java (se utilizzata)", + "max_playlists": "Numero massimo di playlist utilizzate come origini", + "tts_pause_time": "Secondi di pausa prima e dopo il TTS", + "tts_volume": "Volume TTS (variabile nell'intervallo [0,1])" + }, + "description": "Impostare le varie opzioni per l'integrazione forked-daapd.", + "title": "Configura le opzioni forked-daapd" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/translations/ko.json b/homeassistant/components/forked_daapd/translations/ko.json new file mode 100644 index 00000000000..5522eda3a76 --- /dev/null +++ b/homeassistant/components/forked_daapd/translations/ko.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "not_forked_daapd": "\uae30\uae30\uac00 forked-daapd \uc11c\ubc84\uac00 \uc544\ub2d9\ub2c8\ub2e4." + }, + "error": { + "unknown_error": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958", + "websocket_not_enabled": "forked-daapd \uc11c\ubc84 \uc6f9\uc18c\ucf13\uc774 \ube44\ud65c\uc131\ud654 \ub418\uc5b4\uc788\uc2b5\ub2c8\ub2e4.", + "wrong_host_or_port": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694.", + "wrong_password": "\ube44\ubc00\ubc88\ud638\uac00 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", + "wrong_server_type": "forked-daapd \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 forked-daapd \uc11c\ubc84 \ubc84\uc804 27.0 \uc774\uc0c1\uc774 \ud544\uc694\ud569\ub2c8\ub2e4." + }, + "flow_title": "forked-daapd \uc11c\ubc84: {name} ({host})", + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "name": "\uce5c\uc219\ud55c \uc774\ub984", + "password": "API \ube44\ubc00\ubc88\ud638 (\ube44\ubc00\ubc88\ud638\uac00 \uc5c6\uc73c\uba74 \ube44\uc6cc\ub450\uc138\uc694)", + "port": "API \ud3ec\ud2b8" + }, + "title": "forked-daapd \uae30\uae30 \uc124\uc815\ud558\uae30" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "librespot_java_port": "librespot-java \ud30c\uc774\ud504 \ucee8\ud2b8\ub864\uc6a9 \ud3ec\ud2b8 (\uc0ac\uc6a9\ud558\ub294 \uacbd\uc6b0)", + "max_playlists": "\uc18c\uc2a4\ub85c \uc0ac\uc6a9\ub41c \ucd5c\ub300 \uc7ac\uc0dd \ubaa9\ub85d \uc218", + "tts_pause_time": "TTS \uc804\ud6c4\uc5d0 \uc77c\uc2dc\uc911\uc9c0\ud560 \uc2dc\uac04(\ucd08)", + "tts_volume": "TTS \ubcfc\ub968 (0~1 \uc758 \uc2e4\uc218\uac12)" + }, + "description": "forked-daapd \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc5d0 \ub300\ud55c \ub2e4\uc591\ud55c \uc635\uc158\uc744 \uc124\uc815\ud574\uc8fc\uc138\uc694.", + "title": "forked-daapd \uc635\uc158 \uc124\uc815\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/translations/lb.json b/homeassistant/components/forked_daapd/translations/lb.json new file mode 100644 index 00000000000..daa19290219 --- /dev/null +++ b/homeassistant/components/forked_daapd/translations/lb.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert", + "not_forked_daapd": "Apparat ass kee forked-daapd server." + }, + "error": { + "unknown_error": "Onbekannten Feeler.", + "websocket_not_enabled": "forked-daapd server websocket net aktiv.", + "wrong_host_or_port": "Feeler beim verbannen, iwwerpr\u00e9if w.e.g d'Adresse a Port.", + "wrong_password": "Ong\u00ebltegt Passwuert.", + "wrong_server_type": "D'forked-daapd Integratioun ben\u00e9idegt een forked-daapd server mat Versioun >= 27.0." + }, + "flow_title": "forked-daapd server: {name} ({host})", + "step": { + "user": { + "data": { + "host": "Apparat", + "name": "Numm", + "password": "API Passwuert (eidel loosse fir kee Passwuert)", + "port": "API Port" + }, + "title": "forked-daapd Apparat ariichten" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "librespot_java_port": "Port fir librespot-java pipe Kontroll (falls benotzt)", + "max_playlists": "Maximal Unzuel vun Playlists d\u00e9i als Quell benotzt ginn", + "tts_pause_time": "Pause an sekonnen vir an no dem TTS", + "tts_volume": "TTS Lautst\u00e4erkt (float an der range [0,1])" + }, + "description": "Verschidden Optioune fir forked-daapd Integratioun d\u00e9fin\u00e9ieren.", + "title": "Optioune fir forked-daapd konfigur\u00e9ieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/translations/nl.json b/homeassistant/components/forked_daapd/translations/nl.json new file mode 100644 index 00000000000..73dd47b2498 --- /dev/null +++ b/homeassistant/components/forked_daapd/translations/nl.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd.", + "not_forked_daapd": "Apparaat is geen forked-daapd-server." + }, + "error": { + "unknown_error": "Onbekende fout.", + "websocket_not_enabled": "forked-daapd server websocket niet ingeschakeld.", + "wrong_host_or_port": "Verbinding mislukt, controleer het host-adres en poort.", + "wrong_password": "Onjuist wachtwoord.", + "wrong_server_type": "De forked-daapd-integratie vereist een forked-daapd-server met versie >= 27.0." + }, + "flow_title": "forked-daapd server: {naam} ({host})", + "step": { + "user": { + "data": { + "host": "Host", + "name": "Vriendelijke naam", + "password": "API-wachtwoord (leeg laten als er geen wachtwoord is)", + "port": "API-poort" + }, + "title": "Stel een forked-daapd apparaat in" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "librespot_java_port": "Poort voor librespot-java pipe control (indien gebruikt)", + "max_playlists": "Maximum aantal afspeellijsten dat als bronnen wordt gebruikt", + "tts_pause_time": "Seconden om te pauzeren voor en na TTS", + "tts_volume": "TTS-volume (float in het bereik [0,1])" + }, + "description": "Stel verschillende opties in voor de fork-daapd integratie.", + "title": "Configureer forked-daapd opties" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/translations/no.json b/homeassistant/components/forked_daapd/translations/no.json new file mode 100644 index 00000000000..8cb7ec812ae --- /dev/null +++ b/homeassistant/components/forked_daapd/translations/no.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert.", + "not_forked_daapd": "Enheten er ikke en forked-daapd-server." + }, + "error": { + "unknown_error": "Ukjent feil.", + "websocket_not_enabled": "websocket for forked-daapd server ikke aktivert.", + "wrong_host_or_port": "Kan ikke koble til. Vennligst sjekk vert og port.", + "wrong_password": "Feil passord.", + "wrong_server_type": "Forked-daapd integrasjon krever en gaffel-daapd server med versjon \"= 27.0." + }, + "flow_title": "forked-daapd-server: {name} ( {host} )", + "step": { + "user": { + "data": { + "host": "Vert", + "name": "Vennlig navn", + "password": "API-passord (la st\u00e5 tomt hvis ingen passord)", + "port": "API-port" + }, + "title": "Konfigurere forked-daapd-enhet" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "librespot_java_port": "Port for librespot-java pipe control (hvis brukt)", + "max_playlists": "Maks antall spillelister brukt som kilder", + "tts_pause_time": "Sekunder for \u00e5 sette pause f\u00f8r og etter TTS", + "tts_volume": "TTS-volum (flyter i omr\u00e5det [0,1])" + }, + "description": "Angi ulike alternativer for forked-daapd integrasjon.", + "title": "Konfigurer alternativer for forked-daapd" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/translations/pl.json b/homeassistant/components/forked_daapd/translations/pl.json new file mode 100644 index 00000000000..f0434c4794b --- /dev/null +++ b/homeassistant/components/forked_daapd/translations/pl.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o API (pozostaw puste, je\u015bli nie ma has\u0142a)", + "port": "Port API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/translations/pt-BR.json b/homeassistant/components/forked_daapd/translations/pt-BR.json new file mode 100644 index 00000000000..c45178d6a72 --- /dev/null +++ b/homeassistant/components/forked_daapd/translations/pt-BR.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado.", + "not_forked_daapd": "O dispositivo n\u00e3o \u00e9 um servidor forked-daapd." + }, + "error": { + "unknown_error": "Erro desconhecido.", + "websocket_not_enabled": "websocket do servidor forked-daapd n\u00e3o ativado.", + "wrong_host_or_port": "N\u00e3o foi poss\u00edvel conectar. Por favor, verifique o endere\u00e7o e a porta.", + "wrong_password": "Senha incorreta.", + "wrong_server_type": "A integra\u00e7\u00e3o forked-daapd requer um servidor forked-daapd com vers\u00e3o >= 27.0." + }, + "flow_title": "servidor forked-daapd: {name} ({host})", + "step": { + "user": { + "data": { + "host": "Endere\u00e7o (IP)", + "name": "Nome amig\u00e1vel", + "password": "Senha da API (deixe em branco se n\u00e3o houver senha)", + "port": "Porta API" + }, + "title": "Configurar dispositivo forked-daapd" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "librespot_java_port": "Porta para controle de pipe librespot-java (se usado)", + "max_playlists": "N\u00famero m\u00e1ximo de listas de reprodu\u00e7\u00e3o usadas como fontes", + "tts_pause_time": "Segundos para pausar antes e depois do TTS", + "tts_volume": "Volume TTS (flutua\u00e7\u00e3o na faixa [0,1])" + }, + "description": "Defina v\u00e1rias op\u00e7\u00f5es para a integra\u00e7\u00e3o forked-daapd.", + "title": "Configurar op\u00e7\u00f5es forked-daapd" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/translations/ru.json b/homeassistant/components/forked_daapd/translations/ru.json index 448eff8244a..89bd71cb041 100644 --- a/homeassistant/components/forked_daapd/translations/ru.json +++ b/homeassistant/components/forked_daapd/translations/ru.json @@ -9,7 +9,7 @@ "websocket_not_enabled": "\u0412\u0435\u0431-\u0441\u043e\u043a\u0435\u0442 forked-daapd \u043d\u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d.", "wrong_host_or_port": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430.", "wrong_password": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c.", - "wrong_server_type": "\u042d\u0442\u043e \u043d\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 forked-daapd." + "wrong_server_type": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0432\u0435\u0440 forked-daapd \u0432\u0435\u0440\u0441\u0438\u0438 27.0 \u0438\u043b\u0438 \u0432\u044b\u0448\u0435." }, "flow_title": "\u0421\u0435\u0440\u0432\u0435\u0440 forked-daapd: {name} ({host})", "step": { @@ -20,7 +20,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c API (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0435\u0441\u043b\u0438 \u043d\u0435\u0442 \u043f\u0430\u0440\u043e\u043b\u044f)", "port": "\u041f\u043e\u0440\u0442 API" }, - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 forked-daapd" + "title": "forked-daapd" } } }, diff --git a/homeassistant/components/forked_daapd/translations/zh-Hant.json b/homeassistant/components/forked_daapd/translations/zh-Hant.json index b6134c2b720..ec51bbacea3 100644 --- a/homeassistant/components/forked_daapd/translations/zh-Hant.json +++ b/homeassistant/components/forked_daapd/translations/zh-Hant.json @@ -9,7 +9,7 @@ "websocket_not_enabled": "forked-daapd \u4f3a\u670d\u5668 websocket \u672a\u958b\u555f\u3002", "wrong_host_or_port": "\u7121\u6cd5\u9023\u7dda\uff0c\u8acb\u78ba\u8a8d\u4e3b\u6a5f\u8207\u901a\u8a0a\u57e0\u3002", "wrong_password": "\u5bc6\u78bc\u932f\u8aa4\u3002", - "wrong_server_type": "\u975e forked-daapd \u4f3a\u670d\u5668\u3002" + "wrong_server_type": "forked-daapd \u6574\u5408\u9700\u8981\u7248\u6b21 >= 27.0 \u7248\u4e4b forked-daapd \u4f3a\u670d\u5668\u3002" }, "flow_title": "forked-daapd \u4f3a\u670d\u5668\uff1a{name} ({host})", "step": { diff --git a/homeassistant/components/freebox/translations/hu.json b/homeassistant/components/freebox/translations/hu.json index 2b5265bc671..937f441845c 100644 --- a/homeassistant/components/freebox/translations/hu.json +++ b/homeassistant/components/freebox/translations/hu.json @@ -2,6 +2,10 @@ "config": { "step": { "user": { + "data": { + "host": "Hoszt", + "port": "Port" + }, "title": "Freebox" } } diff --git a/homeassistant/components/freebox/translations/ko.json b/homeassistant/components/freebox/translations/ko.json index eca391cbd9f..ce0bc09989e 100644 --- a/homeassistant/components/freebox/translations/ko.json +++ b/homeassistant/components/freebox/translations/ko.json @@ -10,7 +10,7 @@ }, "step": { "link": { - "description": "\"Submit\" \uc744 \ud074\ub9ad\ud55c \ub2e4\uc74c \ub77c\uc6b0\ud130\uc758 \uc624\ub978\ucabd \ud654\uc0b4\ud45c\ub97c \ud130\uce58\ud558\uc5ec Home Assistant \uc5d0 Freebox \ub97c \ub4f1\ub85d\ud574\uc8fc\uc138\uc694.\n\n![\ub77c\uc6b0\ud130\uc758 \ubc84\ud2bc \uc704\uce58](/static/images/config_freebox.png)", + "description": "\ud655\uc778\uc744 \ud074\ub9ad\ud55c \ub2e4\uc74c \ub77c\uc6b0\ud130\uc758 \uc624\ub978\ucabd \ud654\uc0b4\ud45c\ub97c \ud130\uce58\ud558\uc5ec Home Assistant \uc5d0 Freebox \ub97c \ub4f1\ub85d\ud574\uc8fc\uc138\uc694.\n\n![\ub77c\uc6b0\ud130\uc758 \ubc84\ud2bc \uc704\uce58](/static/images/config_freebox.png)", "title": "Freebox \ub77c\uc6b0\ud130 \uc5f0\uacb0\ud558\uae30" }, "user": { diff --git a/homeassistant/components/freebox/translations/pl.json b/homeassistant/components/freebox/translations/pl.json index 770194e338e..c465e16fcb6 100644 --- a/homeassistant/components/freebox/translations/pl.json +++ b/homeassistant/components/freebox/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "[%key_id:common::config_flow::data::host%]" + "already_configured": "Nazwa hosta lub adres IP" }, "error": { "connection_failed": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", @@ -10,13 +10,13 @@ }, "step": { "link": { - "description": "Kliknij \"Zatwierd\u017a\", a nast\u0119pnie naci\u015bnij przycisk strza\u0142ki w prawo na routerze, aby zarejestrowa\u0107 Freebox w Home Assistan'cie. \n\n ![Lokalizacja przycisku na routerze] (/static/images/config_freebox.png)", - "title": "Po\u0142\u0105cz z routerem Freebox" + "description": "Kliknij \"Zatwierd\u017a\", a nast\u0119pnie naci\u015bnij przycisk strza\u0142ki w prawo na routerze, aby zarejestrowa\u0107 Freebox w Home Assistancie. \n\n ![Lokalizacja przycisku na routerze] (/static/images/config_freebox.png)", + "title": "Po\u0142\u0105czenie z routerem Freebox" }, "user": { "data": { - "host": "[%key_id:common::config_flow::data::host%]", - "port": "[%key_id:common::config_flow::data::port%]" + "host": "Nazwa hosta lub adres IP", + "port": "Port" }, "title": "Freebox" } diff --git a/homeassistant/components/fritzbox/translations/ca.json b/homeassistant/components/fritzbox/translations/ca.json index 70c2173c641..af82f1fc582 100644 --- a/homeassistant/components/fritzbox/translations/ca.json +++ b/homeassistant/components/fritzbox/translations/ca.json @@ -16,17 +16,15 @@ "password": "Contrasenya", "username": "Nom d'usuari" }, - "description": "Vols configurar {name}?", - "title": "AVM FRITZ!Box" + "description": "Vols configurar {name}?" }, "user": { "data": { - "host": "Amfitri\u00f3 o adre\u00e7a IP", + "host": "Amfitri\u00f3", "password": "Contrasenya", "username": "Nom d'usuari" }, - "description": "Introdueix la teva informaci\u00f3 de AVM FRITZ!Box.", - "title": "AVM FRITZ!Box" + "description": "Introdueix la teva informaci\u00f3 de AVM FRITZ!Box." } } } diff --git a/homeassistant/components/fritzbox/translations/de.json b/homeassistant/components/fritzbox/translations/de.json index 1a2e046d933..e6e485497a3 100644 --- a/homeassistant/components/fritzbox/translations/de.json +++ b/homeassistant/components/fritzbox/translations/de.json @@ -16,8 +16,7 @@ "password": "Passwort", "username": "Benutzername" }, - "description": "M\u00f6chten Sie {name} einrichten?", - "title": "AVM FRITZ! Box" + "description": "M\u00f6chten Sie {name} einrichten?" }, "user": { "data": { @@ -25,8 +24,7 @@ "password": "Passwort", "username": "Benutzername" }, - "description": "Geben Sie Ihre AVM FRITZ! Box-Informationen ein.", - "title": "AVM FRITZ! Box" + "description": "Geben Sie Ihre AVM FRITZ! Box-Informationen ein." } } } diff --git a/homeassistant/components/fritzbox/translations/en.json b/homeassistant/components/fritzbox/translations/en.json index 4f43626c667..cc9b13619a7 100644 --- a/homeassistant/components/fritzbox/translations/en.json +++ b/homeassistant/components/fritzbox/translations/en.json @@ -16,8 +16,7 @@ "password": "Password", "username": "Username" }, - "description": "Do you want to set up {name}?", - "title": "AVM FRITZ!Box" + "description": "Do you want to set up {name}?" }, "user": { "data": { @@ -25,8 +24,7 @@ "password": "Password", "username": "Username" }, - "description": "Enter your AVM FRITZ!Box information.", - "title": "AVM FRITZ!Box" + "description": "Enter your AVM FRITZ!Box information." } } } diff --git a/homeassistant/components/fritzbox/translations/es-419.json b/homeassistant/components/fritzbox/translations/es-419.json index 4e8003a06d8..9cc1a40daa3 100644 --- a/homeassistant/components/fritzbox/translations/es-419.json +++ b/homeassistant/components/fritzbox/translations/es-419.json @@ -16,8 +16,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "\u00bfDesea configurar {name}?", - "title": "AVM FRITZ!Box" + "description": "\u00bfDesea configurar {name}?" }, "user": { "data": { @@ -25,8 +24,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Ingrese la informaci\u00f3n de su AVM FRITZ!Box.", - "title": "AVM FRITZ!Box" + "description": "Ingrese la informaci\u00f3n de su AVM FRITZ!Box." } } } diff --git a/homeassistant/components/fritzbox/translations/es.json b/homeassistant/components/fritzbox/translations/es.json index 05f956d2382..123b98ee9dc 100644 --- a/homeassistant/components/fritzbox/translations/es.json +++ b/homeassistant/components/fritzbox/translations/es.json @@ -16,8 +16,7 @@ "password": "Contrase\u00f1a", "username": "Usuario" }, - "description": "\u00bfQuieres configurar {name}?", - "title": "AVM FRITZ! Box" + "description": "\u00bfQuieres configurar {name}?" }, "user": { "data": { @@ -25,8 +24,7 @@ "password": "Contrase\u00f1a", "username": "Usuario" }, - "description": "Introduce tu informaci\u00f3n de AVM FRITZ!Box.", - "title": "AVM FRITZ! Box" + "description": "Introduce tu informaci\u00f3n de AVM FRITZ!Box." } } } diff --git a/homeassistant/components/fritzbox/translations/fi.json b/homeassistant/components/fritzbox/translations/fi.json index bb4fb818a67..db9b821bee6 100644 --- a/homeassistant/components/fritzbox/translations/fi.json +++ b/homeassistant/components/fritzbox/translations/fi.json @@ -11,8 +11,7 @@ "data": { "password": "Salasana", "username": "K\u00e4ytt\u00e4j\u00e4tunnus" - }, - "title": "AVM FRITZ!Box" + } } } } diff --git a/homeassistant/components/fritzbox/translations/fr.json b/homeassistant/components/fritzbox/translations/fr.json index 97f8d97430d..0a84e1ec2f3 100644 --- a/homeassistant/components/fritzbox/translations/fr.json +++ b/homeassistant/components/fritzbox/translations/fr.json @@ -16,8 +16,7 @@ "password": "Mot de passe", "username": "Nom d'utilisateur" }, - "description": "Voulez-vous configurer {name} ?", - "title": "AVM FRITZ!Box" + "description": "Voulez-vous configurer {name} ?" }, "user": { "data": { @@ -25,8 +24,7 @@ "password": "Mot de passe", "username": "Nom d'utilisateur" }, - "description": "Entrez les informations de votre AVM FRITZ!Box.", - "title": "AVM FRITZ!Box" + "description": "Entrez les informations de votre AVM FRITZ!Box." } } } diff --git a/homeassistant/components/fritzbox/translations/hu.json b/homeassistant/components/fritzbox/translations/hu.json index 09397b70a7d..6a08b68d863 100644 --- a/homeassistant/components/fritzbox/translations/hu.json +++ b/homeassistant/components/fritzbox/translations/hu.json @@ -9,6 +9,7 @@ }, "user": { "data": { + "host": "Hoszt", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" } diff --git a/homeassistant/components/fritzbox/translations/it.json b/homeassistant/components/fritzbox/translations/it.json index f51868956bc..3b99e985871 100644 --- a/homeassistant/components/fritzbox/translations/it.json +++ b/homeassistant/components/fritzbox/translations/it.json @@ -16,17 +16,15 @@ "password": "Password", "username": "Nome utente" }, - "description": "Vuoi impostare {name}?", - "title": "AVM FRITZ!Box" + "description": "Vuoi impostare {name}?" }, "user": { "data": { - "host": "Host o indirizzo IP", + "host": "Host", "password": "Password", "username": "Nome utente" }, - "description": "Inserisci le informazioni del tuo AVM FRITZ!Box .", - "title": "AVM FRITZ!Box" + "description": "Inserisci le informazioni del tuo AVM FRITZ!Box ." } } } diff --git a/homeassistant/components/fritzbox/translations/ko.json b/homeassistant/components/fritzbox/translations/ko.json index 395749b2b9f..0d27f8e0606 100644 --- a/homeassistant/components/fritzbox/translations/ko.json +++ b/homeassistant/components/fritzbox/translations/ko.json @@ -16,8 +16,7 @@ "password": "\ube44\ubc00\ubc88\ud638", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, - "description": "{name} \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "AVM FRITZ!Box" + "description": "{name} \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" }, "user": { "data": { @@ -25,8 +24,7 @@ "password": "\ube44\ubc00\ubc88\ud638", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, - "description": "AVM FRITZ!Box \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", - "title": "AVM FRITZ!Box" + "description": "AVM FRITZ!Box \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694." } } } diff --git a/homeassistant/components/fritzbox/translations/lb.json b/homeassistant/components/fritzbox/translations/lb.json index d9126243577..12ff01c1306 100644 --- a/homeassistant/components/fritzbox/translations/lb.json +++ b/homeassistant/components/fritzbox/translations/lb.json @@ -16,8 +16,7 @@ "password": "Passwuert", "username": "Benotzernumm" }, - "description": "Soll {name} konfigur\u00e9iert ginn?", - "title": "AVM FRITZ!Box" + "description": "Soll {name} konfigur\u00e9iert ginn?" }, "user": { "data": { @@ -25,8 +24,7 @@ "password": "Passwuert", "username": "Benotzernumm" }, - "description": "F\u00ebll d\u00e9ng AVM FRITZ!Box Informatiounen aus.", - "title": "AVM FRITZ!Box" + "description": "F\u00ebll d\u00e9ng AVM FRITZ!Box Informatiounen aus." } } } diff --git a/homeassistant/components/fritzbox/translations/nl.json b/homeassistant/components/fritzbox/translations/nl.json index d6391d16803..ea8b715b7ea 100644 --- a/homeassistant/components/fritzbox/translations/nl.json +++ b/homeassistant/components/fritzbox/translations/nl.json @@ -16,8 +16,7 @@ "password": "Wachtwoord", "username": "Gebruikersnaam" }, - "description": "Wilt u {name} instellen?", - "title": "AVM FRITZ!Box" + "description": "Wilt u {name} instellen?" }, "user": { "data": { @@ -25,8 +24,7 @@ "password": "Wachtwoord", "username": "Gebruikersnaam" }, - "description": "Voer uw AVM FRITZ!Box informatie in.", - "title": "AVM FRITZ!Box" + "description": "Voer uw AVM FRITZ!Box informatie in." } } } diff --git a/homeassistant/components/fritzbox/translations/no.json b/homeassistant/components/fritzbox/translations/no.json index a21067bb112..55518d0288a 100644 --- a/homeassistant/components/fritzbox/translations/no.json +++ b/homeassistant/components/fritzbox/translations/no.json @@ -16,8 +16,7 @@ "password": "Passord", "username": "Brukernavn" }, - "description": "Vil du sette opp {name} ?", - "title": "" + "description": "Vil du sette opp {name} ?" }, "user": { "data": { @@ -25,8 +24,7 @@ "password": "Passord", "username": "Brukernavn" }, - "description": "Fyll inn AVM FRITZ!Box informasjonen.", - "title": "" + "description": "Fyll inn AVM FRITZ!Box informasjonen." } } } diff --git a/homeassistant/components/fritzbox/translations/pl.json b/homeassistant/components/fritzbox/translations/pl.json index be545f40b1a..0c8f96e83a4 100644 --- a/homeassistant/components/fritzbox/translations/pl.json +++ b/homeassistant/components/fritzbox/translations/pl.json @@ -13,20 +13,18 @@ "step": { "confirm": { "data": { - "password": "[%key_id:common::config_flow::data::password%]", - "username": "[%key_id:common::config_flow::data::username%]" + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" }, - "description": "Czy chcesz skonfigurowa\u0107 {name}?", - "title": "AVM FRITZ! Box" + "description": "Czy chcesz skonfigurowa\u0107 {name}?" }, "user": { "data": { - "host": "[%key_id:common::config_flow::data::host%]", - "password": "[%key_id:common::config_flow::data::password%]", - "username": "[%key_id:common::config_flow::data::username%]" + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" }, - "description": "Wprowad\u017a informacje o urz\u0105dzeniu AVM FRITZ! Box.", - "title": "AVM FRITZ! Box" + "description": "Wprowad\u017a informacje o urz\u0105dzeniu AVM FRITZ! Box." } } } diff --git a/homeassistant/components/fritzbox/translations/pt-BR.json b/homeassistant/components/fritzbox/translations/pt-BR.json new file mode 100644 index 00000000000..6fd7f35d8c5 --- /dev/null +++ b/homeassistant/components/fritzbox/translations/pt-BR.json @@ -0,0 +1,12 @@ +{ + "config": { + "error": { + "auth_failed": "Nome de usu\u00e1rio e/ou senha est\u00e3o incorretos." + }, + "step": { + "confirm": { + "description": "Voc\u00ea quer configurar o {name}?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/translations/ru.json b/homeassistant/components/fritzbox/translations/ru.json index ce8f88bc4e4..635e0a2b891 100644 --- a/homeassistant/components/fritzbox/translations/ru.json +++ b/homeassistant/components/fritzbox/translations/ru.json @@ -16,8 +16,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u041b\u043e\u0433\u0438\u043d" }, - "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?", - "title": "AVM FRITZ!Box" + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" }, "user": { "data": { @@ -25,8 +24,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u041b\u043e\u0433\u0438\u043d" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 AVM FRITZ!Box.", - "title": "AVM FRITZ!Box" + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 AVM FRITZ!Box." } } } diff --git a/homeassistant/components/fritzbox/translations/sl.json b/homeassistant/components/fritzbox/translations/sl.json index 484ab28b265..cd85c58af24 100644 --- a/homeassistant/components/fritzbox/translations/sl.json +++ b/homeassistant/components/fritzbox/translations/sl.json @@ -16,8 +16,7 @@ "password": "Geslo", "username": "Uporabni\u0161ko ime" }, - "description": "Ali \u017eelite nastaviti {name} ?", - "title": "AVM FRITZ!Box" + "description": "Ali \u017eelite nastaviti {name} ?" }, "user": { "data": { @@ -25,8 +24,7 @@ "password": "Geslo", "username": "Uporabni\u0161ko ime" }, - "description": "Vnesite svoje podatke za AVM FRITZ! Box.", - "title": "AVM FRITZ!Box" + "description": "Vnesite svoje podatke za AVM FRITZ! Box." } } } diff --git a/homeassistant/components/fritzbox/translations/zh-Hant.json b/homeassistant/components/fritzbox/translations/zh-Hant.json index f517a667d97..415ad9c71bd 100644 --- a/homeassistant/components/fritzbox/translations/zh-Hant.json +++ b/homeassistant/components/fritzbox/translations/zh-Hant.json @@ -16,8 +16,7 @@ "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f", - "title": "AVM FRITZ!Box" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" }, "user": { "data": { @@ -25,8 +24,7 @@ "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u8f38\u5165 AVM FRITZ!Box \u8cc7\u8a0a\u3002", - "title": "AVM FRITZ!Box" + "description": "\u8f38\u5165 AVM FRITZ!Box \u8cc7\u8a0a\u3002" } } } diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 14ae15dd87b..682c3e9b62f 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20200519.5"], + "requirements": ["home-assistant-frontend==20200603.2"], "dependencies": [ "api", "auth", diff --git a/homeassistant/components/garmin_connect/manifest.json b/homeassistant/components/garmin_connect/manifest.json index 09c916104df..7a57b1bf102 100644 --- a/homeassistant/components/garmin_connect/manifest.json +++ b/homeassistant/components/garmin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "garmin_connect", "name": "Garmin Connect", "documentation": "https://www.home-assistant.io/integrations/garmin_connect", - "requirements": ["garminconnect==0.1.10"], + "requirements": ["garminconnect==0.1.13"], "codeowners": ["@cyberjunky"], "config_flow": true } diff --git a/homeassistant/components/garmin_connect/translations/hu.json b/homeassistant/components/garmin_connect/translations/hu.json index fd805fc4e4d..2ada884847f 100644 --- a/homeassistant/components/garmin_connect/translations/hu.json +++ b/homeassistant/components/garmin_connect/translations/hu.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Ez a fi\u00f3k m\u00e1r konfigur\u00e1lva van." + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" }, "error": { "cannot_connect": "Nem siker\u00fclt csatlakozni, pr\u00f3b\u00e1lkozzon \u00fajra.", diff --git a/homeassistant/components/garmin_connect/translations/pl.json b/homeassistant/components/garmin_connect/translations/pl.json index 2ecd1114799..982c7b2c50b 100644 --- a/homeassistant/components/garmin_connect/translations/pl.json +++ b/homeassistant/components/garmin_connect/translations/pl.json @@ -1,19 +1,19 @@ { "config": { "abort": { - "already_configured": "To konto jest ju\u017c skonfigurowane." + "already_configured": "Konto jest ju\u017c skonfigurowane." }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "[%key_id:common::config_flow::error::invalid_auth%]", + "invalid_auth": "Niepoprawne uwierzytelnienie.", "too_many_requests": "Zbyt wiele \u017c\u0105da\u0144, spr\u00f3buj ponownie p\u00f3\u017aniej.", - "unknown": "[%key_id:common::config_flow::error::unknown%]" + "unknown": "Nieoczekiwany b\u0142\u0105d." }, "step": { "user": { "data": { - "password": "[%key_id:common::config_flow::data::password%]", - "username": "[%key_id:common::config_flow::data::username%]" + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" }, "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce", "title": "Garmin Connect" diff --git a/homeassistant/components/garmin_connect/translations/pt-BR.json b/homeassistant/components/garmin_connect/translations/pt-BR.json new file mode 100644 index 00000000000..157ac3f0477 --- /dev/null +++ b/homeassistant/components/garmin_connect/translations/pt-BR.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "description": "Digite suas credenciais.", + "title": "Garmin Connect" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/translations/pt-BR.json b/homeassistant/components/gdacs/translations/pt-BR.json new file mode 100644 index 00000000000..1e866fa8059 --- /dev/null +++ b/homeassistant/components/gdacs/translations/pt-BR.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Local j\u00e1 est\u00e1 configurado." + }, + "step": { + "user": { + "data": { + "radius": "Raio" + }, + "title": "Preencha os detalhes do filtro." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gearbest/__init__.py b/homeassistant/components/gearbest/__init__.py deleted file mode 100644 index c97d9469296..00000000000 --- a/homeassistant/components/gearbest/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The gearbest component.""" diff --git a/homeassistant/components/gearbest/manifest.json b/homeassistant/components/gearbest/manifest.json deleted file mode 100644 index 4729fd6b6f3..00000000000 --- a/homeassistant/components/gearbest/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "domain": "gearbest", - "name": "Gearbest", - "documentation": "https://www.home-assistant.io/integrations/gearbest", - "requirements": ["gearbest_parser==1.0.7"], - "codeowners": ["@HerrHofrat"] -} diff --git a/homeassistant/components/gearbest/sensor.py b/homeassistant/components/gearbest/sensor.py deleted file mode 100644 index b9b2a35b89d..00000000000 --- a/homeassistant/components/gearbest/sensor.py +++ /dev/null @@ -1,123 +0,0 @@ -"""Parse prices of an item from gearbest.""" -from datetime import timedelta -import logging - -from gearbest_parser import CurrencyConverter, GearbestParser -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_CURRENCY, CONF_ID, CONF_NAME, CONF_URL -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import track_time_interval -from homeassistant.util import Throttle - -_LOGGER = logging.getLogger(__name__) - -CONF_ITEMS = "items" - -ICON = "mdi:coin" -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=2 * 60 * 60) # 2h -MIN_TIME_BETWEEN_CURRENCY_UPDATES = timedelta(seconds=12 * 60 * 60) # 12h - - -_ITEM_SCHEMA = vol.All( - vol.Schema( - { - vol.Exclusive(CONF_URL, "XOR"): cv.string, - vol.Exclusive(CONF_ID, "XOR"): cv.string, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_CURRENCY): cv.string, - } - ), - cv.has_at_least_one_key(CONF_URL, CONF_ID), -) - -_ITEMS_SCHEMA = vol.Schema([_ITEM_SCHEMA]) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Required(CONF_ITEMS): _ITEMS_SCHEMA, vol.Required(CONF_CURRENCY): cv.string} -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Gearbest sensor.""" - - currency = config.get(CONF_CURRENCY) - - sensors = [] - items = config.get(CONF_ITEMS) - - converter = CurrencyConverter() - converter.update() - - for item in items: - try: - sensors.append(GearbestSensor(converter, item, currency)) - except ValueError as exc: - _LOGGER.error(exc) - - def currency_update(event_time): - """Update currency list.""" - converter.update() - - track_time_interval(hass, currency_update, MIN_TIME_BETWEEN_CURRENCY_UPDATES) - - add_entities(sensors, True) - - -class GearbestSensor(Entity): - """Implementation of the sensor.""" - - def __init__(self, converter, item, currency): - """Initialize the sensor.""" - - self._name = item.get(CONF_NAME) - self._parser = GearbestParser() - self._parser.set_currency_converter(converter) - self._item = self._parser.load( - item.get(CONF_ID), item.get(CONF_URL), item.get(CONF_CURRENCY, currency) - ) - if self._item is None: - raise ValueError("id and url could not be resolved") - - @property - def name(self): - """Return the name of the item.""" - return self._name if self._name is not None else self._item.name - - @property - def icon(self): - """Return the icon for the frontend.""" - return ICON - - @property - def state(self): - """Return the price of the selected product.""" - return self._item.price - - @property - def unit_of_measurement(self): - """Return the currency.""" - return self._item.currency - - @property - def entity_picture(self): - """Return the image.""" - return self._item.image - - @property - def device_state_attributes(self): - """Return the state attributes.""" - attrs = { - "name": self._item.name, - "description": self._item.description, - "currency": self._item.currency, - "url": self._item.url, - } - return attrs - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest price from gearbest and updates the state.""" - self._item.update() diff --git a/homeassistant/components/geofency/translations/it.json b/homeassistant/components/geofency/translations/it.json index 0640d351e53..4a74e58e5ca 100644 --- a/homeassistant/components/geofency/translations/it.json +++ b/homeassistant/components/geofency/translations/it.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u00c8 necessaria una sola istanza." }, "create_entry": { - "default": "Per inviare eventi a Home Assistant, dovrai configurare la funzionalit\u00e0 webhook in Geofency.\n\n Compila le seguenti informazioni: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n\n Vedi [la documentazione]({docs_url}) for ulteriori dettagli." + "default": "Per inviare eventi a Home Assistant, dovrai configurare la funzionalit\u00e0 webhook in Geofency.\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Method: POST \n\n Vedi [la documentazione]({docs_url}) for ulteriori dettagli." }, "step": { "user": { diff --git a/homeassistant/components/geofency/translations/pl.json b/homeassistant/components/geofency/translations/pl.json index 9180d3483ef..698aa515b9a 100644 --- a/homeassistant/components/geofency/translations/pl.json +++ b/homeassistant/components/geofency/translations/pl.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Wymagana jest tylko jedna instancja." }, "create_entry": { - "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant'a, musisz skonfigurowa\u0107 webhook w Geofency. \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}), by pozna\u0107 szczeg\u00f3\u0142y." + "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistanta, musisz skonfigurowa\u0107 webhook w Geofency. \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}), by pozna\u0107 szczeg\u00f3\u0142y." }, "step": { "user": { diff --git a/homeassistant/components/geonetnz_quakes/translations/pt-BR.json b/homeassistant/components/geonetnz_quakes/translations/pt-BR.json index 9f08d6b820c..ee705850b03 100644 --- a/homeassistant/components/geonetnz_quakes/translations/pt-BR.json +++ b/homeassistant/components/geonetnz_quakes/translations/pt-BR.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Local j\u00e1 est\u00e1 configurado." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/glances/translations/hu.json b/homeassistant/components/glances/translations/hu.json index ebb44dc1d6e..0958efee4ae 100644 --- a/homeassistant/components/glances/translations/hu.json +++ b/homeassistant/components/glances/translations/hu.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "host": "Kiszolg\u00e1l\u00f3", + "host": "Hoszt", "name": "N\u00e9v", "password": "Jelsz\u00f3", "port": "Port", diff --git a/homeassistant/components/glances/translations/ko.json b/homeassistant/components/glances/translations/ko.json index 2cf0aa1d595..336c9f3b3e5 100644 --- a/homeassistant/components/glances/translations/ko.json +++ b/homeassistant/components/glances/translations/ko.json @@ -29,7 +29,7 @@ "data": { "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \ube48\ub3c4" }, - "description": "Glances \uc635\uc158 \uad6c\uc131" + "description": "Glances \uc635\uc158 \uc124\uc815\ud558\uae30" } } } diff --git a/homeassistant/components/glances/translations/pl.json b/homeassistant/components/glances/translations/pl.json index 2f36613b360..25179e951ad 100644 --- a/homeassistant/components/glances/translations/pl.json +++ b/homeassistant/components/glances/translations/pl.json @@ -10,12 +10,12 @@ "step": { "user": { "data": { - "host": "[%key_id:common::config_flow::data::host%]", + "host": "Nazwa hosta lub adres IP", "name": "Nazwa", - "password": "[%key_id:common::config_flow::data::password%]", - "port": "[%key_id:common::config_flow::data::port%]", + "password": "Has\u0142o", + "port": "Port", "ssl": "U\u017cyj SSL/TLS, aby po\u0142\u0105czy\u0107 si\u0119 z systemem Glances", - "username": "[%key_id:common::config_flow::data::username%]", + "username": "Nazwa u\u017cytkownika", "verify_ssl": "Sprawd\u017a certyfikacj\u0119 systemu", "version": "Glances wersja API (2 lub 3)" }, diff --git a/homeassistant/components/gogogate2/__init__.py b/homeassistant/components/gogogate2/__init__.py index ef802a4aa59..36f623f7895 100644 --- a/homeassistant/components/gogogate2/__init__.py +++ b/homeassistant/components/gogogate2/__init__.py @@ -1 +1,36 @@ """The gogogate2 component.""" +from homeassistant.components.cover import DOMAIN as COVER_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .common import get_data_update_coordinator + + +async def async_setup(hass: HomeAssistant, base_config: dict) -> bool: + """Set up for Gogogate2 controllers.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Do setup of Gogogate2.""" + data_update_coordinator = get_data_update_coordinator(hass, config_entry) + await data_update_coordinator.async_refresh() + + if not data_update_coordinator.last_update_success: + raise ConfigEntryNotReady() + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, COVER_DOMAIN) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Unload Gogogate2 config entry.""" + hass.async_create_task( + hass.config_entries.async_forward_entry_unload(config_entry, COVER_DOMAIN) + ) + + return True diff --git a/homeassistant/components/gogogate2/common.py b/homeassistant/components/gogogate2/common.py new file mode 100644 index 00000000000..c69dee662b0 --- /dev/null +++ b/homeassistant/components/gogogate2/common.py @@ -0,0 +1,99 @@ +"""Common code for GogoGate2 component.""" +from datetime import timedelta +import logging +from typing import Awaitable, Callable, NamedTuple, Optional + +import async_timeout +from gogogate2_api import GogoGate2Api +from gogogate2_api.common import Door + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DATA_UPDATE_COORDINATOR, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class StateData(NamedTuple): + """State data for a cover entity.""" + + config_unique_id: str + unique_id: Optional[str] + door: Optional[Door] + + +class GogoGateDataUpdateCoordinator(DataUpdateCoordinator): + """Manages polling for state changes from the device.""" + + def __init__( + self, + hass: HomeAssistant, + logger: logging.Logger, + api: GogoGate2Api, + *, + name: str, + update_interval: timedelta, + update_method: Optional[Callable[[], Awaitable]] = None, + request_refresh_debouncer: Optional[Debouncer] = None, + ): + """Initialize the data update coordinator.""" + DataUpdateCoordinator.__init__( + self, + hass, + logger, + name=name, + update_interval=update_interval, + update_method=update_method, + request_refresh_debouncer=request_refresh_debouncer, + ) + self.api = api + + +def get_data_update_coordinator( + hass: HomeAssistant, config_entry: ConfigEntry +) -> GogoGateDataUpdateCoordinator: + """Get an update coordinator.""" + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN].setdefault(config_entry.entry_id, {}) + config_entry_data = hass.data[DOMAIN][config_entry.entry_id] + + if DATA_UPDATE_COORDINATOR not in config_entry_data: + api = get_api(config_entry.data) + + async def async_update_data(): + try: + async with async_timeout.timeout(3): + return await hass.async_add_executor_job(api.info) + except Exception as exception: + raise UpdateFailed(f"Error communicating with API: {exception}") + + config_entry_data[DATA_UPDATE_COORDINATOR] = GogoGateDataUpdateCoordinator( + hass, + _LOGGER, + api, + # Name of the data. For logging purposes. + name="gogogate2", + update_method=async_update_data, + # Polling interval. Will only be polled if there are subscribers. + update_interval=timedelta(seconds=5), + ) + + return config_entry_data[DATA_UPDATE_COORDINATOR] + + +def cover_unique_id(config_entry: ConfigEntry, door: Door) -> str: + """Generate a cover entity unique id.""" + return f"{config_entry.unique_id}_{door.door_id}" + + +def get_api(config_data: dict) -> GogoGate2Api: + """Get an api object for config data.""" + return GogoGate2Api( + config_data[CONF_IP_ADDRESS], + config_data[CONF_USERNAME], + config_data[CONF_PASSWORD], + ) diff --git a/homeassistant/components/gogogate2/config_flow.py b/homeassistant/components/gogogate2/config_flow.py new file mode 100644 index 00000000000..bca340fa62b --- /dev/null +++ b/homeassistant/components/gogogate2/config_flow.py @@ -0,0 +1,74 @@ +"""Config flow for Gogogate2.""" +import logging +import re + +from gogogate2_api.common import ApiError +from gogogate2_api.const import ApiErrorCode +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.config_entries import SOURCE_IMPORT, ConfigFlow +from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME + +from .common import get_api +from .const import DOMAIN # pylint: disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +class Gogogate2FlowHandler(ConfigFlow, domain=DOMAIN): + """Gogogate2 config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + async def async_step_import(self, config_data: dict = None): + """Handle importing of configuration.""" + result = await self.async_step_user(config_data) + self._abort_if_unique_id_configured() + return result + + async def async_step_user(self, user_input: dict = None): + """Handle user initiated flow.""" + user_input = user_input or {} + errors = {} + + if user_input: + api = get_api(user_input) + try: + data = await self.hass.async_add_executor_job(api.info) + await self.async_set_unique_id(re.sub("\\..*$", "", data.remoteaccess)) + return self.async_create_entry(title=data.gogogatename, data=user_input) + + except ApiError as api_error: + if api_error.code in ( + ApiErrorCode.CREDENTIALS_NOT_SET, + ApiErrorCode.CREDENTIALS_INCORRECT, + ): + errors["base"] = "invalid_auth" + else: + errors["base"] = "cannot_connect" + + except Exception: # pylint: disable=broad-except + errors["base"] = "cannot_connect" + + if errors and self.source == SOURCE_IMPORT: + return self.async_abort(reason="cannot_connect") + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_IP_ADDRESS, default=user_input.get(CONF_IP_ADDRESS, "") + ): str, + vol.Required( + CONF_USERNAME, default=user_input.get(CONF_USERNAME, "") + ): str, + vol.Required( + CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "") + ): str, + } + ), + errors=errors, + ) diff --git a/homeassistant/components/gogogate2/const.py b/homeassistant/components/gogogate2/const.py new file mode 100644 index 00000000000..359de5f750c --- /dev/null +++ b/homeassistant/components/gogogate2/const.py @@ -0,0 +1,4 @@ +"""Constants for integration.""" + +DOMAIN = "gogogate2" +DATA_UPDATE_COORDINATOR = "data_update_coordinator" diff --git a/homeassistant/components/gogogate2/cover.py b/homeassistant/components/gogogate2/cover.py index 68babd3debe..05fed7621d4 100644 --- a/homeassistant/components/gogogate2/cover.py +++ b/homeassistant/components/gogogate2/cover.py @@ -1,112 +1,190 @@ """Support for Gogogate2 garage Doors.""" +from datetime import datetime, timedelta import logging +from typing import Callable, List, Optional -from pygogogate2 import Gogogate2API as pygogogate2 +from gogogate2_api.common import Door, DoorStatus, get_configured_doors, get_door_by_id import voluptuous as vol -from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN, CoverEntity +from homeassistant.components.cover import ( + DEVICE_CLASS_GARAGE, + SUPPORT_CLOSE, + SUPPORT_OPEN, + CoverEntity, +) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_IP_ADDRESS, - CONF_NAME, CONF_PASSWORD, CONF_USERNAME, - STATE_CLOSED, + STATE_CLOSING, + STATE_OPENING, ) +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +from .common import ( + GogoGateDataUpdateCoordinator, + cover_unique_id, + get_data_update_coordinator, +) +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = "gogogate2" - -NOTIFICATION_ID = "gogogate2_notification" -NOTIFICATION_TITLE = "Gogogate2 Cover Setup" COVER_SCHEMA = vol.Schema( { vol.Required(CONF_IP_ADDRESS): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, } ) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Gogogate2 component.""" - - ip_address = config.get(CONF_IP_ADDRESS) - name = config.get(CONF_NAME) - password = config.get(CONF_PASSWORD) - username = config.get(CONF_USERNAME) - - mygogogate2 = pygogogate2(username, password, ip_address) - - try: - devices = mygogogate2.get_devices() - if devices is False: - raise ValueError("Username or Password is incorrect or no devices found") - - add_entities(MyGogogate2Device(mygogogate2, door, name) for door in devices) - - except (TypeError, KeyError, NameError, ValueError) as ex: - _LOGGER.error("%s", ex) - hass.components.persistent_notification.create( - (f"Error: {ex}
You will need to restart hass after fixing."), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, +async def async_setup_platform( + hass: HomeAssistant, config: dict, add_entities: Callable, discovery_info=None +) -> None: + """Convert old style file configs to new style configs.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config ) + ) -class MyGogogate2Device(CoverEntity): - """Representation of a Gogogate2 cover.""" +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], Optional[bool]], None], +) -> None: + """Set up the config entry.""" + data_update_coordinator = get_data_update_coordinator(hass, config_entry) - def __init__(self, mygogogate2, device, name): - """Initialize with API object, device id.""" - self.mygogogate2 = mygogogate2 - self.device_id = device["door"] - self._name = name or device["name"] - self._status = device["status"] - self._available = None + async_add_entities( + [ + Gogogate2Cover(config_entry, data_update_coordinator, door) + for door in get_configured_doors(data_update_coordinator.data) + ] + ) + + +class Gogogate2Cover(CoverEntity): + """Cover entity for goggate2.""" + + def __init__( + self, + config_entry: ConfigEntry, + data_update_coordinator: GogoGateDataUpdateCoordinator, + door: Door, + ) -> None: + """Initialize the object.""" + self._config_entry = config_entry + self._data_update_coordinator = data_update_coordinator + self._door = door + self._api = data_update_coordinator.api + self._unique_id = cover_unique_id(config_entry, door) + self._is_available = True + self._transition_state: Optional[str] = None + self._transition_state_start: Optional[datetime] = None + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._is_available + + @property + def should_poll(self) -> bool: + """Return False as the data manager handles dispatching data.""" + return False + + @property + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return self._unique_id @property def name(self): - """Return the name of the garage door if any.""" - return self._name if self._name else DEFAULT_NAME + """Return the name of the door.""" + return self._door.name @property def is_closed(self): """Return true if cover is closed, else False.""" - return self._status == STATE_CLOSED + if self._door.status == DoorStatus.OPENED: + return False + if self._door.status == DoorStatus.CLOSED: + return True + + return None + + @property + def is_opening(self): + """Return if the cover is opening or not.""" + return self._transition_state == STATE_OPENING + + @property + def is_closing(self): + """Return if the cover is closing or not.""" + return self._transition_state == STATE_CLOSING @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" - return "garage" + return DEVICE_CLASS_GARAGE @property def supported_features(self): """Flag supported features.""" return SUPPORT_OPEN | SUPPORT_CLOSE + async def async_open_cover(self, **kwargs): + """Open the door.""" + await self.hass.async_add_executor_job(self._api.open_door, self._door.door_id) + self._transition_state = STATE_OPENING + self._transition_state_start = datetime.now() + + async def async_close_cover(self, **kwargs): + """Close the door.""" + await self.hass.async_add_executor_job(self._api.close_door, self._door.door_id) + self._transition_state = STATE_CLOSING + self._transition_state_start = datetime.now() + @property - def available(self): - """Could the device be accessed during the last update call.""" - return self._available + def state_attributes(self): + """Return the state attributes.""" + attrs = super().state_attributes + attrs["door_id"] = self._door.door_id + return attrs - def close_cover(self, **kwargs): - """Issue close command to cover.""" - self.mygogogate2.close_device(self.device_id) + @callback + def async_on_data_updated(self) -> None: + """Receive data from data dispatcher.""" + if not self._data_update_coordinator.last_update_success: + self._is_available = False + self.async_write_ha_state() + return - def open_cover(self, **kwargs): - """Issue open command to cover.""" - self.mygogogate2.open_device(self.device_id) + door = get_door_by_id(self._door.door_id, self._data_update_coordinator.data) - def update(self): - """Update status of cover.""" - try: - self._status = self.mygogogate2.get_status(self.device_id) - self._available = True - except (TypeError, KeyError, NameError, ValueError) as ex: - _LOGGER.error("%s", ex) - self._status = None - self._available = False + # Check if the transition state should expire. + if self._transition_state: + is_transition_state_expired = ( + datetime.now() - self._transition_state_start + ) > timedelta(seconds=60) + + if is_transition_state_expired or self._door.status != door.status: + self._transition_state = None + self._transition_state_start = None + + # Set the state. + self._door = door + self._is_available = True + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Register update dispatcher.""" + self.async_on_remove( + self._data_update_coordinator.async_add_listener(self.async_on_data_updated) + ) diff --git a/homeassistant/components/gogogate2/manifest.json b/homeassistant/components/gogogate2/manifest.json index 829df5a1c37..98aabba43b8 100644 --- a/homeassistant/components/gogogate2/manifest.json +++ b/homeassistant/components/gogogate2/manifest.json @@ -1,7 +1,8 @@ { "domain": "gogogate2", "name": "Gogogate2", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gogogate2", - "requirements": ["pygogogate2==0.1.1"], - "codeowners": [] + "requirements": ["gogogate2-api==1.0.3"], + "codeowners": ["@vangorra"] } diff --git a/homeassistant/components/gogogate2/strings.json b/homeassistant/components/gogogate2/strings.json new file mode 100644 index 00000000000..bbd4e8d80d1 --- /dev/null +++ b/homeassistant/components/gogogate2/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, + "abort": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "step": { + "user": { + "title": "Setup GogoGate2", + "description": "Provide requisite information below.", + "data": { + "ip_address": "IP Address", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + } + } +} diff --git a/homeassistant/components/gogogate2/translations/ca.json b/homeassistant/components/gogogate2/translations/ca.json new file mode 100644 index 00000000000..43525e1870d --- /dev/null +++ b/homeassistant/components/gogogate2/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "cannot_connect": "No s'ha pogut connectar" + }, + "error": { + "cannot_connect": "No s'ha pogut connectar", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + }, + "step": { + "user": { + "data": { + "ip_address": "Adre\u00e7a IP", + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "Proporciona, a continuaci\u00f3, la informaci\u00f3 necess\u00e0ria.", + "title": "Configuraci\u00f3 de GogoGate2" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/translations/de.json b/homeassistant/components/gogogate2/translations/de.json new file mode 100644 index 00000000000..119d198615c --- /dev/null +++ b/homeassistant/components/gogogate2/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, + "step": { + "user": { + "data": { + "ip_address": "IP-Adresse", + "password": "Passwort", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/translations/en.json b/homeassistant/components/gogogate2/translations/en.json new file mode 100644 index 00000000000..d5a93091d91 --- /dev/null +++ b/homeassistant/components/gogogate2/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "cannot_connect": "Failed to connect" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication" + }, + "step": { + "user": { + "data": { + "ip_address": "IP Address", + "password": "Password", + "username": "Username" + }, + "description": "Provide requisite information below.", + "title": "Setup GogoGate2" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/translations/es.json b/homeassistant/components/gogogate2/translations/es.json new file mode 100644 index 00000000000..1498cc12368 --- /dev/null +++ b/homeassistant/components/gogogate2/translations/es.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "cannot_connect": "No se pudo conectar" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" + }, + "step": { + "user": { + "data": { + "ip_address": "Direcci\u00f3n IP", + "password": "Contrase\u00f1a", + "username": "Usuario" + }, + "description": "Proporciona la informaci\u00f3n requerida a continuaci\u00f3n.", + "title": "Configurar GotoGate2" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/translations/hu.json b/homeassistant/components/gogogate2/translations/hu.json new file mode 100644 index 00000000000..952a502a72d --- /dev/null +++ b/homeassistant/components/gogogate2/translations/hu.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/translations/it.json b/homeassistant/components/gogogate2/translations/it.json new file mode 100644 index 00000000000..378d55630a4 --- /dev/null +++ b/homeassistant/components/gogogate2/translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "cannot_connect": "Impossibile connettersi" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida" + }, + "step": { + "user": { + "data": { + "ip_address": "Indirizzo IP", + "password": "Password", + "username": "Nome utente" + }, + "description": "Fornire le informazioni richieste di seguito.", + "title": "Configurazione GogoGate2" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/translations/ko.json b/homeassistant/components/gogogate2/translations/ko.json new file mode 100644 index 00000000000..55b32812bfa --- /dev/null +++ b/homeassistant/components/gogogate2/translations/ko.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "ip_address": "IP \uc8fc\uc18c", + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "\uc544\ub798\uc5d0 \ud544\uc218 \uc815\ubcf4\ub97c \uc81c\uacf5\ud574\uc8fc\uc138\uc694.", + "title": "GogoGate2 \uc124\uce58\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/translations/lb.json b/homeassistant/components/gogogate2/translations/lb.json new file mode 100644 index 00000000000..4183e7dcbc7 --- /dev/null +++ b/homeassistant/components/gogogate2/translations/lb.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "cannot_connect": "Feeler beim verbannen" + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "invalid_auth": "Ong\u00eblteg Authentifikatioun" + }, + "step": { + "user": { + "data": { + "ip_address": "IP Adresse", + "password": "Passwuert", + "username": "Benotzernumm" + }, + "description": "G\u00ebff noutwendeg Informatioun hei \u00ebnnen un.", + "title": "GogoGate 2 ariichten" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/translations/nl.json b/homeassistant/components/gogogate2/translations/nl.json new file mode 100644 index 00000000000..ad8e894d093 --- /dev/null +++ b/homeassistant/components/gogogate2/translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "cannot_connect": "Kon niet verbinden" + }, + "error": { + "cannot_connect": "Kon niet verbinden", + "invalid_auth": "Ongeldige authenticatie" + }, + "step": { + "user": { + "data": { + "ip_address": "IP-adres", + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "description": "Geef hieronder de vereiste informatie op.", + "title": "Stel GogoGate2 in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/translations/no.json b/homeassistant/components/gogogate2/translations/no.json new file mode 100644 index 00000000000..794c72e9aeb --- /dev/null +++ b/homeassistant/components/gogogate2/translations/no.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "user": { + "data": { + "ip_address": "IP adresse", + "password": "Passord", + "username": "Brukernavn" + }, + "description": "Oppgi n\u00f8dvendig informasjon nedenfor.", + "title": "Konfigurer GogoGate2" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/translations/pl.json b/homeassistant/components/gogogate2/translations/pl.json new file mode 100644 index 00000000000..84a683b4dbd --- /dev/null +++ b/homeassistant/components/gogogate2/translations/pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "invalid_auth": "Niepoprawne uwierzytelnienie." + }, + "step": { + "user": { + "data": { + "ip_address": "Adres IP", + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Wprowad\u017a wymagane informacje poni\u017cej.", + "title": "Konfiguracja GogoGate2" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/translations/pt-BR.json b/homeassistant/components/gogogate2/translations/pt-BR.json new file mode 100644 index 00000000000..79dc7af8131 --- /dev/null +++ b/homeassistant/components/gogogate2/translations/pt-BR.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "user": { + "data": { + "ip_address": "Endere\u00e7o IP", + "password": "Senha", + "username": "Nome de usu\u00e1rio" + }, + "description": "Forne\u00e7a as informa\u00e7\u00f5es necess\u00e1rias abaixo.", + "title": "Configurar GogoGate2" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/translations/ru.json b/homeassistant/components/gogogate2/translations/ru.json new file mode 100644 index 00000000000..9f428658820 --- /dev/null +++ b/homeassistant/components/gogogate2/translations/ru.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f." + }, + "error": { + "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." + }, + "step": { + "user": { + "data": { + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 GogoGate2.", + "title": "GogoGate2" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/translations/zh-Hant.json b/homeassistant/components/gogogate2/translations/zh-Hant.json new file mode 100644 index 00000000000..7ba01116084 --- /dev/null +++ b/homeassistant/components/gogogate2/translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + }, + "step": { + "user": { + "data": { + "ip_address": "IP \u4f4d\u5740", + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8acb\u65bc\u4e0b\u65b9\u63d0\u4f9b\u6240\u9700\u8cc7\u8a0a\u3002", + "title": "\u8a2d\u5b9a GogoGate2" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 93afb43cf52..4f1accec4e0 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -15,6 +15,7 @@ import voluptuous as vol from voluptuous.error import Error as VoluptuousError import yaml +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import generate_entity_id @@ -26,8 +27,6 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "google" ENTITY_ID_FORMAT = DOMAIN + ".{}" -CONF_CLIENT_ID = "client_id" -CONF_CLIENT_SECRET = "client_secret" CONF_TRACK_NEW = "track_new_calendar" CONF_CAL_ID = "cal_id" diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 9a133b5e6b7..c1314aeaa41 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -8,6 +8,7 @@ from homeassistant.components import ( fan, group, input_boolean, + input_select, light, lock, media_player, @@ -44,6 +45,7 @@ DEFAULT_EXPOSED_DOMAINS = [ "fan", "group", "input_boolean", + "input_select", "light", "media_player", "scene", @@ -73,6 +75,7 @@ TYPE_DOOR = f"{PREFIX_TYPES}DOOR" TYPE_TV = f"{PREFIX_TYPES}TV" TYPE_SPEAKER = f"{PREFIX_TYPES}SPEAKER" TYPE_ALARM = f"{PREFIX_TYPES}SECURITYSYSTEM" +TYPE_SETTOP = f"{PREFIX_TYPES}SETTOP" SERVICE_REQUEST_SYNC = "request_sync" HOMEGRAPH_URL = "https://homegraph.googleapis.com/" @@ -112,9 +115,10 @@ DOMAIN_TO_GOOGLE_TYPES = { fan.DOMAIN: TYPE_FAN, group.DOMAIN: TYPE_SWITCH, input_boolean.DOMAIN: TYPE_SWITCH, + input_select.DOMAIN: TYPE_SENSOR, light.DOMAIN: TYPE_LIGHT, lock.DOMAIN: TYPE_LOCK, - media_player.DOMAIN: TYPE_SWITCH, + media_player.DOMAIN: TYPE_SETTOP, scene.DOMAIN: TYPE_SCENE, script.DOMAIN: TYPE_SCENE, switch.DOMAIN: TYPE_SWITCH, diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index dd948f1fc51..49e08e90e4b 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -426,6 +426,7 @@ class GoogleEntity: "webhookId": self.config.local_sdk_webhook_id, "httpPort": self.hass.http.server_port, "httpSSL": self.hass.config.api.use_ssl, + "uuid": await self.hass.helpers.instance_id.async_get(), "baseUrl": get_url(self.hass, prefer_external=True), "proxyDeviceId": agent_user_id, } diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 3da91b2b612..41f980fbbdf 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -9,6 +9,7 @@ from homeassistant.components import ( fan, group, input_boolean, + input_select, light, lock, media_player, @@ -41,9 +42,13 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, + STATE_IDLE, STATE_LOCKED, STATE_OFF, STATE_ON, + STATE_PAUSED, + STATE_PLAYING, + STATE_STANDBY, STATE_UNAVAILABLE, STATE_UNKNOWN, TEMP_CELSIUS, @@ -51,7 +56,7 @@ from homeassistant.const import ( ) from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.helpers.network import get_url -from homeassistant.util import color as color_util, temperature as temp_util +from homeassistant.util import color as color_util, dt, temperature as temp_util from .const import ( CHALLENGE_ACK_NEEDED, @@ -84,6 +89,8 @@ TRAIT_OPENCLOSE = f"{PREFIX_TRAITS}OpenClose" TRAIT_VOLUME = f"{PREFIX_TRAITS}Volume" TRAIT_ARMDISARM = f"{PREFIX_TRAITS}ArmDisarm" TRAIT_HUMIDITY_SETTING = f"{PREFIX_TRAITS}HumiditySetting" +TRAIT_TRANSPORT_CONTROL = f"{PREFIX_TRAITS}TransportControl" +TRAIT_MEDIA_STATE = f"{PREFIX_TRAITS}MediaState" PREFIX_COMMANDS = "action.devices.commands." COMMAND_ONOFF = f"{PREFIX_COMMANDS}OnOff" @@ -108,6 +115,15 @@ COMMAND_OPENCLOSE = f"{PREFIX_COMMANDS}OpenClose" COMMAND_SET_VOLUME = f"{PREFIX_COMMANDS}setVolume" COMMAND_VOLUME_RELATIVE = f"{PREFIX_COMMANDS}volumeRelative" COMMAND_ARMDISARM = f"{PREFIX_COMMANDS}ArmDisarm" +COMMAND_MEDIA_NEXT = f"{PREFIX_COMMANDS}mediaNext" +COMMAND_MEDIA_PAUSE = f"{PREFIX_COMMANDS}mediaPause" +COMMAND_MEDIA_PREVIOUS = f"{PREFIX_COMMANDS}mediaPrevious" +COMMAND_MEDIA_RESUME = f"{PREFIX_COMMANDS}mediaResume" +COMMAND_MEDIA_SEEK_RELATIVE = f"{PREFIX_COMMANDS}mediaSeekRelative" +COMMAND_MEDIA_SEEK_TO_POSITION = f"{PREFIX_COMMANDS}mediaSeekToPosition" +COMMAND_MEDIA_SHUFFLE = f"{PREFIX_COMMANDS}mediaShuffle" +COMMAND_MEDIA_STOP = f"{PREFIX_COMMANDS}mediaStop" + TRAITS = [] @@ -1131,11 +1147,15 @@ class ModesTrait(_Trait): SYNONYMS = { "input source": ["input source", "input", "source"], "sound mode": ["sound mode", "effects"], + "option": ["option", "setting", "mode", "value"], } @staticmethod def supported(domain, features, device_class): """Test if state is supported.""" + if domain == input_select.DOMAIN: + return True + if domain != media_player.DOMAIN: return False @@ -1174,15 +1194,20 @@ class ModesTrait(_Trait): attrs = self.state.attributes modes = [] - if media_player.ATTR_INPUT_SOURCE_LIST in attrs: - modes.append( - _generate("input source", attrs[media_player.ATTR_INPUT_SOURCE_LIST]) - ) + if self.state.domain == media_player.DOMAIN: + if media_player.ATTR_INPUT_SOURCE_LIST in attrs: + modes.append( + _generate( + "input source", attrs[media_player.ATTR_INPUT_SOURCE_LIST] + ) + ) - if media_player.ATTR_SOUND_MODE_LIST in attrs: - modes.append( - _generate("sound mode", attrs[media_player.ATTR_SOUND_MODE_LIST]) - ) + if media_player.ATTR_SOUND_MODE_LIST in attrs: + modes.append( + _generate("sound mode", attrs[media_player.ATTR_SOUND_MODE_LIST]) + ) + elif self.state.domain == input_select.DOMAIN: + modes.append(_generate("option", attrs[input_select.ATTR_OPTIONS])) payload = {"availableModes": modes} @@ -1194,11 +1219,16 @@ class ModesTrait(_Trait): response = {} mode_settings = {} - if media_player.ATTR_INPUT_SOURCE_LIST in attrs: - mode_settings["input source"] = attrs.get(media_player.ATTR_INPUT_SOURCE) + if self.state.domain == media_player.DOMAIN: + if media_player.ATTR_INPUT_SOURCE_LIST in attrs: + mode_settings["input source"] = attrs.get( + media_player.ATTR_INPUT_SOURCE + ) - if media_player.ATTR_SOUND_MODE_LIST in attrs: - mode_settings["sound mode"] = attrs.get(media_player.ATTR_SOUND_MODE) + if media_player.ATTR_SOUND_MODE_LIST in attrs: + mode_settings["sound mode"] = attrs.get(media_player.ATTR_SOUND_MODE) + elif self.state.domain == input_select.DOMAIN: + mode_settings["option"] = self.state.state if mode_settings: response["on"] = self.state.state != STATE_OFF @@ -1210,6 +1240,28 @@ class ModesTrait(_Trait): async def execute(self, command, data, params, challenge): """Execute an SetModes command.""" settings = params.get("updateModeSettings") + + if self.state.domain == input_select.DOMAIN: + option = params["updateModeSettings"]["option"] + await self.hass.services.async_call( + input_select.DOMAIN, + input_select.SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: self.state.entity_id, + input_select.ATTR_OPTION: option, + }, + blocking=True, + context=data.context, + ) + + return + if self.state.domain != media_player.DOMAIN: + _LOGGER.info( + "Received an Options command for unrecognised domain %s", + self.state.domain, + ) + return + requested_source = settings.get("input source") sound_mode = settings.get("sound mode") @@ -1463,3 +1515,183 @@ def _verify_ack_challenge(data, state, challenge): return if not challenge or not challenge.get("ack"): raise ChallengeNeeded(CHALLENGE_ACK_NEEDED) + + +MEDIA_COMMAND_SUPPORT_MAPPING = { + COMMAND_MEDIA_NEXT: media_player.SUPPORT_NEXT_TRACK, + COMMAND_MEDIA_PAUSE: media_player.SUPPORT_PAUSE, + COMMAND_MEDIA_PREVIOUS: media_player.SUPPORT_PREVIOUS_TRACK, + COMMAND_MEDIA_RESUME: media_player.SUPPORT_PLAY, + COMMAND_MEDIA_SEEK_RELATIVE: media_player.SUPPORT_SEEK, + COMMAND_MEDIA_SEEK_TO_POSITION: media_player.SUPPORT_SEEK, + COMMAND_MEDIA_SHUFFLE: media_player.SUPPORT_SHUFFLE_SET, + COMMAND_MEDIA_STOP: media_player.SUPPORT_STOP, +} + +MEDIA_COMMAND_ATTRIBUTES = { + COMMAND_MEDIA_NEXT: "NEXT", + COMMAND_MEDIA_PAUSE: "PAUSE", + COMMAND_MEDIA_PREVIOUS: "PREVIOUS", + COMMAND_MEDIA_RESUME: "RESUME", + COMMAND_MEDIA_SEEK_RELATIVE: "SEEK_RELATIVE", + COMMAND_MEDIA_SEEK_TO_POSITION: "SEEK_TO_POSITION", + COMMAND_MEDIA_SHUFFLE: "SHUFFLE", + COMMAND_MEDIA_STOP: "STOP", +} + + +@register_trait +class TransportControlTrait(_Trait): + """Trait to control media playback. + + https://developers.google.com/actions/smarthome/traits/transportcontrol + """ + + name = TRAIT_TRANSPORT_CONTROL + commands = [ + COMMAND_MEDIA_NEXT, + COMMAND_MEDIA_PAUSE, + COMMAND_MEDIA_PREVIOUS, + COMMAND_MEDIA_RESUME, + COMMAND_MEDIA_SEEK_RELATIVE, + COMMAND_MEDIA_SEEK_TO_POSITION, + COMMAND_MEDIA_SHUFFLE, + COMMAND_MEDIA_STOP, + ] + + @staticmethod + def supported(domain, features, device_class): + """Test if state is supported.""" + if domain == media_player.DOMAIN: + for feature in MEDIA_COMMAND_SUPPORT_MAPPING.values(): + if features & feature: + return True + + return False + + def sync_attributes(self): + """Return opening direction.""" + response = {} + + if self.state.domain == media_player.DOMAIN: + features = self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + + support = [] + for command, feature in MEDIA_COMMAND_SUPPORT_MAPPING.items(): + if features & feature: + support.append(MEDIA_COMMAND_ATTRIBUTES[command]) + response["transportControlSupportedCommands"] = support + + return response + + def query_attributes(self): + """Return the attributes of this trait for this entity.""" + + return {} + + async def execute(self, command, data, params, challenge): + """Execute a media command.""" + + service_attrs = {ATTR_ENTITY_ID: self.state.entity_id} + + if command == COMMAND_MEDIA_SEEK_RELATIVE: + service = media_player.SERVICE_MEDIA_SEEK + + rel_position = params["relativePositionMs"] / 1000 + seconds_since = 0 # Default to 0 seconds + if self.state.state == STATE_PLAYING: + now = dt.utcnow() + upd_at = self.state.attributes.get( + media_player.ATTR_MEDIA_POSITION_UPDATED_AT, now + ) + seconds_since = (now - upd_at).total_seconds() + position = self.state.attributes.get(media_player.ATTR_MEDIA_POSITION, 0) + max_position = self.state.attributes.get( + media_player.ATTR_MEDIA_DURATION, 0 + ) + service_attrs[media_player.ATTR_MEDIA_SEEK_POSITION] = min( + max(position + seconds_since + rel_position, 0), max_position + ) + elif command == COMMAND_MEDIA_SEEK_TO_POSITION: + service = media_player.SERVICE_MEDIA_SEEK + + max_position = self.state.attributes.get( + media_player.ATTR_MEDIA_DURATION, 0 + ) + service_attrs[media_player.ATTR_MEDIA_SEEK_POSITION] = min( + max(params["absPositionMs"] / 1000, 0), max_position + ) + elif command == COMMAND_MEDIA_NEXT: + service = media_player.SERVICE_MEDIA_NEXT_TRACK + elif command == COMMAND_MEDIA_PAUSE: + service = media_player.SERVICE_MEDIA_PAUSE + elif command == COMMAND_MEDIA_PREVIOUS: + service = media_player.SERVICE_MEDIA_PREVIOUS_TRACK + elif command == COMMAND_MEDIA_RESUME: + service = media_player.SERVICE_MEDIA_PLAY + elif command == COMMAND_MEDIA_SHUFFLE: + service = media_player.SERVICE_SHUFFLE_SET + + # Google Assistant only supports enabling shuffle + service_attrs[media_player.ATTR_MEDIA_SHUFFLE] = True + elif command == COMMAND_MEDIA_STOP: + service = media_player.SERVICE_MEDIA_STOP + else: + raise SmartHomeError(ERR_NOT_SUPPORTED, "Command not supported") + + await self.hass.services.async_call( + media_player.DOMAIN, + service, + service_attrs, + blocking=True, + context=data.context, + ) + + +@register_trait +class MediaStateTrait(_Trait): + """Trait to get media playback state. + + https://developers.google.com/actions/smarthome/traits/mediastate + """ + + name = TRAIT_MEDIA_STATE + commands = [] + + activity_lookup = { + STATE_OFF: "INACTIVE", + STATE_IDLE: "STANDBY", + STATE_PLAYING: "ACTIVE", + STATE_ON: "STANDBY", + STATE_PAUSED: "STANDBY", + STATE_STANDBY: "STANDBY", + STATE_UNAVAILABLE: "INACTIVE", + STATE_UNKNOWN: "INACTIVE", + } + + playback_lookup = { + STATE_OFF: "STOPPED", + STATE_IDLE: "STOPPED", + STATE_PLAYING: "PLAYING", + STATE_ON: "STOPPED", + STATE_PAUSED: "PAUSED", + STATE_STANDBY: "STOPPED", + STATE_UNAVAILABLE: "STOPPED", + STATE_UNKNOWN: "STOPPED", + } + + @staticmethod + def supported(domain, features, device_class): + """Test if state is supported.""" + return domain == media_player.DOMAIN + + def sync_attributes(self): + """Return attributes for a sync request.""" + return {"supportActivityState": True, "supportPlaybackState": True} + + def query_attributes(self): + """Return the attributes of this trait for this entity.""" + return { + "activityState": self.activity_lookup.get(self.state.state, "INACTIVE"), + "playbackState": self.playback_lookup.get(self.state.state, "STOPPED"), + } diff --git a/homeassistant/components/gpslogger/translations/it.json b/homeassistant/components/gpslogger/translations/it.json index 8c26f19ac5b..32e6b574096 100644 --- a/homeassistant/components/gpslogger/translations/it.json +++ b/homeassistant/components/gpslogger/translations/it.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u00c8 necessaria una sola istanza." }, "create_entry": { - "default": "Per inviare eventi a Home Assistant, dovrai configurare la funzionalit\u00e0 webhook in GPSLogger.\n\n Compila le seguenti informazioni: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n\n Vedi [la documentazione]({docs_url}) for ulteriori dettagli." + "default": "Per inviare eventi a Home Assistant, dovrai configurare la funzionalit\u00e0 webhook in GPSLogger.\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Method: POST \n\n Vedi [la documentazione]({docs_url}) for ulteriori dettagli." }, "step": { "user": { diff --git a/homeassistant/components/gpslogger/translations/pl.json b/homeassistant/components/gpslogger/translations/pl.json index 76cac00025f..8b3486496a0 100644 --- a/homeassistant/components/gpslogger/translations/pl.json +++ b/homeassistant/components/gpslogger/translations/pl.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Wymagana jest tylko jedna instancja." }, "create_entry": { - "default": "Aby wysy\u0142a\u0107 lokalizacje do Home Assistant'a, musisz skonfigurowa\u0107 webhook w aplikacji GPSLogger. \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}), by pozna\u0107 szczeg\u00f3\u0142y." + "default": "Aby wysy\u0142a\u0107 lokalizacje do Home Assistanta, musisz skonfigurowa\u0107 webhook w aplikacji GPSLogger. \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}), by pozna\u0107 szczeg\u00f3\u0142y." }, "step": { "user": { diff --git a/homeassistant/components/graphite/__init__.py b/homeassistant/components/graphite/__init__.py index 00dabd59d8f..327e8293be7 100644 --- a/homeassistant/components/graphite/__init__.py +++ b/homeassistant/components/graphite/__init__.py @@ -139,7 +139,16 @@ class GraphiteFeeder(threading.Thread): _LOGGER.debug("Event processing thread stopped") self._queue.task_done() return - if event.event_type == EVENT_STATE_CHANGED and event.data.get("new_state"): + if event.event_type == EVENT_STATE_CHANGED: + if not event.data.get("new_state"): + _LOGGER.debug( + "Skipping %s without new_state for %s", + event.event_type, + event.data["entity_id"], + ) + self._queue.task_done() + continue + _LOGGER.debug( "Processing STATE_CHANGED event for %s", event.data["entity_id"] ) diff --git a/homeassistant/components/griddy/translations/pl.json b/homeassistant/components/griddy/translations/pl.json index e62ce8f7bdc..e28b4b6f2e6 100644 --- a/homeassistant/components/griddy/translations/pl.json +++ b/homeassistant/components/griddy/translations/pl.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "unknown": "[%key_id:common::config_flow::error::unknown%]" + "unknown": "Nieoczekiwany b\u0142\u0105d." }, "step": { "user": { diff --git a/homeassistant/components/griddy/translations/pt-BR.json b/homeassistant/components/griddy/translations/pt-BR.json new file mode 100644 index 00000000000..dc9c1362dc4 --- /dev/null +++ b/homeassistant/components/griddy/translations/pt-BR.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha ao conectar, tente novamente", + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index c4e691eeff9..0832d466f8c 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -35,7 +35,9 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, CONF_ENTITIES, CONF_NAME, - STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, ) from homeassistant.core import State, callback import homeassistant.helpers.config_validation as cv @@ -73,6 +75,8 @@ class CoverGroup(CoverEntity): """Initialize a CoverGroup entity.""" self._name = name self._is_closed = False + self._is_closing = False + self._is_opening = False self._cover_position: Optional[int] = 100 self._tilt_position = None self._supported_features = 0 @@ -176,6 +180,16 @@ class CoverGroup(CoverEntity): """Return if all covers in group are closed.""" return self._is_closed + @property + def is_opening(self): + """Return if the cover is opening or not.""" + return self._is_opening + + @property + def is_closing(self): + """Return if the cover is closing or not.""" + return self._is_closing + @property def current_cover_position(self) -> Optional[int]: """Return current position for all covers.""" @@ -253,13 +267,21 @@ class CoverGroup(CoverEntity): self._assumed_state = False self._is_closed = True + self._is_closing = False + self._is_opening = False for entity_id in self._entities: state = self.hass.states.get(entity_id) if not state: continue - if state.state != STATE_CLOSED: + if state.state == STATE_OPEN: self._is_closed = False break + if state.state == STATE_CLOSING: + self._is_closing = True + break + if state.state == STATE_OPENING: + self._is_opening = True + break self._cover_position = None if self._covers[KEY_POSITION]: diff --git a/homeassistant/components/group/translations/ca.json b/homeassistant/components/group/translations/ca.json index bbbd84b2147..21b6361589c 100644 --- a/homeassistant/components/group/translations/ca.json +++ b/homeassistant/components/group/translations/ca.json @@ -1,17 +1,17 @@ { "state": { "_": { - "closed": "Tancat", + "closed": "Tancat/da", "home": "A casa", "locked": "Bloquejat", "not_home": "Fora", - "off": "Desactivat", - "ok": "Correcte", - "on": "Activat", - "open": "Obert", + "off": "OFF", + "ok": "OK", + "on": "ON", + "open": "Obert/a", "problem": "Problema", "unlocked": "Desbloquejat" } }, - "title": "Grups" + "title": "Grup" } \ No newline at end of file diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index 6bfdf7f2552..c228bcbe4ab 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, CONF_USERNAME, + ELECTRICAL_CURRENT_AMPERE, ENERGY_KILO_WATT_HOUR, FREQUENCY_HERTZ, POWER_WATT, @@ -61,17 +62,37 @@ INVERTER_SENSOR_TYPES = { "power", ), "inverter_voltage_input_1": ("Input 1 voltage", VOLT, "vpv1", None), - "inverter_amperage_input_1": ("Input 1 Amperage", "A", "ipv1", None), + "inverter_amperage_input_1": ( + "Input 1 Amperage", + ELECTRICAL_CURRENT_AMPERE, + "ipv1", + None, + ), "inverter_wattage_input_1": ("Input 1 Wattage", POWER_WATT, "ppv1", "power"), "inverter_voltage_input_2": ("Input 2 voltage", VOLT, "vpv2", None), - "inverter_amperage_input_2": ("Input 2 Amperage", "A", "ipv2", None), + "inverter_amperage_input_2": ( + "Input 2 Amperage", + ELECTRICAL_CURRENT_AMPERE, + "ipv2", + None, + ), "inverter_wattage_input_2": ("Input 2 Wattage", POWER_WATT, "ppv2", "power"), "inverter_voltage_input_3": ("Input 3 voltage", VOLT, "vpv3", None), - "inverter_amperage_input_3": ("Input 3 Amperage", "A", "ipv3", None), + "inverter_amperage_input_3": ( + "Input 3 Amperage", + ELECTRICAL_CURRENT_AMPERE, + "ipv3", + None, + ), "inverter_wattage_input_3": ("Input 3 Wattage", POWER_WATT, "ppv3", "power"), "inverter_internal_wattage": ("Internal wattage", POWER_WATT, "ppv", "power"), "inverter_reactive_voltage": ("Reactive voltage", VOLT, "vacr", None), - "inverter_inverter_reactive_amperage": ("Reactive amperage", "A", "iacr", None), + "inverter_inverter_reactive_amperage": ( + "Reactive amperage", + ELECTRICAL_CURRENT_AMPERE, + "iacr", + None, + ), "inverter_frequency": ("AC frequency", FREQUENCY_HERTZ, "fac", None), "inverter_current_wattage": ("Output power", POWER_WATT, "pac", "power"), "inverter_current_reactive_wattage": ( diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py new file mode 100644 index 00000000000..8ccf69c7077 --- /dev/null +++ b/homeassistant/components/guardian/__init__.py @@ -0,0 +1,364 @@ +"""The Elexa Guardian integration.""" +import asyncio +from datetime import timedelta + +from aioguardian import Client +from aioguardian.commands.device import ( + DEFAULT_FIRMWARE_UPGRADE_FILENAME, + DEFAULT_FIRMWARE_UPGRADE_PORT, + DEFAULT_FIRMWARE_UPGRADE_URL, +) +from aioguardian.errors import GuardianError +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_ATTRIBUTION, + CONF_FILENAME, + CONF_IP_ADDRESS, + CONF_PORT, + CONF_URL, +) +from homeassistant.core import HomeAssistant, callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.service import ( + async_register_admin_service, + verify_domain_control, +) + +from .const import ( + CONF_UID, + DATA_CLIENT, + DATA_DIAGNOSTICS, + DATA_PAIR_DUMP, + DATA_PING, + DATA_SENSOR_STATUS, + DATA_VALVE_STATUS, + DATA_WIFI_STATUS, + DOMAIN, + LOGGER, + SENSOR_KIND_AP_INFO, + SENSOR_KIND_LEAK_DETECTED, + SENSOR_KIND_TEMPERATURE, + SWITCH_KIND_VALVE, + TOPIC_UPDATE, +) + +DATA_ENTITY_TYPE_MAP = { + SENSOR_KIND_AP_INFO: DATA_WIFI_STATUS, + SENSOR_KIND_LEAK_DETECTED: DATA_SENSOR_STATUS, + SENSOR_KIND_TEMPERATURE: DATA_SENSOR_STATUS, + SWITCH_KIND_VALVE: DATA_VALVE_STATUS, +} + +DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) + +PLATFORMS = ["binary_sensor", "sensor", "switch"] + +SERVICE_UPGRADE_FIRMWARE_SCHEMA = vol.Schema( + { + vol.Optional(CONF_URL, default=DEFAULT_FIRMWARE_UPGRADE_URL): cv.url, + vol.Optional(CONF_PORT, default=DEFAULT_FIRMWARE_UPGRADE_PORT): cv.port, + vol.Optional( + CONF_FILENAME, default=DEFAULT_FIRMWARE_UPGRADE_FILENAME + ): cv.string, + } +) + + +@callback +def async_get_api_category(entity_kind: str): + """Get the API data category to which an entity belongs.""" + return DATA_ENTITY_TYPE_MAP.get(entity_kind) + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Elexa Guardian component.""" + hass.data[DOMAIN] = {DATA_CLIENT: {}} + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Elexa Guardian from a config entry.""" + _verify_domain_control = verify_domain_control(hass, DOMAIN) + + guardian = Guardian(hass, entry) + await guardian.async_update() + hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] = guardian + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + @_verify_domain_control + async def disable_ap(call): + """Disable the device's onboard access point.""" + try: + async with guardian.client: + await guardian.client.device.wifi_disable_ap() + except GuardianError as err: + LOGGER.error("Error during service call: %s", err) + return + + @_verify_domain_control + async def enable_ap(call): + """Enable the device's onboard access point.""" + try: + async with guardian.client: + await guardian.client.device.wifi_enable_ap() + except GuardianError as err: + LOGGER.error("Error during service call: %s", err) + return + + @_verify_domain_control + async def reboot(call): + """Reboot the device.""" + try: + async with guardian.client: + await guardian.client.device.reboot() + except GuardianError as err: + LOGGER.error("Error during service call: %s", err) + return + + @_verify_domain_control + async def reset_valve_diagnostics(call): + """Fully reset system motor diagnostics.""" + try: + async with guardian.client: + await guardian.client.valve.valve_reset() + except GuardianError as err: + LOGGER.error("Error during service call: %s", err) + return + + @_verify_domain_control + async def upgrade_firmware(call): + """Upgrade the device firmware.""" + try: + async with guardian.client: + await guardian.client.device.upgrade_firmware( + url=call.data[CONF_URL], + port=call.data[CONF_PORT], + filename=call.data[CONF_FILENAME], + ) + except GuardianError as err: + LOGGER.error("Error during service call: %s", err) + return + + for service, method, schema in [ + ("disable_ap", disable_ap, None), + ("enable_ap", enable_ap, None), + ("reboot", reboot, None), + ("reset_valve_diagnostics", reset_valve_diagnostics, None), + ("upgrade_firmware", upgrade_firmware, SERVICE_UPGRADE_FIRMWARE_SCHEMA), + ]: + async_register_admin_service(hass, DOMAIN, service, method, schema=schema) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN][DATA_CLIENT].pop(entry.entry_id) + + return unload_ok + + +class Guardian: + """Define a class to communicate with the Guardian device.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry): + """Initialize.""" + self._async_cancel_time_interval_listener = None + self._hass = hass + self.client = Client(entry.data[CONF_IP_ADDRESS]) + self.data = {} + self.uid = entry.data[CONF_UID] + + self._api_coros = { + DATA_DIAGNOSTICS: self.client.device.diagnostics, + DATA_PAIR_DUMP: self.client.sensor.pair_dump, + DATA_PING: self.client.device.ping, + DATA_SENSOR_STATUS: self.client.sensor.sensor_status, + DATA_VALVE_STATUS: self.client.valve.valve_status, + DATA_WIFI_STATUS: self.client.device.wifi_status, + } + + self._api_category_count = { + DATA_SENSOR_STATUS: 0, + DATA_VALVE_STATUS: 0, + DATA_WIFI_STATUS: 0, + } + + self._api_lock = asyncio.Lock() + + async def _async_get_data_from_api(self, api_category: str): + """Update and save data for a particular API category.""" + if self._api_category_count.get(api_category) == 0: + return + + try: + result = await self._api_coros[api_category]() + except GuardianError as err: + LOGGER.error("Error while fetching %s data: %s", api_category, err) + self.data[api_category] = {} + else: + self.data[api_category] = result["data"] + + async def _async_update_listener_action(self, _): + """Define an async_track_time_interval action to update data.""" + await self.async_update() + + @callback + def async_deregister_api_interest(self, sensor_kind: str): + """Decrement the number of entities with data needs from an API category.""" + # If this deregistration should leave us with no registration at all, remove the + # time interval: + if sum(self._api_category_count.values()) == 0: + if self._async_cancel_time_interval_listener: + self._async_cancel_time_interval_listener() + self._async_cancel_time_interval_listener = None + return + + api_category = async_get_api_category(sensor_kind) + if api_category: + self._api_category_count[api_category] -= 1 + + async def async_register_api_interest(self, sensor_kind: str): + """Increment the number of entities with data needs from an API category.""" + # If this is the first registration we have, start a time interval: + if not self._async_cancel_time_interval_listener: + self._async_cancel_time_interval_listener = async_track_time_interval( + self._hass, self._async_update_listener_action, DEFAULT_SCAN_INTERVAL, + ) + + api_category = async_get_api_category(sensor_kind) + + if not api_category: + return + + self._api_category_count[api_category] += 1 + + # If a sensor registers interest in a particular API call and the data doesn't + # exist for it yet, make the API call and grab the data: + async with self._api_lock: + if api_category not in self.data: + async with self.client: + await self._async_get_data_from_api(api_category) + + async def async_update(self): + """Get updated data from the device.""" + async with self.client: + tasks = [ + self._async_get_data_from_api(api_category) + for api_category in self._api_coros + ] + + await asyncio.gather(*tasks) + + LOGGER.debug("Received new data: %s", self.data) + async_dispatcher_send(self._hass, TOPIC_UPDATE.format(self.uid)) + + +class GuardianEntity(Entity): + """Define a base Guardian entity.""" + + def __init__( + self, guardian: Guardian, kind: str, name: str, device_class: str, icon: str + ): + """Initialize.""" + self._attrs = {ATTR_ATTRIBUTION: "Data provided by Elexa"} + self._available = True + self._device_class = device_class + self._guardian = guardian + self._icon = icon + self._kind = kind + self._name = name + + @property + def available(self): + """Return whether the entity is available.""" + return bool(self._guardian.data[DATA_PING]) + + @property + def device_class(self): + """Return the device class.""" + return self._device_class + + @property + def device_info(self): + """Return device registry information for this entity.""" + return { + "identifiers": {(DOMAIN, self._guardian.uid)}, + "manufacturer": "Elexa", + "model": self._guardian.data[DATA_DIAGNOSTICS]["firmware"], + "name": f"Guardian {self._guardian.uid}", + } + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attrs + + @property + def icon(self) -> str: + """Return the icon.""" + return self._icon + + @property + def name(self): + """Return the name of the entity.""" + return f"Guardian {self._guardian.uid}: {self._name}" + + @property + def should_poll(self) -> bool: + """Return True if entity has to be polled for state.""" + return False + + @property + def unique_id(self): + """Return the unique ID of the entity.""" + return f"{self._guardian.uid}_{self._kind}" + + @callback + def _update_from_latest_data(self): + """Update the entity.""" + raise NotImplementedError + + async def async_added_to_hass(self): + """Register callbacks.""" + + @callback + def update(): + """Update the state.""" + self._update_from_latest_data() + self.async_write_ha_state() + + self.async_on_remove( + async_dispatcher_connect( + self.hass, TOPIC_UPDATE.format(self._guardian.uid), update + ) + ) + + await self._guardian.async_register_api_interest(self._kind) + + self._update_from_latest_data() + + async def async_will_remove_from_hass(self) -> None: + """Disconnect dispatcher listener when removed.""" + self._guardian.async_deregister_api_interest(self._kind) diff --git a/homeassistant/components/guardian/binary_sensor.py b/homeassistant/components/guardian/binary_sensor.py new file mode 100644 index 00000000000..f9d70d03d5d --- /dev/null +++ b/homeassistant/components/guardian/binary_sensor.py @@ -0,0 +1,62 @@ +"""Binary sensors for the Elexa Guardian integration.""" +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.core import callback + +from . import GuardianEntity +from .const import ( + DATA_CLIENT, + DATA_SENSOR_STATUS, + DATA_WIFI_STATUS, + DOMAIN, + SENSOR_KIND_AP_INFO, + SENSOR_KIND_LEAK_DETECTED, +) + +ATTR_CONNECTED_CLIENTS = "connected_clients" + +SENSORS = [ + (SENSOR_KIND_AP_INFO, "Onboard AP Enabled", "connectivity"), + (SENSOR_KIND_LEAK_DETECTED, "Leak Detected", "moisture"), +] + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Guardian switches based on a config entry.""" + guardian = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] + async_add_entities( + [ + GuardianBinarySensor(guardian, kind, name, device_class) + for kind, name, device_class in SENSORS + ], + True, + ) + + +class GuardianBinarySensor(GuardianEntity, BinarySensorEntity): + """Define a generic Guardian sensor.""" + + def __init__(self, guardian, kind, name, device_class): + """Initialize.""" + super().__init__(guardian, kind, name, device_class, None) + + self._is_on = True + + @property + def is_on(self): + """Return True if the binary sensor is on.""" + return self._is_on + + @callback + def _update_from_latest_data(self): + """Update the entity.""" + if self._kind == SENSOR_KIND_AP_INFO: + self._is_on = self._guardian.data[DATA_WIFI_STATUS]["ap_enabled"] + self._attrs.update( + { + ATTR_CONNECTED_CLIENTS: self._guardian.data[DATA_WIFI_STATUS][ + "ap_clients" + ] + } + ) + elif self._kind == SENSOR_KIND_LEAK_DETECTED: + self._is_on = self._guardian.data[DATA_SENSOR_STATUS]["wet"] diff --git a/homeassistant/components/guardian/config_flow.py b/homeassistant/components/guardian/config_flow.py new file mode 100644 index 00000000000..dae8fafb1e0 --- /dev/null +++ b/homeassistant/components/guardian/config_flow.py @@ -0,0 +1,111 @@ +"""Config flow for Elexa Guardian integration.""" +from aioguardian import Client +from aioguardian.errors import GuardianError +import voluptuous as vol + +from homeassistant import config_entries, core +from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT +from homeassistant.core import callback + +from .const import CONF_UID, DOMAIN, LOGGER # pylint:disable=unused-import + +DATA_SCHEMA = vol.Schema( + {vol.Required(CONF_IP_ADDRESS): str, vol.Required(CONF_PORT, default=7777): int} +) + +UNIQUE_ID = "guardian_{0}" + + +@callback +def async_get_pin_from_discovery_hostname(hostname): + """Get the device's 4-digit PIN from its zeroconf-discovered hostname.""" + return hostname.split(".")[0].split("-")[1] + + +@callback +def async_get_pin_from_uid(uid): + """Get the device's 4-digit PIN from its UID.""" + return uid[-4:] + + +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. + """ + async with Client(data[CONF_IP_ADDRESS]) as client: + ping_data = await client.device.ping() + + return { + CONF_UID: ping_data["data"]["uid"], + } + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Elexa Guardian.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize.""" + self.discovery_info = {} + + async def _async_set_unique_id(self, pin): + """Set the config entry's unique ID (based on the device's 4-digit PIN).""" + await self.async_set_unique_id(UNIQUE_ID.format(pin)) + self._abort_if_unique_id_configured() + + async def async_step_user(self, user_input=None): + """Handle configuration via the UI.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors={} + ) + + try: + info = await validate_input(self.hass, user_input) + except GuardianError as err: + LOGGER.error("Error while connecting to unit: %s", err) + return self.async_show_form( + step_id="user", + data_schema=DATA_SCHEMA, + errors={CONF_IP_ADDRESS: "cannot_connect"}, + ) + + pin = async_get_pin_from_uid(info[CONF_UID]) + await self._async_set_unique_id(pin) + + return self.async_create_entry( + title=info[CONF_UID], data={CONF_UID: info["uid"], **user_input} + ) + + async def async_step_zeroconf(self, discovery_info=None): + """Handle the configuration via zeroconf.""" + if discovery_info is None: + return self.async_abort(reason="connection_error") + + pin = async_get_pin_from_discovery_hostname(discovery_info["hostname"]) + await self._async_set_unique_id(pin) + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context[CONF_IP_ADDRESS] = discovery_info["host"] + + if any( + discovery_info["host"] == flow["context"][CONF_IP_ADDRESS] + for flow in self._async_in_progress() + ): + return self.async_abort(reason="already_in_progress") + + self.discovery_info = { + CONF_IP_ADDRESS: discovery_info["host"], + CONF_PORT: discovery_info["port"], + } + + return await self.async_step_zeroconf_confirm() + + async def async_step_zeroconf_confirm(self, user_input=None): + """Finish the configuration via zeroconf.""" + if user_input is None: + return self.async_show_form(step_id="zeroconf_confirm") + return await self.async_step_user(self.discovery_info) diff --git a/homeassistant/components/guardian/const.py b/homeassistant/components/guardian/const.py new file mode 100644 index 00000000000..f1d60fd07da --- /dev/null +++ b/homeassistant/components/guardian/const.py @@ -0,0 +1,25 @@ +"""Constants for the Elexa Guardian integration.""" +import logging + +DOMAIN = "guardian" + +LOGGER = logging.getLogger(__package__) + +CONF_UID = "uid" + +DATA_CLIENT = "client" +DATA_DIAGNOSTICS = "diagnostics" +DATA_PAIR_DUMP = "pair_sensor" +DATA_PING = "ping" +DATA_SENSOR_STATUS = "sensor_status" +DATA_VALVE_STATUS = "valve_status" +DATA_WIFI_STATUS = "wifi_status" + +SENSOR_KIND_AP_INFO = "ap_enabled" +SENSOR_KIND_LEAK_DETECTED = "leak_detected" +SENSOR_KIND_TEMPERATURE = "temperature" +SENSOR_KIND_UPTIME = "uptime" + +SWITCH_KIND_VALVE = "valve" + +TOPIC_UPDATE = "guardian_update_{0}" diff --git a/homeassistant/components/guardian/manifest.json b/homeassistant/components/guardian/manifest.json new file mode 100644 index 00000000000..a3e2d9e66ee --- /dev/null +++ b/homeassistant/components/guardian/manifest.json @@ -0,0 +1,18 @@ +{ + "domain": "guardian", + "name": "Elexa Guardian", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/guardian", + "requirements": [ + "aioguardian==0.2.3" + ], + "ssdp": [], + "zeroconf": [ + "_api._udp.local." + ], + "homekit": {}, + "dependencies": [], + "codeowners": [ + "@bachya" + ] +} diff --git a/homeassistant/components/guardian/sensor.py b/homeassistant/components/guardian/sensor.py new file mode 100644 index 00000000000..4da200224cf --- /dev/null +++ b/homeassistant/components/guardian/sensor.py @@ -0,0 +1,73 @@ +"""Sensors for the Elexa Guardian integration.""" +from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_FAHRENHEIT, TIME_MINUTES +from homeassistant.core import callback + +from . import Guardian, GuardianEntity +from .const import ( + DATA_CLIENT, + DATA_DIAGNOSTICS, + DATA_SENSOR_STATUS, + DOMAIN, + SENSOR_KIND_TEMPERATURE, + SENSOR_KIND_UPTIME, +) + +SENSORS = [ + ( + SENSOR_KIND_TEMPERATURE, + "Temperature", + DEVICE_CLASS_TEMPERATURE, + None, + TEMP_FAHRENHEIT, + ), + (SENSOR_KIND_UPTIME, "Uptime", None, "mdi:timer", TIME_MINUTES), +] + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Guardian switches based on a config entry.""" + guardian = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] + async_add_entities( + [ + GuardianSensor(guardian, kind, name, device_class, icon, unit) + for kind, name, device_class, icon, unit in SENSORS + ], + True, + ) + + +class GuardianSensor(GuardianEntity): + """Define a generic Guardian sensor.""" + + def __init__( + self, + guardian: Guardian, + kind: str, + name: str, + device_class: str, + icon: str, + unit: str, + ): + """Initialize.""" + super().__init__(guardian, kind, name, device_class, icon) + + self._state = None + self._unit = unit + + @property + def state(self): + """Return the sensor state.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._unit + + @callback + def _update_from_latest_data(self): + """Update the entity.""" + if self._kind == SENSOR_KIND_TEMPERATURE: + self._state = self._guardian.data[DATA_SENSOR_STATUS]["temperature"] + elif self._kind == SENSOR_KIND_UPTIME: + self._state = self._guardian.data[DATA_DIAGNOSTICS]["uptime"] diff --git a/homeassistant/components/guardian/services.yaml b/homeassistant/components/guardian/services.yaml new file mode 100644 index 00000000000..42565448451 --- /dev/null +++ b/homeassistant/components/guardian/services.yaml @@ -0,0 +1,21 @@ +# Describes the format for available Elexa Guardians services +disable_ap: + description: Disable the device's onboard access point. +enable_ap: + description: Enable the device's onboard access point. +reboot: + description: Reboot the device. +reset_valve_diagnostics: + description: Fully (and irrecoverably) reset all valve diagnostics. +upgrade_firmware: + description: Upgrade the device firmware. + fields: + url: + description: (optional) The URL of the server hosting the firmware file. + example: https://repo.guardiancloud.services/gvc/fw + port: + description: (optional) The port on which the firmware file is served. + example: 443 + filename: + description: (optional) The firmware filename. + example: latest.bin diff --git a/homeassistant/components/guardian/strings.json b/homeassistant/components/guardian/strings.json new file mode 100644 index 00000000000..3f87d3260f4 --- /dev/null +++ b/homeassistant/components/guardian/strings.json @@ -0,0 +1,22 @@ +{ + "title": "Elexa Guardian", + "config": { + "step": { + "user": { + "description": "Configure a local Elexa Guardian device.", + "data": { + "ip_address": "IP Address", + "port": "Port" + } + }, + "zeroconf_confirm": { + "description": "Do you want to set up this Guardian device?" + } + }, + "abort": { + "already_configured": "This Guardian device has already been configured.", + "already_in_progress": "Guardian device configuration is already in process.", + "connection_error": "Failed to connect to the Guardian device." + } + } +} diff --git a/homeassistant/components/guardian/switch.py b/homeassistant/components/guardian/switch.py new file mode 100644 index 00000000000..9917482b5b6 --- /dev/null +++ b/homeassistant/components/guardian/switch.py @@ -0,0 +1,83 @@ +"""Switches for the Elexa Guardian integration.""" +from aioguardian.errors import GuardianError + +from homeassistant.components.switch import SwitchEntity +from homeassistant.core import callback + +from . import Guardian, GuardianEntity +from .const import DATA_CLIENT, DATA_VALVE_STATUS, DOMAIN, LOGGER, SWITCH_KIND_VALVE + +ATTR_AVG_CURRENT = "average_current" +ATTR_INST_CURRENT = "instantaneous_current" +ATTR_INST_CURRENT_DDT = "instantaneous_current_ddt" +ATTR_TRAVEL_COUNT = "travel_count" + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Guardian switches based on a config entry.""" + guardian = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] + async_add_entities([GuardianSwitch(guardian)], True) + + +class GuardianSwitch(GuardianEntity, SwitchEntity): + """Define a switch to open/close the Guardian valve.""" + + def __init__(self, guardian: Guardian): + """Initialize.""" + super().__init__(guardian, SWITCH_KIND_VALVE, "Valve", None, "mdi:water") + + self._is_on = True + + @property + def is_on(self): + """Return True if the valve is open.""" + return self._is_on + + @callback + def _update_from_latest_data(self): + """Update the entity.""" + self._is_on = self._guardian.data[DATA_VALVE_STATUS]["state"] in ( + "start_opening", + "opening", + "finish_opening", + "opened", + ) + + self._attrs.update( + { + ATTR_AVG_CURRENT: self._guardian.data[DATA_VALVE_STATUS][ + "average_current" + ], + ATTR_INST_CURRENT: self._guardian.data[DATA_VALVE_STATUS][ + "instantaneous_current" + ], + ATTR_INST_CURRENT_DDT: self._guardian.data[DATA_VALVE_STATUS][ + "instantaneous_current_ddt" + ], + ATTR_TRAVEL_COUNT: self._guardian.data[DATA_VALVE_STATUS][ + "travel_count" + ], + } + ) + + async def async_turn_off(self, **kwargs) -> None: + """Turn the valve off (closed).""" + try: + async with self._guardian.client: + await self._guardian.client.valve.valve_close() + except GuardianError as err: + LOGGER.error("Error while closing the valve: %s", err) + return + + self._is_on = False + + async def async_turn_on(self, **kwargs) -> None: + """Turn the valve on (open).""" + try: + async with self._guardian.client: + await self._guardian.client.valve.valve_open() + except GuardianError as err: + LOGGER.error("Error while opening the valve: %s", err) + return + + self._is_on = True diff --git a/homeassistant/components/guardian/translations/ca.json b/homeassistant/components/guardian/translations/ca.json new file mode 100644 index 00000000000..a94126753be --- /dev/null +++ b/homeassistant/components/guardian/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Aquest dispositiu Guardian ja est\u00e0 configurat.", + "already_in_progress": "La configuraci\u00f3 del dispositiu Guardian ja est\u00e0 en curs.", + "connection_error": "No s'ha pogut connectar amb el dispositiu Guardian." + }, + "step": { + "user": { + "data": { + "ip_address": "Adre\u00e7a IP", + "port": "Port" + }, + "description": "Configura un dispositiu Elexa Guardian local." + }, + "zeroconf_confirm": { + "description": "Vols configurar aquest dispositiu Guardian?" + } + } + }, + "title": "Elexa Guardian" +} \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/en.json b/homeassistant/components/guardian/translations/en.json new file mode 100644 index 00000000000..40a8a003c12 --- /dev/null +++ b/homeassistant/components/guardian/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "This Guardian device has already been configured.", + "already_in_progress": "Guardian device configuration is already in process.", + "connection_error": "Failed to connect to the Guardian device." + }, + "step": { + "user": { + "data": { + "ip_address": "IP Address", + "port": "Port" + }, + "description": "Configure a local Elexa Guardian device." + }, + "zeroconf_confirm": { + "description": "Do you want to set up this Guardian device?" + } + } + }, + "title": "Elexa Guardian" +} \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/es.json b/homeassistant/components/guardian/translations/es.json new file mode 100644 index 00000000000..ec2a724e944 --- /dev/null +++ b/homeassistant/components/guardian/translations/es.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Este dispositivo Guardian ya ha sido configurado", + "already_in_progress": "La configuraci\u00f3n del dispositivo Guardian ya est\u00e1 en proceso.", + "connection_error": "No se ha podido conectar con el dispositivo Guardian." + }, + "step": { + "user": { + "data": { + "ip_address": "Direcci\u00f3n IP", + "port": "Puerto" + }, + "description": "Configurar un dispositivo local Elexa Guardian." + }, + "zeroconf_confirm": { + "description": "\u00bfQuieres configurar este dispositivo Guardian?" + } + } + }, + "title": "Elexa Guardian" +} \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/it.json b/homeassistant/components/guardian/translations/it.json new file mode 100644 index 00000000000..3f1999be761 --- /dev/null +++ b/homeassistant/components/guardian/translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Questo dispositivo Guardian \u00e8 gi\u00e0 stato configurato", + "already_in_progress": "La configurazione del dispositivo Guardian \u00e8 gi\u00e0 in corso.", + "connection_error": "Impossibile connettersi al dispositivo Guardian." + }, + "step": { + "user": { + "data": { + "ip_address": "Indirizzo IP", + "port": "Porta" + }, + "description": "Configurare un dispositivo Elexa Guardian locale." + }, + "zeroconf_confirm": { + "description": "Vuoi configurare questo dispositivo Guardian?" + } + } + }, + "title": "Elexa Guardian" +} \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/ko.json b/homeassistant/components/guardian/translations/ko.json new file mode 100644 index 00000000000..4580fba76d1 --- /dev/null +++ b/homeassistant/components/guardian/translations/ko.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\uc774 Guardian \uae30\uae30\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "already_in_progress": "Guardian \uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4.", + "connection_error": "Guardian \uae30\uae30\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "step": { + "user": { + "data": { + "ip_address": "IP \uc8fc\uc18c", + "port": "\ud3ec\ud2b8" + }, + "description": "\ub85c\uceec Elexa Guardian \uae30\uae30\ub97c \uad6c\uc131\ud574\uc8fc\uc138\uc694." + }, + "zeroconf_confirm": { + "description": "\uc774 Guardian \uae30\uae30\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + } + } + }, + "title": "Elexa Guardian" +} \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/lb.json b/homeassistant/components/guardian/translations/lb.json new file mode 100644 index 00000000000..1de8bb9baea --- /dev/null +++ b/homeassistant/components/guardian/translations/lb.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "D\u00ebse Guardian Apparat ass scho konfigur\u00e9iert.", + "already_in_progress": "Guardian Apparat Konfiguratioun ass schonn am gaang.", + "connection_error": "Feeler beim verbannen mam Guardian Apparat." + }, + "step": { + "user": { + "data": { + "ip_address": "IP Adresse", + "port": "Port" + }, + "description": "Ee lokalen Elexa Guardian Apparat ariichten." + }, + "zeroconf_confirm": { + "description": "Soll d\u00ebsen Guardian Apparat konfigur\u00e9iert ginn?" + } + } + }, + "title": "Elexa Guardian" +} \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/nl.json b/homeassistant/components/guardian/translations/nl.json new file mode 100644 index 00000000000..a1cd1def87a --- /dev/null +++ b/homeassistant/components/guardian/translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Dit Guardian-apparaat is al geconfigureerd.", + "already_in_progress": "De configuratie van het Guardian-apparaat is al bezig.", + "connection_error": "Kan geen verbinding maken met het Guardian-apparaat." + }, + "step": { + "user": { + "data": { + "ip_address": "IP-adres", + "port": "Poort" + }, + "description": "Configureer een lokaal Elexa Guardian-apparaat." + }, + "zeroconf_confirm": { + "description": "Wilt u dit Guardian-apparaat instellen?" + } + } + }, + "title": "Elexa Guardian" +} \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/no.json b/homeassistant/components/guardian/translations/no.json new file mode 100644 index 00000000000..b398079cc36 --- /dev/null +++ b/homeassistant/components/guardian/translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Denne Guardian-enheten er allerede konfigurert.", + "already_in_progress": "Konfigurasjon av Guardian-enheter er allerede i gang.", + "connection_error": "Kan ikke koble til Guardian-enheten." + }, + "step": { + "user": { + "data": { + "ip_address": "IP adresse", + "port": "Port" + }, + "description": "Konfigurer en lokal Elexa Guardian-enhet." + }, + "zeroconf_confirm": { + "description": "Vil du konfigurere denne Guardian-enheten?" + } + } + }, + "title": "Elexa Guardian" +} \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/pl.json b/homeassistant/components/guardian/translations/pl.json new file mode 100644 index 00000000000..a49582f814e --- /dev/null +++ b/homeassistant/components/guardian/translations/pl.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z urz\u0105dzeniem Guardian, spr\u00f3buj ponownie." + }, + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/ru.json b/homeassistant/components/guardian/translations/ru.json new file mode 100644 index 00000000000..c9fe3b07ff7 --- /dev/null +++ b/homeassistant/components/guardian/translations/ru.json @@ -0,0 +1,22 @@ +{ + "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.", + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443." + }, + "step": { + "user": { + "data": { + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Elexa Guardian." + }, + "zeroconf_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u044d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Elexa Guardian?" + } + } + }, + "title": "Elexa Guardian" +} \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/zh-Hant.json b/homeassistant/components/guardian/translations/zh-Hant.json new file mode 100644 index 00000000000..d91c0c3ba8c --- /dev/null +++ b/homeassistant/components/guardian/translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Guardian \u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "Guardian \u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", + "connection_error": "Guardian \u8a2d\u5099\u9023\u7dda\u5931\u6557\u3002" + }, + "step": { + "user": { + "data": { + "ip_address": "IP \u4f4d\u5740", + "port": "\u901a\u8a0a\u57e0" + }, + "description": "\u8a2d\u5b9a\u5340\u57df Elexa Guardian \u8a2d\u5099\u3002" + }, + "zeroconf_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Guardian \u8a2d\u5099\uff1f" + } + } + }, + "title": "Elexa Guardian" +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/ca.json b/homeassistant/components/hangouts/translations/ca.json index a186723f345..7daa1b4f671 100644 --- a/homeassistant/components/hangouts/translations/ca.json +++ b/homeassistant/components/hangouts/translations/ca.json @@ -14,7 +14,7 @@ "data": { "2fa": "Pin 2FA" }, - "description": "buit", + "description": "Buit", "title": "Verificaci\u00f3 en dos passos" }, "user": { @@ -23,7 +23,7 @@ "email": "Correu electr\u00f2nic", "password": "Contrasenya" }, - "description": "buit", + "description": "Buit", "title": "Inici de sessi\u00f3 de Google Hangouts" } } diff --git a/homeassistant/components/hangouts/translations/fi.json b/homeassistant/components/hangouts/translations/fi.json index 68bc9eb0389..959a2c06a63 100644 --- a/homeassistant/components/hangouts/translations/fi.json +++ b/homeassistant/components/hangouts/translations/fi.json @@ -5,6 +5,9 @@ }, "step": { "2fa": { + "data": { + "2fa": "2FA-pin" + }, "title": "Kaksivaiheinen tunnistus" }, "user": { diff --git a/homeassistant/components/hangouts/translations/hu.json b/homeassistant/components/hangouts/translations/hu.json index ea7fd49a548..9a9f5b41598 100644 --- a/homeassistant/components/hangouts/translations/hu.json +++ b/homeassistant/components/hangouts/translations/hu.json @@ -19,7 +19,7 @@ }, "user": { "data": { - "email": "E-Mail C\u00edm", + "email": "E-mail", "password": "Jelsz\u00f3" }, "description": "\u00dcres", diff --git a/homeassistant/components/hangouts/translations/it.json b/homeassistant/components/hangouts/translations/it.json index 094280da4ef..29ddab24913 100644 --- a/homeassistant/components/hangouts/translations/it.json +++ b/homeassistant/components/hangouts/translations/it.json @@ -20,7 +20,7 @@ "user": { "data": { "authorization_code": "Codice di autorizzazione (necessario per l'autenticazione manuale)", - "email": "Indirizzo E-mail", + "email": "E-mail", "password": "Password" }, "description": "Vuoto", diff --git a/homeassistant/components/hangouts/translations/pl.json b/homeassistant/components/hangouts/translations/pl.json index f4a1d0a0fdd..69c3020bbfb 100644 --- a/homeassistant/components/hangouts/translations/pl.json +++ b/homeassistant/components/hangouts/translations/pl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Google Hangouts jest ju\u017c skonfigurowany.", - "unknown": "[%key_id:common::config_flow::error::unknown%]" + "unknown": "Nieoczekiwany b\u0142\u0105d." }, "error": { "invalid_2fa": "Nieprawid\u0142owe uwierzytelnienie dwusk\u0142adnikowe, spr\u00f3buj ponownie.", @@ -20,8 +20,8 @@ "user": { "data": { "authorization_code": "Kod autoryzacji (wymagany do r\u0119cznego uwierzytelnienia)", - "email": "[%key_id:common::config_flow::data::email%]", - "password": "[%key_id:common::config_flow::data::password%]" + "email": "Adres e-mail", + "password": "Has\u0142o" }, "description": "Pusty", "title": "Logowanie do Google Hangouts" diff --git a/homeassistant/components/harmony/translations/ca.json b/homeassistant/components/harmony/translations/ca.json index 90d8f064301..5bb279c0482 100644 --- a/homeassistant/components/harmony/translations/ca.json +++ b/homeassistant/components/harmony/translations/ca.json @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP", + "host": "Amfitri\u00f3", "name": "Nom del Hub" }, "title": "Configuraci\u00f3 de Logitech Harmony Hub" diff --git a/homeassistant/components/harmony/translations/hu.json b/homeassistant/components/harmony/translations/hu.json new file mode 100644 index 00000000000..cbf055e2fba --- /dev/null +++ b/homeassistant/components/harmony/translations/hu.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hoszt" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/it.json b/homeassistant/components/harmony/translations/it.json index 8095fa05156..c658e69e0c0 100644 --- a/homeassistant/components/harmony/translations/it.json +++ b/homeassistant/components/harmony/translations/it.json @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "Nome dell'host o indirizzo IP", + "host": "Host", "name": "Nome Hub" }, "title": "Configurare Logitech Harmony Hub" diff --git a/homeassistant/components/harmony/translations/ko.json b/homeassistant/components/harmony/translations/ko.json index 5751c22bbbe..528f5e9cc7e 100644 --- a/homeassistant/components/harmony/translations/ko.json +++ b/homeassistant/components/harmony/translations/ko.json @@ -7,7 +7,7 @@ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, - "flow_title": "Logitech Harmony Hub {name}", + "flow_title": "Logitech Harmony Hub: {name}", "step": { "link": { "description": "{name} ({host}) \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", diff --git a/homeassistant/components/harmony/translations/pl.json b/homeassistant/components/harmony/translations/pl.json index a1d7ecfeca8..12bbcfaca18 100644 --- a/homeassistant/components/harmony/translations/pl.json +++ b/homeassistant/components/harmony/translations/pl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "unknown": "[%key_id:common::config_flow::error::unknown%]" + "unknown": "Nieoczekiwany b\u0142\u0105d." }, "flow_title": "Logitech Harmony Hub {name}", "step": { @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "[%key_id:common::config_flow::data::host%]", + "host": "Nazwa hosta lub adres IP", "name": "Nazwa huba" }, "title": "Konfiguracja Logitech Harmony Hub" diff --git a/homeassistant/components/harmony/translations/pt-BR.json b/homeassistant/components/harmony/translations/pt-BR.json new file mode 100644 index 00000000000..7fe3f58cad6 --- /dev/null +++ b/homeassistant/components/harmony/translations/pt-BR.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar, tente novamente", + "unknown": "Erro inesperado" + }, + "flow_title": "Logitech Harmony Hub {name}", + "step": { + "link": { + "description": "Voc\u00ea quer configurar o {name} ({host})?", + "title": "Configura\u00e7\u00e3o do Logitech Harmony Hub" + }, + "user": { + "data": { + "name": "Nome do Hub" + }, + "title": "Configura\u00e7\u00e3o do Logitech Harmony Hub" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "activity": "A atividade padr\u00e3o a ser executada quando nenhuma for especificada.", + "delay_secs": "O atraso entre o envio de comandos." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 6a383e28ff1..0bd766589a1 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -17,6 +17,7 @@ from homeassistant.const import ( from homeassistant.core import DOMAIN as HASS_DOMAIN, callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import bind_hass from homeassistant.util.dt import utcnow @@ -124,6 +125,21 @@ MAP_SERVICE_API = { } +@bind_hass +async def async_get_addon_info(hass: HomeAssistantType, addon_id: str) -> dict: + """Return add-on info. + + The addon_id is a snakecased concatenation of the 'repository' value + found in the add-on info and the 'slug' value found in the add-on config.json. + In the add-on info the addon_id is called 'slug'. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + result = await hassio.get_addon_info(addon_id) + return result["data"] + + @callback @bind_hass def get_homeassistant_version(hass): @@ -188,7 +204,7 @@ async def async_setup(hass, config): hass.data[DOMAIN] = hassio = HassIO(hass.loop, websession, host) if not await hassio.is_connected(): - _LOGGER.warning("Not connected with Hass.io / system to busy!") + _LOGGER.warning("Not connected with Hass.io / system too busy!") store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) data = await store.async_load() diff --git a/homeassistant/components/heos/translations/bg.json b/homeassistant/components/heos/translations/bg.json index 4f52830af09..40156a8b3cd 100644 --- a/homeassistant/components/heos/translations/bg.json +++ b/homeassistant/components/heos/translations/bg.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "access_token": "\u0410\u0434\u0440\u0435\u0441", "host": "\u0410\u0434\u0440\u0435\u0441" }, "description": "\u041c\u043e\u043b\u044f, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0438\u043c\u0435\u0442\u043e \u043d\u0430 \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0430 Heos \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e (\u0437\u0430 \u043f\u0440\u0435\u0434\u043f\u043e\u0447\u0438\u0442\u0430\u043d\u0435 \u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0434\u0430 \u0435 \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u043e \u0441 \u043a\u0430\u0431\u0435\u043b \u043a\u044a\u043c \u043c\u0440\u0435\u0436\u0430\u0442\u0430).", diff --git a/homeassistant/components/heos/translations/ca.json b/homeassistant/components/heos/translations/ca.json index 02e8e22d920..d8bcf494615 100644 --- a/homeassistant/components/heos/translations/ca.json +++ b/homeassistant/components/heos/translations/ca.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "access_token": "Amfitri\u00f3", "host": "Amfitri\u00f3" }, "description": "Introdueix el nom de l'amfitri\u00f3 o l'adre\u00e7a IP d'un dispositiu Heos (preferiblement un connectat a la xarxa per cable).", diff --git a/homeassistant/components/heos/translations/da.json b/homeassistant/components/heos/translations/da.json index b395497d67a..3537ba30553 100644 --- a/homeassistant/components/heos/translations/da.json +++ b/homeassistant/components/heos/translations/da.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "access_token": "V\u00e6rt", "host": "V\u00e6rt" }, "description": "Indtast v\u00e6rtsnavnet eller IP-adressen p\u00e5 en Heos-enhed (helst en tilsluttet via ledning til netv\u00e6rket).", diff --git a/homeassistant/components/heos/translations/de.json b/homeassistant/components/heos/translations/de.json index bbd0d8beec7..4a886f0550b 100644 --- a/homeassistant/components/heos/translations/de.json +++ b/homeassistant/components/heos/translations/de.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "access_token": "Host", "host": "Host" }, "description": "Bitte gib den Hostnamen oder die IP-Adresse eines Heos-Ger\u00e4ts ein (vorzugsweise eines, das per Kabel mit dem Netzwerk verbunden ist).", diff --git a/homeassistant/components/heos/translations/en.json b/homeassistant/components/heos/translations/en.json index 3227e5115f9..be84c08fa5e 100644 --- a/homeassistant/components/heos/translations/en.json +++ b/homeassistant/components/heos/translations/en.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "access_token": "Host", "host": "Host" }, "description": "Please enter the host name or IP address of a Heos device (preferably one connected via wire to the network).", diff --git a/homeassistant/components/heos/translations/es-419.json b/homeassistant/components/heos/translations/es-419.json index 902f65bf5e1..6c45936d8be 100644 --- a/homeassistant/components/heos/translations/es-419.json +++ b/homeassistant/components/heos/translations/es-419.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "access_token": "Host", "host": "Host" }, "description": "Ingrese el nombre de host o la direcci\u00f3n IP de un dispositivo Heos (preferiblemente uno conectado por cable a la red).", diff --git a/homeassistant/components/heos/translations/es.json b/homeassistant/components/heos/translations/es.json index b79871d487c..ecde21aaf6b 100644 --- a/homeassistant/components/heos/translations/es.json +++ b/homeassistant/components/heos/translations/es.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "access_token": "Host", "host": "Host" }, "description": "Introduce el nombre de host o direcci\u00f3n IP de un dispositivo Heos (preferiblemente conectado por cable a la red).", diff --git a/homeassistant/components/heos/translations/fi.json b/homeassistant/components/heos/translations/fi.json index 73e50dbf8ce..dbed4d27dd2 100644 --- a/homeassistant/components/heos/translations/fi.json +++ b/homeassistant/components/heos/translations/fi.json @@ -2,9 +2,6 @@ "config": { "step": { "user": { - "data": { - "access_token": "Palvelin" - }, "title": "Yhdist\u00e4 Heosiin" } } diff --git a/homeassistant/components/heos/translations/fr.json b/homeassistant/components/heos/translations/fr.json index 7f76c932c71..39cb39e33f5 100644 --- a/homeassistant/components/heos/translations/fr.json +++ b/homeassistant/components/heos/translations/fr.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "access_token": "H\u00f4te", "host": "H\u00f4te" }, "description": "Veuillez saisir le nom d\u2019h\u00f4te ou l\u2019adresse IP d\u2019un p\u00e9riph\u00e9rique Heos (de pr\u00e9f\u00e9rence connect\u00e9 au r\u00e9seau filaire).", diff --git a/homeassistant/components/heos/translations/hu.json b/homeassistant/components/heos/translations/hu.json index 8cd10b3c246..cbf055e2fba 100644 --- a/homeassistant/components/heos/translations/hu.json +++ b/homeassistant/components/heos/translations/hu.json @@ -3,8 +3,7 @@ "step": { "user": { "data": { - "access_token": "Kiszolg\u00e1l\u00f3", - "host": "Kiszolg\u00e1l\u00f3" + "host": "Hoszt" } } } diff --git a/homeassistant/components/heos/translations/it.json b/homeassistant/components/heos/translations/it.json index c40bbb95554..81350edf9ad 100644 --- a/homeassistant/components/heos/translations/it.json +++ b/homeassistant/components/heos/translations/it.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "access_token": "Host", "host": "Host" }, "description": "Inserire il nome host o l'indirizzo IP di un dispositivo Heos (preferibilmente uno connesso alla rete tramite cavo).", diff --git a/homeassistant/components/heos/translations/ko.json b/homeassistant/components/heos/translations/ko.json index 1e7902adfb3..acf26df1cec 100644 --- a/homeassistant/components/heos/translations/ko.json +++ b/homeassistant/components/heos/translations/ko.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "access_token": "\ud638\uc2a4\ud2b8", "host": "\ud638\uc2a4\ud2b8" }, "description": "Heos \uae30\uae30\uc758 \ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. (\uc720\uc120 \ub124\ud2b8\uc6cc\ud06c\ub85c \uc5f0\uacb0\ud558\ub294 \uac83\uc774 \uc88b\uc2b5\ub2c8\ub2e4)", diff --git a/homeassistant/components/heos/translations/lb.json b/homeassistant/components/heos/translations/lb.json index de124207d64..e7997f68b8c 100644 --- a/homeassistant/components/heos/translations/lb.json +++ b/homeassistant/components/heos/translations/lb.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "access_token": "Apparat", "host": "Apparat" }, "description": "Gitt den Numm oder IP-Adress vun engem Heos-Apparat an (am beschten iwwer Kabel mam Reseau verbonnen).", diff --git a/homeassistant/components/heos/translations/nl.json b/homeassistant/components/heos/translations/nl.json index f3e9dfed7e3..2b85788f7af 100644 --- a/homeassistant/components/heos/translations/nl.json +++ b/homeassistant/components/heos/translations/nl.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "access_token": "Host", "host": "Host" }, "description": "Voer de hostnaam of het IP-adres van een Heos-apparaat in (bij voorkeur een die via een kabel is verbonden met het netwerk).", diff --git a/homeassistant/components/heos/translations/no.json b/homeassistant/components/heos/translations/no.json index 706fbc31cb4..7a8a9ccb7f1 100644 --- a/homeassistant/components/heos/translations/no.json +++ b/homeassistant/components/heos/translations/no.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "access_token": "Vert", "host": "Vert" }, "description": "Vennligst fyll inn vertsnavnet eller IP-adressen til en Heos-enhet (helst en tilkoblet nettverket via kabel).", diff --git a/homeassistant/components/heos/translations/pl.json b/homeassistant/components/heos/translations/pl.json index a421e92dd8a..5394d57bb74 100644 --- a/homeassistant/components/heos/translations/pl.json +++ b/homeassistant/components/heos/translations/pl.json @@ -9,8 +9,7 @@ "step": { "user": { "data": { - "access_token": "[%key_id:common::config_flow::data::host%]", - "host": "[%key_id:common::config_flow::data::host%]" + "host": "Nazwa hosta lub adres IP" }, "description": "Wprowad\u017a nazw\u0119 hosta lub adres IP urz\u0105dzenia Heos (najlepiej pod\u0142\u0105czonego przewodowo do sieci).", "title": "Po\u0142\u0105czenie z Heos" diff --git a/homeassistant/components/heos/translations/pt-BR.json b/homeassistant/components/heos/translations/pt-BR.json index abacf5c8ca1..55d7b76d96e 100644 --- a/homeassistant/components/heos/translations/pt-BR.json +++ b/homeassistant/components/heos/translations/pt-BR.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "access_token": "Host", "host": "Host" }, "description": "Por favor, digite o nome do host ou o endere\u00e7o IP de um dispositivo Heos (de prefer\u00eancia para conex\u00f5es conectadas por cabo \u00e0 sua rede).", diff --git a/homeassistant/components/heos/translations/pt.json b/homeassistant/components/heos/translations/pt.json index d0c219cefa9..ce7cbc3f548 100644 --- a/homeassistant/components/heos/translations/pt.json +++ b/homeassistant/components/heos/translations/pt.json @@ -3,7 +3,6 @@ "step": { "user": { "data": { - "access_token": "Servidor", "host": "Servidor" } } diff --git a/homeassistant/components/heos/translations/ru.json b/homeassistant/components/heos/translations/ru.json index 9983242b349..b92ea253ebe 100644 --- a/homeassistant/components/heos/translations/ru.json +++ b/homeassistant/components/heos/translations/ru.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "access_token": "\u0425\u043e\u0441\u0442", "host": "\u0425\u043e\u0441\u0442" }, "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 HEOS (\u043f\u0440\u0435\u0434\u043f\u043e\u0447\u0442\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0441\u0435\u0442\u0438 \u0447\u0435\u0440\u0435\u0437 \u043a\u0430\u0431\u0435\u043b\u044c).", diff --git a/homeassistant/components/heos/translations/sl.json b/homeassistant/components/heos/translations/sl.json index 76fe3aadc5d..abe3d6dc97a 100644 --- a/homeassistant/components/heos/translations/sl.json +++ b/homeassistant/components/heos/translations/sl.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "access_token": "Gostitelj", "host": "Gostitelj" }, "description": "Vnesite ime gostitelja ali naslov IP naprave Heos (po mo\u017enosti eno, ki je z omre\u017ejem povezana \u017ei\u010dno).", diff --git a/homeassistant/components/heos/translations/sv.json b/homeassistant/components/heos/translations/sv.json index 8215388a161..4ade6944e51 100644 --- a/homeassistant/components/heos/translations/sv.json +++ b/homeassistant/components/heos/translations/sv.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "access_token": "V\u00e4rd", "host": "V\u00e4rd" }, "description": "Ange v\u00e4rdnamnet eller IP-adressen f\u00f6r en Heos-enhet (helst en ansluten via kabel till n\u00e4tverket).", diff --git a/homeassistant/components/heos/translations/zh-Hant.json b/homeassistant/components/heos/translations/zh-Hant.json index 01ca002dd54..937a53a1e9f 100644 --- a/homeassistant/components/heos/translations/zh-Hant.json +++ b/homeassistant/components/heos/translations/zh-Hant.json @@ -9,7 +9,6 @@ "step": { "user": { "data": { - "access_token": "\u4e3b\u6a5f\u7aef", "host": "\u4e3b\u6a5f\u7aef" }, "description": "\u8acb\u8f38\u5165\u4e3b\u6a5f\u6bb5\u540d\u7a31\u6216 Heos \u8a2d\u5099 IP \u4f4d\u5740\uff08\u5df2\u900f\u904e\u6709\u7dda\u7db2\u8def\u9023\u7dda\uff09\u3002", diff --git a/homeassistant/components/hisense_aehw4a1/translations/bg.json b/homeassistant/components/hisense_aehw4a1/translations/bg.json index 607347ff9e9..ed54398ecab 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/bg.json +++ b/homeassistant/components/hisense_aehw4a1/translations/bg.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Hisense AEH-W4A1?", - "title": "Hisense AEH-W4A1" + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Hisense AEH-W4A1?" } } } diff --git a/homeassistant/components/hisense_aehw4a1/translations/ca.json b/homeassistant/components/hisense_aehw4a1/translations/ca.json index a0aef80e031..3a05c1d6123 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/ca.json +++ b/homeassistant/components/hisense_aehw4a1/translations/ca.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Vols configurar AEH-W4A1 de Hisense?", - "title": "Hisense AEH-W4A1" + "description": "Vols configurar AEH-W4A1 de Hisense?" } } } diff --git a/homeassistant/components/hisense_aehw4a1/translations/da.json b/homeassistant/components/hisense_aehw4a1/translations/da.json index d75ffed4c56..90184302eec 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/da.json +++ b/homeassistant/components/hisense_aehw4a1/translations/da.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Vil du konfigurere Hisense AEH-W4A1?", - "title": "Hisense AEH-W4A1" + "description": "Vil du konfigurere Hisense AEH-W4A1?" } } } diff --git a/homeassistant/components/hisense_aehw4a1/translations/de.json b/homeassistant/components/hisense_aehw4a1/translations/de.json index e42d91082e8..d5f4f429740 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/de.json +++ b/homeassistant/components/hisense_aehw4a1/translations/de.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "M\u00f6chtest du Hisense AEH-W4A1 einrichten?", - "title": "Hisense AEH-W4A1" + "description": "M\u00f6chtest du Hisense AEH-W4A1 einrichten?" } } } diff --git a/homeassistant/components/hisense_aehw4a1/translations/en.json b/homeassistant/components/hisense_aehw4a1/translations/en.json index ca0738ec9a8..4b292600249 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/en.json +++ b/homeassistant/components/hisense_aehw4a1/translations/en.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Do you want to set up Hisense AEH-W4A1?", - "title": "Hisense AEH-W4A1" + "description": "Do you want to set up Hisense AEH-W4A1?" } } } diff --git a/homeassistant/components/hisense_aehw4a1/translations/es-419.json b/homeassistant/components/hisense_aehw4a1/translations/es-419.json index c9c4270360a..1a3a8ca221d 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/es-419.json +++ b/homeassistant/components/hisense_aehw4a1/translations/es-419.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u00bfDesea configurar Hisense AEH-W4A1?", - "title": "Hisense AEH-W4A1" + "description": "\u00bfDesea configurar Hisense AEH-W4A1?" } } } diff --git a/homeassistant/components/hisense_aehw4a1/translations/es.json b/homeassistant/components/hisense_aehw4a1/translations/es.json index c9c4270360a..1a3a8ca221d 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/es.json +++ b/homeassistant/components/hisense_aehw4a1/translations/es.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u00bfDesea configurar Hisense AEH-W4A1?", - "title": "Hisense AEH-W4A1" + "description": "\u00bfDesea configurar Hisense AEH-W4A1?" } } } diff --git a/homeassistant/components/hisense_aehw4a1/translations/fr.json b/homeassistant/components/hisense_aehw4a1/translations/fr.json index dafe3836a50..7fa1598fa76 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/fr.json +++ b/homeassistant/components/hisense_aehw4a1/translations/fr.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Voulez-vous configurer AEH-W4A1?", - "title": "Hisense AEH-W4A1" + "description": "Voulez-vous configurer AEH-W4A1?" } } } diff --git a/homeassistant/components/hisense_aehw4a1/translations/hu.json b/homeassistant/components/hisense_aehw4a1/translations/hu.json index 389653422fd..0b21d7c4c32 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/hu.json +++ b/homeassistant/components/hisense_aehw4a1/translations/hu.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Szeretn\u00e9 be\u00e1ll\u00edtani Hisense AEH-W4A1-et?", - "title": "Hisense AEH-W4A1" + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani Hisense AEH-W4A1-et?" } } } diff --git a/homeassistant/components/hisense_aehw4a1/translations/it.json b/homeassistant/components/hisense_aehw4a1/translations/it.json index 3d878ed40be..0da43b27a82 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/it.json +++ b/homeassistant/components/hisense_aehw4a1/translations/it.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Voui configurare Hisense AEH-W4A1", - "title": "Hisense AEH-W4A1" + "description": "Voui configurare Hisense AEH-W4A1" } } } diff --git a/homeassistant/components/hisense_aehw4a1/translations/ko.json b/homeassistant/components/hisense_aehw4a1/translations/ko.json index 2c472277b00..27d0ff88f6a 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/ko.json +++ b/homeassistant/components/hisense_aehw4a1/translations/ko.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Hisense AEH-W4A1 \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "Hisense AEH-W4A1" + "description": "Hisense AEH-W4A1 \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" } } } diff --git a/homeassistant/components/hisense_aehw4a1/translations/lb.json b/homeassistant/components/hisense_aehw4a1/translations/lb.json index cdfbb069c0f..d97f9fc2893 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/lb.json +++ b/homeassistant/components/hisense_aehw4a1/translations/lb.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Soll Hisense AEH-W4A1 konfigur\u00e9iert ginn?", - "title": "Hisense AEH-W4A1" + "description": "Soll Hisense AEH-W4A1 konfigur\u00e9iert ginn?" } } } diff --git a/homeassistant/components/hisense_aehw4a1/translations/nl.json b/homeassistant/components/hisense_aehw4a1/translations/nl.json index 9fef289f545..14f2445f63e 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/nl.json +++ b/homeassistant/components/hisense_aehw4a1/translations/nl.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Wilt u Hisense AEH-W4A1 instellen?", - "title": "Hisense AEH-W4A1" + "description": "Wilt u Hisense AEH-W4A1 instellen?" } } } diff --git a/homeassistant/components/hisense_aehw4a1/translations/no.json b/homeassistant/components/hisense_aehw4a1/translations/no.json index bc048ef2286..6a8d6cbe443 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/no.json +++ b/homeassistant/components/hisense_aehw4a1/translations/no.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Vil du sette opp Hisense AEH-W4A1?", - "title": "" + "description": "Vil du sette opp Hisense AEH-W4A1?" } } } diff --git a/homeassistant/components/hisense_aehw4a1/translations/pl.json b/homeassistant/components/hisense_aehw4a1/translations/pl.json index 77e5c6298eb..61f15a99806 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/pl.json +++ b/homeassistant/components/hisense_aehw4a1/translations/pl.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Chcesz skonfigurowa\u0107 AEH-W4A1?", - "title": "Hisense AEH-W4A1" + "description": "Chcesz skonfigurowa\u0107 AEH-W4A1?" } } } diff --git a/homeassistant/components/hisense_aehw4a1/translations/ru.json b/homeassistant/components/hisense_aehw4a1/translations/ru.json index bb406e90f92..0d982c13eb0 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/ru.json +++ b/homeassistant/components/hisense_aehw4a1/translations/ru.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Hisense AEH-W4A1?", - "title": "Hisense AEH-W4A1" + "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 Hisense AEH-W4A1?" } } } diff --git a/homeassistant/components/hisense_aehw4a1/translations/sl.json b/homeassistant/components/hisense_aehw4a1/translations/sl.json index d24c8398652..daeabc2b84c 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/sl.json +++ b/homeassistant/components/hisense_aehw4a1/translations/sl.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Ali \u017eelite nastaviti Hisense AEH-W4A1?", - "title": "Hisense AEH-W4A1" + "description": "Ali \u017eelite nastaviti Hisense AEH-W4A1?" } } } diff --git a/homeassistant/components/hisense_aehw4a1/translations/sv.json b/homeassistant/components/hisense_aehw4a1/translations/sv.json index 01d484075e0..84798e81fa9 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/sv.json +++ b/homeassistant/components/hisense_aehw4a1/translations/sv.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Vill du konfigurera Hisense AEH-W4A1?", - "title": "Hisense AEH-W4A1" + "description": "Vill du konfigurera Hisense AEH-W4A1?" } } } diff --git a/homeassistant/components/hisense_aehw4a1/translations/zh-Hant.json b/homeassistant/components/hisense_aehw4a1/translations/zh-Hant.json index 44feda4fffc..296fbb5d9d5 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/zh-Hant.json +++ b/homeassistant/components/hisense_aehw4a1/translations/zh-Hant.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u6d77\u4fe1 AEH-W4A1\uff1f", - "title": "\u6d77\u4fe1 AEH-W4A1" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u6d77\u4fe1 AEH-W4A1\uff1f" } } } diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 6fc68b2833e..4933b00ffde 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -4,13 +4,15 @@ from datetime import timedelta from itertools import groupby import logging import time +from typing import Optional, cast +from aiohttp import web from sqlalchemy import and_, func import voluptuous as vol from homeassistant.components import recorder from homeassistant.components.http import HomeAssistantView -from homeassistant.components.recorder.models import States +from homeassistant.components.recorder.models import DB_TIMEZONE, States from homeassistant.components.recorder.util import execute, session_scope from homeassistant.const import ( ATTR_HIDDEN, @@ -20,6 +22,7 @@ from homeassistant.const import ( CONF_INCLUDE, HTTP_BAD_REQUEST, ) +from homeassistant.core import split_entity_id import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util @@ -30,6 +33,9 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "history" CONF_ORDER = "use_include_order" +STATE_KEY = "state" +LAST_CHANGED_KEY = "last_changed" + CONFIG_SCHEMA = vol.Schema( { DOMAIN: recorder.FILTER_SCHEMA.extend( @@ -41,16 +47,27 @@ CONFIG_SCHEMA = vol.Schema( SIGNIFICANT_DOMAINS = ("climate", "device_tracker", "thermostat", "water_heater") IGNORE_DOMAINS = ("zone", "scene") +NEED_ATTRIBUTE_DOMAINS = {"climate", "water_heater", "thermostat", "script"} +SCRIPT_DOMAIN = "script" +ATTR_CAN_CANCEL = "can_cancel" -def get_significant_states( +def get_significant_states(hass, *args, **kwargs): + """Wrap _get_significant_states with a sql session.""" + with session_scope(hass=hass) as session: + return _get_significant_states(hass, session, *args, **kwargs) + + +def _get_significant_states( hass, + session, start_time, end_time=None, entity_ids=None, filters=None, include_start_time_state=True, significant_changes_only=True, + minimal_response=False, ): """ Return states changes during UTC period start_time - end_time. @@ -61,38 +78,40 @@ def get_significant_states( """ timer_start = time.perf_counter() - with session_scope(hass=hass) as session: - if significant_changes_only: - query = session.query(States).filter( - ( - States.domain.in_(SIGNIFICANT_DOMAINS) - | (States.last_changed == States.last_updated) - ) - & (States.last_updated > start_time) + if significant_changes_only: + query = session.query(States).filter( + ( + States.domain.in_(SIGNIFICANT_DOMAINS) + | (States.last_changed == States.last_updated) ) - else: - query = session.query(States).filter(States.last_updated > start_time) - - if filters: - query = filters.apply(query, entity_ids) - - if end_time is not None: - query = query.filter(States.last_updated < end_time) - - query = query.order_by(States.last_updated) - - states = ( - state - for state in execute(query) - if (_is_significant(state) and not state.attributes.get(ATTR_HIDDEN, False)) + & (States.last_updated > start_time) ) + else: + query = session.query(States).filter(States.last_updated > start_time) + + if filters: + query = filters.apply(query, entity_ids) + + if end_time is not None: + query = query.filter(States.last_updated < end_time) + + query = query.order_by(States.entity_id, States.last_updated) + + states = execute(query, to_native=False) if _LOGGER.isEnabledFor(logging.DEBUG): elapsed = time.perf_counter() - timer_start _LOGGER.debug("get_significant_states took %fs", elapsed) - return states_to_json( - hass, states, start_time, entity_ids, filters, include_start_time_state + return _sorted_states_to_json( + hass, + session, + states, + start_time, + entity_ids, + filters, + include_start_time_state, + minimal_response, ) @@ -113,9 +132,11 @@ def state_changes_during_period(hass, start_time, end_time=None, entity_id=None) entity_ids = [entity_id] if entity_id is not None else None - states = execute(query.order_by(States.last_updated)) + states = execute( + query.order_by(States.entity_id, States.last_updated), to_native=False + ) - return states_to_json(hass, states, start_time, entity_ids) + return _sorted_states_to_json(hass, session, states, start_time, entity_ids) def get_last_state_changes(hass, number_of_states, entity_id): @@ -132,100 +153,132 @@ def get_last_state_changes(hass, number_of_states, entity_id): entity_ids = [entity_id] if entity_id is not None else None states = execute( - query.order_by(States.last_updated.desc()).limit(number_of_states) + query.order_by(States.entity_id, States.last_updated.desc()).limit( + number_of_states + ), + to_native=False, ) - return states_to_json( - hass, reversed(states), start_time, entity_ids, include_start_time_state=False - ) + return _sorted_states_to_json( + hass, + session, + reversed(states), + start_time, + entity_ids, + include_start_time_state=False, + ) def get_states(hass, utc_point_in_time, entity_ids=None, run=None, filters=None): """Return the states at a specific point in time.""" if run is None: - run = recorder.run_information(hass, utc_point_in_time) + run = recorder.run_information_from_instance(hass, utc_point_in_time) # History did not run before utc_point_in_time if run is None: return [] with session_scope(hass=hass) as session: - query = session.query(States) + return _get_states_with_session( + session, utc_point_in_time, entity_ids, run, filters + ) - if entity_ids and len(entity_ids) == 1: - # Use an entirely different (and extremely fast) query if we only - # have a single entity id - query = ( - query.filter( - States.last_updated >= run.start, - States.last_updated < utc_point_in_time, - States.entity_id.in_(entity_ids), - ) - .order_by(States.last_updated.desc()) - .limit(1) + +def _get_states_with_session( + session, utc_point_in_time, entity_ids=None, run=None, filters=None +): + """Return the states at a specific point in time.""" + if run is None: + run = recorder.run_information_with_session(session, utc_point_in_time) + + # History did not run before utc_point_in_time + if run is None: + return [] + + query = session.query(States) + + if entity_ids and len(entity_ids) == 1: + # Use an entirely different (and extremely fast) query if we only + # have a single entity id + query = ( + query.filter( + States.last_updated >= run.start, + States.last_updated < utc_point_in_time, + States.entity_id.in_(entity_ids), ) + .order_by(States.last_updated.desc()) + .limit(1) + ) - else: - # We have more than one entity to look at (most commonly we want - # all entities,) so we need to do a search on all states since the - # last recorder run started. + else: + # We have more than one entity to look at (most commonly we want + # all entities,) so we need to do a search on all states since the + # last recorder run started. - most_recent_states_by_date = session.query( - States.entity_id.label("max_entity_id"), - func.max(States.last_updated).label("max_last_updated"), - ).filter( - (States.last_updated >= run.start) - & (States.last_updated < utc_point_in_time) - ) + most_recent_states_by_date = session.query( + States.entity_id.label("max_entity_id"), + func.max(States.last_updated).label("max_last_updated"), + ).filter( + (States.last_updated >= run.start) + & (States.last_updated < utc_point_in_time) + ) - if entity_ids: - most_recent_states_by_date.filter(States.entity_id.in_(entity_ids)) + if entity_ids: + most_recent_states_by_date.filter(States.entity_id.in_(entity_ids)) - most_recent_states_by_date = most_recent_states_by_date.group_by( - States.entity_id - ) + most_recent_states_by_date = most_recent_states_by_date.group_by( + States.entity_id + ) - most_recent_states_by_date = most_recent_states_by_date.subquery() + most_recent_states_by_date = most_recent_states_by_date.subquery() - most_recent_state_ids = session.query( - func.max(States.state_id).label("max_state_id") - ).join( - most_recent_states_by_date, - and_( - States.entity_id == most_recent_states_by_date.c.max_entity_id, - States.last_updated - == most_recent_states_by_date.c.max_last_updated, - ), - ) + most_recent_state_ids = session.query( + func.max(States.state_id).label("max_state_id") + ).join( + most_recent_states_by_date, + and_( + States.entity_id == most_recent_states_by_date.c.max_entity_id, + States.last_updated == most_recent_states_by_date.c.max_last_updated, + ), + ) - most_recent_state_ids = most_recent_state_ids.group_by(States.entity_id) + most_recent_state_ids = most_recent_state_ids.group_by(States.entity_id) - most_recent_state_ids = most_recent_state_ids.subquery() + most_recent_state_ids = most_recent_state_ids.subquery() - query = query.join( - most_recent_state_ids, - States.state_id == most_recent_state_ids.c.max_state_id, - ).filter(~States.domain.in_(IGNORE_DOMAINS)) + query = query.join( + most_recent_state_ids, + States.state_id == most_recent_state_ids.c.max_state_id, + ).filter(~States.domain.in_(IGNORE_DOMAINS)) - if filters: - query = filters.apply(query, entity_ids) + if filters: + query = filters.apply(query, entity_ids) - return [ - state - for state in execute(query) - if not state.attributes.get(ATTR_HIDDEN, False) - ] + return [ + state + for state in execute(query) + if not state.attributes.get(ATTR_HIDDEN, False) + ] -def states_to_json( - hass, states, start_time, entity_ids, filters=None, include_start_time_state=True +def _sorted_states_to_json( + hass, + session, + states, + start_time, + entity_ids, + filters=None, + include_start_time_state=True, + minimal_response=False, ): """Convert SQL results into JSON friendly data structure. This takes our state list and turns it into a JSON friendly data structure {'entity_id': [list of states], 'entity_id2': [list of states]} + States must be sorted by entity_id and last_updated + We also need to go back and create a synthetic zero data point for each list of states, otherwise our graphs won't start on the Y axis correctly. @@ -239,7 +292,10 @@ def states_to_json( # Get the states at the start time timer_start = time.perf_counter() if include_start_time_state: - for state in get_states(hass, start_time, entity_ids, filters=filters): + run = recorder.run_information_from_instance(hass, start_time) + for state in _get_states_with_session( + session, start_time, entity_ids, run=run, filters=filters + ): state.last_changed = start_time state.last_updated = start_time result[state.entity_id].append(state) @@ -250,7 +306,61 @@ def states_to_json( # Append all changes to it for ent_id, group in groupby(states, lambda state: state.entity_id): - result[ent_id].extend(group) + domain = split_entity_id(ent_id)[0] + ent_results = result[ent_id] + if not minimal_response or domain in NEED_ATTRIBUTE_DOMAINS: + ent_results.extend( + [ + native_state + for native_state in (db_state.to_native() for db_state in group) + if ( + domain != SCRIPT_DOMAIN + or native_state.attributes.get(ATTR_CAN_CANCEL) + ) + and not native_state.attributes.get(ATTR_HIDDEN, False) + ] + ) + continue + + # With minimal response we only provide a native + # State for the first and last response. All the states + # in-between only provide the "state" and the + # "last_changed". + if not ent_results: + ent_results.append(next(group).to_native()) + + initial_state = ent_results[-1] + prev_state = ent_results[-1] + initial_state_count = len(ent_results) + + for db_state in group: + if ATTR_HIDDEN in db_state.attributes and db_state.to_native().attributes.get( + ATTR_HIDDEN, False + ): + continue + + # With minimal response we do not care about attribute + # changes so we can filter out duplicate states + if db_state.state == prev_state.state: + continue + + ent_results.append( + { + STATE_KEY: db_state.state, + LAST_CHANGED_KEY: f"{str(db_state.last_changed).replace(' ','T').split('.')[0]}{DB_TIMEZONE}", + } + ) + prev_state = db_state + + if ( + prev_state + and prev_state != initial_state + and len(ent_results) != initial_state_count + ): + # There was at least one state change + # replace the last minimal state with + # a full state + ent_results[-1] = prev_state.to_native() # Filter out the empty lists if some states had 0 results. return {key: val for key, val in result.items() if val} @@ -296,20 +406,22 @@ class HistoryPeriodView(HomeAssistantView): self.filters = filters self.use_include_order = use_include_order - async def get(self, request, datetime=None): + async def get( + self, request: web.Request, datetime: Optional[str] = None + ) -> web.Response: """Return history over a period of time.""" - timer_start = time.perf_counter() - if datetime: - datetime = dt_util.parse_datetime(datetime) - if datetime is None: + if datetime: + datetime_ = dt_util.parse_datetime(datetime) + + if datetime_ is None: return self.json_message("Invalid datetime", HTTP_BAD_REQUEST) now = dt_util.utcnow() one_day = timedelta(days=1) - if datetime: - start_time = dt_util.as_utc(datetime) + if datetime_: + start_time = dt_util.as_utc(datetime_) else: start_time = now - one_day @@ -333,18 +445,50 @@ class HistoryPeriodView(HomeAssistantView): request.query.get("significant_changes_only", "1") != "0" ) + minimal_response = "minimal_response" in request.query + hass = request.app["hass"] - result = await hass.async_add_job( - get_significant_states, - hass, - start_time, - end_time, - entity_ids, - self.filters, - include_start_time_state, - significant_changes_only, + return cast( + web.Response, + await hass.async_add_executor_job( + self._sorted_significant_states_json, + hass, + start_time, + end_time, + entity_ids, + include_start_time_state, + significant_changes_only, + minimal_response, + ), ) + + def _sorted_significant_states_json( + self, + hass, + start_time, + end_time, + entity_ids, + include_start_time_state, + significant_changes_only, + minimal_response, + ): + """Fetch significant stats from the database as json.""" + timer_start = time.perf_counter() + + with session_scope(hass=hass) as session: + result = _get_significant_states( + hass, + session, + start_time, + end_time, + entity_ids, + self.filters, + include_start_time_state, + significant_changes_only, + minimal_response, + ) + result = list(result.values()) if _LOGGER.isEnabledFor(logging.DEBUG): elapsed = time.perf_counter() - timer_start @@ -363,7 +507,7 @@ class HistoryPeriodView(HomeAssistantView): sorted_result.extend(result) result = sorted_result - return await hass.async_add_job(self.json, result) + return self.json(result) class Filters: @@ -428,12 +572,3 @@ class Filters: if self.excluded_entities: query = query.filter(~States.entity_id.in_(self.excluded_entities)) return query - - -def _is_significant(state): - """Test if state is significant for history charts. - - Will only test for things that are not filtered out in SQL. - """ - # scripts that are not cancellable will never change state - return state.domain != "script" or state.attributes.get("can_cancel") diff --git a/homeassistant/components/hitron_coda/device_tracker.py b/homeassistant/components/hitron_coda/device_tracker.py index a49e3cc6d21..ace6540fe71 100644 --- a/homeassistant/components/hitron_coda/device_tracker.py +++ b/homeassistant/components/hitron_coda/device_tracker.py @@ -34,7 +34,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_scanner(_hass, config): - """Validate the configuration and return a Nmap scanner.""" + """Validate the configuration and return a Hitron CODA-4582U scanner.""" scanner = HitronCODADeviceScanner(config[DOMAIN]) return scanner if scanner.success_init else None diff --git a/homeassistant/components/home_connect/translations/pl.json b/homeassistant/components/home_connect/translations/pl.json index 08e4860453a..054fe071e74 100644 --- a/homeassistant/components/home_connect/translations/pl.json +++ b/homeassistant/components/home_connect/translations/pl.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "missing_configuration": "[%key_id:common::config_flow::abort::oauth2_missing_configuration%]" + "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105." }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono z Home Connect." }, "step": { "pick_implementation": { - "title": "[%key_id:common::config_flow::title::oauth2_pick_implementation%]" + "title": "Wybierz metod\u0119 uwierzytelniania" } } } diff --git a/homeassistant/components/home_connect/translations/pt-BR.json b/homeassistant/components/home_connect/translations/pt-BR.json new file mode 100644 index 00000000000..ff8e13aed1f --- /dev/null +++ b/homeassistant/components/home_connect/translations/pt-BR.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "missing_configuration": "O componente Home Connect n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index d14ef438a66..7e4a0433344 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -75,6 +75,7 @@ def _ensure_no_intersection(value): CONF_SCENE_ID = "scene_id" CONF_SNAPSHOT = "snapshot_entities" DATA_PLATFORM = "homeassistant_scene" +EVENT_SCENE_RELOADED = "scene_reloaded" STATES_SCHEMA = vol.All(dict, _convert_states) @@ -182,6 +183,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= _process_scenes_config(hass, async_add_entities, p_config) + hass.bus.async_fire(EVENT_SCENE_RELOADED, context=call.context) + hass.helpers.service.async_register_admin_service( SCENE_DOMAIN, SERVICE_RELOAD, reload_config ) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 428f8e30abf..a315ddd41e9 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -6,11 +6,16 @@ import os from aiohttp import web import voluptuous as vol -from zeroconf import InterfaceChoice from homeassistant.components import zeroconf -from homeassistant.components.binary_sensor import DEVICE_CLASS_BATTERY_CHARGING +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_BATTERY_CHARGING, + DEVICE_CLASS_MOTION, + DOMAIN as BINARY_SENSOR_DOMAIN, +) +from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN from homeassistant.components.http import HomeAssistantView +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_BATTERY_CHARGING, @@ -58,13 +63,13 @@ from .const import ( CONF_FILTER, CONF_LINKED_BATTERY_CHARGING_SENSOR, CONF_LINKED_BATTERY_SENSOR, + CONF_LINKED_MOTION_SENSOR, CONF_SAFE_MODE, CONF_ZEROCONF_DEFAULT_INTERFACE, CONFIG_OPTIONS, DEFAULT_AUTO_START, DEFAULT_PORT, DEFAULT_SAFE_MODE, - DEFAULT_ZEROCONF_DEFAULT_INTERFACE, DOMAIN, EVENT_HOMEKIT_CHANGED, HOMEKIT, @@ -106,23 +111,24 @@ def _has_all_unique_names_and_ports(bridges): return bridges -BRIDGE_SCHEMA = vol.Schema( - { - vol.Optional(CONF_NAME, default=BRIDGE_NAME): vol.All( - cv.string, vol.Length(min=3, max=25) - ), - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_IP_ADDRESS): vol.All(ipaddress.ip_address, cv.string), - vol.Optional(CONF_ADVERTISE_IP): vol.All(ipaddress.ip_address, cv.string), - vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): cv.boolean, - vol.Optional(CONF_SAFE_MODE, default=DEFAULT_SAFE_MODE): cv.boolean, - vol.Optional(CONF_FILTER, default={}): BASE_FILTER_SCHEMA, - vol.Optional(CONF_ENTITY_CONFIG, default={}): validate_entity_config, - vol.Optional( - CONF_ZEROCONF_DEFAULT_INTERFACE, default=DEFAULT_ZEROCONF_DEFAULT_INTERFACE, - ): cv.boolean, - }, - extra=vol.ALLOW_EXTRA, +BRIDGE_SCHEMA = vol.All( + cv.deprecated(CONF_ZEROCONF_DEFAULT_INTERFACE), + vol.Schema( + { + vol.Optional(CONF_NAME, default=BRIDGE_NAME): vol.All( + cv.string, vol.Length(min=3, max=25) + ), + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_IP_ADDRESS): vol.All(ipaddress.ip_address, cv.string), + vol.Optional(CONF_ADVERTISE_IP): vol.All(ipaddress.ip_address, cv.string), + vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): cv.boolean, + vol.Optional(CONF_SAFE_MODE, default=DEFAULT_SAFE_MODE): cv.boolean, + vol.Optional(CONF_FILTER, default={}): BASE_FILTER_SCHEMA, + vol.Optional(CONF_ENTITY_CONFIG, default={}): validate_entity_config, + vol.Optional(CONF_ZEROCONF_DEFAULT_INTERFACE): cv.boolean, + }, + extra=vol.ALLOW_EXTRA, + ), ) CONFIG_SCHEMA = vol.Schema( @@ -226,11 +232,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): }, ) ) - interface_choice = ( - InterfaceChoice.Default - if options.get(CONF_ZEROCONF_DEFAULT_INTERFACE) - else None - ) homekit = HomeKit( hass, @@ -241,11 +242,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): entity_config, safe_mode, advertise_ip, - interface_choice, entry.entry_id, ) - await hass.async_add_executor_job(homekit.setup) - await homekit.async_setup_zeroconf() + zeroconf_instance = await zeroconf.async_get_instance(hass) + await hass.async_add_executor_job(homekit.setup, zeroconf_instance) undo_listener = entry.add_update_listener(_async_update_listener) @@ -397,7 +397,6 @@ class HomeKit: entity_config, safe_mode, advertise_ip=None, - interface_choice=None, entry_id=None, ): """Initialize a HomeKit object.""" @@ -409,14 +408,13 @@ class HomeKit: self._config = entity_config self._safe_mode = safe_mode self._advertise_ip = advertise_ip - self._interface_choice = interface_choice self._entry_id = entry_id self.status = STATUS_READY self.bridge = None self.driver = None - def setup(self): + def setup(self, zeroconf_instance): """Set up bridge and accessory driver.""" # pylint: disable=import-outside-toplevel from .accessories import HomeBridge, HomeDriver @@ -433,7 +431,7 @@ class HomeKit: port=self._port, persist_file=persist_file, advertised_address=self._advertise_ip, - interface_choice=self._interface_choice, + zeroconf_instance=zeroconf_instance, ) # If we do not load the mac address will be wrong @@ -448,12 +446,6 @@ class HomeKit: _LOGGER.debug("Safe_mode selected for %s", self._name) self.driver.safe_mode = True - async def async_setup_zeroconf(self): - """Share the system zeroconf instance.""" - # Replace the existing zeroconf instance. - await self.hass.async_add_executor_job(self.driver.advertiser.close) - self.driver.advertiser = await zeroconf.async_get_instance(self.hass) - def reset_accessories(self, entity_ids): """Reset the accessory to load the latest configuration.""" aid_storage = self.hass.data[DOMAIN][self._entry_id][AID_STORAGE] @@ -522,8 +514,9 @@ class HomeKit: device_lookup = ent_reg.async_get_device_class_lookup( { - ("binary_sensor", DEVICE_CLASS_BATTERY_CHARGING), - ("sensor", DEVICE_CLASS_BATTERY), + (BINARY_SENSOR_DOMAIN, DEVICE_CLASS_BATTERY_CHARGING), + (BINARY_SENSOR_DOMAIN, DEVICE_CLASS_MOTION), + (SENSOR_DOMAIN, DEVICE_CLASS_BATTERY), } ) @@ -537,9 +530,7 @@ class HomeKit: await self._async_set_device_info_attributes( ent_reg_ent, dev_reg, state.entity_id ) - self._async_configure_linked_battery_sensors( - ent_reg_ent, device_lookup, state - ) + self._async_configure_linked_sensors(ent_reg_ent, device_lookup, state) bridged_states.append(state) @@ -629,9 +620,7 @@ class HomeKit: self.hass.add_job(self.driver.stop) @callback - def _async_configure_linked_battery_sensors( - self, ent_reg_ent, device_lookup, state - ): + def _async_configure_linked_sensors(self, ent_reg_ent, device_lookup, state): if ( ent_reg_ent is None or ent_reg_ent.device_id is None @@ -644,7 +633,7 @@ class HomeKit: if ATTR_BATTERY_CHARGING not in state.attributes: battery_charging_binary_sensor_entity_id = device_lookup[ ent_reg_ent.device_id - ].get(("binary_sensor", DEVICE_CLASS_BATTERY_CHARGING)) + ].get((BINARY_SENSOR_DOMAIN, DEVICE_CLASS_BATTERY_CHARGING)) if battery_charging_binary_sensor_entity_id: self._config.setdefault(state.entity_id, {}).setdefault( CONF_LINKED_BATTERY_CHARGING_SENSOR, @@ -653,13 +642,22 @@ class HomeKit: if ATTR_BATTERY_LEVEL not in state.attributes: battery_sensor_entity_id = device_lookup[ent_reg_ent.device_id].get( - ("sensor", DEVICE_CLASS_BATTERY) + (SENSOR_DOMAIN, DEVICE_CLASS_BATTERY) ) if battery_sensor_entity_id: self._config.setdefault(state.entity_id, {}).setdefault( CONF_LINKED_BATTERY_SENSOR, battery_sensor_entity_id ) + if state.entity_id.startswith(f"{CAMERA_DOMAIN}."): + motion_binary_sensor_entity_id = device_lookup[ent_reg_ent.device_id].get( + (BINARY_SENSOR_DOMAIN, DEVICE_CLASS_MOTION) + ) + if motion_binary_sensor_entity_id: + self._config.setdefault(state.entity_id, {}).setdefault( + CONF_LINKED_MOTION_SENSOR, motion_binary_sensor_entity_id, + ) + async def _async_set_device_info_attributes(self, ent_reg_ent, dev_reg, entity_id): """Set attributes that will be used for homekit device info.""" ent_cfg = self._config.setdefault(entity_id, {}) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 3cd3c46613b..cb1c76656bb 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -25,6 +25,7 @@ from homeassistant.const import ( DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, STATE_ON, + STATE_UNAVAILABLE, TEMP_CELSIUS, TEMP_FAHRENHEIT, UNIT_PERCENTAGE, @@ -322,6 +323,12 @@ class HomeAccessory(Accessory): CHAR_STATUS_LOW_BATTERY, value=0 ) + @property + def available(self): + """Return if accessory is available.""" + state = self.hass.states.get(self.entity_id) + return state is not None and state.state != STATE_UNAVAILABLE + async def run(self): """Handle accessory driver started event. diff --git a/homeassistant/components/homekit/aidmanager.py b/homeassistant/components/homekit/aidmanager.py index 487865f22ab..31c341c9144 100644 --- a/homeassistant/components/homekit/aidmanager.py +++ b/homeassistant/components/homekit/aidmanager.py @@ -11,7 +11,6 @@ This module generates and stores them in a HA storage. """ import logging import random -from zlib import adler32 from fnvhash import fnv1a_32 @@ -44,11 +43,6 @@ def get_system_unique_id(entity: RegistryEntry): def _generate_aids(unique_id: str, entity_id: str) -> int: """Generate accessory aid.""" - # Backward compatibility: Previously HA used to *only* do adler32 on the entity id. - # Not stable if entity ID changes - # Not robust against collisions - yield adler32(entity_id.encode("utf-8")) - if unique_id: # Use fnv1a_32 of the unique id as # fnv1a_32 has less collisions than @@ -96,15 +90,7 @@ class AccessoryAidStorage: # There is no data about aid allocations yet return - # Remove the UNIQUE_IDS_KEY in 0.112 and later - # The beta version used UNIQUE_IDS_KEY but - # since we now have entity ids in the dict - # we use ALLOCATIONS_KEY but check for - # UNIQUE_IDS_KEY in case the database has not - # been upgraded yet - self.allocations = raw_storage.get( - ALLOCATIONS_KEY, raw_storage.get(UNIQUE_IDS_KEY, {}) - ) + self.allocations = raw_storage.get(ALLOCATIONS_KEY, {}) self.allocated_aids = set(self.allocations.values()) def get_or_allocate_aid_for_entity_id(self, entity_id: str): @@ -118,17 +104,17 @@ class AccessoryAidStorage: def _get_or_allocate_aid(self, unique_id: str, entity_id: str): """Allocate (and return) a new aid for an accessory.""" - # Prefer the unique_id over the - # entitiy_id - storage_key = unique_id or entity_id - - if storage_key in self.allocations: - return self.allocations[storage_key] + if unique_id and unique_id in self.allocations: + return self.allocations[unique_id] + if entity_id in self.allocations: + return self.allocations[entity_id] for aid in _generate_aids(unique_id, entity_id): if aid in INVALID_AIDS: continue if aid not in self.allocated_aids: + # Prefer the unique_id over the entitiy_id + storage_key = unique_id or entity_id self.allocations[storage_key] = aid self.allocated_aids.add(aid) self.async_schedule_save() diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index 1dde5be9e98..4cd6b9ffd78 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -23,11 +23,9 @@ from .const import ( CONF_FILTER, CONF_SAFE_MODE, CONF_VIDEO_CODEC, - CONF_ZEROCONF_DEFAULT_INTERFACE, DEFAULT_AUTO_START, DEFAULT_CONFIG_FLOW_PORT, DEFAULT_SAFE_MODE, - DEFAULT_ZEROCONF_DEFAULT_INTERFACE, SHORT_BRIDGE_NAME, VIDEO_CODEC_COPY, ) @@ -227,14 +225,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): vol.Optional( CONF_SAFE_MODE, default=self.homekit_options.get(CONF_SAFE_MODE, DEFAULT_SAFE_MODE), - ): bool, - vol.Optional( - CONF_ZEROCONF_DEFAULT_INTERFACE, - default=self.homekit_options.get( - CONF_ZEROCONF_DEFAULT_INTERFACE, - DEFAULT_ZEROCONF_DEFAULT_INTERFACE, - ), - ): bool, + ): bool } ) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 3291fab7a30..75a3ad5520b 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -27,6 +27,7 @@ ATTR_INTERGRATION = "platform" ATTR_MANUFACTURER = "manufacturer" ATTR_MODEL = "model" ATTR_SOFTWARE_VERSION = "sw_version" +ATTR_KEY_NAME = "key_name" # #### Config #### CONF_ADVERTISE_IP = "advertise_ip" @@ -40,6 +41,7 @@ CONF_FEATURE_LIST = "feature_list" CONF_FILTER = "filter" CONF_LINKED_BATTERY_SENSOR = "linked_battery_sensor" CONF_LINKED_BATTERY_CHARGING_SENSOR = "linked_battery_charging_sensor" +CONF_LINKED_MOTION_SENSOR = "linked_motion_sensor" CONF_LOW_BATTERY_THRESHOLD = "low_battery_threshold" CONF_MAX_FPS = "max_fps" CONF_MAX_HEIGHT = "max_height" @@ -66,7 +68,6 @@ DEFAULT_MAX_WIDTH = 1920 DEFAULT_PORT = 51827 DEFAULT_CONFIG_FLOW_PORT = 51828 DEFAULT_SAFE_MODE = False -DEFAULT_ZEROCONF_DEFAULT_INTERFACE = False DEFAULT_VIDEO_CODEC = VIDEO_CODEC_LIBX264 DEFAULT_VIDEO_MAP = "0:v:0" DEFAULT_VIDEO_PACKET_SIZE = 1316 @@ -79,6 +80,7 @@ FEATURE_TOGGLE_MUTE = "toggle_mute" # #### HomeKit Component Event #### EVENT_HOMEKIT_CHANGED = "homekit_state_change" +EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED = "homekit_tv_remote_key_pressed" # #### HomeKit Component Services #### SERVICE_HOMEKIT_START = "start" @@ -228,6 +230,21 @@ THRESHOLD_CO2 = 1000 DEFAULT_MIN_TEMP_WATER_HEATER = 40 # °C DEFAULT_MAX_TEMP_WATER_HEATER = 60 # °C +# #### Media Player Key Names #### +KEY_ARROW_DOWN = "arrow_down" +KEY_ARROW_LEFT = "arrow_left" +KEY_ARROW_RIGHT = "arrow_right" +KEY_ARROW_UP = "arrow_up" +KEY_BACK = "back" +KEY_EXIT = "exit" +KEY_FAST_FORWARD = "fast_forward" +KEY_INFORMATION = "information" +KEY_NEXT_TRACK = "next_track" +KEY_PREVIOUS_TRACK = "previous_track" +KEY_REWIND = "rewind" +KEY_SELECT = "select" +KEY_PLAY_PAUSE = "play_pause" + # #### Door states #### HK_DOOR_OPEN = 0 HK_DOOR_CLOSED = 1 @@ -249,7 +266,6 @@ HK_NOT_CHARGABLE = 2 CONFIG_OPTIONS = [ CONF_FILTER, CONF_AUTO_START, - CONF_ZEROCONF_DEFAULT_INTERFACE, CONF_SAFE_MODE, CONF_ENTITY_CONFIG, ] diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 8f7382b5d85..985fcc1e799 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -2,7 +2,7 @@ "domain": "homekit", "name": "HomeKit", "documentation": "https://www.home-assistant.io/integrations/homekit", - "requirements": ["HAP-python==2.8.4","fnvhash==0.1.0","PyQRCode==1.2.1","base36==0.1.1","PyTurboJPEG==1.4.0"], + "requirements": ["HAP-python==2.9.1","fnvhash==0.1.0","PyQRCode==1.2.1","base36==0.1.1","PyTurboJPEG==1.4.0"], "dependencies": ["http", "camera", "ffmpeg"], "after_dependencies": ["logbook", "zeroconf"], "codeowners": ["@bdraco"], diff --git a/homeassistant/components/homekit/strings.json b/homeassistant/components/homekit/strings.json index 131ecd8db4c..39011d75719 100644 --- a/homeassistant/components/homekit/strings.json +++ b/homeassistant/components/homekit/strings.json @@ -30,8 +30,7 @@ "advanced": { "data": { "auto_start": "[%key:component::homekit::config::step::user::data::auto_start%]", - "safe_mode": "Safe Mode (enable only if pairing fails)", - "zeroconf_default_interface": "Use default zeroconf interface (enable if the bridge cannot be found in the Home app)" + "safe_mode": "Safe Mode (enable only if pairing fails)" }, "description": "These settings only need to be adjusted if the HomeKit bridge is not functional.", "title": "Advanced Configuration" diff --git a/homeassistant/components/homekit/translations/ca.json b/homeassistant/components/homekit/translations/ca.json index 79d2139fedc..0282b701ad7 100644 --- a/homeassistant/components/homekit/translations/ca.json +++ b/homeassistant/components/homekit/translations/ca.json @@ -33,6 +33,7 @@ "data": { "camera_copy": "C\u00e0meres que admeten fluxos H.264 natius" }, + "description": "Comprova les c\u00e0meres que suporten fluxos nadius H.264. Si alguna c\u00e0mera not proporciona una sortida H.264, el sistema transcodificar\u00e0 el v\u00eddeo a H.264 per a HomeKit. La transcodificaci\u00f3 necessita una CPU potent i probablement no funcioni en ordinadors petits (SBC).", "title": "Selecci\u00f3 del c\u00f2dec de v\u00eddeo de c\u00e0mera" }, "exclude": { diff --git a/homeassistant/components/homekit/translations/ko.json b/homeassistant/components/homekit/translations/ko.json index 52b329104df..7f9d6d26d11 100644 --- a/homeassistant/components/homekit/translations/ko.json +++ b/homeassistant/components/homekit/translations/ko.json @@ -11,7 +11,7 @@ "user": { "data": { "auto_start": "\uc790\ub3d9 \uc2dc\uc791 (Z-Wave \ub610\ub294 \uae30\ud0c0 \uc9c0\uc5f0\ub41c \uc2dc\uc791 \uc2dc\uc2a4\ud15c\uc744 \uc0ac\uc6a9\ud558\ub294 \uacbd\uc6b0 \ube44\ud65c\uc131\ud654\ud574\uc8fc\uc138\uc694)", - "include_domains": "\ud3ec\ud568 \ud560 \ub3c4\uba54\uc778" + "include_domains": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778" }, "description": "HomeKit \ube0c\ub9ac\uc9c0\ub97c \uc0ac\uc6a9\ud558\uba74 HomeKit \uc5d0\uc11c Home Assistant \uad6c\uc131\uc694\uc18c\uc5d0 \uc561\uc138\uc2a4\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. HomeKit \ube0c\ub9ac\uc9c0\ub294 \ube0c\ub9ac\uc9c0 \uc790\uccb4\ub97c \ud3ec\ud568\ud558\uc5ec \uc778\uc2a4\ud134\uc2a4\ub2f9 150\uac1c\uc758 \uc561\uc138\uc11c\ub9ac\ub85c \uc81c\ud55c\ub429\ub2c8\ub2e4. \ucd5c\ub300 \uc561\uc138\uc11c\ub9ac \uc218\ub97c \ucd08\uacfc\ud558\uc5ec \ube0c\ub9ac\uc9d5\ud558\ub824\uba74 \uc5ec\ub7ec \ub3c4\uba54\uc778\uc5d0 \ub300\ud574 \uc5ec\ub7ec \uac1c\uc758 \ud648\ud0b7 \ube0c\ub9ac\uc9c0\ub97c \uc0ac\uc6a9\ud558\ub294 \uac83\uc774 \uc88b\uc2b5\ub2c8\ub2e4. \uad6c\uc131\uc694\uc18c\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 \uae30\ubcf8 \ube0c\ub9ac\uc9c0\uc758 YAML \uc744 \ud1b5\ud574\uc11c\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", "title": "HomeKit \ube0c\ub9ac\uc9c0 \ud65c\uc131\ud654\ud558\uae30" @@ -24,9 +24,9 @@ "data": { "auto_start": "\uc790\ub3d9 \uc2dc\uc791 (Z-Wave \ub610\ub294 \uae30\ud0c0 \uc9c0\uc5f0\ub41c \uc2dc\uc791 \uc2dc\uc2a4\ud15c\uc744 \uc0ac\uc6a9\ud558\ub294 \uacbd\uc6b0 \ube44\ud65c\uc131\ud654\ud574\uc8fc\uc138\uc694)", "safe_mode": "\uc548\uc804 \ubaa8\ub4dc (\ud398\uc5b4\ub9c1\uc774 \uc2e4\ud328\ud55c \uacbd\uc6b0\uc5d0\ub9cc \ud65c\uc131\ud654\ud574\uc8fc\uc138\uc694)", - "zeroconf_default_interface": "\uae30\ubcf8 zeroconf \uc778\ud130\ud398\uc774\uc2a4 \uc0ac\uc6a9 (Home \uc571\uc5d0\uc11c \ube0c\ub9ac\uc9c0\ub97c \ucc3e\uc744 \uc218\uc5c6\ub294 \uacbd\uc6b0 \ud65c\uc131\ud654\ud574\uc8fc\uc138\uc694)" + "zeroconf_default_interface": "\uae30\ubcf8 zeroconf \uc778\ud130\ud398\uc774\uc2a4 \uc0ac\uc6a9 (Home \uc571\uc5d0\uc11c \ube0c\ub9ac\uc9c0\ub97c \ucc3e\uc744 \uc218 \uc5c6\ub294 \uacbd\uc6b0 \ud65c\uc131\ud654\ud574\uc8fc\uc138\uc694)" }, - "description": "\uc774 \uc124\uc815\uc740 HomeKit \ube0c\ub9ac\uc9c0\uac00 \uc791\ub3d9\ud558\uc9c0 \uc54a\ub294 \uacbd\uc6b0\uc5d0\ub9cc \uc870\uc815\ud558\uba74 \ub429\ub2c8\ub2e4.", + "description": "\uc774 \uc124\uc815\uc740 HomeKit \ube0c\ub9ac\uc9c0\uac00 \uc791\ub3d9\ud558\uc9c0 \uc54a\ub294 \uacbd\uc6b0\uc5d0\ub9cc \uc124\uc815\ud574\uc8fc\uc138\uc694.", "title": "\uace0\uae09 \uad6c\uc131\ud558\uae30" }, "cameras": { @@ -38,16 +38,16 @@ }, "exclude": { "data": { - "exclude_entities": "\uc81c\uc678 \ud560 \uad6c\uc131\uc694\uc18c" + "exclude_entities": "\uc81c\uc678\ud560 \uad6c\uc131\uc694\uc18c" }, "description": "\ube0c\ub9ac\uc9c0\ud558\uc9c0 \uc54a\uc73c\ub824\ub294 \uad6c\uc131\uc694\uc18c\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", "title": "\ube0c\ub9ac\uc9c0\uc5d0\uc11c \uc120\ud0dd\ud55c \ub3c4\uba54\uc778\uc758 \uad6c\uc131\uc694\uc18c \uc81c\uc678\ud558\uae30" }, "init": { "data": { - "include_domains": "\ud3ec\ud568 \ud560 \ub3c4\uba54\uc778" + "include_domains": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778" }, - "description": "\"\ud3ec\ud568 \ud560 \ub3c4\uba54\uc778\"\uc758 \uad6c\uc131\uc694\uc18c\ub294 HomeKit \uc5d0 \uc5f0\uacb0\ub429\ub2c8\ub2e4. \ub2e4\uc74c \ud654\uba74\uc5d0\uc11c \uc774 \ubaa9\ub85d\uc758 \uc81c\uc678\ud560 \uad6c\uc131\uc694\uc18c\ub97c \uc120\ud0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "description": "\"\ud3ec\ud568\ud560 \ub3c4\uba54\uc778\"\uc758 \uad6c\uc131\uc694\uc18c\ub294 HomeKit \uc5d0 \uc5f0\uacb0\ub429\ub2c8\ub2e4. \ub2e4\uc74c \ud654\uba74\uc5d0\uc11c \uc774 \ubaa9\ub85d\uc758 \uc81c\uc678\ud560 \uad6c\uc131\uc694\uc18c\ub97c \uc120\ud0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", "title": "\ube0c\ub9ac\uc9c0 \ud560 \ub3c4\uba54\uc778 \uc120\ud0dd\ud558\uae30" }, "yaml": { diff --git a/homeassistant/components/homekit/translations/pt-BR.json b/homeassistant/components/homekit/translations/pt-BR.json new file mode 100644 index 00000000000..149c16d4288 --- /dev/null +++ b/homeassistant/components/homekit/translations/pt-BR.json @@ -0,0 +1,17 @@ +{ + "options": { + "step": { + "advanced": { + "title": "Configura\u00e7\u00e3o avan\u00e7ada" + }, + "cameras": { + "title": "Selecione o codec de v\u00eddeo da c\u00e2mera." + }, + "exclude": { + "data": { + "exclude_entities": "Entidades para excluir" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/ru.json b/homeassistant/components/homekit/translations/ru.json index efec33bd187..dca8c10b8de 100644 --- a/homeassistant/components/homekit/translations/ru.json +++ b/homeassistant/components/homekit/translations/ru.json @@ -13,8 +13,8 @@ "auto_start": "\u0410\u0432\u0442\u043e\u0437\u0430\u043f\u0443\u0441\u043a (\u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438 Z-Wave \u0438\u043b\u0438 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u044b \u043e\u0442\u043b\u043e\u0436\u0435\u043d\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0443\u0441\u043a\u0430)", "include_domains": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u044b" }, - "description": "HomeKit Bridge \u043f\u043e\u0437\u0432\u043e\u043b\u0438\u0442 \u0432\u0430\u043c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u043e\u0431\u044a\u0435\u043a\u0442\u0430\u043c Home Assistant \u0432 HomeKit. HomeKit Bridge \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d 150 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430\u043c\u0438 \u043d\u0430 \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440, \u0432\u043a\u043b\u044e\u0447\u0430\u044f \u0441\u0430\u043c \u043c\u043e\u0441\u0442. \u0415\u0441\u043b\u0438 \u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u0431\u043e\u043b\u044c\u0448\u0435, \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e HomeKit Bridge \u0434\u043b\u044f \u0440\u0430\u0437\u043d\u044b\u0445 \u0434\u043e\u043c\u0435\u043d\u043e\u0432. \u0414\u0435\u0442\u0430\u043b\u044c\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 YAML \u0434\u043b\u044f \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0433\u043e \u0431\u0440\u0438\u0434\u0436\u0430.", - "title": "\u0410\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c HomeKit Bridge" + "description": "HomeKit Bridge \u043f\u043e\u0437\u0432\u043e\u043b\u0438\u0442 \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u043e\u0431\u044a\u0435\u043a\u0442\u0430\u043c Home Assistant \u0447\u0435\u0440\u0435\u0437 HomeKit. HomeKit Bridge \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d 150 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430\u043c\u0438 \u043d\u0430 \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440, \u0432\u043a\u043b\u044e\u0447\u0430\u044f \u0441\u0430\u043c \u0431\u0440\u0438\u0434\u0436. \u0415\u0441\u043b\u0438 \u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u0431\u043e\u043b\u044c\u0448\u0435, \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e HomeKit Bridge \u0434\u043b\u044f \u0440\u0430\u0437\u043d\u044b\u0445 \u0434\u043e\u043c\u0435\u043d\u043e\u0432. \u0414\u0435\u0442\u0430\u043b\u044c\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 YAML \u0434\u043b\u044f \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0433\u043e \u0431\u0440\u0438\u0434\u0436\u0430.", + "title": "HomeKit Bridge" } } }, diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py index c4e52f07832..e25c5189262 100644 --- a/homeassistant/components/homekit/type_cameras.py +++ b/homeassistant/components/homekit/type_cameras.py @@ -12,18 +12,23 @@ from pyhap.camera import ( ) from pyhap.const import CATEGORY_CAMERA -from homeassistant.components.camera.const import DOMAIN as DOMAIN_CAMERA from homeassistant.components.ffmpeg import DATA_FFMPEG +from homeassistant.const import STATE_ON from homeassistant.core import callback -from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.event import ( + async_track_state_change, + async_track_time_interval, +) from homeassistant.util import get_local_ip from .accessories import TYPES, HomeAccessory from .const import ( + CHAR_MOTION_DETECTED, CHAR_STREAMING_STRATUS, CONF_AUDIO_CODEC, CONF_AUDIO_MAP, CONF_AUDIO_PACKET_SIZE, + CONF_LINKED_MOTION_SENSOR, CONF_MAX_FPS, CONF_MAX_HEIGHT, CONF_MAX_WIDTH, @@ -44,6 +49,7 @@ from .const import ( DEFAULT_VIDEO_MAP, DEFAULT_VIDEO_PACKET_SIZE, SERV_CAMERA_RTP_STREAM_MANAGEMENT, + SERV_MOTION_SENSOR, ) from .img_util import scale_jpeg_camera_image from .util import pid_is_alive @@ -126,7 +132,6 @@ class Camera(HomeAccessory, PyhapCamera): """Initialize a Camera accessory object.""" self._ffmpeg = hass.data[DATA_FFMPEG] self._cur_session = None - self._camera = hass.data[DOMAIN_CAMERA] for config_key in CONFIG_DEFAULTS: if config_key not in config: config[config_key] = CONFIG_DEFAULTS[config_key] @@ -180,6 +185,47 @@ class Camera(HomeAccessory, PyhapCamera): category=CATEGORY_CAMERA, options=options, ) + self._char_motion_detected = None + self.linked_motion_sensor = self.config.get(CONF_LINKED_MOTION_SENSOR) + if not self.linked_motion_sensor: + return + state = self.hass.states.get(self.linked_motion_sensor) + if not state: + return + serv_motion = self.add_preload_service(SERV_MOTION_SENSOR) + self._char_motion_detected = serv_motion.configure_char( + CHAR_MOTION_DETECTED, value=False + ) + self._async_update_motion_state(None, None, state) + + async def run_handler(self): + """Handle accessory driver started event. + + Run inside the Home Assistant event loop. + """ + if self._char_motion_detected: + async_track_state_change( + self.hass, self.linked_motion_sensor, self._async_update_motion_state + ) + + await super().run_handler() + + @callback + def _async_update_motion_state( + self, entity_id=None, old_state=None, new_state=None + ): + """Handle link motion sensor state change to update HomeKit value.""" + detected = new_state.state == STATE_ON + if self._char_motion_detected.value == detected: + return + + self._char_motion_detected.set_value(detected) + _LOGGER.debug( + "%s: Set linked motion %s sensor to %d", + self.entity_id, + self.linked_motion_sensor, + detected, + ) @callback def async_update_state(self, new_state): @@ -188,14 +234,13 @@ class Camera(HomeAccessory, PyhapCamera): async def _async_get_stream_source(self): """Find the camera stream source url.""" - camera = self._camera.get_entity(self.entity_id) - if not camera or not camera.is_on: - return None stream_source = self.config.get(CONF_STREAM_SOURCE) if stream_source: return stream_source try: - stream_source = await camera.stream_source() + stream_source = await self.hass.components.camera.async_get_stream_source( + self.entity_id + ) except Exception: # pylint: disable=broad-except _LOGGER.exception( "Failed to get stream source - this could be a transient error or your camera might not be compatible with HomeKit yet" diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py index 4a104972b02..886c15a5fb9 100644 --- a/homeassistant/components/homekit/type_media_players.py +++ b/homeassistant/components/homekit/type_media_players.py @@ -40,6 +40,7 @@ from homeassistant.core import callback from .accessories import TYPES, HomeAccessory from .const import ( + ATTR_KEY_NAME, CHAR_ACTIVE, CHAR_ACTIVE_IDENTIFIER, CHAR_CONFIGURED_NAME, @@ -56,10 +57,24 @@ from .const import ( CHAR_VOLUME_CONTROL_TYPE, CHAR_VOLUME_SELECTOR, CONF_FEATURE_LIST, + EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, + KEY_ARROW_DOWN, + KEY_ARROW_LEFT, + KEY_ARROW_RIGHT, + KEY_ARROW_UP, + KEY_BACK, + KEY_EXIT, + KEY_FAST_FORWARD, + KEY_INFORMATION, + KEY_NEXT_TRACK, + KEY_PLAY_PAUSE, + KEY_PREVIOUS_TRACK, + KEY_REWIND, + KEY_SELECT, SERV_INPUT_SOURCE, SERV_SWITCH, SERV_TELEVISION, @@ -70,19 +85,19 @@ from .util import get_media_player_features _LOGGER = logging.getLogger(__name__) MEDIA_PLAYER_KEYS = { - # 0: "Rewind", - # 1: "FastForward", - # 2: "NextTrack", - # 3: "PreviousTrack", - # 4: "ArrowUp", - # 5: "ArrowDown", - # 6: "ArrowLeft", - # 7: "ArrowRight", - # 8: "Select", - # 9: "Back", - # 10: "Exit", - 11: SERVICE_MEDIA_PLAY_PAUSE, - # 15: "Information", + 0: KEY_REWIND, + 1: KEY_FAST_FORWARD, + 2: KEY_NEXT_TRACK, + 3: KEY_PREVIOUS_TRACK, + 4: KEY_ARROW_UP, + 5: KEY_ARROW_DOWN, + 6: KEY_ARROW_LEFT, + 7: KEY_ARROW_RIGHT, + 8: KEY_SELECT, + 9: KEY_BACK, + 10: KEY_EXIT, + 11: KEY_PLAY_PAUSE, + 15: KEY_INFORMATION, } # Names may not contain special characters @@ -363,19 +378,28 @@ class TelevisionMediaPlayer(HomeAccessory): def set_remote_key(self, value): """Send remote key value if call came from HomeKit.""" _LOGGER.debug("%s: Set remote key to %s", self.entity_id, value) - service = MEDIA_PLAYER_KEYS.get(value) - if service: - # Handle Play Pause - if service == SERVICE_MEDIA_PLAY_PAUSE: - state = self.hass.states.get(self.entity_id).state - if state in (STATE_PLAYING, STATE_PAUSED): - service = ( - SERVICE_MEDIA_PLAY - if state == STATE_PAUSED - else SERVICE_MEDIA_PAUSE - ) + key_name = MEDIA_PLAYER_KEYS.get(value) + if key_name is None: + _LOGGER.warning("%s: Unhandled key press for %s", self.entity_id, value) + return + + if key_name == KEY_PLAY_PAUSE: + # Handle Play Pause by directly updating the media player entity. + state = self.hass.states.get(self.entity_id).state + if state in (STATE_PLAYING, STATE_PAUSED): + service = ( + SERVICE_MEDIA_PLAY if state == STATE_PAUSED else SERVICE_MEDIA_PAUSE + ) + else: + service = SERVICE_MEDIA_PLAY_PAUSE params = {ATTR_ENTITY_ID: self.entity_id} self.call_service(DOMAIN, service, params) + else: + # Other keys can be handled by listening to the event bus + self.hass.bus.fire( + EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED, + {ATTR_KEY_NAME: key_name, ATTR_ENTITY_ID: self.entity_id}, + ) @callback def async_update_state(self, new_state): diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index d35c463ca39..0465e33388d 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -11,7 +11,7 @@ import socket import pyqrcode import voluptuous as vol -from homeassistant.components import fan, media_player, sensor +from homeassistant.components import binary_sensor, fan, media_player, sensor from homeassistant.const import ( ATTR_CODE, ATTR_SUPPORTED_FEATURES, @@ -32,7 +32,9 @@ from .const import ( CONF_AUDIO_PACKET_SIZE, CONF_FEATURE, CONF_FEATURE_LIST, + CONF_LINKED_BATTERY_CHARGING_SENSOR, CONF_LINKED_BATTERY_SENSOR, + CONF_LINKED_MOTION_SENSOR, CONF_LOW_BATTERY_THRESHOLD, CONF_MAX_FPS, CONF_MAX_HEIGHT, @@ -83,6 +85,9 @@ BASIC_INFO_SCHEMA = vol.Schema( { vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_LINKED_BATTERY_SENSOR): cv.entity_domain(sensor.DOMAIN), + vol.Optional(CONF_LINKED_BATTERY_CHARGING_SENSOR): cv.entity_domain( + binary_sensor.DOMAIN + ), vol.Optional( CONF_LOW_BATTERY_THRESHOLD, default=DEFAULT_LOW_BATTERY_THRESHOLD ): cv.positive_int, @@ -115,6 +120,7 @@ CAMERA_SCHEMA = BASIC_INFO_SCHEMA.extend( vol.Optional( CONF_VIDEO_PACKET_SIZE, default=DEFAULT_VIDEO_PACKET_SIZE ): cv.positive_int, + vol.Optional(CONF_LINKED_MOTION_SENSOR): cv.entity_domain(binary_sensor.DOMAIN), } ) diff --git a/homeassistant/components/homekit_controller/translations/ca.json b/homeassistant/components/homekit_controller/translations/ca.json index 3407d93c63f..bf19b9af672 100644 --- a/homeassistant/components/homekit_controller/translations/ca.json +++ b/homeassistant/components/homekit_controller/translations/ca.json @@ -6,7 +6,7 @@ "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.", + "invalid_config_entry": "Aquest dispositiu s'est\u00e0 mostrant com a llest per a ser vinculat per\u00f2 ja hi ha una entrada de configuraci\u00f3 conflictiva a Home Assistant que s'ha d'eliminar primer.", "no_devices": "No s'han trobat dispositius desvinculats." }, "error": { @@ -14,7 +14,7 @@ "busy_error": "El dispositiu ha refusat la vinculaci\u00f3 perqu\u00e8 actualment ho est\u00e0 intentant amb un altre controlador diferent.", "max_peers_error": "El dispositiu ha refusat la vinculaci\u00f3 perqu\u00e8 no t\u00e9 suficient espai lliure.", "max_tries_error": "El dispositiu ha refusat la vinculaci\u00f3 perqu\u00e8 ha rebut m\u00e9s de 100 intents d'autenticaci\u00f3 fallits.", - "pairing_failed": "S'ha produ\u00eft un error mentre s'intentava la vinculaci\u00f3 amb el dispositiu. Pot ser que sigui un error temporal o pot ser que el teu dispositiu encara no estigui suportat.", + "pairing_failed": "S'ha produ\u00eft un error mentre s'intentava la vinculaci\u00f3 amb aquest dispositiu. Pot ser que sigui un error temporal o pot ser que el teu dispositiu encara no sigui compatible.", "unable_to_pair": "No s'ha pogut vincular, torna-ho a provar.", "unknown_error": "El dispositiu ha em\u00e8s un error desconegut. Vinculaci\u00f3 fallida." }, @@ -36,5 +36,5 @@ } } }, - "title": "Accessori HomeKit" + "title": "Controlador HomeKit" } \ No newline at end of file diff --git a/homeassistant/components/homematic/binary_sensor.py b/homeassistant/components/homematic/binary_sensor.py index 041f2f02643..7315a266a30 100644 --- a/homeassistant/components/homematic/binary_sensor.py +++ b/homeassistant/components/homematic/binary_sensor.py @@ -30,6 +30,8 @@ SENSOR_TYPES_CLASS = { "TiltSensor": None, "WeatherSensor": None, "IPContact": DEVICE_CLASS_OPENING, + "MotionIPV2": DEVICE_CLASS_MOTION, + "IPRemoteMotionV2": DEVICE_CLASS_MOTION, } diff --git a/homeassistant/components/homematic/const.py b/homeassistant/components/homematic/const.py index 188ec1e2445..9339fca84e5 100644 --- a/homeassistant/components/homematic/const.py +++ b/homeassistant/components/homematic/const.py @@ -55,6 +55,7 @@ HM_DEVICE_TYPES = { "IPKeySwitch", "IPKeySwitchLevel", "IPMultiIO", + "IPWSwitch", ], DISCOVER_LIGHTS: [ "Dimmer", @@ -106,6 +107,8 @@ HM_DEVICE_TYPES = { "MotionIPV2", "IPMultiIO", "IPThermostatWall2", + "IPRemoteMotionV2", + "HBUNISenWEA", ], DISCOVER_CLIMATE: [ "Thermostat", @@ -145,6 +148,8 @@ HM_DEVICE_TYPES = { "TiltIP", "IPShutterContactSabotage", "IPContact", + "IPRemoteMotionV2", + "IPWInputDevice", ], DISCOVER_COVER: ["Blind", "KeyBlind", "IPKeyBlind", "IPKeyBlindTilt"], DISCOVER_LOCKS: ["KeyMatic"], diff --git a/homeassistant/components/homematic/manifest.json b/homeassistant/components/homematic/manifest.json index 31b26bbd511..de55b941b91 100644 --- a/homeassistant/components/homematic/manifest.json +++ b/homeassistant/components/homematic/manifest.json @@ -2,6 +2,6 @@ "domain": "homematic", "name": "Homematic", "documentation": "https://www.home-assistant.io/integrations/homematic", - "requirements": ["pyhomematic==0.1.66"], + "requirements": ["pyhomematic==0.1.67"], "codeowners": ["@pvizeli", "@danielperna84"] } diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index da803b406bf..6fa78602944 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -62,6 +62,7 @@ HM_UNIT_HA_CAST = { "AIR_PRESSURE": "hPa", "FREQUENCY": FREQUENCY_HERTZ, "VALUE": "#", + "VALVE_STATE": UNIT_PERCENTAGE, } HM_DEVICE_CLASS_HA_CAST = { diff --git a/homeassistant/components/homematicip_cloud/translations/ko.json b/homeassistant/components/homematicip_cloud/translations/ko.json index 733fc9ebf51..54c129bec6d 100644 --- a/homeassistant/components/homematicip_cloud/translations/ko.json +++ b/homeassistant/components/homematicip_cloud/translations/ko.json @@ -21,7 +21,7 @@ "title": "HomematicIP \uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8 \uc120\ud0dd\ud558\uae30" }, "link": { - "description": "Home Assistant \uc5d0 HomematicIP \ub97c \ub4f1\ub85d\ud558\ub824\uba74 \uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8\uc758 \ud30c\ub780\uc0c9 \ubc84\ud2bc\uacfc Submit \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694.\n\n![\ube0c\ub9ac\uc9c0\uc758 \ubc84\ud2bc \uc704\uce58 \ubcf4\uae30](/static/images/config_flows/config_homematicip_cloud.png)", + "description": "Home Assistant \uc5d0 HomematicIP \ub97c \ub4f1\ub85d\ud558\ub824\uba74 \uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8\uc758 \ud30c\ub780\uc0c9 \ubc84\ud2bc\uacfc \ud655\uc778\uc744 \ud074\ub9ad\ud574\uc8fc\uc138\uc694.\n\n![\ube0c\ub9ac\uc9c0\uc758 \ubc84\ud2bc \uc704\uce58 \ubcf4\uae30](/static/images/config_flows/config_homematicip_cloud.png)", "title": "\uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8 \uc5f0\uacb0\ud558\uae30" } } diff --git a/homeassistant/components/homematicip_cloud/translations/pl.json b/homeassistant/components/homematicip_cloud/translations/pl.json index 2229348efa1..d51af1e32ea 100644 --- a/homeassistant/components/homematicip_cloud/translations/pl.json +++ b/homeassistant/components/homematicip_cloud/translations/pl.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Punkt dost\u0119pu jest ju\u017c skonfigurowany.", "connection_aborted": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z serwerem HMIP", - "unknown": "[%key_id:common::config_flow::error::unknown%]" + "unknown": "Nieoczekiwany b\u0142\u0105d." }, "error": { "invalid_pin": "Nieprawid\u0142owy kod PIN, spr\u00f3buj ponownie.", @@ -22,7 +22,7 @@ }, "link": { "description": "Naci\u015bnij niebieski przycisk na punkcie dost\u0119pu i przycisk przesy\u0142ania, aby zarejestrowa\u0107 HomematicIP w Home Assistant. \n\n![Location of button on bridge](/static/images/config_flows/config_homematicip_cloud.png)", - "title": "Po\u0142\u0105cz z punktem dost\u0119pu" + "title": "Po\u0142\u0105czenie z punktem dost\u0119pu" } } } diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 069fc42c884..b387cea350e 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -4,7 +4,7 @@ import logging import os import ssl from traceback import extract_stack -from typing import Optional, cast +from typing import Dict, Optional, cast from aiohttp import web from aiohttp.web_exceptions import HTTPMovedPermanently @@ -15,10 +15,11 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, SERVER_PORT, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import storage import homeassistant.helpers.config_validation as cv from homeassistant.loader import bind_hass +from homeassistant.setup import ATTR_COMPONENT, EVENT_COMPONENT_LOADED import homeassistant.util as hass_util from homeassistant.util import ssl as ssl_util @@ -133,7 +134,7 @@ class ApiConfig: def base_url(self) -> str: """Proxy property to find caller of this deprecated property.""" found_frame = None - for frame in reversed(extract_stack()): + for frame in reversed(extract_stack()[:-1]): for path in ("custom_components/", "homeassistant/components/"): try: index = frame.filename.index(path) @@ -216,29 +217,36 @@ async def async_setup(hass, config): ssl_profile=ssl_profile, ) - async def stop_server(event): + startup_listeners = [] + + async def stop_server(event: Event) -> None: """Stop the server.""" await server.stop() - async def start_server(event): + async def start_server(event: Event) -> None: """Start the server.""" + + for listener in startup_listeners: + listener() + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_server) - await server.start() - # If we are set up successful, we store the HTTP settings for safe mode. - store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY) + await start_http_server_and_save_config(hass, dict(conf), server) - if CONF_TRUSTED_PROXIES in conf: - conf_to_save = dict(conf) - conf_to_save[CONF_TRUSTED_PROXIES] = [ - str(ip.network_address) for ip in conf_to_save[CONF_TRUSTED_PROXIES] - ] - else: - conf_to_save = conf + async def async_wait_frontend_load(event: Event) -> None: + """Wait for the frontend to load.""" - await store.async_save(conf_to_save) + if event.data[ATTR_COMPONENT] != "frontend": + return - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_server) + await start_server(event) + + startup_listeners.append( + hass.bus.async_listen(EVENT_COMPONENT_LOADED, async_wait_frontend_load) + ) + startup_listeners.append( + hass.bus.async_listen(EVENT_HOMEASSISTANT_START, start_server) + ) hass.http = server @@ -418,3 +426,20 @@ class HomeAssistantHTTP: """Stop the aiohttp server.""" await self.site.stop() await self.runner.cleanup() + + +async def start_http_server_and_save_config( + hass: HomeAssistant, conf: Dict, server: HomeAssistantHTTP +) -> None: + """Startup the http server and save the config.""" + await server.start() # type: ignore + + # If we are set up successful, we store the HTTP settings for safe mode. + store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY) + + if CONF_TRUSTED_PROXIES in conf: + conf[CONF_TRUSTED_PROXIES] = [ + str(ip.network_address) for ip in conf[CONF_TRUSTED_PROXIES] + ] + + await store.async_save(conf) diff --git a/homeassistant/components/http/data_validator.py b/homeassistant/components/http/data_validator.py index 84bdb4e1c17..1c9e796dc86 100644 --- a/homeassistant/components/http/data_validator.py +++ b/homeassistant/components/http/data_validator.py @@ -1,12 +1,14 @@ """Decorator for view methods to help with data validation.""" from functools import wraps import logging +from typing import Any, Awaitable, Callable +from aiohttp import web import voluptuous as vol from homeassistant.const import HTTP_BAD_REQUEST -# mypy: allow-untyped-defs +from .view import HomeAssistantView _LOGGER = logging.getLogger(__name__) @@ -20,7 +22,7 @@ class RequestDataValidator: Will return a 400 if no JSON provided or doesn't match schema. """ - def __init__(self, schema, allow_empty=False): + def __init__(self, schema: vol.Schema, allow_empty: bool = False) -> None: """Initialize the decorator.""" if isinstance(schema, dict): schema = vol.Schema(schema) @@ -28,11 +30,15 @@ class RequestDataValidator: self._schema = schema self._allow_empty = allow_empty - def __call__(self, method): + def __call__( + self, method: Callable[..., Awaitable[web.StreamResponse]] + ) -> Callable: """Decorate a function.""" @wraps(method) - async def wrapper(view, request, *args, **kwargs): + async def wrapper( + view: HomeAssistantView, request: web.Request, *args: Any, **kwargs: Any + ) -> web.StreamResponse: """Wrap a request handler with data validation.""" data = None try: diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index 40ca43ff695..eb6c757384e 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -2,9 +2,10 @@ import asyncio import json import logging -from typing import List, Optional +from typing import Any, Callable, List, Optional from aiohttp import web +from aiohttp.typedefs import LooseHeaders from aiohttp.web_exceptions import ( HTTPBadRequest, HTTPInternalServerError, @@ -13,7 +14,7 @@ from aiohttp.web_exceptions import ( import voluptuous as vol from homeassistant import exceptions -from homeassistant.const import CONTENT_TYPE_JSON, HTTP_OK +from homeassistant.const import CONTENT_TYPE_JSON, HTTP_OK, HTTP_SERVICE_UNAVAILABLE from homeassistant.core import Context, is_callback from homeassistant.helpers.json import JSONEncoder @@ -22,9 +23,6 @@ from .const import KEY_AUTHENTICATED, KEY_HASS, KEY_REAL_IP _LOGGER = logging.getLogger(__name__) -# mypy: allow-untyped-defs, no-check-untyped-defs - - class HomeAssistantView: """Base view for all views.""" @@ -35,7 +33,7 @@ class HomeAssistantView: cors_allowed = False @staticmethod - def context(request): + def context(request: web.Request) -> Context: """Generate a context from a request.""" user = request.get("hass_user") if user is None: @@ -44,7 +42,9 @@ class HomeAssistantView: return Context(user_id=user.id) @staticmethod - def json(result, status_code=HTTP_OK, headers=None): + def json( + result: Any, status_code: int = HTTP_OK, headers: Optional[LooseHeaders] = None, + ) -> web.Response: """Return a JSON response.""" try: msg = json.dumps( @@ -63,15 +63,19 @@ class HomeAssistantView: return response def json_message( - self, message, status_code=HTTP_OK, message_code=None, headers=None - ): + self, + message: str, + status_code: int = HTTP_OK, + message_code: Optional[str] = None, + headers: Optional[LooseHeaders] = None, + ) -> web.Response: """Return a JSON message response.""" data = {"message": message} if message_code is not None: data["code"] = message_code return self.json(data, status_code, headers=headers) - def register(self, app, router): + def register(self, app: web.Application, router: web.UrlDispatcher) -> None: """Register the view with a router.""" assert self.url is not None, "No url set for view" urls = [self.url] + self.extra_urls @@ -95,16 +99,16 @@ class HomeAssistantView: app["allow_cors"](route) -def request_handler_factory(view, handler): +def request_handler_factory(view: HomeAssistantView, handler: Callable) -> Callable: """Wrap the handler classes.""" assert asyncio.iscoroutinefunction(handler) or is_callback( handler ), "Handler should be a coroutine or a callback." - async def handle(request): + async def handle(request: web.Request) -> web.StreamResponse: """Handle incoming request.""" - if not request.app[KEY_HASS].is_running: - return web.Response(status=503) + if request.app[KEY_HASS].is_stopping: + return web.Response(status=HTTP_SERVICE_UNAVAILABLE) authenticated = request.get(KEY_AUTHENTICATED, False) @@ -139,15 +143,17 @@ def request_handler_factory(view, handler): if isinstance(result, tuple): result, status_code = result - if isinstance(result, str): - result = result.encode("utf-8") + if isinstance(result, bytes): + bresult = result + elif isinstance(result, str): + bresult = result.encode("utf-8") elif result is None: - result = b"" - elif not isinstance(result, bytes): + bresult = b"" + else: assert ( False ), f"Result should be None, string, bytes or Response. Got: {result}" - return web.Response(body=result, status=status_code) + return web.Response(body=bresult, status=status_code) return handle diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 272efa5d722..87f66f87700 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -68,6 +68,7 @@ from .const import ( KEY_MONITORING_TRAFFIC_STATISTICS, KEY_NET_CURRENT_PLMN, KEY_NET_NET_MODE, + KEY_SMS_SMS_COUNT, KEY_WLAN_HOST_LIST, KEY_WLAN_WIFI_FEATURE_SWITCH, NOTIFY_SUPPRESS_TIMEOUT, @@ -243,6 +244,7 @@ class Router: ) self._get_data(KEY_NET_CURRENT_PLMN, self.client.net.current_plmn) self._get_data(KEY_NET_NET_MODE, self.client.net.net_mode) + self._get_data(KEY_SMS_SMS_COUNT, self.client.sms.sms_count) self._get_data(KEY_WLAN_HOST_LIST, self.client.wlan.host_list) self._get_data( KEY_WLAN_WIFI_FEATURE_SWITCH, self.client.wlan.wifi_feature_switch diff --git a/homeassistant/components/huawei_lte/const.py b/homeassistant/components/huawei_lte/const.py index 583c1c7d6f1..1019d4d19cd 100644 --- a/homeassistant/components/huawei_lte/const.py +++ b/homeassistant/components/huawei_lte/const.py @@ -32,6 +32,7 @@ KEY_MONITORING_STATUS = "monitoring_status" KEY_MONITORING_TRAFFIC_STATISTICS = "monitoring_traffic_statistics" KEY_NET_CURRENT_PLMN = "net_current_plmn" KEY_NET_NET_MODE = "net_net_mode" +KEY_SMS_SMS_COUNT = "sms_sms_count" KEY_WLAN_HOST_LIST = "wlan_host_list" KEY_WLAN_WIFI_FEATURE_SWITCH = "wlan_wifi_feature_switch" @@ -47,6 +48,7 @@ SENSOR_KEYS = { KEY_MONITORING_TRAFFIC_STATISTICS, KEY_NET_CURRENT_PLMN, KEY_NET_NET_MODE, + KEY_SMS_SMS_COUNT, } SWITCH_KEYS = {KEY_DIALUP_MOBILE_DATASWITCH} diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index 0660aa361f5..fd574784838 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -15,5 +15,5 @@ "manufacturer": "Huawei" } ], - "codeowners": ["@scop"] + "codeowners": ["@scop", "@fphammerle"] } diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 84d8e72c2ff..018e5236c74 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -22,6 +22,7 @@ from .const import ( KEY_MONITORING_TRAFFIC_STATISTICS, KEY_NET_CURRENT_PLMN, KEY_NET_NET_MODE, + KEY_SMS_SMS_COUNT, SENSOR_KEYS, ) @@ -195,6 +196,9 @@ SENSOR_META = { None, ), ), + (KEY_SMS_SMS_COUNT, "LocalUnread"): dict( + name="SMS unread", icon="mdi:email-receive", + ), } diff --git a/homeassistant/components/huawei_lte/translations/ca.json b/homeassistant/components/huawei_lte/translations/ca.json index cb8a4331a57..a8f4f9584c2 100644 --- a/homeassistant/components/huawei_lte/translations/ca.json +++ b/homeassistant/components/huawei_lte/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Aquest dispositiu ja est\u00e0 configurat", + "already_configured": "Aquest dispositiu ja ha estat configurat", "already_in_progress": "Aquest dispositiu ja s'est\u00e0 configurant", "not_huawei_lte": "No \u00e9s un dispositiu Huawei LTE" }, diff --git a/homeassistant/components/huawei_lte/translations/ko.json b/homeassistant/components/huawei_lte/translations/ko.json index ba39176d04a..53a7c4fc822 100644 --- a/homeassistant/components/huawei_lte/translations/ko.json +++ b/homeassistant/components/huawei_lte/translations/ko.json @@ -23,7 +23,7 @@ "url": "URL \uc8fc\uc18c", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, - "description": "\uae30\uae30 \uc561\uc138\uc2a4 \uc138\ubd80 \uc0ac\ud56d\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694. \uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \uc124\uc815\ud558\ub294 \uac83\uc740 \uc120\ud0dd \uc0ac\ud56d\uc774\uc9c0\ub9cc \ub354 \ub9ce\uc740 \uae30\ub2a5\uc744 \uc9c0\uc6d0\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ubc18\uba74, \uc778\uc99d \ub41c \uc5f0\uacb0\uc744 \uc0ac\uc6a9\ud558\uba74, \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uac00 \ud65c\uc131\ud654 \ub41c \uc0c1\ud0dc\uc5d0\uc11c \ub2e4\ub978 \ubc29\ubc95\uc73c\ub85c Home Assistant \uc758 \uc678\ubd80\uc5d0\uc11c \uae30\uae30\uc758 \uc6f9 \uc778\ud130\ud398\uc774\uc2a4\uc5d0 \uc561\uc138\uc2a4\ud558\ub294 \ub370 \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "description": "\uae30\uae30 \uc561\uc138\uc2a4 \uc138\ubd80 \uc0ac\ud56d\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694. \uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \uc124\uc815\ud558\ub294 \uac83\uc740 \uc120\ud0dd \uc0ac\ud56d\uc774\uc9c0\ub9cc \ub354 \ub9ce\uc740 \uae30\ub2a5\uc744 \uc9c0\uc6d0\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ubc18\uba74, \uc778\uc99d\ub41c \uc5f0\uacb0\uc744 \uc0ac\uc6a9\ud558\uba74, \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uac00 \ud65c\uc131\ud654\ub41c \uc0c1\ud0dc\uc5d0\uc11c \ub2e4\ub978 \ubc29\ubc95\uc73c\ub85c Home Assistant \uc758 \uc678\ubd80\uc5d0\uc11c \uae30\uae30\uc758 \uc6f9 \uc778\ud130\ud398\uc774\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud558\ub294 \ub370 \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", "title": "Huawei LTE \uc124\uc815\ud558\uae30" } } diff --git a/homeassistant/components/huawei_lte/translations/pl.json b/homeassistant/components/huawei_lte/translations/pl.json index 57768de8fc1..c36b37b5252 100644 --- a/homeassistant/components/huawei_lte/translations/pl.json +++ b/homeassistant/components/huawei_lte/translations/pl.json @@ -19,11 +19,11 @@ "step": { "user": { "data": { - "password": "[%key_id:common::config_flow::data::password%]", + "password": "Has\u0142o", "url": "URL", - "username": "[%key_id:common::config_flow::data::username%]" + "username": "Nazwa u\u017cytkownika" }, - "description": "Wprowad\u017a szczeg\u00f3\u0142y dost\u0119pu do urz\u0105dzenia. Okre\u015blenie nazwy u\u017cytkownika i has\u0142a jest opcjonalne, ale umo\u017cliwia obs\u0142ug\u0119 wi\u0119kszej liczby funkcji integracji. Z drugiej strony u\u017cycie autoryzowanego po\u0142\u0105czenia mo\u017ce powodowa\u0107 problemy z dost\u0119pem do interfejsu internetowego urz\u0105dzenia z zewn\u0105trz Home Assistant'a gdy integracja jest aktywna.", + "description": "Wprowad\u017a szczeg\u00f3\u0142y dost\u0119pu do urz\u0105dzenia. Okre\u015blenie nazwy u\u017cytkownika i has\u0142a jest opcjonalne, ale umo\u017cliwia obs\u0142ug\u0119 wi\u0119kszej liczby funkcji integracji. Z drugiej strony u\u017cycie autoryzowanego po\u0142\u0105czenia mo\u017ce powodowa\u0107 problemy z dost\u0119pem do interfejsu internetowego urz\u0105dzenia z zewn\u0105trz Home Assistanta gdy integracja jest aktywna.", "title": "Konfiguracja Huawei LTE" } } diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index c9d543dba94..c8d7de55ce8 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -296,18 +296,29 @@ class HueLight(LightEntity): @property def min_mireds(self): """Return the coldest color_temp that this light supports.""" - if self.is_group or "ct" not in self.light.controlcapabilities: + if self.is_group: return super().min_mireds - return self.light.controlcapabilities["ct"]["min"] + min_mireds = self.light.controlcapabilities.get("ct", {}).get("min") + + # We filter out '0' too, which can be incorrectly reported by 3rd party buls + if not min_mireds: + return super().min_mireds + + return min_mireds @property def max_mireds(self): """Return the warmest color_temp that this light supports.""" - if self.is_group or "ct" not in self.light.controlcapabilities: + if self.is_group: return super().max_mireds - return self.light.controlcapabilities["ct"]["max"] + max_mireds = self.light.controlcapabilities.get("ct", {}).get("max") + + if not max_mireds: + return super().max_mireds + + return max_mireds @property def is_on(self): diff --git a/homeassistant/components/hue/translations/ko.json b/homeassistant/components/hue/translations/ko.json index 62a466565d9..3f4b3fad8d0 100644 --- a/homeassistant/components/hue/translations/ko.json +++ b/homeassistant/components/hue/translations/ko.json @@ -44,8 +44,8 @@ "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc774 \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \uc190\uc744 \ub5c4 \ub54c", "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub20c\ub9b4 \ub54c", "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc5d0\uc11c \uc190\uc744 \ub5c4 \ub54c", - "remote_double_button_long_press": "\"{subtype}\" \ubaa8\ub450 \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \uc190\uc744 \ub5c4 \ub54c", - "remote_double_button_short_press": "\"{subtype}\" \ubaa8\ub450 \uc190\uc744 \ub5c4 \ub54c" + "remote_double_button_long_press": "\"{subtype}\"\uc5d0\uc11c \ubaa8\ub450 \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \uc190\uc744 \ub5c4 \ub54c", + "remote_double_button_short_press": "\"{subtype}\"\uc5d0\uc11c \ubaa8\ub450 \uc190\uc744 \ub5c4 \ub54c" } } } \ No newline at end of file diff --git a/homeassistant/components/hue/translations/pl.json b/homeassistant/components/hue/translations/pl.json index acd2161a024..46df7ce48e8 100644 --- a/homeassistant/components/hue/translations/pl.json +++ b/homeassistant/components/hue/translations/pl.json @@ -6,9 +6,9 @@ "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", + "no_bridges": "Nie wykryto mostk\u00f3w Hue.", "not_hue_bridge": "To nie jest mostek Hue", - "unknown": "[%key_id:common::config_flow::error::unknown%]" + "unknown": "Nieoczekiwany b\u0142\u0105d." }, "error": { "linking": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d w trakcie \u0142\u0105czenia.", @@ -17,7 +17,7 @@ "step": { "init": { "data": { - "host": "[%key_id:common::config_flow::data::host%]" + "host": "Nazwa hosta lub adres IP" }, "title": "Wybierz mostek Hue" }, @@ -35,17 +35,17 @@ "button_4": "czwarty przycisk", "dim_down": "nast\u0105pi zmniejszenie jasno\u015bci", "dim_up": "nast\u0105pi zwi\u0119kszenie jasno\u015bci", - "double_buttons_1_3": "Przyciski pierwszy i trzeci", - "double_buttons_2_4": "Przyciski drugi i czwarty", - "turn_off": "Nast\u0105pi wy\u0142\u0105czenie", - "turn_on": "Nast\u0105pi w\u0142\u0105czenie" + "double_buttons_1_3": "przyciski pierwszy i trzeci", + "double_buttons_2_4": "przyciski drugi i czwarty", + "turn_off": "wy\u0142\u0105cznik", + "turn_on": "w\u0142\u0105cznik" }, "trigger_type": { "remote_button_long_release": "\"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", "remote_button_short_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty", "remote_button_short_release": "\"{subtype}\" zostanie zwolniony", - "remote_double_button_long_press": "Obydwa \"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", - "remote_double_button_short_press": "Obydwa \"{subtype}\" zostanie zwolniony" + "remote_double_button_long_press": "oba \"{subtype}\" zostan\u0105 zwolnione po d\u0142ugim naci\u015bni\u0119ciu", + "remote_double_button_short_press": "oba \"{subtype}\" zostan\u0105 zwolnione" } } } \ No newline at end of file diff --git a/homeassistant/components/hue/translations/pt-BR.json b/homeassistant/components/hue/translations/pt-BR.json index 251ee74c9de..9a7e8094b11 100644 --- a/homeassistant/components/hue/translations/pt-BR.json +++ b/homeassistant/components/hue/translations/pt-BR.json @@ -26,5 +26,11 @@ "title": "Hub de links" } } + }, + "device_automation": { + "trigger_subtype": { + "double_buttons_1_3": "Primeiro e terceiro bot\u00f5es", + "double_buttons_2_4": "Segundo e quarto bot\u00f5es" + } } } \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/__init__.py b/homeassistant/components/hunterdouglas_powerview/__init__.py index df06060fb75..3df895c94ce 100644 --- a/homeassistant/components/hunterdouglas_powerview/__init__.py +++ b/homeassistant/components/hunterdouglas_powerview/__init__.py @@ -56,6 +56,9 @@ from .const import ( USER_DATA, ) +PARALLEL_UPDATES = 1 + + DEVICE_SCHEMA = vol.Schema( {DOMAIN: vol.Schema({vol.Required(CONF_HOST): cv.string})}, extra=vol.ALLOW_EXTRA ) @@ -130,7 +133,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async with async_timeout.timeout(10): shade_entries = await shades.get_resources() if not shade_entries: - raise UpdateFailed(f"Failed to fetch new shade data.") + raise UpdateFailed("Failed to fetch new shade data.") return _async_map_data_by_id(shade_entries[SHADE_DATA]) coordinator = DataUpdateCoordinator( diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index ea56d56352a..871413b9f5b 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -45,6 +45,8 @@ _LOGGER = logging.getLogger(__name__) # from one state to another TRANSITION_COMPLETE_DURATION = 30 +PARALLEL_UPDATES = 1 + async def async_setup_entry(hass, entry, async_add_entities): """Set up the hunter douglas shades.""" diff --git a/homeassistant/components/hunterdouglas_powerview/translations/hu.json b/homeassistant/components/hunterdouglas_powerview/translations/hu.json index baa3b135d42..61461d1796c 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/hu.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/hu.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "host": "IP-c\u00edm" + "host": "IP c\u00edm" } } } diff --git a/homeassistant/components/hunterdouglas_powerview/translations/pl.json b/homeassistant/components/hunterdouglas_powerview/translations/pl.json index 774007fc86e..cad41869ced 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/pl.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/pl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "unknown": "[%key_id:common::config_flow::error::unknown%]" + "unknown": "Nieoczekiwany b\u0142\u0105d." }, "step": { "link": { diff --git a/homeassistant/components/iaqualink/manifest.json b/homeassistant/components/iaqualink/manifest.json index 68b4554f73a..d0d9b7ed7f2 100644 --- a/homeassistant/components/iaqualink/manifest.json +++ b/homeassistant/components/iaqualink/manifest.json @@ -4,5 +4,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iaqualink/", "codeowners": ["@flz"], - "requirements": ["iaqualink==0.3.3"] + "requirements": ["iaqualink==0.3.4"] } diff --git a/homeassistant/components/iaqualink/translations/ca.json b/homeassistant/components/iaqualink/translations/ca.json index b86dd4358b0..196f4970f93 100644 --- a/homeassistant/components/iaqualink/translations/ca.json +++ b/homeassistant/components/iaqualink/translations/ca.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "Contrasenya", - "username": "Nom d'usuari / Correu electr\u00f2nic" + "username": "Nom d'usuari" }, "description": "Introdueix el nom d'usuari i la contrasenya del teu compte d'iAqualink.", "title": "Connexi\u00f3 amb iAqualink" diff --git a/homeassistant/components/iaqualink/translations/hu.json b/homeassistant/components/iaqualink/translations/hu.json index b0b9393acde..dee4ed9ee0f 100644 --- a/homeassistant/components/iaqualink/translations/hu.json +++ b/homeassistant/components/iaqualink/translations/hu.json @@ -4,7 +4,7 @@ "user": { "data": { "password": "Jelsz\u00f3", - "username": "Felhaszn\u00e1l\u00f3n\u00e9v / e-mail c\u00edm" + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" } } } diff --git a/homeassistant/components/iaqualink/translations/it.json b/homeassistant/components/iaqualink/translations/it.json index 471b21cdc9e..568f91961f2 100644 --- a/homeassistant/components/iaqualink/translations/it.json +++ b/homeassistant/components/iaqualink/translations/it.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "Password", - "username": "Nome Utente / Indirizzo E-mail" + "username": "Nome utente" }, "description": "Inserisci il nome utente e la password del tuo account iAqualink.", "title": "Collegati a iAqualink" diff --git a/homeassistant/components/iaqualink/translations/pl.json b/homeassistant/components/iaqualink/translations/pl.json index a247cadf1a4..f0582b633f1 100644 --- a/homeassistant/components/iaqualink/translations/pl.json +++ b/homeassistant/components/iaqualink/translations/pl.json @@ -9,11 +9,11 @@ "step": { "user": { "data": { - "password": "[%key_id:common::config_flow::data::password%]", - "username": "[%key_id:common::config_flow::data::username%]/adres e-mail" + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" }, "description": "Wprowad\u017a nazw\u0119 u\u017cytkownika i has\u0142o do konta iAqualink.", - "title": "Po\u0142\u0105cz z iAqualink" + "title": "Po\u0142\u0105czenie z iAqualink" } } } diff --git a/homeassistant/components/icloud/translations/de.json b/homeassistant/components/icloud/translations/de.json index d09889453e0..5857c3b34f0 100644 --- a/homeassistant/components/icloud/translations/de.json +++ b/homeassistant/components/icloud/translations/de.json @@ -20,6 +20,7 @@ "user": { "data": { "password": "Passwort", + "username": "Email", "with_family": "Mit Familie" }, "description": "Gib deine Zugangsdaten ein", diff --git a/homeassistant/components/icloud/translations/es.json b/homeassistant/components/icloud/translations/es.json index c2cbaf175f9..49bd9d612eb 100644 --- a/homeassistant/components/icloud/translations/es.json +++ b/homeassistant/components/icloud/translations/es.json @@ -20,6 +20,7 @@ "user": { "data": { "password": "Contrase\u00f1a", + "username": "Correo electr\u00f3nico", "with_family": "Con la familia" }, "description": "Ingrese sus credenciales", diff --git a/homeassistant/components/icloud/translations/hu.json b/homeassistant/components/icloud/translations/hu.json index 194b92eb2fc..4dcf547619c 100644 --- a/homeassistant/components/icloud/translations/hu.json +++ b/homeassistant/components/icloud/translations/hu.json @@ -15,7 +15,8 @@ }, "user": { "data": { - "password": "Jelsz\u00f3" + "password": "Jelsz\u00f3", + "username": "E-mail" }, "description": "Adja meg hiteles\u00edt\u0151 adatait", "title": "iCloud hiteles\u00edt\u0151 adatok" diff --git a/homeassistant/components/icloud/translations/it.json b/homeassistant/components/icloud/translations/it.json index 80cdfed7ace..27429081e1e 100644 --- a/homeassistant/components/icloud/translations/it.json +++ b/homeassistant/components/icloud/translations/it.json @@ -20,6 +20,7 @@ "user": { "data": { "password": "Password", + "username": "E-mail", "with_family": "Con la famiglia" }, "description": "Inserisci le tue credenziali", diff --git a/homeassistant/components/icloud/translations/lb.json b/homeassistant/components/icloud/translations/lb.json index 9e923d6ffd1..b6e9abe94bc 100644 --- a/homeassistant/components/icloud/translations/lb.json +++ b/homeassistant/components/icloud/translations/lb.json @@ -20,6 +20,7 @@ "user": { "data": { "password": "Passwuert", + "username": "E-Mail", "with_family": "Mat der Famill" }, "description": "F\u00ebllt \u00e4r Umeldungs Informatiounen aus", diff --git a/homeassistant/components/icloud/translations/pl.json b/homeassistant/components/icloud/translations/pl.json index 0bccf920554..20e3f8c2fb4 100644 --- a/homeassistant/components/icloud/translations/pl.json +++ b/homeassistant/components/icloud/translations/pl.json @@ -19,8 +19,8 @@ }, "user": { "data": { - "password": "[%key_id:common::config_flow::data::password%]", - "username": "[%key_id:common::config_flow::data::email%]", + "password": "Has\u0142o", + "username": "Adres e-mail", "with_family": "Z rodzin\u0105" }, "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce", diff --git a/homeassistant/components/icloud/translations/pt-BR.json b/homeassistant/components/icloud/translations/pt-BR.json index 364c0aca85c..219930a94fd 100644 --- a/homeassistant/components/icloud/translations/pt-BR.json +++ b/homeassistant/components/icloud/translations/pt-BR.json @@ -18,7 +18,8 @@ }, "user": { "data": { - "password": "Senha" + "password": "Senha", + "username": "E-mail" }, "description": "Insira suas credenciais", "title": "credenciais do iCloud" diff --git a/homeassistant/components/ifttt/translations/fi.json b/homeassistant/components/ifttt/translations/fi.json index 0570e4a1a66..b01accbb371 100644 --- a/homeassistant/components/ifttt/translations/fi.json +++ b/homeassistant/components/ifttt/translations/fi.json @@ -2,6 +2,11 @@ "config": { "abort": { "one_instance_allowed": "Vain yksi instanssi on tarpeen." + }, + "step": { + "user": { + "description": "Haluatko varmasti m\u00e4\u00e4ritt\u00e4\u00e4 IFTTT:n?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/ifttt/translations/it.json b/homeassistant/components/ifttt/translations/it.json index ce037f1c5c3..1989efd733c 100644 --- a/homeassistant/components/ifttt/translations/it.json +++ b/homeassistant/components/ifttt/translations/it.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u00c8 necessaria una sola istanza." }, "create_entry": { - "default": "Per inviare eventi a Home Assistant, dovrai utilizzare l'azione \"Esegui una richiesta web\" dall'applet [Weblet di IFTTT] ( {applet_url} ). \n\n Compila le seguenti informazioni: \n\n - URL: ` {webhook_url} ` \n - Metodo: POST \n - Tipo di contenuto: application / json \n\n Vedi [la documentazione] ( {docs_url} ) su come configurare le automazioni per gestire i dati in arrivo." + "default": "Per inviare eventi a Home Assistant, \u00e8 necessario utilizzare l'azione \"Crea una richiesta web\" dall'[applet IFTTT Webhook]({applet_url}). \n\n Compilare le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Metodo: POST \n - Tipo di contenuto: application/json \n\nVedere [la documentazione]({docs_url}) su come configurare le automazioni per gestire i dati in arrivo." }, "step": { "user": { diff --git a/homeassistant/components/ifttt/translations/pl.json b/homeassistant/components/ifttt/translations/pl.json index 47bf2c29a16..d35b30cbf8f 100644 --- a/homeassistant/components/ifttt/translations/pl.json +++ b/homeassistant/components/ifttt/translations/pl.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Wymagana jest tylko jedna instancja." }, "create_entry": { - "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant'a, musisz u\u017cy\u0107 akcji \"Make a web request\" z [IFTTT Webhook apletu]({applet_url}). \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}`\n - Metoda: POST\n - Typ zawarto\u015bci: application/json\n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}) na temat konfiguracji automatyzacji, by obs\u0142u\u017cy\u0107 przychodz\u0105ce dane." + "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistanta, musisz u\u017cy\u0107 akcji \"Make a web request\" z [IFTTT Webhook apletu]({applet_url}). \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}`\n - Metoda: POST\n - Typ zawarto\u015bci: application/json\n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}) na temat konfiguracji automatyzacji, by obs\u0142u\u017cy\u0107 przychodz\u0105ce dane." }, "step": { "user": { diff --git a/homeassistant/components/input_boolean/translations/ca.json b/homeassistant/components/input_boolean/translations/ca.json index 0ef459d9bb5..23600285d58 100644 --- a/homeassistant/components/input_boolean/translations/ca.json +++ b/homeassistant/components/input_boolean/translations/ca.json @@ -1,8 +1,8 @@ { "state": { "_": { - "off": "Desactivat", - "on": "Activat" + "off": "OFF", + "on": "ON" } }, "title": "Entrada booleana" diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index ce17cc6c77d..c28c04f589d 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -1,7 +1,8 @@ """Support for INSTEON Modems (PLM and Hub).""" +import asyncio import logging -import insteonplm +from pyinsteon import async_close, async_connect, devices from homeassistant.const import ( CONF_HOST, @@ -24,21 +25,75 @@ from .const import ( CONF_SUBCAT, CONF_UNITCODE, CONF_X10, - CONF_X10_ALL_LIGHTS_OFF, - CONF_X10_ALL_LIGHTS_ON, - CONF_X10_ALL_UNITS_OFF, DOMAIN, - INSTEON_ENTITIES, + INSTEON_COMPONENTS, + ON_OFF_EVENTS, ) from .schemas import CONFIG_SCHEMA # noqa F440 -from .utils import async_register_services, register_new_device_callback +from .utils import ( + add_on_off_event_device, + async_register_services, + get_device_platforms, + register_new_device_callback, +) _LOGGER = logging.getLogger(__name__) +async def async_id_unknown_devices(config_dir): + """Send device ID commands to all unidentified devices.""" + await devices.async_load(id_devices=1) + for addr in devices: + device = devices[addr] + flags = True + for name in device.operating_flags: + if not device.operating_flags[name].is_loaded: + flags = False + break + if flags: + for name in device.properties: + if not device.properties[name].is_loaded: + flags = False + break + + # Cannot be done concurrently due to issues with the underlying protocol. + if not device.aldb.is_loaded or not flags: + await device.async_read_config() + + await devices.async_save(workdir=config_dir) + + +async def async_setup_platforms(hass, config): + """Initiate the connection and services.""" + tasks = [ + hass.helpers.discovery.async_load_platform(component, DOMAIN, {}, config) + for component in INSTEON_COMPONENTS + ] + await asyncio.gather(*tasks) + + for address in devices: + device = devices[address] + platforms = get_device_platforms(device) + if ON_OFF_EVENTS in platforms: + add_on_off_event_device(hass, device) + + _LOGGER.debug("Insteon device count: %s", len(devices)) + register_new_device_callback(hass, config) + async_register_services(hass) + + # Cannot be done concurrently due to issues with the underlying protocol. + for address in devices: + await devices[address].async_status() + await async_id_unknown_devices(hass.config.config_dir) + + +async def close_insteon_connection(*args): + """Close the Insteon connection.""" + await async_close() + + async def async_setup(hass, config): """Set up the connection to the modem.""" - insteon_modem = None conf = config[DOMAIN] port = conf.get(CONF_PORT) @@ -47,68 +102,50 @@ async def async_setup(hass, config): username = conf.get(CONF_HUB_USERNAME) password = conf.get(CONF_HUB_PASSWORD) hub_version = conf.get(CONF_HUB_VERSION) - overrides = conf.get(CONF_OVERRIDE, []) - x10_devices = conf.get(CONF_X10, []) - x10_all_units_off_housecode = conf.get(CONF_X10_ALL_UNITS_OFF) - x10_all_lights_on_housecode = conf.get(CONF_X10_ALL_LIGHTS_ON) - x10_all_lights_off_housecode = conf.get(CONF_X10_ALL_LIGHTS_OFF) if host: - _LOGGER.info("Connecting to Insteon Hub on %s", host) - conn = await insteonplm.Connection.create( + _LOGGER.info("Connecting to Insteon Hub on %s:%d", host, ip_port) + else: + _LOGGER.info("Connecting to Insteon PLM on %s", port) + + try: + await async_connect( + device=port, host=host, port=ip_port, username=username, password=password, hub_version=hub_version, - loop=hass.loop, - workdir=hass.config.config_dir, - ) - else: - _LOGGER.info("Looking for Insteon PLM on %s", port) - conn = await insteonplm.Connection.create( - device=port, loop=hass.loop, workdir=hass.config.config_dir ) + except ConnectionError: + _LOGGER.error("Could not connect to Insteon modem") + return False + _LOGGER.info("Connection to Insteon modem successful") - insteon_modem = conn.protocol + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_insteon_connection) + conf = config[DOMAIN] + overrides = conf.get(CONF_OVERRIDE, []) + x10_devices = conf.get(CONF_X10, []) - 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) + await devices.async_load( + workdir=hass.config.config_dir, id_devices=0, load_modem_aldb=0 + ) for device_override in overrides: - # # Override the device default capabilities for a specific address - # address = device_override.get("address") - for prop in device_override: - if prop in [CONF_CAT, CONF_SUBCAT]: - insteon_modem.devices.add_override(address, prop, device_override[prop]) - elif prop in [CONF_FIRMWARE, CONF_PRODUCT_KEY]: - insteon_modem.devices.add_override( - address, CONF_PRODUCT_KEY, device_override[prop] - ) + if not devices.get(address): + cat = device_override[CONF_CAT] + subcat = device_override[CONF_SUBCAT] + firmware = device_override.get(CONF_FIRMWARE) + if firmware is None: + firmware = device_override.get(CONF_PRODUCT_KEY, 0) + devices.set_id(address, cat, subcat, firmware) - if x10_all_units_off_housecode: - device = insteon_modem.add_x10_device( - x10_all_units_off_housecode, 20, "allunitsoff" - ) - if x10_all_lights_on_housecode: - device = insteon_modem.add_x10_device( - x10_all_lights_on_housecode, 21, "alllightson" - ) - if x10_all_lights_off_housecode: - device = insteon_modem.add_x10_device( - x10_all_lights_off_housecode, 22, "alllightsoff" - ) for device in x10_devices: housecode = device.get(CONF_HOUSECODE) unitcode = device.get(CONF_UNITCODE) - x10_type = "onoff" + x10_type = "on_off" steps = device.get(CONF_DIM_STEPS, 22) if device.get(CONF_PLATFORM) == "light": x10_type = "dimmable" @@ -117,8 +154,7 @@ async def async_setup(hass, config): _LOGGER.debug( "Adding X10 device to Insteon: %s %d %s", housecode, unitcode, x10_type ) - device = insteon_modem.add_x10_device(housecode, unitcode, x10_type) - if device and hasattr(device.states[0x01], "steps"): - device.states[0x01].steps = steps + device = devices.add_x10_device(housecode, unitcode, x10_type, steps) + asyncio.create_task(async_setup_platforms(hass, config)) return True diff --git a/homeassistant/components/insteon/binary_sensor.py b/homeassistant/components/insteon/binary_sensor.py index 81c3c58ef12..cd74f738187 100644 --- a/homeassistant/components/insteon/binary_sensor.py +++ b/homeassistant/components/insteon/binary_sensor.py @@ -1,50 +1,69 @@ """Support for INSTEON dimmers via PowerLinc Modem.""" import logging -from homeassistant.components.binary_sensor import BinarySensorEntity +from pyinsteon.groups import ( + CO_SENSOR, + DOOR_SENSOR, + HEARTBEAT, + LEAK_SENSOR_WET, + LIGHT_SENSOR, + LOW_BATTERY, + MOTION_SENSOR, + OPEN_CLOSE_SENSOR, + SENSOR_MALFUNCTION, + SMOKE_SENSOR, + TEST_SENSOR, +) + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_GAS, + DEVICE_CLASS_LIGHT, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_OPENING, + DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_SAFETY, + DEVICE_CLASS_SMOKE, + DOMAIN, + BinarySensorEntity, +) from .insteon_entity import InsteonEntity +from .utils import async_add_insteon_entities _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { - "openClosedSensor": "opening", - "ioLincSensor": "opening", - "motionSensor": "motion", - "doorSensor": "door", - "wetLeakSensor": "moisture", - "lightSensor": "light", - "batterySensor": "battery", + OPEN_CLOSE_SENSOR: DEVICE_CLASS_OPENING, + MOTION_SENSOR: DEVICE_CLASS_MOTION, + DOOR_SENSOR: DEVICE_CLASS_DOOR, + LEAK_SENSOR_WET: DEVICE_CLASS_MOISTURE, + LIGHT_SENSOR: DEVICE_CLASS_LIGHT, + LOW_BATTERY: DEVICE_CLASS_BATTERY, + CO_SENSOR: DEVICE_CLASS_GAS, + SMOKE_SENSOR: DEVICE_CLASS_SMOKE, + TEST_SENSOR: DEVICE_CLASS_SAFETY, + SENSOR_MALFUNCTION: DEVICE_CLASS_PROBLEM, + HEARTBEAT: DEVICE_CLASS_PROBLEM, } async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the INSTEON device class for the hass platform.""" - insteon_modem = hass.data["insteon"].get("modem") - - address = discovery_info["address"] - device = insteon_modem.devices[address] - state_key = discovery_info["state_key"] - name = device.states[state_key].name - if name != "dryLeakSensor": - _LOGGER.debug( - "Adding device %s entity %s to Binary Sensor platform", - device.address.hex, - name, - ) - - new_entity = InsteonBinarySensor(device, state_key) - - async_add_entities([new_entity]) + """Set up the INSTEON entity class for the hass platform.""" + async_add_insteon_entities( + hass, DOMAIN, InsteonBinarySensorEntity, async_add_entities, discovery_info + ) -class InsteonBinarySensor(InsteonEntity, BinarySensorEntity): - """A Class for an Insteon device entity.""" +class InsteonBinarySensorEntity(InsteonEntity, BinarySensorEntity): + """A Class for an Insteon binary sensor entity.""" - def __init__(self, device, state_key): + def __init__(self, device, group): """Initialize the INSTEON binary sensor.""" - super().__init__(device, state_key) - self._sensor_type = SENSOR_TYPES.get(self._insteon_device_state.name) + super().__init__(device, group) + self._sensor_type = SENSOR_TYPES.get(self._insteon_device_group.name) @property def device_class(self): @@ -54,9 +73,4 @@ class InsteonBinarySensor(InsteonEntity, BinarySensorEntity): @property def is_on(self): """Return the boolean response if the node is on.""" - on_val = bool(self._insteon_device_state.value) - - if self._insteon_device_state.name in ["lightSensor", "ioLincSensor"]: - return not on_val - - return on_val + return bool(self._insteon_device_group.value) diff --git a/homeassistant/components/insteon/climate.py b/homeassistant/components/insteon/climate.py new file mode 100644 index 00000000000..79af0892f94 --- /dev/null +++ b/homeassistant/components/insteon/climate.py @@ -0,0 +1,228 @@ +"""Support for Insteon thermostat.""" +import logging +from typing import List, Optional + +from pyinsteon.constants import ThermostatMode +from pyinsteon.operating_flag import CELSIUS + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + CURRENT_HVAC_COOL, + CURRENT_HVAC_FAN, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + DOMAIN, + HVAC_MODE_AUTO, + HVAC_MODE_COOL, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + SUPPORT_FAN_MODE, + SUPPORT_TARGET_HUMIDITY, + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, +) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT + +from .insteon_entity import InsteonEntity +from .utils import async_add_insteon_entities + +_LOGGER = logging.getLogger(__name__) + +COOLING = 1 +HEATING = 2 +DEHUMIDIFYING = 3 +HUMIDIFYING = 4 + +TEMPERATURE = 10 +HUMIDITY = 11 +SYSTEM_MODE = 12 +FAN_MODE = 13 +COOL_SET_POINT = 14 +HEAT_SET_POINT = 15 +HUMIDITY_HIGH = 16 +HUMIDITY_LOW = 17 + + +HVAC_MODES = { + 0: HVAC_MODE_OFF, + 1: HVAC_MODE_HEAT, + 2: HVAC_MODE_COOL, + 3: HVAC_MODE_HEAT_COOL, +} +FAN_MODES = {4: HVAC_MODE_AUTO, 8: HVAC_MODE_FAN_ONLY} +SUPPORTED_FEATURES = ( + SUPPORT_FAN_MODE + | SUPPORT_TARGET_HUMIDITY + | SUPPORT_TARGET_TEMPERATURE + | SUPPORT_TARGET_TEMPERATURE_RANGE +) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Insteon platform.""" + async_add_insteon_entities( + hass, DOMAIN, InsteonClimateEntity, async_add_entities, discovery_info + ) + + +class InsteonClimateEntity(InsteonEntity, ClimateEntity): + """A Class for an Insteon climate entity.""" + + @property + def supported_features(self): + """Return the supported features for this entity.""" + return SUPPORTED_FEATURES + + @property + def temperature_unit(self) -> str: + """Return the unit of measurement.""" + if self._insteon_device.properties[CELSIUS].value: + return TEMP_CELSIUS + return TEMP_FAHRENHEIT + + @property + def current_humidity(self) -> Optional[int]: + """Return the current humidity.""" + return self._insteon_device.groups[HUMIDITY].value + + @property + def hvac_mode(self) -> str: + """Return hvac operation ie. heat, cool mode.""" + return HVAC_MODES[self._insteon_device.groups[SYSTEM_MODE].value] + + @property + def hvac_modes(self) -> List[str]: + """Return the list of available hvac operation modes.""" + return list(HVAC_MODES.values()) + + @property + def current_temperature(self) -> Optional[float]: + """Return the current temperature.""" + return self._insteon_device.groups[TEMPERATURE].value + + @property + def target_temperature(self) -> Optional[float]: + """Return the temperature we try to reach.""" + if self._insteon_device.groups[SYSTEM_MODE].value == ThermostatMode.HEAT: + return self._insteon_device.groups[HEAT_SET_POINT].value + if self._insteon_device.groups[SYSTEM_MODE].value == ThermostatMode.COOL: + return self._insteon_device.groups[COOL_SET_POINT].value + return None + + @property + def target_temperature_high(self) -> Optional[float]: + """Return the highbound target temperature we try to reach.""" + if self._insteon_device.groups[SYSTEM_MODE].value == ThermostatMode.AUTO: + return self._insteon_device.groups[COOL_SET_POINT].value + return None + + @property + def target_temperature_low(self) -> Optional[float]: + """Return the lowbound target temperature we try to reach.""" + if self._insteon_device.groups[SYSTEM_MODE].value == ThermostatMode.AUTO: + return self._insteon_device.groups[HEAT_SET_POINT].value + return None + + @property + def fan_mode(self) -> Optional[str]: + """Return the fan setting.""" + return FAN_MODES[self._insteon_device.groups[FAN_MODE].value] + + @property + def fan_modes(self) -> Optional[List[str]]: + """Return the list of available fan modes.""" + return list(FAN_MODES.values()) + + @property + def target_humidity(self) -> Optional[int]: + """Return the humidity we try to reach.""" + high = self._insteon_device.groups[HUMIDITY_HIGH].value + low = self._insteon_device.groups[HUMIDITY_LOW].value + # May not be loaded yet so return a default if required + return (high + low) / 2 if high and low else None + + @property + def min_humidity(self) -> int: + """Return the minimum humidity.""" + return 1 + + @property + def hvac_action(self) -> Optional[str]: + """Return the current running hvac operation if supported. + + Need to be one of CURRENT_HVAC_*. + """ + if self._insteon_device.groups[COOLING].value: + return CURRENT_HVAC_COOL + if self._insteon_device.groups[HEATING].value: + return CURRENT_HVAC_HEAT + if self._insteon_device.groups[FAN_MODE].value == ThermostatMode.FAN_ALWAYS_ON: + return CURRENT_HVAC_FAN + return CURRENT_HVAC_IDLE + + @property + def device_state_attributes(self): + """Provide attributes for display on device card.""" + attr = super().device_state_attributes + humidifier = "off" + if self._insteon_device.groups[DEHUMIDIFYING].value: + humidifier = "dehumidifying" + if self._insteon_device.groups[HUMIDIFYING].value: + humidifier = "humidifying" + attr["humidifier"] = humidifier + return attr + + async def async_set_temperature(self, **kwargs) -> None: + """Set new target temperature.""" + target_temp = kwargs.get(ATTR_TEMPERATURE) + target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) + target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) + if target_temp is not None: + if self._insteon_device.groups[SYSTEM_MODE].value == ThermostatMode.HEAT: + await self._insteon_device.async_set_heat_set_point(target_temp) + elif self._insteon_device.groups[SYSTEM_MODE].value == ThermostatMode.COOL: + await self._insteon_device.async_set_cool_set_point(target_temp) + else: + await self._insteon_device.async_set_heat_set_point(target_temp_low) + await self._insteon_device.async_set_cool_set_point(target_temp_high) + + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set new target fan mode.""" + mode = list(FAN_MODES.keys())[list(FAN_MODES.values()).index(fan_mode)] + await self._insteon_device.async_set_mode(mode) + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set new target hvac mode.""" + mode = list(HVAC_MODES.keys())[list(HVAC_MODES.values()).index(hvac_mode)] + await self._insteon_device.async_set_mode(mode) + + async def async_set_humidity(self, humidity): + """Set new humidity level.""" + change = humidity - self.target_humidity + high = self._insteon_device.groups[HUMIDITY_HIGH].value + change + low = self._insteon_device.groups[HUMIDITY_LOW].value + change + await self._insteon_device.async_set_humidity_low_set_point(low) + await self._insteon_device.async_set_humidity_high_set_point(high) + + async def async_added_to_hass(self): + """Register INSTEON update events.""" + await super().async_added_to_hass() + await self._insteon_device.async_read_op_flags() + for group in [ + COOLING, + HEATING, + DEHUMIDIFYING, + HUMIDIFYING, + HEAT_SET_POINT, + FAN_MODE, + SYSTEM_MODE, + TEMPERATURE, + HUMIDITY, + HUMIDITY_HIGH, + HUMIDITY_LOW, + ]: + self._insteon_device.groups[group].subscribe(self.async_entity_update) diff --git a/homeassistant/components/insteon/const.py b/homeassistant/components/insteon/const.py index b01409f49ff..c55d733b73d 100644 --- a/homeassistant/components/insteon/const.py +++ b/homeassistant/components/insteon/const.py @@ -1,7 +1,47 @@ """Constants used by insteon component.""" +from pyinsteon.groups import ( + CO_SENSOR, + COVER, + DIMMABLE_FAN, + DIMMABLE_LIGHT, + DIMMABLE_LIGHT_MAIN, + DIMMABLE_OUTLET, + DOOR_SENSOR, + HEARTBEAT, + LEAK_SENSOR_WET, + LIGHT_SENSOR, + LOW_BATTERY, + MOTION_SENSOR, + NEW_SENSOR, + ON_OFF_OUTLET_BOTTOM, + ON_OFF_OUTLET_TOP, + ON_OFF_SWITCH, + ON_OFF_SWITCH_A, + ON_OFF_SWITCH_B, + ON_OFF_SWITCH_C, + ON_OFF_SWITCH_D, + ON_OFF_SWITCH_E, + ON_OFF_SWITCH_F, + ON_OFF_SWITCH_G, + ON_OFF_SWITCH_H, + ON_OFF_SWITCH_MAIN, + OPEN_CLOSE_SENSOR, + RELAY, + SENSOR_MALFUNCTION, + SMOKE_SENSOR, + TEST_SENSOR, +) DOMAIN = "insteon" -INSTEON_ENTITIES = "entities" + +INSTEON_COMPONENTS = [ + "binary_sensor", + "climate", + "cover", + "fan", + "light", + "switch", +] CONF_IP_PORT = "ip_port" CONF_HUB_USERNAME = "username" @@ -37,9 +77,12 @@ SRV_RESPONDER = "responder" SRV_HOUSECODE = "housecode" SRV_SCENE_ON = "scene_on" SRV_SCENE_OFF = "scene_off" +SRV_ADD_DEFAULT_LINKS = "add_default_links" SIGNAL_LOAD_ALDB = "load_aldb" SIGNAL_PRINT_ALDB = "print_aldb" +SIGNAL_SAVE_DEVICES = "save_devices" +SIGNAL_ADD_DEFAULT_LINKS = "add_default_links" HOUSECODES = [ "a", @@ -60,47 +103,42 @@ HOUSECODES = [ "p", ] -BUTTON_PRESSED_STATE_NAME = "onLevelButton" -EVENT_BUTTON_ON = "insteon.button_on" -EVENT_BUTTON_OFF = "insteon.button_off" +EVENT_GROUP_ON = "insteon.button_on" +EVENT_GROUP_OFF = "insteon.button_off" +EVENT_GROUP_ON_FAST = "insteon.button_on_fast" +EVENT_GROUP_OFF_FAST = "insteon.button_off_fast" EVENT_CONF_BUTTON = "button" - +ON_OFF_EVENTS = "on_off_events" 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", + DIMMABLE_LIGHT_MAIN: "Main", + ON_OFF_SWITCH_A: "Button A", + ON_OFF_SWITCH_B: "Button B", + ON_OFF_SWITCH_C: "Button C", + ON_OFF_SWITCH_D: "Button D", + ON_OFF_SWITCH_E: "Button E", + ON_OFF_SWITCH_F: "Button F", + ON_OFF_SWITCH_G: "Button G", + ON_OFF_SWITCH_H: "Button H", + ON_OFF_SWITCH_MAIN: "Main", + DIMMABLE_FAN: "Fan", + DIMMABLE_LIGHT: "Light", + DIMMABLE_OUTLET: "Outlet", + MOTION_SENSOR: "Motion", + LIGHT_SENSOR: "Light", + LOW_BATTERY: "Battery", + LEAK_SENSOR_WET: "Wet", + DOOR_SENSOR: "Door", + SMOKE_SENSOR: "Smoke", + CO_SENSOR: "Carbon Monoxide", + TEST_SENSOR: "Test", + NEW_SENSOR: "New", + SENSOR_MALFUNCTION: "Malfunction", + HEARTBEAT: "Heartbeat", + OPEN_CLOSE_SENSOR: "Sensor", + ON_OFF_SWITCH: "Light", + ON_OFF_OUTLET_TOP: "Top", + ON_OFF_OUTLET_BOTTOM: "Bottom", + COVER: "Cover", + RELAY: "Relay", } diff --git a/homeassistant/components/insteon/cover.py b/homeassistant/components/insteon/cover.py index b325a6ebd84..4f6fbd80dce 100644 --- a/homeassistant/components/insteon/cover.py +++ b/homeassistant/components/insteon/cover.py @@ -4,6 +4,7 @@ import math from homeassistant.components.cover import ( ATTR_POSITION, + DOMAIN, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, @@ -11,6 +12,7 @@ from homeassistant.components.cover import ( ) from .insteon_entity import InsteonEntity +from .utils import async_add_insteon_entities _LOGGER = logging.getLogger(__name__) @@ -19,33 +21,22 @@ SUPPORTED_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Insteon platform.""" - if not discovery_info: - return - - insteon_modem = hass.data["insteon"].get("modem") - - address = discovery_info["address"] - device = insteon_modem.devices[address] - state_key = discovery_info["state_key"] - - _LOGGER.debug( - "Adding device %s entity %s to Cover platform", - device.address.hex, - device.states[state_key].name, + async_add_insteon_entities( + hass, DOMAIN, InsteonCoverEntity, async_add_entities, discovery_info ) - new_entity = InsteonCoverEntity(device, state_key) - - async_add_entities([new_entity]) - class InsteonCoverEntity(InsteonEntity, CoverEntity): - """A Class for an Insteon device.""" + """A Class for an Insteon cover entity.""" @property def current_cover_position(self): """Return the current cover position.""" - return int(math.ceil(self._insteon_device_state.value * 100 / 255)) + if self._insteon_device_group.value is not None: + pos = self._insteon_device_group.value + else: + pos = 0 + return int(math.ceil(pos * 100 / 255)) @property def supported_features(self): @@ -58,17 +49,19 @@ class InsteonCoverEntity(InsteonEntity, CoverEntity): return bool(self.current_cover_position) async def async_open_cover(self, **kwargs): - """Open device.""" - self._insteon_device_state.open() + """Open cover.""" + await self._insteon_device.async_open() async def async_close_cover(self, **kwargs): - """Close device.""" - self._insteon_device_state.close() + """Close cover.""" + await self._insteon_device.async_close() async def async_set_cover_position(self, **kwargs): """Set the cover position.""" position = int(kwargs[ATTR_POSITION] * 255 / 100) if position == 0: - self._insteon_device_state.close() + await self._insteon_device.async_close() else: - self._insteon_device_state.set_position(position) + await self._insteon_device.async_open( + open_level=position, group=self._insteon_device_group.group + ) diff --git a/homeassistant/components/insteon/fan.py b/homeassistant/components/insteon/fan.py index 6ad7436faf5..3b324b97782 100644 --- a/homeassistant/components/insteon/fan.py +++ b/homeassistant/components/insteon/fan.py @@ -1,7 +1,10 @@ """Support for INSTEON fans via PowerLinc Modem.""" import logging +from pyinsteon.constants import FanSpeed + from homeassistant.components.fan import ( + DOMAIN, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, @@ -9,43 +12,40 @@ from homeassistant.components.fan import ( SUPPORT_SET_SPEED, FanEntity, ) -from homeassistant.const import STATE_OFF from .insteon_entity import InsteonEntity +from .utils import async_add_insteon_entities _LOGGER = logging.getLogger(__name__) - -SPEED_TO_HEX = {SPEED_OFF: 0x00, SPEED_LOW: 0x3F, SPEED_MEDIUM: 0xBE, SPEED_HIGH: 0xFF} - -FAN_SPEEDS = [STATE_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] +FAN_SPEEDS = [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] +SPEED_TO_VALUE = { + SPEED_OFF: FanSpeed.OFF, + SPEED_LOW: FanSpeed.LOW, + SPEED_MEDIUM: FanSpeed.MEDIUM, + SPEED_HIGH: FanSpeed.HIGH, +} async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the INSTEON device class for the hass platform.""" - insteon_modem = hass.data["insteon"].get("modem") - - address = discovery_info["address"] - device = insteon_modem.devices[address] - state_key = discovery_info["state_key"] - - _LOGGER.debug( - "Adding device %s entity %s to Fan platform", - device.address.hex, - device.states[state_key].name, + """Set up the INSTEON entity class for the hass platform.""" + async_add_insteon_entities( + hass, DOMAIN, InsteonFanEntity, async_add_entities, discovery_info ) - new_entity = InsteonFan(device, state_key) - async_add_entities([new_entity]) - - -class InsteonFan(InsteonEntity, FanEntity): - """An INSTEON fan component.""" +class InsteonFanEntity(InsteonEntity, FanEntity): + """An INSTEON fan entity.""" @property def speed(self) -> str: """Return the current speed.""" - return self._hex_to_speed(self._insteon_device_state.value) + if self._insteon_device_group.value == FanSpeed.HIGH: + return SPEED_HIGH + if self._insteon_device_group.value == FanSpeed.MEDIUM: + return SPEED_MEDIUM + if self._insteon_device_group.value == FanSpeed.LOW: + return SPEED_LOW + return SPEED_OFF @property def speed_list(self) -> list: @@ -58,30 +58,19 @@ class InsteonFan(InsteonEntity, FanEntity): return SUPPORT_SET_SPEED async def async_turn_on(self, speed: str = None, **kwargs) -> None: - """Turn on the entity.""" + """Turn on the fan.""" if speed is None: speed = SPEED_MEDIUM await self.async_set_speed(speed) async def async_turn_off(self, **kwargs) -> None: - """Turn off the entity.""" - await self.async_set_speed(SPEED_OFF) + """Turn off the fan.""" + await self._insteon_device.async_fan_off() async def async_set_speed(self, speed: str) -> None: """Set the speed of the fan.""" - fan_speed = SPEED_TO_HEX[speed] - if fan_speed == 0x00: - self._insteon_device_state.off() + fan_speed = SPEED_TO_VALUE[speed] + if fan_speed == FanSpeed.OFF: + await self._insteon_device.async_fan_off() else: - self._insteon_device_state.set_level(fan_speed) - - @staticmethod - def _hex_to_speed(speed: int): - hex_speed = SPEED_OFF - if speed > 0xFE: - hex_speed = SPEED_HIGH - elif speed > 0x7F: - hex_speed = SPEED_MEDIUM - elif speed > 0: - hex_speed = SPEED_LOW - return hex_speed + await self._insteon_device.async_fan_on(on_level=fan_speed) diff --git a/homeassistant/components/insteon/insteon_entity.py b/homeassistant/components/insteon/insteon_entity.py index b453cad2e07..787c64ec841 100644 --- a/homeassistant/components/insteon/insteon_entity.py +++ b/homeassistant/components/insteon/insteon_entity.py @@ -2,14 +2,17 @@ import logging from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity import Entity from .const import ( - DOMAIN, - INSTEON_ENTITIES, + SIGNAL_ADD_DEFAULT_LINKS, SIGNAL_LOAD_ALDB, SIGNAL_PRINT_ALDB, + SIGNAL_SAVE_DEVICES, STATE_NAME_LABEL_MAP, ) from .utils import print_aldb_to_log @@ -20,11 +23,14 @@ _LOGGER = logging.getLogger(__name__) class InsteonEntity(Entity): """INSTEON abstract base entity.""" - def __init__(self, device, state_key): + def __init__(self, device, group): """Initialize the INSTEON binary sensor.""" - self._insteon_device_state = device.states[state_key] + self._insteon_device_group = device.groups[group] self._insteon_device = device - self._insteon_device.aldb.add_loaded_callback(self._aldb_loaded) + + def __hash__(self): + """Return the hash of the Insteon Entity.""" + return hash(self._insteon_device) @property def should_poll(self): @@ -34,20 +40,20 @@ class InsteonEntity(Entity): @property def address(self): """Return the address of the node.""" - return self._insteon_device.address.human + return str(self._insteon_device.address) @property def group(self): """Return the INSTEON group that the entity responds to.""" - return self._insteon_device_state.group + return self._insteon_device_group.group @property def unique_id(self) -> str: """Return a unique ID.""" - if self._insteon_device_state.group == 0x01: + if self._insteon_device_group.group == 0x01: uid = self._insteon_device.id else: - uid = f"{self._insteon_device.id}_{self._insteon_device_state.group}" + uid = f"{self._insteon_device.id}_{self._insteon_device_group.group}" return uid @property @@ -61,7 +67,7 @@ class InsteonEntity(Entity): extension = self._get_label() if extension: extension = f" {extension}" - return f"{description} {self._insteon_device.address.human}{extension}" + return f"{description} {self._insteon_device.address}{extension}" @property def device_state_attributes(self): @@ -69,56 +75,53 @@ class InsteonEntity(Entity): return {"insteon_address": self.address, "insteon_group": self.group} @callback - def async_entity_update(self, deviceid, group, val): + def async_entity_update(self, name, address, value, group): """Receive notification from transport that new data exists.""" _LOGGER.debug( - "Received update for device %s group %d value %s", - deviceid.human, - group, - val, + "Received update for device %s group %d value %s", address, group, value, ) self.async_write_ha_state() async def async_added_to_hass(self): """Register INSTEON update events.""" _LOGGER.debug( - "Tracking updates for device %s group %d statename %s", + "Tracking updates for device %s group %d name %s", self.address, self.group, - self._insteon_device_state.name, + self._insteon_device_group.name, ) - self._insteon_device_state.register_updates(self.async_entity_update) - self.hass.data[DOMAIN][INSTEON_ENTITIES].add(self.entity_id) + self._insteon_device_group.subscribe(self.async_entity_update) load_signal = f"{self.entity_id}_{SIGNAL_LOAD_ALDB}" self.async_on_remove( - async_dispatcher_connect(self.hass, load_signal, self._load_aldb) + async_dispatcher_connect(self.hass, load_signal, self._async_read_aldb) ) print_signal = f"{self.entity_id}_{SIGNAL_PRINT_ALDB}" - self.async_on_remove( - async_dispatcher_connect(self.hass, print_signal, self._print_aldb) + async_dispatcher_connect(self.hass, print_signal, self._print_aldb) + default_links_signal = f"{self.entity_id}_{SIGNAL_ADD_DEFAULT_LINKS}" + async_dispatcher_connect( + self.hass, default_links_signal, self._async_add_default_links ) - def _load_aldb(self, reload=False): - """Load the device All-Link Database.""" - if reload: - self._insteon_device.aldb.clear() - self._insteon_device.read_aldb() + async def _async_read_aldb(self, reload): + """Call device load process and print to log.""" + await self._insteon_device.aldb.async_load(refresh=reload) + self._print_aldb() + async_dispatcher_send(self.hass, SIGNAL_SAVE_DEVICES) 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] + if len(self._insteon_device.groups) > 1: + if self._insteon_device_group.name in STATE_NAME_LABEL_MAP: + label = STATE_NAME_LABEL_MAP[self._insteon_device_group.name] else: label = f"Group {self.group:d}" return label + + async def _async_add_default_links(self): + """Add default links between the device and the modem.""" + await self._insteon_device.async_add_default_links(self.address) diff --git a/homeassistant/components/insteon/ipdb.py b/homeassistant/components/insteon/ipdb.py index 6aba40d6df9..a3e79fcd6d4 100644 --- a/homeassistant/components/insteon/ipdb.py +++ b/homeassistant/components/insteon/ipdb.py @@ -1,81 +1,117 @@ -"""Insteon product database.""" -import collections +"""Utility methods for the Insteon platform.""" +import logging -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, +from pyinsteon.device_types import ( + ClimateControl_Thermostat, + ClimateControl_WirelessThermostat, + DimmableLightingControl, + DimmableLightingControl_DinRail, + DimmableLightingControl_FanLinc, + DimmableLightingControl_InLineLinc, + DimmableLightingControl_KeypadLinc_6, + DimmableLightingControl_KeypadLinc_8, + DimmableLightingControl_LampLinc, + DimmableLightingControl_OutletLinc, + DimmableLightingControl_SwitchLinc, + DimmableLightingControl_ToggleLinc, + GeneralController_ControlLinc, + GeneralController_MiniRemote_4, + GeneralController_MiniRemote_8, + GeneralController_MiniRemote_Switch, + GeneralController_RemoteLinc, + SecurityHealthSafety_DoorSensor, + SecurityHealthSafety_LeakSensor, + SecurityHealthSafety_MotionSensor, + SecurityHealthSafety_OpenCloseSensor, + SecurityHealthSafety_Smokebridge, + SensorsActuators_IOLink, + SwitchedLightingControl, + SwitchedLightingControl_ApplianceLinc, + SwitchedLightingControl_DinRail, + SwitchedLightingControl_InLineLinc, + SwitchedLightingControl_KeypadLinc_6, + SwitchedLightingControl_KeypadLinc_8, + SwitchedLightingControl_OnOffOutlet, + SwitchedLightingControl_OutletLinc, + SwitchedLightingControl_SwitchLinc, + SwitchedLightingControl_ToggleLinc, + WindowCovering, + X10Dimmable, + X10OnOff, X10OnOffSensor, - X10OnOffSwitch, ) -State = collections.namedtuple("Product", "stateType platform") +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR +from homeassistant.components.climate import DOMAIN as CLIMATE +from homeassistant.components.cover import DOMAIN as COVER +from homeassistant.components.fan import DOMAIN as FAN +from homeassistant.components.light import DOMAIN as LIGHT +from homeassistant.components.switch import DOMAIN as SWITCH + +from .const import ON_OFF_EVENTS + +_LOGGER = logging.getLogger(__name__) + +DEVICE_PLATFORM = { + DimmableLightingControl: {LIGHT: [1], ON_OFF_EVENTS: [1]}, + DimmableLightingControl_DinRail: {LIGHT: [1], ON_OFF_EVENTS: [1]}, + DimmableLightingControl_FanLinc: {LIGHT: [1], FAN: [2], ON_OFF_EVENTS: [1, 2]}, + DimmableLightingControl_InLineLinc: {LIGHT: [1], ON_OFF_EVENTS: [1]}, + DimmableLightingControl_KeypadLinc_6: { + LIGHT: [1], + SWITCH: [3, 4, 5, 6], + ON_OFF_EVENTS: [1, 3, 4, 5, 6], + }, + DimmableLightingControl_KeypadLinc_8: { + LIGHT: [1], + SWITCH: range(2, 9), + ON_OFF_EVENTS: range(1, 9), + }, + DimmableLightingControl_LampLinc: {LIGHT: [1], ON_OFF_EVENTS: [1]}, + DimmableLightingControl_OutletLinc: {LIGHT: [1], ON_OFF_EVENTS: [1]}, + DimmableLightingControl_SwitchLinc: {LIGHT: [1], ON_OFF_EVENTS: [1]}, + DimmableLightingControl_ToggleLinc: {LIGHT: [1], ON_OFF_EVENTS: [1]}, + GeneralController_ControlLinc: {ON_OFF_EVENTS: [1]}, + GeneralController_MiniRemote_4: {ON_OFF_EVENTS: range(1, 5)}, + GeneralController_MiniRemote_8: {ON_OFF_EVENTS: range(1, 9)}, + GeneralController_MiniRemote_Switch: {ON_OFF_EVENTS: [1, 2]}, + GeneralController_RemoteLinc: {ON_OFF_EVENTS: [1]}, + SecurityHealthSafety_DoorSensor: {BINARY_SENSOR: [1, 3, 4], ON_OFF_EVENTS: [1]}, + SecurityHealthSafety_LeakSensor: {BINARY_SENSOR: [2, 4]}, + SecurityHealthSafety_MotionSensor: {BINARY_SENSOR: [1, 2, 3], ON_OFF_EVENTS: [1]}, + SecurityHealthSafety_OpenCloseSensor: {BINARY_SENSOR: [1]}, + SecurityHealthSafety_Smokebridge: {BINARY_SENSOR: [1, 2, 3, 4, 6, 7]}, + SensorsActuators_IOLink: {SWITCH: [1], BINARY_SENSOR: [2], ON_OFF_EVENTS: [1, 2]}, + SwitchedLightingControl: {SWITCH: [1], ON_OFF_EVENTS: [1]}, + SwitchedLightingControl_ApplianceLinc: {SWITCH: [1], ON_OFF_EVENTS: [1]}, + SwitchedLightingControl_DinRail: {SWITCH: [1], ON_OFF_EVENTS: [1]}, + SwitchedLightingControl_InLineLinc: {SWITCH: [1], ON_OFF_EVENTS: [1]}, + SwitchedLightingControl_KeypadLinc_6: { + SWITCH: [1, 3, 4, 5, 6], + ON_OFF_EVENTS: [1, 3, 4, 5, 6], + }, + SwitchedLightingControl_KeypadLinc_8: { + SWITCH: range(1, 9), + ON_OFF_EVENTS: range(1, 9), + }, + SwitchedLightingControl_OnOffOutlet: {SWITCH: [1, 2], ON_OFF_EVENTS: [1, 2]}, + SwitchedLightingControl_OutletLinc: {SWITCH: [1], ON_OFF_EVENTS: [1]}, + SwitchedLightingControl_SwitchLinc: {SWITCH: [1], ON_OFF_EVENTS: [1]}, + SwitchedLightingControl_ToggleLinc: {SWITCH: [1], ON_OFF_EVENTS: [1]}, + ClimateControl_Thermostat: {CLIMATE: [1]}, + ClimateControl_WirelessThermostat: {CLIMATE: [1]}, + WindowCovering: {COVER: [1]}, + X10Dimmable: {LIGHT: [1]}, + X10OnOff: {SWITCH: [1]}, + X10OnOffSensor: {BINARY_SENSOR: [1]}, +} -class IPDB: - """Embodies the INSTEON Product Database static data and access methods.""" +def get_device_platforms(device): + """Return the HA platforms for a device type.""" + return DEVICE_PLATFORM.get(type(device), {}).keys() - 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.""" - yield from self.states - - 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 +def get_platform_groups(device, domain) -> dict: + """Return the platforms that a device belongs in.""" + return DEVICE_PLATFORM.get(type(device), {}).get(domain, {}) diff --git a/homeassistant/components/insteon/light.py b/homeassistant/components/insteon/light.py index afd575c363b..5ad02b6da5e 100644 --- a/homeassistant/components/insteon/light.py +++ b/homeassistant/components/insteon/light.py @@ -3,11 +3,13 @@ import logging from homeassistant.components.light import ( ATTR_BRIGHTNESS, + DOMAIN, SUPPORT_BRIGHTNESS, LightEntity, ) from .insteon_entity import InsteonEntity +from .utils import async_add_insteon_entities _LOGGER = logging.getLogger(__name__) @@ -16,31 +18,18 @@ MAX_BRIGHTNESS = 255 async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Insteon component.""" - insteon_modem = hass.data["insteon"].get("modem") - - address = discovery_info["address"] - device = insteon_modem.devices[address] - state_key = discovery_info["state_key"] - - _LOGGER.debug( - "Adding device %s entity %s to Light platform", - device.address.hex, - device.states[state_key].name, + async_add_insteon_entities( + hass, DOMAIN, InsteonDimmerEntity, async_add_entities, discovery_info ) - new_entity = InsteonDimmerDevice(device, state_key) - async_add_entities([new_entity]) - - -class InsteonDimmerDevice(InsteonEntity, LightEntity): - """A Class for an Insteon device.""" +class InsteonDimmerEntity(InsteonEntity, LightEntity): + """A Class for an Insteon light entity.""" @property def brightness(self): """Return the brightness of this light between 0..255.""" - onlevel = self._insteon_device_state.value - return int(onlevel) + return self._insteon_device_group.value @property def is_on(self): @@ -53,13 +42,15 @@ class InsteonDimmerDevice(InsteonEntity, LightEntity): return SUPPORT_BRIGHTNESS async def async_turn_on(self, **kwargs): - """Turn device on.""" + """Turn light on.""" if ATTR_BRIGHTNESS in kwargs: brightness = int(kwargs[ATTR_BRIGHTNESS]) - self._insteon_device_state.set_level(brightness) + await self._insteon_device.async_on( + on_level=brightness, group=self._insteon_device_group.group + ) else: - self._insteon_device_state.on() + await self._insteon_device.async_on(group=self._insteon_device_group.group) async def async_turn_off(self, **kwargs): - """Turn device off.""" - self._insteon_device_state.off() + """Turn light off.""" + await self._insteon_device.async_off(self._insteon_device_group.group) diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index 8410c6b6ef4..63c258d5f58 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -2,6 +2,6 @@ "domain": "insteon", "name": "Insteon", "documentation": "https://www.home-assistant.io/integrations/insteon", - "requirements": ["insteonplm==0.16.8"], - "codeowners": [] -} + "requirements": ["pyinsteon==1.0.3"], + "codeowners": ["@teharris1"] +} \ No newline at end of file diff --git a/homeassistant/components/insteon/schemas.py b/homeassistant/components/insteon/schemas.py index e3f2644ac56..0fe8f30d95b 100644 --- a/homeassistant/components/insteon/schemas.py +++ b/homeassistant/components/insteon/schemas.py @@ -11,7 +11,6 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_PORT, ENTITY_MATCH_ALL, - ENTITY_MATCH_NONE, ) import homeassistant.helpers.config_validation as cv @@ -57,7 +56,6 @@ def set_default_port(schema: Dict) -> Dict: CONF_DEVICE_OVERRIDE_SCHEMA = vol.All( - cv.deprecated(CONF_PLATFORM), vol.Schema( { vol.Required(CONF_ADDRESS): cv.string, @@ -86,6 +84,9 @@ CONF_X10_SCHEMA = vol.All( CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( + cv.deprecated(CONF_X10_ALL_UNITS_OFF), + cv.deprecated(CONF_X10_ALL_LIGHTS_ON), + cv.deprecated(CONF_X10_ALL_LIGHTS_OFF), vol.Schema( { vol.Exclusive( @@ -101,9 +102,6 @@ CONFIG_SCHEMA = vol.Schema( 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] ), @@ -134,9 +132,7 @@ DEL_ALL_LINK_SCHEMA = vol.Schema( LOAD_ALDB_SCHEMA = vol.Schema( { - vol.Required(CONF_ENTITY_ID): vol.Any( - cv.entity_id, ENTITY_MATCH_ALL, ENTITY_MATCH_NONE - ), + vol.Required(CONF_ENTITY_ID): vol.Any(cv.entity_id, ENTITY_MATCH_ALL), vol.Optional(SRV_LOAD_DB_RELOAD, default=False): cv.boolean, } ) @@ -151,3 +147,6 @@ X10_HOUSECODE_SCHEMA = vol.Schema({vol.Required(SRV_HOUSECODE): vol.In(HOUSECODE TRIGGER_SCENE_SCHEMA = vol.Schema( {vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255)} ) + + +ADD_DEFAULT_LINKS_SCHEMA = vol.Schema({vol.Required(CONF_ENTITY_ID): cv.entity_id}) diff --git a/homeassistant/components/insteon/sensor.py b/homeassistant/components/insteon/sensor.py deleted file mode 100644 index 475723b105d..00000000000 --- a/homeassistant/components/insteon/sensor.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Support for INSTEON dimmers via PowerLinc Modem.""" -import logging - -from homeassistant.helpers.entity import Entity - -from .insteon_entity import InsteonEntity - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the INSTEON device class for the hass platform.""" - insteon_modem = hass.data["insteon"].get("modem") - - address = discovery_info["address"] - device = insteon_modem.devices[address] - state_key = discovery_info["state_key"] - - _LOGGER.debug( - "Adding device %s entity %s to Sensor platform", - device.address.hex, - device.states[state_key].name, - ) - - new_entity = InsteonSensorDevice(device, state_key) - - async_add_entities([new_entity]) - - -class InsteonSensorDevice(InsteonEntity, Entity): - """A Class for an Insteon device.""" diff --git a/homeassistant/components/insteon/services.yaml b/homeassistant/components/insteon/services.yaml index 3d232569e9c..716b9a1e040 100644 --- a/homeassistant/components/insteon/services.yaml +++ b/homeassistant/components/insteon/services.yaml @@ -60,3 +60,9 @@ scene_off: group: description: INSTEON group or scene number example: 26 +add_default_links: + description: Add the default links between the device and the Insteon Modem (IM) + fields: + entity_id: + description: Name of the device to load. Use "all" to load the database of all devices. + example: "light.1a2b3c" diff --git a/homeassistant/components/insteon/switch.py b/homeassistant/components/insteon/switch.py index 3a0668459c9..9d4e12b0b46 100644 --- a/homeassistant/components/insteon/switch.py +++ b/homeassistant/components/insteon/switch.py @@ -1,66 +1,33 @@ """Support for INSTEON dimmers via PowerLinc Modem.""" import logging -from homeassistant.components.switch import SwitchEntity +from homeassistant.components.switch import DOMAIN, SwitchEntity from .insteon_entity import InsteonEntity +from .utils import async_add_insteon_entities _LOGGER = logging.getLogger(__name__) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the INSTEON device class for the hass platform.""" - insteon_modem = hass.data["insteon"].get("modem") - - address = discovery_info["address"] - device = insteon_modem.devices[address] - state_key = discovery_info["state_key"] - - state_name = device.states[state_key].name - - _LOGGER.debug( - "Adding device %s entity %s to Switch platform", device.address.hex, state_name, + """Set up the INSTEON entity class for the hass platform.""" + async_add_insteon_entities( + hass, DOMAIN, InsteonSwitchEntity, async_add_entities, discovery_info ) - new_entity = None - if state_name == "openClosedRelay": - new_entity = InsteonOpenClosedDevice(device, state_key) - else: - new_entity = InsteonSwitchDevice(device, state_key) - if new_entity is not None: - async_add_entities([new_entity]) - - -class InsteonSwitchDevice(InsteonEntity, SwitchEntity): - """A Class for an Insteon device.""" +class InsteonSwitchEntity(InsteonEntity, SwitchEntity): + """A Class for an Insteon switch entity.""" @property def is_on(self): """Return the boolean response if the node is on.""" - return bool(self._insteon_device_state.value) + return bool(self._insteon_device_group.value) async def async_turn_on(self, **kwargs): - """Turn device on.""" - self._insteon_device_state.on() + """Turn switch on.""" + await self._insteon_device.async_on(group=self._insteon_device_group.group) async def async_turn_off(self, **kwargs): - """Turn device off.""" - self._insteon_device_state.off() - - -class InsteonOpenClosedDevice(InsteonEntity, SwitchEntity): - """A Class for an Insteon device.""" - - @property - def is_on(self): - """Return the boolean response if the node is on.""" - return bool(self._insteon_device_state.value) - - async def async_turn_on(self, **kwargs): - """Turn device on.""" - self._insteon_device_state.open() - - async def async_turn_off(self, **kwargs): - """Turn device off.""" - self._insteon_device_state.close() + """Turn switch off.""" + await self._insteon_device.async_off(group=self._insteon_device_group.group) diff --git a/homeassistant/components/insteon/utils.py b/homeassistant/components/insteon/utils.py index 26768936291..32a0949dfeb 100644 --- a/homeassistant/components/insteon/utils.py +++ b/homeassistant/components/insteon/utils.py @@ -1,24 +1,47 @@ """Utilities used by insteon component.""" - +import asyncio import logging -from insteonplm.devices import ALDBStatus +from pyinsteon import devices +from pyinsteon.constants import ALDBStatus +from pyinsteon.events import OFF_EVENT, OFF_FAST_EVENT, ON_EVENT, ON_FAST_EVENT +from pyinsteon.managers.link_manager import ( + async_enter_linking_mode, + async_enter_unlinking_mode, +) +from pyinsteon.managers.scene_manager import ( + async_trigger_scene_off, + async_trigger_scene_on, +) +from pyinsteon.managers.x10_manager import ( + async_x10_all_lights_off, + async_x10_all_lights_on, + async_x10_all_units_off, +) 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 homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, + dispatcher_send, +) from .const import ( - BUTTON_PRESSED_STATE_NAME, DOMAIN, - EVENT_BUTTON_OFF, - EVENT_BUTTON_ON, EVENT_CONF_BUTTON, - INSTEON_ENTITIES, + EVENT_GROUP_OFF, + EVENT_GROUP_OFF_FAST, + EVENT_GROUP_ON, + EVENT_GROUP_ON_FAST, + ON_OFF_EVENTS, + SIGNAL_ADD_DEFAULT_LINKS, SIGNAL_LOAD_ALDB, SIGNAL_PRINT_ALDB, + SIGNAL_SAVE_DEVICES, SRV_ADD_ALL_LINK, + SRV_ADD_DEFAULT_LINKS, SRV_ALL_LINK_GROUP, SRV_ALL_LINK_MODE, SRV_CONTROLLER, @@ -34,9 +57,10 @@ from .const import ( SRV_X10_ALL_LIGHTS_ON, SRV_X10_ALL_UNITS_OFF, ) -from .ipdb import IPDB +from .ipdb import get_device_platforms, get_platform_groups from .schemas import ( ADD_ALL_LINK_SCHEMA, + ADD_DEFAULT_LINKS_SCHEMA, DEL_ALL_LINK_SCHEMA, LOAD_ALDB_SCHEMA, PRINT_ALDB_SCHEMA, @@ -47,91 +71,129 @@ from .schemas import ( _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 - event = EVENT_BUTTON_ON if val else EVENT_BUTTON_OFF - _LOGGER.debug( - "Firing event %s with address %s and button %s", event, address.hex, button - ) - hass.bus.fire(event, schema) +def add_on_off_event_device(hass, device): + """Register an Insteon device as an on/off event device.""" @callback - def async_new_insteon_device(device): + def async_fire_group_on_off_event(name, address, group, button): + # Firing an event when a button is pressed. + if button and button[-2] == "_": + button_id = button[-1].lower() + else: + button_id = None + + schema = {CONF_ADDRESS: address} + if button_id: + schema[EVENT_CONF_BUTTON] = button_id + if name == ON_EVENT: + event = EVENT_GROUP_ON + if name == OFF_EVENT: + event = EVENT_GROUP_OFF + if name == ON_FAST_EVENT: + event = EVENT_GROUP_ON_FAST + if name == OFF_FAST_EVENT: + event = EVENT_GROUP_OFF_FAST + _LOGGER.debug("Firing event %s with %s", event, schema) + hass.bus.async_fire(event, schema) + + for group in device.events: + if isinstance(group, int): + for event in device.events[group]: + if event in [ + OFF_EVENT, + ON_EVENT, + OFF_FAST_EVENT, + ON_FAST_EVENT, + ]: + _LOGGER.debug( + "Registering on/off event for %s %d %s", + str(device.address), + group, + event, + ) + device.events[group][event].subscribe( + async_fire_group_on_off_event, force_strong_ref=True + ) + + +def register_new_device_callback(hass, config): + """Register callback for new Insteon device.""" + new_device_lock = asyncio.Lock() + + @callback + def async_new_insteon_device(address=None): """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 + hass.async_create_task(async_create_new_entities(address)) - if platform == "on_off_events": - device.states[state_key].register_updates(_fire_button_on_off_event) + async def async_create_new_entities(address): + _LOGGER.debug( + "Adding new INSTEON device to Home Assistant with address %s", address + ) + async with new_device_lock: + await devices.async_save(workdir=hass.config.config_dir) + device = devices[address] + await device.async_status() + platforms = get_device_platforms(device) + tasks = [] + for platform in platforms: + if platform == ON_OFF_EVENTS: + add_on_off_event_device(hass, device) - else: - _LOGGER.info( - "New INSTEON device: %s (%s) %s", - device.address, - device.states[state_key].name, + else: + tasks.append( + discovery.async_load_platform( + hass, platform, + DOMAIN, + discovered={"address": device.address.id}, + hass_config=config, ) + ) + await asyncio.gather(*tasks) - 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) + devices.subscribe(async_new_insteon_device, force_strong_ref=True) @callback -def async_register_services(hass, config, insteon_modem): +def async_register_services(hass): """Register services used by insteon component.""" - def add_all_link(service): + async def async_srv_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) + link_mode = mode.lower() == SRV_CONTROLLER + await async_enter_linking_mode(link_mode, group) - def del_all_link(service): + async def async_srv_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) + await async_enter_unlinking_mode(group) - def load_aldb(service): + async def async_srv_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) + await async_srv_load_aldb_all(reload) else: - _send_load_aldb_signal(entity_id, reload) + signal = f"{entity_id}_{SIGNAL_LOAD_ALDB}" + async_dispatcher_send(hass, signal, 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) + async def async_srv_load_aldb_all(reload): + """Load the All-Link database for all devices.""" + # Cannot be done concurrently due to issues with the underlying protocol. + for address in devices: + device = devices[address] + if device != devices.modem and device.cat != 0x03: + await device.aldb.async_load( + refresh=reload, callback=async_srv_save_devices + ) + + async def async_srv_save_devices(): + """Write the Insteon device configuration to file.""" + _LOGGER.debug("Saving Insteon devices") + await devices.async_save(hass.config.config_dir) def print_aldb(service): """Print the All-Link Database for a device.""" @@ -145,71 +207,99 @@ def async_register_services(hass, config, insteon_modem): """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) + print_aldb_to_log(devices.modem.aldb) - def x10_all_units_off(service): + async def async_srv_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) + await async_x10_all_units_off(housecode) - def x10_all_lights_off(service): + async def async_srv_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) + await async_x10_all_lights_off(housecode) - def x10_all_lights_on(service): + async def async_srv_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) + await async_x10_all_lights_on(housecode) - def scene_on(service): + async def async_srv_scene_on(service): """Trigger an INSTEON scene ON.""" group = service.data.get(SRV_ALL_LINK_GROUP) - insteon_modem.trigger_group_on(group) + await async_trigger_scene_on(group) - def scene_off(service): + async def async_srv_scene_off(service): """Trigger an INSTEON scene ON.""" group = service.data.get(SRV_ALL_LINK_GROUP) - insteon_modem.trigger_group_off(group) + await async_trigger_scene_off(group) + + @callback + def async_add_default_links(service): + """Add the default All-Link entries to a device.""" + entity_id = service.data[CONF_ENTITY_ID] + signal = f"{entity_id}_{SIGNAL_ADD_DEFAULT_LINKS}" + async_dispatcher_send(hass, signal) hass.services.async_register( - DOMAIN, SRV_ADD_ALL_LINK, add_all_link, schema=ADD_ALL_LINK_SCHEMA + DOMAIN, SRV_ADD_ALL_LINK, async_srv_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 + DOMAIN, SRV_DEL_ALL_LINK, async_srv_del_all_link, schema=DEL_ALL_LINK_SCHEMA ) hass.services.async_register( - DOMAIN, SRV_LOAD_ALDB, load_aldb, schema=LOAD_ALDB_SCHEMA + DOMAIN, SRV_LOAD_ALDB, async_srv_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 + DOMAIN, + SRV_X10_ALL_UNITS_OFF, + async_srv_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 + DOMAIN, + SRV_X10_ALL_LIGHTS_OFF, + async_srv_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 + DOMAIN, + SRV_X10_ALL_LIGHTS_ON, + async_srv_x10_all_lights_on, + schema=X10_HOUSECODE_SCHEMA, ) hass.services.async_register( - DOMAIN, SRV_SCENE_ON, scene_on, schema=TRIGGER_SCENE_SCHEMA + DOMAIN, SRV_SCENE_ON, async_srv_scene_on, schema=TRIGGER_SCENE_SCHEMA ) hass.services.async_register( - DOMAIN, SRV_SCENE_OFF, scene_off, schema=TRIGGER_SCENE_SCHEMA + DOMAIN, SRV_SCENE_OFF, async_srv_scene_off, schema=TRIGGER_SCENE_SCHEMA ) + + hass.services.async_register( + DOMAIN, + SRV_ADD_DEFAULT_LINKS, + async_add_default_links, + schema=ADD_DEFAULT_LINKS_SCHEMA, + ) + async_dispatcher_connect(hass, SIGNAL_SAVE_DEVICES, async_srv_save_devices) _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) + # This service is useless if the log level is not INFO for the + # insteon component. Setting the log level to INFO and resetting it + # back when we are done + orig_log_level = _LOGGER.level + if orig_log_level > logging.INFO: + _LOGGER.setLevel(logging.INFO) + _LOGGER.info("%s ALDB load status is %s", aldb.address, 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.warning("All-Link database not loaded") _LOGGER.info("RecID In Use Mode HWM Group Address Data 1 Data 2 Data 3") _LOGGER.info("----- ------ ---- --- ----- -------- ------ ------ ------") @@ -217,12 +307,30 @@ def print_aldb_to_log(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" + in_use = "Y" if rec.is_in_use else "N" + mode = "C" if rec.is_controller else "R" + hwm = "Y" if rec.is_high_water_mark else "N" log_msg = ( f" {rec.mem_addr:04x} {in_use:s} {mode:s} {hwm:s} " - f"{rec.group:3d} {rec.address.human:s} {rec.data1:3d} " + f"{rec.group:3d} {str(rec.target):s} {rec.data1:3d} " f"{rec.data2:3d} {rec.data3:3d}" ) _LOGGER.info(log_msg) + _LOGGER.setLevel(orig_log_level) + + +@callback +def async_add_insteon_entities( + hass, platform, entity_type, async_add_entities, discovery_info +): + """Add Insteon devices to a platform.""" + new_entities = [] + device_list = [discovery_info.get("address")] if discovery_info else devices + + for address in device_list: + device = devices[address] + groups = get_platform_groups(device, platform) + for group in groups: + new_entities.append(entity_type(device, group)) + if new_entities: + async_add_entities(new_entities) diff --git a/homeassistant/components/ios/translations/bg.json b/homeassistant/components/ios/translations/bg.json index 69e1523ac1f..3160ff48272 100644 --- a/homeassistant/components/ios/translations/bg.json +++ b/homeassistant/components/ios/translations/bg.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Home Assistant iOS \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430?", - "title": "Home Assistant iOS" + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Home Assistant iOS \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430?" } } } diff --git a/homeassistant/components/ios/translations/ca.json b/homeassistant/components/ios/translations/ca.json index 3f16dbc2204..4b90303b435 100644 --- a/homeassistant/components/ios/translations/ca.json +++ b/homeassistant/components/ios/translations/ca.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "Vols configurar el component Home Assistant iOS?", - "title": "Home Assistant iOS" + "description": "Vols configurar el component Home Assistant iOS?" } } } diff --git a/homeassistant/components/ios/translations/cs.json b/homeassistant/components/ios/translations/cs.json index afb0307492d..a215b9fe13c 100644 --- a/homeassistant/components/ios/translations/cs.json +++ b/homeassistant/components/ios/translations/cs.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "Chcete nastavit komponenty Home Assistant iOS?", - "title": "Home Assistant iOS" + "description": "Chcete nastavit komponenty Home Assistant iOS?" } } } diff --git a/homeassistant/components/ios/translations/da.json b/homeassistant/components/ios/translations/da.json index 37db62d1205..046874653da 100644 --- a/homeassistant/components/ios/translations/da.json +++ b/homeassistant/components/ios/translations/da.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "Er du sikker p\u00e5 at du vil konfigurere Home Assistant iOS?", - "title": "Home Assistant iOS" + "description": "Er du sikker p\u00e5 at du vil konfigurere Home Assistant iOS?" } } } diff --git a/homeassistant/components/ios/translations/de.json b/homeassistant/components/ios/translations/de.json index 337ed3ad7c0..e9e592d18c2 100644 --- a/homeassistant/components/ios/translations/de.json +++ b/homeassistant/components/ios/translations/de.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "M\u00f6chtest du die Home Assistant iOS-Komponente einrichten?", - "title": "Home Assistant iOS" + "description": "M\u00f6chtest du die Home Assistant iOS-Komponente einrichten?" } } } diff --git a/homeassistant/components/ios/translations/en.json b/homeassistant/components/ios/translations/en.json index 6352142e717..46857921642 100644 --- a/homeassistant/components/ios/translations/en.json +++ b/homeassistant/components/ios/translations/en.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "Do you want to set up the Home Assistant iOS component?", - "title": "Home Assistant iOS" + "description": "Do you want to set up the Home Assistant iOS component?" } } } diff --git a/homeassistant/components/ios/translations/es-419.json b/homeassistant/components/ios/translations/es-419.json index 5938e27930e..a359cc3ae82 100644 --- a/homeassistant/components/ios/translations/es-419.json +++ b/homeassistant/components/ios/translations/es-419.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "\u00bfDesea configurar el componente iOS de Home Assistant?", - "title": "Home Assistant iOS" + "description": "\u00bfDesea configurar el componente iOS de Home Assistant?" } } } diff --git a/homeassistant/components/ios/translations/es.json b/homeassistant/components/ios/translations/es.json index 9e94d536718..e66c48a2355 100644 --- a/homeassistant/components/ios/translations/es.json +++ b/homeassistant/components/ios/translations/es.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "\u00bfDesea configurar el componente iOS de Home Assistant?", - "title": "Home Assistant iOS" + "description": "\u00bfDesea configurar el componente iOS de Home Assistant?" } } } diff --git a/homeassistant/components/ios/translations/fr.json b/homeassistant/components/ios/translations/fr.json index 02fb127e363..a6318718f94 100644 --- a/homeassistant/components/ios/translations/fr.json +++ b/homeassistant/components/ios/translations/fr.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "Voulez-vous configurer le composant Home Assistant iOS?", - "title": "Home Assistant iOS" + "description": "Voulez-vous configurer le composant Home Assistant iOS?" } } } diff --git a/homeassistant/components/ios/translations/he.json b/homeassistant/components/ios/translations/he.json index fd0c5c7c470..deb8eae6b38 100644 --- a/homeassistant/components/ios/translations/he.json +++ b/homeassistant/components/ios/translations/he.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea Home Assistant iOS?", - "title": "Home Assistant iOS" + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea Home Assistant iOS?" } } } diff --git a/homeassistant/components/ios/translations/hu.json b/homeassistant/components/ios/translations/hu.json index 77bbc56e392..f716fd36e9a 100644 --- a/homeassistant/components/ios/translations/hu.json +++ b/homeassistant/components/ios/translations/hu.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a Home Assistant iOS komponenst?", - "title": "Home Assistant iOS" + "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a Home Assistant iOS komponenst?" } } } diff --git a/homeassistant/components/ios/translations/id.json b/homeassistant/components/ios/translations/id.json index 1c69ea0a48a..46449f1c044 100644 --- a/homeassistant/components/ios/translations/id.json +++ b/homeassistant/components/ios/translations/id.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "Apakah Anda ingin mengatur komponen iOS Home Assistant?", - "title": "Home Asisten iOS" + "description": "Apakah Anda ingin mengatur komponen iOS Home Assistant?" } } } diff --git a/homeassistant/components/ios/translations/it.json b/homeassistant/components/ios/translations/it.json index e41382ea62e..763293d6b54 100644 --- a/homeassistant/components/ios/translations/it.json +++ b/homeassistant/components/ios/translations/it.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "Vuoi configurare il componente Home Assistant iOS?", - "title": "Home Assistant iOS" + "description": "Vuoi configurare il componente Home Assistant iOS?" } } } diff --git a/homeassistant/components/ios/translations/ko.json b/homeassistant/components/ios/translations/ko.json index fdd036f26a8..6abe9380473 100644 --- a/homeassistant/components/ios/translations/ko.json +++ b/homeassistant/components/ios/translations/ko.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "Home Assistant iOS \ucef4\ud3ec\ub10c\ud2b8\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "Home Assistant iOS" + "description": "Home Assistant iOS \ucef4\ud3ec\ub10c\ud2b8\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" } } } diff --git a/homeassistant/components/ios/translations/lb.json b/homeassistant/components/ios/translations/lb.json index bb6ec7b9f8a..85d78613d55 100644 --- a/homeassistant/components/ios/translations/lb.json +++ b/homeassistant/components/ios/translations/lb.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "W\u00ebllt dir d'Home Assistant iOS Komponent ariichten?", - "title": "Home Assistant iOS" + "description": "W\u00ebllt dir d'Home Assistant iOS Komponent ariichten?" } } } diff --git a/homeassistant/components/ios/translations/nl.json b/homeassistant/components/ios/translations/nl.json index 34050865768..0575b4558af 100644 --- a/homeassistant/components/ios/translations/nl.json +++ b/homeassistant/components/ios/translations/nl.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "Wilt u het Home Assistant iOS component instellen?", - "title": "Home Assistant iOS" + "description": "Wilt u het Home Assistant iOS component instellen?" } } } diff --git a/homeassistant/components/ios/translations/nn.json b/homeassistant/components/ios/translations/nn.json index 2fc5a3f1c52..5fb94712039 100644 --- a/homeassistant/components/ios/translations/nn.json +++ b/homeassistant/components/ios/translations/nn.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "Vil du sette opp Home Assistant iOS-komponenten?", - "title": "Home Assistant Ios" + "description": "Vil du sette opp Home Assistant iOS-komponenten?" } } } diff --git a/homeassistant/components/ios/translations/no.json b/homeassistant/components/ios/translations/no.json index 2814d73f555..1b8dd7b3c8e 100644 --- a/homeassistant/components/ios/translations/no.json +++ b/homeassistant/components/ios/translations/no.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "\u00d8nsker du \u00e5 sette opp Home Assistant iOS-komponenten?", - "title": "" + "description": "\u00d8nsker du \u00e5 sette opp Home Assistant iOS-komponenten?" } } } diff --git a/homeassistant/components/ios/translations/pl.json b/homeassistant/components/ios/translations/pl.json index 58b02fe8cad..4defef3926c 100644 --- a/homeassistant/components/ios/translations/pl.json +++ b/homeassistant/components/ios/translations/pl.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "Czy chcesz skonfigurowa\u0107 Home Assistant iOS?", - "title": "Home Assistant iOS" + "description": "Czy chcesz skonfigurowa\u0107 Home Assistant iOS?" } } } diff --git a/homeassistant/components/ios/translations/pt-BR.json b/homeassistant/components/ios/translations/pt-BR.json index 8eee79de746..fffbfae2249 100644 --- a/homeassistant/components/ios/translations/pt-BR.json +++ b/homeassistant/components/ios/translations/pt-BR.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "Deseja configurar o componente iOS do Home Assistant?", - "title": "Home Assistant iOS" + "description": "Deseja configurar o componente iOS do Home Assistant?" } } } diff --git a/homeassistant/components/ios/translations/pt.json b/homeassistant/components/ios/translations/pt.json index 52a1a66ba32..319ba1e3759 100644 --- a/homeassistant/components/ios/translations/pt.json +++ b/homeassistant/components/ios/translations/pt.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "Deseja configurar o componente iOS do Home Assistant?", - "title": "Home Assistant iOS" + "description": "Deseja configurar o componente iOS do Home Assistant?" } } } diff --git a/homeassistant/components/ios/translations/ro.json b/homeassistant/components/ios/translations/ro.json index b4b13d3955a..0cf0f56f68f 100644 --- a/homeassistant/components/ios/translations/ro.json +++ b/homeassistant/components/ios/translations/ro.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "Dori\u021bi s\u0103 configura\u021bi componenta Home Assistant iOS?", - "title": "Home Assistant iOS" + "description": "Dori\u021bi s\u0103 configura\u021bi componenta Home Assistant iOS?" } } } diff --git a/homeassistant/components/ios/translations/ru.json b/homeassistant/components/ios/translations/ru.json index d19343acc89..11df628045d 100644 --- a/homeassistant/components/ios/translations/ru.json +++ b/homeassistant/components/ios/translations/ru.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Home Assistant iOS?", - "title": "Home Assistant iOS" + "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 Home Assistant iOS?" } } } diff --git a/homeassistant/components/ios/translations/sl.json b/homeassistant/components/ios/translations/sl.json index e0958432a6e..8e104033bd3 100644 --- a/homeassistant/components/ios/translations/sl.json +++ b/homeassistant/components/ios/translations/sl.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "Ali \u017eelite nastaviti komponento za Home Assistant iOS?", - "title": "Home Assistant iOS" + "description": "Ali \u017eelite nastaviti komponento za Home Assistant iOS?" } } } diff --git a/homeassistant/components/ios/translations/sv.json b/homeassistant/components/ios/translations/sv.json index 03d91e02193..f2f0791b9c2 100644 --- a/homeassistant/components/ios/translations/sv.json +++ b/homeassistant/components/ios/translations/sv.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "Vill du konfigurera Home Assistants iOS komponent?", - "title": "Home Assistant iOS" + "description": "Vill du konfigurera Home Assistants iOS komponent?" } } } diff --git a/homeassistant/components/ios/translations/zh-Hans.json b/homeassistant/components/ios/translations/zh-Hans.json index 8e659dbadc1..5c5c29a41d5 100644 --- a/homeassistant/components/ios/translations/zh-Hans.json +++ b/homeassistant/components/ios/translations/zh-Hans.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "\u662f\u5426\u8981\u8bbe\u7f6e Home Assistant iOS \u7ec4\u4ef6\uff1f", - "title": "Home Assistant iOS" + "description": "\u662f\u5426\u8981\u8bbe\u7f6e Home Assistant iOS \u7ec4\u4ef6\uff1f" } } } diff --git a/homeassistant/components/ios/translations/zh-Hant.json b/homeassistant/components/ios/translations/zh-Hant.json index ba85332cb7f..8ca797ecf84 100644 --- a/homeassistant/components/ios/translations/zh-Hant.json +++ b/homeassistant/components/ios/translations/zh-Hant.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant iOS \u5143\u4ef6\uff1f", - "title": "Home Assistant iOS" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant iOS \u5143\u4ef6\uff1f" } } } diff --git a/homeassistant/components/ipma/translations/fi.json b/homeassistant/components/ipma/translations/fi.json index 00ae9b3df2f..b0c5fcc15cd 100644 --- a/homeassistant/components/ipma/translations/fi.json +++ b/homeassistant/components/ipma/translations/fi.json @@ -9,7 +9,8 @@ "latitude": "Leveysaste", "longitude": "Pituusaste", "name": "Nimi" - } + }, + "title": "Sijainti" } } } diff --git a/homeassistant/components/ipma/translations/pt-BR.json b/homeassistant/components/ipma/translations/pt-BR.json index 4a0d8e0b01b..f2af40324eb 100644 --- a/homeassistant/components/ipma/translations/pt-BR.json +++ b/homeassistant/components/ipma/translations/pt-BR.json @@ -8,6 +8,7 @@ "data": { "latitude": "Latitude", "longitude": "Longitude", + "mode": "Modo", "name": "Nome" }, "description": "Instituto Portugu\u00eas do Mar e Atmosfera", diff --git a/homeassistant/components/ipp/translations/ca.json b/homeassistant/components/ipp/translations/ca.json index a7bc8d04890..cd619583e52 100644 --- a/homeassistant/components/ipp/translations/ca.json +++ b/homeassistant/components/ipp/translations/ca.json @@ -6,7 +6,8 @@ "connection_upgrade": "No s'ha pogut connectar amb la impressora, es necessita actualitzar la connexi\u00f3.", "ipp_error": "S'ha produ\u00eft un error IPP.", "ipp_version_error": "La versi\u00f3 IPP no \u00e9s compatible amb la impressora.", - "parse_error": "No s'ha pogut analitzar la resposta de la impressora." + "parse_error": "No s'ha pogut analitzar la resposta de la impressora.", + "unique_id_required": "Falta la identificaci\u00f3 \u00fanica al dispositiu, necess\u00e0ria per al descobriment." }, "error": { "connection_error": "No s'ha pogut connectar", @@ -26,7 +27,7 @@ "title": "Enlla\u00e7 d'impressora" }, "zeroconf_confirm": { - "description": "Vols afegir la impressora {name} a Home Assistant?", + "description": "Vols configurar {name}?", "title": "Impressora descoberta" } } diff --git a/homeassistant/components/ipp/translations/de.json b/homeassistant/components/ipp/translations/de.json index 3c43cd08cc5..3ee2f2159b9 100644 --- a/homeassistant/components/ipp/translations/de.json +++ b/homeassistant/components/ipp/translations/de.json @@ -6,7 +6,8 @@ "connection_upgrade": "Verbindung zum Drucker fehlgeschlagen, da ein Verbindungsupgrade erforderlich ist.", "ipp_error": "IPP-Fehler festgestellt.", "ipp_version_error": "IPP-Version wird vom Drucker nicht unterst\u00fctzt.", - "parse_error": "Antwort vom Drucker konnte nicht analysiert werden." + "parse_error": "Antwort vom Drucker konnte nicht analysiert werden.", + "unique_id_required": "Ger\u00e4t fehlt die f\u00fcr die Entdeckung erforderliche eindeutige Identifizierung." }, "error": { "connection_error": "Verbindung zum Drucker fehlgeschlagen.", diff --git a/homeassistant/components/ipp/translations/en.json b/homeassistant/components/ipp/translations/en.json index abbe4e8a5f8..0267c5b5091 100644 --- a/homeassistant/components/ipp/translations/en.json +++ b/homeassistant/components/ipp/translations/en.json @@ -6,7 +6,8 @@ "connection_upgrade": "Failed to connect to printer due to connection upgrade being required.", "ipp_error": "Encountered IPP error.", "ipp_version_error": "IPP version not supported by printer.", - "parse_error": "Failed to parse response from printer." + "parse_error": "Failed to parse response from printer.", + "unique_id_required": "Device missing unique identification required for discovery." }, "error": { "connection_error": "Failed to connect", diff --git a/homeassistant/components/ipp/translations/es.json b/homeassistant/components/ipp/translations/es.json index 5f4a1370b68..51c7e63677d 100644 --- a/homeassistant/components/ipp/translations/es.json +++ b/homeassistant/components/ipp/translations/es.json @@ -6,7 +6,8 @@ "connection_upgrade": "No se pudo conectar con la impresora debido a que se requiere una actualizaci\u00f3n de la conexi\u00f3n.", "ipp_error": "Error IPP encontrado.", "ipp_version_error": "Versi\u00f3n de IPP no compatible con la impresora.", - "parse_error": "Error al analizar la respuesta de la impresora." + "parse_error": "Error al analizar la respuesta de la impresora.", + "unique_id_required": "El dispositivo no tiene identificaci\u00f3n \u00fanica necesaria para el descubrimiento." }, "error": { "connection_error": "Error al conectar", diff --git a/homeassistant/components/ipp/translations/hu.json b/homeassistant/components/ipp/translations/hu.json index 66e835ec100..657491280df 100644 --- a/homeassistant/components/ipp/translations/hu.json +++ b/homeassistant/components/ipp/translations/hu.json @@ -1,5 +1,20 @@ { "config": { - "flow_title": "Nyomtat\u00f3: {name}" + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "connection_error": "Sikertelen csatlakoz\u00e1s" + }, + "error": { + "connection_error": "Sikertelen csatlakoz\u00e1s" + }, + "flow_title": "Nyomtat\u00f3: {name}", + "step": { + "user": { + "data": { + "host": "Hoszt", + "port": "Port" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/it.json b/homeassistant/components/ipp/translations/it.json index d2f53fec24b..c1eacf00c28 100644 --- a/homeassistant/components/ipp/translations/it.json +++ b/homeassistant/components/ipp/translations/it.json @@ -1,15 +1,16 @@ { "config": { "abort": { - "already_configured": "Questa stampante \u00e8 gi\u00e0 configurata.", - "connection_error": "Impossibile connettersi alla stampante.", + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "connection_error": "Impossibile connettersi", "connection_upgrade": "Impossibile connettersi alla stampante a causa della necessit\u00e0 dell'aggiornamento della connessione.", "ipp_error": "Si \u00e8 verificato un errore IPP.", "ipp_version_error": "Versione IPP non supportata dalla stampante.", - "parse_error": "Impossibile analizzare la risposta dalla stampante." + "parse_error": "Impossibile analizzare la risposta dalla stampante.", + "unique_id_required": "Identificazione univoca del dispositivo mancante necessaria per l'individuazione." }, "error": { - "connection_error": "Impossibile connettersi alla stampante.", + "connection_error": "Impossibile connettersi", "connection_upgrade": "Impossibile connettersi alla stampante. Riprovare selezionando l'opzione SSL/TLS." }, "flow_title": "Stampante: {name}", @@ -17,7 +18,7 @@ "user": { "data": { "base_path": "Percorso relativo alla stampante", - "host": "Host o indirizzo IP", + "host": "Host", "port": "Porta", "ssl": "La stampante supporta la comunicazione su SSL/TLS", "verify_ssl": "La stampante utilizza un certificato SSL adeguato" @@ -26,7 +27,7 @@ "title": "Collegare la stampante" }, "zeroconf_confirm": { - "description": "Vuoi aggiungere la stampante denominata `{name}` a Home Assistant?", + "description": "Vuoi configurare {name}?", "title": "Stampante rilevata" } } diff --git a/homeassistant/components/ipp/translations/ko.json b/homeassistant/components/ipp/translations/ko.json index abf0c270dac..dd071ed41ec 100644 --- a/homeassistant/components/ipp/translations/ko.json +++ b/homeassistant/components/ipp/translations/ko.json @@ -6,7 +6,8 @@ "connection_upgrade": "\ud504\ub9b0\ud130\uc5d0 \uc5f0\uacb0\ud558\ub824\uba74 \uc5f0\uacb0\uc744 \uc5c5\uadf8\ub808\uc774\ub4dc\ud574\uc57c \ud569\ub2c8\ub2e4.", "ipp_error": "IPP \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", "ipp_version_error": "\ud504\ub9b0\ud130\uc5d0\uc11c IPP \ubc84\uc804\uc744 \uc9c0\uc6d0\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", - "parse_error": "\ud504\ub9b0\ud130\uc758 \uc751\ub2f5\uc744 \uc77d\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4." + "parse_error": "\ud504\ub9b0\ud130\uc758 \uc751\ub2f5\uc744 \uc77d\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", + "unique_id_required": "\uae30\uae30 \uac80\uc0c9\uc5d0 \ud544\uc694\ud55c \uace0\uc720\ud55c ID \uac00 \uc874\uc7ac\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." }, "error": { "connection_error": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", diff --git a/homeassistant/components/ipp/translations/lb.json b/homeassistant/components/ipp/translations/lb.json index 7a92b59a47a..5e5c9306061 100644 --- a/homeassistant/components/ipp/translations/lb.json +++ b/homeassistant/components/ipp/translations/lb.json @@ -6,7 +6,8 @@ "connection_upgrade": "Feeler beim verbannen mam Printer well eng Aktualis\u00e9ierung vun der Verbindung erfuerderlech ass.", "ipp_error": "IPP Feeler opgetrueden.", "ipp_version_error": "IPP Versioun net vum Printer \u00ebnnerst\u00ebtzt.", - "parse_error": "Feeler beim ausliesen vun der \u00c4ntwert vum Printer." + "parse_error": "Feeler beim ausliesen vun der \u00c4ntwert vum Printer.", + "unique_id_required": "Dem Apparat feelt eng eenzegarteg Identifikatioun d\u00e9i ben\u00e9idegt ass fir d'Entdeckung." }, "error": { "connection_error": "Feeler beim verbannen mam Printer.", diff --git a/homeassistant/components/ipp/translations/nl.json b/homeassistant/components/ipp/translations/nl.json index 12f9d37ec7a..e021c8a0010 100644 --- a/homeassistant/components/ipp/translations/nl.json +++ b/homeassistant/components/ipp/translations/nl.json @@ -6,7 +6,8 @@ "connection_upgrade": "Kan geen verbinding maken met de printer omdat een upgrade van de verbinding vereist is.", "ipp_error": "Er is een IPP-fout opgetreden.", "ipp_version_error": "IPP-versie wordt niet ondersteund door printer.", - "parse_error": "Ongeldige reactie van de printer." + "parse_error": "Ongeldige reactie van de printer.", + "unique_id_required": "Apparaat ontbreekt een unieke identificatie die nodig is voor de ontdekking." }, "error": { "connection_error": "Kan geen verbinding maken met de printer.", diff --git a/homeassistant/components/ipp/translations/no.json b/homeassistant/components/ipp/translations/no.json index afc03cf90b2..c031864cf4d 100644 --- a/homeassistant/components/ipp/translations/no.json +++ b/homeassistant/components/ipp/translations/no.json @@ -6,7 +6,8 @@ "connection_upgrade": "Kunne ikke koble til skriveren fordi tilkoblingsoppgradering var n\u00f8dvendig.", "ipp_error": "Oppdaget IPP-feil.", "ipp_version_error": "IPP-versjon st\u00f8ttes ikke av skriveren.", - "parse_error": "Kan ikke analysere svar fra skriveren." + "parse_error": "Kan ikke analysere svar fra skriveren.", + "unique_id_required": "Enheten mangler unik identifikasjon som kreves for oppdagelse." }, "error": { "connection_error": "Klarte ikke \u00e5 koble til skriveren.", @@ -26,7 +27,7 @@ "title": "Koble til skriveren din" }, "zeroconf_confirm": { - "description": "\u00d8nsker du \u00e5 legge skriveren med navnet {name} til Home Assistant?", + "description": "Vil du konfigurere {name}?", "title": "Oppdaget skriver" } } diff --git a/homeassistant/components/ipp/translations/pl.json b/homeassistant/components/ipp/translations/pl.json index 8ead666dee1..4e5af33041c 100644 --- a/homeassistant/components/ipp/translations/pl.json +++ b/homeassistant/components/ipp/translations/pl.json @@ -2,14 +2,15 @@ "config": { "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", - "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z drukark\u0105.", + "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", "connection_upgrade": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z drukark\u0105 z powodu konieczno\u015bci uaktualnienia po\u0142\u0105czenia.", "ipp_error": "Wyst\u0105pi\u0142 b\u0142\u0105d IPP.", "ipp_version_error": "Wersja IPP nieobs\u0142ugiwana przez drukark\u0119.", - "parse_error": "Nie mo\u017cna przeanalizowa\u0107 odpowiedzi z drukarki." + "parse_error": "Nie mo\u017cna przeanalizowa\u0107 odpowiedzi z drukarki.", + "unique_id_required": "Urz\u0105dzenie nie posiada unikalnej identyfikacji wymaganej do wykrycia." }, "error": { - "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z drukark\u0105.", + "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", "connection_upgrade": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z drukark\u0105. Spr\u00f3buj ponownie z zaznaczon\u0105 opcj\u0105 SSL/TLS." }, "flow_title": "Drukarka: {name}", @@ -17,8 +18,8 @@ "user": { "data": { "base_path": "\u015acie\u017cka wzgl\u0119dna do drukarki", - "host": "[%key_id:common::config_flow::data::host%]", - "port": "[%key_id:common::config_flow::data::port%]", + "host": "Nazwa hosta lub adres IP", + "port": "Port", "ssl": "Drukarka obs\u0142uguje komunikacj\u0119 przez SSL/TLS", "verify_ssl": "Drukarka u\u017cywa prawid\u0142owego certyfikatu" }, @@ -26,7 +27,7 @@ "title": "Po\u0142\u0105cz swoj\u0105 drukark\u0119" }, "zeroconf_confirm": { - "description": "Czy chcesz doda\u0107 drukark\u0119 o nazwie `{name}` do Home Assistant'a?", + "description": "Czy chcesz doda\u0107 drukark\u0119 o nazwie `{name}` do Home Assistanta?", "title": "Wykryto drukark\u0119" } } diff --git a/homeassistant/components/ipp/translations/pt-BR.json b/homeassistant/components/ipp/translations/pt-BR.json new file mode 100644 index 00000000000..f992520501e --- /dev/null +++ b/homeassistant/components/ipp/translations/pt-BR.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "connection_upgrade": "Falha ao conectar \u00e0 impressora devido \u00e0 atualiza\u00e7\u00e3o da conex\u00e3o ser necess\u00e1ria.", + "ipp_error": "Erro IPP encontrado.", + "ipp_version_error": "Vers\u00e3o IPP n\u00e3o suportada pela impressora.", + "unique_id_required": "Dispositivo faltando identifica\u00e7\u00e3o \u00fanica necess\u00e1ria para a descoberta." + }, + "error": { + "connection_upgrade": "Falha ao conectar \u00e0 impressora. Por favor, tente novamente com a op\u00e7\u00e3o SSL/TLS marcada." + }, + "flow_title": "Impressora: {name}", + "step": { + "user": { + "data": { + "base_path": "Caminho relativo para a impressora", + "ssl": "A impressora suporta comunica\u00e7\u00e3o via SSL/TLS", + "verify_ssl": "A impressora usa um certificado SSL adequado" + }, + "description": "Configure sua impressora via IPP (Internet Printing Protocol) para integrar-se ao Home Assistant.", + "title": "Vincule sua impressora" + }, + "zeroconf_confirm": { + "title": "Impressora descoberta" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/ru.json b/homeassistant/components/ipp/translations/ru.json index c2d9e6b5cc0..6fdfd333773 100644 --- a/homeassistant/components/ipp/translations/ru.json +++ b/homeassistant/components/ipp/translations/ru.json @@ -6,7 +6,8 @@ "connection_upgrade": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0443 \u0438\u0437-\u0437\u0430 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f.", "ipp_error": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 IPP.", "ipp_version_error": "\u0412\u0435\u0440\u0441\u0438\u044f IPP \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u043e\u043c.", - "parse_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0440\u0430\u0437\u043e\u0431\u0440\u0430\u0442\u044c \u043e\u0442\u0432\u0435\u0442 \u043e\u0442 \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430." + "parse_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0440\u0430\u0437\u043e\u0431\u0440\u0430\u0442\u044c \u043e\u0442\u0432\u0435\u0442 \u043e\u0442 \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430.", + "unique_id_required": "\u041d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430\u044f \u0434\u043b\u044f \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f." }, "error": { "connection_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", diff --git a/homeassistant/components/ipp/translations/zh-Hant.json b/homeassistant/components/ipp/translations/zh-Hant.json index fa7593c1ea2..d1c9c00bdb8 100644 --- a/homeassistant/components/ipp/translations/zh-Hant.json +++ b/homeassistant/components/ipp/translations/zh-Hant.json @@ -6,7 +6,8 @@ "connection_upgrade": "\u7531\u65bc\u9700\u8981\u5148\u5347\u7d1a\u9023\u7dda\u3001\u9023\u7dda\u81f3\u5370\u8868\u6a5f\u5931\u6557\u3002", "ipp_error": "\u767c\u751f IPP \u932f\u8aa4\u3002", "ipp_version_error": "\u4e0d\u652f\u63f4\u5370\u8868\u6a5f\u7684 IPP \u7248\u672c\u3002", - "parse_error": "\u7372\u5f97\u5370\u8868\u6a5f\u56de\u61c9\u5931\u6557\u3002" + "parse_error": "\u7372\u5f97\u5370\u8868\u6a5f\u56de\u61c9\u5931\u6557\u3002", + "unique_id_required": "\u8a2d\u5099\u7f3a\u5c11\u641c\u5c0b\u6240\u9700\u7368\u4e00\u8b58\u5225\u3002" }, "error": { "connection_error": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/islamic_prayer_times/translations/pt-BR.json b/homeassistant/components/islamic_prayer_times/translations/pt-BR.json new file mode 100644 index 00000000000..ee9946cf834 --- /dev/null +++ b/homeassistant/components/islamic_prayer_times/translations/pt-BR.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Apenas uma \u00fanica inst\u00e2ncia \u00e9 necess\u00e1ria." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/isy994/climate.py b/homeassistant/components/isy994/climate.py index 8299265a381..7dfd9a083d3 100644 --- a/homeassistant/components/isy994/climate.py +++ b/homeassistant/components/isy994/climate.py @@ -133,7 +133,7 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): # Which state values used depends on the mode property's UOM: uom = hvac_mode.uom # Handle special case for ISYv4 Firmware: - if uom == UOM_ISYV4_NONE: + if uom in (UOM_ISYV4_NONE, ""): uom = ( UOM_HVAC_MODE_INSTEON if self._node.protocol == PROTO_INSTEON diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index f7042a5860a..afbe44011d8 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -168,6 +168,23 @@ UNDO_UPDATE_LISTENER = "undo_update_listener" UDN_UUID_PREFIX = "uuid:" ISY_URL_POSTFIX = "/desc" +# Special Units of Measure +UOM_ISYV4_DEGREES = "degrees" +UOM_ISYV4_NONE = "n/a" + +UOM_ISY_CELSIUS = 1 +UOM_ISY_FAHRENHEIT = 2 + +UOM_8_BIT_RANGE = "100" +UOM_BARRIER = "97" +UOM_DOUBLE_TEMP = "101" +UOM_HVAC_ACTIONS = "66" +UOM_HVAC_MODE_GENERIC = "67" +UOM_HVAC_MODE_INSTEON = "98" +UOM_FAN_MODES = "99" +UOM_INDEX = "25" +UOM_ON_OFF = "2" + # Do not use the Home Assistant consts for the states here - we're matching exact API # responses, not using them for Home Assistant states # Insteon Types: https://www.universal-devices.com/developers/wsdk/5.0.4/1_fam.xml @@ -215,7 +232,7 @@ NODE_FILTERS = { "RemoteLinc2_ADV", ], FILTER_INSTEON_TYPE: ["0.16.", "0.17.", "0.18.", "9.0.", "9.7."], - FILTER_ZWAVE_CAT: (["118", "143"] + list(map(str, range(180, 185)))), + FILTER_ZWAVE_CAT: (["118", "143"] + list(map(str, range(180, 186)))), }, LOCK: { FILTER_UOM: ["11"], @@ -232,10 +249,10 @@ NODE_FILTERS = { FILTER_ZWAVE_CAT: [], }, COVER: { - FILTER_UOM: ["97"], + FILTER_UOM: [UOM_BARRIER], FILTER_STATES: ["open", "closed", "closing", "opening", "stopped"], - FILTER_NODE_DEF_ID: [], - FILTER_INSTEON_TYPE: [], + FILTER_NODE_DEF_ID: ["DimmerMotorSwitch_ADV"], + FILTER_INSTEON_TYPE: [TYPE_CATEGORY_COVER], FILTER_ZWAVE_CAT: [], }, LIGHT: { @@ -256,7 +273,7 @@ NODE_FILTERS = { FILTER_ZWAVE_CAT: ["109", "119"], }, SWITCH: { - FILTER_UOM: ["2", "78"], + FILTER_UOM: [UOM_ON_OFF, "78"], FILTER_STATES: ["on", "off"], FILTER_NODE_DEF_ID: [ "AlertModuleArmed", @@ -286,7 +303,7 @@ NODE_FILTERS = { FILTER_ZWAVE_CAT: ["121", "122", "123", "137", "141", "147"], }, CLIMATE: { - FILTER_UOM: ["2"], + FILTER_UOM: [UOM_ON_OFF], FILTER_STATES: ["heating", "cooling", "idle", "fan_only", "off"], FILTER_NODE_DEF_ID: ["TempLinc", "Thermostat"], FILTER_INSTEON_TYPE: ["4.8", TYPE_CATEGORY_CLIMATE], @@ -294,20 +311,6 @@ NODE_FILTERS = { }, } -UOM_ISYV4_DEGREES = "degrees" -UOM_ISYV4_NONE = "n/a" - -UOM_ISY_CELSIUS = 1 -UOM_ISY_FAHRENHEIT = 2 - -UOM_DOUBLE_TEMP = "101" -UOM_HVAC_ACTIONS = "66" -UOM_HVAC_MODE_GENERIC = "67" -UOM_HVAC_MODE_INSTEON = "98" -UOM_FAN_MODES = "99" -UOM_INDEX = "25" -UOM_ON_OFF = "2" - UOM_FRIENDLY_NAME = { "1": "A", "3": f"btu/{TIME_HOURS}", @@ -388,7 +391,7 @@ UOM_FRIENDLY_NAME = { "90": FREQUENCY_HERTZ, "91": DEGREE, "92": f"{DEGREE} South", - "100": "", # Range 0-255, no unit. + UOM_8_BIT_RANGE: "", # Range 0-255, no unit. UOM_DOUBLE_TEMP: UOM_DOUBLE_TEMP, "102": "kWs", "103": "$", @@ -556,7 +559,7 @@ UOM_TO_STATES = { 3: "moderately polluted", 4: "highly polluted", }, - "97": { # Barrier Status + UOM_BARRIER: { # Barrier Status **{ 0: STATE_CLOSED, 100: STATE_OPEN, diff --git a/homeassistant/components/isy994/cover.py b/homeassistant/components/isy994/cover.py index e0e47592a37..41273f61f01 100644 --- a/homeassistant/components/isy994/cover.py +++ b/homeassistant/components/isy994/cover.py @@ -3,11 +3,25 @@ from typing import Callable from pyisy.constants import ISY_VALUE_UNKNOWN -from homeassistant.components.cover import DOMAIN as COVER, CoverEntity +from homeassistant.components.cover import ( + ATTR_POSITION, + DOMAIN as COVER, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + CoverEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType -from .const import _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_PROGRAMS +from .const import ( + _LOGGER, + DOMAIN as ISY994_DOMAIN, + ISY994_NODES, + ISY994_PROGRAMS, + UOM_8_BIT_RANGE, + UOM_BARRIER, +) from .entity import ISYNodeEntity, ISYProgramEntity from .helpers import migrate_old_unique_ids from .services import async_setup_device_services @@ -40,6 +54,8 @@ class ISYCoverEntity(ISYNodeEntity, CoverEntity): """Return the current cover position.""" if self._node.status == ISY_VALUE_UNKNOWN: return None + if self._node.uom == UOM_8_BIT_RANGE: + return int(self._node.status * 100 / 255) return sorted((0, self._node.status, 100))[1] @property @@ -49,9 +65,15 @@ class ISYCoverEntity(ISYNodeEntity, CoverEntity): return None return self._node.status == 0 + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION + def open_cover(self, **kwargs) -> None: """Send the open cover command to the ISY994 cover device.""" - if not self._node.turn_on(val=100): + val = 100 if self._node.uom == UOM_BARRIER else None + if not self._node.turn_on(val=val): _LOGGER.error("Unable to open the cover") def close_cover(self, **kwargs) -> None: @@ -59,6 +81,14 @@ class ISYCoverEntity(ISYNodeEntity, CoverEntity): if not self._node.turn_off(): _LOGGER.error("Unable to close the cover") + def set_cover_position(self, **kwargs): + """Move the cover to a specific position.""" + position = kwargs[ATTR_POSITION] + if self._node.uom == UOM_8_BIT_RANGE: + position = int(position * 255 / 100) + if not self._node.turn_on(val=position): + _LOGGER.error("Unable to set cover position") + class ISYCoverProgramEntity(ISYProgramEntity, CoverEntity): """Representation of an ISY994 cover program.""" diff --git a/homeassistant/components/isy994/translations/ca.json b/homeassistant/components/isy994/translations/ca.json index aa9c188f8dc..1fd7c21793e 100644 --- a/homeassistant/components/isy994/translations/ca.json +++ b/homeassistant/components/isy994/translations/ca.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "[%key::common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key::common::config_flow::error::invalid_auth%]", + "invalid_host": "L'entrada de l'amfitri\u00f3 no t\u00e9 el fromat d'URL complet, ex: http://192.168.10.100:80", "unknown": "Error inesperat" }, "flow_title": "Dispositius universals ISY994 {name} ({host})", @@ -17,6 +18,7 @@ "tls": "Versi\u00f3 TLS del controlador ISY.", "username": "[%key::common::config_flow::data::username%]" }, + "description": "L'entrada de l'amfitri\u00f3 ha de tenir el format d'URL complet, ex: http://192.168.10.100:80", "title": "Connexi\u00f3 amb ISY994" } } diff --git a/homeassistant/components/isy994/translations/es.json b/homeassistant/components/isy994/translations/es.json index 1edd249e9d7..23ed579d473 100644 --- a/homeassistant/components/isy994/translations/es.json +++ b/homeassistant/components/isy994/translations/es.json @@ -9,6 +9,7 @@ "invalid_host": "La entrada del host no estaba en formato URL completo, por ejemplo, http://192.168.10.100:80", "unknown": "Error inesperado" }, + "flow_title": "Dispositivos Universales ISY994 {nombre} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/isy994/translations/hu.json b/homeassistant/components/isy994/translations/hu.json new file mode 100644 index 00000000000..6177c39b231 --- /dev/null +++ b/homeassistant/components/isy994/translations/hu.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/it.json b/homeassistant/components/isy994/translations/it.json index ccb5ff85b87..9b35a464c1b 100644 --- a/homeassistant/components/isy994/translations/it.json +++ b/homeassistant/components/isy994/translations/it.json @@ -9,6 +9,7 @@ "invalid_host": "La voce host non era nel formato URL completo, ad esempio http://192.168.10.100:80", "unknown": "Errore imprevisto" }, + "flow_title": "Universal Devices ISY994 {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/isy994/translations/ko.json b/homeassistant/components/isy994/translations/ko.json index c0edb400594..289609adbf7 100644 --- a/homeassistant/components/isy994/translations/ko.json +++ b/homeassistant/components/isy994/translations/ko.json @@ -9,6 +9,7 @@ "invalid_host": "\ud638\uc2a4\ud2b8 \ud56d\ubaa9\uc774 \uc644\uc804\ud55c URL \ud615\uc2dd\uc774 \uc544\ub2d9\ub2c8\ub2e4. \uc608: http://192.168.10.100:80", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, + "flow_title": "ISY994 \ubc94\uc6a9 \uae30\uae30: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/isy994/translations/lb.json b/homeassistant/components/isy994/translations/lb.json index 4d7d4cc47d7..b60ce03e43a 100644 --- a/homeassistant/components/isy994/translations/lb.json +++ b/homeassistant/components/isy994/translations/lb.json @@ -9,6 +9,7 @@ "invalid_host": "Host Entr\u00e9e muss am URL Format sinn, beispill, http://192.168.10.100:80", "unknown": "Onerwaarte Feeler" }, + "flow_title": "Universal Devices ISY994 {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/isy994/translations/nl.json b/homeassistant/components/isy994/translations/nl.json new file mode 100644 index 00000000000..35e52de882f --- /dev/null +++ b/homeassistant/components/isy994/translations/nl.json @@ -0,0 +1,5 @@ +{ + "config": { + "flow_title": "Universele apparaten ISY994 {naam} ({host})" + } +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/no.json b/homeassistant/components/isy994/translations/no.json index 9d9f5b59db9..0e2d4fec686 100644 --- a/homeassistant/components/isy994/translations/no.json +++ b/homeassistant/components/isy994/translations/no.json @@ -4,6 +4,7 @@ "invalid_host": "Vertsoppf\u00f8ringen var ikke i fullstendig URL-format, for eksempel http://192.168.10.100:80", "unknown": "[%key:common::config_flow::error::unknown%" }, + "flow_title": "Universelle enheter ISY994 {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/isy994/translations/pl.json b/homeassistant/components/isy994/translations/pl.json index a934b3d9f6c..27f79ef2801 100644 --- a/homeassistant/components/isy994/translations/pl.json +++ b/homeassistant/components/isy994/translations/pl.json @@ -1,21 +1,41 @@ { "config": { "abort": { - "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." }, "error": { - "cannot_connect": "[%key_id:common::config_flow::error::cannot_connect%]", - "invalid_auth": "[%key_id:common::config_flow::error::invalid_auth%]", - "unknown": "[%key_id:common::config_flow::error::unknown%]" + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "invalid_auth": "Niepoprawne uwierzytelnienie.", + "invalid_host": "Wpis hosta nie by\u0142 w pe\u0142nym formacie URL, np. http://192.168.10.100:80.", + "unknown": "Nieoczekiwany b\u0142\u0105d." }, + "flow_title": "Urz\u0105dzenia uniwersalne ISY994 {name} ({host})", "step": { "user": { "data": { "host": "URL", - "password": "[%key_id:common::config_flow::data::password%]", - "username": "[%key_id:common::config_flow::data::username%]" - } + "password": "Has\u0142o", + "tls": "Wersja TLS kontrolera ISY", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Wpis hosta musi by\u0107 w pe\u0142nym formacie URL, np. http://192.168.10.100:80.", + "title": "Po\u0142\u0105czenie z ISY994" } } - } + }, + "options": { + "step": { + "init": { + "data": { + "ignore_string": "Ci\u0105g ignorowania", + "restore_light_state": "Przywr\u00f3\u0107 jasno\u015b\u0107 \u015bwiat\u0142a", + "sensor_string": "Ci\u0105g sensora w\u0119z\u0142a", + "variable_sensor_string": "Ci\u0105g zmiennej sensora" + }, + "description": "Ustaw opcje dla integracji ISY: \n \u2022 Ci\u0105g sensora w\u0119z\u0142a: ka\u017cde urz\u0105dzenie lub folder, kt\u00f3ry zawiera w nazwie ci\u0105g sensora w\u0119z\u0142a, b\u0119dzie traktowane jako sensor lub sensor binarny. \n \u2022 Ci\u0105g ignorowania: ka\u017cde urz\u0105dzenie z 'ci\u0105giem ignorowania' w nazwie zostanie zignorowane. \n \u2022 Ci\u0105g sensora zmiennej: ka\u017cda zmienna zawieraj\u0105ca 'ci\u0105g sensora zmiennej' zostanie dodana jako sensor. \n \u2022 Przywr\u00f3\u0107 jasno\u015b\u0107 \u015bwiat\u0142a: je\u015bli ta opcja jest w\u0142\u0105czona, poprzednia warto\u015b\u0107 jasno\u015bci zostanie przywr\u00f3cona po w\u0142\u0105czeniu \u015bwiat\u0142a zamiast domy\u015blnej warto\u015bci jasno\u015bci dla urz\u0105dzenia.", + "title": "Opcje ISY994" + } + } + }, + "title": "Urz\u0105dzenia uniwersalne ISY994" } \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/pt-BR.json b/homeassistant/components/isy994/translations/pt-BR.json new file mode 100644 index 00000000000..65a087f8789 --- /dev/null +++ b/homeassistant/components/isy994/translations/pt-BR.json @@ -0,0 +1,32 @@ +{ + "config": { + "error": { + "invalid_host": "A entrada do host n\u00e3o est\u00e1 no formato de URL completo, por exemplo, http://192.168.10.100:80" + }, + "flow_title": "Dispositivos universais ISY994 {name} ({host})", + "step": { + "user": { + "data": { + "host": "URL", + "tls": "A vers\u00e3o TLS do controlador ISY." + }, + "description": "A entrada do endere\u00e7o deve estar no formato de URL completo, por exemplo, http://192.168.10.100:80", + "title": "Conecte-se ao seu ISY994" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ignore_string": "Ignorar texto", + "restore_light_state": "Restaurar o brilho da luz", + "sensor_string": "Texto do sensor node", + "variable_sensor_string": "Texto da vari\u00e1vel do sensor" + }, + "title": "ISY994 Op\u00e7\u00f5es" + } + } + }, + "title": "Dispositivos universais ISY994" +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/ru.json b/homeassistant/components/isy994/translations/ru.json index 8c19d972f40..ed34d79969f 100644 --- a/homeassistant/components/isy994/translations/ru.json +++ b/homeassistant/components/isy994/translations/ru.json @@ -9,6 +9,7 @@ "invalid_host": "URL-\u0430\u0434\u0440\u0435\u0441 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0444\u043e\u0440\u043c\u0430\u0442\u0435 'address[:port]' (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: 'http://192.168.10.100:80').", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, + "flow_title": "Universal Devices ISY994 {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/isy994/translations/zh-Hant.json b/homeassistant/components/isy994/translations/zh-Hant.json index fa3ef3ebd19..4327cca65e4 100644 --- a/homeassistant/components/isy994/translations/zh-Hant.json +++ b/homeassistant/components/isy994/translations/zh-Hant.json @@ -9,6 +9,7 @@ "invalid_host": "\u4e3b\u6a5f\u7aef\u4e26\u672a\u4ee5\u5b8c\u6574\u7db2\u5740\u683c\u5f0f\u8f38\u5165\uff0c\u4f8b\u5982\uff1ahttp://192.168.10.100:80", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, + "flow_title": "Universal Devices ISY994 {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/itach/remote.py b/homeassistant/components/itach/remote.py index 2a1a2eac0ca..8f1f642e49e 100644 --- a/homeassistant/components/itach/remote.py +++ b/homeassistant/components/itach/remote.py @@ -5,7 +5,7 @@ import pyitachip2ir import voluptuous as vol from homeassistant.components import remote -from homeassistant.components.remote import PLATFORM_SCHEMA +from homeassistant.components.remote import ATTR_NUM_REPEATS, PLATFORM_SCHEMA from homeassistant.const import ( CONF_DEVICES, CONF_HOST, @@ -106,19 +106,22 @@ class ITachIP2IRRemote(remote.RemoteEntity): def turn_on(self, **kwargs): """Turn the device on.""" self._power = True - self.itachip2ir.send(self._name, "ON", 1) + num_repeats = kwargs.get(ATTR_NUM_REPEATS, 1) + self.itachip2ir.send(self._name, "ON", num_repeats) self.schedule_update_ha_state() def turn_off(self, **kwargs): """Turn the device off.""" self._power = False - self.itachip2ir.send(self._name, "OFF", 1) + num_repeats = kwargs.get(ATTR_NUM_REPEATS, 1) + self.itachip2ir.send(self._name, "OFF", num_repeats) self.schedule_update_ha_state() def send_command(self, command, **kwargs): """Send a command to one device.""" + num_repeats = kwargs.get(ATTR_NUM_REPEATS, 1) for single_command in command: - self.itachip2ir.send(self._name, single_command, 1) + self.itachip2ir.send(self._name, single_command, num_repeats) def update(self): """Update the device.""" diff --git a/homeassistant/components/izone/translations/bg.json b/homeassistant/components/izone/translations/bg.json index 2aed0f309b4..d9866ef1d13 100644 --- a/homeassistant/components/izone/translations/bg.json +++ b/homeassistant/components/izone/translations/bg.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 iZone?", - "title": "iZone" + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 iZone?" } } } diff --git a/homeassistant/components/izone/translations/ca.json b/homeassistant/components/izone/translations/ca.json index 016e0fb070e..811b3cc8c29 100644 --- a/homeassistant/components/izone/translations/ca.json +++ b/homeassistant/components/izone/translations/ca.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Vols configurar iZone?", - "title": "iZone" + "description": "Vols configurar iZone?" } } } diff --git a/homeassistant/components/izone/translations/da.json b/homeassistant/components/izone/translations/da.json index 6c08029d9e0..f6343f917f8 100644 --- a/homeassistant/components/izone/translations/da.json +++ b/homeassistant/components/izone/translations/da.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Vil du konfigurere iZone?", - "title": "iZone" + "description": "Vil du konfigurere iZone?" } } } diff --git a/homeassistant/components/izone/translations/de.json b/homeassistant/components/izone/translations/de.json index 47032413607..ea59cc39b27 100644 --- a/homeassistant/components/izone/translations/de.json +++ b/homeassistant/components/izone/translations/de.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "M\u00f6chtest du iZone einrichten?", - "title": "iZone" + "description": "M\u00f6chtest du iZone einrichten?" } } } diff --git a/homeassistant/components/izone/translations/en.json b/homeassistant/components/izone/translations/en.json index 7c9b7008f24..03cc003752b 100644 --- a/homeassistant/components/izone/translations/en.json +++ b/homeassistant/components/izone/translations/en.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Do you want to set up iZone?", - "title": "iZone" + "description": "Do you want to set up iZone?" } } } diff --git a/homeassistant/components/izone/translations/es-419.json b/homeassistant/components/izone/translations/es-419.json index b645c427e3e..6b346f7a962 100644 --- a/homeassistant/components/izone/translations/es-419.json +++ b/homeassistant/components/izone/translations/es-419.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u00bfDesea configurar iZone?", - "title": "iZone" + "description": "\u00bfDesea configurar iZone?" } } } diff --git a/homeassistant/components/izone/translations/es.json b/homeassistant/components/izone/translations/es.json index 664cc72d96f..0ae7e16a512 100644 --- a/homeassistant/components/izone/translations/es.json +++ b/homeassistant/components/izone/translations/es.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u00bfQuieres configurar iZone?", - "title": "iZone" + "description": "\u00bfQuieres configurar iZone?" } } } diff --git a/homeassistant/components/izone/translations/fr.json b/homeassistant/components/izone/translations/fr.json index 9fb3163ae15..0c6faf83e6e 100644 --- a/homeassistant/components/izone/translations/fr.json +++ b/homeassistant/components/izone/translations/fr.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Voulez-vous configurer iZone?", - "title": "iZone" + "description": "Voulez-vous configurer iZone?" } } } diff --git a/homeassistant/components/izone/translations/hu.json b/homeassistant/components/izone/translations/hu.json index 76b88ebb6b4..026093232a3 100644 --- a/homeassistant/components/izone/translations/hu.json +++ b/homeassistant/components/izone/translations/hu.json @@ -2,8 +2,7 @@ "config": { "step": { "confirm": { - "description": "Szeretn\u00e9 be\u00e1ll\u00edtani az iZone-t?", - "title": "iZone" + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani az iZone-t?" } } } diff --git a/homeassistant/components/izone/translations/it.json b/homeassistant/components/izone/translations/it.json index 8fd2930e8ff..79151ca44a4 100644 --- a/homeassistant/components/izone/translations/it.json +++ b/homeassistant/components/izone/translations/it.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Vuoi configurare iZone?", - "title": "iZone" + "description": "Vuoi configurare iZone?" } } } diff --git a/homeassistant/components/izone/translations/ko.json b/homeassistant/components/izone/translations/ko.json index 4174226ab55..85aec276562 100644 --- a/homeassistant/components/izone/translations/ko.json +++ b/homeassistant/components/izone/translations/ko.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "iZone \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "iZone" + "description": "iZone \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" } } } diff --git a/homeassistant/components/izone/translations/lb.json b/homeassistant/components/izone/translations/lb.json index 5b0576a12eb..2c8f2573e75 100644 --- a/homeassistant/components/izone/translations/lb.json +++ b/homeassistant/components/izone/translations/lb.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Soll iZone konfigur\u00e9iert ginn?", - "title": "iZone" + "description": "Soll iZone konfigur\u00e9iert ginn?" } } } diff --git a/homeassistant/components/izone/translations/nl.json b/homeassistant/components/izone/translations/nl.json index 21dfa2f9ad4..22d1a3c4963 100644 --- a/homeassistant/components/izone/translations/nl.json +++ b/homeassistant/components/izone/translations/nl.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Wilt u iZone instellen?", - "title": "iZone" + "description": "Wilt u iZone instellen?" } } } diff --git a/homeassistant/components/izone/translations/no.json b/homeassistant/components/izone/translations/no.json index 854805948ee..5c3e4248339 100644 --- a/homeassistant/components/izone/translations/no.json +++ b/homeassistant/components/izone/translations/no.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Vil du \u00e5 sette opp iZone?", - "title": "" + "description": "Vil du \u00e5 sette opp iZone?" } } } diff --git a/homeassistant/components/izone/translations/pl.json b/homeassistant/components/izone/translations/pl.json index 2d0bcdd3292..de8eff21232 100644 --- a/homeassistant/components/izone/translations/pl.json +++ b/homeassistant/components/izone/translations/pl.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Chcesz skonfigurowa\u0107 iZone?", - "title": "iZone" + "description": "Chcesz skonfigurowa\u0107 iZone?" } } } diff --git a/homeassistant/components/izone/translations/ru.json b/homeassistant/components/izone/translations/ru.json index 41c39c32c68..23b0a85370c 100644 --- a/homeassistant/components/izone/translations/ru.json +++ b/homeassistant/components/izone/translations/ru.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c iZone?", - "title": "iZone" + "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 iZone?" } } } diff --git a/homeassistant/components/izone/translations/sl.json b/homeassistant/components/izone/translations/sl.json index 2e05823ebe6..6ce860a74af 100644 --- a/homeassistant/components/izone/translations/sl.json +++ b/homeassistant/components/izone/translations/sl.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Ali \u017eelite nastaviti iZone?", - "title": "iZone" + "description": "Ali \u017eelite nastaviti iZone?" } } } diff --git a/homeassistant/components/izone/translations/sv.json b/homeassistant/components/izone/translations/sv.json index fce7cd70960..473f70d6f94 100644 --- a/homeassistant/components/izone/translations/sv.json +++ b/homeassistant/components/izone/translations/sv.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Vill du konfigurera iZone?", - "title": "iZone" + "description": "Vill du konfigurera iZone?" } } } diff --git a/homeassistant/components/izone/translations/zh-Hant.json b/homeassistant/components/izone/translations/zh-Hant.json index 7f7bb327ea9..283136b5c86 100644 --- a/homeassistant/components/izone/translations/zh-Hant.json +++ b/homeassistant/components/izone/translations/zh-Hant.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a iZone\uff1f", - "title": "iZone" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a iZone\uff1f" } } } diff --git a/homeassistant/components/juicenet/sensor.py b/homeassistant/components/juicenet/sensor.py index e7408cf9f85..c5d37cb8180 100644 --- a/homeassistant/components/juicenet/sensor.py +++ b/homeassistant/components/juicenet/sensor.py @@ -2,6 +2,7 @@ import logging from homeassistant.const import ( + ELECTRICAL_CURRENT_AMPERE, ENERGY_WATT_HOUR, POWER_WATT, TEMP_CELSIUS, @@ -19,7 +20,7 @@ SENSOR_TYPES = { "status": ["Charging Status", None], "temperature": ["Temperature", TEMP_CELSIUS], "voltage": ["Voltage", VOLT], - "amps": ["Amps", "A"], + "amps": ["Amps", ELECTRICAL_CURRENT_AMPERE], "watts": ["Watts", POWER_WATT], "charge_time": ["Charge time", TIME_SECONDS], "energy_added": ["Energy added", ENERGY_WATT_HOUR], diff --git a/homeassistant/components/juicenet/translations/pl.json b/homeassistant/components/juicenet/translations/pl.json index 4da73cd5cbf..601ce0c9128 100644 --- a/homeassistant/components/juicenet/translations/pl.json +++ b/homeassistant/components/juicenet/translations/pl.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "[%key_id:common::config_flow::abort::already_configured_account%]" + "already_configured": "Konto jest ju\u017c skonfigurowane." }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "[%key_id:common::config_flow::error::invalid_auth%]", - "unknown": "[%key_id:common::config_flow::error::unknown%]" + "invalid_auth": "Niepoprawne uwierzytelnienie.", + "unknown": "Nieoczekiwany b\u0142\u0105d." }, "step": { "user": { diff --git a/homeassistant/components/juicenet/translations/pt-BR.json b/homeassistant/components/juicenet/translations/pt-BR.json new file mode 100644 index 00000000000..281a9dc8931 --- /dev/null +++ b/homeassistant/components/juicenet/translations/pt-BR.json @@ -0,0 +1,9 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha ao conectar, tente novamente", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keba/__init__.py b/homeassistant/components/keba/__init__.py index cbc9428b2ea..764110f94b9 100644 --- a/homeassistant/components/keba/__init__.py +++ b/homeassistant/components/keba/__init__.py @@ -12,7 +12,7 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) DOMAIN = "keba" -SUPPORTED_COMPONENTS = ["binary_sensor", "sensor", "lock"] +SUPPORTED_COMPONENTS = ["binary_sensor", "sensor", "lock", "notify"] CONF_RFID = "rfid" CONF_FS = "failsafe" diff --git a/homeassistant/components/keba/manifest.json b/homeassistant/components/keba/manifest.json index 0b1b72d99ab..29c4ec86c49 100644 --- a/homeassistant/components/keba/manifest.json +++ b/homeassistant/components/keba/manifest.json @@ -2,6 +2,6 @@ "domain": "keba", "name": "Keba Charging Station", "documentation": "https://www.home-assistant.io/integrations/keba", - "requirements": ["keba-kecontact==1.0.0"], + "requirements": ["keba-kecontact==1.1.0"], "codeowners": ["@dannerph"] } diff --git a/homeassistant/components/keba/notify.py b/homeassistant/components/keba/notify.py new file mode 100644 index 00000000000..7b3f23277a6 --- /dev/null +++ b/homeassistant/components/keba/notify.py @@ -0,0 +1,33 @@ +"""Support for Keba notifications.""" +import logging + +from homeassistant.components.notify import ATTR_DATA, BaseNotificationService + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_get_service(hass, config, discovery_info=None): + """Return the notify service.""" + + client = hass.data[DOMAIN] + return KebaNotificationService(client) + + +class KebaNotificationService(BaseNotificationService): + """Notification service for KEBA EV Chargers.""" + + def __init__(self, client): + """Initialize the service.""" + self._client = client + + async def async_send_message(self, message="", **kwargs): + """Send the message.""" + text = message.replace(" ", "$") # Will be translated back by the display + + data = kwargs[ATTR_DATA] or {} + min_time = float(data.get("min_time", 2)) + max_time = float(data.get("max_time", 10)) + + await self._client.set_text(text, min_time, max_time) diff --git a/homeassistant/components/keba/sensor.py b/homeassistant/components/keba/sensor.py index d9e6118ff32..443e1bcd1bc 100644 --- a/homeassistant/components/keba/sensor.py +++ b/homeassistant/components/keba/sensor.py @@ -1,7 +1,11 @@ """Support for KEBA charging station sensors.""" import logging -from homeassistant.const import DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR +from homeassistant.const import ( + DEVICE_CLASS_POWER, + ELECTRICAL_CURRENT_AMPERE, + ENERGY_KILO_WATT_HOUR, +) from homeassistant.helpers.entity import Entity from . import DOMAIN @@ -17,7 +21,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= keba = hass.data[DOMAIN] sensors = [ - KebaSensor(keba, "Curr user", "Max Current", "max_current", "mdi:flash", "A"), + KebaSensor( + keba, + "Curr user", + "Max Current", + "max_current", + "mdi:flash", + ELECTRICAL_CURRENT_AMPERE, + ), KebaSensor( keba, "Setenergy", diff --git a/homeassistant/components/keenetic_ndms2/device_tracker.py b/homeassistant/components/keenetic_ndms2/device_tracker.py index 598e29cf583..d98806dfc05 100644 --- a/homeassistant/components/keenetic_ndms2/device_tracker.py +++ b/homeassistant/components/keenetic_ndms2/device_tracker.py @@ -35,7 +35,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_scanner(_hass, config): - """Validate the configuration and return a Nmap scanner.""" + """Validate the configuration and return a Keenetic NDMS2 scanner.""" scanner = KeeneticNDMS2DeviceScanner(config[DOMAIN]) return scanner if scanner.success_init else None diff --git a/homeassistant/components/kef/manifest.json b/homeassistant/components/kef/manifest.json index bb68d37707f..1eb9a9e19c2 100644 --- a/homeassistant/components/kef/manifest.json +++ b/homeassistant/components/kef/manifest.json @@ -3,5 +3,5 @@ "name": "KEF", "documentation": "https://www.home-assistant.io/integrations/kef", "codeowners": ["@basnijholt"], - "requirements": ["aiokef==0.2.9", "getmac==0.8.2"] + "requirements": ["aiokef==0.2.10", "getmac==0.8.2"] } diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index cf87a7dd447..1ba4d63ae4f 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -160,9 +160,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= platform.async_register_entity_service(SERVICE_UPDATE_DSP, {}, "update_dsp") def add_service(name, which, option): + options = DSP_OPTION_MAPPING[which] + dtype = type(options[0]) # int or float platform.async_register_entity_service( name, - {vol.Required(option): vol.In(DSP_OPTION_MAPPING[which])}, + {vol.Required(option): vol.All(vol.Coerce(dtype), vol.In(options))}, f"set_{which}", ) diff --git a/homeassistant/components/konnected/translations/ca.json b/homeassistant/components/konnected/translations/ca.json index d35146410ed..86ef5ab5fc6 100644 --- a/homeassistant/components/konnected/translations/ca.json +++ b/homeassistant/components/konnected/translations/ca.json @@ -20,11 +20,10 @@ }, "user": { "data": { - "host": "Adre\u00e7a IP del dispositiu Konnected", - "port": "Port del dispositiu Konnected" + "host": "Adre\u00e7a IP", + "port": "Port" }, - "description": "Introdueix la informaci\u00f3 d'amfitri\u00f3 del panell Konnected.", - "title": "Descoberta de dispositiu Konnected" + "description": "Introdueix la informaci\u00f3 d'amfitri\u00f3 del panell Konnected." } } }, diff --git a/homeassistant/components/konnected/translations/da.json b/homeassistant/components/konnected/translations/da.json index 48751d0d8dc..e8ff101a25b 100644 --- a/homeassistant/components/konnected/translations/da.json +++ b/homeassistant/components/konnected/translations/da.json @@ -23,8 +23,7 @@ "host": "Konnected-enhedens IP-adresse", "port": "Konnected-enhedsport" }, - "description": "Indtast v\u00e6rtsinformationen for dit Konnected-panel.", - "title": "Find Konnected-enhed" + "description": "Indtast v\u00e6rtsinformationen for dit Konnected-panel." } } }, diff --git a/homeassistant/components/konnected/translations/de.json b/homeassistant/components/konnected/translations/de.json index fd88b143914..3fd857851fb 100644 --- a/homeassistant/components/konnected/translations/de.json +++ b/homeassistant/components/konnected/translations/de.json @@ -23,8 +23,7 @@ "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" + "description": "Bitte geben Sie die Hostinformationen f\u00fcr Ihr Konnected Panel ein." } } }, diff --git a/homeassistant/components/konnected/translations/en.json b/homeassistant/components/konnected/translations/en.json index 694255903c0..12ddb5d3cb1 100644 --- a/homeassistant/components/konnected/translations/en.json +++ b/homeassistant/components/konnected/translations/en.json @@ -23,8 +23,7 @@ "host": "IP address", "port": "Port" }, - "description": "Please enter the host information for your Konnected Panel.", - "title": "Discover Konnected Device" + "description": "Please enter the host information for your Konnected Panel." } } }, diff --git a/homeassistant/components/konnected/translations/es-419.json b/homeassistant/components/konnected/translations/es-419.json index a63ba501960..b333b527fdb 100644 --- a/homeassistant/components/konnected/translations/es-419.json +++ b/homeassistant/components/konnected/translations/es-419.json @@ -23,8 +23,7 @@ "host": "Direcci\u00f3n IP del dispositivo Konnected", "port": "Puerto de dispositivo Konnected" }, - "description": "Ingrese la informaci\u00f3n del host para su Panel Konnected.", - "title": "Descubrir el dispositivo Konnected" + "description": "Ingrese la informaci\u00f3n del host para su Panel Konnected." } } }, diff --git a/homeassistant/components/konnected/translations/es.json b/homeassistant/components/konnected/translations/es.json index 05e3fa3368c..eae14b2ca1a 100644 --- a/homeassistant/components/konnected/translations/es.json +++ b/homeassistant/components/konnected/translations/es.json @@ -23,8 +23,7 @@ "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" + "description": "Introduzca la informaci\u00f3n del host de su panel Konnected." } } }, diff --git a/homeassistant/components/konnected/translations/fr.json b/homeassistant/components/konnected/translations/fr.json index a463803a8fe..c9ab6062daa 100644 --- a/homeassistant/components/konnected/translations/fr.json +++ b/homeassistant/components/konnected/translations/fr.json @@ -15,6 +15,7 @@ "title": "Appareil Konnected pr\u00eat" }, "import_confirm": { + "description": "Un panneau d'alarme Konnected avec l'ID {id} a \u00e9t\u00e9 d\u00e9couvert dans configuration.yaml. Ce flux vous permettra de l'importer dans une entr\u00e9e de configuration.", "title": "Importer un appareil connect\u00e9" }, "user": { @@ -22,8 +23,7 @@ "host": "Adresse IP de l\u2019appareil Konnected", "port": "Port de l'appareil Konnected" }, - "description": "Veuillez saisir les informations de l\u2019h\u00f4te de votre panneau Konnected.", - "title": "D\u00e9couverte d\u2019appareil Konnected" + "description": "Veuillez saisir les informations de l\u2019h\u00f4te de votre panneau Konnected." } } }, @@ -78,9 +78,14 @@ "alarm2_out2": "OUT2/ALARM2", "out1": "OUT1" }, + "description": "S\u00e9lectionnez la configuration des E / S restantes ci-dessous. Vous pourrez configurer des options d\u00e9taill\u00e9es dans les \u00e9tapes suivantes.", "title": "Configurer les E/S \u00e9tendues" }, "options_misc": { + "data": { + "blink": "Voyant du panneau clignotant lors de l'envoi d'un changement d'\u00e9tat" + }, + "description": "Veuillez s\u00e9lectionner le comportement souhat\u00e9 de votre panneau", "title": "Configurer divers" }, "options_switch": { @@ -91,7 +96,9 @@ "name": "Nom (facultatif)", "pause": "Pause entre les impulsions (ms) (facultatif)", "repeat": "Nombre de r\u00e9p\u00e9tition (-1=infini) (facultatif)" - } + }, + "description": "Veuillez s\u00e9lectionner les options de sortie pour {zone} : \u00e9tat {state}", + "title": "Configurer la sortie commutable" } } } diff --git a/homeassistant/components/konnected/translations/hu.json b/homeassistant/components/konnected/translations/hu.json index b7d4017a87e..1cc44a02646 100644 --- a/homeassistant/components/konnected/translations/hu.json +++ b/homeassistant/components/konnected/translations/hu.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "user": { + "data": { + "host": "IP c\u00edm", + "port": "Port" + } + } + } + }, "options": { "step": { "options_digital": { diff --git a/homeassistant/components/konnected/translations/it.json b/homeassistant/components/konnected/translations/it.json index 84fdcac4880..bdef475dc6e 100644 --- a/homeassistant/components/konnected/translations/it.json +++ b/homeassistant/components/konnected/translations/it.json @@ -20,11 +20,10 @@ }, "user": { "data": { - "host": "Indirizzo IP del dispositivo Konnected", - "port": "Porta del dispositivo Konnected" + "host": "Indirizzo IP", + "port": "Porta" }, - "description": "Si prega di inserire le informazioni dell'host per il tuo Pannello Konnected.", - "title": "Rileva il dispositivo Konnected" + "description": "Si prega di inserire le informazioni dell'host per il tuo Pannello Konnected." } } }, diff --git a/homeassistant/components/konnected/translations/ko.json b/homeassistant/components/konnected/translations/ko.json index ba36f231141..811c134255f 100644 --- a/homeassistant/components/konnected/translations/ko.json +++ b/homeassistant/components/konnected/translations/ko.json @@ -23,8 +23,7 @@ "host": "IP \uc8fc\uc18c", "port": "\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" + "description": "Konnected \ud328\ub110\uc758 \ud638\uc2a4\ud2b8 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694." } } }, diff --git a/homeassistant/components/konnected/translations/lb.json b/homeassistant/components/konnected/translations/lb.json index 7e8ed675992..1ef36601d04 100644 --- a/homeassistant/components/konnected/translations/lb.json +++ b/homeassistant/components/konnected/translations/lb.json @@ -23,8 +23,7 @@ "host": "Konnected Apparat IP Adress", "port": "Konnected Apparat Port" }, - "description": "Informatioune vum Konnected Panel aginn.", - "title": "Konnected Apparat entdecken" + "description": "Informatioune vum Konnected Panel aginn." } } }, diff --git a/homeassistant/components/konnected/translations/nl.json b/homeassistant/components/konnected/translations/nl.json index 17bb20be765..dcb5f1ed6c4 100644 --- a/homeassistant/components/konnected/translations/nl.json +++ b/homeassistant/components/konnected/translations/nl.json @@ -17,8 +17,7 @@ "host": "IP-adres van Konnected apparaat", "port": "Konnected apparaat poort" }, - "description": "Voer de host-informatie in voor uw Konnected-paneel.", - "title": "Ontdek Konnected Device" + "description": "Voer de host-informatie in voor uw Konnected-paneel." } } }, diff --git a/homeassistant/components/konnected/translations/no.json b/homeassistant/components/konnected/translations/no.json index b0504c4e3aa..391bf848673 100644 --- a/homeassistant/components/konnected/translations/no.json +++ b/homeassistant/components/konnected/translations/no.json @@ -23,8 +23,7 @@ "host": "Konnected enhet IP-adresse", "port": "Koblet enhetsport" }, - "description": "Vennligst skriv inn verten informasjon for din Konnected Panel.", - "title": "Oppdag Konnected Enheten" + "description": "Vennligst skriv inn verten informasjon for din Konnected Panel." } } }, diff --git a/homeassistant/components/konnected/translations/pl.json b/homeassistant/components/konnected/translations/pl.json index a71d458bd32..1f9b60bbae3 100644 --- a/homeassistant/components/konnected/translations/pl.json +++ b/homeassistant/components/konnected/translations/pl.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]", + "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": "[%key_id:common::config_flow::error::unknown%]" + "not_konn_panel": "Nie rozpoznano urz\u0105dzenia Konnected.io.", + "unknown": "Nieoczekiwany b\u0142\u0105d." }, "error": { "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z panelem Konnected na {host}:{port}" @@ -20,11 +20,10 @@ }, "user": { "data": { - "host": "Adres IP urz\u0105dzenia Konnected", - "port": "[%key_id:common::config_flow::data::port%] urz\u0105dzenia Konnected" + "host": "Adres IP", + "port": "Port" }, - "description": "Wprowad\u017a informacje o ho\u015bcie panelu Konnected.", - "title": "Wykryj urz\u0105dzenie Konnected" + "description": "Wprowad\u017a informacje o ho\u015bcie panelu Konnected." } } }, @@ -90,7 +89,7 @@ "data": { "api_host": "Zast\u0119powanie adresu URL hosta API (opcjonalnie)", "blink": "Miganie diody LED panelu podczas wysy\u0142ania zmiany stanu", - "override_api_host": "Zast\u0105p domy\u015blny adres URL API Home Assistant'a" + "override_api_host": "Zast\u0105p domy\u015blny adres URL API Home Assistanta" }, "description": "Wybierz po\u017c\u0105dane zachowanie dla swojego panelu", "title": "R\u00f3\u017cne opcje" diff --git a/homeassistant/components/konnected/translations/pt-BR.json b/homeassistant/components/konnected/translations/pt-BR.json new file mode 100644 index 00000000000..b31bd6feb8a --- /dev/null +++ b/homeassistant/components/konnected/translations/pt-BR.json @@ -0,0 +1,65 @@ +{ + "config": { + "step": { + "user": { + "description": "Por favor, digite as informa\u00e7\u00f5es do host para o seu Painel Konnected." + } + } + }, + "options": { + "abort": { + "not_konn_panel": "N\u00e3o \u00e9 um dispositivo Konnected.io reconhecido" + }, + "error": { + "bad_host": "URL de host da API de substitui\u00e7\u00e3o inv\u00e1lido" + }, + "step": { + "options_binary": { + "data": { + "inverse": "Inverter o estado aberto/fechado", + "name": "Nome (opcional)", + "type": "Tipo de sensor bin\u00e1rio" + }, + "description": "Selecione as op\u00e7\u00f5es para o sensor bin\u00e1rio conectado a {zone}", + "title": "Configurar sensor bin\u00e1rio" + }, + "options_digital": { + "data": { + "name": "Nome (opcional)", + "poll_interval": "Intervalo de vota\u00e7\u00e3o (minutos) (opcional)", + "type": "Tipo de sensor" + }, + "description": "Selecione as op\u00e7\u00f5es para o sensor digital conectado a {zone}", + "title": "Configurar 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": "SA\u00cdDA" + }, + "title": "Configurar I/O" + }, + "options_io_ext": { + "data": { + "10": "Zona 10", + "11": "Zona 11", + "12": "Zona 12", + "8": "Zona 8", + "9": "Zona 9" + } + }, + "options_misc": { + "data": { + "api_host": "Substituir URL do host da API (opcional)", + "override_api_host": "Substituir o URL padr\u00e3o do painel do host da API do Home Assistant" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/ru.json b/homeassistant/components/konnected/translations/ru.json index 75ee7761d55..d3c2816963c 100644 --- a/homeassistant/components/konnected/translations/ru.json +++ b/homeassistant/components/konnected/translations/ru.json @@ -23,8 +23,7 @@ "host": "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": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Konnected" + "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." } } }, diff --git a/homeassistant/components/konnected/translations/sl.json b/homeassistant/components/konnected/translations/sl.json index 0a6a52d2fb1..88e0b696416 100644 --- a/homeassistant/components/konnected/translations/sl.json +++ b/homeassistant/components/konnected/translations/sl.json @@ -23,8 +23,7 @@ "host": "IP-naslov Konnected naprave", "port": "Vrata Konnected naprave" }, - "description": "Vnesite podatke o gostitelju v svoj Konnected Panel.", - "title": "Odkrijte Konnected napravo" + "description": "Vnesite podatke o gostitelju v svoj Konnected Panel." } } }, diff --git a/homeassistant/components/konnected/translations/sv.json b/homeassistant/components/konnected/translations/sv.json index 3e6136fb2c3..3b875e17738 100644 --- a/homeassistant/components/konnected/translations/sv.json +++ b/homeassistant/components/konnected/translations/sv.json @@ -19,8 +19,7 @@ "host": "Konnected-enhetens IP-adress", "port": "Konnected-enhetens port" }, - "description": "Ange v\u00e4rdinformationen f\u00f6r din Konnected Panel.", - "title": "Uppt\u00e4ck Konnected-enhet" + "description": "Ange v\u00e4rdinformationen f\u00f6r din Konnected Panel." } } }, diff --git a/homeassistant/components/konnected/translations/zh-Hant.json b/homeassistant/components/konnected/translations/zh-Hant.json index 4660e3c2f29..45d57eee3c4 100644 --- a/homeassistant/components/konnected/translations/zh-Hant.json +++ b/homeassistant/components/konnected/translations/zh-Hant.json @@ -23,8 +23,7 @@ "host": "IP \u4f4d\u5740", "port": "\u901a\u8a0a\u57e0" }, - "description": "\u8acb\u8f38\u5165 Konnected \u9762\u677f\u4e3b\u6a5f\u7aef\u8cc7\u8a0a\u3002", - "title": "\u641c\u7d22 Konnected \u8a2d\u5099" + "description": "\u8acb\u8f38\u5165 Konnected \u9762\u677f\u4e3b\u6a5f\u7aef\u8cc7\u8a0a\u3002" } } }, diff --git a/homeassistant/components/lametric/__init__.py b/homeassistant/components/lametric/__init__.py index 9281affa492..797f0982f00 100644 --- a/homeassistant/components/lametric/__init__.py +++ b/homeassistant/components/lametric/__init__.py @@ -4,14 +4,14 @@ import logging from lmnotify import LaMetricManager import voluptuous as vol +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -CONF_CLIENT_ID = "client_id" -CONF_CLIENT_SECRET = "client_secret" DOMAIN = "lametric" + LAMETRIC_DEVICES = "LAMETRIC_DEVICES" CONFIG_SCHEMA = vol.Schema( diff --git a/homeassistant/components/lcn/cover.py b/homeassistant/components/lcn/cover.py index 05ee17a7daf..a5bea69e9ae 100644 --- a/homeassistant/components/lcn/cover.py +++ b/homeassistant/components/lcn/cover.py @@ -122,7 +122,9 @@ class LcnRelayCover(LcnDevice, CoverEntity): self.motor_port_onoff = self.motor.value * 2 self.motor_port_updown = self.motor_port_onoff + 1 - self._closed = None + self._is_closed = False + self._is_closing = False + self._is_opening = False async def async_added_to_hass(self): """Run when entity about to be added to hass.""" @@ -132,11 +134,28 @@ class LcnRelayCover(LcnDevice, CoverEntity): @property def is_closed(self): """Return if the cover is closed.""" - return self._closed + return self._is_closed + + @property + def is_opening(self): + """Return if the cover is opening or not.""" + return self._is_opening + + @property + def is_closing(self): + """Return if the cover is closing or not.""" + return self._is_closing + + @property + def assumed_state(self): + """Return True if unable to access real state of the entity.""" + return True async def async_close_cover(self, **kwargs): """Close the cover.""" - self._closed = True + self._is_closed = True + self._is_opening = False + self._is_closing = True states = [pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4 states[self.motor.value] = pypck.lcn_defs.MotorStateModifier.DOWN self.address_connection.control_motors_relays(states) @@ -144,7 +163,9 @@ class LcnRelayCover(LcnDevice, CoverEntity): async def async_open_cover(self, **kwargs): """Open the cover.""" - self._closed = False + self._is_closed = False + self._is_opening = True + self._is_closing = False states = [pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4 states[self.motor.value] = pypck.lcn_defs.MotorStateModifier.UP self.address_connection.control_motors_relays(states) @@ -152,7 +173,10 @@ class LcnRelayCover(LcnDevice, CoverEntity): async def async_stop_cover(self, **kwargs): """Stop the cover.""" - self._closed = None + if self._is_opening or self._is_closing: + self._is_closed = self._is_closing + self._is_closing = False + self._is_opening = False states = [pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4 states[self.motor.value] = pypck.lcn_defs.MotorStateModifier.STOP self.address_connection.control_motors_relays(states) @@ -165,6 +189,11 @@ class LcnRelayCover(LcnDevice, CoverEntity): states = input_obj.states # list of boolean values (relay on/off) if states[self.motor_port_onoff]: # motor is on - self._closed = states[self.motor_port_updown] # set direction + self._is_opening = not states[self.motor_port_updown] # set direction + self._is_closing = states[self.motor_port_updown] # set direction + self._is_closed = self._is_closing + else: + self._is_opening = False + self._is_closing = False self.async_write_ha_state() diff --git a/homeassistant/components/lg_soundbar/media_player.py b/homeassistant/components/lg_soundbar/media_player.py index f3c89d6138e..a10b46f89ce 100644 --- a/homeassistant/components/lg_soundbar/media_player.py +++ b/homeassistant/components/lg_soundbar/media_player.py @@ -114,6 +114,11 @@ class LGDevice(MediaPlayerEntity): self._device.get_settings() self._device.get_product_info() + # Temporary fix until handling of unknown equaliser settings is integrated in the temescal library + for equaliser in self._equalisers: + if equaliser >= len(temescal.equalisers): + temescal.equalisers.append("unknown " + str(equaliser)) + @property def name(self): """Return the name of the device.""" @@ -139,9 +144,8 @@ class LGDevice(MediaPlayerEntity): @property def sound_mode(self): """Return the current sound mode.""" - - if self._equaliser == -1: - return "" + if self._equaliser == -1 or self._equaliser >= len(temescal.equalisers): + return None return temescal.equalisers[self._equaliser] @property @@ -156,7 +160,7 @@ class LGDevice(MediaPlayerEntity): def source(self): """Return the current input source.""" if self._function == -1: - return "" + return None return temescal.functions[self._function] @property diff --git a/homeassistant/components/life360/translations/hu.json b/homeassistant/components/life360/translations/hu.json index 7f158a24622..091e23e0d43 100644 --- a/homeassistant/components/life360/translations/hu.json +++ b/homeassistant/components/life360/translations/hu.json @@ -1,8 +1,12 @@ { "config": { + "abort": { + "user_already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + }, "error": { "invalid_username": "\u00c9rv\u00e9nytelen felhaszn\u00e1l\u00f3n\u00e9v", - "unexpected": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt a kommunik\u00e1ci\u00f3ban a Life360 szerverrel" + "unexpected": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt a kommunik\u00e1ci\u00f3ban a Life360 szerverrel", + "user_already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" }, "step": { "user": { diff --git a/homeassistant/components/life360/translations/pl.json b/homeassistant/components/life360/translations/pl.json index 1bf35e8853b..19a6c6d8828 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": "[%key_id:common::config_flow::abort::already_configured_account%]" + "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})." @@ -16,8 +16,8 @@ "step": { "user": { "data": { - "password": "[%key_id:common::config_flow::data::password%]", - "username": "[%key_id:common::config_flow::data::username%]" + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" }, "description": "Aby skonfigurowa\u0107 zaawansowane ustawienia, zapoznaj si\u0119 z [dokumentacj\u0105 Life360]({docs_url}). Mo\u017cesz to zrobi\u0107 przed dodaniem kont.", "title": "Informacje o koncie Life360" diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index f36b64f2397..2b7629cdaf2 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -371,6 +371,12 @@ class LIFXManager: # Read initial state ack = AwaitAioLIFX().wait + + # Used to populate sw_version + # no need to wait as we do not + # need it until later + bulb.get_hostfirmware() + color_resp = await ack(bulb.get_color) if color_resp: version_resp = await ack(bulb.get_version) @@ -459,7 +465,13 @@ class LIFXLight(LightEntity): "manufacturer": "LIFX", } - model = aiolifx().products.product_map.get(self.bulb.product) + version = self.bulb.host_firmware_version + if version is not None: + info["sw_version"] = version + + product_map = aiolifx().products.product_map + + model = product_map.get(self.bulb.product) or self.bulb.product if model is not None: info["model"] = model diff --git a/homeassistant/components/lifx/translations/bg.json b/homeassistant/components/lifx/translations/bg.json index e44a4524eef..e7ce46d836e 100644 --- a/homeassistant/components/lifx/translations/bg.json +++ b/homeassistant/components/lifx/translations/bg.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 LIFX?", - "title": "LIFX" + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 LIFX?" } } } diff --git a/homeassistant/components/lifx/translations/ca.json b/homeassistant/components/lifx/translations/ca.json index a9a45f1f485..edc525a92cb 100644 --- a/homeassistant/components/lifx/translations/ca.json +++ b/homeassistant/components/lifx/translations/ca.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Vols configurar LIFX?", - "title": "LIFX" + "description": "Vols configurar LIFX?" } } } diff --git a/homeassistant/components/lifx/translations/cs.json b/homeassistant/components/lifx/translations/cs.json index 9ebb8c43b42..7deccd0eac8 100644 --- a/homeassistant/components/lifx/translations/cs.json +++ b/homeassistant/components/lifx/translations/cs.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Chcete nastavit LIFX?", - "title": "LIFX" + "description": "Chcete nastavit LIFX?" } } } diff --git a/homeassistant/components/lifx/translations/da.json b/homeassistant/components/lifx/translations/da.json index b6faac51acc..14fbf83cbed 100644 --- a/homeassistant/components/lifx/translations/da.json +++ b/homeassistant/components/lifx/translations/da.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Konfigurer LIFX?", - "title": "LIFX" + "description": "Konfigurer LIFX?" } } } diff --git a/homeassistant/components/lifx/translations/de.json b/homeassistant/components/lifx/translations/de.json index 654fbe82aeb..f88e27ff168 100644 --- a/homeassistant/components/lifx/translations/de.json +++ b/homeassistant/components/lifx/translations/de.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "M\u00f6chtest du LIFX einrichten?", - "title": "LIFX" + "description": "M\u00f6chtest du LIFX einrichten?" } } } diff --git a/homeassistant/components/lifx/translations/en.json b/homeassistant/components/lifx/translations/en.json index fe7daaaed7d..ab4d5458d82 100644 --- a/homeassistant/components/lifx/translations/en.json +++ b/homeassistant/components/lifx/translations/en.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Do you want to set up LIFX?", - "title": "LIFX" + "description": "Do you want to set up LIFX?" } } } diff --git a/homeassistant/components/lifx/translations/es-419.json b/homeassistant/components/lifx/translations/es-419.json index 8676326e995..023cec6a6db 100644 --- a/homeassistant/components/lifx/translations/es-419.json +++ b/homeassistant/components/lifx/translations/es-419.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u00bfDesea configurar LIFX?", - "title": "LIFX" + "description": "\u00bfDesea configurar LIFX?" } } } diff --git a/homeassistant/components/lifx/translations/es.json b/homeassistant/components/lifx/translations/es.json index 5ed177125fc..c5b157a1499 100644 --- a/homeassistant/components/lifx/translations/es.json +++ b/homeassistant/components/lifx/translations/es.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u00bfQuieres configurar LIFX?", - "title": "LIFX" + "description": "\u00bfQuieres configurar LIFX?" } } } diff --git a/homeassistant/components/lifx/translations/fi.json b/homeassistant/components/lifx/translations/fi.json index 16fffbb2e5c..a92bc699280 100644 --- a/homeassistant/components/lifx/translations/fi.json +++ b/homeassistant/components/lifx/translations/fi.json @@ -2,8 +2,7 @@ "config": { "step": { "confirm": { - "description": "Haluatko m\u00e4\u00e4ritt\u00e4\u00e4 LIFX:n?", - "title": "LIFX" + "description": "Haluatko m\u00e4\u00e4ritt\u00e4\u00e4 LIFX:n?" } } } diff --git a/homeassistant/components/lifx/translations/fr.json b/homeassistant/components/lifx/translations/fr.json index 60dc00a32fe..837ca29a314 100644 --- a/homeassistant/components/lifx/translations/fr.json +++ b/homeassistant/components/lifx/translations/fr.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Voulez-vous configurer LIFX?", - "title": "LIFX" + "description": "Voulez-vous configurer LIFX?" } } } diff --git a/homeassistant/components/lifx/translations/hu.json b/homeassistant/components/lifx/translations/hu.json index 9b8fe3632c2..0b6cdb39fd4 100644 --- a/homeassistant/components/lifx/translations/hu.json +++ b/homeassistant/components/lifx/translations/hu.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a LIFX-t?", - "title": "LIFX" + "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a LIFX-t?" } } } diff --git a/homeassistant/components/lifx/translations/it.json b/homeassistant/components/lifx/translations/it.json index 380acd0a875..40b4c98f490 100644 --- a/homeassistant/components/lifx/translations/it.json +++ b/homeassistant/components/lifx/translations/it.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Vuoi configurare LIFX?", - "title": "LIFX" + "description": "Vuoi configurare LIFX?" } } } diff --git a/homeassistant/components/lifx/translations/ko.json b/homeassistant/components/lifx/translations/ko.json index 13a5ca08152..040ac405e2d 100644 --- a/homeassistant/components/lifx/translations/ko.json +++ b/homeassistant/components/lifx/translations/ko.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "LIFX \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "LIFX" + "description": "LIFX \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" } } } diff --git a/homeassistant/components/lifx/translations/lb.json b/homeassistant/components/lifx/translations/lb.json index 8bf15060ed4..0dcc857010c 100644 --- a/homeassistant/components/lifx/translations/lb.json +++ b/homeassistant/components/lifx/translations/lb.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Soll LIFX konfigur\u00e9iert ginn?", - "title": "LIFX" + "description": "Soll LIFX konfigur\u00e9iert ginn?" } } } diff --git a/homeassistant/components/lifx/translations/nl.json b/homeassistant/components/lifx/translations/nl.json index e8a42bd5676..60efcdffa46 100644 --- a/homeassistant/components/lifx/translations/nl.json +++ b/homeassistant/components/lifx/translations/nl.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Wilt u LIFX instellen?", - "title": "LIFX" + "description": "Wilt u LIFX instellen?" } } } diff --git a/homeassistant/components/lifx/translations/no.json b/homeassistant/components/lifx/translations/no.json index e646db4b2ad..708efba9cc7 100644 --- a/homeassistant/components/lifx/translations/no.json +++ b/homeassistant/components/lifx/translations/no.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u00d8nsker du \u00e5 sette opp LIFX?", - "title": "" + "description": "\u00d8nsker du \u00e5 sette opp LIFX?" } } } diff --git a/homeassistant/components/lifx/translations/pl.json b/homeassistant/components/lifx/translations/pl.json index 53e623e05df..d24cfd2473e 100644 --- a/homeassistant/components/lifx/translations/pl.json +++ b/homeassistant/components/lifx/translations/pl.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Czy chcesz skonfigurowa\u0107 LIFX?", - "title": "LIFX" + "description": "Czy chcesz skonfigurowa\u0107 LIFX?" } } } diff --git a/homeassistant/components/lifx/translations/pt-BR.json b/homeassistant/components/lifx/translations/pt-BR.json index e95f452c1e4..cf374894623 100644 --- a/homeassistant/components/lifx/translations/pt-BR.json +++ b/homeassistant/components/lifx/translations/pt-BR.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Voc\u00ea quer configurar o LIFX?", - "title": "LIFX" + "description": "Voc\u00ea quer configurar o LIFX?" } } } diff --git a/homeassistant/components/lifx/translations/pt.json b/homeassistant/components/lifx/translations/pt.json index 73fdb846b1e..56064a70e0d 100644 --- a/homeassistant/components/lifx/translations/pt.json +++ b/homeassistant/components/lifx/translations/pt.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Deseja configurar o LIFX?", - "title": "LIFX" + "description": "Deseja configurar o LIFX?" } } } diff --git a/homeassistant/components/lifx/translations/ro.json b/homeassistant/components/lifx/translations/ro.json index 6ef2bd8697c..56e9307a8b3 100644 --- a/homeassistant/components/lifx/translations/ro.json +++ b/homeassistant/components/lifx/translations/ro.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Dori\u021bi s\u0103 configura\u021bi LIFX?", - "title": "LIFX" + "description": "Dori\u021bi s\u0103 configura\u021bi LIFX?" } } } diff --git a/homeassistant/components/lifx/translations/ru.json b/homeassistant/components/lifx/translations/ru.json index a7f6dfbd6a8..9ffd9a95270 100644 --- a/homeassistant/components/lifx/translations/ru.json +++ b/homeassistant/components/lifx/translations/ru.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c LIFX?", - "title": "LIFX" + "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 LIFX?" } } } diff --git a/homeassistant/components/lifx/translations/sl.json b/homeassistant/components/lifx/translations/sl.json index 5bcb8b328c4..dbbe051afd1 100644 --- a/homeassistant/components/lifx/translations/sl.json +++ b/homeassistant/components/lifx/translations/sl.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Ali \u017eelite nastaviti LIFX?", - "title": "LIFX" + "description": "Ali \u017eelite nastaviti LIFX?" } } } diff --git a/homeassistant/components/lifx/translations/sv.json b/homeassistant/components/lifx/translations/sv.json index c85449078de..82a55b48edb 100644 --- a/homeassistant/components/lifx/translations/sv.json +++ b/homeassistant/components/lifx/translations/sv.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Vill du st\u00e4lla in LIFX?", - "title": "LIFX" + "description": "Vill du st\u00e4lla in LIFX?" } } } diff --git a/homeassistant/components/lifx/translations/zh-Hans.json b/homeassistant/components/lifx/translations/zh-Hans.json index 6d30adcd453..bf9b4277312 100644 --- a/homeassistant/components/lifx/translations/zh-Hans.json +++ b/homeassistant/components/lifx/translations/zh-Hans.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u60a8\u60f3\u8981\u914d\u7f6e LIFX \u5417\uff1f", - "title": "LIFX" + "description": "\u60a8\u60f3\u8981\u914d\u7f6e LIFX \u5417\uff1f" } } } diff --git a/homeassistant/components/lifx/translations/zh-Hant.json b/homeassistant/components/lifx/translations/zh-Hant.json index fc9e407b4b2..bc20e93c596 100644 --- a/homeassistant/components/lifx/translations/zh-Hant.json +++ b/homeassistant/components/lifx/translations/zh-Hant.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a LIFX\uff1f", - "title": "LIFX" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a LIFX\uff1f" } } } diff --git a/homeassistant/components/light/translations/ca.json b/homeassistant/components/light/translations/ca.json index ce5bb5c3c7b..1e91f5005ce 100644 --- a/homeassistant/components/light/translations/ca.json +++ b/homeassistant/components/light/translations/ca.json @@ -19,9 +19,9 @@ }, "state": { "_": { - "off": "Apagada", - "on": "Encesa" + "off": "OFF", + "on": "ON" } }, - "title": "Llums" + "title": "Llum" } \ No newline at end of file diff --git a/homeassistant/components/light/translations/pt-BR.json b/homeassistant/components/light/translations/pt-BR.json index 207305ec08b..27b9b46297a 100644 --- a/homeassistant/components/light/translations/pt-BR.json +++ b/homeassistant/components/light/translations/pt-BR.json @@ -8,6 +8,10 @@ "condition_type": { "is_off": "{entity_name} est\u00e1 desligado", "is_on": "{entity_name} est\u00e1 ligado" + }, + "trigger_type": { + "turned_off": "{entity_name} desligado", + "turned_on": "{entity_name} ligado" } }, "state": { diff --git a/homeassistant/components/linky/translations/pl.json b/homeassistant/components/linky/translations/pl.json index 5452a549ec1..1fc09298fd7 100644 --- a/homeassistant/components/linky/translations/pl.json +++ b/homeassistant/components/linky/translations/pl.json @@ -12,8 +12,8 @@ "step": { "user": { "data": { - "password": "[%key_id:common::config_flow::data::password%]", - "username": "[%key_id:common::config_flow::data::email%]" + "password": "Has\u0142o", + "username": "Adres e-mail" }, "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce", "title": "Linky" diff --git a/homeassistant/components/llamalab_automate/notify.py b/homeassistant/components/llamalab_automate/notify.py index f093edbbc6b..b4ed9a4e628 100644 --- a/homeassistant/components/llamalab_automate/notify.py +++ b/homeassistant/components/llamalab_automate/notify.py @@ -4,13 +4,19 @@ import logging import requests import voluptuous as vol -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService +from homeassistant.components.notify import ( + ATTR_DATA, + PLATFORM_SCHEMA, + BaseNotificationService, +) from homeassistant.const import CONF_API_KEY, CONF_DEVICE, HTTP_OK from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) _RESOURCE = "https://llamalab.com/automate/cloud/message" +ATTR_PRIORITY = "priority" + CONF_TO = "to" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -42,11 +48,20 @@ class AutomateNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a user.""" - _LOGGER.debug("Sending to: %s, %s", self._recipient, str(self._device)) + + # Extract params from data dict + data = dict(kwargs.get(ATTR_DATA) or {}) + priority = data.get(ATTR_PRIORITY, "Normal") + + _LOGGER.debug( + "Sending to: %s, %s, prio: %s", self._recipient, str(self._device), priority + ) + data = { "secret": self._secret, "to": self._recipient, "device": self._device, + "priority": priority, "payload": message, } diff --git a/homeassistant/components/local_ip/translations/pt-BR.json b/homeassistant/components/local_ip/translations/pt-BR.json index 24246ca5732..be06de8b7f6 100644 --- a/homeassistant/components/local_ip/translations/pt-BR.json +++ b/homeassistant/components/local_ip/translations/pt-BR.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "Somente uma \u00fanica configura\u00e7\u00e3o do IP local \u00e9 permitida." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/locative/translations/it.json b/homeassistant/components/locative/translations/it.json index c023cd832b9..210fbfe4c28 100644 --- a/homeassistant/components/locative/translations/it.json +++ b/homeassistant/components/locative/translations/it.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u00c8 necessaria una sola istanza." }, "create_entry": { - "default": "Per inviare localit\u00e0 a Home Assistant, dovrai configurare la funzionalit\u00e0 Webhook nell'app Locative.\n\n Compila le seguenti informazioni: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n\n Vedi [la documentazione]({docs_url}) for ulteriori dettagli." + "default": "Per inviare localit\u00e0 a Home Assistant, dovrai configurare la funzionalit\u00e0 Webhook nell'app Locative.\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Method: POST \n\n Vedi [la documentazione]({docs_url}) for ulteriori dettagli." }, "step": { "user": { diff --git a/homeassistant/components/locative/translations/pl.json b/homeassistant/components/locative/translations/pl.json index 7294b31ef74..1323b1e284b 100644 --- a/homeassistant/components/locative/translations/pl.json +++ b/homeassistant/components/locative/translations/pl.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Wymagana jest tylko jedna instancja." }, "create_entry": { - "default": "Aby wysy\u0142a\u0107 lokalizacje do Home Assistant'a, musisz skonfigurowa\u0107 webhook w aplikacji Locative. \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}), by pozna\u0107 szczeg\u00f3\u0142y." + "default": "Aby wysy\u0142a\u0107 lokalizacje do Home Assistanta, musisz skonfigurowa\u0107 webhook w aplikacji Locative. \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}), by pozna\u0107 szczeg\u00f3\u0142y." }, "step": { "user": { diff --git a/homeassistant/components/lock/translations/ca.json b/homeassistant/components/lock/translations/ca.json index a59841298fa..bd8beb73f85 100644 --- a/homeassistant/components/lock/translations/ca.json +++ b/homeassistant/components/lock/translations/ca.json @@ -20,5 +20,5 @@ "unlocked": "Desbloquejat" } }, - "title": "Panys" + "title": "Pany" } \ No newline at end of file diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index cd6a06720ae..82506c35b3b 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -22,7 +22,6 @@ from homeassistant.const import ( ATTR_NAME, CONF_EXCLUDE, CONF_INCLUDE, - EVENT_AUTOMATION_TRIGGERED, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_LOGBOOK_ENTRY, @@ -82,7 +81,6 @@ ALL_EVENT_TYPES = [ EVENT_LOGBOOK_ENTRY, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - EVENT_AUTOMATION_TRIGGERED, EVENT_SCRIPT_STARTED, ] @@ -316,17 +314,6 @@ def humanify(hass, events): "context_user_id": event.context.user_id, } - elif event.event_type == EVENT_AUTOMATION_TRIGGERED: - yield { - "when": event.time_fired, - "name": event.data.get(ATTR_NAME), - "message": "has been triggered", - "domain": "automation", - "entity_id": event.data.get(ATTR_ENTITY_ID), - "context_id": event.context.id, - "context_user_id": event.context.user_id, - } - elif event.event_type == EVENT_SCRIPT_STARTED: yield { "when": event.time_fired, @@ -461,10 +448,6 @@ def _keep_event(hass, event, entities_filter): domain = event.data.get(ATTR_DOMAIN) entity_id = event.data.get(ATTR_ENTITY_ID) - elif event.event_type == EVENT_AUTOMATION_TRIGGERED: - domain = "automation" - entity_id = event.data.get(ATTR_ENTITY_ID) - elif event.event_type == EVENT_SCRIPT_STARTED: domain = "script" entity_id = event.data.get(ATTR_ENTITY_ID) diff --git a/homeassistant/components/logger/__init__.py b/homeassistant/components/logger/__init__.py index 961718a30ee..ddf41640e6f 100644 --- a/homeassistant/components/logger/__init__.py +++ b/homeassistant/components/logger/__init__.py @@ -1,6 +1,6 @@ """Support for setting the level of logging for components.""" -from collections import OrderedDict import logging +import re import voluptuous as vol @@ -50,33 +50,54 @@ CONFIG_SCHEMA = vol.Schema( class HomeAssistantLogFilter(logging.Filter): """A log filter.""" - def __init__(self, logfilter): + def __init__(self): """Initialize the filter.""" super().__init__() - self.logfilter = logfilter + self._default = None + self._logs = None + self._log_rx = None + + def update_default_level(self, default_level): + """Update the default logger level.""" + self._default = default_level + + def update_log_filter(self, logs): + """Rebuild the internal filter from new config.""" + # + # A precompiled regex is used to avoid + # the overhead of a list transversal + # + # Sort to make sure the longer + # names are always matched first + # so they take precedence of the shorter names + # to allow for more granular settings. + # + names_by_len = sorted(list(logs), key=len, reverse=True) + self._log_rx = re.compile("".join(["^(?:", "|".join(names_by_len), ")"])) + self._logs = logs def filter(self, record): """Filter the log entries.""" # Log with filtered severity - if LOGGER_LOGS in self.logfilter: - for filtername in self.logfilter[LOGGER_LOGS]: - logseverity = self.logfilter[LOGGER_LOGS][filtername] - if record.name.startswith(filtername): - return record.levelno >= logseverity + if self._log_rx: + match = self._log_rx.match(record.name) + if match: + return record.levelno >= self._logs[match.group(0)] # Log with default severity - default = self.logfilter[LOGGER_DEFAULT] - return record.levelno >= default + return record.levelno >= self._default async def async_setup(hass, config): """Set up the logger component.""" logfilter = {} + hass_filter = HomeAssistantLogFilter() def set_default_log_level(level): """Set the default log level for components.""" logfilter[LOGGER_DEFAULT] = LOGSEVERITY[level] + hass_filter.update_default_level(LOGSEVERITY[level]) def set_log_levels(logpoints): """Set the specified log levels.""" @@ -90,9 +111,9 @@ async def async_setup(hass, config): for key, value in logpoints.items(): logs[key] = LOGSEVERITY[value] - logfilter[LOGGER_LOGS] = OrderedDict( - sorted(logs.items(), key=lambda t: len(t[0]), reverse=True) - ) + logfilter[LOGGER_LOGS] = logs + + hass_filter.update_log_filter(logs) # Set default log severity if LOGGER_DEFAULT in config.get(DOMAIN): @@ -106,7 +127,7 @@ async def async_setup(hass, config): # Set log filter for all log handler for handler in logging.root.handlers: handler.setLevel(logging.NOTSET) - handler.addFilter(HomeAssistantLogFilter(logfilter)) + handler.addFilter(hass_filter) if LOGGER_LOGS in config.get(DOMAIN): set_log_levels(config.get(DOMAIN)[LOGGER_LOGS]) diff --git a/homeassistant/components/logi_circle/__init__.py b/homeassistant/components/logi_circle/__init__.py index 0a6d471889f..87ee2e6e384 100644 --- a/homeassistant/components/logi_circle/__init__.py +++ b/homeassistant/components/logi_circle/__init__.py @@ -12,6 +12,8 @@ from homeassistant import config_entries from homeassistant.components.camera import ATTR_FILENAME, CAMERA_SERVICE_SCHEMA from homeassistant.const import ( ATTR_MODE, + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, CONF_MONITORED_CONDITIONS, CONF_SENSORS, EVENT_HOMEASSISTANT_STOP, @@ -22,8 +24,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from . import config_flow from .const import ( CONF_API_KEY, - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, CONF_REDIRECT_URI, DATA_LOGI, DEFAULT_CACHEDB, diff --git a/homeassistant/components/logi_circle/config_flow.py b/homeassistant/components/logi_circle/config_flow.py index 527353eebf6..55b617c0748 100644 --- a/homeassistant/components/logi_circle/config_flow.py +++ b/homeassistant/components/logi_circle/config_flow.py @@ -9,17 +9,15 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.http import HomeAssistantView -from homeassistant.const import CONF_SENSORS, HTTP_BAD_REQUEST -from homeassistant.core import callback - -from .const import ( - CONF_API_KEY, +from homeassistant.const import ( CONF_CLIENT_ID, CONF_CLIENT_SECRET, - CONF_REDIRECT_URI, - DEFAULT_CACHEDB, - DOMAIN, + CONF_SENSORS, + HTTP_BAD_REQUEST, ) +from homeassistant.core import callback + +from .const import CONF_API_KEY, CONF_REDIRECT_URI, DEFAULT_CACHEDB, DOMAIN _TIMEOUT = 15 # seconds diff --git a/homeassistant/components/logi_circle/const.py b/homeassistant/components/logi_circle/const.py index 333a85e9b77..a0905aee63e 100644 --- a/homeassistant/components/logi_circle/const.py +++ b/homeassistant/components/logi_circle/const.py @@ -1,15 +1,14 @@ """Constants in Logi Circle component.""" from homeassistant.const import UNIT_PERCENTAGE -CONF_CLIENT_ID = "client_id" -CONF_CLIENT_SECRET = "client_secret" +DOMAIN = "logi_circle" +DATA_LOGI = DOMAIN + CONF_API_KEY = "api_key" CONF_REDIRECT_URI = "redirect_uri" DEFAULT_CACHEDB = ".logi_cache.pickle" -DOMAIN = "logi_circle" -DATA_LOGI = DOMAIN LED_MODE_KEY = "LED" RECORDING_MODE_KEY = "RECORDING_MODE" diff --git a/homeassistant/components/logi_circle/translations/ca.json b/homeassistant/components/logi_circle/translations/ca.json index 8b81f752058..97fcaf575fc 100644 --- a/homeassistant/components/logi_circle/translations/ca.json +++ b/homeassistant/components/logi_circle/translations/ca.json @@ -16,7 +16,7 @@ }, "step": { "auth": { - "description": "V\u00e9s a l'enlla\u00e7 de sota i Accepta l'acc\u00e9s al teu compte de Logi Circle, despr\u00e9s, torna i prem Envia (tamb\u00e9 a sota).\n\n[Enlla\u00e7]({authorization_url})", + "description": "V\u00e9s a l'enlla\u00e7 de sota i **Accepta** l'acc\u00e9s al teu compte de Logi Circle, despr\u00e9s torna i prem **Envia** (tamb\u00e9 a sota).\n\n[Enlla\u00e7]({authorization_url})", "title": "Autenticaci\u00f3 amb Logi Circle" }, "user": { diff --git a/homeassistant/components/logi_circle/translations/en.json b/homeassistant/components/logi_circle/translations/en.json index 4befbe95f60..48e33359bdc 100644 --- a/homeassistant/components/logi_circle/translations/en.json +++ b/homeassistant/components/logi_circle/translations/en.json @@ -16,7 +16,7 @@ }, "step": { "auth": { - "description": "Please follow the link below and Accept access to your Logi Circle account, then come back and press Submit below.\n\n[Link]({authorization_url})", + "description": "Please follow the link below and **Accept** access to your Logi Circle account, then come back and press **Submit** below.\n\n[Link]({authorization_url})", "title": "Authenticate with Logi Circle" }, "user": { diff --git a/homeassistant/components/logi_circle/translations/fi.json b/homeassistant/components/logi_circle/translations/fi.json index 8b7c30df298..84fe07d445e 100644 --- a/homeassistant/components/logi_circle/translations/fi.json +++ b/homeassistant/components/logi_circle/translations/fi.json @@ -1,10 +1,14 @@ { "config": { + "error": { + "auth_error": "API-todennus ep\u00e4onnistui." + }, "step": { "user": { "data": { "flow_impl": "Tarjoaja" - } + }, + "title": "Todentamisen tarjoaja" } } } diff --git a/homeassistant/components/logi_circle/translations/it.json b/homeassistant/components/logi_circle/translations/it.json index 6ce7948472e..6bb23254cda 100644 --- a/homeassistant/components/logi_circle/translations/it.json +++ b/homeassistant/components/logi_circle/translations/it.json @@ -4,7 +4,7 @@ "already_setup": "\u00c8 possibile configurare solo un singolo account Logi Circle.", "external_error": "Si \u00e8 verificata un'eccezione da un altro flusso.", "external_setup": "Logi Circle configurato con successo da un altro flusso.", - "no_flows": "Devi configurare Logi Circle prima di poter eseguire l'autenticazione. [Si prega di leggere le istruzioni] (https://www.home-assistant.io/components/logi_circle/)." + "no_flows": "Devi configurare Logi Circle prima di poter eseguire l'autenticazione. [Si prega di leggere le istruzioni](https://www.home-assistant.io/components/logi_circle/)." }, "create_entry": { "default": "Autenticato con successo con Logi Circle." @@ -16,7 +16,7 @@ }, "step": { "auth": { - "description": "Segui il link qui sotto e Accetta l'accesso al tuo account Logi Circle, quindi torna indietro e premi Invia qui sotto. \n\n [Link]({authorization_url})", + "description": "Segui il link qui sotto e **Accetta** l'accesso al tuo account Logi Circle, quindi torna indietro e premi **Invia** qui sotto. \n\n [Link]({authorization_url})", "title": "Autenticarsi con Logi Circle" }, "user": { diff --git a/homeassistant/components/logi_circle/translations/ko.json b/homeassistant/components/logi_circle/translations/ko.json index cb3e2aab323..f3fe51e2d25 100644 --- a/homeassistant/components/logi_circle/translations/ko.json +++ b/homeassistant/components/logi_circle/translations/ko.json @@ -12,11 +12,11 @@ "error": { "auth_error": "API \uc2b9\uc778\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4.", "auth_timeout": "\uc5d1\uc138\uc2a4 \ud1a0\ud070 \uc694\uccad\uc911 \uc2b9\uc778 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "follow_link": "Submit \ubc84\ud2bc\uc744 \ub204\ub974\uae30 \uc804\uc5d0 \ub9c1\ud06c\ub97c \ub530\ub77c \uc778\uc99d\uc744 \ubc1b\uc544\uc8fc\uc138\uc694" + "follow_link": "\ud655\uc778\uc744 \ud074\ub9ad\ud558\uae30 \uc804\uc5d0 \ub9c1\ud06c\ub97c \ub530\ub77c \uc778\uc99d\uc744 \ubc1b\uc544\uc8fc\uc138\uc694" }, "step": { "auth": { - "description": "\uc544\ub798 \ub9c1\ud06c\ub97c \ud074\ub9ad\ud558\uc5ec Logi Circle \uacc4\uc815\uc5d0 \ub300\ud574 \ub3d9\uc758 \ud55c \ub2e4\uc74c, \ub2e4\uc2dc \ub3cc\uc544\uc640\uc11c \ud558\ub2e8\uc758 Submit \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694.\n\n[\ub9c1\ud06c]({authorization_url})", + "description": "\uc544\ub798 \ub9c1\ud06c\ub97c \ud074\ub9ad\ud558\uc5ec Logi Circle \uacc4\uc815\uc5d0 \ub300\ud574 **\ub3d9\uc758**\ud55c \ub2e4\uc74c, \ub2e4\uc2dc \ub3cc\uc544\uc640\uc11c \ud558\ub2e8\uc758 **\ud655\uc778**\uc744 \ud074\ub9ad\ud574\uc8fc\uc138\uc694.\n\n[\ub9c1\ud06c]({authorization_url})", "title": "Logi Circle \uc778\uc99d\ud558\uae30" }, "user": { diff --git a/homeassistant/components/logi_circle/translations/no.json b/homeassistant/components/logi_circle/translations/no.json index 7c0cae590cd..82fa73d492d 100644 --- a/homeassistant/components/logi_circle/translations/no.json +++ b/homeassistant/components/logi_circle/translations/no.json @@ -16,7 +16,7 @@ }, "step": { "auth": { - "description": "Vennligst f\u00f8lg lenken nedenfor og Godta tilgang til Logi Circle kontoen din, kom deretter tilbake og trykk Send nedenfor. \n\n [Link]({authorization_url})", + "description": "Vennligst f\u00f8lg lenken nedenfor og **Godta** tilgang til Logi Circle kontoen din, kom deretter tilbake og trykk **Send** nedenfor. \n\n [Link]({authorization_url})", "title": "Godkjenn med Logi Circle" }, "user": { diff --git a/homeassistant/components/logi_circle/translations/ru.json b/homeassistant/components/logi_circle/translations/ru.json index 906b647bbef..60db9528cef 100644 --- a/homeassistant/components/logi_circle/translations/ru.json +++ b/homeassistant/components/logi_circle/translations/ru.json @@ -16,7 +16,7 @@ }, "step": { "auth": { - "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e [\u0441\u0441\u044b\u043b\u043a\u0435]({authorization_url}) \u0438 \u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0435 \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Logi Circle, \u0437\u0430\u0442\u0435\u043c \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c.", + "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e [\u0441\u0441\u044b\u043b\u043a\u0435]({authorization_url}) \u0438 **\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0435** \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Logi Circle, \u0437\u0430\u0442\u0435\u043c \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **\u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c**.", "title": "Logi Circle" }, "user": { diff --git a/homeassistant/components/logi_circle/translations/zh-Hant.json b/homeassistant/components/logi_circle/translations/zh-Hant.json index a94269808d3..6602491c8a2 100644 --- a/homeassistant/components/logi_circle/translations/zh-Hant.json +++ b/homeassistant/components/logi_circle/translations/zh-Hant.json @@ -16,7 +16,7 @@ }, "step": { "auth": { - "description": "\u8acb\u4f7f\u7528\u4e0b\u65b9\u9023\u7d50\u4e26\u9ede\u9078\u63a5\u53d7\u4ee5\u5b58\u53d6 Logi Circle \u5e33\u865f\uff0c\u7136\u5f8c\u8fd4\u56de\u6b64\u9801\u9762\u4e26\u9ede\u9078\u4e0b\u65b9\u7684\u50b3\u9001\u3002\n\n[Link]({authorization_url})", + "description": "\u8acb\u4f7f\u7528\u4e0b\u65b9\u9023\u7d50\u4e26\u9ede\u9078 **\u63a5\u53d7** \u4ee5\u5b58\u53d6 Logi Circle \u5e33\u865f\uff0c\u7136\u5f8c\u8fd4\u56de\u6b64\u9801\u9762\u4e26\u9ede\u9078\u4e0b\u65b9\u7684 **\u50b3\u9001**\u3002\n\n[\u9023\u7d50]({authorization_url})", "title": "\u4ee5 Logi Circle \u8a8d\u8b49" }, "user": { diff --git a/homeassistant/components/lutron_caseta/translations/ca.json b/homeassistant/components/lutron_caseta/translations/ca.json index 40ad8a42f82..4b6be6e3a6e 100644 --- a/homeassistant/components/lutron_caseta/translations/ca.json +++ b/homeassistant/components/lutron_caseta/translations/ca.json @@ -13,6 +13,5 @@ "title": "No s'ha pogut importar la configuraci\u00f3 de l'enlla\u00e7 de Cas\u00e9ta." } } - }, - "title": "Lutron Cas\u00e9ta" + } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/en.json b/homeassistant/components/lutron_caseta/translations/en.json index 5397985b936..469bcae37c7 100644 --- a/homeassistant/components/lutron_caseta/translations/en.json +++ b/homeassistant/components/lutron_caseta/translations/en.json @@ -13,6 +13,5 @@ "title": "Failed to import Cas\u00e9ta bridge configuration." } } - }, - "title": "Lutron Cas\u00e9ta" + } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/es.json b/homeassistant/components/lutron_caseta/translations/es.json index db3a46f3221..3440342d40f 100644 --- a/homeassistant/components/lutron_caseta/translations/es.json +++ b/homeassistant/components/lutron_caseta/translations/es.json @@ -13,6 +13,5 @@ "title": "Error al importar la configuraci\u00f3n del bridge Cas\u00e9ta." } } - }, - "title": "Lutron Cas\u00e9ta" + } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/it.json b/homeassistant/components/lutron_caseta/translations/it.json index a73e72fa1ae..9e4cb9ec02c 100644 --- a/homeassistant/components/lutron_caseta/translations/it.json +++ b/homeassistant/components/lutron_caseta/translations/it.json @@ -13,6 +13,5 @@ "title": "Impossibile importare la configurazione del bridge Cas\u00e9ta." } } - }, - "title": "Lutron Cas\u00e9ta" + } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/ko.json b/homeassistant/components/lutron_caseta/translations/ko.json index c1578edeed4..8c5caec998e 100644 --- a/homeassistant/components/lutron_caseta/translations/ko.json +++ b/homeassistant/components/lutron_caseta/translations/ko.json @@ -13,6 +13,5 @@ "title": "Cas\u00e9ta \ube0c\ub9ac\uc9c0 \uad6c\uc131\uc744 \uac00\uc838\uc624\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4." } } - }, - "title": "Lutron Cas\u00e9ta" + } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/lb.json b/homeassistant/components/lutron_caseta/translations/lb.json index 90549b6f464..8da7ed29d7f 100644 --- a/homeassistant/components/lutron_caseta/translations/lb.json +++ b/homeassistant/components/lutron_caseta/translations/lb.json @@ -13,6 +13,5 @@ "title": "Feeler beim import vun der Cas\u00e9ta Bridge Konfiguratioun" } } - }, - "title": "Lutron Cas\u00e9ta" + } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/no.json b/homeassistant/components/lutron_caseta/translations/no.json index 5d09a7bf9a1..b63e8fb4111 100644 --- a/homeassistant/components/lutron_caseta/translations/no.json +++ b/homeassistant/components/lutron_caseta/translations/no.json @@ -13,6 +13,5 @@ "title": "Kan ikke importere Cas\u00e9ta bridge-konfigurasjon." } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/pl.json b/homeassistant/components/lutron_caseta/translations/pl.json index 970d722fe4c..9559e7f0e83 100644 --- a/homeassistant/components/lutron_caseta/translations/pl.json +++ b/homeassistant/components/lutron_caseta/translations/pl.json @@ -1,3 +1,7 @@ { - "title": "Lutron Cas\u00e9ta" + "config": { + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia mostkiem Cas\u00e9ta, sprawd\u017a konfiguracj\u0119 hosta i certyfikatu." + } + } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/pt-BR.json b/homeassistant/components/lutron_caseta/translations/pt-BR.json new file mode 100644 index 00000000000..091f7990989 --- /dev/null +++ b/homeassistant/components/lutron_caseta/translations/pt-BR.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Ponte Cas\u00e9ta j\u00e1 configurada.", + "cannot_connect": "Instala\u00e7\u00e3o cancelada da ponte Cas\u00e9ta devido \u00e0 falha na conex\u00e3o." + }, + "error": { + "cannot_connect": "Falha ao conectar \u00e0 ponte Cas\u00e9ta; verifique sua configura\u00e7\u00e3o de endere\u00e7o e certificado." + }, + "step": { + "import_failed": { + "description": "N\u00e3o foi poss\u00edvel configurar a ponte (host: {host}) importada do configuration.yaml.", + "title": "Falha ao importar a configura\u00e7\u00e3o da ponte Cas\u00e9ta." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/ru.json b/homeassistant/components/lutron_caseta/translations/ru.json index 9b2b56a70e7..f7f566b1f54 100644 --- a/homeassistant/components/lutron_caseta/translations/ru.json +++ b/homeassistant/components/lutron_caseta/translations/ru.json @@ -13,6 +13,5 @@ "title": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0448\u043b\u044e\u0437\u0430." } } - }, - "title": "Lutron Cas\u00e9ta" + } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/zh-Hant.json b/homeassistant/components/lutron_caseta/translations/zh-Hant.json index ae46bc41258..f975949e6e7 100644 --- a/homeassistant/components/lutron_caseta/translations/zh-Hant.json +++ b/homeassistant/components/lutron_caseta/translations/zh-Hant.json @@ -13,6 +13,5 @@ "title": "\u532f\u5165 Cas\u00e9ta bridge \u8a2d\u5b9a\u5931\u6557\u3002" } } - }, - "title": "Lutron Cas\u00e9ta" + } } \ No newline at end of file diff --git a/homeassistant/components/lyft/sensor.py b/homeassistant/components/lyft/sensor.py index 5e8555f857d..98084b28f0c 100644 --- a/homeassistant/components/lyft/sensor.py +++ b/homeassistant/components/lyft/sensor.py @@ -8,15 +8,13 @@ from lyft_rides.errors import APIError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import TIME_MINUTES +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, TIME_MINUTES import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -CONF_CLIENT_ID = "client_id" -CONF_CLIENT_SECRET = "client_secret" CONF_END_LATITUDE = "end_latitude" CONF_END_LONGITUDE = "end_longitude" CONF_PRODUCT_IDS = "product_ids" diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py index c3a24fe9b02..e5a0f16863d 100644 --- a/homeassistant/components/mailbox/__init__.py +++ b/homeassistant/components/mailbox/__init__.py @@ -200,7 +200,7 @@ class MailboxPlatformsView(MailboxView): url = "/api/mailbox/platforms" name = "api:mailbox:platforms" - async def get(self, request): + async def get(self, request: web.Request) -> web.Response: """Retrieve list of platforms.""" platforms = [] for mailbox in self.mailboxes: diff --git a/homeassistant/components/mailgun/translations/pl.json b/homeassistant/components/mailgun/translations/pl.json index e24f2d7ec8a..5a6b92505b7 100644 --- a/homeassistant/components/mailgun/translations/pl.json +++ b/homeassistant/components/mailgun/translations/pl.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Wymagana jest tylko jedna instancja." }, "create_entry": { - "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant'a, musisz skonfigurowa\u0107 [Mailgun Webhook]({mailgun_url}). \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n - Typ zawarto\u015bci: application/json\n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}) na temat konfiguracji automatyzacji, by obs\u0142u\u017cy\u0107 przychodz\u0105ce dane." + "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistanta, musisz skonfigurowa\u0107 [Mailgun Webhook]({mailgun_url}). \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n - Typ zawarto\u015bci: application/json\n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}) na temat konfiguracji automatyzacji, by obs\u0142u\u017cy\u0107 przychodz\u0105ce dane." }, "step": { "user": { diff --git a/homeassistant/components/mastodon/notify.py b/homeassistant/components/mastodon/notify.py index 88716de5773..058137393f5 100644 --- a/homeassistant/components/mastodon/notify.py +++ b/homeassistant/components/mastodon/notify.py @@ -6,14 +6,12 @@ from mastodon.Mastodon import MastodonAPIError, MastodonUnauthorizedError import voluptuous as vol from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService -from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_CLIENT_SECRET import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) CONF_BASE_URL = "base_url" -CONF_CLIENT_ID = "client_id" -CONF_CLIENT_SECRET = "client_secret" DEFAULT_URL = "https://mastodon.social" diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 95cb1db65a8..b70729a7435 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.05.08"], + "requirements": ["youtube_dl==2020.05.29"], "dependencies": ["media_player"], "codeowners": [], "quality_scale": "internal" diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 0d73c93ec71..9ba37e6c18a 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -12,6 +12,7 @@ from urllib.parse import urlparse from aiohttp import web from aiohttp.hdrs import CACHE_CONTROL, CONTENT_TYPE +from aiohttp.typedefs import LooseHeaders import async_timeout import voluptuous as vol @@ -863,7 +864,7 @@ class MediaPlayerImageView(HomeAssistantView): """Initialize a media player view.""" self.component = component - async def get(self, request, entity_id): + async def get(self, request: web.Request, entity_id: str) -> web.Response: """Start a get request.""" player = self.component.get_entity(entity_id) if player is None: @@ -883,7 +884,7 @@ class MediaPlayerImageView(HomeAssistantView): if data is None: return web.Response(status=HTTP_INTERNAL_SERVER_ERROR) - headers = {CACHE_CONTROL: "max-age=3600"} + headers: LooseHeaders = {CACHE_CONTROL: "max-age=3600"} return web.Response(body=data, content_type=content_type, headers=headers) diff --git a/homeassistant/components/media_player/translations/ca.json b/homeassistant/components/media_player/translations/ca.json index d4aec480562..1a1c161e5ec 100644 --- a/homeassistant/components/media_player/translations/ca.json +++ b/homeassistant/components/media_player/translations/ca.json @@ -11,9 +11,9 @@ "state": { "_": { "idle": "Inactiu", - "off": "Apagat", - "on": "Enc\u00e8s", - "paused": "Pausat", + "off": "OFF", + "on": "ON", + "paused": "Pausat/da", "playing": "Reproduint", "standby": "En espera" } diff --git a/homeassistant/components/mediaroom/media_player.py b/homeassistant/components/mediaroom/media_player.py index abbfa21761f..9c0812af6e5 100644 --- a/homeassistant/components/mediaroom/media_player.py +++ b/homeassistant/components/mediaroom/media_player.py @@ -1,7 +1,13 @@ """Support for the Mediaroom Set-up-box.""" import logging -from pymediaroom import PyMediaroomError, Remote, State, install_mediaroom_protocol +from pymediaroom import ( + COMMANDS, + PyMediaroomError, + Remote, + State, + install_mediaroom_protocol, +) import voluptuous as vol from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity @@ -41,6 +47,8 @@ DEFAULT_NAME = "Mediaroom STB" DEFAULT_TIMEOUT = 9 DISCOVERY_MEDIAROOM = "mediaroom_discovery_installed" +MEDIA_TYPE_MEDIAROOM = "mediaroom" + SIGNAL_STB_NOTIFY = "mediaroom_stb_discovered" SUPPORT_MEDIAROOM = ( SUPPORT_PAUSE @@ -190,15 +198,21 @@ class MediaroomDevice(MediaPlayerEntity): _LOGGER.debug( "STB(%s) Play media: %s (%s)", self.stb.stb_ip, media_id, media_type ) - if media_type != MEDIA_TYPE_CHANNEL: - _LOGGER.error("invalid media type") - return - if not media_id.isdigit(): - _LOGGER.error("media_id must be a channel number") + if media_type == MEDIA_TYPE_CHANNEL: + if not media_id.isdigit(): + _LOGGER.error("Invalid media_id %s: Must be a channel number", media_id) + return + media_id = int(media_id) + elif media_type == MEDIA_TYPE_MEDIAROOM: + if media_id not in COMMANDS: + _LOGGER.error("Invalid media_id %s: Must be a command", media_id) + return + else: + _LOGGER.error("Invalid media type %s", media_type) return try: - await self.stb.send_cmd(int(media_id)) + await self.stb.send_cmd(media_id) if self._optimistic: self._state = STATE_PLAYING self._available = True diff --git a/homeassistant/components/melcloud/translations/ca.json b/homeassistant/components/melcloud/translations/ca.json index 92472d020c4..2da4f4b57f3 100644 --- a/homeassistant/components/melcloud/translations/ca.json +++ b/homeassistant/components/melcloud/translations/ca.json @@ -11,8 +11,8 @@ "step": { "user": { "data": { - "password": "Contrasenya de MELCloud.", - "username": "Correu electr\u00f2nic d'inici de sessi\u00f3 a MELCloud." + "password": "Contrasenya", + "username": "Correu electr\u00f2nic" }, "description": "Connecta't amb el teu compte de MELCloud.", "title": "Connexi\u00f3 amb MELCloud" diff --git a/homeassistant/components/melcloud/translations/hu.json b/homeassistant/components/melcloud/translations/hu.json new file mode 100644 index 00000000000..62699ecb468 --- /dev/null +++ b/homeassistant/components/melcloud/translations/hu.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "E-mail" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/translations/it.json b/homeassistant/components/melcloud/translations/it.json index c027473ad14..2a312561932 100644 --- a/homeassistant/components/melcloud/translations/it.json +++ b/homeassistant/components/melcloud/translations/it.json @@ -11,8 +11,8 @@ "step": { "user": { "data": { - "password": "Password MELCloud.", - "username": "Email utilizzata per accedere a MELCloud." + "password": "Password", + "username": "E-mail" }, "description": "Connettiti utilizzando il tuo account MELCloud.", "title": "Connettersi a MELCloud" diff --git a/homeassistant/components/melcloud/translations/pl.json b/homeassistant/components/melcloud/translations/pl.json index 4b6c4d70b80..44467601826 100644 --- a/homeassistant/components/melcloud/translations/pl.json +++ b/homeassistant/components/melcloud/translations/pl.json @@ -5,14 +5,14 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "[%key_id:common::config_flow::error::invalid_auth%]", - "unknown": "[%key_id:common::config_flow::error::unknown%]" + "invalid_auth": "Niepoprawne uwierzytelnienie.", + "unknown": "Nieoczekiwany b\u0142\u0105d." }, "step": { "user": { "data": { - "password": "[%key_id:common::config_flow::data::password%] MELCloud", - "username": "[%key_id:common::config_flow::data::email%]" + "password": "Has\u0142o", + "username": "Adres e-mail" }, "description": "Po\u0142\u0105cz u\u017cywaj\u0105c swojego konta MELCloud.", "title": "Po\u0142\u0105czenie z MELCloud" diff --git a/homeassistant/components/meteo_france/translations/pl.json b/homeassistant/components/meteo_france/translations/pl.json index 12e74728fc8..46f3c1fdc27 100644 --- a/homeassistant/components/meteo_france/translations/pl.json +++ b/homeassistant/components/meteo_france/translations/pl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Miasto jest ju\u017c skonfigurowane.", - "unknown": "Nieznany b\u0142\u0105d: spr\u00f3buj ponownie p\u00f3\u017aniej" + "unknown": "Nieznany b\u0142\u0105d: spr\u00f3buj ponownie p\u00f3\u017aniej." }, "step": { "user": { diff --git a/homeassistant/components/meteo_france/translations/pt-BR.json b/homeassistant/components/meteo_france/translations/pt-BR.json new file mode 100644 index 00000000000..f23bdb1379d --- /dev/null +++ b/homeassistant/components/meteo_france/translations/pt-BR.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Cidade j\u00e1 configurada", + "unknown": "Erro desconhecido: tente novamente mais tarde" + }, + "step": { + "user": { + "data": { + "city": "Cidade" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/translations/hu.json b/homeassistant/components/mikrotik/translations/hu.json index fffa8b293da..e68e0ceb414 100644 --- a/homeassistant/components/mikrotik/translations/hu.json +++ b/homeassistant/components/mikrotik/translations/hu.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "Kiszolg\u00e1l\u00f3", + "host": "Hoszt", "name": "N\u00e9v", "password": "Jelsz\u00f3", "port": "Port", diff --git a/homeassistant/components/mikrotik/translations/it.json b/homeassistant/components/mikrotik/translations/it.json index 69cdadb4dfc..104392236b2 100644 --- a/homeassistant/components/mikrotik/translations/it.json +++ b/homeassistant/components/mikrotik/translations/it.json @@ -27,7 +27,7 @@ "device_tracker": { "data": { "arp_ping": "Attivare il ping ARP", - "detection_time": "Considerare l'intervallo di casa", + "detection_time": "Considerare in casa nell'intervallo di", "force_dhcp": "Scansione forzata con DHCP" } } diff --git a/homeassistant/components/mikrotik/translations/pl.json b/homeassistant/components/mikrotik/translations/pl.json index 4dae38268d4..20b0348570c 100644 --- a/homeassistant/components/mikrotik/translations/pl.json +++ b/homeassistant/components/mikrotik/translations/pl.json @@ -11,11 +11,11 @@ "step": { "user": { "data": { - "host": "[%key_id:common::config_flow::data::host%]", + "host": "Nazwa hosta lub adres IP", "name": "Nazwa", - "password": "[%key_id:common::config_flow::data::password%]", - "port": "[%key_id:common::config_flow::data::port%]", - "username": "[%key_id:common::config_flow::data::username%]", + "password": "Has\u0142o", + "port": "Port", + "username": "Nazwa u\u017cytkownika", "verify_ssl": "U\u017cyj SSL" }, "title": "Konfiguracja routera Mikrotik" diff --git a/homeassistant/components/mikrotik/translations/pt-BR.json b/homeassistant/components/mikrotik/translations/pt-BR.json new file mode 100644 index 00000000000..06ad1cba6d0 --- /dev/null +++ b/homeassistant/components/mikrotik/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Mikrotik j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Conex\u00e3o malsucedida", + "name_exists": "O nome j\u00e1 existe", + "wrong_credentials": "Credenciais erradas" + }, + "step": { + "user": { + "data": { + "name": "Nome", + "verify_ssl": "Usar SSL" + }, + "title": "Configurar roteador Mikrotik" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mill/translations/es.json b/homeassistant/components/mill/translations/es.json index fb0c69dfd07..e6b30851b80 100644 --- a/homeassistant/components/mill/translations/es.json +++ b/homeassistant/components/mill/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "La cuenta ya ha sido configurada" }, "error": { - "connection_error": "Fallo al conectarse" + "connection_error": "No se pudo conectar" }, "step": { "user": { diff --git a/homeassistant/components/mill/translations/hu.json b/homeassistant/components/mill/translations/hu.json new file mode 100644 index 00000000000..c8dec2dcb2d --- /dev/null +++ b/homeassistant/components/mill/translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "connection_error": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mill/translations/pl.json b/homeassistant/components/mill/translations/pl.json index 3f41ac78c7d..c9bef09227c 100644 --- a/homeassistant/components/mill/translations/pl.json +++ b/homeassistant/components/mill/translations/pl.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "already_configured": "[%key_id:common::config_flow::abort::already_configured_account%]" + "already_configured": "Konto jest ju\u017c skonfigurowane." }, "error": { - "connection_error": "[%key_id:common::config_flow::error::cannot_connect%]" + "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." }, "step": { "user": { "data": { - "password": "[%key_id:common::config_flow::data::password%]", - "username": "[%key_id:common::config_flow::data::username%]" + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" } } } diff --git a/homeassistant/components/min_max/sensor.py b/homeassistant/components/min_max/sensor.py index aa58cc0be21..a9032787420 100644 --- a/homeassistant/components/min_max/sensor.py +++ b/homeassistant/components/min_max/sensor.py @@ -19,17 +19,23 @@ from homeassistant.helpers.event import async_track_state_change _LOGGER = logging.getLogger(__name__) ATTR_MIN_VALUE = "min_value" +ATTR_MIN_ENTITY_ID = "min_entity_id" ATTR_MAX_VALUE = "max_value" +ATTR_MAX_ENTITY_ID = "max_entity_id" ATTR_COUNT_SENSORS = "count_sensors" ATTR_MEAN = "mean" ATTR_LAST = "last" +ATTR_LAST_ENTITY_ID = "last_entity_id" ATTR_TO_PROPERTY = [ ATTR_COUNT_SENSORS, ATTR_MAX_VALUE, + ATTR_MAX_ENTITY_ID, ATTR_MEAN, ATTR_MIN_VALUE, + ATTR_MIN_ENTITY_ID, ATTR_LAST, + ATTR_LAST_ENTITY_ID, ] CONF_ENTITY_IDS = "entity_ids" @@ -72,34 +78,36 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= def calc_min(sensor_values): """Calculate min value, honoring unknown states.""" val = None - for sval in sensor_values: - if sval != STATE_UNKNOWN: - if val is None or val > sval: - val = sval - return val + entity_id = None + for sensor_id, sensor_value in sensor_values: + if sensor_value != STATE_UNKNOWN: + if val is None or val > sensor_value: + entity_id, val = sensor_id, sensor_value + return entity_id, val def calc_max(sensor_values): """Calculate max value, honoring unknown states.""" val = None - for sval in sensor_values: - if sval != STATE_UNKNOWN: - if val is None or val < sval: - val = sval - return val + entity_id = None + for sensor_id, sensor_value in sensor_values: + if sensor_value != STATE_UNKNOWN: + if val is None or val < sensor_value: + entity_id, val = sensor_id, sensor_value + return entity_id, val def calc_mean(sensor_values, round_digits): """Calculate mean value, honoring unknown states.""" - val = 0 + sensor_value_sum = 0 count = 0 - for sval in sensor_values: - if sval != STATE_UNKNOWN: - val += sval + for _, sensor_value in sensor_values: + if sensor_value != STATE_UNKNOWN: + sensor_value_sum += sensor_value count += 1 if count == 0: return None - return round(val / count, round_digits) + return round(sensor_value_sum / count, round_digits) class MinMaxSensor(Entity): @@ -119,6 +127,7 @@ class MinMaxSensor(Entity): self._unit_of_measurement = None self._unit_of_measurement_mismatch = False self.min_value = self.max_value = self.mean = self.last = None + self.min_entity_id = self.max_entity_id = self.last_entity_id = None self.count_sensors = len(self._entity_ids) self.states = {} @@ -149,6 +158,7 @@ class MinMaxSensor(Entity): try: self.states[entity] = float(new_state.state) self.last = float(new_state.state) + self.last_entity_id = entity except ValueError: _LOGGER.warning( "Unable to store state. Only numerical states are supported" @@ -201,7 +211,11 @@ class MinMaxSensor(Entity): async def async_update(self): """Get the latest data and updates the states.""" - sensor_values = [self.states[k] for k in self._entity_ids if k in self.states] - self.min_value = calc_min(sensor_values) - self.max_value = calc_max(sensor_values) + sensor_values = [ + (entity_id, self.states[entity_id]) + for entity_id in self._entity_ids + if entity_id in self.states + ] + self.min_entity_id, self.min_value = calc_min(sensor_values) + self.max_entity_id, self.max_value = calc_max(sensor_values) self.mean = calc_mean(sensor_values, self._round_digits) diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py index 3a8598d3fac..164bb264f90 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -197,6 +197,7 @@ class MinecraftServer: if status_response.players.sample is not None: for player in status_response.players.sample: self.players_list.append(player.name) + self.players_list.sort() # Inform user once about successful update if necessary. if self._last_status_request_failed: diff --git a/homeassistant/components/minecraft_server/translations/hu.json b/homeassistant/components/minecraft_server/translations/hu.json index 3a2cb80fc9f..78778568e51 100644 --- a/homeassistant/components/minecraft_server/translations/hu.json +++ b/homeassistant/components/minecraft_server/translations/hu.json @@ -6,7 +6,7 @@ "step": { "user": { "data": { - "host": "Kiszolg\u00e1l\u00f3", + "host": "Hoszt", "name": "N\u00e9v" }, "title": "Kapcsold \u00f6ssze a Minecraft szervered" diff --git a/homeassistant/components/minecraft_server/translations/pl.json b/homeassistant/components/minecraft_server/translations/pl.json index 24749767b23..77d83f37174 100644 --- a/homeassistant/components/minecraft_server/translations/pl.json +++ b/homeassistant/components/minecraft_server/translations/pl.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "[%key_id:common::config_flow::data::host%]", + "host": "Nazwa hosta lub adres IP", "name": "Nazwa" }, "description": "Skonfiguruj instancj\u0119 serwera Minecraft, aby umo\u017cliwi\u0107 monitorowanie.", diff --git a/homeassistant/components/minecraft_server/translations/pt-BR.json b/homeassistant/components/minecraft_server/translations/pt-BR.json new file mode 100644 index 00000000000..5aa2fc3609a --- /dev/null +++ b/homeassistant/components/minecraft_server/translations/pt-BR.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O host j\u00e1 est\u00e1 configurado." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index 0fc4a5ee407..6e83a08c508 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -56,7 +56,6 @@ 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" ERR_INVALID_FORMAT = "invalid_format" diff --git a/homeassistant/components/mobile_app/translations/bg.json b/homeassistant/components/mobile_app/translations/bg.json index 487a036e44b..6cbe826000c 100644 --- a/homeassistant/components/mobile_app/translations/bg.json +++ b/homeassistant/components/mobile_app/translations/bg.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \"\u041c\u043e\u0431\u0438\u043b\u043d\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435\"?", - "title": "\u041c\u043e\u0431\u0438\u043b\u043d\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435" + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \"\u041c\u043e\u0431\u0438\u043b\u043d\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435\"?" } } } diff --git a/homeassistant/components/mobile_app/translations/ca.json b/homeassistant/components/mobile_app/translations/ca.json index 84e613ca978..bb070f391a7 100644 --- a/homeassistant/components/mobile_app/translations/ca.json +++ b/homeassistant/components/mobile_app/translations/ca.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "Vols configurar el component d'aplicaci\u00f3 m\u00f2bil?", - "title": "Aplicaci\u00f3 m\u00f2bil" + "description": "Vols configurar el component d'aplicaci\u00f3 m\u00f2bil?" } } } diff --git a/homeassistant/components/mobile_app/translations/cs.json b/homeassistant/components/mobile_app/translations/cs.json index b1d244cc4d6..5b5a68db066 100644 --- a/homeassistant/components/mobile_app/translations/cs.json +++ b/homeassistant/components/mobile_app/translations/cs.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "Chcete nastavit komponentu Mobiln\u00ed aplikace?", - "title": "Mobiln\u00ed aplikace" + "description": "Chcete nastavit komponentu Mobiln\u00ed aplikace?" } } } diff --git a/homeassistant/components/mobile_app/translations/da.json b/homeassistant/components/mobile_app/translations/da.json index e497ef546e3..19061af3686 100644 --- a/homeassistant/components/mobile_app/translations/da.json +++ b/homeassistant/components/mobile_app/translations/da.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "Vil du konfigurere mobilapp-komponenten?", - "title": "Mobilapp" + "description": "Vil du konfigurere mobilapp-komponenten?" } } } diff --git a/homeassistant/components/mobile_app/translations/de.json b/homeassistant/components/mobile_app/translations/de.json index f7181424964..0a2f8461bed 100644 --- a/homeassistant/components/mobile_app/translations/de.json +++ b/homeassistant/components/mobile_app/translations/de.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "M\u00f6chtest du die Mobile App-Komponente einrichten?", - "title": "Mobile App" + "description": "M\u00f6chtest du die Mobile App-Komponente einrichten?" } } } diff --git a/homeassistant/components/mobile_app/translations/en.json b/homeassistant/components/mobile_app/translations/en.json index 9ddf3c95586..6def5e98582 100644 --- a/homeassistant/components/mobile_app/translations/en.json +++ b/homeassistant/components/mobile_app/translations/en.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "Do you want to set up the Mobile App component?", - "title": "Mobile App" + "description": "Do you want to set up the Mobile App component?" } } } diff --git a/homeassistant/components/mobile_app/translations/es-419.json b/homeassistant/components/mobile_app/translations/es-419.json index 0ad6fb613f9..cf5d9c4e872 100644 --- a/homeassistant/components/mobile_app/translations/es-419.json +++ b/homeassistant/components/mobile_app/translations/es-419.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "\u00bfDesea configurar el componente de la aplicaci\u00f3n m\u00f3vil?", - "title": "Aplicaci\u00f3n movil" + "description": "\u00bfDesea configurar el componente de la aplicaci\u00f3n m\u00f3vil?" } } } diff --git a/homeassistant/components/mobile_app/translations/es.json b/homeassistant/components/mobile_app/translations/es.json index ca145bd68c2..c442cc88a26 100644 --- a/homeassistant/components/mobile_app/translations/es.json +++ b/homeassistant/components/mobile_app/translations/es.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "\u00bfQuieres configurar el componente de la aplicaci\u00f3n para el m\u00f3vil?", - "title": "Aplicaci\u00f3n para el m\u00f3vil" + "description": "\u00bfQuieres configurar el componente de la aplicaci\u00f3n para el m\u00f3vil?" } } } diff --git a/homeassistant/components/mobile_app/translations/fr.json b/homeassistant/components/mobile_app/translations/fr.json index 55d1d4e0d9e..09317e4a00d 100644 --- a/homeassistant/components/mobile_app/translations/fr.json +++ b/homeassistant/components/mobile_app/translations/fr.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "Voulez-vous configurer le composant Application mobile?", - "title": "Application mobile" + "description": "Voulez-vous configurer le composant Application mobile?" } } } diff --git a/homeassistant/components/mobile_app/translations/hu.json b/homeassistant/components/mobile_app/translations/hu.json index 608a4a7879b..c44f51b02e1 100644 --- a/homeassistant/components/mobile_app/translations/hu.json +++ b/homeassistant/components/mobile_app/translations/hu.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a mobil alkalmaz\u00e1s komponenst?", - "title": "Mobil alkalmaz\u00e1s" + "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a mobil alkalmaz\u00e1s komponenst?" } } } diff --git a/homeassistant/components/mobile_app/translations/it.json b/homeassistant/components/mobile_app/translations/it.json index 1b91c566a53..8ff6dfb982e 100644 --- a/homeassistant/components/mobile_app/translations/it.json +++ b/homeassistant/components/mobile_app/translations/it.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "Si desidera configurare il componente App per dispositivi mobili?", - "title": "App per dispositivi mobili" + "description": "Si desidera configurare il componente App per dispositivi mobili?" } } } diff --git a/homeassistant/components/mobile_app/translations/ko.json b/homeassistant/components/mobile_app/translations/ko.json index f427f3e3a12..03478e1cf2a 100644 --- a/homeassistant/components/mobile_app/translations/ko.json +++ b/homeassistant/components/mobile_app/translations/ko.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "\ubaa8\ubc14\uc77c \uc571 \ucef4\ud3ec\ub10c\ud2b8\uc758 \uc124\uc815\uc744 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "\ubaa8\ubc14\uc77c \uc571" + "description": "\ubaa8\ubc14\uc77c \uc571 \ucef4\ud3ec\ub10c\ud2b8\uc758 \uc124\uc815\uc744 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" } } } diff --git a/homeassistant/components/mobile_app/translations/lb.json b/homeassistant/components/mobile_app/translations/lb.json index 77fd6517410..6e15dd34634 100644 --- a/homeassistant/components/mobile_app/translations/lb.json +++ b/homeassistant/components/mobile_app/translations/lb.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "Soll d'Mobil App konfigur\u00e9iert ginn?", - "title": "Mobil App" + "description": "Soll d'Mobil App konfigur\u00e9iert ginn?" } } } diff --git a/homeassistant/components/mobile_app/translations/nl.json b/homeassistant/components/mobile_app/translations/nl.json index 06375b4a0b6..9d5bdcfa20a 100644 --- a/homeassistant/components/mobile_app/translations/nl.json +++ b/homeassistant/components/mobile_app/translations/nl.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "Wilt u de Mobile App component instellen?", - "title": "Mobiele app" + "description": "Wilt u de Mobile App component instellen?" } } } diff --git a/homeassistant/components/mobile_app/translations/no.json b/homeassistant/components/mobile_app/translations/no.json index 131e007751c..346a5eb6b6d 100644 --- a/homeassistant/components/mobile_app/translations/no.json +++ b/homeassistant/components/mobile_app/translations/no.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "Vil du sette opp mobilapp-komponenten?", - "title": "Mobilapp" + "description": "Vil du sette opp mobilapp-komponenten?" } } } diff --git a/homeassistant/components/mobile_app/translations/pl.json b/homeassistant/components/mobile_app/translations/pl.json index a6e7e738903..e5440984b37 100644 --- a/homeassistant/components/mobile_app/translations/pl.json +++ b/homeassistant/components/mobile_app/translations/pl.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "Czy chcesz skonfigurowa\u0107 komponent aplikacji mobilnej?", - "title": "Aplikacja mobilna" + "description": "Czy chcesz skonfigurowa\u0107 komponent aplikacji mobilnej?" } } } diff --git a/homeassistant/components/mobile_app/translations/pt-BR.json b/homeassistant/components/mobile_app/translations/pt-BR.json index 79917f35d78..4c211f4bc53 100644 --- a/homeassistant/components/mobile_app/translations/pt-BR.json +++ b/homeassistant/components/mobile_app/translations/pt-BR.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "Deseja configurar o componente do aplicativo m\u00f3vel?", - "title": "Aplicativo m\u00f3vel" + "description": "Deseja configurar o componente do aplicativo m\u00f3vel?" } } } diff --git a/homeassistant/components/mobile_app/translations/ru.json b/homeassistant/components/mobile_app/translations/ru.json index f2d2200f448..7bb103b852e 100644 --- a/homeassistant/components/mobile_app/translations/ru.json +++ b/homeassistant/components/mobile_app/translations/ru.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435?", - "title": "\u041c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435" + "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 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435?" } } } diff --git a/homeassistant/components/mobile_app/translations/sl.json b/homeassistant/components/mobile_app/translations/sl.json index bb79e2d1e52..777776ce42d 100644 --- a/homeassistant/components/mobile_app/translations/sl.json +++ b/homeassistant/components/mobile_app/translations/sl.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "Ali \u017eelite nastaviti komponento aplikacije Mobile App?", - "title": "Mobilna Aplikacija" + "description": "Ali \u017eelite nastaviti komponento aplikacije Mobile App?" } } } diff --git a/homeassistant/components/mobile_app/translations/sv.json b/homeassistant/components/mobile_app/translations/sv.json index 497179b2a19..68473a92763 100644 --- a/homeassistant/components/mobile_app/translations/sv.json +++ b/homeassistant/components/mobile_app/translations/sv.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "Vill du konfigurera komponenten Mobile App?", - "title": "Mobilapp" + "description": "Vill du konfigurera komponenten Mobile App?" } } } diff --git a/homeassistant/components/mobile_app/translations/uk.json b/homeassistant/components/mobile_app/translations/uk.json index 0916a0ac34f..4a48dd3775d 100644 --- a/homeassistant/components/mobile_app/translations/uk.json +++ b/homeassistant/components/mobile_app/translations/uk.json @@ -2,8 +2,7 @@ "config": { "step": { "confirm": { - "description": "\u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043c\u043e\u0431\u0456\u043b\u044c\u043d\u043e\u0433\u043e \u0434\u043e\u0434\u0430\u0442\u043a\u0430?", - "title": "\u041c\u043e\u0431\u0456\u043b\u044c\u043d\u0438\u0439 \u0434\u043e\u0434\u0430\u0442\u043e\u043a" + "description": "\u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043c\u043e\u0431\u0456\u043b\u044c\u043d\u043e\u0433\u043e \u0434\u043e\u0434\u0430\u0442\u043a\u0430?" } } } diff --git a/homeassistant/components/mobile_app/translations/vi.json b/homeassistant/components/mobile_app/translations/vi.json index 272e4d6f04f..478ce0e45fd 100644 --- a/homeassistant/components/mobile_app/translations/vi.json +++ b/homeassistant/components/mobile_app/translations/vi.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "B\u1ea1n c\u00f3 mu\u1ed1n thi\u1ebft l\u1eadp th\u00e0nh ph\u1ea7n \u1ee8ng d\u1ee5ng di \u0111\u1ed9ng kh\u00f4ng?", - "title": "\u1ee8ng d\u1ee5ng di \u0111\u1ed9ng" + "description": "B\u1ea1n c\u00f3 mu\u1ed1n thi\u1ebft l\u1eadp th\u00e0nh ph\u1ea7n \u1ee8ng d\u1ee5ng di \u0111\u1ed9ng kh\u00f4ng?" } } } diff --git a/homeassistant/components/mobile_app/translations/zh-Hans.json b/homeassistant/components/mobile_app/translations/zh-Hans.json index 5bef54b6df6..b48ca1e4263 100644 --- a/homeassistant/components/mobile_app/translations/zh-Hans.json +++ b/homeassistant/components/mobile_app/translations/zh-Hans.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "\u60a8\u60f3\u8981\u914d\u7f6e\u79fb\u52a8\u5e94\u7528\u7a0b\u5e8f\u7ec4\u4ef6\u5417\uff1f", - "title": "\u79fb\u52a8\u5e94\u7528" + "description": "\u60a8\u60f3\u8981\u914d\u7f6e\u79fb\u52a8\u5e94\u7528\u7a0b\u5e8f\u7ec4\u4ef6\u5417\uff1f" } } } diff --git a/homeassistant/components/mobile_app/translations/zh-Hant.json b/homeassistant/components/mobile_app/translations/zh-Hant.json index d088b64b4d4..e09710e4235 100644 --- a/homeassistant/components/mobile_app/translations/zh-Hant.json +++ b/homeassistant/components/mobile_app/translations/zh-Hant.json @@ -5,8 +5,7 @@ }, "step": { "confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u624b\u6a5f App \u5143\u4ef6\uff1f", - "title": "\u624b\u6a5f App" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u624b\u6a5f App \u5143\u4ef6\uff1f" } } } diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 610173ca337..c71f3699019 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -1,4 +1,5 @@ """Webhook handlers for mobile_app.""" +import asyncio from functools import wraps import logging import secrets @@ -28,7 +29,7 @@ from homeassistant.const import ( HTTP_CREATED, ) from homeassistant.core import EventOrigin -from homeassistant.exceptions import HomeAssistantError, ServiceNotFound, TemplateError +from homeassistant.exceptions import ServiceNotFound, TemplateError from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.template import attach @@ -77,7 +78,6 @@ from .const import ( ERR_ENCRYPTION_NOT_AVAILABLE, ERR_ENCRYPTION_REQUIRED, ERR_INVALID_FORMAT, - ERR_SENSOR_DUPLICATE_UNIQUE_ID, ERR_SENSOR_NOT_REGISTERED, SIGNAL_LOCATION_UPDATE, SIGNAL_SENSOR_UPDATE, @@ -95,6 +95,7 @@ from .helpers import ( _LOGGER = logging.getLogger(__name__) +DELAY_SAVE = 10 WEBHOOK_COMMANDS = Registry() @@ -184,7 +185,10 @@ async def handle_webhook( "Received webhook payload for type %s: %s", webhook_type, webhook_payload ) - return await WEBHOOK_COMMANDS[webhook_type](hass, config_entry, webhook_payload) + # Shield so we make sure we finish the webhook, even if sender hangs up. + return await asyncio.shield( + WEBHOOK_COMMANDS[webhook_type](hass, config_entry, webhook_payload) + ) @WEBHOOK_COMMANDS.register("call_service") @@ -361,31 +365,30 @@ async def webhook_enable_encryption(hass, config_entry, data): async def webhook_register_sensor(hass, config_entry, data): """Handle a register sensor webhook.""" entity_type = data[ATTR_SENSOR_TYPE] - unique_id = data[ATTR_SENSOR_UNIQUE_ID] unique_store_key = f"{config_entry.data[CONF_WEBHOOK_ID]}_{unique_id}" - - if unique_store_key in hass.data[DOMAIN][entity_type]: - _LOGGER.error("Refusing to re-register existing sensor %s!", unique_id) - return error_response( - ERR_SENSOR_DUPLICATE_UNIQUE_ID, - f"{entity_type} {unique_id} already exists!", - status=409, - ) + existing_sensor = unique_store_key in hass.data[DOMAIN][entity_type] data[CONF_WEBHOOK_ID] = config_entry.data[CONF_WEBHOOK_ID] + # If sensor already is registered, update current state instead + if existing_sensor: + _LOGGER.debug("Re-register existing sensor %s", unique_id) + entry = hass.data[DOMAIN][entity_type][unique_store_key] + data = {**entry, **data} + hass.data[DOMAIN][entity_type][unique_store_key] = data - try: - await hass.data[DOMAIN][DATA_STORE].async_save(savable_state(hass)) - except HomeAssistantError as ex: - _LOGGER.error("Error registering sensor: %s", ex) - return empty_okay_response() + hass.data[DOMAIN][DATA_STORE].async_delay_save( + lambda: savable_state(hass), DELAY_SAVE + ) - register_signal = f"{DOMAIN}_{data[ATTR_SENSOR_TYPE]}_register" - async_dispatcher_send(hass, register_signal, data) + if existing_sensor: + async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, data) + else: + register_signal = f"{DOMAIN}_{data[ATTR_SENSOR_TYPE]}_register" + async_dispatcher_send(hass, register_signal, data) return webhook_response( {"success": True}, registration=config_entry.data, status=HTTP_CREATED, @@ -460,18 +463,14 @@ async def webhook_update_sensor_states(hass, config_entry, data): hass.data[DOMAIN][entity_type][unique_store_key] = new_state - safe = savable_state(hass) - - try: - await hass.data[DOMAIN][DATA_STORE].async_save(safe) - except HomeAssistantError as ex: - _LOGGER.error("Error updating mobile_app registration: %s", ex) - return empty_okay_response() - async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, new_state) resp[unique_id] = {"success": True} + hass.data[DOMAIN][DATA_STORE].async_delay_save( + lambda: savable_state(hass), DELAY_SAVE + ) + return webhook_response(resp, registration=config_entry.data) diff --git a/homeassistant/components/monoprice/translations/ca.json b/homeassistant/components/monoprice/translations/ca.json index e6e6208a8a4..6af5204b91e 100644 --- a/homeassistant/components/monoprice/translations/ca.json +++ b/homeassistant/components/monoprice/translations/ca.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "port": "Port s\u00e8rie", + "port": "Port", "source_1": "Nom de la font #1", "source_2": "Nom de la font #2", "source_3": "Nom de la font #3", diff --git a/homeassistant/components/monoprice/translations/hu.json b/homeassistant/components/monoprice/translations/hu.json new file mode 100644 index 00000000000..892b8b2cd91 --- /dev/null +++ b/homeassistant/components/monoprice/translations/hu.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/monoprice/translations/it.json b/homeassistant/components/monoprice/translations/it.json index d16a249d1a5..b89758a9da3 100644 --- a/homeassistant/components/monoprice/translations/it.json +++ b/homeassistant/components/monoprice/translations/it.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "port": "Porta seriale", + "port": "Porta", "source_1": "Nome della fonte n. 1", "source_2": "Nome della fonte n. 2", "source_3": "Nome della fonte n. 3", diff --git a/homeassistant/components/monoprice/translations/pl.json b/homeassistant/components/monoprice/translations/pl.json index 38dc9f9402f..b5af0e8851f 100644 --- a/homeassistant/components/monoprice/translations/pl.json +++ b/homeassistant/components/monoprice/translations/pl.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "unknown": "[%key_id:common::config_flow::error::unknown%]" + "unknown": "Nieoczekiwany b\u0142\u0105d." }, "step": { "user": { "data": { - "port": "[%key_id:common::config_flow::data::port%] szeregowy", + "port": "Port", "source_1": "Nazwa \u017ar\u00f3d\u0142a #1", "source_2": "Nazwa \u017ar\u00f3d\u0142a #2", "source_3": "Nazwa \u017ar\u00f3d\u0142a #3", @@ -18,7 +18,7 @@ "source_5": "Nazwa \u017ar\u00f3d\u0142a #5", "source_6": "Nazwa \u017ar\u00f3d\u0142a #6" }, - "title": "Po\u0142\u0105cz z urz\u0105dzeniem" + "title": "Po\u0142\u0105czenie z urz\u0105dzeniem" } } }, diff --git a/homeassistant/components/monoprice/translations/pt-BR.json b/homeassistant/components/monoprice/translations/pt-BR.json new file mode 100644 index 00000000000..4eb010468f3 --- /dev/null +++ b/homeassistant/components/monoprice/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha ao conectar, tente novamente", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "source_1": "Nome da fonte #1", + "source_2": "Nome da fonte #2", + "source_3": "Nome da fonte #3", + "source_4": "Nome da fonte #4", + "source_5": "Nome da fonte #5", + "source_6": "Nome da fonte #6" + }, + "title": "Conecte-se ao dispositivo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index a1eaf2f3a2a..2be31895979 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -18,6 +18,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import websocket_api from homeassistant.const import ( + CONF_CLIENT_ID, CONF_DEVICE, CONF_NAME, CONF_PASSWORD, @@ -70,7 +71,6 @@ SERVICE_DUMP = "dump" CONF_EMBEDDED = "embedded" -CONF_CLIENT_ID = "client_id" CONF_DISCOVERY_PREFIX = "discovery_prefix" CONF_KEEPALIVE = "keepalive" CONF_CERTIFICATE = "certificate" diff --git a/homeassistant/components/mqtt/translations/bg.json b/homeassistant/components/mqtt/translations/bg.json index 384efdebffc..7a470b74272 100644 --- a/homeassistant/components/mqtt/translations/bg.json +++ b/homeassistant/components/mqtt/translations/bg.json @@ -15,8 +15,7 @@ "port": "\u041f\u043e\u0440\u0442", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" }, - "description": "\u041c\u043e\u043b\u044f, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f\u0442\u0430 \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0412\u0430\u0448\u0438\u044f MQTT \u0431\u0440\u043e\u043a\u0435\u0440.", - "title": "MQTT" + "description": "\u041c\u043e\u043b\u044f, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f\u0442\u0430 \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0412\u0430\u0448\u0438\u044f MQTT \u0431\u0440\u043e\u043a\u0435\u0440." }, "hassio_confirm": { "data": { diff --git a/homeassistant/components/mqtt/translations/ca.json b/homeassistant/components/mqtt/translations/ca.json index 7b93dfe2fbd..f0c9b5d50d0 100644 --- a/homeassistant/components/mqtt/translations/ca.json +++ b/homeassistant/components/mqtt/translations/ca.json @@ -15,8 +15,7 @@ "port": "Port", "username": "Nom d'usuari" }, - "description": "Introdueix la informaci\u00f3 de connexi\u00f3 del teu broker MQTT.", - "title": "MQTT" + "description": "Introdueix la informaci\u00f3 de connexi\u00f3 del teu broker MQTT." }, "hassio_confirm": { "data": { diff --git a/homeassistant/components/mqtt/translations/cs.json b/homeassistant/components/mqtt/translations/cs.json index d021c1fcba8..b49bc8cf343 100644 --- a/homeassistant/components/mqtt/translations/cs.json +++ b/homeassistant/components/mqtt/translations/cs.json @@ -15,8 +15,7 @@ "port": "Port", "username": "U\u017eivatelsk\u00e9 jm\u00e9no" }, - "description": "Zadejte informace proo p\u0159ipojen\u00ed zprost\u0159edkovatele protokolu MQTT.", - "title": "MQTT" + "description": "Zadejte informace proo p\u0159ipojen\u00ed zprost\u0159edkovatele protokolu MQTT." }, "hassio_confirm": { "data": { diff --git a/homeassistant/components/mqtt/translations/da.json b/homeassistant/components/mqtt/translations/da.json index ba605bd6595..7ff0f2b0a70 100644 --- a/homeassistant/components/mqtt/translations/da.json +++ b/homeassistant/components/mqtt/translations/da.json @@ -15,8 +15,7 @@ "port": "Port", "username": "Brugernavn" }, - "description": "Indtast venligst forbindelsesindstillinger for din MQTT-broker.", - "title": "MQTT" + "description": "Indtast venligst forbindelsesindstillinger for din MQTT-broker." }, "hassio_confirm": { "data": { diff --git a/homeassistant/components/mqtt/translations/de.json b/homeassistant/components/mqtt/translations/de.json index 12e1c5bc46c..75e9908de41 100644 --- a/homeassistant/components/mqtt/translations/de.json +++ b/homeassistant/components/mqtt/translations/de.json @@ -15,8 +15,7 @@ "port": "Port", "username": "Benutzername" }, - "description": "Bitte gib die Verbindungsinformationen deines MQTT-Brokers ein.", - "title": "MQTT" + "description": "Bitte gib die Verbindungsinformationen deines MQTT-Brokers ein." }, "hassio_confirm": { "data": { diff --git a/homeassistant/components/mqtt/translations/en.json b/homeassistant/components/mqtt/translations/en.json index f7bc89da80b..dc3231533d0 100644 --- a/homeassistant/components/mqtt/translations/en.json +++ b/homeassistant/components/mqtt/translations/en.json @@ -15,8 +15,7 @@ "port": "Port", "username": "Username" }, - "description": "Please enter the connection information of your MQTT broker.", - "title": "MQTT" + "description": "Please enter the connection information of your MQTT broker." }, "hassio_confirm": { "data": { diff --git a/homeassistant/components/mqtt/translations/es-419.json b/homeassistant/components/mqtt/translations/es-419.json index afb55aca1ce..d2ddc6691d1 100644 --- a/homeassistant/components/mqtt/translations/es-419.json +++ b/homeassistant/components/mqtt/translations/es-419.json @@ -15,8 +15,7 @@ "port": "Puerto", "username": "Nombre de usuario" }, - "description": "Por favor ingrese la informaci\u00f3n de conexi\u00f3n de su agente MQTT.", - "title": "MQTT" + "description": "Por favor ingrese la informaci\u00f3n de conexi\u00f3n de su agente MQTT." }, "hassio_confirm": { "data": { diff --git a/homeassistant/components/mqtt/translations/es.json b/homeassistant/components/mqtt/translations/es.json index 5ba2211f912..a55d2d7bd07 100644 --- a/homeassistant/components/mqtt/translations/es.json +++ b/homeassistant/components/mqtt/translations/es.json @@ -15,8 +15,7 @@ "port": "Puerto", "username": "Usuario" }, - "description": "Por favor, introduce la informaci\u00f3n de tu agente MQTT", - "title": "MQTT" + "description": "Por favor, introduce la informaci\u00f3n de tu agente MQTT" }, "hassio_confirm": { "data": { diff --git a/homeassistant/components/mqtt/translations/fi.json b/homeassistant/components/mqtt/translations/fi.json index 3659e489d8a..27a956beb33 100644 --- a/homeassistant/components/mqtt/translations/fi.json +++ b/homeassistant/components/mqtt/translations/fi.json @@ -8,8 +8,7 @@ "password": "Salasana", "port": "Portti", "username": "K\u00e4ytt\u00e4j\u00e4tunnus" - }, - "title": "MQTT" + } }, "hassio_confirm": { "data": { diff --git a/homeassistant/components/mqtt/translations/fr.json b/homeassistant/components/mqtt/translations/fr.json index f97807b015f..6571ce3b724 100644 --- a/homeassistant/components/mqtt/translations/fr.json +++ b/homeassistant/components/mqtt/translations/fr.json @@ -15,8 +15,7 @@ "port": "Port", "username": "Nom d'utilisateur" }, - "description": "Veuillez entrer les informations de connexion de votre broker MQTT.", - "title": "MQTT" + "description": "Veuillez entrer les informations de connexion de votre broker MQTT." }, "hassio_confirm": { "data": { diff --git a/homeassistant/components/mqtt/translations/he.json b/homeassistant/components/mqtt/translations/he.json index f8defa49708..bd083dfc1ec 100644 --- a/homeassistant/components/mqtt/translations/he.json +++ b/homeassistant/components/mqtt/translations/he.json @@ -15,8 +15,7 @@ "port": "\u05e4\u05d5\u05e8\u05d8", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" }, - "description": "\u05e0\u05d0 \u05dc\u05d4\u05d6\u05d9\u05df \u05d0\u05ea \u05e4\u05e8\u05d8\u05d9 \u05d4\u05d7\u05d9\u05d1\u05d5\u05e8 \u05e9\u05dc \u05d4\u05d1\u05e8\u05d5\u05e7\u05e8 MQTT \u05e9\u05dc\u05da.", - "title": "MQTT" + "description": "\u05e0\u05d0 \u05dc\u05d4\u05d6\u05d9\u05df \u05d0\u05ea \u05e4\u05e8\u05d8\u05d9 \u05d4\u05d7\u05d9\u05d1\u05d5\u05e8 \u05e9\u05dc \u05d4\u05d1\u05e8\u05d5\u05e7\u05e8 MQTT \u05e9\u05dc\u05da." } } } diff --git a/homeassistant/components/mqtt/translations/hr.json b/homeassistant/components/mqtt/translations/hr.json index 67994227513..6ddc827fff9 100644 --- a/homeassistant/components/mqtt/translations/hr.json +++ b/homeassistant/components/mqtt/translations/hr.json @@ -6,8 +6,7 @@ "password": "Lozinka", "port": "Port", "username": "Korisni\u010dko ime" - }, - "title": "MQTT" + } } } } diff --git a/homeassistant/components/mqtt/translations/hu.json b/homeassistant/components/mqtt/translations/hu.json index ee0ac26e0cd..8cc6aa0857f 100644 --- a/homeassistant/components/mqtt/translations/hu.json +++ b/homeassistant/components/mqtt/translations/hu.json @@ -15,15 +15,14 @@ "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "K\u00e9rlek, add meg az MQTT br\u00f3ker kapcsol\u00f3d\u00e1si adatait.", - "title": "MQTT" + "description": "K\u00e9rlek, add meg az MQTT br\u00f3ker kapcsol\u00f3d\u00e1si adatait." }, "hassio_confirm": { "data": { "discovery": "Felfedez\u00e9s enged\u00e9lyez\u00e9se" }, "description": "Be szeretn\u00e9d konfigru\u00e1lni, hogy a Home Assistant a(z) {addon} Hass.io add-on \u00e1ltal biztos\u00edtott MQTT br\u00f3kerhez csatlakozzon?", - "title": "MQTT Broker a Hass.io b\u0151v\u00edtm\u00e9nyen kereszt\u00fcl" + "title": "MQTT Br\u00f3ker a Hass.io b\u0151v\u00edtm\u00e9nnyel" } } }, @@ -37,6 +36,16 @@ "button_6": "Hatodik gomb", "turn_off": "Kikapcsol\u00e1s", "turn_on": "Bekapcsol\u00e1s" + }, + "trigger_type": { + "button_double_press": "\"{subtype}\" dupla kattint\u00e1s", + "button_long_press": "\"{subtype}\" folyamatosan nyomva tartva", + "button_long_release": "\"{subtype}\" nyomva tart\u00e1s ut\u00e1n felengedve", + "button_quadruple_press": "\"{subtype}\" n\u00e9gyszeres kattint\u00e1s", + "button_quintuple_press": "\"{subtype}\" \u00f6tsz\u00f6r\u00f6s kattint\u00e1s", + "button_short_press": "\"{subtype}\" lenyomva", + "button_short_release": "\"{subtype}\" felengedve", + "button_triple_press": "\"{subtype}\" tripla kattint\u00e1s" } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/id.json b/homeassistant/components/mqtt/translations/id.json index 0afc9ee7021..e21052a501f 100644 --- a/homeassistant/components/mqtt/translations/id.json +++ b/homeassistant/components/mqtt/translations/id.json @@ -14,8 +14,7 @@ "port": "Port", "username": "Nama pengguna" }, - "description": "Harap masukkan informasi koneksi dari broker MQTT Anda.", - "title": "MQTT" + "description": "Harap masukkan informasi koneksi dari broker MQTT Anda." } } } diff --git a/homeassistant/components/mqtt/translations/it.json b/homeassistant/components/mqtt/translations/it.json index 2007a946dd5..3506049abce 100644 --- a/homeassistant/components/mqtt/translations/it.json +++ b/homeassistant/components/mqtt/translations/it.json @@ -15,8 +15,7 @@ "port": "Porta", "username": "Nome utente" }, - "description": "Inserisci le informazioni di connessione del tuo broker MQTT.", - "title": "MQTT" + "description": "Inserisci le informazioni di connessione del tuo broker MQTT." }, "hassio_confirm": { "data": { diff --git a/homeassistant/components/mqtt/translations/ko.json b/homeassistant/components/mqtt/translations/ko.json index 659cec20394..a337c05fe63 100644 --- a/homeassistant/components/mqtt/translations/ko.json +++ b/homeassistant/components/mqtt/translations/ko.json @@ -15,8 +15,7 @@ "port": "\ud3ec\ud2b8", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, - "description": "MQTT \ube0c\ub85c\ucee4\uc758 \uc5f0\uacb0 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", - "title": "MQTT" + "description": "MQTT \ube0c\ub85c\ucee4\uc758 \uc5f0\uacb0 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694." }, "hassio_confirm": { "data": { diff --git a/homeassistant/components/mqtt/translations/lb.json b/homeassistant/components/mqtt/translations/lb.json index ba84a2e30c5..4ffa66b10bd 100644 --- a/homeassistant/components/mqtt/translations/lb.json +++ b/homeassistant/components/mqtt/translations/lb.json @@ -15,8 +15,7 @@ "port": "Port", "username": "Benotzernumm" }, - "description": "Gitt Verbindungs Informatioune vun \u00e4rem MQTT Broker an.", - "title": "MQTT" + "description": "Gitt Verbindungs Informatioune vun \u00e4rem MQTT Broker an." }, "hassio_confirm": { "data": { diff --git a/homeassistant/components/mqtt/translations/nl.json b/homeassistant/components/mqtt/translations/nl.json index f7c099c2cb6..7953c744f27 100644 --- a/homeassistant/components/mqtt/translations/nl.json +++ b/homeassistant/components/mqtt/translations/nl.json @@ -15,8 +15,7 @@ "port": "Poort", "username": "Gebruikersnaam" }, - "description": "MQTT", - "title": "MQTT" + "description": "MQTT" }, "hassio_confirm": { "data": { diff --git a/homeassistant/components/mqtt/translations/nn.json b/homeassistant/components/mqtt/translations/nn.json index ab64bd1b9fc..fb62ec04585 100644 --- a/homeassistant/components/mqtt/translations/nn.json +++ b/homeassistant/components/mqtt/translations/nn.json @@ -14,8 +14,7 @@ "port": "Port", "username": "Brukarnamn" }, - "description": "Ver vennleg \u00e5 skriv inn tilkoplingsinformasjonen for MQTT-meglaren din", - "title": "MQTT" + "description": "Ver vennleg \u00e5 skriv inn tilkoplingsinformasjonen for MQTT-meglaren din" } } } diff --git a/homeassistant/components/mqtt/translations/no.json b/homeassistant/components/mqtt/translations/no.json index 456b757cb88..962da69062b 100644 --- a/homeassistant/components/mqtt/translations/no.json +++ b/homeassistant/components/mqtt/translations/no.json @@ -15,8 +15,7 @@ "port": "Port", "username": "Brukernavn" }, - "description": "Vennligst fyll ut tilkoblingsinformasjonen for din MQTT megler.", - "title": "" + "description": "Vennligst fyll ut tilkoblingsinformasjonen for din MQTT megler." }, "hassio_confirm": { "data": { diff --git a/homeassistant/components/mqtt/translations/pl.json b/homeassistant/components/mqtt/translations/pl.json index 4d5044e1fb0..3606ac35481 100644 --- a/homeassistant/components/mqtt/translations/pl.json +++ b/homeassistant/components/mqtt/translations/pl.json @@ -11,18 +11,17 @@ "data": { "broker": "Po\u015brednik", "discovery": "W\u0142\u0105cz wykrywanie", - "password": "[%key_id:common::config_flow::data::password%]", - "port": "[%key_id:common::config_flow::data::port%]", - "username": "[%key_id:common::config_flow::data::username%]" + "password": "Has\u0142o", + "port": "Port", + "username": "Nazwa u\u017cytkownika" }, - "description": "Wprowad\u017a informacje o po\u0142\u0105czeniu po\u015brednika MQTT.", - "title": "MQTT" + "description": "Wprowad\u017a informacje o po\u0142\u0105czeniu po\u015brednika MQTT." }, "hassio_confirm": { "data": { "discovery": "W\u0142\u0105cz wykrywanie" }, - "description": "Czy chcesz skonfigurowa\u0107 Home Assistant'a, aby po\u0142\u0105czy\u0142 si\u0119 z po\u015brednikiem MQTT dostarczonym przez dodatek Hass.io {addon}?", + "description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby po\u0142\u0105czy\u0142 si\u0119 z po\u015brednikiem MQTT dostarczonym przez dodatek Hass.io {addon}?", "title": "Po\u015brednik MQTT za po\u015brednictwem dodatku Hass.io" } } diff --git a/homeassistant/components/mqtt/translations/pt-BR.json b/homeassistant/components/mqtt/translations/pt-BR.json index 1fe0ef5c6e5..3a441d0c1f1 100644 --- a/homeassistant/components/mqtt/translations/pt-BR.json +++ b/homeassistant/components/mqtt/translations/pt-BR.json @@ -15,8 +15,7 @@ "port": "Porta", "username": "Nome de usu\u00e1rio" }, - "description": "Por favor, insira as informa\u00e7\u00f5es de conex\u00e3o do seu agente MQTT.", - "title": "MQTT" + "description": "Por favor, insira as informa\u00e7\u00f5es de conex\u00e3o do seu agente MQTT." }, "hassio_confirm": { "data": { @@ -26,5 +25,17 @@ "title": "MQTT Broker via add-on Hass.io" } } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "Primeiro bot\u00e3o", + "button_2": "Segundo bot\u00e3o", + "button_3": "Terceiro bot\u00e3o", + "button_4": "Quarto bot\u00e3o", + "button_5": "Quinto bot\u00e3o", + "button_6": "Sexto bot\u00e3o", + "turn_off": "Desligar", + "turn_on": "Ligar" + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/pt.json b/homeassistant/components/mqtt/translations/pt.json index ef17b291566..7fa10a5592b 100644 --- a/homeassistant/components/mqtt/translations/pt.json +++ b/homeassistant/components/mqtt/translations/pt.json @@ -15,8 +15,7 @@ "port": "Porto", "username": "Nome de Utilizador" }, - "description": "Por favor, insira os detalhes de liga\u00e7\u00e3o ao seu broker MQTT.", - "title": "MQTT" + "description": "Por favor, insira os detalhes de liga\u00e7\u00e3o ao seu broker MQTT." }, "hassio_confirm": { "data": { diff --git a/homeassistant/components/mqtt/translations/ro.json b/homeassistant/components/mqtt/translations/ro.json index 5fb6d3a3e65..2292b58d01d 100644 --- a/homeassistant/components/mqtt/translations/ro.json +++ b/homeassistant/components/mqtt/translations/ro.json @@ -15,8 +15,7 @@ "port": "Port", "username": "Nume de utilizator" }, - "description": "Introduce\u021bi informa\u021biile de conectare ale brokerului dvs. MQTT.", - "title": "MQTT" + "description": "Introduce\u021bi informa\u021biile de conectare ale brokerului dvs. MQTT." }, "hassio_confirm": { "data": { diff --git a/homeassistant/components/mqtt/translations/ru.json b/homeassistant/components/mqtt/translations/ru.json index 3de009aa002..e1c5c0d979e 100644 --- a/homeassistant/components/mqtt/translations/ru.json +++ b/homeassistant/components/mqtt/translations/ru.json @@ -15,8 +15,7 @@ "port": "\u041f\u043e\u0440\u0442", "username": "\u041b\u043e\u0433\u0438\u043d" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a \u0412\u0430\u0448\u0435\u043c\u0443 \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT.", - "title": "MQTT" + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a \u0412\u0430\u0448\u0435\u043c\u0443 \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT." }, "hassio_confirm": { "data": { diff --git a/homeassistant/components/mqtt/translations/sl.json b/homeassistant/components/mqtt/translations/sl.json index 9ca44a36d84..691fdcf058a 100644 --- a/homeassistant/components/mqtt/translations/sl.json +++ b/homeassistant/components/mqtt/translations/sl.json @@ -15,8 +15,7 @@ "port": "port", "username": "Uporabni\u0161ko ime" }, - "description": "Prosimo vnesite informacije o povezavi va\u0161ega MQTT posrednika.", - "title": "MQTT" + "description": "Prosimo vnesite informacije o povezavi va\u0161ega MQTT posrednika." }, "hassio_confirm": { "data": { diff --git a/homeassistant/components/mqtt/translations/sv.json b/homeassistant/components/mqtt/translations/sv.json index 9415beedbf7..d5d73c4ae55 100644 --- a/homeassistant/components/mqtt/translations/sv.json +++ b/homeassistant/components/mqtt/translations/sv.json @@ -15,8 +15,7 @@ "port": "Port", "username": "Anv\u00e4ndarnamn" }, - "description": "V\u00e4nligen ange anslutningsinformationen f\u00f6r din MQTT broker.", - "title": "MQTT" + "description": "V\u00e4nligen ange anslutningsinformationen f\u00f6r din MQTT broker." }, "hassio_confirm": { "data": { diff --git a/homeassistant/components/mqtt/translations/zh-Hans.json b/homeassistant/components/mqtt/translations/zh-Hans.json index a5da0304c5d..4a9b860f873 100644 --- a/homeassistant/components/mqtt/translations/zh-Hans.json +++ b/homeassistant/components/mqtt/translations/zh-Hans.json @@ -15,8 +15,7 @@ "port": "\u7aef\u53e3", "username": "\u7528\u6237\u540d" }, - "description": "\u8bf7\u8f93\u5165\u60a8\u7684 MQTT \u670d\u52a1\u5668\u7684\u8fde\u63a5\u4fe1\u606f\u3002", - "title": "MQTT" + "description": "\u8bf7\u8f93\u5165\u60a8\u7684 MQTT \u670d\u52a1\u5668\u7684\u8fde\u63a5\u4fe1\u606f\u3002" }, "hassio_confirm": { "data": { diff --git a/homeassistant/components/mqtt/translations/zh-Hant.json b/homeassistant/components/mqtt/translations/zh-Hant.json index 0bf4064bf52..2b50e38ae7e 100644 --- a/homeassistant/components/mqtt/translations/zh-Hant.json +++ b/homeassistant/components/mqtt/translations/zh-Hant.json @@ -15,8 +15,7 @@ "port": "\u901a\u8a0a\u57e0", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u8acb\u8f38\u5165 MQTT Broker \u9023\u7dda\u8cc7\u8a0a\u3002", - "title": "MQTT" + "description": "\u8acb\u8f38\u5165 MQTT Broker \u9023\u7dda\u8cc7\u8a0a\u3002" }, "hassio_confirm": { "data": { diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index c4259272bb3..b69565f7114 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -395,33 +395,21 @@ class MqttVacuum( @property def status(self): """Return a status string for the vacuum.""" - if self.supported_features & SUPPORT_STATUS == 0: - return None - return self._status @property def fan_speed(self): """Return the status of the vacuum.""" - if self.supported_features & SUPPORT_FAN_SPEED == 0: - return None - return self._fan_speed @property def fan_speed_list(self): - """Return the status of the vacuum. - - No need to check SUPPORT_FAN_SPEED, this won't be called if fan_speed is None. - """ + """Return the status of the vacuum.""" return self._fan_speed_list @property def battery_level(self): """Return the status of the vacuum.""" - if self.supported_features & SUPPORT_BATTERY == 0: - return None - return max(0, min(100, self._battery_level)) @property diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index 9049df45110..628f85614fe 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -275,24 +275,16 @@ class MqttStateVacuum( @property def fan_speed(self): """Return fan speed of the vacuum.""" - if self.supported_features & SUPPORT_FAN_SPEED == 0: - return None - return self._state_attrs.get(FAN_SPEED, 0) @property def fan_speed_list(self): - """Return fan speed list of the vacuum. - - No need to check SUPPORT_FAN_SPEED, this won't be called if fan_speed is None. - """ + """Return fan speed list of the vacuum.""" return self._fan_speed_list @property def battery_level(self): """Return battery level of the vacuum.""" - if self.supported_features & SUPPORT_BATTERY == 0: - return None return max(0, min(100, self._state_attrs.get(BATTERY, 0))) @property diff --git a/homeassistant/components/myq/translations/hu.json b/homeassistant/components/myq/translations/hu.json new file mode 100644 index 00000000000..dee4ed9ee0f --- /dev/null +++ b/homeassistant/components/myq/translations/hu.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/myq/translations/pl.json b/homeassistant/components/myq/translations/pl.json index 30a4221134e..aefc8336903 100644 --- a/homeassistant/components/myq/translations/pl.json +++ b/homeassistant/components/myq/translations/pl.json @@ -5,14 +5,14 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "[%key_id:common::config_flow::error::invalid_auth%]", - "unknown": "[%key_id:common::config_flow::error::unknown%]" + "invalid_auth": "Niepoprawne uwierzytelnienie.", + "unknown": "Nieoczekiwany b\u0142\u0105d." }, "step": { "user": { "data": { - "password": "[%key_id:common::config_flow::data::password%]", - "username": "[%key_id:common::config_flow::data::username%]" + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" }, "title": "Po\u0142\u0105czenie z bramk\u0105 MyQ" } diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index 15876163762..d170f476158 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -4,6 +4,8 @@ from homeassistant.components.sensor import DOMAIN from homeassistant.const import ( CONDUCTIVITY, DEGREE, + ELECTRICAL_CURRENT_AMPERE, + ELECTRICAL_VOLT_AMPERE, ENERGY_KILO_WATT_HOUR, FREQUENCY_HERTZ, LENGTH_METERS, @@ -41,12 +43,12 @@ SENSORS = { "S_LIGHT_LEVEL": ["lx", "mdi:white-balance-sunny"], }, "V_VOLTAGE": [VOLT, "mdi:flash"], - "V_CURRENT": ["A", "mdi:flash-auto"], + "V_CURRENT": [ELECTRICAL_CURRENT_AMPERE, "mdi:flash-auto"], "V_PH": ["pH", None], "V_ORP": ["mV", None], "V_EC": [CONDUCTIVITY, None], "V_VAR": ["var", None], - "V_VA": ["VA", None], + "V_VA": [ELECTRICAL_VOLT_AMPERE, None], } diff --git a/homeassistant/components/nad/media_player.py b/homeassistant/components/nad/media_player.py index 2d9afbb7541..6145a89a594 100644 --- a/homeassistant/components/nad/media_player.py +++ b/homeassistant/components/nad/media_player.py @@ -64,71 +64,53 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the NAD platform.""" - if config.get(CONF_TYPE) == "RS232": + if config.get(CONF_TYPE) in ("RS232", "Telnet"): add_entities( - [ - NAD( - config.get(CONF_NAME), - NADReceiver(config.get(CONF_SERIAL_PORT)), - config.get(CONF_MIN_VOLUME), - config.get(CONF_MAX_VOLUME), - config.get(CONF_SOURCE_DICT), - ) - ], - True, - ) - elif config.get(CONF_TYPE) == "Telnet": - add_entities( - [ - NAD( - config.get(CONF_NAME), - NADReceiverTelnet(config.get(CONF_HOST), config.get(CONF_PORT)), - config.get(CONF_MIN_VOLUME), - config.get(CONF_MAX_VOLUME), - config.get(CONF_SOURCE_DICT), - ) - ], - True, + [NAD(config)], True, ) else: add_entities( - [ - NADtcp( - config.get(CONF_NAME), - NADReceiverTCP(config.get(CONF_HOST)), - config.get(CONF_MIN_VOLUME), - config.get(CONF_MAX_VOLUME), - config.get(CONF_VOLUME_STEP), - ) - ], - True, + [NADtcp(config)], True, ) class NAD(MediaPlayerEntity): """Representation of a NAD Receiver.""" - def __init__(self, name, nad_receiver, min_volume, max_volume, source_dict): + def __init__(self, config): """Initialize the NAD Receiver device.""" - self._name = name - self._nad_receiver = nad_receiver - self._min_volume = min_volume - self._max_volume = max_volume - self._source_dict = source_dict + self.config = config + self._instantiate_nad_receiver() + self._min_volume = config[CONF_MIN_VOLUME] + self._max_volume = config[CONF_MAX_VOLUME] + self._source_dict = config[CONF_SOURCE_DICT] self._reverse_mapping = {value: key for key, value in self._source_dict.items()} self._volume = self._state = self._mute = self._source = None + def _instantiate_nad_receiver(self) -> NADReceiver: + if self.config[CONF_TYPE] == "RS232": + self._nad_receiver = NADReceiver(self.config[CONF_SERIAL_PORT]) + else: + host = self.config.get(CONF_HOST) + port = self.config[CONF_PORT] + self._nad_receiver = NADReceiverTelnet(host, port) + @property def name(self): """Return the name of the device.""" - return self._name + return self.config[CONF_NAME] @property def state(self): """Return the state of the device.""" return self._state + @property + def icon(self): + """Return the icon for the device.""" + return "mdi:speaker-multiple" + @property def volume_level(self): """Volume level of the media player (0..1).""" @@ -185,23 +167,28 @@ class NAD(MediaPlayerEntity): """List of available input sources.""" return sorted(list(self._reverse_mapping.keys())) - def update(self): + @property + def available(self): + """Return if device is available.""" + return self._state is not None + + def update(self) -> None: """Retrieve latest state.""" - if self._nad_receiver.main_power("?") == "Off": - self._state = STATE_OFF - else: - self._state = STATE_ON + power_state = self._nad_receiver.main_power("?") + if not power_state: + self._state = None + return + self._state = ( + STATE_ON if self._nad_receiver.main_power("?") == "On" else STATE_OFF + ) - if self._nad_receiver.main_mute("?") == "Off": - self._mute = False - else: - self._mute = True - - volume = self._nad_receiver.main_volume("?") - # Some receivers cannot report the volume, e.g. C 356BEE, - # instead they only support stepping the volume up or down - self._volume = self.calc_volume(volume) if volume is not None else None - self._source = self._source_dict.get(self._nad_receiver.main_source("?")) + if self._state == STATE_ON: + self._mute = self._nad_receiver.main_mute("?") == "On" + volume = self._nad_receiver.main_volume("?") + # Some receivers cannot report the volume, e.g. C 356BEE, + # instead they only support stepping the volume up or down + self._volume = self.calc_volume(volume) if volume is not None else None + self._source = self._source_dict.get(self._nad_receiver.main_source("?")) def calc_volume(self, decibel): """ @@ -227,13 +214,13 @@ class NAD(MediaPlayerEntity): class NADtcp(MediaPlayerEntity): """Representation of a NAD Digital amplifier.""" - def __init__(self, name, nad_device, min_volume, max_volume, volume_step): + def __init__(self, config): """Initialize the amplifier.""" - self._name = name - self._nad_receiver = nad_device - self._min_vol = (min_volume + 90) * 2 # from dB to nad vol (0-200) - self._max_vol = (max_volume + 90) * 2 # from dB to nad vol (0-200) - self._volume_step = volume_step + self._name = config[CONF_NAME] + self._nad_receiver = NADReceiverTCP(config.get(CONF_HOST)) + self._min_vol = (config[CONF_MIN_VOLUME] + 90) * 2 # from dB to nad vol (0-200) + self._max_vol = (config[CONF_MAX_VOLUME] + 90) * 2 # from dB to nad vol (0-200) + self._volume_step = config[CONF_VOLUME_STEP] self._state = None self._mute = None self._nad_volume = None diff --git a/homeassistant/components/nanoleaf/light.py b/homeassistant/components/nanoleaf/light.py index 5073a421e49..a7bf75a15d2 100644 --- a/homeassistant/components/nanoleaf/light.py +++ b/homeassistant/components/nanoleaf/light.py @@ -210,6 +210,10 @@ class NanoleafLight(LightEntity): self._light.brightness = int(brightness / 2.55) if effect: + if effect not in self._effects_list: + raise ValueError( + f"Attempting to apply effect not in the effect list: '{effect}'" + ) self._light.effect = effect def turn_off(self, **kwargs): @@ -227,8 +231,13 @@ class NanoleafLight(LightEntity): self._available = self._light.available self._brightness = self._light.brightness self._color_temp = self._light.color_temperature - self._effect = self._light.effect self._effects_list = self._light.effects + # Nanoleaf api returns non-existent effect named "*Solid*" when light set to solid color. + # This causes various issues with scening (see https://github.com/home-assistant/core/issues/36359). + # Until fixed at the library level, we should ensure the effect exists before saving to light properties + self._effect = ( + self._light.effect if self._light.effect in self._effects_list else None + ) self._hs_color = self._light.hue, self._light.saturation self._state = self._light.on except Unavailable as err: diff --git a/homeassistant/components/neato/translations/pl.json b/homeassistant/components/neato/translations/pl.json index c7b37fa083d..80e0a1df48e 100644 --- a/homeassistant/components/neato/translations/pl.json +++ b/homeassistant/components/neato/translations/pl.json @@ -9,13 +9,13 @@ }, "error": { "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce", - "unexpected_error": "[%key_id:common::config_flow::error::unknown%]" + "unexpected_error": "Nieoczekiwany b\u0142\u0105d." }, "step": { "user": { "data": { - "password": "[%key_id:common::config_flow::data::password%]", - "username": "[%key_id:common::config_flow::data::username%]", + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika", "vendor": "Dostawca" }, "description": "Zapoznaj si\u0119 z [dokumentacj\u0105 Neato]({docs_url}).", diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index f92f6466156..8ddd6da6dcf 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -10,6 +10,8 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import ( CONF_BINARY_SENSORS, + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, CONF_FILENAME, CONF_MONITORED_CONDITIONS, CONF_SENSORS, @@ -37,8 +39,6 @@ DATA_NEST_CONFIG = "nest_config" SIGNAL_NEST_UPDATE = "nest_update" NEST_CONFIG_FILE = "nest.conf" -CONF_CLIENT_ID = "client_id" -CONF_CLIENT_SECRET = "client_secret" ATTR_ETA = "eta" ATTR_ETA_WINDOW = "eta_window" diff --git a/homeassistant/components/nest/translations/fi.json b/homeassistant/components/nest/translations/fi.json index 561b03f9286..d810bd0657e 100644 --- a/homeassistant/components/nest/translations/fi.json +++ b/homeassistant/components/nest/translations/fi.json @@ -11,7 +11,8 @@ "init": { "data": { "flow_impl": "Tarjoaja" - } + }, + "title": "Todentamisen tarjoaja" }, "link": { "data": { diff --git a/homeassistant/components/nest/translations/it.json b/homeassistant/components/nest/translations/it.json index 2e567a27ce1..b525781fdeb 100644 --- a/homeassistant/components/nest/translations/it.json +++ b/homeassistant/components/nest/translations/it.json @@ -4,7 +4,7 @@ "already_setup": "\u00c8 possibile configurare un solo account Nest.", "authorize_url_fail": "Errore sconosciuto nel generare l'url di autorizzazione", "authorize_url_timeout": "Tempo scaduto nel generare l'url di autorizzazione", - "no_flows": "Devi configurare Nest prima di poter eseguire l'autenticazione. [Si prega di leggere le istruzioni] (https://www.home-assistant.io/components/nest/)." + "no_flows": "Devi configurare Nest prima di poter eseguire l'autenticazione. [Si prega di leggere le istruzioni](https://www.home-assistant.io/components/nest/)." }, "error": { "internal_error": "Errore interno nella convalida del codice", diff --git a/homeassistant/components/nest/translations/pl.json b/homeassistant/components/nest/translations/pl.json index 775e644a113..75d0d99f5d8 100644 --- a/homeassistant/components/nest/translations/pl.json +++ b/homeassistant/components/nest/translations/pl.json @@ -3,7 +3,7 @@ "abort": { "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Nest.", "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji.", - "authorize_url_timeout": "[%key_id:common::config_flow::abort::oauth2_authorize_url_timeout%]", + "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", "no_flows": "Musisz skonfigurowa\u0107 Nest, aby m\u00f3c si\u0119 z nim uwierzytelni\u0107. Zapoznaj si\u0119 z [instrukcj\u0105](https://www.home-assistant.io/components/nest/)." }, "error": { @@ -25,7 +25,7 @@ "code": "Kod PIN" }, "description": "Aby po\u0142\u0105czy\u0107 z kontem Nest, [wykonaj autoryzacj\u0119]({url}). \n\n Po autoryzacji skopiuj i wklej podany kod PIN poni\u017cej.", - "title": "Po\u0142\u0105cz z kontem Nest" + "title": "Po\u0142\u0105czenie z kontem Nest" } } } diff --git a/homeassistant/components/netatmo/translations/hu.json b/homeassistant/components/netatmo/translations/hu.json index fac2dcb754f..4104aba464c 100644 --- a/homeassistant/components/netatmo/translations/hu.json +++ b/homeassistant/components/netatmo/translations/hu.json @@ -1,10 +1,17 @@ { "config": { "abort": { - "already_setup": "Csak egy Netatmo-fi\u00f3kot \u00e1ll\u00edthatsz be." + "already_setup": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", + "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t." }, "create_entry": { - "default": "A Netatmo sikeresen hiteles\u00edtett." + "default": "Sikeres hiteles\u00edt\u00e9s" + }, + "step": { + "pick_implementation": { + "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" + } } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/pl.json b/homeassistant/components/netatmo/translations/pl.json index 718b44d60db..0f1c29801fe 100644 --- a/homeassistant/components/netatmo/translations/pl.json +++ b/homeassistant/components/netatmo/translations/pl.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Netatmo.", - "authorize_url_timeout": "[%key_id:common::config_flow::abort::oauth2_authorize_url_timeout%]", - "missing_configuration": "[%key_id:common::config_flow::abort::oauth2_missing_configuration%]" + "already_setup": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.", + "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", + "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105." }, "create_entry": { - "default": "Pomy\u015blnie uwierzytelniono z Netatmo." + "default": "Pomy\u015blnie uwierzytelniono" }, "step": { "pick_implementation": { - "title": "[%key_id:common::config_flow::title::oauth2_pick_implementation%]" + "title": "Wybierz metod\u0119 uwierzytelniania" } } } diff --git a/homeassistant/components/nexia/translations/pl.json b/homeassistant/components/nexia/translations/pl.json index b6d13766dee..84844ddbd94 100644 --- a/homeassistant/components/nexia/translations/pl.json +++ b/homeassistant/components/nexia/translations/pl.json @@ -5,14 +5,14 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "[%key_id:common::config_flow::error::invalid_auth%]", - "unknown": "[%key_id:common::config_flow::error::unknown%]" + "invalid_auth": "Niepoprawne uwierzytelnienie.", + "unknown": "Nieoczekiwany b\u0142\u0105d." }, "step": { "user": { "data": { - "password": "[%key_id:common::config_flow::data::password%]", - "username": "[%key_id:common::config_flow::data::username%]" + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" }, "title": "Po\u0142\u0105czenie z mynexia.com" } diff --git a/homeassistant/components/notion/translations/ca.json b/homeassistant/components/notion/translations/ca.json index 7a4831517f0..05626cbd483 100644 --- a/homeassistant/components/notion/translations/ca.json +++ b/homeassistant/components/notion/translations/ca.json @@ -11,7 +11,7 @@ "user": { "data": { "password": "Contrasenya", - "username": "Nom d'usuari / correu electr\u00f2nic" + "username": "Nom d'usuari" }, "title": "Introdueix la teva informaci\u00f3" } diff --git a/homeassistant/components/notion/translations/hu.json b/homeassistant/components/notion/translations/hu.json index 285e6c7b485..fdd239f03d2 100644 --- a/homeassistant/components/notion/translations/hu.json +++ b/homeassistant/components/notion/translations/hu.json @@ -8,7 +8,7 @@ "user": { "data": { "password": "Jelsz\u00f3", - "username": "Felhaszn\u00e1l\u00f3n\u00e9v/Email C\u00edm" + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, "title": "T\u00f6ltsd ki az adataid" } diff --git a/homeassistant/components/notion/translations/it.json b/homeassistant/components/notion/translations/it.json index b60cdeb60f0..c7626f59202 100644 --- a/homeassistant/components/notion/translations/it.json +++ b/homeassistant/components/notion/translations/it.json @@ -11,7 +11,7 @@ "user": { "data": { "password": "Password", - "username": "Nome utente / indirizzo E-mail" + "username": "Nome utente" }, "title": "Inserisci le tue informazioni" } diff --git a/homeassistant/components/notion/translations/pl.json b/homeassistant/components/notion/translations/pl.json index 3350a712504..8e3c60b6956 100644 --- a/homeassistant/components/notion/translations/pl.json +++ b/homeassistant/components/notion/translations/pl.json @@ -10,8 +10,8 @@ "step": { "user": { "data": { - "password": "[%key_id:common::config_flow::data::password%]", - "username": "[%key_id:common::config_flow::data::username%]/adres e-mail" + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" }, "title": "Wprowad\u017a dane" } diff --git a/homeassistant/components/nuheat/translations/pl.json b/homeassistant/components/nuheat/translations/pl.json index d32d1f11e38..d55b545d040 100644 --- a/homeassistant/components/nuheat/translations/pl.json +++ b/homeassistant/components/nuheat/translations/pl.json @@ -5,19 +5,19 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "[%key_id:common::config_flow::error::invalid_auth%]", + "invalid_auth": "Niepoprawne uwierzytelnienie.", "invalid_thermostat": "Numer seryjny termostatu jest nieprawid\u0142owy.", - "unknown": "[%key_id:common::config_flow::error::unknown%]" + "unknown": "Nieoczekiwany b\u0142\u0105d." }, "step": { "user": { "data": { - "password": "[%key_id:common::config_flow::data::password%]", + "password": "Has\u0142o", "serial_number": "Numer seryjny termostatu", - "username": "[%key_id:common::config_flow::data::username%]" + "username": "Nazwa u\u017cytkownika" }, "description": "Musisz uzyska\u0107 numeryczny numer seryjny lub identyfikator termostatu, loguj\u0105c si\u0119 na https://MyNuHeat.com i wybieraj\u0105c termostat(y).", - "title": "Po\u0142\u0105cz z NuHeat" + "title": "Po\u0142\u0105czenie z NuHeat" } } } diff --git a/homeassistant/components/nuheat/translations/pt-BR.json b/homeassistant/components/nuheat/translations/pt-BR.json new file mode 100644 index 00000000000..0fb1e678edd --- /dev/null +++ b/homeassistant/components/nuheat/translations/pt-BR.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "O termostato j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar, tente novamente", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "invalid_thermostat": "O n\u00famero de s\u00e9rie do termostato \u00e9 inv\u00e1lido.", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "serial_number": "N\u00famero de s\u00e9rie do termostato." + }, + "description": "Voc\u00ea precisar\u00e1 obter o n\u00famero de s\u00e9rie ou ID num\u00e9rico do seu termostato acessando https://MyNuHeat.com e selecionando seu(s) termostato(s).", + "title": "Conecte-se ao NuHeat" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index 3d382496b28..13825cede94 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -1,4 +1,5 @@ """Nuki.io lock platform.""" +from abc import ABC, abstractmethod from datetime import timedelta import logging @@ -50,9 +51,10 @@ LOCK_N_GO_SERVICE_SCHEMA = vol.Schema( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Nuki lock platform.""" bridge = NukiBridge( - config[CONF_HOST], config[CONF_TOKEN], config[CONF_PORT], DEFAULT_TIMEOUT + config[CONF_HOST], config[CONF_TOKEN], config[CONF_PORT], DEFAULT_TIMEOUT, ) - devices = [NukiLock(lock) for lock in bridge.locks] + + devices = [NukiLockEntity(lock) for lock in bridge.locks] def service_handler(service): """Service handler for nuki services.""" @@ -65,41 +67,43 @@ def setup_platform(hass, config, add_entities, discovery_info=None): lock.lock_n_go(unlatch=unlatch) hass.services.register( - DOMAIN, SERVICE_LOCK_N_GO, service_handler, schema=LOCK_N_GO_SERVICE_SCHEMA + DOMAIN, SERVICE_LOCK_N_GO, service_handler, schema=LOCK_N_GO_SERVICE_SCHEMA, ) + devices.extend([NukiOpenerEntity(opener) for opener in bridge.openers]) + add_entities(devices) -class NukiLock(LockEntity): - """Representation of a Nuki lock.""" +class NukiDeviceEntity(LockEntity, ABC): + """Representation of a Nuki device.""" - def __init__(self, nuki_lock): + def __init__(self, nuki_device): """Initialize the lock.""" - self._nuki_lock = nuki_lock - self._available = nuki_lock.state not in ERROR_STATES + self._nuki_device = nuki_device + self._available = nuki_device.state not in ERROR_STATES @property def name(self): """Return the name of the lock.""" - return self._nuki_lock.name + return self._nuki_device.name @property def unique_id(self) -> str: """Return a unique ID.""" - return self._nuki_lock.nuki_id + return self._nuki_device.nuki_id @property + @abstractmethod def is_locked(self): """Return true if lock is locked.""" - return self._nuki_lock.is_locked @property def device_state_attributes(self): """Return the device specific state attributes.""" data = { - ATTR_BATTERY_CRITICAL: self._nuki_lock.battery_critical, - ATTR_NUKI_ID: self._nuki_lock.nuki_id, + ATTR_BATTERY_CRITICAL: self._nuki_device.battery_critical, + ATTR_NUKI_ID: self._nuki_device.nuki_id, } return data @@ -117,28 +121,49 @@ class NukiLock(LockEntity): """Update the nuki lock properties.""" for level in (False, True): try: - self._nuki_lock.update(aggressive=level) + self._nuki_device.update(aggressive=level) except RequestException: _LOGGER.warning("Network issues detect with %s", self.name) self._available = False continue # If in error state, we force an update and repoll data - self._available = self._nuki_lock.state not in ERROR_STATES + self._available = self._nuki_device.state not in ERROR_STATES if self._available: break + @abstractmethod def lock(self, **kwargs): """Lock the device.""" - self._nuki_lock.lock() + + @abstractmethod + def unlock(self, **kwargs): + """Unlock the device.""" + + @abstractmethod + def open(self, **kwargs): + """Open the door latch.""" + + +class NukiLockEntity(NukiDeviceEntity): + """Representation of a Nuki lock.""" + + @property + def is_locked(self): + """Return true if lock is locked.""" + return self._nuki_device.is_locked + + def lock(self, **kwargs): + """Lock the device.""" + self._nuki_device.lock() def unlock(self, **kwargs): """Unlock the device.""" - self._nuki_lock.unlock() + self._nuki_device.unlock() def open(self, **kwargs): """Open the door latch.""" - self._nuki_lock.unlatch() + self._nuki_device.unlatch() def lock_n_go(self, unlatch=False, **kwargs): """Lock and go. @@ -146,4 +171,25 @@ class NukiLock(LockEntity): This will first unlock the door, then wait for 20 seconds (or another amount of time depending on the lock settings) and relock. """ - self._nuki_lock.lock_n_go(unlatch, kwargs) + self._nuki_device.lock_n_go(unlatch, kwargs) + + +class NukiOpenerEntity(NukiDeviceEntity): + """Representation of a Nuki opener.""" + + @property + def is_locked(self): + """Return true if ring-to-open is enabled.""" + return not self._nuki_device.is_rto_activated + + def lock(self, **kwargs): + """Disable ring-to-open.""" + self._nuki_device.deactivate_rto() + + def unlock(self, **kwargs): + """Enable ring-to-open.""" + self._nuki_device.activate_rto() + + def open(self, **kwargs): + """Buzz open the door.""" + self._nuki_device.electric_strike_actuation() diff --git a/homeassistant/components/nuki/manifest.json b/homeassistant/components/nuki/manifest.json index a51ff3752a5..386b36a3ca9 100644 --- a/homeassistant/components/nuki/manifest.json +++ b/homeassistant/components/nuki/manifest.json @@ -2,6 +2,6 @@ "domain": "nuki", "name": "Nuki", "documentation": "https://www.home-assistant.io/integrations/nuki", - "requirements": ["pynuki==1.3.3"], + "requirements": ["pynuki==1.3.7"], "codeowners": ["@pvizeli"] } diff --git a/homeassistant/components/nut/const.py b/homeassistant/components/nut/const.py index 9b4908530a8..47959108a2c 100644 --- a/homeassistant/components/nut/const.py +++ b/homeassistant/components/nut/const.py @@ -5,6 +5,8 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_TEMPERATURE, ) from homeassistant.const import ( + ELECTRICAL_CURRENT_AMPERE, + ELECTRICAL_VOLT_AMPERE, FREQUENCY_HERTZ, POWER_WATT, TEMP_CELSIUS, @@ -61,8 +63,8 @@ SENSOR_TYPES = { "ups.display.language": ["Language", "", "mdi:information-outline", None], "ups.contacts": ["External Contacts", "", "mdi:information-outline", None], "ups.efficiency": ["Efficiency", UNIT_PERCENTAGE, "mdi:gauge", None], - "ups.power": ["Current Apparent Power", "VA", "mdi:flash", None], - "ups.power.nominal": ["Nominal Power", "VA", "mdi:flash", None], + "ups.power": ["Current Apparent Power", ELECTRICAL_VOLT_AMPERE, "mdi:flash", None], + "ups.power.nominal": ["Nominal Power", ELECTRICAL_VOLT_AMPERE, "mdi:flash", None], "ups.realpower": [ "Current Real Power", POWER_WATT, @@ -107,8 +109,18 @@ SENSOR_TYPES = { "battery.voltage.low": ["Low Battery Voltage", VOLT, "mdi:flash", None], "battery.voltage.high": ["High Battery Voltage", VOLT, "mdi:flash", None], "battery.capacity": ["Battery Capacity", "Ah", "mdi:flash", None], - "battery.current": ["Battery Current", "A", "mdi:flash", None], - "battery.current.total": ["Total Battery Current", "A", "mdi:flash", None], + "battery.current": [ + "Battery Current", + ELECTRICAL_CURRENT_AMPERE, + "mdi:flash", + None, + ], + "battery.current.total": [ + "Total Battery Current", + ELECTRICAL_CURRENT_AMPERE, + "mdi:flash", + None, + ], "battery.temperature": [ "Battery Temperature", TEMP_CELSIUS, @@ -168,8 +180,13 @@ SENSOR_TYPES = { "mdi:information-outline", None, ], - "output.current": ["Output Current", "A", "mdi:flash", None], - "output.current.nominal": ["Nominal Output Current", "A", "mdi:flash", None], + "output.current": ["Output Current", ELECTRICAL_CURRENT_AMPERE, "mdi:flash", None], + "output.current.nominal": [ + "Nominal Output Current", + ELECTRICAL_CURRENT_AMPERE, + "mdi:flash", + None, + ], "output.voltage": ["Output Voltage", VOLT, "mdi:flash", None], "output.voltage.nominal": ["Nominal Output Voltage", VOLT, "mdi:flash", None], "output.frequency": ["Output Frequency", FREQUENCY_HERTZ, "mdi:flash", None], diff --git a/homeassistant/components/nut/translations/ca.json b/homeassistant/components/nut/translations/ca.json index d00f3d08185..13c1bde3173 100644 --- a/homeassistant/components/nut/translations/ca.json +++ b/homeassistant/components/nut/translations/ca.json @@ -39,7 +39,7 @@ "resources": "Recursos", "scan_interval": "Interval d'escaneig (segons)" }, - "description": "Selecciona els recursos del sensor" + "description": "Selecciona els recursos del sensor." } } } diff --git a/homeassistant/components/nut/translations/hu.json b/homeassistant/components/nut/translations/hu.json new file mode 100644 index 00000000000..1ca56c7684f --- /dev/null +++ b/homeassistant/components/nut/translations/hu.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hoszt", + "password": "Jelsz\u00f3", + "port": "Port", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nut/translations/pl.json b/homeassistant/components/nut/translations/pl.json index 282d1e15ab6..5fb9082d676 100644 --- a/homeassistant/components/nut/translations/pl.json +++ b/homeassistant/components/nut/translations/pl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "unknown": "[%key_id:common::config_flow::error::unknown%]" + "unknown": "Nieoczekiwany b\u0142\u0105d." }, "step": { "resources": { @@ -23,12 +23,12 @@ }, "user": { "data": { - "host": "[%key_id:common::config_flow::data::host%]", - "password": "[%key_id:common::config_flow::data::password%]", - "port": "[%key_id:common::config_flow::data::port%]", - "username": "[%key_id:common::config_flow::data::username%]" + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o", + "port": "Port", + "username": "Nazwa u\u017cytkownika" }, - "title": "Po\u0142\u0105cz z serwerem NUT" + "title": "Po\u0142\u0105czenie z serwerem NUT" } } }, diff --git a/homeassistant/components/nut/translations/pt-BR.json b/homeassistant/components/nut/translations/pt-BR.json new file mode 100644 index 00000000000..8b6b7538ba8 --- /dev/null +++ b/homeassistant/components/nut/translations/pt-BR.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar, tente novamente", + "unknown": "Erro inesperado" + }, + "step": { + "resources": { + "data": { + "resources": "Recursos" + }, + "title": "Escolha os recursos para monitorar" + }, + "ups": { + "data": { + "alias": "Alias", + "resources": "Recursos" + }, + "title": "Escolha o no-break (UPS) para monitorar" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Intervalo de escaneamento (segundos)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nws/__init__.py b/homeassistant/components/nws/__init__.py index 9f0579dc20e..a0958be8d9e 100644 --- a/homeassistant/components/nws/__init__.py +++ b/homeassistant/components/nws/__init__.py @@ -2,15 +2,18 @@ import asyncio import datetime import logging +from typing import Awaitable, Callable, Optional from pynws import SimpleNWS from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import debounce from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.util.dt import utcnow from .const import ( CONF_STATION, @@ -26,7 +29,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = ["weather"] DEFAULT_SCAN_INTERVAL = datetime.timedelta(minutes=10) - +FAILED_SCAN_INTERVAL = datetime.timedelta(minutes=1) DEBOUNCE_TIME = 60 # in seconds @@ -40,6 +43,59 @@ async def async_setup(hass: HomeAssistant, config: dict): return True +class NwsDataUpdateCoordinator(DataUpdateCoordinator): + """ + NWS data update coordinator. + + Implements faster data update intervals for failed updates and exposes a last successful update time. + """ + + def __init__( + self, + hass: HomeAssistant, + logger: logging.Logger, + *, + name: str, + update_interval: datetime.timedelta, + failed_update_interval: datetime.timedelta, + update_method: Optional[Callable[[], Awaitable]] = None, + request_refresh_debouncer: Optional[debounce.Debouncer] = None, + ): + """Initialize NWS coordinator.""" + super().__init__( + hass, + logger, + name=name, + update_interval=update_interval, + update_method=update_method, + request_refresh_debouncer=request_refresh_debouncer, + ) + self.failed_update_interval = failed_update_interval + self.last_update_success_time = None + + @callback + def _schedule_refresh(self) -> None: + """Schedule a refresh.""" + if self._unsub_refresh: + self._unsub_refresh() + self._unsub_refresh = None + + # We _floor_ utcnow to create a schedule on a rounded second, + # minimizing the time between the point and the real activation. + # That way we obtain a constant update frequency, + # as long as the update process takes less than a second + if self.last_update_success: + update_interval = self.update_interval + self.last_update_success_time = utcnow() + else: + update_interval = self.failed_update_interval + self._unsub_refresh = async_track_point_in_utc_time( + self.hass, + self._handle_refresh_interval, + utcnow().replace(microsecond=0) + update_interval, + ) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up a National Weather Service entry.""" latitude = entry.data[CONF_LATITUDE] @@ -53,34 +109,37 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): nws_data = SimpleNWS(latitude, longitude, api_key, client_session) await nws_data.set_station(station) - coordinator_observation = DataUpdateCoordinator( + coordinator_observation = NwsDataUpdateCoordinator( hass, _LOGGER, name=f"NWS observation station {station}", update_method=nws_data.update_observation, update_interval=DEFAULT_SCAN_INTERVAL, + failed_update_interval=FAILED_SCAN_INTERVAL, request_refresh_debouncer=debounce.Debouncer( hass, _LOGGER, cooldown=DEBOUNCE_TIME, immediate=True ), ) - coordinator_forecast = DataUpdateCoordinator( + coordinator_forecast = NwsDataUpdateCoordinator( hass, _LOGGER, name=f"NWS forecast station {station}", update_method=nws_data.update_forecast, update_interval=DEFAULT_SCAN_INTERVAL, + failed_update_interval=FAILED_SCAN_INTERVAL, request_refresh_debouncer=debounce.Debouncer( hass, _LOGGER, cooldown=DEBOUNCE_TIME, immediate=True ), ) - coordinator_forecast_hourly = DataUpdateCoordinator( + coordinator_forecast_hourly = NwsDataUpdateCoordinator( hass, _LOGGER, name=f"NWS forecast hourly station {station}", update_method=nws_data.update_forecast_hourly, update_interval=DEFAULT_SCAN_INTERVAL, + failed_update_interval=FAILED_SCAN_INTERVAL, request_refresh_debouncer=debounce.Debouncer( hass, _LOGGER, cooldown=DEBOUNCE_TIME, immediate=True ), diff --git a/homeassistant/components/nws/translations/ca.json b/homeassistant/components/nws/translations/ca.json index bbb316439e5..a2239f5f97d 100644 --- a/homeassistant/components/nws/translations/ca.json +++ b/homeassistant/components/nws/translations/ca.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "api_key": "Clau API (correu electr\u00f2nic)", + "api_key": "Clau API", "latitude": "Latitud", "longitude": "Longitud", "station": "Codi d'estaci\u00f3 METAR" diff --git a/homeassistant/components/nws/translations/hu.json b/homeassistant/components/nws/translations/hu.json new file mode 100644 index 00000000000..5f4f7bb8bee --- /dev/null +++ b/homeassistant/components/nws/translations/hu.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API kulcs" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nws/translations/it.json b/homeassistant/components/nws/translations/it.json index f4b110f857f..92c519513b4 100644 --- a/homeassistant/components/nws/translations/it.json +++ b/homeassistant/components/nws/translations/it.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "api_key": "Chiave API (email)", + "api_key": "Chiave API", "latitude": "Latitudine", "longitude": "Logitudine", "station": "Codice stazione METAR" diff --git a/homeassistant/components/nws/translations/pl.json b/homeassistant/components/nws/translations/pl.json index ad94c694837..ab1011d9d56 100644 --- a/homeassistant/components/nws/translations/pl.json +++ b/homeassistant/components/nws/translations/pl.json @@ -1,22 +1,22 @@ { "config": { "abort": { - "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "unknown": "[%key_id:common::config_flow::error::unknown%]" + "unknown": "Nieoczekiwany b\u0142\u0105d." }, "step": { "user": { "data": { - "api_key": "[%key_id:common::config_flow::data::api_key%] (e-mail)", + "api_key": "Klucz API", "latitude": "Szeroko\u015b\u0107 geograficzna", "longitude": "D\u0142ugo\u015b\u0107 geograficzna", "station": "Kod stacji METAR" }, "description": "Je\u015bli nie podasz kodu stacji METAR, do znalezienia najbli\u017cszej stacji zostan\u0105 u\u017cyte wsp\u00f3\u0142rz\u0119dne geograficzne.", - "title": "Po\u0142\u0105cz z National Weather Service" + "title": "Po\u0142\u0105czenie z National Weather Service" } } } diff --git a/homeassistant/components/nws/translations/pt-BR.json b/homeassistant/components/nws/translations/pt-BR.json new file mode 100644 index 00000000000..3d168bcce30 --- /dev/null +++ b/homeassistant/components/nws/translations/pt-BR.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar, tente novamente", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude", + "station": "C\u00f3digo da esta\u00e7\u00e3o METAR" + }, + "description": "Se um c\u00f3digo de esta\u00e7\u00e3o METAR n\u00e3o for especificado, a latitude e a longitude ser\u00e3o usadas para encontrar a esta\u00e7\u00e3o mais pr\u00f3xima.", + "title": "Conecte-se ao Servi\u00e7o Nacional de Meteorologia" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index 807591e0c2b..7e1ca37ab6b 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -1,4 +1,5 @@ """Support for NWS weather service.""" +from datetime import timedelta import logging from homeassistant.components.weather import ( @@ -24,6 +25,7 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util.distance import convert as convert_distance +from homeassistant.util.dt import utcnow from homeassistant.util.pressure import convert as convert_pressure from homeassistant.util.temperature import convert as convert_temperature @@ -47,6 +49,9 @@ _LOGGER = logging.getLogger(__name__) PARALLEL_UPDATES = 0 +OBSERVATION_VALID_TIME = timedelta(minutes=20) +FORECAST_VALID_TIME = timedelta(minutes=45) + def convert_condition(time, weather): """ @@ -287,10 +292,23 @@ class NWSWeather(WeatherEntity): @property def available(self): """Return if state is available.""" - return ( + last_success = ( self.coordinator_observation.last_update_success and self.coordinator_forecast.last_update_success ) + if ( + self.coordinator_observation.last_update_success_time + and self.coordinator_forecast.last_update_success_time + ): + last_success_time = ( + utcnow() - self.coordinator_observation.last_update_success_time + < OBSERVATION_VALID_TIME + and utcnow() - self.coordinator_forecast.last_update_success_time + < FORECAST_VALID_TIME + ) + else: + last_success_time = False + return last_success or last_success_time async def async_update(self): """Update the entity. diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index 12a4546dbeb..f12d3ab2346 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -11,6 +11,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_HOST, CONF_PORT, + ELECTRICAL_CURRENT_AMPERE, TEMP_CELSIUS, UNIT_PERCENTAGE, VOLT, @@ -84,7 +85,7 @@ SENSOR_TYPES = { "voltage": ["voltage", VOLT], "voltage_VAD": ["voltage", VOLT], "voltage_VDD": ["voltage", VOLT], - "current": ["current", "A"], + "current": ["current", ELECTRICAL_CURRENT_AMPERE], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/onvif/config_flow.py b/homeassistant/components/onvif/config_flow.py index 29784b25429..fada2d497a7 100644 --- a/homeassistant/components/onvif/config_flow.py +++ b/homeassistant/components/onvif/config_flow.py @@ -205,10 +205,20 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # Get the MAC address to use as the unique ID for the config flow if not self.device_id: - network_interfaces = await device_mgmt.GetNetworkInterfaces() - for interface in network_interfaces: - if interface.Enabled: - self.device_id = interface.Info.HwAddress + try: + network_interfaces = await device_mgmt.GetNetworkInterfaces() + for interface in network_interfaces: + if interface.Enabled: + self.device_id = interface.Info.HwAddress + except Fault as fault: + if "not implemented" not in fault.message: + raise fault + + LOGGER.debug( + "Couldn't get network interfaces from ONVIF deivice '%s'. Error: %s", + self.onvif_config[CONF_NAME], + fault, + ) # If no network interfaces are exposed, fallback to serial number if not self.device_id: diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index bbce71f5d28..39f37eb02e4 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -100,16 +100,6 @@ class ONVIFDevice: if self.capabilities.ptz: self.device.create_ptz_service() - if self._dt_diff_seconds > 300 and self.capabilities.events: - self.capabilities.events = False - LOGGER.warning( - "The system clock on '%s' is more than 5 minutes off. " - "Although this device supports events, they will be " - "disabled until the device clock is fixed as we will " - "not be able to renew the subscription.", - self.name, - ) - if self.capabilities.events: self.events = EventManager( self.hass, self.device, self.config_entry.unique_id @@ -217,10 +207,20 @@ class ONVIFDevice: # Grab the last MAC address for backwards compatibility mac = None - network_interfaces = await device_mgmt.GetNetworkInterfaces() - for interface in network_interfaces: - if interface.Enabled: - mac = interface.Info.HwAddress + try: + network_interfaces = await device_mgmt.GetNetworkInterfaces() + for interface in network_interfaces: + if interface.Enabled: + mac = interface.Info.HwAddress + except Fault as fault: + if "not implemented" not in fault.message: + raise fault + + LOGGER.debug( + "Couldn't get network interfaces from ONVIF deivice '%s'. Error: %s", + self.name, + fault, + ) return DeviceInfo( device_info.Manufacturer, diff --git a/homeassistant/components/onvif/event.py b/homeassistant/components/onvif/event.py index 3888db4fa8e..9084a06e7db 100644 --- a/homeassistant/components/onvif/event.py +++ b/homeassistant/components/onvif/event.py @@ -107,7 +107,9 @@ class EventManager: if not self._subscription: return - termination_time = (dt_util.utcnow() + dt.timedelta(minutes=30)).isoformat() + termination_time = ( + (dt_util.utcnow() + dt.timedelta(days=1)).replace(microsecond=0).isoformat() + ) await self._subscription.Renew(termination_time) async def async_pull_messages(self, _now: dt = None) -> None: @@ -119,8 +121,10 @@ class EventManager: req.Timeout = dt.timedelta(seconds=60) response = await pullpoint.PullMessages(req) - # Renew subscription if less than 60 seconds left - if (response.TerminationTime - dt_util.utcnow()).total_seconds() < 60: + # Renew subscription if less than two hours is left + if ( + dt_util.as_utc(response.TerminationTime) - dt_util.utcnow() + ).total_seconds() < 7200: await self.async_renew() # Parse response diff --git a/homeassistant/components/onvif/translations/de.json b/homeassistant/components/onvif/translations/de.json index 2a8dcb1b67b..46aeb515716 100644 --- a/homeassistant/components/onvif/translations/de.json +++ b/homeassistant/components/onvif/translations/de.json @@ -34,6 +34,7 @@ "manual_input": { "data": { "host": "Host", + "name": "Name", "port": "Port" }, "title": "Konfigurieren Sie das ONVIF-Ger\u00e4t" diff --git a/homeassistant/components/onvif/translations/hu.json b/homeassistant/components/onvif/translations/hu.json new file mode 100644 index 00000000000..dfa0aec8765 --- /dev/null +++ b/homeassistant/components/onvif/translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "auth": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + }, + "manual_input": { + "data": { + "host": "Hoszt", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/it.json b/homeassistant/components/onvif/translations/it.json index 689babb357c..374c65b99a1 100644 --- a/homeassistant/components/onvif/translations/it.json +++ b/homeassistant/components/onvif/translations/it.json @@ -34,6 +34,7 @@ "manual_input": { "data": { "host": "Host", + "name": "Nome", "port": "Porta" }, "title": "Configurare il dispositivo ONVIF" diff --git a/homeassistant/components/onvif/translations/ko.json b/homeassistant/components/onvif/translations/ko.json index 9715d89f72b..5619a6daa2b 100644 --- a/homeassistant/components/onvif/translations/ko.json +++ b/homeassistant/components/onvif/translations/ko.json @@ -40,7 +40,7 @@ "title": "ONVIF \uae30\uae30 \uad6c\uc131\ud558\uae30" }, "user": { - "description": "submit \uc744 \ud074\ub9ad\ud558\uba74 \ud504\ub85c\ud544 S \ub97c \uc9c0\uc6d0\ud558\ub294 ONVIF \uae30\uae30\ub97c \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uac80\uc0c9\ud569\ub2c8\ub2e4. \n\n\uc77c\ubd80 \uc81c\uc870\uc5c5\uccb4\ub294 \uae30\ubcf8\uac12\uc73c\ub85c ONVIF \ub97c \ube44\ud65c\uc131\ud654 \ud574 \ub193\uc740 \uacbd\uc6b0\uac00 \uc788\uc2b5\ub2c8\ub2e4. \uce74\uba54\ub77c \uad6c\uc131\uc5d0\uc11c ONVIF \uac00 \ud65c\uc131\ud654\ub418\uc5b4 \uc788\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", + "description": "\ud655\uc778\uc744 \ud074\ub9ad\ud558\uba74 \ud504\ub85c\ud544 S \ub97c \uc9c0\uc6d0\ud558\ub294 ONVIF \uae30\uae30\ub97c \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uac80\uc0c9\ud569\ub2c8\ub2e4. \n\n\uc77c\ubd80 \uc81c\uc870\uc5c5\uccb4\ub294 \uae30\ubcf8\uac12\uc73c\ub85c ONVIF \ub97c \ube44\ud65c\uc131\ud654 \ud574 \ub193\uc740 \uacbd\uc6b0\uac00 \uc788\uc2b5\ub2c8\ub2e4. \uce74\uba54\ub77c \uad6c\uc131\uc5d0\uc11c ONVIF \uac00 \ud65c\uc131\ud654\ub418\uc5b4 \uc788\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", "title": "ONVIF \uae30\uae30 \uc124\uc815\ud558\uae30" } } diff --git a/homeassistant/components/onvif/translations/lb.json b/homeassistant/components/onvif/translations/lb.json index fb91a80dd1c..024a673c442 100644 --- a/homeassistant/components/onvif/translations/lb.json +++ b/homeassistant/components/onvif/translations/lb.json @@ -34,6 +34,7 @@ "manual_input": { "data": { "host": "Apparat", + "name": "Numm", "port": "Port" }, "title": "ONVIF Apparat ariichten" diff --git a/homeassistant/components/onvif/translations/nl.json b/homeassistant/components/onvif/translations/nl.json index 90a8c4d4993..71ba0d115be 100644 --- a/homeassistant/components/onvif/translations/nl.json +++ b/homeassistant/components/onvif/translations/nl.json @@ -17,6 +17,7 @@ "manual_input": { "data": { "host": "Host", + "name": "Naam", "port": "Poort" } } diff --git a/homeassistant/components/onvif/translations/pl.json b/homeassistant/components/onvif/translations/pl.json index afd4df73b66..d60d45c746f 100644 --- a/homeassistant/components/onvif/translations/pl.json +++ b/homeassistant/components/onvif/translations/pl.json @@ -34,12 +34,13 @@ "manual_input": { "data": { "host": "Nazwa hosta lub adres IP", + "name": "Nazwa", "port": "Port" }, "title": "Konfigurowanie urz\u0105dzenia ONVIF" }, "user": { - "description": "Klikaj\u0105c przycisk Prze\u015blij, Twoja sie\u0107 zostanie przeszukana pod k\u0105tem urz\u0105dze\u0144 ONVIF obs\u0142uguj\u0105cych profil S.\n\nNiekt\u00f3rzy producenci zacz\u0119li domy\u015blnie wy\u0142\u0105cza\u0107 ONVIF. Upewnij si\u0119, \u017ce ONVIF jest w\u0142\u0105czony w konfiguracji kamery.", + "description": "Klikaj\u0105c przycisk Zatwierd\u017a, Twoja sie\u0107 zostanie przeszukana pod k\u0105tem urz\u0105dze\u0144 ONVIF obs\u0142uguj\u0105cych profil S.\n\nNiekt\u00f3rzy producenci zacz\u0119li domy\u015blnie wy\u0142\u0105cza\u0107 ONVIF. Upewnij si\u0119, \u017ce ONVIF jest w\u0142\u0105czony w konfiguracji kamery.", "title": "Konfiguracja urz\u0105dzenia ONVIF" } } diff --git a/homeassistant/components/onvif/translations/pt-BR.json b/homeassistant/components/onvif/translations/pt-BR.json index 3eb03c86f52..7d8689cfeae 100644 --- a/homeassistant/components/onvif/translations/pt-BR.json +++ b/homeassistant/components/onvif/translations/pt-BR.json @@ -34,6 +34,7 @@ "manual_input": { "data": { "host": "Endere\u00e7o (IP)", + "name": "Nome", "port": "Porta" }, "title": "Configurar dispositivo ONVIF" diff --git a/homeassistant/components/onvif/translations/zh-Hant.json b/homeassistant/components/onvif/translations/zh-Hant.json index c7a0f88d1b9..c96eb2138f1 100644 --- a/homeassistant/components/onvif/translations/zh-Hant.json +++ b/homeassistant/components/onvif/translations/zh-Hant.json @@ -34,6 +34,7 @@ "manual_input": { "data": { "host": "\u4e3b\u6a5f\u7aef", + "name": "\u540d\u7a31", "port": "\u901a\u8a0a\u57e0" }, "title": "\u8a2d\u5b9a ONVIF \u8a2d\u5099" diff --git a/homeassistant/components/opencv/image_processing.py b/homeassistant/components/opencv/image_processing.py index f35d063de50..028d6eacf24 100644 --- a/homeassistant/components/opencv/image_processing.py +++ b/homeassistant/components/opencv/image_processing.py @@ -62,7 +62,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( CONF_NEIGHBORS, DEFAULT_NEIGHBORS ): cv.positive_int, vol.Optional(CONF_MIN_SIZE, DEFAULT_MIN_SIZE): vol.Schema( - (int, int) + vol.All(vol.ExactSequence([int, int]), vol.Coerce(tuple)) ), } ), @@ -160,6 +160,9 @@ class OpenCVImageProcessor(ImageProcessingEntity): """Process the image.""" cv_image = cv2.imdecode(numpy.asarray(bytearray(image)), cv2.IMREAD_UNCHANGED) + matches = {} + total_matches = 0 + for name, classifier in self._classifiers.items(): scale = DEFAULT_SCALE neighbors = DEFAULT_NEIGHBORS @@ -177,8 +180,6 @@ class OpenCVImageProcessor(ImageProcessingEntity): detections = cascade.detectMultiScale( cv_image, scaleFactor=scale, minNeighbors=neighbors, minSize=min_size ) - matches = {} - total_matches = 0 regions = [] # pylint: disable=invalid-name for (x, y, w, h) in detections: diff --git a/homeassistant/components/opengarage/cover.py b/homeassistant/components/opengarage/cover.py index 70b4d8c98ee..cf6825c867b 100644 --- a/homeassistant/components/opengarage/cover.py +++ b/homeassistant/components/opengarage/cover.py @@ -25,6 +25,7 @@ from homeassistant.const import ( ) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import format_mac _LOGGER = logging.getLogger(__name__) @@ -72,8 +73,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= device_config[CONF_VERIFY_SSL], async_get_clientsession(hass), ) - - covers.append(OpenGarageCover(device_config.get(CONF_NAME), open_garage)) + status = await open_garage.update_state() + covers.append( + OpenGarageCover( + device_config.get(CONF_NAME), open_garage, format_mac(status["mac"]) + ) + ) async_add_entities(covers, True) @@ -81,7 +86,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class OpenGarageCover(CoverEntity): """Representation of a OpenGarage cover.""" - def __init__(self, name, open_garage): + def __init__(self, name, open_garage, device_id): """Initialize the cover.""" self._name = name self._open_garage = open_garage @@ -89,6 +94,7 @@ class OpenGarageCover(CoverEntity): self._state_before_move = None self._device_state_attributes = {} self._available = True + self._device_id = device_id @property def name(self): @@ -181,3 +187,8 @@ class OpenGarageCover(CoverEntity): def supported_features(self): """Flag supported features.""" return SUPPORT_OPEN | SUPPORT_CLOSE + + @property + def unique_id(self): + """Return a unique ID.""" + return self._device_id diff --git a/homeassistant/components/openhome/const.py b/homeassistant/components/openhome/const.py new file mode 100644 index 00000000000..09fcd2ef0e2 --- /dev/null +++ b/homeassistant/components/openhome/const.py @@ -0,0 +1,5 @@ +"""Constants for the Openhome component.""" +DOMAIN = "openhome" +SERVICE_INVOKE_PIN = "invoke_pin" +ATTR_PIN_INDEX = "pin" +DATA_OPENHOME = "openhome" diff --git a/homeassistant/components/openhome/manifest.json b/homeassistant/components/openhome/manifest.json index 8105c01dfc5..3a94215a7b1 100644 --- a/homeassistant/components/openhome/manifest.json +++ b/homeassistant/components/openhome/manifest.json @@ -2,6 +2,6 @@ "domain": "openhome", "name": "Linn / OpenHome", "documentation": "https://www.home-assistant.io/integrations/openhome", - "requirements": ["openhomedevice==0.6.3"], + "requirements": ["openhomedevice==0.7.2"], "codeowners": [] } diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py index 4225228e271..b4258a88347 100644 --- a/homeassistant/components/openhome/media_player.py +++ b/homeassistant/components/openhome/media_player.py @@ -2,6 +2,7 @@ import logging from openhomedevice.Device import Device +import voluptuous as vol from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( @@ -20,35 +21,45 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_STEP, ) from homeassistant.const import STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING +from homeassistant.helpers import config_validation as cv, entity_platform + +from .const import ATTR_PIN_INDEX, DATA_OPENHOME, SERVICE_INVOKE_PIN SUPPORT_OPENHOME = SUPPORT_SELECT_SOURCE | SUPPORT_TURN_OFF | SUPPORT_TURN_ON _LOGGER = logging.getLogger(__name__) -DEVICES = [] - -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 Openhome platform.""" if not discovery_info: - return True + return + + openhome_data = hass.data.setdefault(DATA_OPENHOME, set()) name = discovery_info.get("name") description = discovery_info.get("ssdp_description") + _LOGGER.info("Openhome device found: %s", name) - device = Device(description) + device = await hass.async_add_executor_job(Device, description) # if device has already been discovered - if device.Uuid() in [x.unique_id for x in DEVICES]: + if device.Uuid() in openhome_data: return True - device = OpenhomeDevice(hass, device) + entity = OpenhomeDevice(hass, device) - add_entities([device], True) - DEVICES.append(device) + async_add_entities([entity]) + openhome_data.add(device.Uuid()) - return True + platform = entity_platform.current_platform.get() + + platform.async_register_entity_service( + SERVICE_INVOKE_PIN, + {vol.Required(ATTR_PIN_INDEX): cv.positive_int}, + "invoke_pin", + ) class OpenhomeDevice(MediaPlayerEntity): @@ -162,6 +173,10 @@ class OpenhomeDevice(MediaPlayerEntity): """Select input source.""" self._device.SetSource(self._source_index[source]) + def invoke_pin(self, pin): + """Invoke pin.""" + self._device.InvokePin(pin) + @property def name(self): """Return the name of the device.""" diff --git a/homeassistant/components/openhome/services.yaml b/homeassistant/components/openhome/services.yaml new file mode 100644 index 00000000000..e8ae5fb55da --- /dev/null +++ b/homeassistant/components/openhome/services.yaml @@ -0,0 +1,11 @@ +# Describes the format for available openhome services + +invoke_pin: + description: Invoke a pin on the specified device. + fields: + entity_id: + description: The name of the openhome device to invoke the pin on + example: media_player.main_room + pin: + description: Which pin to invoke + example: 4 diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index e6a302975a5..1403778aae2 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -4,9 +4,7 @@ import logging from pyopenuv import Client from pyopenuv.errors import OpenUvError -import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_API_KEY, @@ -26,7 +24,6 @@ from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.entity import Entity from homeassistant.helpers.service import verify_domain_control -from .config_flow import configured_instances from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -55,54 +52,14 @@ TYPE_SAFE_EXPOSURE_TIME_4 = "safe_exposure_time_type_4" TYPE_SAFE_EXPOSURE_TIME_5 = "safe_exposure_time_type_5" TYPE_SAFE_EXPOSURE_TIME_6 = "safe_exposure_time_type_6" +PLATFORMS = ["binary_sensor", "sensor"] -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_API_KEY): cv.string, - vol.Optional(CONF_ELEVATION): float, - vol.Optional(CONF_LATITUDE): cv.latitude, - vol.Optional(CONF_LONGITUDE): cv.longitude, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) +CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.115") async def async_setup(hass, config): """Set up the OpenUV component.""" - hass.data[DOMAIN] = {} - hass.data[DOMAIN][DATA_OPENUV_CLIENT] = {} - hass.data[DOMAIN][DATA_OPENUV_LISTENER] = {} - - if DOMAIN not in config: - return True - - conf = config[DOMAIN] - - identifier = ( - f"{conf.get(CONF_LATITUDE, hass.config.latitude)}, " - f"{conf.get(CONF_LONGITUDE, hass.config.longitude)}" - ) - if identifier in configured_instances(hass): - return True - - data = {CONF_API_KEY: conf[CONF_API_KEY]} - if CONF_LATITUDE in conf: - data[CONF_LATITUDE] = conf[CONF_LATITUDE] - if CONF_LONGITUDE in conf: - data[CONF_LONGITUDE] = conf[CONF_LONGITUDE] - if CONF_ELEVATION in conf: - data[CONF_ELEVATION] = conf[CONF_ELEVATION] - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=data - ) - ) - + hass.data[DOMAIN] = {DATA_OPENUV_CLIENT: {}, DATA_OPENUV_LISTENER: {}} return True @@ -127,7 +84,7 @@ async def async_setup_entry(hass, config_entry): _LOGGER.error("Config entry failed: %s", err) raise ConfigEntryNotReady - for component in ("binary_sensor", "sensor"): + for component in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(config_entry, component) ) @@ -139,8 +96,6 @@ async def async_setup_entry(hass, config_entry): await openuv.async_update() async_dispatcher_send(hass, TOPIC_UPDATE) - hass.services.async_register(DOMAIN, "update_data", update_data) - @_verify_domain_control async def update_uv_index_data(service): """Refresh OpenUV UV index data.""" @@ -148,8 +103,6 @@ async def async_setup_entry(hass, config_entry): await openuv.async_update_uv_index_data() async_dispatcher_send(hass, TOPIC_UPDATE) - hass.services.async_register(DOMAIN, "update_uv_index_data", update_uv_index_data) - @_verify_domain_control async def update_protection_data(service): """Refresh OpenUV protection window data.""" @@ -157,25 +110,30 @@ async def async_setup_entry(hass, config_entry): await openuv.async_update_protection_data() async_dispatcher_send(hass, TOPIC_UPDATE) - hass.services.async_register( - DOMAIN, "update_protection_data", update_protection_data - ) + for service, method in [ + ("update_data", update_data), + ("update_uv_index_data", update_uv_index_data), + ("update_protection_data", update_protection_data), + ]: + hass.services.async_register(DOMAIN, service, method) return True async def async_unload_entry(hass, config_entry): """Unload an OpenUV config entry.""" - hass.data[DOMAIN][DATA_OPENUV_CLIENT].pop(config_entry.entry_id) + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(config_entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN][DATA_OPENUV_CLIENT].pop(config_entry.entry_id) - tasks = [ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in ("binary_sensor", "sensor") - ] - - await asyncio.gather(*tasks) - - return True + return unload_ok async def async_migrate_entry(hass, config_entry): diff --git a/homeassistant/components/openuv/config_flow.py b/homeassistant/components/openuv/config_flow.py index 821c9624c58..bf359ef1b30 100644 --- a/homeassistant/components/openuv/config_flow.py +++ b/homeassistant/components/openuv/config_flow.py @@ -10,24 +10,21 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, ) -from homeassistant.core import callback from homeassistant.helpers import aiohttp_client, config_validation as cv -from .const import DOMAIN +from .const import DOMAIN # pylint: disable=unused-import - -@callback -def configured_instances(hass): - """Return a set of configured OpenUV instances.""" - return { - f"{entry.data.get(CONF_LATITUDE, hass.config.latitude)}, " - f"{entry.data.get(CONF_LONGITUDE, hass.config.longitude)}" - for entry in hass.config_entries.async_entries(DOMAIN) +CONFIG_SCHEMA = vol.Schema( + { + vol.Required(CONF_API_KEY): str, + vol.Inclusive(CONF_LATITUDE, "coords"): cv.latitude, + vol.Inclusive(CONF_LONGITUDE, "coords"): cv.longitude, + vol.Optional(CONF_ELEVATION): vol.Coerce(float), } +) -@config_entries.HANDLERS.register(DOMAIN) -class OpenUvFlowHandler(config_entries.ConfigFlow): +class OpenUvFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle an OpenUV config flow.""" VERSION = 2 @@ -35,17 +32,8 @@ class OpenUvFlowHandler(config_entries.ConfigFlow): async def _show_form(self, errors=None): """Show the form to the user.""" - data_schema = vol.Schema( - { - vol.Required(CONF_API_KEY): str, - vol.Optional(CONF_LATITUDE): cv.latitude, - vol.Optional(CONF_LONGITUDE): cv.longitude, - vol.Optional(CONF_ELEVATION): vol.Coerce(float), - } - ) - return self.async_show_form( - step_id="user", data_schema=data_schema, errors=errors if errors else {} + step_id="user", data_schema=CONFIG_SCHEMA, errors=errors if errors else {}, ) async def async_step_import(self, import_config): @@ -54,16 +42,16 @@ class OpenUvFlowHandler(config_entries.ConfigFlow): async def async_step_user(self, user_input=None): """Handle the start of the config flow.""" - if not user_input: return await self._show_form() - identifier = ( - f"{user_input.get(CONF_LATITUDE, self.hass.config.latitude)}, " - f"{user_input.get(CONF_LONGITUDE, self.hass.config.longitude)}" - ) - if identifier in configured_instances(self.hass): - return await self._show_form({CONF_LATITUDE: "identifier_exists"}) + if user_input.get(CONF_LATITUDE): + identifier = f"{user_input[CONF_LATITUDE]}, {user_input[CONF_LONGITUDE]}" + else: + identifier = "Default Coordinates" + + await self.async_set_unique_id(identifier) + self._abort_if_unique_id_configured() websession = aiohttp_client.async_get_clientsession(self.hass) client = Client(user_input[CONF_API_KEY], 0, 0, websession) diff --git a/homeassistant/components/openuv/strings.json b/homeassistant/components/openuv/strings.json index 0c4913cf6c1..0777b139cf9 100644 --- a/homeassistant/components/openuv/strings.json +++ b/homeassistant/components/openuv/strings.json @@ -13,7 +13,10 @@ }, "error": { "identifier_exists": "Coordinates already registered", - "invalid_api_key": "Invalid API key" + "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]" + }, + "abort": { + "already_configured": "These coordinates are already registered." } } -} \ No newline at end of file +} diff --git a/homeassistant/components/openuv/translations/ca.json b/homeassistant/components/openuv/translations/ca.json index e09ec648ee3..2bc06623cc2 100644 --- a/homeassistant/components/openuv/translations/ca.json +++ b/homeassistant/components/openuv/translations/ca.json @@ -1,13 +1,16 @@ { "config": { + "abort": { + "already_configured": "Aquestes coordenades ja estan registrades." + }, "error": { "identifier_exists": "Les coordenades ja estan registrades", - "invalid_api_key": "Clau API no v\u00e0lida" + "invalid_api_key": "Clau API inv\u00e0lida" }, "step": { "user": { "data": { - "api_key": "Clau API d'OpenUV", + "api_key": "Clau API", "elevation": "Elevaci\u00f3", "latitude": "Latitud", "longitude": "Longitud" diff --git a/homeassistant/components/openuv/translations/en.json b/homeassistant/components/openuv/translations/en.json index 4c59a587fcd..77bdae11e05 100644 --- a/homeassistant/components/openuv/translations/en.json +++ b/homeassistant/components/openuv/translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "These coordinates are already registered." + }, "error": { "identifier_exists": "Coordinates already registered", "invalid_api_key": "Invalid API key" diff --git a/homeassistant/components/openuv/translations/es.json b/homeassistant/components/openuv/translations/es.json index 3c87781d2c5..4eb27857310 100644 --- a/homeassistant/components/openuv/translations/es.json +++ b/homeassistant/components/openuv/translations/es.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Estas coordenadas ya est\u00e1n registradas." + }, "error": { "identifier_exists": "Coordenadas ya registradas", "invalid_api_key": "Clave API inv\u00e1lida" diff --git a/homeassistant/components/openuv/translations/fi.json b/homeassistant/components/openuv/translations/fi.json index 65b313a059b..4b023bff905 100644 --- a/homeassistant/components/openuv/translations/fi.json +++ b/homeassistant/components/openuv/translations/fi.json @@ -11,7 +11,8 @@ "elevation": "Korkeus merenpinnasta", "latitude": "Leveysaste", "longitude": "Pituusaste" - } + }, + "title": "T\u00e4yt\u00e4 tietosi" } } } diff --git a/homeassistant/components/openuv/translations/hu.json b/homeassistant/components/openuv/translations/hu.json index 6d7e4e99e26..8e06c5d4ad0 100644 --- a/homeassistant/components/openuv/translations/hu.json +++ b/homeassistant/components/openuv/translations/hu.json @@ -7,7 +7,7 @@ "step": { "user": { "data": { - "api_key": "OpenUV API kulcs", + "api_key": "API kulcs", "elevation": "Magass\u00e1g", "latitude": "Sz\u00e9less\u00e9g", "longitude": "Hossz\u00fas\u00e1g" diff --git a/homeassistant/components/openuv/translations/it.json b/homeassistant/components/openuv/translations/it.json index 89d80216fd5..e6113500fbe 100644 --- a/homeassistant/components/openuv/translations/it.json +++ b/homeassistant/components/openuv/translations/it.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Queste coordinate sono gi\u00e0 registrate." + }, "error": { "identifier_exists": "Coordinate gi\u00e0 registrate", "invalid_api_key": "Chiave API non valida" @@ -7,7 +10,7 @@ "step": { "user": { "data": { - "api_key": "API Key di OpenUV", + "api_key": "Chiave API", "elevation": "Altitudine", "latitude": "Latitudine", "longitude": "Longitudine" diff --git a/homeassistant/components/openuv/translations/ko.json b/homeassistant/components/openuv/translations/ko.json index 46c0e2f0526..9d252f9a945 100644 --- a/homeassistant/components/openuv/translations/ko.json +++ b/homeassistant/components/openuv/translations/ko.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\uc88c\ud45c\uac12\uc774 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, "error": { "identifier_exists": "\uc88c\ud45c\uac12\uc774 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "invalid_api_key": "\uc798\ubabb\ub41c API \ud0a4" diff --git a/homeassistant/components/openuv/translations/lb.json b/homeassistant/components/openuv/translations/lb.json index ebb09f2ded6..7ca44391875 100644 --- a/homeassistant/components/openuv/translations/lb.json +++ b/homeassistant/components/openuv/translations/lb.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "D\u00ebs Koordinate si scho registr\u00e9iert" + }, "error": { "identifier_exists": "Koordinate si scho\u00a0registr\u00e9iert", "invalid_api_key": "Ong\u00ebltegen API Schl\u00ebssel" diff --git a/homeassistant/components/openuv/translations/nl.json b/homeassistant/components/openuv/translations/nl.json index c4f7834ff89..e3badcb796a 100644 --- a/homeassistant/components/openuv/translations/nl.json +++ b/homeassistant/components/openuv/translations/nl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Deze co\u00f6rdinaten zijn al geregistreerd." + }, "error": { "identifier_exists": "Co\u00f6rdinaten al geregistreerd", "invalid_api_key": "Ongeldige API-sleutel" diff --git a/homeassistant/components/openuv/translations/no.json b/homeassistant/components/openuv/translations/no.json index 4152b732a7b..6ef1d389794 100644 --- a/homeassistant/components/openuv/translations/no.json +++ b/homeassistant/components/openuv/translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Disse koordinatene er allerede registrert." + }, "error": { "identifier_exists": "Koordinatene er allerede registrert", "invalid_api_key": "Ugyldig API-n\u00f8kkel" diff --git a/homeassistant/components/openuv/translations/pl.json b/homeassistant/components/openuv/translations/pl.json index 871f701b399..28a43bd10d9 100644 --- a/homeassistant/components/openuv/translations/pl.json +++ b/homeassistant/components/openuv/translations/pl.json @@ -7,7 +7,7 @@ "step": { "user": { "data": { - "api_key": "[%key_id:common::config_flow::data::api_key%] OpenUV", + "api_key": "Klucz API", "elevation": "Wysoko\u015b\u0107", "latitude": "Szeroko\u015b\u0107 geograficzna", "longitude": "D\u0142ugo\u015b\u0107 geograficzna" diff --git a/homeassistant/components/openuv/translations/ru.json b/homeassistant/components/openuv/translations/ru.json index 5b4ce86e963..0e748093241 100644 --- a/homeassistant/components/openuv/translations/ru.json +++ b/homeassistant/components/openuv/translations/ru.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u041a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u044b \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u044b." + }, "error": { "identifier_exists": "\u041a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u044b \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u044b.", "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API." diff --git a/homeassistant/components/openuv/translations/zh-Hant.json b/homeassistant/components/openuv/translations/zh-Hant.json index d9d4abc0161..e589915ee89 100644 --- a/homeassistant/components/openuv/translations/zh-Hant.json +++ b/homeassistant/components/openuv/translations/zh-Hant.json @@ -1,8 +1,11 @@ { "config": { + "abort": { + "already_configured": "\u6b64\u4e9b\u5ea7\u6a19\u5df2\u8a3b\u518a\u3002" + }, "error": { "identifier_exists": "\u8a72\u5ea7\u6a19\u5df2\u8a3b\u518a", - "invalid_api_key": "API \u5bc6\u78bc\u7121\u6548" + "invalid_api_key": "API \u5bc6\u9470\u7121\u6548" }, "step": { "user": { diff --git a/homeassistant/components/owntracks/strings.json b/homeassistant/components/owntracks/strings.json index b82761461ec..12aba21be72 100644 --- a/homeassistant/components/owntracks/strings.json +++ b/homeassistant/components/owntracks/strings.json @@ -8,7 +8,7 @@ }, "abort": { "one_instance_allowed": "Only a single instance is necessary." }, "create_entry": { - "default": "\n\nOn Android, open [the OwnTracks app]({android_url}), go to preferences -> connection. Change the following settings:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\nOn iOS, open [the OwnTracks app]({ios_url}), tap (i) icon in top left -> settings. Change the following settings:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret}\n\nSee [the documentation]({docs_url}) for more information." + "default": "\n\nOn Android, open [the OwnTracks app]({android_url}), go to preferences -> connection. Change the following settings:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: `''`\n - Device ID: `''`\n\nOn iOS, open [the OwnTracks app]({ios_url}), tap (i) icon in top left -> settings. Change the following settings:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: `''`\n\n{secret}\n\nSee [the documentation]({docs_url}) for more information." } } } diff --git a/homeassistant/components/ozw/climate.py b/homeassistant/components/ozw/climate.py new file mode 100644 index 00000000000..92887069a84 --- /dev/null +++ b/homeassistant/components/ozw/climate.py @@ -0,0 +1,339 @@ +"""Support for Z-Wave climate devices.""" +from enum import IntEnum +import logging +from typing import Optional, Tuple + +from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, ClimateEntity +from homeassistant.components.climate.const import ( + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + CURRENT_HVAC_COOL, + CURRENT_HVAC_FAN, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, + HVAC_MODE_AUTO, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + PRESET_NONE, + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, +) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DATA_UNSUBSCRIBE, DOMAIN +from .entity import ZWaveDeviceEntity + +VALUE_LIST = "List" +VALUE_ID = "Value" +VALUE_LABEL = "Label" +VALUE_SELECTED_ID = "Selected_id" +VALUE_SELECTED_LABEL = "Selected" + +ATTR_FAN_ACTION = "fan_action" +ATTR_VALVE_POSITION = "valve_position" +_LOGGER = logging.getLogger(__name__) + + +class ThermostatMode(IntEnum): + """Enum with all (known/used) Z-Wave ThermostatModes.""" + + # https://github.com/OpenZWave/open-zwave/blob/master/cpp/src/command_classes/ThermostatMode.cpp + OFF = 0 + HEAT = 1 + COOL = 2 + AUTO = 3 + AUXILIARY = 4 + RESUME_ON = 5 + FAN = 6 + FURNANCE = 7 + DRY = 8 + MOIST = 9 + AUTO_CHANGE_OVER = 10 + HEATING_ECON = 11 + COOLING_ECON = 12 + AWAY = 13 + FULL_POWER = 15 + MANUFACTURER_SPECIFIC = 31 + + +MODE_SETPOINT_MAPPINGS = { + ThermostatMode.OFF: (), + ThermostatMode.HEAT: ("setpoint_heating",), + ThermostatMode.COOL: ("setpoint_cooling",), + ThermostatMode.AUTO: ("setpoint_heating", "setpoint_cooling"), + ThermostatMode.AUXILIARY: ("setpoint_heating",), + ThermostatMode.FURNANCE: ("setpoint_furnace",), + ThermostatMode.DRY: ("setpoint_dry_air",), + ThermostatMode.MOIST: ("setpoint_moist_air",), + ThermostatMode.AUTO_CHANGE_OVER: ("setpoint_auto_changeover",), + ThermostatMode.HEATING_ECON: ("setpoint_eco_heating",), + ThermostatMode.COOLING_ECON: ("setpoint_eco_cooling",), + ThermostatMode.AWAY: ("setpoint_away_heating", "setpoint_away_cooling"), + ThermostatMode.FULL_POWER: ("setpoint_full_power",), +} + + +# strings, OZW and/or qt-ozw does not send numeric values +# https://github.com/OpenZWave/open-zwave/blob/master/cpp/src/command_classes/ThermostatOperatingState.cpp +HVAC_CURRENT_MAPPINGS = { + "idle": CURRENT_HVAC_IDLE, + "heat": CURRENT_HVAC_HEAT, + "pending heat": CURRENT_HVAC_IDLE, + "heating": CURRENT_HVAC_HEAT, + "cool": CURRENT_HVAC_COOL, + "pending cool": CURRENT_HVAC_IDLE, + "cooling": CURRENT_HVAC_COOL, + "fan only": CURRENT_HVAC_FAN, + "vent / economiser": CURRENT_HVAC_FAN, + "off": CURRENT_HVAC_OFF, +} + + +# Map Z-Wave HVAC Mode to Home Assistant value +ZW_HVAC_MODE_MAPPINGS = { + ThermostatMode.OFF: HVAC_MODE_OFF, + ThermostatMode.HEAT: HVAC_MODE_HEAT, + ThermostatMode.COOL: HVAC_MODE_COOL, + ThermostatMode.AUTO: HVAC_MODE_AUTO, + ThermostatMode.AUXILIARY: HVAC_MODE_HEAT, + ThermostatMode.FAN: HVAC_MODE_FAN_ONLY, + ThermostatMode.FURNANCE: HVAC_MODE_HEAT, + ThermostatMode.DRY: HVAC_MODE_DRY, + ThermostatMode.AUTO_CHANGE_OVER: HVAC_MODE_HEAT_COOL, + ThermostatMode.HEATING_ECON: HVAC_MODE_HEAT, + ThermostatMode.COOLING_ECON: HVAC_MODE_COOL, + ThermostatMode.AWAY: HVAC_MODE_HEAT_COOL, + ThermostatMode.FULL_POWER: HVAC_MODE_HEAT, +} + +# Map Home Assistant HVAC Mode to Z-Wave value +HVAC_MODE_ZW_MAPPINGS = { + HVAC_MODE_OFF: ThermostatMode.OFF, + HVAC_MODE_HEAT: ThermostatMode.HEAT, + HVAC_MODE_COOL: ThermostatMode.COOL, + HVAC_MODE_AUTO: ThermostatMode.AUTO, + HVAC_MODE_FAN_ONLY: ThermostatMode.FAN, + HVAC_MODE_DRY: ThermostatMode.DRY, + HVAC_MODE_HEAT_COOL: ThermostatMode.AUTO_CHANGE_OVER, +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Z-Wave Climate from Config Entry.""" + + @callback + def async_add_climate(values): + """Add Z-Wave Climate.""" + async_add_entities([ZWaveClimateEntity(values)]) + + hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + async_dispatcher_connect( + hass, f"{DOMAIN}_new_{CLIMATE_DOMAIN}", async_add_climate + ) + ) + + +class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): + """Representation of a Z-Wave Climate device.""" + + def __init__(self, values): + """Initialize the entity.""" + super().__init__(values) + self._current_mode_setpoint_values = self._get_current_mode_setpoint_values() + + @callback + def on_value_update(self): + """Call when the underlying value(s) is added or updated.""" + self._current_mode_setpoint_values = self._get_current_mode_setpoint_values() + + @property + def hvac_mode(self): + """Return hvac operation ie. heat, cool mode.""" + if not self.values.mode: + return None + return ZW_HVAC_MODE_MAPPINGS.get( + self.values.mode.value[VALUE_SELECTED_ID], HVAC_MODE_AUTO + ) + + @property + def hvac_modes(self): + """Return the list of available hvac operation modes.""" + if not self.values.mode: + return [] + # Z-Wave uses one list for both modes and presets. Extract the unique modes + all_modes = [] + for val in self.values.mode.value[VALUE_LIST]: + hass_mode = ZW_HVAC_MODE_MAPPINGS.get(val[VALUE_ID]) + if hass_mode and hass_mode not in all_modes: + all_modes.append(hass_mode) + return all_modes + + @property + def fan_mode(self): + """Return the fan speed set.""" + return self.values.fan_mode.value[VALUE_SELECTED_LABEL] + + @property + def fan_modes(self): + """Return a list of available fan modes.""" + return [entry[VALUE_LABEL] for entry in self.values.fan_mode.value[VALUE_LIST]] + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + if self.values.temperature and self.values.temperature.units == "F": + return TEMP_FAHRENHEIT + return TEMP_CELSIUS + + @property + def current_temperature(self): + """Return the current temperature.""" + if not self.values.temperature: + return None + return self.values.temperature.value + + @property + def hvac_action(self): + """Return the current running hvac operation if supported.""" + if not self.values.operating_state: + return None + cur_state = self.values.operating_state.value.lower() + return HVAC_CURRENT_MAPPINGS.get(cur_state) + + @property + def preset_mode(self): + """Return preset operation ie. eco, away.""" + # Z-Wave uses mode-values > 10 for presets + if self.values.mode.value[VALUE_SELECTED_ID] > 10: + return self.values.mode.value[VALUE_SELECTED_LABEL] + return PRESET_NONE + + @property + def preset_modes(self): + """Return the list of available preset operation modes.""" + # Z-Wave uses mode-values > 10 for presets + return [PRESET_NONE] + [ + val[VALUE_LABEL] + for val in self.values.mode.value[VALUE_LIST] + if val[VALUE_ID] > 10 + ] + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._current_mode_setpoint_values[0].value + + @property + def target_temperature_low(self) -> Optional[float]: + """Return the lowbound target temperature we try to reach.""" + return self._current_mode_setpoint_values[0].value + + @property + def target_temperature_high(self) -> Optional[float]: + """Return the highbound target temperature we try to reach.""" + return self._current_mode_setpoint_values[1].value + + async def async_set_temperature(self, **kwargs): + """Set new target temperature. + + Must know if single or double setpoint. + """ + if len(self._current_mode_setpoint_values) == 1: + setpoint = self._current_mode_setpoint_values[0] + target_temp = kwargs.get(ATTR_TEMPERATURE) + if setpoint is not None and target_temp is not None: + setpoint.send_value(target_temp) + elif len(self._current_mode_setpoint_values) == 2: + (setpoint_low, setpoint_high) = self._current_mode_setpoint_values + target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) + target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) + if setpoint_low is not None and target_temp_low is not None: + setpoint_low.send_value(target_temp_low) + if setpoint_high is not None and target_temp_high is not None: + setpoint_high.send_value(target_temp_high) + + async def async_set_fan_mode(self, fan_mode): + """Set new target fan mode.""" + # get id for this fan_mode + fan_mode_value = _get_list_id(self.values.fan_mode.value[VALUE_LIST], fan_mode) + if fan_mode_value is None: + _LOGGER.warning("Received an invalid fan mode: %s", fan_mode) + return + self.values.fan_mode.send_value(fan_mode_value) + + async def async_set_hvac_mode(self, hvac_mode): + """Set new target hvac mode.""" + if not self.values.mode: + return + if hvac_mode not in self.hvac_modes: + _LOGGER.warning("Received an invalid hvac mode: %s", hvac_mode) + return + hvac_mode_value = HVAC_MODE_ZW_MAPPINGS.get(hvac_mode) + self.values.mode.send_value(hvac_mode_value) + + async def async_set_preset_mode(self, preset_mode): + """Set new target preset mode.""" + if preset_mode == PRESET_NONE: + # try to restore to the (translated) main hvac mode + await self.async_set_hvac_mode(self.hvac_mode) + return + preset_mode_value = _get_list_id( + self.values.mode.value[VALUE_LIST], preset_mode + ) + if preset_mode_value is None: + _LOGGER.warning("Received an invalid preset mode: %s", preset_mode) + return + self.values.mode.send_value(preset_mode_value) + + @property + def device_state_attributes(self): + """Return the optional state attributes.""" + data = super().device_state_attributes + if self.values.fan_action: + data[ATTR_FAN_ACTION] = self.values.fan_action.value + if self.values.valve_position: + data[ + ATTR_VALVE_POSITION + ] = f"{self.values.valve_position.value} {self.values.valve_position.units}" + return data + + @property + def supported_features(self): + """Return the list of supported features.""" + support = 0 + if len(self._current_mode_setpoint_values) == 1: + support |= SUPPORT_TARGET_TEMPERATURE + if len(self._current_mode_setpoint_values) > 1: + support |= SUPPORT_TARGET_TEMPERATURE_RANGE + if self.values.fan_mode: + support |= SUPPORT_FAN_MODE + if self.values.mode: + support |= SUPPORT_PRESET_MODE + return support + + def _get_current_mode_setpoint_values(self) -> Tuple: + """Return a tuple of current setpoint Z-Wave value(s).""" + current_mode = self.values.mode.value[VALUE_SELECTED_ID] + setpoint_names = MODE_SETPOINT_MAPPINGS.get(current_mode, ()) + # we do not want None values in our tuple so check if the value exists + return tuple( + getattr(self.values, value_name) + for value_name in setpoint_names + if getattr(self.values, value_name, None) + ) + + +def _get_list_id(value_lst, value_lbl): + """Return the id for the value in the list.""" + return next( + (val[VALUE_ID] for val in value_lst if val[VALUE_LABEL] == value_lbl), None + ) diff --git a/homeassistant/components/ozw/const.py b/homeassistant/components/ozw/const.py index 59f189d124d..8115b18a0e8 100644 --- a/homeassistant/components/ozw/const.py +++ b/homeassistant/components/ozw/const.py @@ -1,12 +1,23 @@ """Constants for the ozw integration.""" from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN +from homeassistant.components.fan import DOMAIN as FAN_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN DOMAIN = "ozw" DATA_UNSUBSCRIBE = "unsubscribe" -PLATFORMS = [BINARY_SENSOR_DOMAIN, LIGHT_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN] +PLATFORMS = [ + BINARY_SENSOR_DOMAIN, + CLIMATE_DOMAIN, + FAN_DOMAIN, + LIGHT_DOMAIN, + LOCK_DOMAIN, + SENSOR_DOMAIN, + SWITCH_DOMAIN, +] # MQTT Topics TOPIC_OPENZWAVE = "OpenZWave" diff --git a/homeassistant/components/ozw/discovery.py b/homeassistant/components/ozw/discovery.py index 38940c9ed6e..f9b0bdb3551 100644 --- a/homeassistant/components/ozw/discovery.py +++ b/homeassistant/components/ozw/discovery.py @@ -30,6 +30,119 @@ DISCOVERY_SCHEMAS = ( } }, }, + { # Z-Wave Thermostat device translates to Climate entity + const.DISC_COMPONENT: "climate", + const.DISC_GENERIC_DEVICE_CLASS: ( + const_ozw.GENERIC_TYPE_THERMOSTAT, + const_ozw.GENERIC_TYPE_SENSOR_MULTILEVEL, + ), + const.DISC_SPECIFIC_DEVICE_CLASS: ( + const_ozw.SPECIFIC_TYPE_THERMOSTAT_GENERAL, + const_ozw.SPECIFIC_TYPE_THERMOSTAT_GENERAL_V2, + const_ozw.SPECIFIC_TYPE_SETBACK_THERMOSTAT, + const_ozw.SPECIFIC_TYPE_THERMOSTAT_HEATING, + const_ozw.SPECIFIC_TYPE_SETPOINT_THERMOSTAT, + const_ozw.SPECIFIC_TYPE_NOT_USED, + ), + const.DISC_VALUES: { + const.DISC_PRIMARY: { + const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_MODE,) + }, + "mode": { + const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_MODE,), + const.DISC_OPTIONAL: True, + }, + "temperature": { + const.DISC_COMMAND_CLASS: (CommandClass.SENSOR_MULTILEVEL,), + const.DISC_INDEX: (1,), + const.DISC_OPTIONAL: True, + }, + "fan_mode": { + const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_FAN_MODE,), + const.DISC_OPTIONAL: True, + }, + "operating_state": { + const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_OPERATING_STATE,), + const.DISC_OPTIONAL: True, + }, + "fan_action": { + const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_FAN_STATE,), + const.DISC_OPTIONAL: True, + }, + "valve_position": { + const.DISC_COMMAND_CLASS: (CommandClass.SWITCH_MULTILEVEL,), + const.DISC_INDEX: (0,), + const.DISC_OPTIONAL: True, + }, + "setpoint_heating": { + const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), + const.DISC_INDEX: (1,), + const.DISC_OPTIONAL: True, + }, + "setpoint_cooling": { + const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), + const.DISC_INDEX: (2,), + const.DISC_OPTIONAL: True, + }, + "setpoint_furnace": { + const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), + const.DISC_INDEX: (7,), + const.DISC_OPTIONAL: True, + }, + "setpoint_dry_air": { + const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), + const.DISC_INDEX: (8,), + const.DISC_OPTIONAL: True, + }, + "setpoint_moist_air": { + const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), + const.DISC_INDEX: (9,), + const.DISC_OPTIONAL: True, + }, + "setpoint_auto_changeover": { + const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), + const.DISC_INDEX: (10,), + const.DISC_OPTIONAL: True, + }, + "setpoint_eco_heating": { + const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), + const.DISC_INDEX: (11,), + const.DISC_OPTIONAL: True, + }, + "setpoint_eco_cooling": { + const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), + const.DISC_INDEX: (12,), + const.DISC_OPTIONAL: True, + }, + "setpoint_away_heating": { + const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), + const.DISC_INDEX: (13,), + const.DISC_OPTIONAL: True, + }, + "setpoint_away_cooling": { + const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), + const.DISC_INDEX: (14,), + const.DISC_OPTIONAL: True, + }, + "setpoint_full_power": { + const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), + const.DISC_INDEX: (15,), + const.DISC_OPTIONAL: True, + }, + }, + }, + { # Fan + const.DISC_COMPONENT: "fan", + const.DISC_GENERIC_DEVICE_CLASS: const_ozw.GENERIC_TYPE_SWITCH_MULTILEVEL, + const.DISC_SPECIFIC_DEVICE_CLASS: const_ozw.SPECIFIC_TYPE_FAN_SWITCH, + const.DISC_VALUES: { + const.DISC_PRIMARY: { + const.DISC_COMMAND_CLASS: CommandClass.SWITCH_MULTILEVEL, + const.DISC_INDEX: ValueIndex.SWITCH_MULTILEVEL_LEVEL, + const.DISC_TYPE: ValueType.BYTE, + }, + }, + }, { # Light const.DISC_COMPONENT: "light", const.DISC_GENERIC_DEVICE_CLASS: ( @@ -98,6 +211,16 @@ DISCOVERY_SCHEMAS = ( } }, }, + { # Lock platform + const.DISC_COMPONENT: "lock", + const.DISC_VALUES: { + const.DISC_PRIMARY: { + const.DISC_COMMAND_CLASS: (CommandClass.DOOR_LOCK,), + const.DISC_TYPE: ValueType.BOOL, + const.DISC_GENRE: ValueGenre.USER, + } + }, + }, ) diff --git a/homeassistant/components/ozw/fan.py b/homeassistant/components/ozw/fan.py new file mode 100644 index 00000000000..818bd710496 --- /dev/null +++ b/homeassistant/components/ozw/fan.py @@ -0,0 +1,95 @@ +"""Support for Z-Wave fans.""" +import logging +import math + +from homeassistant.components.fan import ( + DOMAIN as FAN_DOMAIN, + SPEED_HIGH, + SPEED_LOW, + SPEED_MEDIUM, + SPEED_OFF, + SUPPORT_SET_SPEED, + FanEntity, +) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DATA_UNSUBSCRIBE, DOMAIN +from .entity import ZWaveDeviceEntity + +_LOGGER = logging.getLogger(__name__) + +SUPPORTED_FEATURES = SUPPORT_SET_SPEED + +# Value will first be divided to an integer +VALUE_TO_SPEED = {0: SPEED_OFF, 1: SPEED_LOW, 2: SPEED_MEDIUM, 3: SPEED_HIGH} +SPEED_TO_VALUE = {SPEED_OFF: 0, SPEED_LOW: 1, SPEED_MEDIUM: 50, SPEED_HIGH: 99} +SPEED_LIST = [*SPEED_TO_VALUE] + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Z-Wave Fan from Config Entry.""" + + @callback + def async_add_fan(values): + """Add Z-Wave Fan.""" + fan = ZwaveFan(values) + async_add_entities([fan]) + + hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + async_dispatcher_connect(hass, f"{DOMAIN}_new_{FAN_DOMAIN}", async_add_fan) + ) + + +class ZwaveFan(ZWaveDeviceEntity, FanEntity): + """Representation of a Z-Wave fan.""" + + def __init__(self, values): + """Initialize the fan.""" + super().__init__(values) + self._previous_speed = None + + async def async_set_speed(self, speed): + """Set the speed of the fan.""" + if speed not in SPEED_TO_VALUE: + _LOGGER.warning("Invalid speed received: %s", speed) + return + self._previous_speed = speed + self.values.primary.send_value(SPEED_TO_VALUE[speed]) + + async def async_turn_on(self, speed=None, **kwargs): + """Turn the device on.""" + if speed is None: + # Value 255 tells device to return to previous value + self.values.primary.send_value(255) + else: + await self.async_set_speed(speed) + + async def async_turn_off(self, **kwargs): + """Turn the device off.""" + self.values.primary.send_value(0) + + @property + def is_on(self): + """Return true if device is on (speed above 0).""" + return self.values.primary.value > 0 + + @property + def speed(self): + """Return the current speed. + + The Z-Wave speed value is a byte 0-255. 255 means previous value. + The normal range of the speed is 0-99. 0 means off. + """ + value = math.ceil(self.values.primary.value * 3 / 100) + return VALUE_TO_SPEED.get(value, self._previous_speed) + + @property + def speed_list(self): + """Get the list of available speeds.""" + return SPEED_LIST + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORTED_FEATURES diff --git a/homeassistant/components/ozw/lock.py b/homeassistant/components/ozw/lock.py new file mode 100644 index 00000000000..60e0ee7ffd3 --- /dev/null +++ b/homeassistant/components/ozw/lock.py @@ -0,0 +1,39 @@ +"""Representation of Z-Wave locks.""" +from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockEntity +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DATA_UNSUBSCRIBE, DOMAIN +from .entity import ZWaveDeviceEntity + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Z-Wave lock from config entry.""" + + @callback + def async_add_lock(value): + """Add Z-Wave Lock.""" + lock = ZWaveLock(value) + + async_add_entities([lock]) + + hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + async_dispatcher_connect(hass, f"{DOMAIN}_new_{LOCK_DOMAIN}", async_add_lock) + ) + + +class ZWaveLock(ZWaveDeviceEntity, LockEntity): + """Representation of a Z-Wave lock.""" + + @property + def is_locked(self): + """Return a boolean for the state of the lock.""" + return bool(self.values.primary.value) + + async def async_lock(self, **kwargs): + """Lock the lock.""" + self.values.primary.send_value(True) + + async def async_unlock(self, **kwargs): + """Unlock the lock.""" + self.values.primary.send_value(False) diff --git a/homeassistant/components/ozw/translations/pt-BR.json b/homeassistant/components/ozw/translations/pt-BR.json new file mode 100644 index 00000000000..f08cdc09053 --- /dev/null +++ b/homeassistant/components/ozw/translations/pt-BR.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Confirme a configura\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/panasonic_viera/translations/hu.json b/homeassistant/components/panasonic_viera/translations/hu.json new file mode 100644 index 00000000000..2c3a9a820f9 --- /dev/null +++ b/homeassistant/components/panasonic_viera/translations/hu.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "IP c\u00edm" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/panasonic_viera/translations/pl.json b/homeassistant/components/panasonic_viera/translations/pl.json index 5746a08acf3..4d956896e9e 100644 --- a/homeassistant/components/panasonic_viera/translations/pl.json +++ b/homeassistant/components/panasonic_viera/translations/pl.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", "not_connected": "Zdalne po\u0142\u0105czenie z telewizorem Panasonic Viera zosta\u0142o utracone. Sprawd\u017a logi, aby uzyska\u0107 wi\u0119cej informacji.", - "unknown": "Nieznany b\u0142\u0105d, sprawd\u017a logi, aby uzyska\u0107 wi\u0119cej szczeg\u00f3\u0142\u00f3w" + "unknown": "Nieznany b\u0142\u0105d, sprawd\u017a logi, aby uzyska\u0107 wi\u0119cej szczeg\u00f3\u0142\u00f3w." }, "error": { "invalid_pin_code": "Podany kod PIN jest nieprawid\u0142owy", diff --git a/homeassistant/components/panasonic_viera/translations/pt-BR.json b/homeassistant/components/panasonic_viera/translations/pt-BR.json new file mode 100644 index 00000000000..6e85a695b08 --- /dev/null +++ b/homeassistant/components/panasonic_viera/translations/pt-BR.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Esta TV Panasonic Viera j\u00e1 est\u00e1 configurada.", + "not_connected": "A conex\u00e3o remota com a sua TV Panasonic Viera foi perdida. Verifique os logs para obter mais informa\u00e7\u00f5es." + }, + "step": { + "user": { + "description": "Digite o endere\u00e7o IP da sua TV Panasonic Viera", + "title": "Configure sua TV" + } + } + }, + "title": "Panasonic Viera" +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/ca.json b/homeassistant/components/pi_hole/translations/ca.json index c913a799321..7ba84de21f7 100644 --- a/homeassistant/components/pi_hole/translations/ca.json +++ b/homeassistant/components/pi_hole/translations/ca.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El servei ja est\u00e0 configurat" + "already_configured": "El servei ja est\u00e0 configurat", + "duplicated_name": "El nom ja existeix" }, "error": { "cannot_connect": "No s'ha pogut connectar" @@ -9,8 +10,12 @@ "step": { "user": { "data": { + "api_key": "Clau API (opcional)", "host": "Amfitri\u00f3", - "port": "Port" + "name": "Nom", + "port": "Port", + "ssl": "Utilitza SSL", + "verify_ssl": "Verifica el certificat SSL" } } } diff --git a/homeassistant/components/pi_hole/translations/de.json b/homeassistant/components/pi_hole/translations/de.json new file mode 100644 index 00000000000..91655e7245d --- /dev/null +++ b/homeassistant/components/pi_hole/translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Service ist bereits konfiguriert", + "duplicated_name": "Name existiert bereits" + }, + "error": { + "cannot_connect": "Verbindung konnte nicht hergestellt werden" + }, + "step": { + "user": { + "data": { + "api_key": "API-Schl\u00fcssel (optional)", + "host": "Host", + "name": "Name", + "port": "Port", + "ssl": "SSL verwenden", + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/es.json b/homeassistant/components/pi_hole/translations/es.json new file mode 100644 index 00000000000..9725843cef6 --- /dev/null +++ b/homeassistant/components/pi_hole/translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "El servicio ya est\u00e1 configurado", + "duplicated_name": "El nombre ya existe" + }, + "error": { + "cannot_connect": "No se pudo conectar" + }, + "step": { + "user": { + "data": { + "api_key": "Clave API (Opcional)", + "host": "Host", + "name": "Nombre", + "port": "Puerto", + "ssl": "Usar SSL", + "verify_ssl": "Verificar certificado SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/fr.json b/homeassistant/components/pi_hole/translations/fr.json new file mode 100644 index 00000000000..ddd63a02062 --- /dev/null +++ b/homeassistant/components/pi_hole/translations/fr.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "Cl\u00e9 API (facultatif)", + "name": "Nom", + "ssl": "Utiliser SSL", + "verify_ssl": "V\u00e9rifier le certificat SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/hu.json b/homeassistant/components/pi_hole/translations/hu.json new file mode 100644 index 00000000000..f1bd9a106bc --- /dev/null +++ b/homeassistant/components/pi_hole/translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "host": "Hoszt", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/it.json b/homeassistant/components/pi_hole/translations/it.json new file mode 100644 index 00000000000..d8ee9d3c6b7 --- /dev/null +++ b/homeassistant/components/pi_hole/translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "duplicated_name": "Il nome \u00e8 gi\u00e0 esistente" + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API (opzionale)", + "host": "Host", + "name": "Nome", + "port": "Porta", + "ssl": "Utilizzare SSL", + "verify_ssl": "Verificare il certificato SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/ko.json b/homeassistant/components/pi_hole/translations/ko.json new file mode 100644 index 00000000000..8d52c0fce2a --- /dev/null +++ b/homeassistant/components/pi_hole/translations/ko.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "duplicated_name": "\uc774\ub984\uc774 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "api_key": "API \ud0a4 (\uc120\ud0dd \uc0ac\ud56d)", + "host": "\ud638\uc2a4\ud2b8", + "name": "\uc774\ub984", + "port": "\ud3ec\ud2b8", + "ssl": "SSL \uc0ac\uc6a9", + "verify_ssl": "SSL \uc778\uc99d\uc11c \uac80\uc99d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/lb.json b/homeassistant/components/pi_hole/translations/lb.json new file mode 100644 index 00000000000..4224546df43 --- /dev/null +++ b/homeassistant/components/pi_hole/translations/lb.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Service ass scho konfigur\u00e9iert", + "duplicated_name": "Numm g\u00ebtt et schonn" + }, + "error": { + "cannot_connect": "Feeler beim verbannen" + }, + "step": { + "user": { + "data": { + "api_key": "API Schl\u00ebssel (Optionell)", + "host": "Host", + "name": "Numm", + "port": "Port", + "ssl": "SSL benotzen", + "verify_ssl": "SSL Zertifikat iwwerpr\u00e9iwen" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/nl.json b/homeassistant/components/pi_hole/translations/nl.json new file mode 100644 index 00000000000..16ef25a15fa --- /dev/null +++ b/homeassistant/components/pi_hole/translations/nl.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_configured": "Service al geconfigureerd", + "duplicated_name": "Naam bestond al" + }, + "error": { + "cannot_connect": "Kon niet verbinden" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/no.json b/homeassistant/components/pi_hole/translations/no.json new file mode 100644 index 00000000000..f6e9203505c --- /dev/null +++ b/homeassistant/components/pi_hole/translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert", + "duplicated_name": "Navnet eksisterte allerede" + }, + "error": { + "cannot_connect": "Tilkobling feilet" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkel (valgfritt)", + "host": "Vert", + "name": "Navn", + "port": "", + "ssl": "Bruk SSL", + "verify_ssl": "Verifisere SSL-sertifikat" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/pl.json b/homeassistant/components/pi_hole/translations/pl.json new file mode 100644 index 00000000000..c4986e71aa7 --- /dev/null +++ b/homeassistant/components/pi_hole/translations/pl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana.", + "duplicated_name": "Nazwa ju\u017c istnieje." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API (opcjonalnie)", + "host": "Nazwa hosta lub adres IP", + "name": "Nazwa", + "port": "Port", + "ssl": "U\u017cyj SSL", + "verify_ssl": "Weryfikacja certyfikatu SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/pt-BR.json b/homeassistant/components/pi_hole/translations/pt-BR.json new file mode 100644 index 00000000000..c268b1182ce --- /dev/null +++ b/homeassistant/components/pi_hole/translations/pt-BR.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Servi\u00e7o j\u00e1 configurado", + "duplicated_name": "O nome j\u00e1 existe" + }, + "error": { + "cannot_connect": "Falha ao conectar" + }, + "step": { + "user": { + "data": { + "api_key": "Chave de API (Opcional)", + "host": "Endere\u00e7o (IP)", + "name": "Nome", + "port": "Porta", + "ssl": "Usar SSL", + "verify_ssl": "Verifique o certificado SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pjlink/manifest.json b/homeassistant/components/pjlink/manifest.json index ca657923aa8..6b2dd94c0bd 100644 --- a/homeassistant/components/pjlink/manifest.json +++ b/homeassistant/components/pjlink/manifest.json @@ -2,6 +2,6 @@ "domain": "pjlink", "name": "PJLink", "documentation": "https://www.home-assistant.io/integrations/pjlink", - "requirements": ["pypjlink2==1.2.0"], + "requirements": ["pypjlink2==1.2.1"], "codeowners": [] } diff --git a/homeassistant/components/plaato/translations/it.json b/homeassistant/components/plaato/translations/it.json index 2c6235cf17c..d0f04bca9d9 100644 --- a/homeassistant/components/plaato/translations/it.json +++ b/homeassistant/components/plaato/translations/it.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u00c8 necessaria solo una singola istanza." }, "create_entry": { - "default": "Per inviare eventi a Home Assistant, dovrai impostare la funzione webhook in Plaato Airlock. \n\n Inserisci le seguenti informazioni: \n\n - URL: ` {webhook_url} ` \n - Metodo: POST \n\n Vedi [la documentazione]({docs_url}) per ulteriori dettagli." + "default": "Per inviare eventi a Home Assistant, dovrai impostare la funzione webhook in Plaato Airlock. \n\n Inserisci le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Metodo: POST \n\n Vedi [la documentazione]({docs_url}) per ulteriori dettagli." }, "step": { "user": { diff --git a/homeassistant/components/plaato/translations/pl.json b/homeassistant/components/plaato/translations/pl.json index 4edc54e357e..6db0a100e4a 100644 --- a/homeassistant/components/plaato/translations/pl.json +++ b/homeassistant/components/plaato/translations/pl.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Wymagana jest tylko jedna instancja." }, "create_entry": { - "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant'a, musisz skonfigurowa\u0107 webhook w Plaato Airlock. \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}), by pozna\u0107 szczeg\u00f3\u0142y." + "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistanta, musisz skonfigurowa\u0107 webhook w Plaato Airlock. \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}), by pozna\u0107 szczeg\u00f3\u0142y." }, "step": { "user": { diff --git a/homeassistant/components/plant/translations/ca.json b/homeassistant/components/plant/translations/ca.json index 7a2a8d4f616..942297a6157 100644 --- a/homeassistant/components/plant/translations/ca.json +++ b/homeassistant/components/plant/translations/ca.json @@ -1,9 +1,9 @@ { "state": { "_": { - "ok": "Correcte", + "ok": "OK", "problem": "Problema" } }, - "title": "Planta" + "title": "Monitoritzaci\u00f3 de planta" } \ No newline at end of file diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 7f8caf8390b..e460115ef0b 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -1,6 +1,7 @@ """Support to embed Plex.""" import asyncio import functools +import json import logging import plexapi.exceptions @@ -8,18 +9,17 @@ from plexwebsocket import PlexWebsocket import requests.exceptions import voluptuous as vol -from homeassistant import config_entries -from homeassistant.components.media_player import DOMAIN as MP_DOMAIN +from homeassistant.components.media_player.const import ( + ATTR_MEDIA_CONTENT_ID, + ATTR_MEDIA_CONTENT_TYPE, +) from homeassistant.const import ( - CONF_HOST, - CONF_PORT, - CONF_SSL, - CONF_TOKEN, + ATTR_ENTITY_ID, CONF_URL, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import ( @@ -28,61 +28,21 @@ from homeassistant.helpers.dispatcher import ( ) from .const import ( - CONF_IGNORE_NEW_SHARED_USERS, CONF_SERVER, CONF_SERVER_IDENTIFIER, - CONF_SHOW_ALL_CONTROLS, - CONF_USE_EPISODE_ART, - DEFAULT_PORT, - DEFAULT_SSL, - DEFAULT_VERIFY_SSL, DISPATCHERS, DOMAIN as PLEX_DOMAIN, PLATFORMS, PLATFORMS_COMPLETED, - PLEX_MEDIA_PLAYER_OPTIONS, PLEX_SERVER_CONFIG, PLEX_UPDATE_PLATFORMS_SIGNAL, SERVERS, + SERVICE_PLAY_ON_SONOS, WEBSOCKETS, ) from .errors import ShouldUpdateConfigEntry from .server import PlexServer -MEDIA_PLAYER_SCHEMA = vol.All( - cv.deprecated(CONF_SHOW_ALL_CONTROLS, invalidation_version="0.110"), - vol.Schema( - { - vol.Optional(CONF_USE_EPISODE_ART, default=False): cv.boolean, - vol.Optional(CONF_SHOW_ALL_CONTROLS): cv.boolean, - vol.Optional(CONF_IGNORE_NEW_SHARED_USERS, default=False): cv.boolean, - } - ), -) - -SERVER_CONFIG_SCHEMA = vol.Schema( - vol.All( - { - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_TOKEN): cv.string, - vol.Optional(CONF_SERVER): cv.string, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, - vol.Optional(MP_DOMAIN, default={}): MEDIA_PLAYER_SCHEMA, - }, - cv.has_at_least_one_key(CONF_HOST, CONF_TOKEN), - ) -) - -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(PLEX_DOMAIN, invalidation_version="0.111"), - {PLEX_DOMAIN: SERVER_CONFIG_SCHEMA}, - ), - extra=vol.ALLOW_EXTRA, -) - _LOGGER = logging.getLogger(__package__) @@ -93,32 +53,9 @@ async def async_setup(hass, config): {SERVERS: {}, DISPATCHERS: {}, WEBSOCKETS: {}, PLATFORMS_COMPLETED: {}}, ) - plex_config = config.get(PLEX_DOMAIN, {}) - if plex_config: - _async_setup_plex(hass, plex_config) - return True -def _async_setup_plex(hass, config): - """Pass configuration to a config flow.""" - server_config = dict(config) - if MP_DOMAIN in server_config: - hass.data.setdefault(PLEX_MEDIA_PLAYER_OPTIONS, server_config.pop(MP_DOMAIN)) - if CONF_HOST in server_config: - protocol = "https" if server_config.pop(CONF_SSL) else "http" - server_config[ - CONF_URL - ] = f"{protocol}://{server_config.pop(CONF_HOST)}:{server_config.pop(CONF_PORT)}" - hass.async_create_task( - hass.config_entries.flow.async_init( - PLEX_DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=server_config, - ) - ) - - async def async_setup_entry(hass, entry): """Set up Plex from a config entry.""" server_config = entry.data[PLEX_SERVER_CONFIG] @@ -128,14 +65,6 @@ async def async_setup_entry(hass, entry): entry, unique_id=entry.data[CONF_SERVER_IDENTIFIER] ) - if MP_DOMAIN not in entry.options: - options = dict(entry.options) - options.setdefault( - MP_DOMAIN, - hass.data.get(PLEX_MEDIA_PLAYER_OPTIONS) or MEDIA_PLAYER_SCHEMA({}), - ) - hass.config_entries.async_update_entry(entry, options=options) - plex_server = PlexServer( hass, server_config, entry.data[CONF_SERVER_IDENTIFIER], entry.options ) @@ -215,6 +144,24 @@ async def async_setup_entry(hass, entry): ) task.add_done_callback(functools.partial(start_websocket_session, platform)) + async def async_play_on_sonos_service(service_call): + await hass.async_add_executor_job(play_on_sonos, hass, service_call) + + play_on_sonos_schema = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_MEDIA_CONTENT_ID): str, + vol.Optional(ATTR_MEDIA_CONTENT_TYPE): vol.In("music"), + } + ) + + hass.services.async_register( + PLEX_DOMAIN, + SERVICE_PLAY_ON_SONOS, + async_play_on_sonos_service, + schema=play_on_sonos_schema, + ) + return True @@ -244,3 +191,52 @@ async def async_options_updated(hass, entry): """Triggered by config entry options updates.""" server_id = entry.data[CONF_SERVER_IDENTIFIER] hass.data[PLEX_DOMAIN][SERVERS][server_id].options = entry.options + + +def play_on_sonos(hass, service_call): + """Play Plex media on a linked Sonos device.""" + entity_id = service_call.data[ATTR_ENTITY_ID] + content_id = service_call.data[ATTR_MEDIA_CONTENT_ID] + content = json.loads(content_id) + + sonos = hass.components.sonos + try: + sonos_id = sonos.get_coordinator_id(entity_id) + except HomeAssistantError as err: + _LOGGER.error("Cannot get Sonos device: %s", err) + return + + if isinstance(content, int): + content = {"plex_key": content} + + plex_server_name = content.get("plex_server") + shuffle = content.pop("shuffle", 0) + + plex_servers = hass.data[PLEX_DOMAIN][SERVERS].values() + if plex_server_name: + plex_server = [x for x in plex_servers if x.friendly_name == plex_server_name] + if not plex_server: + _LOGGER.error( + "Requested Plex server '%s' not found in %s", + plex_server_name, + list(map(lambda x: x.friendly_name, plex_servers)), + ) + return + else: + plex_server = next(iter(plex_servers)) + + sonos_speaker = plex_server.account.sonos_speaker_by_id(sonos_id) + if sonos_speaker is None: + _LOGGER.error( + "Sonos speaker '%s' could not be found on this Plex account", sonos_id + ) + return + + media = plex_server.lookup_media("music", **content) + if media is None: + _LOGGER.error("Media could not be found: %s", content) + return + + _LOGGER.debug("Attempting to play '%s' on %s", media, sonos_speaker) + playqueue = plex_server.create_playqueue(media, shuffle=shuffle) + sonos_speaker.playMedia(playqueue) diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index e4d3751f661..5057b535ea6 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -13,6 +13,7 @@ from homeassistant import config_entries from homeassistant.components.http.view import HomeAssistantView from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.const import ( + CONF_CLIENT_ID, CONF_HOST, CONF_PORT, CONF_SSL, @@ -29,7 +30,6 @@ from .const import ( # pylint: disable=unused-import AUTH_CALLBACK_NAME, AUTH_CALLBACK_PATH, AUTOMATIC_SETUP_STRING, - CONF_CLIENT_IDENTIFIER, CONF_IGNORE_NEW_SHARED_USERS, CONF_IGNORE_PLEX_WEB_CLIENTS, CONF_MONITORED_USERS, @@ -170,10 +170,6 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Validate a provided configuration.""" errors = {} self.current_login = server_config - is_importing = ( - self.context["source"] # pylint: disable=no-member - == config_entries.SOURCE_IMPORT - ) plex_server = PlexServer(self.hass, server_config) try: @@ -196,11 +192,6 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors[CONF_HOST] = "not_found" except ServerNotSpecified as available_servers: - if is_importing: - _LOGGER.warning( - "Imported configuration has multiple available Plex servers. Specify server in configuration or add a new Integration." - ) - return self.async_abort(reason="non-interactive") self.available_servers = available_servers.args[0] return await self.async_step_select_server() @@ -209,8 +200,6 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="unknown") if errors: - if is_importing: - return self.async_abort(reason="non-interactive") if self._manual: return await self.async_step_manual_setup( user_input=server_config, errors=errors @@ -227,7 +216,7 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): entry_config = {CONF_URL: url} if self.client_id: - entry_config[CONF_CLIENT_IDENTIFIER] = self.client_id + entry_config[CONF_CLIENT_ID] = self.client_id if token: entry_config[CONF_TOKEN] = token if url.startswith("https"): @@ -274,11 +263,6 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors={}, ) - async def async_step_import(self, import_config): - """Import from Plex configuration.""" - _LOGGER.debug("Imported Plex configuration") - return await self.async_step_server_validate(import_config) - async def async_step_integration_discovery(self, discovery_info): """Handle GDM discovery.""" machine_identifier = discovery_info["data"]["Resource-Identifier"] diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index e8bcfb42ca6..9d9b8ed8915 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -16,7 +16,6 @@ PLATFORMS_COMPLETED = "platforms_completed" SERVERS = "servers" WEBSOCKETS = "websockets" -PLEX_MEDIA_PLAYER_OPTIONS = "plex_mp_options" PLEX_SERVER_CONFIG = "server_config" PLEX_NEW_MP_SIGNAL = "plex_new_mp_signal.{}" @@ -24,11 +23,9 @@ PLEX_UPDATE_MEDIA_PLAYER_SIGNAL = "plex_update_mp_signal.{}" PLEX_UPDATE_PLATFORMS_SIGNAL = "plex_update_platforms_signal.{}" PLEX_UPDATE_SENSOR_SIGNAL = "plex_update_sensor_signal.{}" -CONF_CLIENT_IDENTIFIER = "client_id" CONF_SERVER = "server" CONF_SERVER_IDENTIFIER = "server_id" CONF_USE_EPISODE_ART = "use_episode_art" -CONF_SHOW_ALL_CONTROLS = "show_all_controls" CONF_IGNORE_NEW_SHARED_USERS = "ignore_new_shared_users" CONF_IGNORE_PLEX_WEB_CLIENTS = "ignore_plex_web_clients" CONF_MONITORED_USERS = "monitored_users" @@ -43,3 +40,5 @@ X_PLEX_VERSION = __version__ AUTOMATIC_SETUP_STRING = "Obtain a new token from plex.tv" MANUAL_SETUP_STRING = "Configure Plex server manually" + +SERVICE_PLAY_ON_SONOS = "play_on_sonos" diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index e48a37a77d5..386f772947a 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -3,7 +3,12 @@ "name": "Plex Media Server", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/plex", - "requirements": ["plexapi==3.6.0", "plexauth==0.0.5", "plexwebsocket==0.0.8"], + "requirements": [ + "plexapi==4.0.0", + "plexauth==0.0.5", + "plexwebsocket==0.0.10" + ], "dependencies": ["http"], + "after_dependencies": ["sonos"], "codeowners": ["@jjlawren"] } diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index b19f687482c..a25765ec588 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -7,12 +7,9 @@ import requests.exceptions from homeassistant.components.media_player import DOMAIN as MP_DOMAIN, MediaPlayerEntity from homeassistant.components.media_player.const import ( - MEDIA_TYPE_EPISODE, MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, - MEDIA_TYPE_PLAYLIST, MEDIA_TYPE_TVSHOW, - MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, @@ -487,7 +484,7 @@ class PlexMediaPlayer(MediaPlayerEntity): | SUPPORT_VOLUME_MUTE ) - return 0 + return SUPPORT_PLAY_MEDIA def set_volume_level(self, volume): """Set volume level, range 0..1.""" @@ -561,32 +558,12 @@ class PlexMediaPlayer(MediaPlayerEntity): ) return - media_type = media_type.lower() src = json.loads(media_id) - if media_type == PLEX_DOMAIN and isinstance(src, int): - try: - media = self.plex_server.fetch_item(src) - except plexapi.exceptions.NotFound: - _LOGGER.error("Media for key %s not found", src) - return - shuffle = 0 - else: - library = src.get("library_name") - shuffle = src.get("shuffle", 0) - media = None + if isinstance(src, int): + src = {"plex_key": src} - try: - if media_type == MEDIA_TYPE_MUSIC: - media = self._get_music_media(library, src) - elif media_type == MEDIA_TYPE_EPISODE: - media = self._get_tv_media(library, src) - elif media_type == MEDIA_TYPE_PLAYLIST: - media = self.plex_server.playlist(src["playlist_name"]) - elif media_type == MEDIA_TYPE_VIDEO: - media = self.plex_server.library.section(library).get(src["video_name"]) - except plexapi.exceptions.NotFound: - _LOGGER.error("Media could not be found: %s", media_id) - return + shuffle = src.pop("shuffle", 0) + media = self.plex_server.lookup_media(media_type, **src) if media is None: _LOGGER.error("Media could not be found: %s", media_id) @@ -600,79 +577,6 @@ class PlexMediaPlayer(MediaPlayerEntity): except requests.exceptions.ConnectTimeout: _LOGGER.error("Timed out playing on %s", self.name) - def _get_music_media(self, library_name, src): - """Find music media and return a Plex media object.""" - artist_name = src["artist_name"] - album_name = src.get("album_name") - track_name = src.get("track_name") - track_number = src.get("track_number") - - artist = self.plex_server.library.section(library_name).get(artist_name) - - if album_name: - album = artist.album(album_name) - - if track_name: - return album.track(track_name) - - if track_number: - for track in album.tracks(): - if int(track.index) == int(track_number): - return track - return None - - return album - - if track_name: - return artist.searchTracks(track_name, maxresults=1) - return artist - - def _get_tv_media(self, library_name, src): - """Find TV media and return a Plex media object.""" - show_name = src["show_name"] - season_number = src.get("season_number") - episode_number = src.get("episode_number") - target_season = None - target_episode = None - - show = self.plex_server.library.section(library_name).get(show_name) - - if not season_number: - return show - - for season in show.seasons(): - if int(season.seasonNumber) == int(season_number): - target_season = season - break - - if target_season is None: - _LOGGER.error( - "Season not found: %s\\%s - S%sE%s", - library_name, - show_name, - str(season_number).zfill(2), - str(episode_number).zfill(2), - ) - else: - if not episode_number: - return target_season - - for episode in target_season.episodes(): - if int(episode.index) == int(episode_number): - target_episode = episode - break - - if target_episode is None: - _LOGGER.error( - "Episode not found: %s\\%s - S%sE%s", - library_name, - show_name, - str(season_number).zfill(2), - str(episode_number).zfill(2), - ) - - return target_episode - @property def device_state_attributes(self): """Return the scene state attributes.""" diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index a1f5af321f3..dda4c0a46b5 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -3,7 +3,7 @@ import logging import ssl from urllib.parse import urlparse -from plexapi.exceptions import Unauthorized +from plexapi.exceptions import NotFound, Unauthorized import plexapi.myplex import plexapi.playqueue import plexapi.server @@ -11,13 +11,18 @@ from requests import Session import requests.exceptions from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_EPISODE, + MEDIA_TYPE_MUSIC, + MEDIA_TYPE_PLAYLIST, + MEDIA_TYPE_VIDEO, +) +from homeassistant.const import CONF_CLIENT_ID, CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import callback from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( - CONF_CLIENT_IDENTIFIER, CONF_IGNORE_NEW_SHARED_USERS, CONF_IGNORE_PLEX_WEB_CLIENTS, CONF_MONITORED_USERS, @@ -25,6 +30,7 @@ from .const import ( CONF_USE_EPISODE_ART, DEBOUNCE_TIMEOUT, DEFAULT_VERIFY_SSL, + DOMAIN, PLEX_NEW_MP_SIGNAL, PLEX_UPDATE_MEDIA_PLAYER_SIGNAL, PLEX_UPDATE_SENSOR_SIGNAL, @@ -50,6 +56,7 @@ class PlexServer: def __init__(self, hass, server_config, known_server_id=None, options=None): """Initialize a Plex server instance.""" self.hass = hass + self._plex_account = None self._plex_server = None self._created_clients = set() self._known_clients = set() @@ -73,11 +80,18 @@ class PlexServer: ).async_call # Header conditionally added as it is not available in config entry v1 - if CONF_CLIENT_IDENTIFIER in server_config: - plexapi.X_PLEX_IDENTIFIER = server_config[CONF_CLIENT_IDENTIFIER] + if CONF_CLIENT_ID in server_config: + plexapi.X_PLEX_IDENTIFIER = server_config[CONF_CLIENT_ID] plexapi.myplex.BASE_HEADERS = plexapi.reset_base_headers() plexapi.server.BASE_HEADERS = plexapi.reset_base_headers() + @property + def account(self): + """Return a MyPlexAccount instance.""" + if not self._plex_account: + self._plex_account = plexapi.myplex.MyPlexAccount(token=self._token) + return self._plex_account + def connect(self): """Connect to a Plex server directly, obtaining direct URL if necessary.""" config_entry_update_needed = False @@ -339,7 +353,7 @@ class PlexServer: @property def option_use_episode_art(self): """Return use_episode_art option.""" - return self.options[MP_DOMAIN][CONF_USE_EPISODE_ART] + return self.options[MP_DOMAIN].get(CONF_USE_EPISODE_ART, False) @property def option_monitored_users(self): @@ -367,3 +381,157 @@ class PlexServer: def fetch_item(self, item): """Fetch item from Plex server.""" return self._plex_server.fetchItem(item) + + def lookup_media(self, media_type, **kwargs): + """Lookup a piece of media.""" + media_type = media_type.lower() + + if media_type == DOMAIN: + key = kwargs["plex_key"] + try: + return self.fetch_item(key) + except plexapi.exceptions.NotFound: + _LOGGER.error("Media for key %s not found", key) + return None + + if media_type == MEDIA_TYPE_PLAYLIST: + try: + playlist_name = kwargs["playlist_name"] + return self.playlist(playlist_name) + except KeyError: + _LOGGER.error("Must specify 'playlist_name' for this search") + return None + except NotFound: + _LOGGER.error( + "Playlist '%s' not found", playlist_name, + ) + return None + + try: + library_name = kwargs["library_name"] + library_section = self.library.section(library_name) + except KeyError: + _LOGGER.error("Must specify 'library_name' for this search") + return None + except NotFound: + _LOGGER.error("Library '%s' not found", library_name) + return None + + def lookup_music(): + """Search for music and return a Plex media object.""" + album_name = kwargs.get("album_name") + track_name = kwargs.get("track_name") + track_number = kwargs.get("track_number") + + try: + artist_name = kwargs["artist_name"] + artist = library_section.get(artist_name) + except KeyError: + _LOGGER.error("Must specify 'artist_name' for this search") + return None + except NotFound: + _LOGGER.error( + "Artist '%s' not found in '%s'", artist_name, library_name + ) + return None + + if album_name: + try: + album = artist.album(album_name) + except NotFound: + _LOGGER.error( + "Album '%s' by '%s' not found", album_name, artist_name + ) + return None + + if track_name: + try: + return album.track(track_name) + except NotFound: + _LOGGER.error( + "Track '%s' on '%s' by '%s' not found", + track_name, + album_name, + artist_name, + ) + return None + + if track_number: + for track in album.tracks(): + if int(track.index) == int(track_number): + return track + + _LOGGER.error( + "Track %d on '%s' by '%s' not found", + track_number, + album_name, + artist_name, + ) + return None + return album + + if track_name: + try: + return artist.get(track_name) + except NotFound: + _LOGGER.error( + "Track '%s' by '%s' not found", track_name, artist_name + ) + return None + + return artist + + def lookup_tv(): + """Find TV media and return a Plex media object.""" + season_number = kwargs.get("season_number") + episode_number = kwargs.get("episode_number") + + try: + show_name = kwargs["show_name"] + show = library_section.get(show_name) + except KeyError: + _LOGGER.error("Must specify 'show_name' for this search") + return None + except NotFound: + _LOGGER.error("Show '%s' not found in '%s'", show_name, library_name) + return None + + if not season_number: + return show + + try: + season = show.season(int(season_number)) + except NotFound: + _LOGGER.error( + "Season %d of '%s' not found", season_number, show_name, + ) + return None + + if not episode_number: + return season + + try: + return season.episode(episode=int(episode_number)) + except NotFound: + _LOGGER.error( + "Episode not found: %s - S%sE%s", + show_name, + str(season_number).zfill(2), + str(episode_number).zfill(2), + ) + return None + + if media_type == MEDIA_TYPE_MUSIC: + return lookup_music() + if media_type == MEDIA_TYPE_EPISODE: + return lookup_tv() + if media_type == MEDIA_TYPE_VIDEO: + try: + video_name = kwargs["video_name"] + return library_section.get(video_name) + except KeyError: + _LOGGER.error("Must specify 'video_name' for this search") + except NotFound: + _LOGGER.error( + "Movie '%s' not found in '%s'", video_name, library_name, + ) diff --git a/homeassistant/components/plex/services.yaml b/homeassistant/components/plex/services.yaml new file mode 100644 index 00000000000..0245edfb99e --- /dev/null +++ b/homeassistant/components/plex/services.yaml @@ -0,0 +1,13 @@ +play_on_sonos: + description: Play music hosted on a Plex server on a linked Sonos speaker. + fields: + entity_id: + description: Entity ID of a media_player from the Sonos integration. + example: "media_player.sonos_living_room" + media_content_id: + description: The ID of the content to play. See https://www.home-assistant.io/integrations/plex/#music for details. + example: >- + '{ "library_name": "Music", "artist_name": "Stevie Wonder" }' + media_content_type: + description: The type of content to play. Must be "music". + example: "music" diff --git a/homeassistant/components/plex/strings.json b/homeassistant/components/plex/strings.json index 09dbc9a8ecf..2f50e2d3090 100644 --- a/homeassistant/components/plex/strings.json +++ b/homeassistant/components/plex/strings.json @@ -41,8 +41,6 @@ "all_configured": "All linked servers already configured", "already_configured": "This Plex server is already configured", "already_in_progress": "Plex is being configured", - "invalid_import": "Imported configuration is invalid", - "non-interactive": "Non-interactive import", "token_request_timeout": "Timed out obtaining token", "unknown": "Failed for unknown reason" } @@ -60,4 +58,4 @@ } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/plex/translations/bg.json b/homeassistant/components/plex/translations/bg.json index fabb44dc9ea..92cd6f59819 100644 --- a/homeassistant/components/plex/translations/bg.json +++ b/homeassistant/components/plex/translations/bg.json @@ -21,10 +21,6 @@ }, "description": "\u041d\u0430\u043b\u0438\u0447\u043d\u0438 \u0441\u0430 \u043d\u044f\u043a\u043e\u043b\u043a\u043e \u0441\u044a\u0440\u0432\u044a\u0440\u0430, \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0435\u0434\u0438\u043d:", "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 Plex \u0441\u044a\u0440\u0432\u044a\u0440" - }, - "start_website_auth": { - "description": "\u041f\u0440\u043e\u0434\u044a\u043b\u0436\u0435\u0442\u0435 \u0441 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 plex.tv.", - "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 Plex \u0441\u044a\u0440\u0432\u044a\u0440" } } }, diff --git a/homeassistant/components/plex/translations/ca.json b/homeassistant/components/plex/translations/ca.json index 72cc2d97a30..fe78770ed9b 100644 --- a/homeassistant/components/plex/translations/ca.json +++ b/homeassistant/components/plex/translations/ca.json @@ -10,16 +10,17 @@ "unknown": "Ha fallat per motiu desconegut" }, "error": { - "faulty_credentials": "Ha fallat l'autoritzaci\u00f3", + "faulty_credentials": "Ha fallat l'autoritzaci\u00f3, comprova el Token", "host_or_token": "Has de proporcionar almenys o un amfitri\u00f3 (host) o un token", - "no_servers": "No hi ha servidors enlla\u00e7ats amb el compte", + "no_servers": "No hi ha servidors vinculats amb el compte de Plex", "not_found": "No s'ha trobat el servidor Plex", "ssl_error": "Problema amb el certificat SSL" }, + "flow_title": "{name} ({host})", "step": { "manual_setup": { "data": { - "host": "Amfitri\u00f3 (opcional si es proporciona un token)", + "host": "Amfitri\u00f3", "port": "Port", "ssl": "Utilitza SSL", "token": "Token (opcional)", @@ -34,10 +35,6 @@ "description": "Hi ha diversos servidors disponibles, selecciona'n un:", "title": "Selecciona servidor Plex" }, - "start_website_auth": { - "description": "Continua l'autoritzaci\u00f3 a plex.tv.", - "title": "Connexi\u00f3 amb el servidor Plex" - }, "user": { "description": "V\u00e9s a [plex.tv](https://plex.tv) per enlla\u00e7ar un servidor Plex.", "title": "Servidor Multim\u00e8dia Plex" diff --git a/homeassistant/components/plex/translations/da.json b/homeassistant/components/plex/translations/da.json index 06b40f1b1ad..24e51410d00 100644 --- a/homeassistant/components/plex/translations/da.json +++ b/homeassistant/components/plex/translations/da.json @@ -21,10 +21,6 @@ }, "description": "Flere servere til r\u00e5dighed, v\u00e6lg en:", "title": "V\u00e6lg Plex-server" - }, - "start_website_auth": { - "description": "Forts\u00e6t for at godkende p\u00e5 plex.tv.", - "title": "Forbind Plex-server" } } }, diff --git a/homeassistant/components/plex/translations/de.json b/homeassistant/components/plex/translations/de.json index 3d002cc299c..4208e9a528b 100644 --- a/homeassistant/components/plex/translations/de.json +++ b/homeassistant/components/plex/translations/de.json @@ -34,10 +34,6 @@ "description": "Mehrere Server verf\u00fcgbar, w\u00e4hle einen aus:", "title": "Plex-Server ausw\u00e4hlen" }, - "start_website_auth": { - "description": "Weiter zur Autorisierung unter plex.tv.", - "title": "Plex Server verbinden" - }, "user": { "description": "Gehen Sie zu [plex.tv] (https://plex.tv), um einen Plex-Server zu verbinden", "title": "Plex Media Server" diff --git a/homeassistant/components/plex/translations/en.json b/homeassistant/components/plex/translations/en.json index d0c9a0d4e32..78c250ccc37 100644 --- a/homeassistant/components/plex/translations/en.json +++ b/homeassistant/components/plex/translations/en.json @@ -35,10 +35,6 @@ "description": "Multiple servers available, select one:", "title": "Select Plex server" }, - "start_website_auth": { - "description": "Continue to authorize at plex.tv.", - "title": "Connect Plex server" - }, "user": { "description": "Continue to [plex.tv](https://plex.tv) to link a Plex server.", "title": "Plex Media Server" diff --git a/homeassistant/components/plex/translations/es-419.json b/homeassistant/components/plex/translations/es-419.json index 72cd3deefbb..62f5d6dd6d5 100644 --- a/homeassistant/components/plex/translations/es-419.json +++ b/homeassistant/components/plex/translations/es-419.json @@ -21,10 +21,6 @@ }, "description": "M\u00faltiples servidores disponibles, seleccione uno:", "title": "Seleccionar servidor Plex" - }, - "start_website_auth": { - "description": "Continuar autorizando en plex.tv.", - "title": "Conectar a servidor Plex" } } }, diff --git a/homeassistant/components/plex/translations/es.json b/homeassistant/components/plex/translations/es.json index 6959c59deca..6cdecd52408 100644 --- a/homeassistant/components/plex/translations/es.json +++ b/homeassistant/components/plex/translations/es.json @@ -16,10 +16,11 @@ "not_found": "No se ha encontrado el servidor Plex", "ssl_error": "Problema con el certificado SSL" }, + "flow_title": "{name} ({host})", "step": { "manual_setup": { "data": { - "host": "Host (Opcional si se proporciona Token)", + "host": "Host", "port": "Puerto", "ssl": "Usar SSL", "token": "Token (Opcional)", @@ -34,10 +35,6 @@ "description": "Varios servidores disponibles, seleccione uno:", "title": "Seleccione el servidor Plex" }, - "start_website_auth": { - "description": "Contin\u00fae en plex.tv para autorizar", - "title": "Conectar servidor Plex" - }, "user": { "description": "Continuar hacia [plex.tv](https://plex.tv) para vincular un servidor Plex.", "title": "Plex Media Server" diff --git a/homeassistant/components/plex/translations/fr.json b/homeassistant/components/plex/translations/fr.json index 7ec9b29c7ff..b27d648a911 100644 --- a/homeassistant/components/plex/translations/fr.json +++ b/homeassistant/components/plex/translations/fr.json @@ -33,10 +33,6 @@ "description": "Plusieurs serveurs disponibles, s\u00e9lectionnez-en un:", "title": "S\u00e9lectionnez le serveur Plex" }, - "start_website_auth": { - "description": "Continuer d'autoriser sur plex.tv.", - "title": "Connecter un serveur Plex" - }, "user": { "title": "Plex Media Server" }, diff --git a/homeassistant/components/plex/translations/hu.json b/homeassistant/components/plex/translations/hu.json index 839eff780de..48a2da53998 100644 --- a/homeassistant/components/plex/translations/hu.json +++ b/homeassistant/components/plex/translations/hu.json @@ -17,6 +17,7 @@ "step": { "manual_setup": { "data": { + "host": "Hoszt", "port": "Port", "ssl": "Haszn\u00e1ljon SSL-t" } @@ -28,10 +29,6 @@ "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": { "title": "Plex Media Server" }, diff --git a/homeassistant/components/plex/translations/it.json b/homeassistant/components/plex/translations/it.json index d7bd1060985..db0de6b3e13 100644 --- a/homeassistant/components/plex/translations/it.json +++ b/homeassistant/components/plex/translations/it.json @@ -16,10 +16,11 @@ "not_found": "Server Plex non trovato", "ssl_error": "Problema con il certificato SSL" }, + "flow_title": "{name} ({host})", "step": { "manual_setup": { "data": { - "host": "Host (opzionale se si \u00e8 fornito un Token)", + "host": "Host", "port": "Porta", "ssl": "Utilizzare SSL", "token": "Token (opzionale)", @@ -34,10 +35,6 @@ "description": "Sono disponibili pi\u00f9 server, selezionarne uno:", "title": "Selezionare il server Plex" }, - "start_website_auth": { - "description": "Continuare ad autorizzare su plex.tv.", - "title": "Collegare il server Plex" - }, "user": { "description": "Continuare su [plex.tv](https://plex.tv) per collegare un server Plex.", "title": "Plex Media Server" diff --git a/homeassistant/components/plex/translations/ko.json b/homeassistant/components/plex/translations/ko.json index 4443e3bc218..e376913eef6 100644 --- a/homeassistant/components/plex/translations/ko.json +++ b/homeassistant/components/plex/translations/ko.json @@ -16,6 +16,7 @@ "not_found": "Plex \uc11c\ubc84\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "ssl_error": "SSL \uc778\uc99d\uc11c \uac80\uc99d" }, + "flow_title": "{name} ({host})", "step": { "manual_setup": { "data": { @@ -34,10 +35,6 @@ "description": "\uc5ec\ub7ec \uc11c\ubc84\uac00 \uc0ac\uc6a9 \uac00\ub2a5\ud569\ub2c8\ub2e4. \ud558\ub098\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694:", "title": "Plex \uc11c\ubc84 \uc120\ud0dd\ud558\uae30" }, - "start_website_auth": { - "description": "plex.tv \uc5d0\uc11c \uc778\uc99d\uc744 \uc9c4\ud589\ud574\uc8fc\uc138\uc694.", - "title": "Plex \uc11c\ubc84 \uc5f0\uacb0\ud558\uae30" - }, "user": { "description": "Plex \uc11c\ubc84\ub97c \uc5f0\uacb0\ud558\ub824\uba74 [plex.tv](https://plex.tv) \ub85c \uacc4\uc18d \uc9c4\ud589\ud574\uc8fc\uc138\uc694.", "title": "Plex \ubbf8\ub514\uc5b4 \uc11c\ubc84" diff --git a/homeassistant/components/plex/translations/lb.json b/homeassistant/components/plex/translations/lb.json index c325389295c..155fd66f90b 100644 --- a/homeassistant/components/plex/translations/lb.json +++ b/homeassistant/components/plex/translations/lb.json @@ -16,6 +16,7 @@ "not_found": "Kee Plex Server fonnt", "ssl_error": "SSL Zertifikat Problem" }, + "flow_title": "{name} ({host})", "step": { "manual_setup": { "data": { @@ -34,10 +35,6 @@ "description": "M\u00e9i Server disponibel, wielt een aus:", "title": "Plex Server auswielen" }, - "start_website_auth": { - "description": "Weiderfueren op plex.tv fir d'Autorisatioun.", - "title": "Plex Server verbannen" - }, "user": { "description": "Verbann dech mat [plex.tv](https://pley.tv) fir ee Plex Server ze verlinken.", "title": "Plex Media Server" diff --git a/homeassistant/components/plex/translations/nl.json b/homeassistant/components/plex/translations/nl.json index 6938bdea5f5..da13477c4af 100644 --- a/homeassistant/components/plex/translations/nl.json +++ b/homeassistant/components/plex/translations/nl.json @@ -14,6 +14,7 @@ "no_servers": "Geen servers gekoppeld aan account", "not_found": "Plex-server niet gevonden" }, + "flow_title": "{naam} ({host})", "step": { "select_server": { "data": { @@ -21,10 +22,6 @@ }, "description": "Meerdere servers beschikbaar, selecteer er een:", "title": "Selecteer Plex server" - }, - "start_website_auth": { - "description": "Ga verder met autoriseren bij plex.tv.", - "title": "Verbind de Plex server" } } }, diff --git a/homeassistant/components/plex/translations/no.json b/homeassistant/components/plex/translations/no.json index 19a4db4b8a6..ab72275070a 100644 --- a/homeassistant/components/plex/translations/no.json +++ b/homeassistant/components/plex/translations/no.json @@ -16,11 +16,10 @@ "not_found": "Plex-server ikke funnet", "ssl_error": "Problem med SSL-sertifikat" }, + "flow_title": "{name}({host})", "step": { "manual_setup": { "data": { - "host": "Host (valgfritt hvis token f\u00f8lger med)", - "port": "Port", "ssl": "Bruk SSL", "token": "Token (valgfritt)", "verify_ssl": "Verifisere SSL-sertifikat" @@ -34,10 +33,6 @@ "description": "Flere servere tilgjengelig, velg en:", "title": "Velg Plex-server" }, - "start_website_auth": { - "description": "Fortsett \u00e5 godkjenne p\u00e5 plex.tv", - "title": "Koble til Plex-server" - }, "user": { "description": "Fortsett til [plex.tv] (https://plex.tv) for \u00e5 koble en Plex-server.", "title": "Plex Media Server" diff --git a/homeassistant/components/plex/translations/pl.json b/homeassistant/components/plex/translations/pl.json index 6b61382458b..ce67ab36168 100644 --- a/homeassistant/components/plex/translations/pl.json +++ b/homeassistant/components/plex/translations/pl.json @@ -16,13 +16,14 @@ "not_found": "Nie znaleziono serwera Plex", "ssl_error": "Problem z certyfikatem SSL." }, + "flow_title": "{name} ({host})", "step": { "manual_setup": { "data": { - "host": "[%key_id:common::config_flow::data::host%] (opcjonalnie, je\u015bli wprowadzono token)", - "port": "[%key_id:common::config_flow::data::port%]", + "host": "Nazwa hosta lub adres IP", + "port": "Port", "ssl": "U\u017cyj SSL", - "token": "[%key_id:common::config_flow::data::access_token%] (opcjonalnie)", + "token": "Token dost\u0119pu (opcjonalnie)", "verify_ssl": "Weryfikacja certyfikatu SSL" }, "title": "Manualna konfiguracja Plex" @@ -34,10 +35,6 @@ "description": "Dost\u0119pnych jest wiele serwer\u00f3w, wybierz jeden:", "title": "Wybierz serwer Plex" }, - "start_website_auth": { - "description": "Kontynuuj, by dokona\u0107 autoryzacji w plex.tv.", - "title": "Po\u0142\u0105cz z serwerem Plex" - }, "user": { "description": "Przejd\u017a do [plex.tv](https://plex.tv), aby po\u0142\u0105czy\u0107 serwer Plex.", "title": "Serwer medi\u00f3w Plex" @@ -55,6 +52,7 @@ "plex_mp_settings": { "data": { "ignore_new_shared_users": "Ignoruj nowych zarz\u0105dzanych/wsp\u00f3\u0142dzielonych u\u017cytkownik\u00f3w", + "ignore_plex_web_clients": "Zignoruj klient\u00f3w Plex Web", "monitored_users": "Monitorowani u\u017cytkownicy", "use_episode_art": "U\u017cyj grafiki odcinka" }, diff --git a/homeassistant/components/plex/translations/pt-BR.json b/homeassistant/components/plex/translations/pt-BR.json index 0248fc94857..5768214c1a1 100644 --- a/homeassistant/components/plex/translations/pt-BR.json +++ b/homeassistant/components/plex/translations/pt-BR.json @@ -2,6 +2,25 @@ "config": { "abort": { "non-interactive": "Importa\u00e7\u00e3o n\u00e3o interativa" + }, + "error": { + "ssl_error": "Problema no certificado SSL" + }, + "flow_title": "{name} ({host})", + "step": { + "manual_setup": { + "data": { + "ssl": "Usar SSL", + "token": "Token (Opcional)", + "verify_ssl": "Verifique o certificado SSL" + }, + "title": "Configura\u00e7\u00e3o manual do Plex" + }, + "user_advanced": { + "data": { + "setup_method": "M\u00e9todo de configura\u00e7\u00e3o" + } + } } }, "options": { diff --git a/homeassistant/components/plex/translations/ru.json b/homeassistant/components/plex/translations/ru.json index 8c08d3df703..dbf2e6ab63f 100644 --- a/homeassistant/components/plex/translations/ru.json +++ b/homeassistant/components/plex/translations/ru.json @@ -35,10 +35,6 @@ "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0434\u0438\u043d \u0438\u0437 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u0432:", "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 Plex" }, - "start_website_auth": { - "description": "\u041f\u0440\u043e\u0439\u0434\u0438\u0442\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044e \u043d\u0430 plex.tv.", - "title": "Plex" - }, "user": { "description": "\u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 [plex.tv](https://plex.tv), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u043a Home Assistant.", "title": "Plex Media Server" diff --git a/homeassistant/components/plex/translations/sl.json b/homeassistant/components/plex/translations/sl.json index 0d5fb88aae2..36471b43df8 100644 --- a/homeassistant/components/plex/translations/sl.json +++ b/homeassistant/components/plex/translations/sl.json @@ -34,10 +34,6 @@ "description": "Na voljo je ve\u010d stre\u017enikov, izberite enega:", "title": "Izberite stre\u017enik Plex" }, - "start_website_auth": { - "description": "Nadaljujte z avtorizacijo na plex.tv.", - "title": "Pove\u017eite stre\u017enik Plex" - }, "user": { "description": "Nadaljujte do [plex.tv] (https://plex.tv), da pove\u017eete stre\u017enik Plex.", "title": "Plex medijski stre\u017enik" diff --git a/homeassistant/components/plex/translations/sv.json b/homeassistant/components/plex/translations/sv.json index 61490e36c82..ef0af0c7090 100644 --- a/homeassistant/components/plex/translations/sv.json +++ b/homeassistant/components/plex/translations/sv.json @@ -22,10 +22,6 @@ "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": { "title": "Plex Media Server" }, diff --git a/homeassistant/components/plex/translations/zh-Hant.json b/homeassistant/components/plex/translations/zh-Hant.json index 4efd0fb99b6..7e5ced6e034 100644 --- a/homeassistant/components/plex/translations/zh-Hant.json +++ b/homeassistant/components/plex/translations/zh-Hant.json @@ -35,10 +35,6 @@ "description": "\u627e\u5230\u591a\u500b\u4f3a\u670d\u5668\uff0c\u8acb\u9078\u64c7\u4e00\u7d44\uff1a", "title": "\u9078\u64c7 Plex \u4f3a\u670d\u5668" }, - "start_website_auth": { - "description": "\u7e7c\u7e8c\u65bc Plex.tv \u9032\u884c\u8a8d\u8b49\u3002", - "title": "\u9023\u7dda\u81f3 Plex \u4f3a\u670d\u5668" - }, "user": { "description": "\u7e7c\u7e8c\u81f3 [plex.tv](https://plex.tv) \u4ee5\u9023\u7d50\u4e00\u7d44 Plex \u4f3a\u670d\u5668\u3002", "title": "Plex \u5a92\u9ad4\u4f3a\u670d\u5668" diff --git a/homeassistant/components/plugwise/__init__.py b/homeassistant/components/plugwise/__init__.py index 489e7d3f496..a0b98f9d1c0 100644 --- a/homeassistant/components/plugwise/__init__.py +++ b/homeassistant/components/plugwise/__init__.py @@ -1 +1,204 @@ -"""Plugwise Climate (current only Anna) component for Home Assistant.""" +"""Plugwise platform for Home Assistant Core.""" + +import asyncio +from datetime import timedelta +import logging +from typing import Dict + +from Plugwise_Smile.Smile import Smile +import async_timeout +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN + +CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) + +_LOGGER = logging.getLogger(__name__) + +SENSOR_PLATFORMS = ["sensor"] +ALL_PLATFORMS = ["binary_sensor", "climate", "sensor", "switch"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Plugwise platform.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Plugwise Smiles from a config entry.""" + websession = async_get_clientsession(hass, verify_ssl=False) + api = Smile( + host=entry.data["host"], password=entry.data["password"], websession=websession + ) + + try: + connected = await api.connect() + + if not connected: + _LOGGER.error("Unable to connect to Smile") + raise ConfigEntryNotReady + + except Smile.InvalidAuthentication: + _LOGGER.error("Invalid Smile ID") + return False + + except Smile.PlugwiseError: + _LOGGER.error("Error while communicating to device") + raise ConfigEntryNotReady + + except asyncio.TimeoutError: + _LOGGER.error("Timeout while connecting to Smile") + raise ConfigEntryNotReady + + if api.smile_type == "power": + update_interval = timedelta(seconds=10) + else: + update_interval = timedelta(seconds=60) + + async def async_update_data(): + """Update data via API endpoint.""" + try: + async with async_timeout.timeout(10): + await api.full_update_device() + return True + except Smile.XMLDataMissingError: + raise UpdateFailed("Smile update failed") + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="Smile", + update_method=async_update_data, + update_interval=update_interval, + ) + + await coordinator.async_refresh() + + if not coordinator.last_update_success: + raise ConfigEntryNotReady + + api.get_all_devices() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { + "api": api, + "coordinator": coordinator, + } + + device_registry = await dr.async_get_registry(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, api.gateway_id)}, + manufacturer="Plugwise", + name=entry.title, + model=f"Smile {api.smile_name}", + sw_version=api.smile_version[0], + ) + + platforms = ALL_PLATFORMS + + single_master_thermostat = api.single_master_thermostat() + if single_master_thermostat is None: + platforms = SENSOR_PLATFORMS + + for component in platforms: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in ALL_PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +class SmileGateway(Entity): + """Represent Smile Gateway.""" + + def __init__(self, api, coordinator, name, dev_id): + """Initialise the gateway.""" + self._api = api + self._coordinator = coordinator + self._name = name + self._dev_id = dev_id + + self._unique_id = None + self._model = None + + self._entity_name = self._name + + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id + + @property + def should_poll(self): + """Return False, updates are controlled via coordinator.""" + return False + + @property + def available(self): + """Return True if entity is available.""" + return self._coordinator.last_update_success + + @property + def name(self): + """Return the name of the entity, if any.""" + if not self._name: + return None + return self._name + + @property + def device_info(self) -> Dict[str, any]: + """Return the device information.""" + + device_information = { + "identifiers": {(DOMAIN, self._dev_id)}, + "name": self._entity_name, + "manufacturer": "Plugwise", + } + + if self._model is not None: + device_information["model"] = self._model.replace("_", " ").title() + + if self._dev_id != self._api.gateway_id: + device_information["via_device"] = (DOMAIN, self._api.gateway_id) + + return device_information + + async def async_added_to_hass(self): + """Subscribe to updates.""" + self._async_process_data() + self.async_on_remove( + self._coordinator.async_add_listener(self._async_process_data) + ) + + @callback + def _async_process_data(self): + """Interpret and process API data.""" + raise NotImplementedError + + async def async_update(self): + """Update the entity.""" + await self._coordinator.async_request_refresh() diff --git a/homeassistant/components/plugwise/binary_sensor.py b/homeassistant/components/plugwise/binary_sensor.py new file mode 100644 index 00000000000..a2156cd37f9 --- /dev/null +++ b/homeassistant/components/plugwise/binary_sensor.py @@ -0,0 +1,96 @@ +"""Plugwise Binary Sensor component for Home Assistant.""" + +import logging + +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import callback + +from .const import DOMAIN, FLAME_ICON, FLOW_OFF_ICON, FLOW_ON_ICON, IDLE_ICON +from .sensor import SmileSensor + +BINARY_SENSOR_MAP = { + "dhw_state": ["Domestic Hot Water State", None], + "slave_boiler_state": ["Secondary Heater Device State", None], +} + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Smile binary_sensors from a config entry.""" + api = hass.data[DOMAIN][config_entry.entry_id]["api"] + coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] + + entities = [] + + all_devices = api.get_all_devices() + for dev_id, device_properties in all_devices.items(): + if device_properties["class"] == "heater_central": + data = api.get_device_data(dev_id) + for binary_sensor, dummy in BINARY_SENSOR_MAP.items(): + if binary_sensor in data: + entities.append( + PwBinarySensor( + api, + coordinator, + device_properties["name"], + binary_sensor, + dev_id, + device_properties["class"], + ) + ) + + async_add_entities(entities, True) + + +class PwBinarySensor(SmileSensor, BinarySensorEntity): + """Representation of a Plugwise binary_sensor.""" + + def __init__(self, api, coordinator, name, binary_sensor, dev_id, model): + """Set up the Plugwise API.""" + super().__init__(api, coordinator, name, dev_id, binary_sensor) + + self._binary_sensor = binary_sensor + + self._is_on = False + self._icon = None + + self._unique_id = f"{dev_id}-{binary_sensor}" + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._is_on + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return self._icon + + @callback + def _async_process_data(self): + """Update the entity.""" + data = self._api.get_device_data(self._dev_id) + + if not data: + _LOGGER.error("Received no data for device %s.", self._binary_sensor) + self.async_write_ha_state() + return + + if self._binary_sensor in data: + self._is_on = data[self._binary_sensor] + + self._state = STATE_OFF + if self._binary_sensor == "dhw_state": + self._icon = FLOW_OFF_ICON + if self._binary_sensor == "slave_boiler_state": + self._icon = IDLE_ICON + if self._is_on: + self._state = STATE_ON + if self._binary_sensor == "dhw_state": + self._icon = FLOW_ON_ICON + if self._binary_sensor == "slave_boiler_state": + self._icon = FLAME_ICON + + self.async_write_ha_state() diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index 8e2e525217a..42d4aa462b6 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -2,10 +2,9 @@ import logging -import haanna -import voluptuous as vol +from Plugwise_Smile.Smile import Smile -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, @@ -13,129 +12,105 @@ from homeassistant.components.climate.const import ( HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, - HVAC_MODE_OFF, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.const import ( - ATTR_TEMPERATURE, - CONF_HOST, - CONF_NAME, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, - TEMP_CELSIUS, -) -from homeassistant.exceptions import PlatformNotReady -import homeassistant.helpers.config_validation as cv +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.core import callback + +from . import SmileGateway +from .const import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN, SCHEDULE_OFF, SCHEDULE_ON + +HVAC_MODES_HEAT_ONLY = [HVAC_MODE_HEAT, HVAC_MODE_AUTO] +HVAC_MODES_HEAT_COOL = [HVAC_MODE_HEAT_COOL, HVAC_MODE_AUTO] SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE _LOGGER = logging.getLogger(__name__) -# Configuration directives -CONF_MIN_TEMP = "min_temp" -CONF_MAX_TEMP = "max_temp" -CONF_LEGACY = "legacy_anna" -# Default directives -DEFAULT_NAME = "Plugwise Thermostat" -DEFAULT_USERNAME = "smile" -DEFAULT_TIMEOUT = 10 -DEFAULT_PORT = 80 -DEFAULT_ICON = "mdi:thermometer" -DEFAULT_MIN_TEMP = 4 -DEFAULT_MAX_TEMP = 30 +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Smile Thermostats from a config entry.""" + api = hass.data[DOMAIN][config_entry.entry_id]["api"] + coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] -# HVAC modes -HVAC_MODES_1 = [HVAC_MODE_HEAT, HVAC_MODE_AUTO] -HVAC_MODES_2 = [HVAC_MODE_HEAT_COOL, HVAC_MODE_AUTO] - -# Read platform configuration -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_LEGACY, default=False): cv.boolean, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, - vol.Optional(CONF_MIN_TEMP, default=DEFAULT_MIN_TEMP): cv.positive_int, - vol.Optional(CONF_MAX_TEMP, default=DEFAULT_MAX_TEMP): cv.positive_int, - } -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Add the Plugwise (Anna) Thermostat.""" - api = haanna.Haanna( - config[CONF_USERNAME], - config[CONF_PASSWORD], - config[CONF_HOST], - config[CONF_PORT], - config[CONF_LEGACY], - ) - try: - api.ping_anna_thermostat() - except OSError: - _LOGGER.debug("Ping failed, retrying later", exc_info=True) - raise PlatformNotReady - devices = [ - ThermostatDevice( - api, config[CONF_NAME], config[CONF_MIN_TEMP], config[CONF_MAX_TEMP] - ) + entities = [] + thermostat_classes = [ + "thermostat", + "zone_thermostat", + "thermostatic_radiator_valve", ] - add_entities(devices, True) + all_devices = api.get_all_devices() + + for dev_id, device_properties in all_devices.items(): + + if device_properties["class"] not in thermostat_classes: + continue + + thermostat = PwThermostat( + api, + coordinator, + device_properties["name"], + dev_id, + device_properties["location"], + device_properties["class"], + DEFAULT_MIN_TEMP, + DEFAULT_MAX_TEMP, + ) + + entities.append(thermostat) + + async_add_entities(entities, True) -class ThermostatDevice(ClimateEntity): - """Representation of the Plugwise thermostat.""" +class PwThermostat(SmileGateway, ClimateEntity): + """Representation of an Plugwise thermostat.""" - def __init__(self, api, name, min_temp, max_temp): + def __init__( + self, api, coordinator, name, dev_id, loc_id, model, min_temp, max_temp + ): """Set up the Plugwise API.""" + super().__init__(api, coordinator, name, dev_id) + self._api = api + self._loc_id = loc_id + self._model = model self._min_temp = min_temp self._max_temp = max_temp - self._name = name - self._direct_objects = None - self._domain_objects = None - self._outdoor_temperature = None + self._selected_schema = None self._last_active_schema = None self._preset_mode = None self._presets = None self._presets_list = None - self._boiler_status = None - self._heating_status = None - self._cooling_status = None - self._dhw_status = None + self._heating_state = None + self._cooling_state = None + self._compressor_state = None + self._dhw_state = None + self._hvac_mode = None self._schema_names = None self._schema_status = None - self._current_temperature = None - self._thermostat_temperature = None - self._boiler_temperature = None + self._temperature = None + self._setpoint = None self._water_pressure = None - self._schedule_temperature = None + self._schedule_temp = None self._hvac_mode = None + self._single_thermostat = self._api.single_master_thermostat() + self._unique_id = f"{dev_id}-climate" @property def hvac_action(self): - """Return the current hvac action.""" - if self._heating_status or self._boiler_status or self._dhw_status: - return CURRENT_HVAC_HEAT - if self._cooling_status: - return CURRENT_HVAC_COOL - return CURRENT_HVAC_IDLE - - @property - def name(self): - """Return the name of the thermostat, if any.""" - return self._name - - @property - def icon(self): - """Return the icon to use in the frontend.""" - return DEFAULT_ICON + """Return the current action.""" + if self._single_thermostat: + if self._heating_state: + return CURRENT_HVAC_HEAT + if self._cooling_state: + return CURRENT_HVAC_COOL + return CURRENT_HVAC_IDLE + if self._heating_state is not None: + if self._setpoint > self._temperature: + return CURRENT_HVAC_HEAT + return CURRENT_HVAC_IDLE @property def supported_features(self): @@ -146,82 +121,47 @@ class ThermostatDevice(ClimateEntity): def device_state_attributes(self): """Return the device specific state attributes.""" attributes = {} - if self._outdoor_temperature: - attributes["outdoor_temperature"] = self._outdoor_temperature if self._schema_names: - attributes["available_schemas"] = self._schema_names + if len(self._schema_names) > 1: + attributes["available_schemas"] = self._schema_names if self._selected_schema: attributes["selected_schema"] = self._selected_schema - if self._boiler_temperature: - attributes["boiler_temperature"] = self._boiler_temperature - if self._water_pressure: - attributes["water_pressure"] = self._water_pressure return attributes @property def preset_modes(self): - """Return the available preset modes list. - - And make the presets with their temperatures available. - """ + """Return the available preset modes list.""" return self._presets_list @property def hvac_modes(self): """Return the available hvac modes list.""" - if self._heating_status is not None or self._boiler_status is not None: - if self._cooling_status is not None: - return HVAC_MODES_2 - return HVAC_MODES_1 - return None + if self._heating_state is not None: + if self._compressor_state is not None: + return HVAC_MODES_HEAT_COOL + return HVAC_MODES_HEAT_ONLY @property def hvac_mode(self): """Return current active hvac state.""" - if self._schema_status: - return HVAC_MODE_AUTO - if self._heating_status or self._boiler_status or self._dhw_status: - if self._cooling_status: - return HVAC_MODE_HEAT_COOL - return HVAC_MODE_HEAT - return HVAC_MODE_OFF + return self._hvac_mode @property def target_temperature(self): - """Return the target_temperature. - - From the XML the thermostat-value is used because it updates 'immediately' - compared to the target_temperature-value. This way the information on the card - is "immediately" updated after changing the preset, temperature, etc. - """ - return self._thermostat_temperature + """Return the target_temperature.""" + return self._setpoint @property def preset_mode(self): - """Return the active selected schedule-name. - - Or, return the active preset, or return Temporary in case of a manual change - in the set-temperature with a weekschedule active. - Or return Manual in case of a manual change and no weekschedule active. - """ + """Return the active preset.""" if self._presets: - presets = self._presets - preset_temperature = presets.get(self._preset_mode, "none") - if self.hvac_mode == HVAC_MODE_AUTO: - if self._thermostat_temperature == self._schedule_temperature: - return f"{self._selected_schema}" - if self._thermostat_temperature == preset_temperature: - return self._preset_mode - return "Temporary" - if self._thermostat_temperature != preset_temperature: - return "Manual" return self._preset_mode return None @property def current_temperature(self): """Return the current room temperature.""" - return self._current_temperature + return self._temperature @property def min_temp(self): @@ -238,62 +178,91 @@ class ThermostatDevice(ClimateEntity): """Return the unit of measured temperature.""" return TEMP_CELSIUS - def set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs): """Set new target temperature.""" - _LOGGER.debug("Adjusting temperature") temperature = kwargs.get(ATTR_TEMPERATURE) - if temperature is not None and self._min_temp < temperature < self._max_temp: - _LOGGER.debug("Changing temporary temperature") - self._api.set_temperature(self._domain_objects, temperature) + if (temperature is not None) and ( + self._min_temp < temperature < self._max_temp + ): + try: + await self._api.set_temperature(self._loc_id, temperature) + self._setpoint = temperature + self.async_write_ha_state() + except Smile.PlugwiseError: + _LOGGER.error("Error while communicating to device") else: _LOGGER.error("Invalid temperature requested") - def set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode): """Set the hvac mode.""" - _LOGGER.debug("Adjusting hvac_mode (i.e. schedule/schema)") - schema_mode = "false" + state = SCHEDULE_OFF if hvac_mode == HVAC_MODE_AUTO: - schema_mode = "true" - self._api.set_schema_state( - self._domain_objects, self._last_active_schema, schema_mode - ) + state = SCHEDULE_ON + try: + await self._api.set_temperature(self._loc_id, self._schedule_temp) + self._setpoint = self._schedule_temp + except Smile.PlugwiseError: + _LOGGER.error("Error while communicating to device") + try: + await self._api.set_schedule_state( + self._loc_id, self._last_active_schema, state + ) + self._hvac_mode = hvac_mode + self.async_write_ha_state() + except Smile.PlugwiseError: + _LOGGER.error("Error while communicating to device") - def set_preset_mode(self, preset_mode): + async def async_set_preset_mode(self, preset_mode): """Set the preset mode.""" - _LOGGER.debug("Changing preset mode") - self._api.set_preset(self._domain_objects, preset_mode) + try: + await self._api.set_preset(self._loc_id, preset_mode) + self._preset_mode = preset_mode + self._setpoint = self._presets.get(self._preset_mode, "none")[0] + self.async_write_ha_state() + except Smile.PlugwiseError: + _LOGGER.error("Error while communicating to device") - def update(self): - """Update the data from the thermostat.""" - _LOGGER.debug("Update called") - self._direct_objects = self._api.get_direct_objects() - self._domain_objects = self._api.get_domain_objects() - self._outdoor_temperature = self._api.get_outdoor_temperature( - self._domain_objects - ) - self._selected_schema = self._api.get_active_schema_name(self._domain_objects) - self._last_active_schema = self._api.get_last_active_schema_name( - self._domain_objects - ) - self._preset_mode = self._api.get_current_preset(self._domain_objects) - self._presets = self._api.get_presets(self._domain_objects) - self._presets_list = list(self._api.get_presets(self._domain_objects)) - self._boiler_status = self._api.get_boiler_status(self._direct_objects) - self._heating_status = self._api.get_heating_status(self._direct_objects) - self._cooling_status = self._api.get_cooling_status(self._direct_objects) - self._dhw_status = self._api.get_domestic_hot_water_status(self._direct_objects) - self._schema_names = self._api.get_schema_names(self._domain_objects) - self._schema_status = self._api.get_schema_state(self._domain_objects) - self._current_temperature = self._api.get_current_temperature( - self._domain_objects - ) - self._thermostat_temperature = self._api.get_thermostat_temperature( - self._domain_objects - ) - self._schedule_temperature = self._api.get_schedule_temperature( - self._domain_objects - ) - self._boiler_temperature = self._api.get_boiler_temperature( - self._domain_objects - ) - self._water_pressure = self._api.get_water_pressure(self._domain_objects) + @callback + def _async_process_data(self): + """Update the data for this climate device.""" + climate_data = self._api.get_device_data(self._dev_id) + heater_central_data = self._api.get_device_data(self._api.heater_id) + + if "setpoint" in climate_data: + self._setpoint = climate_data["setpoint"] + if "temperature" in climate_data: + self._temperature = climate_data["temperature"] + if "schedule_temperature" in climate_data: + self._schedule_temp = climate_data["schedule_temperature"] + if "available_schedules" in climate_data: + self._schema_names = climate_data["available_schedules"] + if "selected_schedule" in climate_data: + self._selected_schema = climate_data["selected_schedule"] + if self._selected_schema is not None: + self._schema_status = True + else: + self._schema_status = False + if "last_used" in climate_data: + self._last_active_schema = climate_data["last_used"] + if "presets" in climate_data: + self._presets = climate_data["presets"] + if self._presets: + self._presets_list = list(self._presets) + if "active_preset" in climate_data: + self._preset_mode = climate_data["active_preset"] + + if heater_central_data.get("heating_state") is not None: + self._heating_state = heater_central_data["heating_state"] + if heater_central_data.get("cooling_state") is not None: + self._cooling_state = heater_central_data["cooling_state"] + if heater_central_data.get("compressor_state") is not None: + self._compressor_state = heater_central_data["compressor_state"] + + if self._schema_status: + self._hvac_mode = HVAC_MODE_AUTO + elif self._heating_state is not None: + self._hvac_mode = HVAC_MODE_HEAT + if self._compressor_state is not None: + self._hvac_mode = HVAC_MODE_HEAT_COOL + + self.async_write_ha_state() diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py new file mode 100644 index 00000000000..8f82a107576 --- /dev/null +++ b/homeassistant/components/plugwise/config_flow.py @@ -0,0 +1,81 @@ +"""Config flow for Plugwise integration.""" +import logging + +from Plugwise_Smile.Smile import Smile +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema( + {vol.Required(CONF_HOST): str, vol.Required(CONF_PASSWORD): str} +) + + +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. + """ + websession = async_get_clientsession(hass, verify_ssl=False) + api = Smile( + host=data["host"], password=data["password"], timeout=30, websession=websession + ) + + try: + await api.connect() + except Smile.InvalidAuthentication: + raise InvalidAuth + except Smile.ConnectionFailedError: + raise CannotConnect + + return api + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Plugwise Smile.""" + + 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: + api = await validate_input(self.hass, user_input) + + return self.async_create_entry(title=api.smile_name, data=user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + if not errors: + await self.async_set_unique_id(api.gateway_id) + self._abort_if_unique_id_configured() + + return self.async_create_entry(title=api.smile_name, 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.""" diff --git a/homeassistant/components/plugwise/const.py b/homeassistant/components/plugwise/const.py new file mode 100644 index 00000000000..9dc4c24b1e1 --- /dev/null +++ b/homeassistant/components/plugwise/const.py @@ -0,0 +1,41 @@ +"""Constant for Plugwise component.""" +DOMAIN = "plugwise" + +# Sensor mapping +SENSOR_MAP_MODEL = 0 +SENSOR_MAP_UOM = 1 +SENSOR_MAP_DEVICE_CLASS = 2 + +# Default directives +DEFAULT_NAME = "Smile" +DEFAULT_USERNAME = "smile" +DEFAULT_TIMEOUT = 10 +DEFAULT_PORT = 80 +DEFAULT_MIN_TEMP = 4 +DEFAULT_MAX_TEMP = 30 +DEFAULT_SCAN_INTERVAL = {"thermostat": 60, "power": 10} + +# Configuration directives +CONF_MIN_TEMP = "min_temp" +CONF_MAX_TEMP = "max_temp" +CONF_THERMOSTAT = "thermostat" +CONF_POWER = "power" +CONF_HEATER = "heater" +CONF_SOLAR = "solar" +CONF_GAS = "gas" + +ATTR_ILLUMINANCE = "illuminance" +UNIT_LUMEN = "lm" + +CURRENT_HVAC_DHW = "hot_water" + +DEVICE_STATE = "device_state" + +SCHEDULE_ON = "true" +SCHEDULE_OFF = "false" + +COOL_ICON = "mdi:snowflake" +FLAME_ICON = "mdi:fire" +IDLE_ICON = "mdi:circle-off-outline" +FLOW_OFF_ICON = "mdi:water-pump-off" +FLOW_ON_ICON = "mdi:water-pump" diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index 55e43c2a29f..fa4cf32a2ec 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -1,7 +1,8 @@ { "domain": "plugwise", - "name": "Plugwise Anna", + "name": "Plugwise", "documentation": "https://www.home-assistant.io/integrations/plugwise", - "codeowners": ["@laetificat", "@CoMPaTech", "@bouwew"], - "requirements": ["haanna==0.15.0"] + "requirements": ["Plugwise_Smile==0.2.13"], + "codeowners": ["@CoMPaTech", "@bouwew"], + "config_flow": true } diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py new file mode 100644 index 00000000000..eabb5c6655f --- /dev/null +++ b/homeassistant/components/plugwise/sensor.py @@ -0,0 +1,354 @@ +"""Plugwise Sensor component for Home Assistant.""" + +import logging + +from homeassistant.const import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + ENERGY_KILO_WATT_HOUR, + ENERGY_WATT_HOUR, + POWER_WATT, + PRESSURE_BAR, + TEMP_CELSIUS, + UNIT_PERCENTAGE, + VOLUME_CUBIC_METERS, +) +from homeassistant.core import callback +from homeassistant.helpers.entity import Entity + +from . import SmileGateway +from .const import ( + COOL_ICON, + DEVICE_STATE, + DOMAIN, + FLAME_ICON, + IDLE_ICON, + SENSOR_MAP_DEVICE_CLASS, + SENSOR_MAP_MODEL, + SENSOR_MAP_UOM, + UNIT_LUMEN, +) + +_LOGGER = logging.getLogger(__name__) + +ATTR_TEMPERATURE = [ + "Temperature", + TEMP_CELSIUS, + DEVICE_CLASS_TEMPERATURE, +] +ATTR_BATTERY_LEVEL = [ + "Charge", + UNIT_PERCENTAGE, + DEVICE_CLASS_BATTERY, +] +ATTR_ILLUMINANCE = [ + "Illuminance", + UNIT_LUMEN, + DEVICE_CLASS_ILLUMINANCE, +] +ATTR_PRESSURE = ["Pressure", PRESSURE_BAR, DEVICE_CLASS_PRESSURE] + +TEMP_SENSOR_MAP = { + "setpoint": ATTR_TEMPERATURE, + "temperature": ATTR_TEMPERATURE, + "intended_boiler_temperature": ATTR_TEMPERATURE, + "temperature_difference": ATTR_TEMPERATURE, + "outdoor_temperature": ATTR_TEMPERATURE, + "water_temperature": ATTR_TEMPERATURE, + "return_temperature": ATTR_TEMPERATURE, +} + +ENERGY_SENSOR_MAP = { + "electricity_consumed": ["Current Consumed Power", POWER_WATT, DEVICE_CLASS_POWER], + "electricity_produced": ["Current Produced Power", POWER_WATT, DEVICE_CLASS_POWER], + "electricity_consumed_interval": [ + "Consumed Power Interval", + ENERGY_WATT_HOUR, + DEVICE_CLASS_POWER, + ], + "electricity_produced_interval": [ + "Produced Power Interval", + ENERGY_WATT_HOUR, + DEVICE_CLASS_POWER, + ], + "electricity_consumed_off_peak_point": [ + "Current Consumed Power (off peak)", + POWER_WATT, + DEVICE_CLASS_POWER, + ], + "electricity_consumed_peak_point": [ + "Current Consumed Power", + POWER_WATT, + DEVICE_CLASS_POWER, + ], + "electricity_consumed_off_peak_cumulative": [ + "Cumulative Consumed Power (off peak)", + ENERGY_KILO_WATT_HOUR, + DEVICE_CLASS_POWER, + ], + "electricity_consumed_peak_cumulative": [ + "Cumulative Consumed Power", + ENERGY_KILO_WATT_HOUR, + DEVICE_CLASS_POWER, + ], + "electricity_produced_off_peak_point": [ + "Current Consumed Power (off peak)", + POWER_WATT, + DEVICE_CLASS_POWER, + ], + "electricity_produced_peak_point": [ + "Current Consumed Power", + POWER_WATT, + DEVICE_CLASS_POWER, + ], + "electricity_produced_off_peak_cumulative": [ + "Cumulative Consumed Power (off peak)", + ENERGY_KILO_WATT_HOUR, + DEVICE_CLASS_POWER, + ], + "electricity_produced_peak_cumulative": [ + "Cumulative Consumed Power", + ENERGY_KILO_WATT_HOUR, + DEVICE_CLASS_POWER, + ], + "gas_consumed_interval": ["Current Consumed Gas", VOLUME_CUBIC_METERS, None], + "gas_consumed_cumulative": ["Cumulative Consumed Gas", VOLUME_CUBIC_METERS, None], + "net_electricity_point": ["Current net Power", POWER_WATT, DEVICE_CLASS_POWER], + "net_electricity_cumulative": [ + "Cumulative net Power", + ENERGY_KILO_WATT_HOUR, + DEVICE_CLASS_POWER, + ], +} + +MISC_SENSOR_MAP = { + "battery": ATTR_BATTERY_LEVEL, + "illuminance": ATTR_ILLUMINANCE, + "modulation_level": ["Heater Modulation Level", UNIT_PERCENTAGE, None], + "valve_position": ["Valve Position", UNIT_PERCENTAGE, None], + "water_pressure": ATTR_PRESSURE, +} + +INDICATE_ACTIVE_LOCAL_DEVICE = [ + "cooling_state", + "flame_state", +] + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Smile sensors from a config entry.""" + api = hass.data[DOMAIN][config_entry.entry_id]["api"] + coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] + + entities = [] + all_devices = api.get_all_devices() + single_thermostat = api.single_master_thermostat() + for dev_id, device_properties in all_devices.items(): + data = api.get_device_data(dev_id) + for sensor, sensor_type in { + **TEMP_SENSOR_MAP, + **ENERGY_SENSOR_MAP, + **MISC_SENSOR_MAP, + }.items(): + if sensor in data: + if data[sensor] is None: + continue + + if "power" in device_properties["types"]: + model = None + + if "plug" in device_properties["types"]: + model = "Metered Switch" + + entities.append( + PwPowerSensor( + api, + coordinator, + device_properties["name"], + dev_id, + sensor, + sensor_type, + model, + ) + ) + else: + entities.append( + PwThermostatSensor( + api, + coordinator, + device_properties["name"], + dev_id, + sensor, + sensor_type, + ) + ) + + if single_thermostat is False: + for state in INDICATE_ACTIVE_LOCAL_DEVICE: + if state in data: + entities.append( + PwAuxDeviceSensor( + api, + coordinator, + device_properties["name"], + dev_id, + DEVICE_STATE, + ) + ) + break + + async_add_entities(entities, True) + + +class SmileSensor(SmileGateway): + """Represent Smile Sensors.""" + + def __init__(self, api, coordinator, name, dev_id, sensor): + """Initialise the sensor.""" + super().__init__(api, coordinator, name, dev_id) + + self._sensor = sensor + + self._dev_class = None + self._state = None + self._unit_of_measurement = None + + if dev_id == self._api.heater_id: + self._entity_name = "Auxiliary" + + sensorname = sensor.replace("_", " ").title() + self._name = f"{self._entity_name} {sensorname}" + + if dev_id == self._api.gateway_id: + self._entity_name = f"Smile {self._entity_name}" + + self._unique_id = f"{dev_id}-{sensor}" + + @property + def device_class(self): + """Device class of this entity.""" + return self._dev_class + + @property + def state(self): + """Device class of this entity.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._unit_of_measurement + + +class PwThermostatSensor(SmileSensor, Entity): + """Thermostat and climate sensor entities.""" + + def __init__(self, api, coordinator, name, dev_id, sensor, sensor_type): + """Set up the Plugwise API.""" + super().__init__(api, coordinator, name, dev_id, sensor) + + self._model = sensor_type[SENSOR_MAP_MODEL] + self._unit_of_measurement = sensor_type[SENSOR_MAP_UOM] + self._dev_class = sensor_type[SENSOR_MAP_DEVICE_CLASS] + + @callback + def _async_process_data(self): + """Update the entity.""" + data = self._api.get_device_data(self._dev_id) + + if not data: + _LOGGER.error("Received no data for device %s.", self._entity_name) + self.async_write_ha_state() + return + + if data.get(self._sensor) is not None: + measurement = data[self._sensor] + if self._sensor == "battery" or self._sensor == "valve_position": + measurement = measurement * 100 + if self._unit_of_measurement == UNIT_PERCENTAGE: + measurement = int(measurement) + self._state = measurement + + self.async_write_ha_state() + + +class PwAuxDeviceSensor(SmileSensor, Entity): + """Auxiliary sensor entities for the heating/cooling device.""" + + def __init__(self, api, coordinator, name, dev_id, sensor): + """Set up the Plugwise API.""" + super().__init__(api, coordinator, name, dev_id, sensor) + + self._cooling_state = False + self._heating_state = False + self._icon = None + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return self._icon + + @callback + def _async_process_data(self): + """Update the entity.""" + data = self._api.get_device_data(self._dev_id) + + if not data: + _LOGGER.error("Received no data for device %s.", self._entity_name) + self.async_write_ha_state() + return + + if data.get("heating_state") is not None: + self._heating_state = data["heating_state"] + if data.get("cooling_state") is not None: + self._cooling_state = data["cooling_state"] + + self._state = "idle" + self._icon = IDLE_ICON + if self._heating_state: + self._state = "heating" + self._icon = FLAME_ICON + if self._cooling_state: + self._state = "cooling" + self._icon = COOL_ICON + + self.async_write_ha_state() + + +class PwPowerSensor(SmileSensor, Entity): + """Power sensor entities.""" + + def __init__(self, api, coordinator, name, dev_id, sensor, sensor_type, model): + """Set up the Plugwise API.""" + super().__init__(api, coordinator, name, dev_id, sensor) + + self._model = model + if model is None: + self._model = sensor_type[SENSOR_MAP_MODEL] + + self._unit_of_measurement = sensor_type[SENSOR_MAP_UOM] + self._dev_class = sensor_type[SENSOR_MAP_DEVICE_CLASS] + + if dev_id == self._api.gateway_id: + self._model = "P1 DSMR" + + @callback + def _async_process_data(self): + """Update the entity.""" + data = self._api.get_device_data(self._dev_id) + + if not data: + _LOGGER.error("Received no data for device %s.", self._entity_name) + self.async_write_ha_state() + return + + if data.get(self._sensor) is not None: + measurement = data[self._sensor] + if self._unit_of_measurement == ENERGY_KILO_WATT_HOUR: + measurement = int(measurement / 1000) + self._state = measurement + + self.async_write_ha_state() diff --git a/homeassistant/components/plugwise/strings.json b/homeassistant/components/plugwise/strings.json new file mode 100644 index 00000000000..00499a26ac2 --- /dev/null +++ b/homeassistant/components/plugwise/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "user": { + "title": "Connect to the Smile", + "description": "Details", + "data": { + "host": "Smile IP address", + "password": "Smile ID" + } + } + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication, check the 8 characters of your Smile ID", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "This Smile is already configured" + } + } +} diff --git a/homeassistant/components/plugwise/switch.py b/homeassistant/components/plugwise/switch.py new file mode 100644 index 00000000000..50b704e36ac --- /dev/null +++ b/homeassistant/components/plugwise/switch.py @@ -0,0 +1,84 @@ +"""Plugwise Switch component for HomeAssistant.""" + +import logging + +from Plugwise_Smile.Smile import Smile + +from homeassistant.components.switch import SwitchEntity +from homeassistant.core import callback + +from . import SmileGateway +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Smile switches from a config entry.""" + api = hass.data[DOMAIN][config_entry.entry_id]["api"] + coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] + + entities = [] + all_devices = api.get_all_devices() + for dev_id, device_properties in all_devices.items(): + if "plug" in device_properties["types"]: + model = "Metered Switch" + entities.append( + PwSwitch(api, coordinator, device_properties["name"], dev_id, model) + ) + + async_add_entities(entities, True) + + +class PwSwitch(SmileGateway, SwitchEntity): + """Representation of a Plugwise plug.""" + + def __init__(self, api, coordinator, name, dev_id, model): + """Set up the Plugwise API.""" + super().__init__(api, coordinator, name, dev_id) + + self._model = model + + self._is_on = False + + self._unique_id = f"{dev_id}-plug" + + @property + def is_on(self): + """Return true if device is on.""" + return self._is_on + + async def async_turn_on(self, **kwargs): + """Turn the device on.""" + try: + if await self._api.set_relay_state(self._dev_id, "on"): + self._is_on = True + self.async_write_ha_state() + except Smile.PlugwiseError: + _LOGGER.error("Error while communicating to device") + + async def async_turn_off(self, **kwargs): + """Turn the device off.""" + try: + if await self._api.set_relay_state(self._dev_id, "off"): + self._is_on = False + self.async_write_ha_state() + except Smile.PlugwiseError: + _LOGGER.error("Error while communicating to device") + + @callback + def _async_process_data(self): + """Update the data from the Plugs.""" + _LOGGER.debug("Update switch called") + + data = self._api.get_device_data(self._dev_id) + + if not data: + _LOGGER.error("Received no data for device %s.", self._name) + self.async_write_ha_state() + return + + if "relay" in data: + self._is_on = data["relay"] + + self.async_write_ha_state() diff --git a/homeassistant/components/plugwise/translations/ca.json b/homeassistant/components/plugwise/translations/ca.json new file mode 100644 index 00000000000..46acd2a352f --- /dev/null +++ b/homeassistant/components/plugwise/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Smile ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "No s'ha pogut connectar, torna-ho a provar", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida, comprova els 8 car\u00e0cters de l'ID de Smile.", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Adre\u00e7a IP de Smile", + "password": "ID de Smile" + }, + "description": "Detalls", + "title": "Connecta\u2019t amb el Smile" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/en.json b/homeassistant/components/plugwise/translations/en.json new file mode 100644 index 00000000000..8d249e20ed4 --- /dev/null +++ b/homeassistant/components/plugwise/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "This Smile is already configured" + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication, check the 8 characters of your Smile ID", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Smile IP address", + "password": "Smile ID" + }, + "description": "Details", + "title": "Connect to the Smile" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/es.json b/homeassistant/components/plugwise/translations/es.json new file mode 100644 index 00000000000..c73deaf0853 --- /dev/null +++ b/homeassistant/components/plugwise/translations/es.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Este Smile ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntelo de nuevo.", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida, comprueba los 8 caracteres de tu Smile ID", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "host": "Direcci\u00f3n IP de Smile", + "password": "ID Smile" + }, + "description": "Detalles", + "title": "Conectarse a Smile" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/it.json b/homeassistant/components/plugwise/translations/it.json new file mode 100644 index 00000000000..96eceaaf7b2 --- /dev/null +++ b/homeassistant/components/plugwise/translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Smile gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi, si prega di riprovare", + "invalid_auth": "Autenticazione non valida. Controllare gli 8 caratterei dell'ID Smile", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Indirizzo IP Smile", + "password": "ID Smile" + }, + "description": "Dettagli", + "title": "Connettersi al dispositivo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/ko.json b/homeassistant/components/plugwise/translations/ko.json new file mode 100644 index 00000000000..fdc189ab38f --- /dev/null +++ b/homeassistant/components/plugwise/translations/ko.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\uc774 Smile \uc740 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. 8\uc790\uc758 Smile ID \ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "host": "Smile IP \uc8fc\uc18c", + "password": "Smile ID" + }, + "description": "\uc138\ubd80 \uc815\ubcf4", + "title": "Smile \uc5d0 \uc5f0\uacb0\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/nl.json b/homeassistant/components/plugwise/translations/nl.json new file mode 100644 index 00000000000..964675e0c63 --- /dev/null +++ b/homeassistant/components/plugwise/translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Deze Smile is al geconfigureerd" + }, + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "invalid_auth": "Ongeldige authenticatie, controleer de 8 karakters van uw Smile-ID", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "host": "Smile IP-adres", + "password": "Smile-ID" + }, + "description": "Details", + "title": "Maak verbinding met de Smile" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/no.json b/homeassistant/components/plugwise/translations/no.json new file mode 100644 index 00000000000..8205a7dab24 --- /dev/null +++ b/homeassistant/components/plugwise/translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Denne Smile-enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen", + "invalid_auth": "Ugyldig godkjenning, sjekk din 8-tegns Smile ID", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Smile IP-adresse", + "password": "" + }, + "description": "Detaljer", + "title": "Koble til Smile" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/pl.json b/homeassistant/components/plugwise/translations/pl.json new file mode 100644 index 00000000000..8fc4d27abbf --- /dev/null +++ b/homeassistant/components/plugwise/translations/pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "TenSmile jest ju\u017c skonfigurowany." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", + "invalid_auth": "Nieudane uwierzytelnienie, sprawd\u017a Smile ID", + "unknown": "Nieoczekiwany b\u0142\u0105d." + }, + "step": { + "user": { + "data": { + "host": "Adres IP Smile", + "password": "Smile ID" + }, + "description": "Szczeg\u00f3\u0142y", + "title": "Po\u0142\u0105cz si\u0119 ze Smile" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/ru.json b/homeassistant/components/plugwise/translations/ru.json new file mode 100644 index 00000000000..b500247cfdf --- /dev/null +++ b/homeassistant/components/plugwise/translations/ru.json @@ -0,0 +1,22 @@ +{ + "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\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 ID \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." + }, + "step": { + "user": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441", + "password": "ID \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + }, + "description": "\u041f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438", + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/zh-Hant.json b/homeassistant/components/plugwise/translations/zh-Hant.json new file mode 100644 index 00000000000..b35fcea1508 --- /dev/null +++ b/homeassistant/components/plugwise/translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Smile \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", + "invalid_auth": "\u9a57\u8b49\u7121\u6548\uff0c\u8acb\u6aa2\u67e5\u6240\u8f38\u5165\u7684 Smile ID 8 \u4f4d\u5b57\u5143", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "Smile IP \u4f4d\u5740", + "password": "Smile ID" + }, + "description": "\u8a73\u7d30\u8cc7\u8a0a", + "title": "\u9023\u7dda\u81f3 Smile" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index 2d6f39feb11..ba6c621dbd6 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -7,7 +7,12 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_TOKEN, CONF_WEBHOOK_ID +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_TOKEN, + CONF_WEBHOOK_ID, +) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -31,9 +36,6 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -CONF_CLIENT_ID = "client_id" -CONF_CLIENT_SECRET = "client_secret" - DATA_CONFIG_ENTRY_LOCK = "point_config_entry_lock" CONFIG_ENTRY_IS_SETUP = "point_config_entry_is_setup" @@ -82,7 +84,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): # Force token update. entry.data[CONF_TOKEN]["expires_in"] = -1 session = PointSession( - entry.data["refresh_args"]["client_id"], + entry.data["refresh_args"][CONF_CLIENT_ID], token=entry.data[CONF_TOKEN], auto_refresh_kwargs=entry.data["refresh_args"], token_saver=token_saver, diff --git a/homeassistant/components/point/config_flow.py b/homeassistant/components/point/config_flow.py index 3312931085e..5a343f276a7 100644 --- a/homeassistant/components/point/config_flow.py +++ b/homeassistant/components/point/config_flow.py @@ -9,9 +9,10 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.http import HomeAssistantView +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import callback -from .const import CLIENT_ID, CLIENT_SECRET, DOMAIN +from .const import DOMAIN AUTH_CALLBACK_PATH = "/api/minut" AUTH_CALLBACK_NAME = "api:minut" @@ -34,8 +35,8 @@ def register_flow_implementation(hass, domain, client_id, client_secret): hass.data[DATA_FLOW_IMPL] = OrderedDict() hass.data[DATA_FLOW_IMPL][domain] = { - CLIENT_ID: client_id, - CLIENT_SECRET: client_secret, + CONF_CLIENT_ID: client_id, + CONF_CLIENT_SECRET: client_secret, } @@ -112,8 +113,8 @@ class PointFlowHandler(config_entries.ConfigFlow): """Create Minut Point session and get authorization url.""" flow = self.hass.data[DATA_FLOW_IMPL][self.flow_impl] - client_id = flow[CLIENT_ID] - client_secret = flow[CLIENT_SECRET] + client_id = flow[CONF_CLIENT_ID] + client_secret = flow[CONF_CLIENT_SECRET] point_session = PointSession(client_id, client_secret=client_secret) self.hass.http.register_view(MinutAuthCallbackView()) @@ -140,8 +141,8 @@ class PointFlowHandler(config_entries.ConfigFlow): """Create point session and entries.""" flow = self.hass.data[DATA_FLOW_IMPL][DOMAIN] - client_id = flow[CLIENT_ID] - client_secret = flow[CLIENT_SECRET] + client_id = flow[CONF_CLIENT_ID] + client_secret = flow[CONF_CLIENT_SECRET] point_session = PointSession(client_id, client_secret=client_secret) token = await self.hass.async_add_executor_job( point_session.get_access_token, code @@ -159,8 +160,8 @@ class PointFlowHandler(config_entries.ConfigFlow): data={ "token": token, "refresh_args": { - "client_id": client_id, - "client_secret": client_secret, + CONF_CLIENT_ID: client_id, + CONF_CLIENT_SECRET: client_secret, }, }, ) diff --git a/homeassistant/components/point/const.py b/homeassistant/components/point/const.py index 5e78f7ae24e..c21971185f9 100644 --- a/homeassistant/components/point/const.py +++ b/homeassistant/components/point/const.py @@ -2,9 +2,6 @@ from datetime import timedelta DOMAIN = "point" -CLIENT_ID = "client_id" -CLIENT_SECRET = "client_secret" - SCAN_INTERVAL = timedelta(minutes=1) diff --git a/homeassistant/components/point/translations/ca.json b/homeassistant/components/point/translations/ca.json index 84674cafb89..85fffddaf36 100644 --- a/homeassistant/components/point/translations/ca.json +++ b/homeassistant/components/point/translations/ca.json @@ -16,7 +16,7 @@ }, "step": { "auth": { - "description": "V\u00e9s a l'enlla\u00e7 seg\u00fcent i Accepta l'acc\u00e9s al teu compte de Minut, despr\u00e9s torna i prem Envia (a sota). \n\n[Enlla\u00e7]({authorization_url})", + "description": "V\u00e9s a l'enlla\u00e7 seg\u00fcent i **Accepta** l'acc\u00e9s al teu compte de Minut, despr\u00e9s torna i prem **Envia** (a sota). \n\n[Enlla\u00e7]({authorization_url})", "title": "Autenticar Point" }, "user": { diff --git a/homeassistant/components/point/translations/en.json b/homeassistant/components/point/translations/en.json index e7f34fa1b76..df13e9a26b9 100644 --- a/homeassistant/components/point/translations/en.json +++ b/homeassistant/components/point/translations/en.json @@ -16,7 +16,7 @@ }, "step": { "auth": { - "description": "Please follow the link below and Accept access to your Minut account, then come back and press Submit below.\n\n[Link]({authorization_url})", + "description": "Please follow the link below and **Accept** access to your Minut account, then come back and press **Submit** below.\n\n[Link]({authorization_url})", "title": "Authenticate Point" }, "user": { diff --git a/homeassistant/components/point/translations/hu.json b/homeassistant/components/point/translations/hu.json index d63879b3b5e..c31e2c55e6a 100644 --- a/homeassistant/components/point/translations/hu.json +++ b/homeassistant/components/point/translations/hu.json @@ -1,12 +1,17 @@ { "config": { "abort": { + "already_setup": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", "authorize_url_fail": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n.", - "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n." + "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", + "no_flows": "A komponens nincs konfigur\u00e1lva. K\u00e9rlek, k\u00f6vesd a dokument\u00e1ci\u00f3t." + }, + "create_entry": { + "default": "Sikeres hiteles\u00edt\u00e9s" }, "error": { "follow_link": "K\u00e9rlek, k\u00f6vesd a hivatkoz\u00e1st \u00e9s hiteles\u00edtsd magad miel\u0151tt megnyomod a K\u00fcld\u00e9s gombot", - "no_token": "A Minut nincs hiteles\u00edtve" + "no_token": "\u00c9rv\u00e9nytelen hozz\u00e1f\u00e9r\u00e9si token" }, "step": { "auth": { @@ -17,8 +22,8 @@ "data": { "flow_impl": "Szolg\u00e1ltat\u00f3" }, - "description": "V\u00e1laszd ki, hogy melyik hiteles\u00edt\u00e9si szolg\u00e1ltat\u00f3n\u00e1l szeretn\u00e9d hiteles\u00edteni a Pointot.", - "title": "Hiteles\u00edt\u00e9si Szolg\u00e1ltat\u00f3" + "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?", + "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" } } } diff --git a/homeassistant/components/point/translations/it.json b/homeassistant/components/point/translations/it.json index 90f9c1b73cd..8f2b5f94c4b 100644 --- a/homeassistant/components/point/translations/it.json +++ b/homeassistant/components/point/translations/it.json @@ -16,7 +16,7 @@ }, "step": { "auth": { - "description": "Segui il link qui sotto e Accetta l'accesso al tuo account Minut, quindi torna indietro e premi Invia qui sotto. \n\n [Link] ( {authorization_url} )", + "description": "Segui il link qui sotto e **Accetta** l'accesso al tuo account Minut, quindi torna indietro e premi **Invia** qui sotto. \n\n [Link]({authorization_url})", "title": "Autenticare Point" }, "user": { diff --git a/homeassistant/components/point/translations/ko.json b/homeassistant/components/point/translations/ko.json index 8a4eb14f287..417cbf949f5 100644 --- a/homeassistant/components/point/translations/ko.json +++ b/homeassistant/components/point/translations/ko.json @@ -11,12 +11,12 @@ "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "follow_link": "Submit \ubc84\ud2bc\uc744 \ub204\ub974\uae30 \uc804\uc5d0 \ub9c1\ud06c\ub97c \ub530\ub77c \uc778\uc99d\uc744 \ubc1b\uc544\uc8fc\uc138\uc694", + "follow_link": "\ud655\uc778\uc744 \ud074\ub9ad\ud558\uae30 \uc804\uc5d0 \ub9c1\ud06c\ub97c \ub530\ub77c \uc778\uc99d\uc744 \ubc1b\uc544\uc8fc\uc138\uc694", "no_token": "\uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { "auth": { - "description": "\uc544\ub798 \ub9c1\ud06c\ub97c \ud074\ub9ad\ud558\uc5ec Minut \uacc4\uc815\uc5d0 \ub300\ud574 \ub3d9\uc758 \ud55c \ub2e4\uc74c, \ub2e4\uc2dc \ub3cc\uc544\uc640\uc11c \ud558\ub2e8\uc758 Submit \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694. \n\n[\ub9c1\ud06c] ({authorization_url})", + "description": "\uc544\ub798 \ub9c1\ud06c\ub97c \ud074\ub9ad\ud558\uc5ec Minut \uacc4\uc815\uc5d0 \ub300\ud574 **\ub3d9\uc758**\ud55c \ub2e4\uc74c, \ub2e4\uc2dc \ub3cc\uc544\uc640\uc11c \ud558\ub2e8\uc758 **\ud655\uc778**\uc744 \ud074\ub9ad\ud574\uc8fc\uc138\uc694. \n\n[\ub9c1\ud06c]({authorization_url})", "title": "Point \uc778\uc99d\ud558\uae30" }, "user": { diff --git a/homeassistant/components/point/translations/no.json b/homeassistant/components/point/translations/no.json index 30ac5f8e356..eb3fef66166 100644 --- a/homeassistant/components/point/translations/no.json +++ b/homeassistant/components/point/translations/no.json @@ -16,7 +16,7 @@ }, "step": { "auth": { - "description": "Vennligst f\u00f8lg lenken nedenfor og Godta tilgang til Minut-kontoen din, kom tilbake og trykk Send inn nedenfor. \n\n [Link]({authorization_url})", + "description": "Vennligst f\u00f8lg lenken nedenfor og **Godta** tilgang til Minut-kontoen din, kom tilbake og trykk **Send inn** nedenfor. \n\n [Link]({authorization_url})", "title": "Godkjenn Point" }, "user": { diff --git a/homeassistant/components/point/translations/pl.json b/homeassistant/components/point/translations/pl.json index 22fc5da2278..1596ba05916 100644 --- a/homeassistant/components/point/translations/pl.json +++ b/homeassistant/components/point/translations/pl.json @@ -1,18 +1,18 @@ { "config": { "abort": { - "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko konto Point.", + "already_setup": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.", "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji.", - "authorize_url_timeout": "[%key_id:common::config_flow::abort::oauth2_authorize_url_timeout%]", + "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", "external_setup": "Punkt pomy\u015blnie skonfigurowany.", - "no_flows": "Musisz skonfigurowa\u0107 Point, aby m\u00f3c si\u0119 z nim uwierzytelni\u0107. Zapoznaj si\u0119 z [instrukcj\u0105](https://www.home-assistant.io/components/point/)." + "no_flows": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105." }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono przy u\u017cyciu Minut dla urz\u0105dze\u0144 Point" }, "error": { "follow_link": "Prosz\u0119 klikn\u0105\u0107 link i uwierzytelni\u0107 przed naci\u015bni\u0119ciem przycisku \"Zatwierd\u017a\"", - "no_token": "Brak uwierzytelnienia za pomoc\u0105 Minut" + "no_token": "Niepoprawny token dost\u0119pu." }, "step": { "auth": { @@ -24,7 +24,7 @@ "flow_impl": "Dostawca" }, "description": "Wybierz, kt\u00f3rego dostawc\u0119 uwierzytelnienia chcesz u\u017cywa\u0107 z Point.", - "title": "Dostawca uwierzytelnienia" + "title": "Wybierz metod\u0119 uwierzytelniania" } } } diff --git a/homeassistant/components/point/translations/ru.json b/homeassistant/components/point/translations/ru.json index 8c481dc0305..a8dbb47b400 100644 --- a/homeassistant/components/point/translations/ru.json +++ b/homeassistant/components/point/translations/ru.json @@ -16,7 +16,7 @@ }, "step": { "auth": { - "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e [\u0441\u0441\u044b\u043b\u043a\u0435]({authorization_url}) \u0438 \u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0435 \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Minut, \u0437\u0430\u0442\u0435\u043c \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c.", + "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e [\u0441\u0441\u044b\u043b\u043a\u0435]({authorization_url}) \u0438 **\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0435** \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Minut, \u0437\u0430\u0442\u0435\u043c \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **\u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c**.", "title": "Minut Point" }, "user": { diff --git a/homeassistant/components/point/translations/zh-Hant.json b/homeassistant/components/point/translations/zh-Hant.json index 618480cb771..bd0532c1ae2 100644 --- a/homeassistant/components/point/translations/zh-Hant.json +++ b/homeassistant/components/point/translations/zh-Hant.json @@ -16,7 +16,7 @@ }, "step": { "auth": { - "description": "\u8acb\u4f7f\u7528\u4e0b\u65b9\u9023\u7d50\u4e26\u9ede\u9078\u63a5\u53d7\u4ee5\u5b58\u53d6 Minut \u5e33\u865f\uff0c\u7136\u5f8c\u8fd4\u56de\u6b64\u9801\u9762\u4e26\u9ede\u9078\u4e0b\u65b9\u7684\u50b3\u9001\u3002\n\n[Link]({authorization_url})", + "description": "\u8acb\u4f7f\u7528\u4e0b\u65b9\u9023\u7d50\u4e26\u9ede\u9078 **\u63a5\u53d7** \u4ee5\u5b58\u53d6 Minut \u5e33\u865f\uff0c\u7136\u5f8c\u8fd4\u56de\u6b64\u9801\u9762\u4e26\u9ede\u9078\u4e0b\u65b9\u7684 **\u50b3\u9001**\u3002\n\n[\u9023\u7d50]({authorization_url})", "title": "\u8a8d\u8b49 Point" }, "user": { diff --git a/homeassistant/components/powerwall/manifest.json b/homeassistant/components/powerwall/manifest.json index 7b2095c4a2a..da5f6e4b7ed 100644 --- a/homeassistant/components/powerwall/manifest.json +++ b/homeassistant/components/powerwall/manifest.json @@ -3,6 +3,6 @@ "name": "Tesla Powerwall", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/powerwall", - "requirements": ["tesla-powerwall==0.2.8"], + "requirements": ["tesla-powerwall==0.2.10"], "codeowners": ["@bdraco", "@jrester"] } diff --git a/homeassistant/components/powerwall/translations/pl.json b/homeassistant/components/powerwall/translations/pl.json index 3ccdfce648a..20eb71e7c27 100644 --- a/homeassistant/components/powerwall/translations/pl.json +++ b/homeassistant/components/powerwall/translations/pl.json @@ -5,13 +5,13 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "unknown": "[%key_id:common::config_flow::error::unknown%]", + "unknown": "Nieoczekiwany b\u0142\u0105d.", "wrong_version": "Powerwall u\u017cywa wersji oprogramowania, kt\u00f3ra nie jest obs\u0142ugiwana. Rozwa\u017c uaktualnienie lub zg\u0142oszenie tego problemu, aby mo\u017cna go by\u0142o rozwi\u0105za\u0107." }, "step": { "user": { "data": { - "ip_address": "[%key_id:common::config_flow::data::ip%]" + "ip_address": "Adres IP" }, "title": "Po\u0142\u0105czenie z Powerwall" } diff --git a/homeassistant/components/powerwall/translations/pt-BR.json b/homeassistant/components/powerwall/translations/pt-BR.json new file mode 100644 index 00000000000..e97b93d1e66 --- /dev/null +++ b/homeassistant/components/powerwall/translations/pt-BR.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "O powerwall j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar, tente novamente", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "ip_address": "Endere\u00e7o IP" + }, + "title": "Conecte-se ao powerwall" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 314695458b0..aea26414bee 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -7,7 +7,11 @@ import prometheus_client import voluptuous as vol from homeassistant import core as hacore -from homeassistant.components.climate.const import ATTR_CURRENT_TEMPERATURE +from homeassistant.components.climate.const import ( + ATTR_CURRENT_TEMPERATURE, + ATTR_HVAC_ACTION, + CURRENT_HVAC_ACTIONS, +) from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -15,6 +19,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONTENT_TYPE_TEXT_PLAIN, EVENT_STATE_CHANGED, + STATE_ON, TEMP_CELSIUS, TEMP_FAHRENHEIT, UNIT_PERCENTAGE, @@ -151,9 +156,24 @@ class PrometheusMetrics: ) metric.labels(**self._labels(state)).inc() - def _metric(self, metric, factory, documentation, labels=None): - if labels is None: - labels = ["entity", "friendly_name", "domain"] + def _handle_attributes(self, state): + for key, value in state.attributes.items(): + metric = self._metric( + f"{state.domain}_attr_{key.lower()}", + self.prometheus_cli.Gauge, + f"{key} attribute of {state.domain} entity", + ) + + try: + value = float(value) + metric.labels(**self._labels(state)).set(value) + except ValueError: + pass + + def _metric(self, metric, factory, documentation, extra_labels=None): + labels = ["entity", "friendly_name", "domain"] + if extra_labels is not None: + labels.extend(extra_labels) try: return self._metrics[metric] @@ -249,7 +269,7 @@ class PrometheusMetrics: ) try: - if "brightness" in state.attributes: + if "brightness" in state.attributes and state.state == STATE_ON: value = state.attributes["brightness"] / 255.0 else: value = self.state_as_number(state) @@ -288,6 +308,16 @@ class PrometheusMetrics: ) metric.labels(**self._labels(state)).set(current_temp) + current_action = state.attributes.get(ATTR_HVAC_ACTION) + if current_action: + metric = self._metric( + "climate_action", self.prometheus_cli.Gauge, "HVAC action", ["action"], + ) + for action in CURRENT_HVAC_ACTIONS: + metric.labels(**dict(self._labels(state), action=action)).set( + float(action == current_action) + ) + def _handle_sensor(self, state): unit = self._unit_string(state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)) @@ -367,6 +397,8 @@ class PrometheusMetrics: except ValueError: pass + self._handle_attributes(state) + def _handle_zwave(self, state): self._battery(state) diff --git a/homeassistant/components/proxmoxve/__init__.py b/homeassistant/components/proxmoxve/__init__.py index 315fb8b1c91..0919feb15e3 100644 --- a/homeassistant/components/proxmoxve/__init__.py +++ b/homeassistant/components/proxmoxve/__init__.py @@ -1,7 +1,6 @@ """Support for Proxmox VE.""" from enum import Enum import logging -import time from proxmoxer import ProxmoxAPI from proxmoxer.backends.https import AuthenticationError @@ -137,25 +136,21 @@ class ProxmoxClient: self._connection_start_time = None def build_client(self): - """Construct the ProxmoxAPI client.""" + """Construct the ProxmoxAPI client. Allows inserting the realm within the `user` value.""" + + if "@" in self._user: + user_id = self._user + else: + user_id = f"{self._user}@{self._realm}" self._proxmox = ProxmoxAPI( self._host, port=self._port, - user=f"{self._user}@{self._realm}", + user=user_id, password=self._password, verify_ssl=self._verify_ssl, ) - self._connection_start_time = time.monotonic() - def get_api_client(self): - """Return the ProxmoxAPI client and rebuild it if necessary.""" - - 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: - self.build_client() - + """Return the ProxmoxAPI client.""" return self._proxmox diff --git a/homeassistant/components/proxmoxve/manifest.json b/homeassistant/components/proxmoxve/manifest.json index 2735bab1b04..4040ca7c469 100644 --- a/homeassistant/components/proxmoxve/manifest.json +++ b/homeassistant/components/proxmoxve/manifest.json @@ -2,6 +2,6 @@ "domain": "proxmoxve", "name": "Proxmox VE", "documentation": "https://www.home-assistant.io/integrations/proxmoxve", - "codeowners": ["@k4ds3"], - "requirements": ["proxmoxer==1.0.4"] + "codeowners": ["@k4ds3", "@jhollowe"], + "requirements": ["proxmoxer==1.1.0"] } diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index d2c6e9859de..390637c26a3 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -157,11 +157,11 @@ def format_unique_id(creds, mac_address): return f"{mac_address}_{suffix}" -def load_games(hass: HomeAssistantType) -> dict: +def load_games(hass: HomeAssistantType, unique_id: str) -> dict: """Load games for sources.""" - g_file = hass.config.path(GAMES_FILE) + g_file = hass.config.path(GAMES_FILE.format(unique_id)) try: - games = load_json(g_file, dict) + games = load_json(g_file) except HomeAssistantError as error: games = {} _LOGGER.error("Failed to load games file: %s", error) @@ -172,20 +172,20 @@ def load_games(hass: HomeAssistantType) -> dict: # If file exists if os.path.isfile(g_file): - games = _reformat_data(hass, games) + games = _reformat_data(hass, games, unique_id) return games -def save_games(hass: HomeAssistantType, games: dict): +def save_games(hass: HomeAssistantType, games: dict, unique_id: str): """Save games to file.""" - g_file = hass.config.path(GAMES_FILE) + g_file = hass.config.path(GAMES_FILE.format(unique_id)) try: save_json(g_file, games) except OSError as error: _LOGGER.error("Could not save game list, %s", error) -def _reformat_data(hass: HomeAssistantType, games: dict) -> dict: +def _reformat_data(hass: HomeAssistantType, games: dict, unique_id: str) -> dict: """Reformat data to correct format.""" data_reformatted = False @@ -204,7 +204,7 @@ def _reformat_data(hass: HomeAssistantType, games: dict) -> dict: _LOGGER.debug("Reformatting media data for item: %s, %s", game, data) if data_reformatted: - save_games(hass, games) + save_games(hass, games, unique_id) return games diff --git a/homeassistant/components/ps4/const.py b/homeassistant/components/ps4/const.py index 779da61ca48..0974286ebe8 100644 --- a/homeassistant/components/ps4/const.py +++ b/homeassistant/components/ps4/const.py @@ -5,7 +5,7 @@ DEFAULT_NAME = "PlayStation 4" DEFAULT_REGION = "United States" DEFAULT_ALIAS = "Home-Assistant" DOMAIN = "ps4" -GAMES_FILE = ".ps4-games.json" +GAMES_FILE = ".ps4-games.{}.json" PS4_DATA = "ps4_data" COMMANDS = ("up", "down", "right", "left", "enter", "back", "option", "ps", "ps_hold") diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index 39b60be0493..9bd4ddbefbe 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -163,7 +163,7 @@ class PS4Device(MediaPlayerEntity): status = self._ps4.status if status is not None: - self._games = load_games(self.hass) + self._games = load_games(self.hass, self._unique_id) if self._games: self.get_source_list() @@ -300,7 +300,7 @@ class PS4Device(MediaPlayerEntity): self._media_image, self._media_type, ) - self._games = load_games(self.hass) + self._games = load_games(self.hass, self._unique_id) self.get_source_list() @@ -324,7 +324,7 @@ class PS4Device(MediaPlayerEntity): } } games.update(game) - save_games(self.hass, games) + save_games(self.hass, games, self._unique_id) async def async_get_device_info(self, status): """Set device info for registry.""" diff --git a/homeassistant/components/ps4/translations/it.json b/homeassistant/components/ps4/translations/it.json index dc73362280e..d010c2e1f45 100644 --- a/homeassistant/components/ps4/translations/it.json +++ b/homeassistant/components/ps4/translations/it.json @@ -4,8 +4,8 @@ "credential_error": "Errore nel recupero delle credenziali.", "devices_configured": "Tutti i dispositivi trovati sono gi\u00e0 configurati.", "no_devices_found": "Nessun dispositivo PlayStation 4 trovato in rete.", - "port_987_bind_error": "Impossibile collegarsi alla porta 987. Per ulteriori informazioni, consultare la [documentazione] (https://www.home-assistant.io/components/ps4/) per ulteriori informazioni.", - "port_997_bind_error": "Impossibile collegarsi alla porta 997. Consultare la [documentazione] (https://www.home-assistant.io/components/ps4/) per ulteriori informazioni." + "port_987_bind_error": "Impossibile collegarsi alla porta 987. Fare riferimento alla [documentazione](https://www.home-assistant.io/components/ps4/) per ulteriori informazioni.", + "port_997_bind_error": "Impossibile collegarsi alla porta 997. Consultare la [documentazione](https://www.home-assistant.io/components/ps4/) per ulteriori informazioni." }, "error": { "credential_timeout": "Servizio credenziali scaduto. Premi Invia per riavviare.", diff --git a/homeassistant/components/ps4/translations/ko.json b/homeassistant/components/ps4/translations/ko.json index 7285ffb97bf..5a9ab246ac2 100644 --- a/homeassistant/components/ps4/translations/ko.json +++ b/homeassistant/components/ps4/translations/ko.json @@ -8,14 +8,14 @@ "port_997_bind_error": "\ud3ec\ud2b8 997 \uc5d0 \ubc14\uc778\ub529 \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ucd94\uac00 \uc815\ubcf4\ub294 [\uc548\ub0b4](https://www.home-assistant.io/components/ps4/) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "error": { - "credential_timeout": "\uc790\uaca9 \uc99d\uba85 \uc11c\ube44\uc2a4 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4. Submit \uc744 \ub20c\ub7ec \ub2e4\uc2dc \uc2dc\uc791\ud574\uc8fc\uc138\uc694.", + "credential_timeout": "\uc790\uaca9 \uc99d\uba85 \uc11c\ube44\uc2a4 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud655\uc778\uc744 \ud074\ub9ad\ud558\uc5ec \ub2e4\uc2dc \uc2dc\uc791\ud574\uc8fc\uc138\uc694.", "login_failed": "PlayStation 4 \uc640 \ud398\uc5b4\ub9c1\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. PIN \uc774 \uc62c\ubc14\ub978\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", "no_ipaddress": "\uad6c\uc131\ud558\uace0\uc790 \ud558\ub294 PlayStation 4 \uc758 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", "not_ready": "PlayStation 4 \uac00 \ucf1c\uc838 \uc788\uc9c0 \uc54a\uac70\ub098 \ub124\ud2b8\uc6cc\ud06c\uc5d0 \uc5f0\uacb0\ub418\uc5b4 \uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." }, "step": { "creds": { - "description": "\uc790\uaca9 \uc99d\uba85\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. 'Submit' \uc744 \ub204\ub978 \ub2e4\uc74c PS4 \uc138\ucee8\ub4dc \uc2a4\ud06c\ub9b0 \uc571\uc5d0\uc11c \uae30\uae30\ub97c \uc0c8\ub85c \uace0\uce68\ud558\uace0 'Home-Assistant' \uae30\uae30\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", + "description": "\uc790\uaca9 \uc99d\uba85\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. '\ud655\uc778'\uc744 \ud074\ub9ad\ud55c \ub2e4\uc74c PS4 \uc138\ucee8\ub4dc \uc2a4\ud06c\ub9b0 \uc571\uc5d0\uc11c \uae30\uae30\ub97c \uc0c8\ub85c \uace0\uce68\ud558\uace0 'Home-Assistant' \uae30\uae30\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", "title": "PlayStation 4" }, "link": { diff --git a/homeassistant/components/ps4/translations/pl.json b/homeassistant/components/ps4/translations/pl.json index a701feb6f32..8cbdfe3d0b7 100644 --- a/homeassistant/components/ps4/translations/pl.json +++ b/homeassistant/components/ps4/translations/pl.json @@ -21,7 +21,7 @@ "link": { "data": { "code": "PIN", - "ip_address": "[%key_id:common::config_flow::data::ip%]", + "ip_address": "Adres IP", "name": "Nazwa", "region": "Region" }, @@ -30,7 +30,7 @@ }, "mode": { "data": { - "ip_address": "[%key_id:common::config_flow::data::ip%] (pozostaw puste, je\u015bli u\u017cywasz wykrywania).", + "ip_address": "Adres IP (pozostaw puste, je\u015bli u\u017cywasz wykrywania)", "mode": "Tryb konfiguracji" }, "description": "Wybierz tryb konfiguracji. Pole adresu IP mo\u017cna pozostawi\u0107 puste, je\u015bli wybierzesz opcj\u0119 Auto Discovery, poniewa\u017c urz\u0105dzenia zostan\u0105 automatycznie wykryte.", diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/pt-BR.json b/homeassistant/components/pvpc_hourly_pricing/translations/pt-BR.json new file mode 100644 index 00000000000..efcaeb801d9 --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/translations/pt-BR.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "A integra\u00e7\u00e3o j\u00e1 est\u00e1 configurada com um sensor existente com essa tarifa" + }, + "step": { + "user": { + "data": { + "name": "Nome do sensor", + "tariff": "Tarifa contratada (1, 2 ou 3 per\u00edodos)" + }, + "description": "Esse sensor usa a API oficial para obter [pre\u00e7os por hora de eletricidade (PVPC)]](https://www.esios.ree.es/es/pvpc) na Espanha. \nPara uma explica\u00e7\u00e3o mais precisa, visite os [documentos de integra\u00e7\u00e3o](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\nSelecione a taxa contratada com base no n\u00famero de per\u00edodos de cobran\u00e7a por dia: \n- 1 per\u00edodo: normal \n- 2 per\u00edodos: discrimina\u00e7\u00e3o (taxa noturna) \n- 3 per\u00edodos: carro el\u00e9trico (taxa noturna de 3 per\u00edodos)", + "title": "Sele\u00e7\u00e3o de tarifas" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rachio/binary_sensor.py b/homeassistant/components/rachio/binary_sensor.py index 49f46578f76..495001e39b9 100644 --- a/homeassistant/components/rachio/binary_sensor.py +++ b/homeassistant/components/rachio/binary_sensor.py @@ -4,6 +4,7 @@ import logging from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_MOISTURE, BinarySensorEntity, ) from homeassistant.core import callback @@ -12,13 +13,21 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( DOMAIN as DOMAIN_RACHIO, KEY_DEVICE_ID, + KEY_RAIN_SENSOR_TRIPPED, KEY_STATUS, KEY_SUBTYPE, SIGNAL_RACHIO_CONTROLLER_UPDATE, + SIGNAL_RACHIO_RAIN_SENSOR_UPDATE, STATUS_ONLINE, ) from .entity import RachioDevice -from .webhooks import SUBTYPE_COLD_REBOOT, SUBTYPE_OFFLINE, SUBTYPE_ONLINE +from .webhooks import ( + SUBTYPE_COLD_REBOOT, + SUBTYPE_OFFLINE, + SUBTYPE_ONLINE, + SUBTYPE_RAIN_SENSOR_DETECTION_OFF, + SUBTYPE_RAIN_SENSOR_DETECTION_ON, +) _LOGGER = logging.getLogger(__name__) @@ -34,6 +43,7 @@ def _create_entities(hass, config_entry): entities = [] for controller in hass.data[DOMAIN_RACHIO][config_entry.entry_id].controllers: entities.append(RachioControllerOnlineBinarySensor(controller)) + entities.append(RachioRainSensor(controller)) return entities @@ -64,16 +74,6 @@ class RachioControllerBinarySensor(RachioDevice, BinarySensorEntity): def _async_handle_update(self, *args, **kwargs) -> None: """Handle an update to the state of this sensor.""" - async def async_added_to_hass(self): - """Subscribe to updates.""" - self.async_on_remove( - async_dispatcher_connect( - self.hass, - SIGNAL_RACHIO_CONTROLLER_UPDATE, - self._async_handle_any_update, - ) - ) - class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor): """Represent a binary sensor that reflects if the controller is online.""" @@ -98,11 +98,6 @@ class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor): """Return the name of an icon for this sensor.""" return "mdi:wifi-strength-4" if self.is_on else "mdi:wifi-strength-off-outline" - async def async_added_to_hass(self): - """Get initial state.""" - self._state = self._controller.init_data[KEY_STATUS] == STATUS_ONLINE - await super().async_added_to_hass() - @callback def _async_handle_update(self, *args, **kwargs) -> None: """Handle an update to the state of this sensor.""" @@ -115,3 +110,61 @@ class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor): self._state = False self.async_write_ha_state() + + async def async_added_to_hass(self): + """Subscribe to updates.""" + self._state = self._controller.init_data[KEY_STATUS] == STATUS_ONLINE + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_RACHIO_CONTROLLER_UPDATE, + self._async_handle_any_update, + ) + ) + + +class RachioRainSensor(RachioControllerBinarySensor): + """Represent a binary sensor that reflects the status of the rain sensor.""" + + @property + def name(self) -> str: + """Return the name of this sensor including the controller name.""" + return f"{self._controller.name} rain sensor" + + @property + def unique_id(self) -> str: + """Return a unique id for this entity.""" + return f"{self._controller.controller_id}-rain_sensor" + + @property + def device_class(self) -> str: + """Return the class of this device.""" + return DEVICE_CLASS_MOISTURE + + @property + def icon(self) -> str: + """Return the icon for this sensor.""" + return "mdi:water" if self.is_on else "mdi:water-off" + + @callback + def _async_handle_update(self, *args, **kwargs) -> None: + """Handle an update to the state of this sensor.""" + if args[0][0][KEY_SUBTYPE] == SUBTYPE_RAIN_SENSOR_DETECTION_ON: + self._state = True + elif args[0][0][KEY_SUBTYPE] == SUBTYPE_RAIN_SENSOR_DETECTION_OFF: + self._state = False + + self.async_write_ha_state() + + async def async_added_to_hass(self): + """Subscribe to updates.""" + self._state = self._controller.init_data[KEY_RAIN_SENSOR_TRIPPED] + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_RACHIO_RAIN_SENSOR_UPDATE, + self._async_handle_any_update, + ) + ) diff --git a/homeassistant/components/rachio/const.py b/homeassistant/components/rachio/const.py index 016218906f4..7f8111bd5e5 100644 --- a/homeassistant/components/rachio/const.py +++ b/homeassistant/components/rachio/const.py @@ -12,6 +12,12 @@ CONF_CUSTOM_URL = "hass_url_override" CONF_MANUAL_RUN_MINS = "manual_run_mins" DEFAULT_MANUAL_RUN_MINS = 10 +# Slope constants +SLOPE_FLAT = "ZERO_THREE" +SLOPE_SLIGHT = "FOUR_SIX" +SLOPE_MODERATE = "SEVEN_TWELVE" +SLOPE_STEEP = "OVER_TWELVE" + # Keys used in the API JSON KEY_DEVICE_ID = "deviceId" KEY_IMAGE_URL = "imageUrl" @@ -24,6 +30,7 @@ KEY_MODEL = "model" KEY_ON = "on" KEY_DURATION = "totalDuration" KEY_RAIN_DELAY = "rainDelayExpirationDate" +KEY_RAIN_SENSOR_TRIPPED = "rainSensorTripped" KEY_STATUS = "status" KEY_SUBTYPE = "subType" KEY_SUMMARY = "summary" @@ -40,9 +47,7 @@ KEY_FLEX_SCHEDULES = "flexScheduleRules" KEY_SCHEDULE_ID = "scheduleId" KEY_CUSTOM_SHADE = "customShade" KEY_CUSTOM_CROP = "customCrop" - -ATTR_ZONE_TYPE = "type" -ATTR_ZONE_SHADE = "shade" +KEY_CUSTOM_SLOPE = "customSlope" # Yes we really do get all these exceptions (hopefully rachiopy switches to requests) RACHIO_API_EXCEPTIONS = ( @@ -57,6 +62,7 @@ STATUS_ONLINE = "ONLINE" SIGNAL_RACHIO_UPDATE = f"{DOMAIN}_update" SIGNAL_RACHIO_CONTROLLER_UPDATE = f"{SIGNAL_RACHIO_UPDATE}_controller" SIGNAL_RACHIO_RAIN_DELAY_UPDATE = f"{SIGNAL_RACHIO_UPDATE}_rain_delay" +SIGNAL_RACHIO_RAIN_SENSOR_UPDATE = f"{SIGNAL_RACHIO_UPDATE}_rain_sensor" SIGNAL_RACHIO_ZONE_UPDATE = f"{SIGNAL_RACHIO_UPDATE}_zone" SIGNAL_RACHIO_SCHEDULE_UPDATE = f"{SIGNAL_RACHIO_UPDATE}_schedule" diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index 95f01d31518..b16e3ce529e 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -9,13 +9,12 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util.dt import as_timestamp, now from .const import ( - ATTR_ZONE_SHADE, - ATTR_ZONE_TYPE, CONF_MANUAL_RUN_MINS, DEFAULT_MANUAL_RUN_MINS, DOMAIN as DOMAIN_RACHIO, KEY_CUSTOM_CROP, KEY_CUSTOM_SHADE, + KEY_CUSTOM_SLOPE, KEY_DEVICE_ID, KEY_DURATION, KEY_ENABLED, @@ -33,6 +32,10 @@ from .const import ( SIGNAL_RACHIO_RAIN_DELAY_UPDATE, SIGNAL_RACHIO_SCHEDULE_UPDATE, SIGNAL_RACHIO_ZONE_UPDATE, + SLOPE_FLAT, + SLOPE_MODERATE, + SLOPE_SLIGHT, + SLOPE_STEEP, ) from .entity import RachioDevice from .webhooks import ( @@ -50,11 +53,14 @@ from .webhooks import ( _LOGGER = logging.getLogger(__name__) -ATTR_ZONE_SUMMARY = "Summary" -ATTR_ZONE_NUMBER = "Zone number" ATTR_SCHEDULE_SUMMARY = "Summary" ATTR_SCHEDULE_ENABLED = "Enabled" ATTR_SCHEDULE_DURATION = "Duration" +ATTR_ZONE_NUMBER = "Zone number" +ATTR_ZONE_SHADE = "Shade" +ATTR_ZONE_SLOPE = "Slope" +ATTR_ZONE_SUMMARY = "Summary" +ATTR_ZONE_TYPE = "Type" async def async_setup_entry(hass, config_entry, async_add_entities): @@ -235,6 +241,7 @@ class RachioZone(RachioSwitch): self._person = person self._shade_type = data.get(KEY_CUSTOM_SHADE, {}).get(KEY_NAME) self._zone_type = data.get(KEY_CUSTOM_CROP, {}).get(KEY_NAME) + self._slope_type = data.get(KEY_CUSTOM_SLOPE, {}).get(KEY_NAME) self._summary = "" self._current_schedule = current_schedule super().__init__(controller) @@ -281,6 +288,15 @@ class RachioZone(RachioSwitch): props[ATTR_ZONE_SHADE] = self._shade_type if self._zone_type: props[ATTR_ZONE_TYPE] = self._zone_type + if self._slope_type: + if self._slope_type == SLOPE_FLAT: + props[ATTR_ZONE_SLOPE] = "Flat" + elif self._slope_type == SLOPE_SLIGHT: + props[ATTR_ZONE_SLOPE] = "Slight" + elif self._slope_type == SLOPE_MODERATE: + props[ATTR_ZONE_SLOPE] = "Moderate" + elif self._slope_type == SLOPE_STEEP: + props[ATTR_ZONE_SLOPE] = "Steep" return props def turn_on(self, **kwargs) -> None: @@ -312,7 +328,7 @@ class RachioZone(RachioSwitch): if args[0][KEY_ZONE_ID] != self.zone_id: return - self._summary = kwargs.get(KEY_SUMMARY, "") + self._summary = args[0][KEY_SUMMARY] if args[0][KEY_SUBTYPE] == SUBTYPE_ZONE_STARTED: self._state = True diff --git a/homeassistant/components/rachio/translations/ca.json b/homeassistant/components/rachio/translations/ca.json index 14b4d8effd8..8da88ec50e9 100644 --- a/homeassistant/components/rachio/translations/ca.json +++ b/homeassistant/components/rachio/translations/ca.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "api_key": "Clau API del compte Rachio." + "api_key": "Clau API" }, "description": "Necessitar\u00e0s la clau API de https://app.rach.io/. Selecciona 'Configuraci\u00f3 del compte' (Account Settings) i, a continuaci\u00f3, clica 'Obtenir clau API' (GET API KEY).", "title": "Connexi\u00f3 amb dispositiu Rachio" diff --git a/homeassistant/components/rachio/translations/hu.json b/homeassistant/components/rachio/translations/hu.json new file mode 100644 index 00000000000..5f4f7bb8bee --- /dev/null +++ b/homeassistant/components/rachio/translations/hu.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API kulcs" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rachio/translations/it.json b/homeassistant/components/rachio/translations/it.json index 5a9c7618b00..f8013eb3551 100644 --- a/homeassistant/components/rachio/translations/it.json +++ b/homeassistant/components/rachio/translations/it.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "api_key": "Chiave API per l'account Rachio." + "api_key": "Chiave API" }, "description": "\u00c8 necessaria la chiave API di https://app.rach.io/. Selezionare 'Impostazioni Account', quindi fare clic su 'GET API KEY'.", "title": "Connettiti al tuo dispositivo Rachio" diff --git a/homeassistant/components/rachio/translations/pl.json b/homeassistant/components/rachio/translations/pl.json index db3fb0466cb..e077fea03a4 100644 --- a/homeassistant/components/rachio/translations/pl.json +++ b/homeassistant/components/rachio/translations/pl.json @@ -1,17 +1,17 @@ { "config": { "abort": { - "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "[%key_id:common::config_flow::error::invalid_auth%]", - "unknown": "[%key_id:common::config_flow::error::unknown%]" + "invalid_auth": "Niepoprawne uwierzytelnienie.", + "unknown": "Nieoczekiwany b\u0142\u0105d." }, "step": { "user": { "data": { - "api_key": "[%key_id:common::config_flow::data::api_key%] dla konta Rachio." + "api_key": "Klucz API" }, "description": "B\u0119dziesz potrzebowa\u0142 klucza API ze strony https://app.rach.io/. Wybierz 'Account Settings', a nast\u0119pnie kliknij 'GET API KEY'.", "title": "Po\u0142\u0105czenie z urz\u0105dzeniem Rachio" diff --git a/homeassistant/components/rachio/webhooks.py b/homeassistant/components/rachio/webhooks.py index a3f95d5a5f3..5daf7852725 100644 --- a/homeassistant/components/rachio/webhooks.py +++ b/homeassistant/components/rachio/webhooks.py @@ -16,6 +16,7 @@ from .const import ( KEY_TYPE, SIGNAL_RACHIO_CONTROLLER_UPDATE, SIGNAL_RACHIO_RAIN_DELAY_UPDATE, + SIGNAL_RACHIO_RAIN_SENSOR_UPDATE, SIGNAL_RACHIO_SCHEDULE_UPDATE, SIGNAL_RACHIO_ZONE_UPDATE, ) @@ -29,14 +30,17 @@ SUBTYPE_COLD_REBOOT = "COLD_REBOOT" SUBTYPE_SLEEP_MODE_ON = "SLEEP_MODE_ON" SUBTYPE_SLEEP_MODE_OFF = "SLEEP_MODE_OFF" SUBTYPE_BROWNOUT_VALVE = "BROWNOUT_VALVE" -SUBTYPE_RAIN_SENSOR_DETECTION_ON = "RAIN_SENSOR_DETECTION_ON" -SUBTYPE_RAIN_SENSOR_DETECTION_OFF = "RAIN_SENSOR_DETECTION_OFF" # Rain delay values TYPE_RAIN_DELAY_STATUS = "RAIN_DELAY" SUBTYPE_RAIN_DELAY_ON = "RAIN_DELAY_ON" SUBTYPE_RAIN_DELAY_OFF = "RAIN_DELAY_OFF" +# Rain sensor values +TYPE_RAIN_SENSOR_STATUS = "RAIN_SENSOR_DETECTION" +SUBTYPE_RAIN_SENSOR_DETECTION_ON = "RAIN_SENSOR_DETECTION_ON" +SUBTYPE_RAIN_SENSOR_DETECTION_OFF = "RAIN_SENSOR_DETECTION_OFF" + # Schedule webhook values TYPE_SCHEDULE_STATUS = "SCHEDULE_STATUS" SUBTYPE_SCHEDULE_STARTED = "SCHEDULE_STARTED" @@ -60,6 +64,7 @@ LISTEN_EVENT_TYPES = [ "DEVICE_STATUS_EVENT", "ZONE_STATUS_EVENT", "RAIN_DELAY_EVENT", + "RAIN_SENSOR_DETECTION_EVENT", "SCHEDULE_STATUS_EVENT", ] WEBHOOK_CONST_ID = "homeassistant.rachio:" @@ -68,6 +73,7 @@ WEBHOOK_PATH = URL_API + DOMAIN SIGNAL_MAP = { TYPE_CONTROLLER_STATUS: SIGNAL_RACHIO_CONTROLLER_UPDATE, TYPE_RAIN_DELAY_STATUS: SIGNAL_RACHIO_RAIN_DELAY_UPDATE, + TYPE_RAIN_SENSOR_STATUS: SIGNAL_RACHIO_RAIN_SENSOR_UPDATE, TYPE_SCHEDULE_STATUS: SIGNAL_RACHIO_SCHEDULE_UPDATE, TYPE_ZONE_STATUS: SIGNAL_RACHIO_ZONE_UPDATE, } diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 936aa8f1f0e..74704398061 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -22,7 +22,6 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -ATTR_NEXT_RUN = "next_run" ATTR_AREA = "area" ATTR_CS_ON = "cs_on" ATTR_CURRENT_CYCLE = "current_cycle" @@ -30,6 +29,7 @@ ATTR_CYCLES = "cycles" ATTR_DELAY = "delay" ATTR_DELAY_ON = "delay_on" ATTR_FIELD_CAPACITY = "field_capacity" +ATTR_NEXT_RUN = "next_run" ATTR_NO_CYCLES = "number_of_cycles" ATTR_PRECIP_RATE = "sprinkler_head_precipitation_rate" ATTR_RESTRICTIONS = "restrictions" @@ -39,6 +39,7 @@ ATTR_SOIL_TYPE = "soil_type" ATTR_SPRINKLER_TYPE = "sprinkler_head_type" ATTR_STATUS = "status" ATTR_SUN_EXPOSURE = "sun_exposure" +ATTR_TIME_REMAINING = "time_remaining" ATTR_VEGETATION_TYPE = "vegetation_type" ATTR_ZONES = "zones" @@ -289,10 +290,10 @@ class RainMachineZone(RainMachineSwitch): self._attrs.update( { - ATTR_ID: self._switch_data["uid"], ATTR_AREA: details.get("waterSense").get("area"), ATTR_CURRENT_CYCLE: self._switch_data.get("cycle"), ATTR_FIELD_CAPACITY: details.get("waterSense").get("fieldCapacity"), + ATTR_ID: self._switch_data["uid"], ATTR_NO_CYCLES: self._switch_data.get("noOfCycles"), ATTR_PRECIP_RATE: details.get("waterSense").get("precipitationRate"), ATTR_RESTRICTIONS: self._switch_data.get("restriction"), @@ -300,6 +301,7 @@ class RainMachineZone(RainMachineSwitch): ATTR_SOIL_TYPE: SOIL_TYPE_MAP.get(details.get("sun")), ATTR_SPRINKLER_TYPE: SPRINKLER_TYPE_MAP.get(details.get("group_id")), ATTR_SUN_EXPOSURE: SUN_EXPOSURE_MAP.get(details.get("sun")), + ATTR_TIME_REMAINING: self._switch_data.get("remaining"), ATTR_VEGETATION_TYPE: VEGETATION_MAP.get(self._switch_data.get("type")), } ) diff --git a/homeassistant/components/rainmachine/translations/pl.json b/homeassistant/components/rainmachine/translations/pl.json index 20a016755b2..8ea3ab6dbc4 100644 --- a/homeassistant/components/rainmachine/translations/pl.json +++ b/homeassistant/components/rainmachine/translations/pl.json @@ -10,9 +10,9 @@ "step": { "user": { "data": { - "ip_address": "[%key_id:common::config_flow::data::host%]", - "password": "[%key_id:common::config_flow::data::password%]", - "port": "[%key_id:common::config_flow::data::port%]" + "ip_address": "Nazwa hosta lub adres IP", + "password": "Has\u0142o", + "port": "Port" }, "title": "Wprowad\u017a dane" } diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index fcccaa2fb9f..8cceedb3985 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -123,24 +123,39 @@ def run_information(hass, point_in_time: Optional[datetime] = None): There is also the run that covers point_in_time. """ + run_info = run_information_from_instance(hass, point_in_time) + if run_info: + return run_info + + with session_scope(hass=hass) as session: + return run_information_with_session(session, point_in_time) + + +def run_information_from_instance(hass, point_in_time: Optional[datetime] = None): + """Return information about current run from the existing instance. + + Does not query the database for older runs. + """ ins = hass.data[DATA_INSTANCE] - recorder_runs = RecorderRuns if point_in_time is None or point_in_time > ins.recording_start: return ins.run_info - with session_scope(hass=hass) as session: - res = ( - session.query(recorder_runs) - .filter( - (recorder_runs.start < point_in_time) - & (recorder_runs.end > point_in_time) - ) - .first() + +def run_information_with_session(session, point_in_time: Optional[datetime] = None): + """Return information about current run from the database.""" + recorder_runs = RecorderRuns + + res = ( + session.query(recorder_runs) + .filter( + (recorder_runs.start < point_in_time) & (recorder_runs.end > point_in_time) ) - if res: - session.expunge(res) - return res + .first() + ) + if res: + session.expunge(res) + return res async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 0d662c572bd..44396c6eccb 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -2,7 +2,7 @@ "domain": "recorder", "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", - "requirements": ["sqlalchemy==1.3.16"], + "requirements": ["sqlalchemy==1.3.17"], "codeowners": [], "quality_scale": "internal" } diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index f3e80a9a739..ce46ae25476 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -1,5 +1,4 @@ """Models for SQLAlchemy.""" -from datetime import datetime import json import logging @@ -29,6 +28,8 @@ SCHEMA_VERSION = 7 _LOGGER = logging.getLogger(__name__) +DB_TIMEZONE = "Z" + class Events(Base): # type: ignore """Event history data.""" @@ -39,7 +40,7 @@ class Events(Base): # type: ignore event_data = Column(Text) origin = Column(String(32)) time_fired = Column(DateTime(timezone=True), index=True) - created = Column(DateTime(timezone=True), default=datetime.utcnow) + created = Column(DateTime(timezone=True), default=dt_util.utcnow) context_id = Column(String(36), index=True) context_user_id = Column(String(36), index=True) # context_parent_id = Column(String(36), index=True) @@ -65,7 +66,7 @@ class Events(Base): # type: ignore self.event_type, json.loads(self.event_data), EventOrigin(self.origin), - _process_timestamp(self.time_fired), + process_timestamp(self.time_fired), context=context, ) except ValueError: @@ -84,9 +85,9 @@ class States(Base): # type: ignore state = Column(String(255)) attributes = Column(Text) event_id = Column(Integer, ForeignKey("events.event_id"), index=True) - last_changed = Column(DateTime(timezone=True), default=datetime.utcnow) - last_updated = Column(DateTime(timezone=True), default=datetime.utcnow, index=True) - created = Column(DateTime(timezone=True), default=datetime.utcnow) + last_changed = Column(DateTime(timezone=True), default=dt_util.utcnow) + last_updated = Column(DateTime(timezone=True), default=dt_util.utcnow, index=True) + created = Column(DateTime(timezone=True), default=dt_util.utcnow) context_id = Column(String(36), index=True) context_user_id = Column(String(36), index=True) # context_parent_id = Column(String(36), index=True) @@ -134,8 +135,8 @@ class States(Base): # type: ignore self.entity_id, self.state, json.loads(self.attributes), - _process_timestamp(self.last_changed), - _process_timestamp(self.last_updated), + process_timestamp(self.last_changed), + process_timestamp(self.last_updated), context=context, # Temp, because database can still store invalid entity IDs # Remove with 1.0 or in 2020. @@ -152,10 +153,10 @@ class RecorderRuns(Base): # type: ignore __tablename__ = "recorder_runs" run_id = Column(Integer, primary_key=True) - start = Column(DateTime(timezone=True), default=datetime.utcnow) + start = Column(DateTime(timezone=True), default=dt_util.utcnow) end = Column(DateTime(timezone=True)) closed_incorrect = Column(Boolean, default=False) - created = Column(DateTime(timezone=True), default=datetime.utcnow) + created = Column(DateTime(timezone=True), default=dt_util.utcnow) __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) @@ -191,10 +192,10 @@ class SchemaChanges(Base): # type: ignore __tablename__ = "schema_changes" change_id = Column(Integer, primary_key=True) schema_version = Column(Integer) - changed = Column(DateTime(timezone=True), default=datetime.utcnow) + changed = Column(DateTime(timezone=True), default=dt_util.utcnow) -def _process_timestamp(ts): +def process_timestamp(ts): """Process a timestamp into datetime object.""" if ts is None: return None diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 693d88ae795..d7f0771b6f5 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -54,7 +54,7 @@ def commit(session, work): return False -def execute(qry): +def execute(qry, to_native=True): """Query the database and convert the objects to HA native form. This method also retries a few times in the case of stale connections. @@ -62,17 +62,25 @@ def execute(qry): for tryno in range(0, RETRIES): try: timer_start = time.perf_counter() - result = [ - row for row in (row.to_native() for row in qry) if row is not None - ] + if to_native: + result = [ + row for row in (row.to_native() for row in qry) if row is not None + ] + else: + result = list(qry) if _LOGGER.isEnabledFor(logging.DEBUG): elapsed = time.perf_counter() - timer_start - _LOGGER.debug( - "converting %d rows to native objects took %fs", - len(result), - elapsed, - ) + if to_native: + _LOGGER.debug( + "converting %d rows to native objects took %fs", + len(result), + elapsed, + ) + else: + _LOGGER.debug( + "querying %d rows took %fs", len(result), elapsed, + ) return result except SQLAlchemyError as err: diff --git a/homeassistant/components/reddit/sensor.py b/homeassistant/components/reddit/sensor.py index ed24dfe47df..0fe4e87f863 100644 --- a/homeassistant/components/reddit/sensor.py +++ b/homeassistant/components/reddit/sensor.py @@ -6,14 +6,18 @@ import praw import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_MAXIMUM, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_MAXIMUM, + CONF_PASSWORD, + CONF_USERNAME, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -CONF_CLIENT_ID = "client_id" -CONF_CLIENT_SECRET = "client_secret" CONF_SORT_BY = "sort_by" CONF_SUBREDDITS = "subreddits" diff --git a/homeassistant/components/remote/translations/ca.json b/homeassistant/components/remote/translations/ca.json index 76252d06ce8..94ff71f6d92 100644 --- a/homeassistant/components/remote/translations/ca.json +++ b/homeassistant/components/remote/translations/ca.json @@ -1,9 +1,9 @@ { "state": { "_": { - "off": "Apagat", - "on": "Enc\u00e8s" + "off": "OFF", + "on": "ON" } }, - "title": "Comandaments" + "title": "Comandament" } \ No newline at end of file diff --git a/homeassistant/components/ring/translations/ca.json b/homeassistant/components/ring/translations/ca.json index bcd3bd43af4..454956fe46c 100644 --- a/homeassistant/components/ring/translations/ca.json +++ b/homeassistant/components/ring/translations/ca.json @@ -19,7 +19,7 @@ "password": "Contrasenya", "username": "Nom d'usuari" }, - "title": "Inici de sessi\u00f3 amb un compte de Ring" + "title": "Inici de sessi\u00f3 amb Ring" } } } diff --git a/homeassistant/components/ring/translations/pl.json b/homeassistant/components/ring/translations/pl.json index d6f427aab4c..96aa7d39159 100644 --- a/homeassistant/components/ring/translations/pl.json +++ b/homeassistant/components/ring/translations/pl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." }, "error": { - "invalid_auth": "[%key_id:common::config_flow::error::invalid_auth%]", - "unknown": "[%key_id:common::config_flow::error::unknown%]" + "invalid_auth": "Niepoprawne uwierzytelnienie.", + "unknown": "Nieoczekiwany b\u0142\u0105d." }, "step": { "2fa": { @@ -16,8 +16,8 @@ }, "user": { "data": { - "password": "[%key_id:common::config_flow::data::password%]", - "username": "[%key_id:common::config_flow::data::username%]" + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" }, "title": "Zaloguj si\u0119 do konta Ring" } diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index ef233f64a1b..a3357ec4cf9 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -4,7 +4,7 @@ from datetime import timedelta import logging from typing import Any, Dict -from rokuecp import Roku, RokuError +from rokuecp import Roku, RokuConnectionError, RokuError from rokuecp.models import Device import voluptuous as vol @@ -92,6 +92,22 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo return unload_ok +def roku_exception_handler(func): + """Decorate Roku calls to handle Roku exceptions.""" + + async def handler(self, *args, **kwargs): + try: + await func(self, *args, **kwargs) + except RokuConnectionError as error: + if self.available: + _LOGGER.error("Error communicating with API: %s", error) + except RokuError as error: + if self.available: + _LOGGER.error("Invalid response from API: %s", error) + + return handler + + class RokuDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching Roku data.""" diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 69c9e24ad89..168d4a4a6fe 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -19,7 +19,7 @@ from homeassistant.components.media_player.const import ( ) from homeassistant.const import STATE_HOME, STATE_IDLE, STATE_PLAYING, STATE_STANDBY -from . import RokuDataUpdateCoordinator, RokuEntity +from . import RokuDataUpdateCoordinator, RokuEntity, roku_exception_handler from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -161,49 +161,60 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): """List of available input sources.""" return ["Home"] + sorted(app.name for app in self.coordinator.data.apps) + @roku_exception_handler async def async_turn_on(self) -> None: """Turn on the Roku.""" await self.coordinator.roku.remote("poweron") + @roku_exception_handler async def async_turn_off(self) -> None: """Turn off the Roku.""" await self.coordinator.roku.remote("poweroff") + @roku_exception_handler async def async_media_pause(self) -> None: """Send pause command.""" if self.state != STATE_STANDBY: await self.coordinator.roku.remote("play") + @roku_exception_handler async def async_media_play(self) -> None: """Send play command.""" if self.state != STATE_STANDBY: await self.coordinator.roku.remote("play") + @roku_exception_handler async def async_media_play_pause(self) -> None: """Send play/pause command.""" if self.state != STATE_STANDBY: await self.coordinator.roku.remote("play") + @roku_exception_handler async def async_media_previous_track(self) -> None: """Send previous track command.""" await self.coordinator.roku.remote("reverse") + @roku_exception_handler async def async_media_next_track(self) -> None: """Send next track command.""" await self.coordinator.roku.remote("forward") + @roku_exception_handler async def async_mute_volume(self, mute) -> None: """Mute the volume.""" await self.coordinator.roku.remote("volume_mute") + @roku_exception_handler async def async_volume_up(self) -> None: """Volume up media player.""" await self.coordinator.roku.remote("volume_up") + @roku_exception_handler async def async_volume_down(self) -> None: """Volume down media player.""" await self.coordinator.roku.remote("volume_down") + @roku_exception_handler async def async_play_media(self, media_type: str, media_id: str, **kwargs) -> None: """Tune to channel.""" if media_type != MEDIA_TYPE_CHANNEL: @@ -216,6 +227,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): await self.coordinator.roku.tune(media_id) + @roku_exception_handler async def async_select_source(self, source: str) -> None: """Select input source.""" if source == "Home": diff --git a/homeassistant/components/roku/remote.py b/homeassistant/components/roku/remote.py index 99e398fea68..5b893b6a0f8 100644 --- a/homeassistant/components/roku/remote.py +++ b/homeassistant/components/roku/remote.py @@ -5,7 +5,7 @@ from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType -from . import RokuDataUpdateCoordinator, RokuEntity +from . import RokuDataUpdateCoordinator, RokuEntity, roku_exception_handler from .const import DOMAIN @@ -43,14 +43,17 @@ class RokuRemote(RokuEntity, RemoteEntity): """Return true if device is on.""" return not self.coordinator.data.state.standby + @roku_exception_handler async def async_turn_on(self, **kwargs) -> None: """Turn the device on.""" await self.coordinator.roku.remote("poweron") + @roku_exception_handler async def async_turn_off(self, **kwargs) -> None: """Turn the device off.""" await self.coordinator.roku.remote("poweroff") + @roku_exception_handler async def async_send_command(self, command: List, **kwargs) -> None: """Send a command to one device.""" num_repeats = kwargs[ATTR_NUM_REPEATS] diff --git a/homeassistant/components/roku/translations/ca.json b/homeassistant/components/roku/translations/ca.json index 60382ca137a..7044b9c7bc0 100644 --- a/homeassistant/components/roku/translations/ca.json +++ b/homeassistant/components/roku/translations/ca.json @@ -1,24 +1,23 @@ { "config": { "abort": { - "already_configured": "El dispositiu Roku ja est\u00e0 configurat", + "already_configured": "El dispositiu ja est\u00e0 configurat", "unknown": "Error inesperat" }, "error": { - "cannot_connect": "No s'ha pogut connectar, torna-ho a provar" + "cannot_connect": "No s'ha pogut connectar" }, "flow_title": "Roku: {name}", "step": { "ssdp_confirm": { - "description": "Vols configurar {name}? Es substituiran les configuracions manuals d'aquest dispositiu en els arxius yaml.", + "description": "Vols configurar {name}?", "title": "Roku" }, "user": { "data": { - "host": "Amfitri\u00f3 o adre\u00e7a IP" + "host": "Amfitri\u00f3" }, - "description": "Introdueix la teva informaci\u00f3 de Roku.", - "title": "Roku" + "description": "Introdueix la teva informaci\u00f3 de Roku." } } } diff --git a/homeassistant/components/roku/translations/de.json b/homeassistant/components/roku/translations/de.json index 90b30dfafdf..45693f05f88 100644 --- a/homeassistant/components/roku/translations/de.json +++ b/homeassistant/components/roku/translations/de.json @@ -21,8 +21,7 @@ "data": { "host": "Host oder IP-Adresse" }, - "description": "Geben Sie Ihre Roku-Informationen ein.", - "title": "Roku" + "description": "Geben Sie Ihre Roku-Informationen ein." } } } diff --git a/homeassistant/components/roku/translations/en.json b/homeassistant/components/roku/translations/en.json index 194705cc7cc..6facd1f3a7c 100644 --- a/homeassistant/components/roku/translations/en.json +++ b/homeassistant/components/roku/translations/en.json @@ -17,8 +17,7 @@ "data": { "host": "Host" }, - "description": "Enter your Roku information.", - "title": "Roku" + "description": "Enter your Roku information." } } } diff --git a/homeassistant/components/roku/translations/es-419.json b/homeassistant/components/roku/translations/es-419.json index 40a76670fe1..00b69c53c72 100644 --- a/homeassistant/components/roku/translations/es-419.json +++ b/homeassistant/components/roku/translations/es-419.json @@ -17,8 +17,7 @@ "data": { "host": "Host o direcci\u00f3n IP" }, - "description": "Ingrese su informaci\u00f3n de Roku.", - "title": "Roku" + "description": "Ingrese su informaci\u00f3n de Roku." } } } diff --git a/homeassistant/components/roku/translations/es.json b/homeassistant/components/roku/translations/es.json index 3f190d41331..222dd1eddec 100644 --- a/homeassistant/components/roku/translations/es.json +++ b/homeassistant/components/roku/translations/es.json @@ -17,8 +17,7 @@ "data": { "host": "Host o direcci\u00f3n IP" }, - "description": "Introduce tu informaci\u00f3n de Roku.", - "title": "Roku" + "description": "Introduce tu informaci\u00f3n de Roku." } } } diff --git a/homeassistant/components/roku/translations/fr.json b/homeassistant/components/roku/translations/fr.json index e2fb99f1d2a..9f7e98423d7 100644 --- a/homeassistant/components/roku/translations/fr.json +++ b/homeassistant/components/roku/translations/fr.json @@ -21,8 +21,7 @@ "data": { "host": "H\u00f4te ou adresse IP" }, - "description": "Entrez vos informations Roku.", - "title": "Roku" + "description": "Entrez vos informations Roku." } } } diff --git a/homeassistant/components/roku/translations/hu.json b/homeassistant/components/roku/translations/hu.json index ab0e6cbad74..8b4e9f9d54d 100644 --- a/homeassistant/components/roku/translations/hu.json +++ b/homeassistant/components/roku/translations/hu.json @@ -1,7 +1,18 @@ { "config": { "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "unknown": "V\u00e1ratlan hiba" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "host": "Hoszt" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/roku/translations/it.json b/homeassistant/components/roku/translations/it.json index 0192c848e09..abbe29fb2a7 100644 --- a/homeassistant/components/roku/translations/it.json +++ b/homeassistant/components/roku/translations/it.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "Il dispositivo Roku \u00e8 gi\u00e0 configurato", + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "unknown": "Errore imprevisto" }, "error": { - "cannot_connect": "Impossibile connettersi, si prega di riprovare" + "cannot_connect": "Impossibile connettersi" }, "flow_title": "Roku: {name}", "step": { @@ -19,10 +19,9 @@ }, "user": { "data": { - "host": "Host o indirizzo IP" + "host": "Host" }, - "description": "Inserisci le tue informazioni Roku.", - "title": "Roku" + "description": "Inserisci le tue informazioni Roku." } } } diff --git a/homeassistant/components/roku/translations/ko.json b/homeassistant/components/roku/translations/ko.json index f6b4066948b..054c1674884 100644 --- a/homeassistant/components/roku/translations/ko.json +++ b/homeassistant/components/roku/translations/ko.json @@ -17,8 +17,7 @@ "data": { "host": "\ud638\uc2a4\ud2b8" }, - "description": "Roku \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", - "title": "Roku" + "description": "Roku \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694." } } } diff --git a/homeassistant/components/roku/translations/lb.json b/homeassistant/components/roku/translations/lb.json index 8674c6802be..896efe8b2fa 100644 --- a/homeassistant/components/roku/translations/lb.json +++ b/homeassistant/components/roku/translations/lb.json @@ -21,8 +21,7 @@ "data": { "host": "Numm oder IP Adresse" }, - "description": "F\u00ebll d\u00e9ng Roku Informatiounen aus.", - "title": "Roku" + "description": "F\u00ebll d\u00e9ng Roku Informatiounen aus." } } } diff --git a/homeassistant/components/roku/translations/nl.json b/homeassistant/components/roku/translations/nl.json index 8f2fae7146e..1d793df1bf4 100644 --- a/homeassistant/components/roku/translations/nl.json +++ b/homeassistant/components/roku/translations/nl.json @@ -17,8 +17,7 @@ "data": { "host": "Host- of IP-adres" }, - "description": "Voer uw Roku-informatie in.", - "title": "Roku" + "description": "Voer uw Roku-informatie in." } } } diff --git a/homeassistant/components/roku/translations/no.json b/homeassistant/components/roku/translations/no.json index e96931bce47..e2c637ac957 100644 --- a/homeassistant/components/roku/translations/no.json +++ b/homeassistant/components/roku/translations/no.json @@ -17,8 +17,7 @@ "data": { "host": "Vert eller IP-adresse" }, - "description": "Fyll inn Roku-informasjonen din.", - "title": "" + "description": "Fyll inn Roku-informasjonen din." } } } diff --git a/homeassistant/components/roku/translations/pl.json b/homeassistant/components/roku/translations/pl.json index 57f314126cb..7ca0148ce35 100644 --- a/homeassistant/components/roku/translations/pl.json +++ b/homeassistant/components/roku/translations/pl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]", - "unknown": "[%key_id:common::config_flow::error::unknown%]" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "unknown": "Nieoczekiwany b\u0142\u0105d." }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie." + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." }, "flow_title": "Roku: {name}", "step": { @@ -21,10 +21,9 @@ }, "user": { "data": { - "host": "[%key_id:common::config_flow::data::host%]" + "host": "Nazwa hosta lub adres IP" }, - "description": "Wprowad\u017a dane Roku.", - "title": "Roku" + "description": "Wprowad\u017a dane Roku." } } } diff --git a/homeassistant/components/roku/translations/pt-BR.json b/homeassistant/components/roku/translations/pt-BR.json new file mode 100644 index 00000000000..d866e4d4ee2 --- /dev/null +++ b/homeassistant/components/roku/translations/pt-BR.json @@ -0,0 +1,14 @@ +{ + "config": { + "flow_title": "Roku: {name}", + "step": { + "ssdp_confirm": { + "description": "Voc\u00ea quer configurar o {name}?", + "title": "Roku" + }, + "user": { + "description": "Digite suas informa\u00e7\u00f5es de Roku." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/translations/ru.json b/homeassistant/components/roku/translations/ru.json index 3500aa96e04..bddfab208c3 100644 --- a/homeassistant/components/roku/translations/ru.json +++ b/homeassistant/components/roku/translations/ru.json @@ -17,8 +17,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e Roku.", - "title": "Roku" + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e Roku." } } } diff --git a/homeassistant/components/roku/translations/sl.json b/homeassistant/components/roku/translations/sl.json index fd442881b81..4a198b3b5c9 100644 --- a/homeassistant/components/roku/translations/sl.json +++ b/homeassistant/components/roku/translations/sl.json @@ -23,8 +23,7 @@ "data": { "host": "Gostitelj ali IP naslov" }, - "description": "Vnesite va\u0161e Roku podatke.", - "title": "Roku" + "description": "Vnesite va\u0161e Roku podatke." } } } diff --git a/homeassistant/components/roku/translations/zh-Hant.json b/homeassistant/components/roku/translations/zh-Hant.json index b0599ce200a..94e6d6cb489 100644 --- a/homeassistant/components/roku/translations/zh-Hant.json +++ b/homeassistant/components/roku/translations/zh-Hant.json @@ -17,8 +17,7 @@ "data": { "host": "\u4e3b\u6a5f\u7aef" }, - "description": "\u8f38\u5165 Roku \u8cc7\u8a0a\u3002", - "title": "Roku" + "description": "\u8f38\u5165 Roku \u8cc7\u8a0a\u3002" } } } diff --git a/homeassistant/components/roomba/irobot_base.py b/homeassistant/components/roomba/irobot_base.py index f510f4965b0..8bc1e22547f 100644 --- a/homeassistant/components/roomba/irobot_base.py +++ b/homeassistant/components/roomba/irobot_base.py @@ -68,10 +68,10 @@ class IRobotEntity(Entity): """Initialize the iRobot handler.""" self.vacuum = roomba self._blid = blid - vacuum_state = roomba_reported_state(roomba) - self._name = vacuum_state.get("name") - self._version = vacuum_state.get("softwareVer") - self._sku = vacuum_state.get("sku") + self.vacuum_state = roomba_reported_state(roomba) + self._name = self.vacuum_state.get("name") + self._version = self.vacuum_state.get("softwareVer") + self._sku = self.vacuum_state.get("sku") @property def should_poll(self): @@ -99,13 +99,32 @@ class IRobotEntity(Entity): "model": self._sku, } + @property + def _battery_level(self): + """Return the battery level of the vacuum cleaner.""" + return self.vacuum_state.get("batPct") + + @property + def _robot_state(self): + """Return the state of the vacuum cleaner.""" + clean_mission_status = self.vacuum_state.get("cleanMissionStatus", {}) + cycle = clean_mission_status.get("cycle") + phase = clean_mission_status.get("phase") + try: + state = STATE_MAP[phase] + except KeyError: + return STATE_ERROR + if cycle != "none" and state in (STATE_IDLE, STATE_DOCKED): + state = STATE_PAUSED + return state + async def async_added_to_hass(self): """Register callback function.""" self.vacuum.register_on_message_callback(self.on_message) - def new_state_filter(self, new_state): - """Filter the new state.""" - raise NotImplementedError + def new_state_filter(self, new_state): # pylint: disable=no-self-use + """Filter out wifi state messages.""" + return len(new_state) > 1 or "signal" not in new_state def on_message(self, json_data): """Update state on message change.""" @@ -120,7 +139,6 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity): def __init__(self, roomba, blid): """Initialize the iRobot handler.""" super().__init__(roomba, blid) - self.vacuum_state = roomba_reported_state(roomba) self._cap_position = self.vacuum_state.get("cap", {}).get("pose") == 1 @property @@ -128,34 +146,15 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity): """Flag vacuum cleaner robot features that are supported.""" return SUPPORT_IROBOT - @property - def fan_speed(self): - """Return the fan speed of the vacuum cleaner.""" - return None - - @property - def fan_speed_list(self): - """Get the list of available fan speed steps of the vacuum cleaner.""" - return [] - @property def battery_level(self): """Return the battery level of the vacuum cleaner.""" - return self.vacuum_state.get("batPct") + return self._battery_level @property def state(self): """Return the state of the vacuum cleaner.""" - clean_mission_status = self.vacuum_state.get("cleanMissionStatus", {}) - cycle = clean_mission_status.get("cycle") - phase = clean_mission_status.get("phase") - try: - state = STATE_MAP[phase] - except KeyError: - return STATE_ERROR - if cycle != "none" and state in (STATE_IDLE, STATE_DOCKED): - state = STATE_PAUSED - return state + return self._robot_state @property def available(self) -> bool: @@ -215,14 +214,10 @@ class IRobotVacuum(IRobotEntity, StateVacuumEntity): def on_message(self, json_data): """Update state on message change.""" - new_state = json_data.get("state", {}).get("reported", {}) - if ( - len(new_state) == 1 and "signal" in new_state - ): # filter out wifi stat messages - return - _LOGGER.debug("Got new state from the vacuum: %s", json_data) - self.vacuum_state = roomba_reported_state(self.vacuum) - self.schedule_update_ha_state() + state = json_data.get("state", {}).get("reported", {}) + if self.new_state_filter(state): + _LOGGER.debug("Got new state from the vacuum: %s", json_data) + self.schedule_update_ha_state() async def async_start(self): """Start or resume the cleaning task.""" diff --git a/homeassistant/components/roomba/sensor.py b/homeassistant/components/roomba/sensor.py index 40dbd52e158..435f26510f8 100644 --- a/homeassistant/components/roomba/sensor.py +++ b/homeassistant/components/roomba/sensor.py @@ -1,9 +1,10 @@ """Sensor for checking the battery level of Roomba.""" import logging +from homeassistant.components.vacuum import STATE_DOCKED from homeassistant.const import DEVICE_CLASS_BATTERY, UNIT_PERCENTAGE +from homeassistant.helpers.icon import icon_for_battery_level -from . import roomba_reported_state from .const import BLID, DOMAIN, ROOMBA_SESSION from .irobot_base import IRobotEntity @@ -42,11 +43,16 @@ class RoombaBattery(IRobotEntity): """Return the unit_of_measurement of the device.""" return UNIT_PERCENTAGE + @property + def icon(self): + """Return the icon for the battery.""" + charging = bool(self._robot_state == STATE_DOCKED) + + return icon_for_battery_level( + battery_level=self._battery_level, charging=charging + ) + @property def state(self): """Return the state of the sensor.""" - return roomba_reported_state(self.vacuum).get("batPct") - - def new_state_filter(self, new_state): - """Filter the new state.""" - return "batPct" in new_state + return self._battery_level diff --git a/homeassistant/components/roomba/translations/ca.json b/homeassistant/components/roomba/translations/ca.json index a133139a8e9..3d7efbe53ae 100644 --- a/homeassistant/components/roomba/translations/ca.json +++ b/homeassistant/components/roomba/translations/ca.json @@ -1,17 +1,15 @@ { "config": { "error": { - "cannot_connect": "No s'ha pogut connectar, torna-ho a provar", - "unknown": "Error inesperat" + "cannot_connect": "No s'ha pogut connectar, torna-ho a provar" }, "step": { "user": { "data": { "blid": "BLID", - "certificate": "Certificat", "continuous": "Cont\u00ednua", "delay": "Retard", - "host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP", + "host": "Amfitri\u00f3", "password": "Contrasenya" }, "description": "Actualment la recuperaci\u00f3 de BLID i la contrasenya \u00e9s un proc\u00e9s manual. Segueix els passos de la documentaci\u00f3 a: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", diff --git a/homeassistant/components/roomba/translations/de.json b/homeassistant/components/roomba/translations/de.json index 5781f5553ba..667f68c454c 100644 --- a/homeassistant/components/roomba/translations/de.json +++ b/homeassistant/components/roomba/translations/de.json @@ -1,14 +1,12 @@ { "config": { "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", - "unknown": "Unerwarteter Fehler" + "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut" }, "step": { "user": { "data": { "blid": "BLID", - "certificate": "Zertifikat", "continuous": "Kontinuierlich", "delay": "Verz\u00f6gerung", "host": "Hostname oder IP-Adresse", diff --git a/homeassistant/components/roomba/translations/en.json b/homeassistant/components/roomba/translations/en.json index a4afc71e5df..677ea5f4749 100644 --- a/homeassistant/components/roomba/translations/en.json +++ b/homeassistant/components/roomba/translations/en.json @@ -1,14 +1,12 @@ { "config": { "error": { - "cannot_connect": "Failed to connect, please try again", - "unknown": "Unexpected error" + "cannot_connect": "Failed to connect, please try again" }, "step": { "user": { "data": { "blid": "BLID", - "certificate": "Certificate", "continuous": "Continuous", "delay": "Delay", "host": "Host", diff --git a/homeassistant/components/roomba/translations/es-419.json b/homeassistant/components/roomba/translations/es-419.json index d452c82b112..6d0295f7ce0 100644 --- a/homeassistant/components/roomba/translations/es-419.json +++ b/homeassistant/components/roomba/translations/es-419.json @@ -1,14 +1,12 @@ { "config": { "error": { - "cannot_connect": "No se pudo conectar, intente nuevamente", - "unknown": "Error inesperado" + "cannot_connect": "No se pudo conectar, intente nuevamente" }, "step": { "user": { "data": { "blid": "BLID", - "certificate": "Certificado", "continuous": "Continuo", "delay": "Retraso", "host": "Nombre de host o direcci\u00f3n IP", diff --git a/homeassistant/components/roomba/translations/es.json b/homeassistant/components/roomba/translations/es.json index 28539c8fd5f..22f1ead1a56 100644 --- a/homeassistant/components/roomba/translations/es.json +++ b/homeassistant/components/roomba/translations/es.json @@ -1,14 +1,12 @@ { "config": { "error": { - "cannot_connect": "No se pudo conectar, por favor, int\u00e9ntalo de nuevo", - "unknown": "Error inesperado" + "cannot_connect": "No se pudo conectar, por favor, int\u00e9ntalo de nuevo" }, "step": { "user": { "data": { "blid": "BLID", - "certificate": "Certificado", "continuous": "Continuo", "delay": "Retardo", "host": "Nombre del host o direcci\u00f3n IP", diff --git a/homeassistant/components/roomba/translations/fr.json b/homeassistant/components/roomba/translations/fr.json index 49d021bc198..d4025d2cddf 100644 --- a/homeassistant/components/roomba/translations/fr.json +++ b/homeassistant/components/roomba/translations/fr.json @@ -1,12 +1,8 @@ { "config": { - "error": { - "unknown": "Erreur inattendue" - }, "step": { "user": { "data": { - "certificate": "Certificat", "continuous": "En continu", "delay": "D\u00e9lai", "host": "Nom d'h\u00f4te ou adresse IP", diff --git a/homeassistant/components/roomba/translations/hu.json b/homeassistant/components/roomba/translations/hu.json new file mode 100644 index 00000000000..357ca74746d --- /dev/null +++ b/homeassistant/components/roomba/translations/hu.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hoszt", + "password": "Jelsz\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/it.json b/homeassistant/components/roomba/translations/it.json index 042959a67c4..babd2082b3c 100644 --- a/homeassistant/components/roomba/translations/it.json +++ b/homeassistant/components/roomba/translations/it.json @@ -1,17 +1,15 @@ { "config": { "error": { - "cannot_connect": "Impossibile connettersi, si prega di riprovare", - "unknown": "Errore imprevisto" + "cannot_connect": "Impossibile connettersi, si prega di riprovare" }, "step": { "user": { "data": { "blid": "BLID", - "certificate": "Certificato", "continuous": "Continuo", "delay": "Ritardo", - "host": "Nome dell'host o indirizzo IP", + "host": "Host", "password": "Password" }, "description": "Attualmente il recupero del BLID e della password \u00e8 un processo manuale. Si prega di seguire i passi descritti nella documentazione all'indirizzo: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", diff --git a/homeassistant/components/roomba/translations/ko.json b/homeassistant/components/roomba/translations/ko.json index 7990cd22bdb..ebf9056c037 100644 --- a/homeassistant/components/roomba/translations/ko.json +++ b/homeassistant/components/roomba/translations/ko.json @@ -1,14 +1,12 @@ { "config": { "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." }, "step": { "user": { "data": { "blid": "BLID", - "certificate": "\uc778\uc99d\uc11c", "continuous": "\uc5f0\uc18d", "delay": "\uc9c0\uc5f0", "host": "\ud638\uc2a4\ud2b8", diff --git a/homeassistant/components/roomba/translations/lb.json b/homeassistant/components/roomba/translations/lb.json index 2771b3f5b2a..9898d7fbd04 100644 --- a/homeassistant/components/roomba/translations/lb.json +++ b/homeassistant/components/roomba/translations/lb.json @@ -1,14 +1,12 @@ { "config": { "error": { - "cannot_connect": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol.", - "unknown": "Onerwaarte Feeler" + "cannot_connect": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol." }, "step": { "user": { "data": { "blid": "BLID", - "certificate": "Zertifikat", "continuous": "Kontinu\u00e9ierlech", "delay": "Delai", "host": "Host Numm oder IP Adresse", diff --git a/homeassistant/components/roomba/translations/nl.json b/homeassistant/components/roomba/translations/nl.json index d49a9f488de..f5268bdf799 100644 --- a/homeassistant/components/roomba/translations/nl.json +++ b/homeassistant/components/roomba/translations/nl.json @@ -1,14 +1,12 @@ { "config": { "error": { - "cannot_connect": "Verbinding mislukt, probeer het opnieuw", - "unknown": "Onverwachte fout" + "cannot_connect": "Verbinding mislukt, probeer het opnieuw" }, "step": { "user": { "data": { "blid": "BLID", - "certificate": "Certificaat", "continuous": "Doorlopend", "delay": "Vertraging", "host": "Hostnaam of IP-adres", diff --git a/homeassistant/components/roomba/translations/no.json b/homeassistant/components/roomba/translations/no.json index c145637a656..10b65f4f315 100644 --- a/homeassistant/components/roomba/translations/no.json +++ b/homeassistant/components/roomba/translations/no.json @@ -1,14 +1,12 @@ { "config": { "error": { - "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen", - "unknown": "Uventet feil" + "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen" }, "step": { "user": { "data": { "blid": "Blid", - "certificate": "Sertifikat", "continuous": "Kontinuerlige", "delay": "Forsinkelse", "host": "Vertsnavn eller IP-adresse", diff --git a/homeassistant/components/roomba/translations/pl.json b/homeassistant/components/roomba/translations/pl.json index 0598a9ea247..4600e3f5c95 100644 --- a/homeassistant/components/roomba/translations/pl.json +++ b/homeassistant/components/roomba/translations/pl.json @@ -1,21 +1,19 @@ { "config": { "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "unknown": "[%key_id:common::config_flow::error::unknown%]" + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie." }, "step": { "user": { "data": { "blid": "BLID", - "certificate": "Certyfikat", "continuous": "Ci\u0105g\u0142y", "delay": "Op\u00f3\u017anienie", - "host": "[%key_id:common::config_flow::data::host%]", - "password": "[%key_id:common::config_flow::data::password%]" + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o" }, "description": "Obecnie pobieranie BLID i has\u0142a jest procesem r\u0119cznym. Prosz\u0119 post\u0119powa\u0107 zgodnie z instrukcjami zawartymi w dokumentacji pod adresem: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials.", - "title": "Po\u0142\u0105cz z urz\u0105dzeniem" + "title": "Po\u0142\u0105czenie z urz\u0105dzeniem" } } }, diff --git a/homeassistant/components/roomba/translations/pt-BR.json b/homeassistant/components/roomba/translations/pt-BR.json new file mode 100644 index 00000000000..a148d1976ad --- /dev/null +++ b/homeassistant/components/roomba/translations/pt-BR.json @@ -0,0 +1,28 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha ao conectar, tente novamente" + }, + "step": { + "user": { + "data": { + "blid": "BLID", + "continuous": "Cont\u00ednuo", + "delay": "Atraso" + }, + "description": "Atualmente, a recupera\u00e7\u00e3o do BLID e da senha \u00e9 um processo manual. Siga as etapas descritas na documenta\u00e7\u00e3o em: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", + "title": "Conecte-se ao dispositivo" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "continuous": "Cont\u00ednuo", + "delay": "Atraso" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/pt.json b/homeassistant/components/roomba/translations/pt.json index b063d57eb39..559712b7e9b 100644 --- a/homeassistant/components/roomba/translations/pt.json +++ b/homeassistant/components/roomba/translations/pt.json @@ -1,13 +1,11 @@ { "config": { "error": { - "cannot_connect": "Falha ao conectar, tente novamente", - "unknown": "Erro inesperado" + "cannot_connect": "Falha ao conectar, tente novamente" }, "step": { "user": { "data": { - "certificate": "Certificado", "continuous": "Cont\u00ednuo", "delay": "Atraso", "host": "Nome servidor ou endere\u00e7o IP", diff --git a/homeassistant/components/roomba/translations/ru.json b/homeassistant/components/roomba/translations/ru.json index fd8af88a9e2..12217a1bc3f 100644 --- a/homeassistant/components/roomba/translations/ru.json +++ b/homeassistant/components/roomba/translations/ru.json @@ -1,14 +1,12 @@ { "config": { "error": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437." }, "step": { "user": { "data": { "blid": "BLID", - "certificate": "\u0421\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442", "continuous": "\u041d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c", "delay": "\u0417\u0430\u0434\u0435\u0440\u0436\u043a\u0430 (\u0441\u0435\u043a.)", "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/roomba/translations/sl.json b/homeassistant/components/roomba/translations/sl.json index 4b834d05fc8..9ad90ab82aa 100644 --- a/homeassistant/components/roomba/translations/sl.json +++ b/homeassistant/components/roomba/translations/sl.json @@ -1,14 +1,12 @@ { "config": { "error": { - "cannot_connect": "Povezava ni uspela, poskusite znova", - "unknown": "Nepri\u010dakovana napaka" + "cannot_connect": "Povezava ni uspela, poskusite znova" }, "step": { "user": { "data": { "blid": "BLID", - "certificate": "Potrdilo", "continuous": "Nenehno", "delay": "Zamik", "host": "Ime gostitelja ali naslov IP", diff --git a/homeassistant/components/roomba/translations/sv.json b/homeassistant/components/roomba/translations/sv.json index 46e0474cc50..ee1f8972ef9 100644 --- a/homeassistant/components/roomba/translations/sv.json +++ b/homeassistant/components/roomba/translations/sv.json @@ -1,14 +1,12 @@ { "config": { "error": { - "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", - "unknown": "Ov\u00e4ntat fel" + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen" }, "step": { "user": { "data": { "blid": "BLID", - "certificate": "Certifikat", "continuous": "Kontinuerlig", "delay": "F\u00f6rdr\u00f6jning", "host": "V\u00e4rdnamn eller IP-adress", diff --git a/homeassistant/components/roomba/translations/zh-Hant.json b/homeassistant/components/roomba/translations/zh-Hant.json index 13968bf7dda..5e51af5efb5 100644 --- a/homeassistant/components/roomba/translations/zh-Hant.json +++ b/homeassistant/components/roomba/translations/zh-Hant.json @@ -1,14 +1,12 @@ { "config": { "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21" }, "step": { "user": { "data": { "blid": "BLID", - "certificate": "\u8a8d\u8b49", "continuous": "\u9023\u7e8c", "delay": "\u5ef6\u9072", "host": "\u4e3b\u6a5f\u7aef", diff --git a/homeassistant/components/samsungtv/translations/ca.json b/homeassistant/components/samsungtv/translations/ca.json index 46ea2bfa4c3..5a5fe7e7986 100644 --- a/homeassistant/components/samsungtv/translations/ca.json +++ b/homeassistant/components/samsungtv/translations/ca.json @@ -15,11 +15,10 @@ }, "user": { "data": { - "host": "Amfitri\u00f3 o adre\u00e7a IP", + "host": "Amfitri\u00f3", "name": "Nom" }, - "description": "Introdeix les dades del televisor Samsung. Si mai abans l'has connectat a Home Assistant haur\u00edes de veure una finestra emergent demanant autenticaci\u00f3.", - "title": "Televisor Samsung" + "description": "Introdeix les dades del televisor Samsung. Si mai abans l'has connectat a Home Assistant haur\u00edes de veure una finestra emergent demanant autenticaci\u00f3." } } } diff --git a/homeassistant/components/samsungtv/translations/da.json b/homeassistant/components/samsungtv/translations/da.json index ec83db6aab8..925abbe66ef 100644 --- a/homeassistant/components/samsungtv/translations/da.json +++ b/homeassistant/components/samsungtv/translations/da.json @@ -18,8 +18,7 @@ "host": "V\u00e6rt eller IP-adresse", "name": "Navn" }, - "description": "Indtast dine Samsung-tv-oplysninger. Hvis du aldrig har oprettet forbindelse til Home Assistant f\u00f8r, b\u00f8r du se en popup p\u00e5 dit tv, der beder om godkendelse.", - "title": "Samsung-tv" + "description": "Indtast dine Samsung-tv-oplysninger. Hvis du aldrig har oprettet forbindelse til Home Assistant f\u00f8r, b\u00f8r du se en popup p\u00e5 dit tv, der beder om godkendelse." } } } diff --git a/homeassistant/components/samsungtv/translations/de.json b/homeassistant/components/samsungtv/translations/de.json index 026f00a8cb2..dff4f98a438 100644 --- a/homeassistant/components/samsungtv/translations/de.json +++ b/homeassistant/components/samsungtv/translations/de.json @@ -18,8 +18,7 @@ "host": "Host oder IP-Adresse", "name": "Name" }, - "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" + "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." } } } diff --git a/homeassistant/components/samsungtv/translations/en.json b/homeassistant/components/samsungtv/translations/en.json index 7ee7c6bd8f4..35510858c55 100644 --- a/homeassistant/components/samsungtv/translations/en.json +++ b/homeassistant/components/samsungtv/translations/en.json @@ -18,8 +18,7 @@ "host": "Host", "name": "Name" }, - "description": "Enter your Samsung TV information. If you never connected Home Assistant before you should see a popup on your TV asking for authorization.", - "title": "Samsung TV" + "description": "Enter your Samsung TV information. If you never connected Home Assistant before you should see a popup on your TV asking for authorization." } } } diff --git a/homeassistant/components/samsungtv/translations/es-419.json b/homeassistant/components/samsungtv/translations/es-419.json index b35146e181e..c4938543d4a 100644 --- a/homeassistant/components/samsungtv/translations/es-419.json +++ b/homeassistant/components/samsungtv/translations/es-419.json @@ -18,8 +18,7 @@ "host": "Host o direcci\u00f3n IP", "name": "Nombre" }, - "description": "Ingrese la informaci\u00f3n de su televisor Samsung. Si nunca conect\u00f3 Home Assistant antes, deber\u00eda ver una ventana emergente en su televisor pidiendo autorizaci\u00f3n.", - "title": "Samsung TV" + "description": "Ingrese la informaci\u00f3n de su televisor Samsung. Si nunca conect\u00f3 Home Assistant antes, deber\u00eda ver una ventana emergente en su televisor pidiendo autorizaci\u00f3n." } } } diff --git a/homeassistant/components/samsungtv/translations/es.json b/homeassistant/components/samsungtv/translations/es.json index a12cce712ed..20bfe052924 100644 --- a/homeassistant/components/samsungtv/translations/es.json +++ b/homeassistant/components/samsungtv/translations/es.json @@ -18,8 +18,7 @@ "host": "Host o direcci\u00f3n IP", "name": "Nombre" }, - "description": "Introduce la informaci\u00f3n de tu televisi\u00f3n Samsung. Si nunca antes te conectaste con Home Assistant, deber\u00edas ver un mensaje en tu televisi\u00f3n pidiendo autorizaci\u00f3n.", - "title": "Samsung TV" + "description": "Introduce la informaci\u00f3n de tu televisi\u00f3n Samsung. Si nunca antes te conectaste con Home Assistant, deber\u00edas ver un mensaje en tu televisi\u00f3n pidiendo autorizaci\u00f3n." } } } diff --git a/homeassistant/components/samsungtv/translations/fr.json b/homeassistant/components/samsungtv/translations/fr.json index e37b3104b1c..5721aa7719c 100644 --- a/homeassistant/components/samsungtv/translations/fr.json +++ b/homeassistant/components/samsungtv/translations/fr.json @@ -18,8 +18,7 @@ "host": "H\u00f4te ou adresse IP", "name": "Nom" }, - "description": "Entrez les informations relatives \u00e0 votre t\u00e9l\u00e9viseur 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.", - "title": "TV Samsung" + "description": "Entrez les informations relatives \u00e0 votre t\u00e9l\u00e9viseur 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." } } } diff --git a/homeassistant/components/samsungtv/translations/hu.json b/homeassistant/components/samsungtv/translations/hu.json index 1704fa04897..8c47eb3ed2a 100644 --- a/homeassistant/components/samsungtv/translations/hu.json +++ b/homeassistant/components/samsungtv/translations/hu.json @@ -15,11 +15,10 @@ }, "user": { "data": { - "host": "Hosztn\u00e9v vagy IP c\u00edm", + "host": "Hoszt", "name": "N\u00e9v" }, - "description": "\u00cdrd be a Samsung TV adatait. Ha m\u00e9g soha nem csatlakozott Home Assistant-hez, akkor meg kell jelennie egy felugr\u00f3 ablaknak a TV k\u00e9perny\u0151j\u00e9n, ahol hiteles\u00edt\u00e9st k\u00e9r.", - "title": "Samsung TV" + "description": "\u00cdrd be a Samsung TV adatait. Ha m\u00e9g soha nem csatlakozott Home Assistant-hez, akkor meg kell jelennie egy felugr\u00f3 ablaknak a TV k\u00e9perny\u0151j\u00e9n, ahol hiteles\u00edt\u00e9st k\u00e9r." } } } diff --git a/homeassistant/components/samsungtv/translations/it.json b/homeassistant/components/samsungtv/translations/it.json index 7b62a48e62f..4236b1d8ed8 100644 --- a/homeassistant/components/samsungtv/translations/it.json +++ b/homeassistant/components/samsungtv/translations/it.json @@ -15,11 +15,10 @@ }, "user": { "data": { - "host": "Host o indirizzo IP", + "host": "Host", "name": "Nome" }, - "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" + "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." } } } diff --git a/homeassistant/components/samsungtv/translations/ko.json b/homeassistant/components/samsungtv/translations/ko.json index 523d9f8c45e..b635057750b 100644 --- a/homeassistant/components/samsungtv/translations/ko.json +++ b/homeassistant/components/samsungtv/translations/ko.json @@ -18,8 +18,7 @@ "host": "\ud638\uc2a4\ud2b8", "name": "\uc774\ub984" }, - "description": "\uc0bc\uc131 TV \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. Home Assistant \ub97c \uc5f0\uacb0 \ud55c \uc801\uc774 \uc5c6\ub2e4\uba74 TV \uc5d0\uc11c \uc778\uc99d\uc744 \uc694\uccad\ud558\ub294 \ud31d\uc5c5\uc774 \ud45c\uc2dc\ub429\ub2c8\ub2e4.", - "title": "\uc0bc\uc131 TV" + "description": "\uc0bc\uc131 TV \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. Home Assistant \ub97c \uc5f0\uacb0 \ud55c \uc801\uc774 \uc5c6\ub2e4\uba74 TV \uc5d0\uc11c \uc778\uc99d\uc744 \uc694\uccad\ud558\ub294 \ud31d\uc5c5\uc774 \ud45c\uc2dc\ub429\ub2c8\ub2e4." } } } diff --git a/homeassistant/components/samsungtv/translations/lb.json b/homeassistant/components/samsungtv/translations/lb.json index 8e54495a1b8..1fd1ce67f27 100644 --- a/homeassistant/components/samsungtv/translations/lb.json +++ b/homeassistant/components/samsungtv/translations/lb.json @@ -18,8 +18,7 @@ "host": "Numm oder IP Adresse", "name": "Numm" }, - "description": "Gitt \u00e4r Samsung TV Informatiounen un. Falls dir Home Assistant nach ni domat verbonnen hutt misst den TV eng Meldung mat enger Authentifiz\u00e9ierung uweisen.", - "title": "Samsnung TV" + "description": "Gitt \u00e4r Samsung TV Informatiounen un. Falls dir Home Assistant nach ni domat verbonnen hutt misst den TV eng Meldung mat enger Authentifiz\u00e9ierung uweisen." } } } diff --git a/homeassistant/components/samsungtv/translations/nl.json b/homeassistant/components/samsungtv/translations/nl.json index 2760ec7a181..17298e3bc8a 100644 --- a/homeassistant/components/samsungtv/translations/nl.json +++ b/homeassistant/components/samsungtv/translations/nl.json @@ -18,8 +18,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" + "description": "Voer uw Samsung TV informatie in. Als u nooit eerder Home Assistant hebt verbonden dan zou u een popup op uw TV moeten zien waarin u om toestemming wordt vraagt." } } } diff --git a/homeassistant/components/samsungtv/translations/no.json b/homeassistant/components/samsungtv/translations/no.json index ac6b4ac87f8..afd5f7c633f 100644 --- a/homeassistant/components/samsungtv/translations/no.json +++ b/homeassistant/components/samsungtv/translations/no.json @@ -18,8 +18,7 @@ "host": "Vert eller IP-adresse", "name": "Navn" }, - "description": "Fyll inn Samsung TV-informasjonen din. Hvis du aldri har koblet til Home Assistant f\u00f8r, vil en popup p\u00e5 TVen be om godkjenning.", - "title": "" + "description": "Fyll inn Samsung TV-informasjonen din. Hvis du aldri har koblet til Home Assistant f\u00f8r, vil en popup p\u00e5 TVen be om godkjenning." } } } diff --git a/homeassistant/components/samsungtv/translations/pl.json b/homeassistant/components/samsungtv/translations/pl.json index 94a909680fa..11de0686a33 100644 --- a/homeassistant/components/samsungtv/translations/pl.json +++ b/homeassistant/components/samsungtv/translations/pl.json @@ -10,16 +10,15 @@ "flow_title": "Samsung TV: {model}", "step": { "confirm": { - "description": "Czy chcesz skonfigurowa\u0107 telewizor Samsung {model}? Je\u015bli nigdy wcze\u015bniej ten telewizor nie by\u0142 \u0142\u0105czony z Home Assistant'em na jego ekranie powinna pojawi\u0107 si\u0119 pro\u015bba o uwierzytelnienie. R\u0119czne konfiguracje tego telewizora zostan\u0105 zast\u0105pione.", + "description": "Czy chcesz skonfigurowa\u0107 telewizor Samsung {model}? Je\u015bli nigdy wcze\u015bniej ten telewizor nie by\u0142 \u0142\u0105czony z Home Assistantem, na jego ekranie powinna pojawi\u0107 si\u0119 pro\u015bba o uwierzytelnienie. R\u0119czne konfiguracje tego telewizora zostan\u0105 zast\u0105pione.", "title": "Samsung TV" }, "user": { "data": { - "host": "[%key_id:common::config_flow::data::host%]", + "host": "Nazwa hosta lub adres IP", "name": "Nazwa" }, - "description": "Wprowad\u017a informacje o telewizorze Samsung. Je\u015bli nigdy wcze\u015bniej ten telewizor nie by\u0142 \u0142\u0105czony z Home Assistant'em na jego ekranie powinna pojawi\u0107 si\u0119 pro\u015bba o uwierzytelnienie.", - "title": "Samsung TV" + "description": "Wprowad\u017a informacje o telewizorze Samsung. Je\u015bli nigdy wcze\u015bniej ten telewizor nie by\u0142 \u0142\u0105czony z Home Assistantem." } } } diff --git a/homeassistant/components/samsungtv/translations/ru.json b/homeassistant/components/samsungtv/translations/ru.json index 53b9dcc3206..c5ee5e8348c 100644 --- a/homeassistant/components/samsungtv/translations/ru.json +++ b/homeassistant/components/samsungtv/translations/ru.json @@ -18,8 +18,7 @@ "host": "\u0425\u043e\u0441\u0442", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435 Samsung. \u0415\u0441\u043b\u0438 \u044d\u0442\u043e\u0442 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0440\u0430\u043d\u0435\u0435 \u043d\u0435 \u0431\u044b\u043b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d \u043a Home Assistant, \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u0434\u043e\u043b\u0436\u043d\u043e \u043f\u043e\u044f\u0432\u0438\u0442\u044c\u0441\u044f \u0432\u0441\u043f\u043b\u044b\u0432\u0430\u044e\u0449\u0435\u0435 \u043e\u043a\u043d\u043e \u0441 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u043c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", - "title": "\u0422\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Samsung" + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435 Samsung. \u0415\u0441\u043b\u0438 \u044d\u0442\u043e\u0442 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0440\u0430\u043d\u0435\u0435 \u043d\u0435 \u0431\u044b\u043b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d \u043a Home Assistant, \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u0434\u043e\u043b\u0436\u043d\u043e \u043f\u043e\u044f\u0432\u0438\u0442\u044c\u0441\u044f \u0432\u0441\u043f\u043b\u044b\u0432\u0430\u044e\u0449\u0435\u0435 \u043e\u043a\u043d\u043e \u0441 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u043c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438." } } } diff --git a/homeassistant/components/samsungtv/translations/sl.json b/homeassistant/components/samsungtv/translations/sl.json index f147edad4d9..1002be5efd1 100644 --- a/homeassistant/components/samsungtv/translations/sl.json +++ b/homeassistant/components/samsungtv/translations/sl.json @@ -18,8 +18,7 @@ "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" + "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." } } } diff --git a/homeassistant/components/samsungtv/translations/sv.json b/homeassistant/components/samsungtv/translations/sv.json index ce75d775f14..5caa8daa275 100644 --- a/homeassistant/components/samsungtv/translations/sv.json +++ b/homeassistant/components/samsungtv/translations/sv.json @@ -18,8 +18,7 @@ "host": "V\u00e4rdnamn eller IP-adress", "name": "Namn" }, - "description": "Ange informationen f\u00f6r din Samsung TV. Om du aldrig har anslutit denna till Home Assistant tidigare borde du se en popup om autentisering p\u00e5 din TV.", - "title": "Samsung TV" + "description": "Ange informationen f\u00f6r din Samsung TV. Om du aldrig har anslutit denna till Home Assistant tidigare borde du se en popup om autentisering p\u00e5 din TV." } } } diff --git a/homeassistant/components/samsungtv/translations/tr.json b/homeassistant/components/samsungtv/translations/tr.json index 3246b833382..296579c17f4 100644 --- a/homeassistant/components/samsungtv/translations/tr.json +++ b/homeassistant/components/samsungtv/translations/tr.json @@ -14,8 +14,7 @@ "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" + "description": "Samsung TV bilgilerini gir. Daha \u00f6nce hi\u00e7 Home Assistant'a ba\u011flamad\u0131ysan, TV'nde izin isteyen bir pencere g\u00f6receksindir." } } } diff --git a/homeassistant/components/samsungtv/translations/zh-Hant.json b/homeassistant/components/samsungtv/translations/zh-Hant.json index 2442cbcaf5f..973b84dd2ee 100644 --- a/homeassistant/components/samsungtv/translations/zh-Hant.json +++ b/homeassistant/components/samsungtv/translations/zh-Hant.json @@ -18,8 +18,7 @@ "host": "\u4e3b\u6a5f\u7aef", "name": "\u540d\u7a31" }, - "description": "\u8f38\u5165\u4e09\u661f\u96fb\u8996\u8cc7\u8a0a\u3002\u5047\u5982\u60a8\u4e4b\u524d\u672a\u66fe\u9023\u7dda\u81f3 Home Assistant\uff0c\u61c9\u8a72\u6703\u65bc\u96fb\u8996\u4e0a\u6536\u5230\u9a57\u8b49\u8a0a\u606f\u3002", - "title": "\u4e09\u661f\u96fb\u8996" + "description": "\u8f38\u5165\u4e09\u661f\u96fb\u8996\u8cc7\u8a0a\u3002\u5047\u5982\u60a8\u4e4b\u524d\u672a\u66fe\u9023\u7dda\u81f3 Home Assistant\uff0c\u61c9\u8a72\u6703\u65bc\u96fb\u8996\u4e0a\u6536\u5230\u9a57\u8b49\u8a0a\u606f\u3002" } } } diff --git a/homeassistant/components/scene/translations/ca.json b/homeassistant/components/scene/translations/ca.json index 744d02a54b2..92aa6caa099 100644 --- a/homeassistant/components/scene/translations/ca.json +++ b/homeassistant/components/scene/translations/ca.json @@ -1,3 +1,3 @@ { - "title": "Escenes" + "title": "Escena" } \ No newline at end of file diff --git a/homeassistant/components/script/translations/ca.json b/homeassistant/components/script/translations/ca.json index af31164cf4c..4369856606f 100644 --- a/homeassistant/components/script/translations/ca.json +++ b/homeassistant/components/script/translations/ca.json @@ -1,9 +1,9 @@ { "state": { "_": { - "off": "Desactivat", - "on": "Activat" + "off": "OFF", + "on": "ON" } }, - "title": "Programes (scripts)" + "title": "Programa (script)" } \ No newline at end of file diff --git a/homeassistant/components/season/translations/sensor.pt-BR.json b/homeassistant/components/season/translations/sensor.pt-BR.json index 4c81e432350..3f157f43c79 100644 --- a/homeassistant/components/season/translations/sensor.pt-BR.json +++ b/homeassistant/components/season/translations/sensor.pt-BR.json @@ -1,5 +1,11 @@ { "state": { + "season__season": { + "autumn": "Outono", + "spring": "Primavera", + "summer": "Ver\u00e3o", + "winter": "Inverno" + }, "season__season__": { "autumn": "Outono", "spring": "Primavera", diff --git a/homeassistant/components/sense/translations/hu.json b/homeassistant/components/sense/translations/hu.json new file mode 100644 index 00000000000..0085d9ea9c4 --- /dev/null +++ b/homeassistant/components/sense/translations/hu.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "email": "E-mail", + "password": "Jelsz\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/it.json b/homeassistant/components/sense/translations/it.json index 2320eef1a9b..4e1ddd01b42 100644 --- a/homeassistant/components/sense/translations/it.json +++ b/homeassistant/components/sense/translations/it.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "email": "Indirizzo E-Mail", + "email": "E-mail", "password": "Password" }, "title": "Connettiti al tuo Sense Energy Monitor" diff --git a/homeassistant/components/sense/translations/pl.json b/homeassistant/components/sense/translations/pl.json index 3a1a75d1c14..c32b61e30ad 100644 --- a/homeassistant/components/sense/translations/pl.json +++ b/homeassistant/components/sense/translations/pl.json @@ -1,18 +1,18 @@ { "config": { "abort": { - "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "[%key_id:common::config_flow::error::invalid_auth%]", - "unknown": "[%key_id:common::config_flow::error::unknown%]" + "invalid_auth": "Niepoprawne uwierzytelnienie.", + "unknown": "Nieoczekiwany b\u0142\u0105d." }, "step": { "user": { "data": { - "email": "[%key_id:common::config_flow::data::email%]", - "password": "[%key_id:common::config_flow::data::password%]" + "email": "Adres e-mail", + "password": "Has\u0142o" }, "title": "Po\u0142\u0105czenie z monitorem energii Sense" } diff --git a/homeassistant/components/sense/translations/pt-BR.json b/homeassistant/components/sense/translations/pt-BR.json new file mode 100644 index 00000000000..d04d91c034b --- /dev/null +++ b/homeassistant/components/sense/translations/pt-BR.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar, tente novamente", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/ca.json b/homeassistant/components/sensor/translations/ca.json index d2b34d0d70e..d80fd795698 100644 --- a/homeassistant/components/sensor/translations/ca.json +++ b/homeassistant/components/sensor/translations/ca.json @@ -1,33 +1,33 @@ { "device_automation": { "condition_type": { - "is_battery_level": "Nivell de bateria de {entity_name}", - "is_humidity": "Humitat de {entity_name}", - "is_illuminance": "Il\u00b7luminaci\u00f3 de {entity_name}", - "is_power": "Pot\u00e8ncia de {entity_name}", - "is_pressure": "Pressi\u00f3 de {entity_name}", - "is_signal_strength": "For\u00e7a del senyal de {entity_name}", - "is_temperature": "Temperatura de {entity_name}", - "is_timestamp": "Marca de temps de {entity_name}", - "is_value": "Valor de {entity_name}" + "is_battery_level": "Nivell de bateria actual de {entity_name}", + "is_humidity": "Humitat actual de {entity_name}", + "is_illuminance": "Il\u00b7luminaci\u00f3 actual de {entity_name}", + "is_power": "Pot\u00e8ncia actual de {entity_name}", + "is_pressure": "Pressi\u00f3 actual de {entity_name}", + "is_signal_strength": "Pot\u00e8ncia de senyal actual de {entity_name}", + "is_temperature": "Temperatura actual de {entity_name}", + "is_timestamp": "Marca temporal actual de {entity_name}", + "is_value": "Valor actual de {entity_name}" }, "trigger_type": { - "battery_level": "Nivell de bateria de {entity_name}", - "humidity": "Humitat de {entity_name}", - "illuminance": "Il\u00b7luminaci\u00f3 de {entity_name}", - "power": "Pot\u00e8ncia de {entity_name}", - "pressure": "Pressi\u00f3 de {entity_name}", - "signal_strength": "For\u00e7a del senyal de {entity_name}", - "temperature": "Temperatura de {entity_name}", - "timestamp": "Marca de temps de {entity_name}", - "value": "Valor de {entity_name}" + "battery_level": "Canvia el nivell de bateria de {entity_name}", + "humidity": "Canvia la humitat de {entity_name}", + "illuminance": "Canvia la il\u00b7luminaci\u00f3 de {entity_name}", + "power": "Canvia la pot\u00e8ncia de {entity_name}", + "pressure": "Canvia la pressi\u00f3 de {entity_name}", + "signal_strength": "Canvia la pot\u00e8ncia de senyal de {entity_name}", + "temperature": "Canvia la temperatura de {entity_name}", + "timestamp": "Canvia la marca temporal de {entity_name}", + "value": "Canvia el valor de {entity_name}" } }, "state": { "_": { - "off": "Desactivat", - "on": "Activat" + "off": "OFF", + "on": "ON" } }, - "title": "Sensors" + "title": "Sensor" } \ No newline at end of file diff --git a/homeassistant/components/sentry/translations/pl.json b/homeassistant/components/sentry/translations/pl.json index 2e350dbfb0a..a66e217b850 100644 --- a/homeassistant/components/sentry/translations/pl.json +++ b/homeassistant/components/sentry/translations/pl.json @@ -5,7 +5,7 @@ }, "error": { "bad_dsn": "Nieprawid\u0142owy DSN", - "unknown": "[%key_id:common::config_flow::error::unknown%]" + "unknown": "Nieoczekiwany b\u0142\u0105d." }, "step": { "user": { diff --git a/homeassistant/components/shopping_list/translations/hu.json b/homeassistant/components/shopping_list/translations/hu.json new file mode 100644 index 00000000000..4a093bea379 --- /dev/null +++ b/homeassistant/components/shopping_list/translations/hu.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "A bev\u00e1s\u00e1rl\u00f3lista m\u00e1r konfigur\u00e1lva van." + }, + "step": { + "user": { + "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a bev\u00e1s\u00e1rl\u00f3list\u00e1t?", + "title": "Bev\u00e1s\u00e1rl\u00f3lista" + } + } + }, + "title": "Bev\u00e1s\u00e1rl\u00f3lista" +} \ No newline at end of file diff --git a/homeassistant/components/shopping_list/translations/pt-BR.json b/homeassistant/components/shopping_list/translations/pt-BR.json new file mode 100644 index 00000000000..9e8b24efa29 --- /dev/null +++ b/homeassistant/components/shopping_list/translations/pt-BR.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "A lista de compras j\u00e1 est\u00e1 configurada." + }, + "step": { + "user": { + "description": "Deseja configurar a lista de compras?", + "title": "Lista de compras" + } + } + }, + "title": "Lista de compras" +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/hu.json b/homeassistant/components/simplisafe/translations/hu.json index 95331d8c8d2..38c48c3a86d 100644 --- a/homeassistant/components/simplisafe/translations/hu.json +++ b/homeassistant/components/simplisafe/translations/hu.json @@ -8,7 +8,7 @@ "user": { "data": { "password": "Jelsz\u00f3", - "username": "Email c\u00edm" + "username": "E-mail" }, "title": "T\u00f6ltsd ki az adataid" } diff --git a/homeassistant/components/simplisafe/translations/it.json b/homeassistant/components/simplisafe/translations/it.json index c63894ceaf2..c970cd6d48b 100644 --- a/homeassistant/components/simplisafe/translations/it.json +++ b/homeassistant/components/simplisafe/translations/it.json @@ -12,7 +12,7 @@ "data": { "code": "Codice (utilizzato nell'Interfaccia Utente di Home Assistant)", "password": "Password", - "username": "Indirizzo E-mail" + "username": "E-mail" }, "title": "Inserisci le tue informazioni." } diff --git a/homeassistant/components/simplisafe/translations/pl.json b/homeassistant/components/simplisafe/translations/pl.json index 9746aab1ee4..0562222eea3 100644 --- a/homeassistant/components/simplisafe/translations/pl.json +++ b/homeassistant/components/simplisafe/translations/pl.json @@ -10,9 +10,9 @@ "step": { "user": { "data": { - "code": "Kod (u\u017cywany w interfejsie Home Assistant'a)", - "password": "[%key_id:common::config_flow::data::password%]", - "username": "[%key_id:common::config_flow::data::email%]" + "code": "Kod (u\u017cywany w interfejsie Home Assistanta)", + "password": "Has\u0142o", + "username": "Adres e-mail" }, "title": "Wprowad\u017a dane" } @@ -22,7 +22,7 @@ "step": { "init": { "data": { - "code": "Kod (u\u017cywany w interfejsie u\u017cytkownika Home Assistant'a)" + "code": "Kod (u\u017cywany w interfejsie u\u017cytkownika Home Assistanta)" }, "title": "Konfiguracja SimpliSafe" } diff --git a/homeassistant/components/slack/notify.py b/homeassistant/components/slack/notify.py index 8cfffc1722a..6f05eebd87d 100644 --- a/homeassistant/components/slack/notify.py +++ b/homeassistant/components/slack/notify.py @@ -18,11 +18,13 @@ from homeassistant.components.notify import ( from homeassistant.const import CONF_API_KEY, CONF_ICON, CONF_USERNAME from homeassistant.core import callback from homeassistant.helpers import aiohttp_client, config_validation as cv +import homeassistant.helpers.template as template _LOGGER = logging.getLogger(__name__) ATTR_ATTACHMENTS = "attachments" ATTR_BLOCKS = "blocks" +ATTR_BLOCKS_TEMPLATE = "blocks_template" ATTR_FILE = "file" CONF_DEFAULT_CHANNEL = "default_channel" @@ -65,6 +67,20 @@ def _async_sanitize_channel_names(channel_list): return [channel.lstrip("#") for channel in channel_list] +@callback +def _async_templatize_blocks(hass, value): + """Recursive template creator helper function.""" + if isinstance(value, list): + return [_async_templatize_blocks(hass, item) for item in value] + if isinstance(value, dict): + return { + key: _async_templatize_blocks(hass, item) for key, item in value.items() + } + + tmpl = template.Template(value, hass=hass) + return tmpl.async_render() + + class SlackNotificationService(BaseNotificationService): """Define the Slack notification logic.""" @@ -142,7 +158,13 @@ class SlackNotificationService(BaseNotificationService): "for them will be dropped in 0.114.0. In most cases, Blocks should be " "used instead: https://www.home-assistant.io/integrations/slack/" ) - blocks = data.get(ATTR_BLOCKS, {}) + + if ATTR_BLOCKS_TEMPLATE in data: + blocks = _async_templatize_blocks(self.hass, data[ATTR_BLOCKS_TEMPLATE]) + elif ATTR_BLOCKS in data: + blocks = data[ATTR_BLOCKS] + else: + blocks = {} return await self._async_send_text_only_message( targets, message, title, attachments, blocks diff --git a/homeassistant/components/smappee/__init__.py b/homeassistant/components/smappee/__init__.py index c31ab97cb95..d230661a9f2 100644 --- a/homeassistant/components/smappee/__init__.py +++ b/homeassistant/components/smappee/__init__.py @@ -7,7 +7,13 @@ from requests.exceptions import RequestException import smappy import voluptuous as vol -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_HOST, + CONF_PASSWORD, + CONF_USERNAME, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform from homeassistant.util import Throttle @@ -17,8 +23,6 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Smappee" DEFAULT_HOST_PASSWORD = "admin" -CONF_CLIENT_ID = "client_id" -CONF_CLIENT_SECRET = "client_secret" CONF_HOST_PASSWORD = "host_password" DOMAIN = "smappee" diff --git a/homeassistant/components/smappee/sensor.py b/homeassistant/components/smappee/sensor.py index cb252dda98b..9558bbc2e62 100644 --- a/homeassistant/components/smappee/sensor.py +++ b/homeassistant/components/smappee/sensor.py @@ -4,6 +4,7 @@ import logging from homeassistant.const import ( DEGREE, + ELECTRICAL_CURRENT_AMPERE, ENERGY_KILO_WATT_HOUR, POWER_WATT, UNIT_PERCENTAGE, @@ -26,7 +27,7 @@ SENSOR_TYPES = { POWER_WATT, "active_power", ], - "current": ["Current", "mdi:gauge", "local", "A", "current"], + "current": ["Current", "mdi:gauge", "local", ELECTRICAL_CURRENT_AMPERE, "current"], "voltage": ["Voltage", "mdi:gauge", "local", VOLT, "voltage"], "active_cosfi": [ "Power Factor", diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index e4d720c94e5..479df05fbb4 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -9,7 +9,12 @@ from pysmartapp.event import EVENT_TYPE_DEVICE from pysmartthings import Attribute, Capability, SmartThings from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ACCESS_TOKEN, HTTP_FORBIDDEN +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + HTTP_FORBIDDEN, +) from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import ( @@ -25,8 +30,6 @@ from .const import ( CONF_APP_ID, CONF_INSTALLED_APP_ID, CONF_LOCATION_ID, - CONF_OAUTH_CLIENT_ID, - CONF_OAUTH_CLIENT_SECRET, CONF_REFRESH_TOKEN, DATA_BROKERS, DATA_MANAGER, @@ -115,8 +118,8 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): # Get SmartApp token to sync subscriptions token = await api.generate_tokens( - entry.data[CONF_OAUTH_CLIENT_ID], - entry.data[CONF_OAUTH_CLIENT_SECRET], + entry.data[CONF_CLIENT_ID], + entry.data[CONF_CLIENT_SECRET], entry.data[CONF_REFRESH_TOKEN], ) hass.config_entries.async_update_entry( @@ -312,8 +315,7 @@ class DeviceBroker: async def regenerate_refresh_token(now): """Generate a new refresh token and update the config entry.""" await self._token.refresh( - self._entry.data[CONF_OAUTH_CLIENT_ID], - self._entry.data[CONF_OAUTH_CLIENT_SECRET], + self._entry.data[CONF_CLIENT_ID], self._entry.data[CONF_CLIENT_SECRET], ) self._hass.config_entries.async_update_entry( self._entry, diff --git a/homeassistant/components/smartthings/config_flow.py b/homeassistant/components/smartthings/config_flow.py index c03ade4d8b1..f8746b597de 100644 --- a/homeassistant/components/smartthings/config_flow.py +++ b/homeassistant/components/smartthings/config_flow.py @@ -7,7 +7,13 @@ from pysmartthings.installedapp import format_install_url import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_ACCESS_TOKEN, HTTP_FORBIDDEN, HTTP_UNAUTHORIZED +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + HTTP_FORBIDDEN, + HTTP_UNAUTHORIZED, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession # pylint: disable=unused-import @@ -17,8 +23,6 @@ from .const import ( CONF_APP_ID, CONF_INSTALLED_APP_ID, CONF_LOCATION_ID, - CONF_OAUTH_CLIENT_ID, - CONF_OAUTH_CLIENT_SECRET, CONF_REFRESH_TOKEN, DOMAIN, VAL_UID_MATCHER, @@ -112,8 +116,8 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): None, ) if existing: - self.oauth_client_id = existing.data[CONF_OAUTH_CLIENT_ID] - self.oauth_client_secret = existing.data[CONF_OAUTH_CLIENT_SECRET] + self.oauth_client_id = existing.data[CONF_CLIENT_ID] + self.oauth_client_secret = existing.data[CONF_CLIENT_SECRET] else: # Get oauth client id/secret by regenerating it app_oauth = AppOAuth(app.app_id) @@ -227,8 +231,8 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data = { CONF_ACCESS_TOKEN: self.access_token, CONF_REFRESH_TOKEN: self.refresh_token, - CONF_OAUTH_CLIENT_ID: self.oauth_client_id, - CONF_OAUTH_CLIENT_SECRET: self.oauth_client_secret, + CONF_CLIENT_ID: self.oauth_client_id, + CONF_CLIENT_SECRET: self.oauth_client_secret, CONF_LOCATION_ID: self.location_id, CONF_APP_ID: self.app_id, CONF_INSTALLED_APP_ID: self.installed_app_id, diff --git a/homeassistant/components/smartthings/const.py b/homeassistant/components/smartthings/const.py index 8e323c0a715..9d779bf9d5b 100644 --- a/homeassistant/components/smartthings/const.py +++ b/homeassistant/components/smartthings/const.py @@ -2,26 +2,31 @@ from datetime import timedelta import re +DOMAIN = "smartthings" + APP_OAUTH_CLIENT_NAME = "Home Assistant" APP_OAUTH_SCOPES = ["r:devices:*"] APP_NAME_PREFIX = "homeassistant." + CONF_APP_ID = "app_id" CONF_CLOUDHOOK_URL = "cloudhook_url" CONF_INSTALLED_APP_ID = "installed_app_id" CONF_INSTANCE_ID = "instance_id" CONF_LOCATION_ID = "location_id" -CONF_OAUTH_CLIENT_ID = "client_id" -CONF_OAUTH_CLIENT_SECRET = "client_secret" CONF_REFRESH_TOKEN = "refresh_token" + DATA_MANAGER = "manager" DATA_BROKERS = "brokers" -DOMAIN = "smartthings" EVENT_BUTTON = "smartthings.button" + SIGNAL_SMARTTHINGS_UPDATE = "smartthings_update" SIGNAL_SMARTAPP_PREFIX = "smartthings_smartap_" + SETTINGS_INSTANCE_ID = "hassInstanceId" + STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 + # Ordered 'specific to least-specific platform' in order for capabilities # to be drawn-down and represented by the most appropriate platform. SUPPORTED_PLATFORMS = [ @@ -35,6 +40,8 @@ SUPPORTED_PLATFORMS = [ "sensor", "scene", ] + TOKEN_REFRESH_INTERVAL = timedelta(days=14) + VAL_UID = "^(?:([0-9a-fA-F]{32})|([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}))$" VAL_UID_MATCHER = re.compile(VAL_UID) diff --git a/homeassistant/components/smartthings/translations/ca.json b/homeassistant/components/smartthings/translations/ca.json index 0baa8147efe..7765eec4c13 100644 --- a/homeassistant/components/smartthings/translations/ca.json +++ b/homeassistant/components/smartthings/translations/ca.json @@ -9,7 +9,7 @@ "token_forbidden": "El token d'autenticaci\u00f3 no t\u00e9 cont\u00e9 els apartats OAuth obligatoris.", "token_invalid_format": "El token d'autenticaci\u00f3 ha d'estar en format UID/GUID", "token_unauthorized": "El token d'autenticaci\u00f3 no \u00e9s v\u00e0lid o ja no est\u00e0 autoritzat.", - "webhook_error": "SmartThings no ha pogut validar l'adre\u00e7a final configurada a `base_url`. Revisa els requisits del component." + "webhook_error": "SmartThings no ha pogut validar l'URL webhook. Comprova que l'URL pot ser accedit des d'Internet i torna-ho a provar." }, "step": { "authorize": { diff --git a/homeassistant/components/smartthings/translations/hu.json b/homeassistant/components/smartthings/translations/hu.json index d71e29e0171..a148bfa04fb 100644 --- a/homeassistant/components/smartthings/translations/hu.json +++ b/homeassistant/components/smartthings/translations/hu.json @@ -8,6 +8,11 @@ "webhook_error": "A SmartThings nem tudta \u00e9rv\u00e9nyes\u00edteni a `base_url`-ben konfigur\u00e1lt v\u00e9gpontot. K\u00e9rlek, tekintsd \u00e1t az \u00f6sszetev\u0151 k\u00f6vetelm\u00e9nyeit." }, "step": { + "pat": { + "data": { + "access_token": "Hozz\u00e1f\u00e9r\u00e9si token" + } + }, "user": { "description": "K\u00e9rlek add meg a SmartThings [Personal Access Tokent]({token_url}), amit az [instrukci\u00f3k] ({component_url}) alapj\u00e1n hozt\u00e1l l\u00e9tre.", "title": "Adja meg a szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si Tokent" diff --git a/homeassistant/components/smartthings/translations/pl.json b/homeassistant/components/smartthings/translations/pl.json index b056a3297fe..f43a6f267b3 100644 --- a/homeassistant/components/smartthings/translations/pl.json +++ b/homeassistant/components/smartthings/translations/pl.json @@ -13,11 +13,11 @@ }, "step": { "authorize": { - "title": "Autoryzuj Home Assistant'a" + "title": "Autoryzuj Home Assistanta" }, "pat": { "data": { - "access_token": "[%key_id:common::config_flow::data::access_token%]" + "access_token": "Token dost\u0119pu" }, "description": "Wprowad\u017a [token dost\u0119pu osobistego]({token_url}) SmartThings, kt\u00f3ry zosta\u0142 utworzony zgodnie z [instrukcj\u0105]({component_url}). Umo\u017cliwi to stworzenie integracji Home Assistant w ramach Twojego konta SmartThings.", "title": "Wprowad\u017a osobisty token dost\u0119pu" @@ -26,7 +26,7 @@ "data": { "location_id": "Lokalizacja" }, - "description": "Wybierz lokalizacj\u0119 SmartThings, kt\u00f3r\u0105 chcesz doda\u0107 do Home Assistant'a. Nast\u0119pnie otwarte zostanie nowe okno i zostaniesz poproszony o zalogowanie si\u0119 i autoryzacj\u0119 instalacji integracji Home Assistant w wybranej lokalizacji.", + "description": "Wybierz lokalizacj\u0119 SmartThings, kt\u00f3r\u0105 chcesz doda\u0107 do Home Assistanta. Nast\u0119pnie otwarte zostanie nowe okno i zostaniesz poproszony o zalogowanie si\u0119 i autoryzacj\u0119 instalacji integracji Home Assistant w wybranej lokalizacji.", "title": "Wybierz lokalizacj\u0119" }, "user": { diff --git a/homeassistant/components/smartthings/translations/pt-BR.json b/homeassistant/components/smartthings/translations/pt-BR.json index a28daf34acc..5b6329a530f 100644 --- a/homeassistant/components/smartthings/translations/pt-BR.json +++ b/homeassistant/components/smartthings/translations/pt-BR.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_available_locations": "N\u00e3o h\u00e1 Locais SmartThings dispon\u00edveis para configura\u00e7\u00e3o no Home Assistant." + }, "error": { "app_setup_error": "N\u00e3o \u00e9 poss\u00edvel configurar o SmartApp. Por favor, tente novamente.", "token_forbidden": "O token n\u00e3o possui os escopos necess\u00e1rios do OAuth.", diff --git a/homeassistant/components/smhi/translations/fi.json b/homeassistant/components/smhi/translations/fi.json index c11e9dbdf70..2a05c47dcbc 100644 --- a/homeassistant/components/smhi/translations/fi.json +++ b/homeassistant/components/smhi/translations/fi.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "name_exists": "Nimi on jo olemassa", + "wrong_location": "Sijainti vain Ruotsi" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/solaredge/translations/ca.json b/homeassistant/components/solaredge/translations/ca.json index ca5d472c9d6..cce579cd7e5 100644 --- a/homeassistant/components/solaredge/translations/ca.json +++ b/homeassistant/components/solaredge/translations/ca.json @@ -9,7 +9,7 @@ "step": { "user": { "data": { - "api_key": "Clau API d'aquest lloc", + "api_key": "Clau API", "name": "Nom d'aquesta instal\u00b7laci\u00f3", "site_id": "SolarEdge site_id" }, diff --git a/homeassistant/components/solaredge/translations/hu.json b/homeassistant/components/solaredge/translations/hu.json index ed182671709..e66bf3b4043 100644 --- a/homeassistant/components/solaredge/translations/hu.json +++ b/homeassistant/components/solaredge/translations/hu.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "api_key": "API kulcs", "name": "Ennek az install\u00e1ci\u00f3nak a neve" }, "title": "Az API param\u00e9terek megad\u00e1sa ehhez a telep\u00edt\u00e9shez" diff --git a/homeassistant/components/solaredge/translations/it.json b/homeassistant/components/solaredge/translations/it.json index eeac06cee2d..28b34bdbd3c 100644 --- a/homeassistant/components/solaredge/translations/it.json +++ b/homeassistant/components/solaredge/translations/it.json @@ -9,7 +9,7 @@ "step": { "user": { "data": { - "api_key": "La chiave API per questo sito", + "api_key": "Chiave API", "name": "Il nome di questa installazione", "site_id": "Il sito-id di SolarEdge" }, diff --git a/homeassistant/components/solaredge/translations/pl.json b/homeassistant/components/solaredge/translations/pl.json index e304337d3e2..2060cca6702 100644 --- a/homeassistant/components/solaredge/translations/pl.json +++ b/homeassistant/components/solaredge/translations/pl.json @@ -9,7 +9,7 @@ "step": { "user": { "data": { - "api_key": "[%key_id:common::config_flow::data::api_key%] dla tej strony", + "api_key": "Klucz API", "name": "Nazwa tej instalacji", "site_id": "SolarEdge site-id" }, diff --git a/homeassistant/components/solaredge_local/sensor.py b/homeassistant/components/solaredge_local/sensor.py index 94aa06c2647..59b0a5e8856 100644 --- a/homeassistant/components/solaredge_local/sensor.py +++ b/homeassistant/components/solaredge_local/sensor.py @@ -12,6 +12,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_IP_ADDRESS, CONF_NAME, + ELECTRICAL_CURRENT_AMPERE, ENERGY_WATT_HOUR, FREQUENCY_HERTZ, POWER_WATT, @@ -103,7 +104,7 @@ SENSOR_TYPES = { "optimizer_current": [ "optimizercurrent", "Average Optimizer Current", - "A", + ELECTRICAL_CURRENT_AMPERE, "mdi:solar-panel", None, ], diff --git a/homeassistant/components/solarlog/translations/ca.json b/homeassistant/components/solarlog/translations/ca.json index a159dd1f6e3..c62b69fa976 100644 --- a/homeassistant/components/solarlog/translations/ca.json +++ b/homeassistant/components/solarlog/translations/ca.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP del dispositiu Solar-Log", + "host": "Amfitri\u00f3", "name": "Prefix utilitzat pels sensors de Solar-Log" }, "title": "Configuraci\u00f3 de la connexi\u00f3 amb Solar-Log" diff --git a/homeassistant/components/solarlog/translations/hu.json b/homeassistant/components/solarlog/translations/hu.json index e52cebefda6..3fa8a9620a0 100644 --- a/homeassistant/components/solarlog/translations/hu.json +++ b/homeassistant/components/solarlog/translations/hu.json @@ -2,6 +2,13 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6zt m\u00e1r konfigur\u00e1ltuk" + }, + "step": { + "user": { + "data": { + "host": "Hoszt" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/solarlog/translations/it.json b/homeassistant/components/solarlog/translations/it.json index 046faae52f7..c227ba7a16c 100644 --- a/homeassistant/components/solarlog/translations/it.json +++ b/homeassistant/components/solarlog/translations/it.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "host": "Il nome host o l'indirizzo IP del dispositivo Solar-Log", + "host": "Host", "name": "Il prefisso da utilizzare per i sensori Solar-Log" }, "title": "Definire la connessione Solar-Log" diff --git a/homeassistant/components/solarlog/translations/pl.json b/homeassistant/components/solarlog/translations/pl.json index 5add902c4fa..6769d51c2c2 100644 --- a/homeassistant/components/solarlog/translations/pl.json +++ b/homeassistant/components/solarlog/translations/pl.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." }, "error": { - "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, sprawd\u017a adres hosta" }, "step": { "user": { "data": { - "host": "[%key_id:common::config_flow::data::host%]", + "host": "Nazwa hosta lub adres IP", "name": "Prefiks dla sensor\u00f3w Solar-Log" }, "title": "Zdefiniuj po\u0142\u0105czenie z Solar-Log" diff --git a/homeassistant/components/soma/translations/hu.json b/homeassistant/components/soma/translations/hu.json index 357ff2ac170..82ec28ff4d7 100644 --- a/homeassistant/components/soma/translations/hu.json +++ b/homeassistant/components/soma/translations/hu.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "Kiszolg\u00e1l\u00f3", + "host": "Hoszt", "port": "Port" }, "description": "K\u00e9rj\u00fck, adja meg a SOMA Connect csatlakoz\u00e1si be\u00e1ll\u00edt\u00e1sait.", diff --git a/homeassistant/components/soma/translations/pl.json b/homeassistant/components/soma/translations/pl.json index 75f379de3d6..9e43072f4e9 100644 --- a/homeassistant/components/soma/translations/pl.json +++ b/homeassistant/components/soma/translations/pl.json @@ -2,9 +2,9 @@ "config": { "abort": { "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Soma.", - "authorize_url_timeout": "[%key_id:common::config_flow::abort::oauth2_authorize_url_timeout%]", + "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", "connection_error": "Nie uda\u0142o si\u0119 po\u0142\u0105czy\u0107 z SOMA Connect.", - "missing_configuration": "[%key_id:common::config_flow::abort::oauth2_missing_configuration%]", + "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", "result_error": "SOMA Connect odpowiedzia\u0142 statusem b\u0142\u0119du." }, "create_entry": { @@ -13,8 +13,8 @@ "step": { "user": { "data": { - "host": "[%key_id:common::config_flow::data::host%]", - "port": "[%key_id:common::config_flow::data::port%]" + "host": "Nazwa hosta lub adres IP", + "port": "Port" }, "description": "Wprowad\u017a ustawienia po\u0142\u0105czenia SOMA Connect.", "title": "SOMA Connect" diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index 619e5a72602..fb20fcd6683 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -3,17 +3,24 @@ import asyncio from datetime import timedelta import logging +from pymfy.api.devices.category import Category from requests import HTTPError import voluptuous as vol from homeassistant.components.somfy import config_flow from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.helpers import ( + config_entry_oauth2_flow, + config_validation as cv, + device_registry as dr, +) from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import Throttle from . import api +from .const import DOMAIN API = "api" @@ -23,10 +30,7 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=30) -DOMAIN = "somfy" -CONF_CLIENT_ID = "client_id" -CONF_CLIENT_SECRET = "client_secret" CONF_OPTIMISTIC = "optimistic" SOMFY_AUTH_CALLBACK_PATH = "/auth/somfy/callback" @@ -86,6 +90,20 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): await update_all_devices(hass) + device_registry = await dr.async_get_registry(hass) + + devices = hass.data[DOMAIN][DEVICES] + hubs = [device for device in devices if Category.HUB.value in device.categories] + + for hub in hubs: + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, hub.id)}, + manufacturer="Somfy", + name=hub.name, + model=hub.type, + ) + for component in SOMFY_COMPONENTS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) @@ -134,7 +152,7 @@ class SomfyEntity(Entity): "identifiers": {(DOMAIN, self.unique_id)}, "name": self.name, "model": self.device.type, - "via_hub": (DOMAIN, self.device.site_id), + "via_hub": (DOMAIN, self.device.parent_id), # For the moment, Somfy only returns their own device. "manufacturer": "Somfy", } diff --git a/homeassistant/components/somfy/manifest.json b/homeassistant/components/somfy/manifest.json index 6e15b01e961..ab24d21b2e1 100644 --- a/homeassistant/components/somfy/manifest.json +++ b/homeassistant/components/somfy/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/somfy", "dependencies": ["http"], "codeowners": ["@tetienne"], - "requirements": ["pymfy==0.7.1"] + "requirements": ["pymfy==0.9.0"] } diff --git a/homeassistant/components/somfy/translations/pl.json b/homeassistant/components/somfy/translations/pl.json index 99d00914410..0c3dedfb466 100644 --- a/homeassistant/components/somfy/translations/pl.json +++ b/homeassistant/components/somfy/translations/pl.json @@ -2,15 +2,15 @@ "config": { "abort": { "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Somfy.", - "authorize_url_timeout": "[%key_id:common::config_flow::abort::oauth2_authorize_url_timeout%]", - "missing_configuration": "[%key_id:common::config_flow::abort::oauth2_missing_configuration%]" + "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", + "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105." }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono z Somfy" }, "step": { "pick_implementation": { - "title": "[%key_id:common::config_flow::title::oauth2_pick_implementation%]" + "title": "Wybierz metod\u0119 uwierzytelniania" } } } diff --git a/homeassistant/components/somfy_mylink/cover.py b/homeassistant/components/somfy_mylink/cover.py index 767abda2fd7..b849a490940 100644 --- a/homeassistant/components/somfy_mylink/cover.py +++ b/homeassistant/components/somfy_mylink/cover.py @@ -46,7 +46,7 @@ class SomfyShade(CoverEntity): def __init__( self, somfy_mylink, - target_id="AABBCC", + target_id, name="SomfyShade", reverse=False, device_class="window", @@ -58,6 +58,11 @@ class SomfyShade(CoverEntity): self._reverse = reverse self._device_class = device_class + @property + def unique_id(self): + """Return the unique ID of this cover.""" + return self._target_id + @property def name(self): """Return the name of the cover.""" diff --git a/homeassistant/components/sonarr/__init__.py b/homeassistant/components/sonarr/__init__.py index 63c194cc969..4228d6c8400 100644 --- a/homeassistant/components/sonarr/__init__.py +++ b/homeassistant/components/sonarr/__init__.py @@ -1 +1,164 @@ -"""The sonarr component.""" +"""The Sonarr component.""" +import asyncio +from datetime import timedelta +from typing import Any, Dict + +from sonarr import Sonarr, SonarrError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_NAME, + CONF_API_KEY, + CONF_HOST, + CONF_PORT, + CONF_SSL, + CONF_VERIFY_SSL, +) +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType + +from .const import ( + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_SOFTWARE_VERSION, + CONF_BASE_PATH, + CONF_UPCOMING_DAYS, + CONF_WANTED_MAX_ITEMS, + DATA_SONARR, + DATA_UNDO_UPDATE_LISTENER, + DEFAULT_UPCOMING_DAYS, + DEFAULT_WANTED_MAX_ITEMS, + DOMAIN, +) + +PLATFORMS = ["sensor"] +SCAN_INTERVAL = timedelta(seconds=30) + + +async def async_setup(hass: HomeAssistantType, config: Dict) -> bool: + """Set up the Sonarr component.""" + hass.data.setdefault(DOMAIN, {}) + return True + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: + """Set up Sonarr from a config entry.""" + if not entry.options: + options = { + CONF_UPCOMING_DAYS: entry.data.get( + CONF_UPCOMING_DAYS, DEFAULT_UPCOMING_DAYS + ), + CONF_WANTED_MAX_ITEMS: entry.data.get( + CONF_WANTED_MAX_ITEMS, DEFAULT_WANTED_MAX_ITEMS + ), + } + hass.config_entries.async_update_entry(entry, options=options) + + sonarr = Sonarr( + host=entry.data[CONF_HOST], + port=entry.data[CONF_PORT], + api_key=entry.data[CONF_API_KEY], + base_path=entry.data[CONF_BASE_PATH], + session=async_get_clientsession(hass), + tls=entry.data[CONF_SSL], + verify_ssl=entry.data[CONF_VERIFY_SSL], + ) + + try: + await sonarr.update() + except SonarrError: + raise ConfigEntryNotReady + + undo_listener = entry.add_update_listener(_async_update_listener) + + hass.data[DOMAIN][entry.entry_id] = { + DATA_SONARR: sonarr, + DATA_UNDO_UPDATE_LISTENER: undo_listener, + } + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + + hass.data[DOMAIN][entry.entry_id][DATA_UNDO_UPDATE_LISTENER]() + + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +async def _async_update_listener(hass: HomeAssistantType, entry: ConfigEntry) -> None: + """Handle options update.""" + async_dispatcher_send( + hass, f"sonarr.{entry.entry_id}.entry_options_update", entry.options + ) + + +class SonarrEntity(Entity): + """Defines a base Sonarr entity.""" + + def __init__( + self, + *, + sonarr: Sonarr, + entry_id: str, + device_id: str, + name: str, + icon: str, + enabled_default: bool = True, + ) -> None: + """Initialize the Sonar entity.""" + self._entry_id = entry_id + self._device_id = device_id + self._enabled_default = enabled_default + self._icon = icon + self._name = name + self.sonarr = sonarr + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name + + @property + def icon(self) -> str: + """Return the mdi icon of the entity.""" + return self._icon + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return self._enabled_default + + @property + def device_info(self) -> Dict[str, Any]: + """Return device information about the application.""" + if self._device_id is None: + return None + + return { + ATTR_IDENTIFIERS: {(DOMAIN, self._device_id)}, + ATTR_NAME: "Activity Sensor", + ATTR_MANUFACTURER: "Sonarr", + ATTR_SOFTWARE_VERSION: self.sonarr.app.info.version, + "entry_type": "service", + } diff --git a/homeassistant/components/sonarr/config_flow.py b/homeassistant/components/sonarr/config_flow.py new file mode 100644 index 00000000000..e82ecb49fda --- /dev/null +++ b/homeassistant/components/sonarr/config_flow.py @@ -0,0 +1,145 @@ +"""Config flow for Sonarr.""" +import logging +from typing import Any, Dict, Optional + +from sonarr import Sonarr, SonarrAccessRestricted, SonarrError +import voluptuous as vol + +from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow, OptionsFlow +from homeassistant.const import ( + CONF_API_KEY, + CONF_HOST, + CONF_PORT, + CONF_SSL, + CONF_VERIFY_SSL, +) +from homeassistant.core import callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import ConfigType, HomeAssistantType + +from .const import ( + CONF_BASE_PATH, + CONF_UPCOMING_DAYS, + CONF_WANTED_MAX_ITEMS, + DEFAULT_BASE_PATH, + DEFAULT_PORT, + DEFAULT_SSL, + DEFAULT_UPCOMING_DAYS, + DEFAULT_VERIFY_SSL, + DEFAULT_WANTED_MAX_ITEMS, +) +from .const import DOMAIN # pylint: disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +async def validate_input(hass: HomeAssistantType, data: dict) -> Dict[str, Any]: + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + session = async_get_clientsession(hass) + + sonarr = Sonarr( + host=data[CONF_HOST], + port=data[CONF_PORT], + api_key=data[CONF_API_KEY], + base_path=data[CONF_BASE_PATH], + tls=data[CONF_SSL], + verify_ssl=data[CONF_VERIFY_SSL], + session=session, + ) + + await sonarr.update() + + return True + + +class SonarrConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Sonarr.""" + + VERSION = 1 + CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return SonarrOptionsFlowHandler(config_entry) + + async def async_step_import( + self, user_input: Optional[ConfigType] = None + ) -> Dict[str, Any]: + """Handle a flow initiated by configuration file.""" + return await self.async_step_user(user_input) + + async def async_step_user( + self, user_input: Optional[ConfigType] = None + ) -> Dict[str, Any]: + """Handle a flow initiated by the user.""" + if user_input is None: + return self._show_setup_form() + + if CONF_VERIFY_SSL not in user_input: + user_input[CONF_VERIFY_SSL] = DEFAULT_VERIFY_SSL + + try: + await validate_input(self.hass, user_input) + except SonarrAccessRestricted: + return self._show_setup_form({"base": "invalid_auth"}) + except SonarrError: + return self._show_setup_form({"base": "cannot_connect"}) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + return self.async_abort(reason="unknown") + + return self.async_create_entry(title=user_input[CONF_HOST], data=user_input) + + def _show_setup_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]: + """Show the setup form to the user.""" + data_schema = { + vol.Required(CONF_HOST): str, + vol.Required(CONF_API_KEY): str, + vol.Optional(CONF_BASE_PATH, default=DEFAULT_BASE_PATH): str, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): int, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool, + } + + if self.show_advanced_options: + data_schema[ + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL) + ] = bool + + return self.async_show_form( + step_id="user", data_schema=vol.Schema(data_schema), errors=errors or {}, + ) + + +class SonarrOptionsFlowHandler(OptionsFlow): + """Handle Sonarr client options.""" + + def __init__(self, config_entry): + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input: Optional[ConfigType] = None): + """Manage Sonarr options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + options = { + vol.Optional( + CONF_UPCOMING_DAYS, + default=self.config_entry.options.get( + CONF_UPCOMING_DAYS, DEFAULT_UPCOMING_DAYS + ), + ): int, + vol.Optional( + CONF_WANTED_MAX_ITEMS, + default=self.config_entry.options.get( + CONF_WANTED_MAX_ITEMS, DEFAULT_WANTED_MAX_ITEMS + ), + ): int, + } + + return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) diff --git a/homeassistant/components/sonarr/const.py b/homeassistant/components/sonarr/const.py new file mode 100644 index 00000000000..52079a9416c --- /dev/null +++ b/homeassistant/components/sonarr/const.py @@ -0,0 +1,29 @@ +"""Constants for Sonarr.""" +DOMAIN = "sonarr" + +# Attributes +ATTR_IDENTIFIERS = "identifiers" +ATTR_MANUFACTURER = "manufacturer" +ATTR_SOFTWARE_VERSION = "sw_version" + +# Config Keys +CONF_BASE_PATH = "base_path" +CONF_DAYS = "days" +CONF_INCLUDED = "include_paths" +CONF_UNIT = "unit" +CONF_UPCOMING_DAYS = "upcoming_days" +CONF_URLBASE = "urlbase" +CONF_WANTED_MAX_ITEMS = "wanted_max_items" + +# Data +DATA_SONARR = "sonarr" +DATA_UNDO_UPDATE_LISTENER = "undo_update_listener" + +# Defaults +DEFAULT_BASE_PATH = "/api" +DEFAULT_HOST = "localhost" +DEFAULT_PORT = 8989 +DEFAULT_SSL = False +DEFAULT_UPCOMING_DAYS = 1 +DEFAULT_VERIFY_SSL = False +DEFAULT_WANTED_MAX_ITEMS = 50 diff --git a/homeassistant/components/sonarr/manifest.json b/homeassistant/components/sonarr/manifest.json index 2fe375fb1df..c1edb8ec521 100644 --- a/homeassistant/components/sonarr/manifest.json +++ b/homeassistant/components/sonarr/manifest.json @@ -2,5 +2,8 @@ "domain": "sonarr", "name": "Sonarr", "documentation": "https://www.home-assistant.io/integrations/sonarr", - "codeowners": ["@ctalkington"] + "codeowners": ["@ctalkington"], + "requirements": ["sonarr==0.2.2"], + "config_flow": true, + "quality_scale": "silver" } diff --git a/homeassistant/components/sonarr/sensor.py b/homeassistant/components/sonarr/sensor.py index 65513db3571..f1945f26836 100644 --- a/homeassistant/components/sonarr/sensor.py +++ b/homeassistant/components/sonarr/sensor.py @@ -1,17 +1,20 @@ -"""Support for Sonarr.""" +"""Support for Sonarr sensors.""" from datetime import timedelta import logging +from typing import Any, Callable, Dict, List, Optional, Union -import requests +from sonarr import Sonarr, SonarrConnectionError, SonarrError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_API_KEY, CONF_HOST, CONF_MONITORED_CONDITIONS, CONF_PORT, CONF_SSL, + CONF_VERIFY_SSL, DATA_BYTES, DATA_EXABYTES, DATA_GIGABYTES, @@ -21,46 +24,32 @@ from homeassistant.const import ( DATA_TERABYTES, DATA_YOTTABYTES, DATA_ZETTABYTES, - HTTP_OK, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import ConfigType, HomeAssistantType import homeassistant.util.dt as dt_util +from . import SonarrEntity +from .const import ( + CONF_BASE_PATH, + CONF_DAYS, + CONF_INCLUDED, + CONF_UNIT, + CONF_UPCOMING_DAYS, + CONF_URLBASE, + CONF_WANTED_MAX_ITEMS, + DATA_SONARR, + DEFAULT_BASE_PATH, + DEFAULT_HOST, + DEFAULT_PORT, + DEFAULT_SSL, + DOMAIN, +) + _LOGGER = logging.getLogger(__name__) -CONF_DAYS = "days" -CONF_INCLUDED = "include_paths" -CONF_UNIT = "unit" -CONF_URLBASE = "urlbase" - -DEFAULT_HOST = "localhost" -DEFAULT_PORT = 8989 -DEFAULT_URLBASE = "" -DEFAULT_DAYS = "1" -DEFAULT_UNIT = DATA_GIGABYTES - -SENSOR_TYPES = { - "diskspace": ["Disk Space", DATA_GIGABYTES, "mdi:harddisk"], - "queue": ["Queue", "Episodes", "mdi:download"], - "upcoming": ["Upcoming", "Episodes", "mdi:television"], - "wanted": ["Wanted", "Episodes", "mdi:television"], - "series": ["Series", "Shows", "mdi:television"], - "commands": ["Commands", "Commands", "mdi:code-braces"], - "status": ["Status", "Status", "mdi:information"], -} - -ENDPOINTS = { - "diskspace": "{0}://{1}:{2}/{3}api/diskspace", - "queue": "{0}://{1}:{2}/{3}api/queue", - "upcoming": "{0}://{1}:{2}/{3}api/calendar?start={4}&end={5}", - "wanted": "{0}://{1}:{2}/{3}api/wanted/missing", - "series": "{0}://{1}:{2}/{3}api/series", - "commands": "{0}://{1}:{2}/{3}api/command", - "status": "{0}://{1}:{2}/{3}api/system/status", -} - -# Support to Yottabytes for the future, why not BYTE_SIZES = [ DATA_BYTES, DATA_KILOBYTES, @@ -72,198 +61,430 @@ BYTE_SIZES = [ DATA_ZETTABYTES, DATA_YOTTABYTES, ] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_API_KEY): cv.string, - vol.Optional(CONF_DAYS, default=DEFAULT_DAYS): cv.string, - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_INCLUDED, default=[]): cv.ensure_list, - vol.Optional(CONF_MONITORED_CONDITIONS, default=["upcoming"]): vol.All( - cv.ensure_list, [vol.In(list(SENSOR_TYPES))] - ), - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_SSL, default=False): cv.boolean, - vol.Optional(CONF_UNIT, default=DEFAULT_UNIT): vol.In(BYTE_SIZES), - vol.Optional(CONF_URLBASE, default=DEFAULT_URLBASE): cv.string, - } + +DEFAULT_URLBASE = "" +DEFAULT_DAYS = "1" +DEFAULT_UNIT = DATA_GIGABYTES + +PLATFORM_SCHEMA = vol.All( + cv.deprecated(CONF_INCLUDED, invalidation_version="0.112"), + cv.deprecated(CONF_MONITORED_CONDITIONS, invalidation_version="0.112"), + cv.deprecated(CONF_UNIT, invalidation_version="0.112"), + PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_DAYS, default=DEFAULT_DAYS): cv.string, + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_INCLUDED, default=[]): cv.ensure_list, + vol.Optional(CONF_MONITORED_CONDITIONS, default=[]): cv.ensure_list, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_UNIT, default=DEFAULT_UNIT): vol.In(BYTE_SIZES), + vol.Optional(CONF_URLBASE, default=DEFAULT_URLBASE): cv.string, + } + ), ) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Sonarr platform.""" - conditions = config.get(CONF_MONITORED_CONDITIONS) - add_entities([SonarrSensor(config, sensor) for sensor in conditions], True) +async def async_setup_platform( + hass: HomeAssistantType, + config: ConfigType, + async_add_entities: Callable[[List[Entity], bool], None], + discovery_info: Any = None, +) -> None: + """Import the platform into a config entry.""" + if len(hass.config_entries.async_entries(DOMAIN)) > 0: + return True + + config[CONF_BASE_PATH] = f"{config[CONF_URLBASE]}{DEFAULT_BASE_PATH}" + config[CONF_UPCOMING_DAYS] = int(config[CONF_DAYS]) + config[CONF_VERIFY_SSL] = False + + del config[CONF_DAYS] + del config[CONF_INCLUDED] + del config[CONF_MONITORED_CONDITIONS] + del config[CONF_URLBASE] + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) + ) -class SonarrSensor(Entity): +async def async_setup_entry( + hass: HomeAssistantType, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up Sonarr sensors based on a config entry.""" + options = entry.options + sonarr = hass.data[DOMAIN][entry.entry_id][DATA_SONARR] + + entities = [ + SonarrCommandsSensor(sonarr, entry.entry_id), + SonarrDiskspaceSensor(sonarr, entry.entry_id), + SonarrQueueSensor(sonarr, entry.entry_id), + SonarrSeriesSensor(sonarr, entry.entry_id), + SonarrUpcomingSensor(sonarr, entry.entry_id, days=options[CONF_UPCOMING_DAYS]), + SonarrWantedSensor( + sonarr, entry.entry_id, max_items=options[CONF_WANTED_MAX_ITEMS] + ), + ] + + async_add_entities(entities, True) + + +def sonarr_exception_handler(func): + """Decorate Sonarr calls to handle Sonarr exceptions. + + A decorator that wraps the passed in function, catches Sonarr errors, + and handles the availability of the entity. + """ + + async def handler(self, *args, **kwargs): + try: + await func(self, *args, **kwargs) + self.last_update_success = True + except SonarrConnectionError as error: + if self.available: + _LOGGER.error("Error communicating with API: %s", error) + self.last_update_success = False + except SonarrError as error: + if self.available: + _LOGGER.error("Invalid response from API: %s", error) + self.last_update_success = False + + return handler + + +class SonarrSensor(SonarrEntity): """Implementation of the Sonarr sensor.""" - def __init__(self, conf, sensor_type): - """Create Sonarr entity.""" + def __init__( + self, + *, + sonarr: Sonarr, + entry_id: str, + enabled_default: bool = True, + icon: str, + key: str, + name: str, + unit_of_measurement: Optional[str] = None, + ) -> None: + """Initialize Sonarr sensor.""" + self._unit_of_measurement = unit_of_measurement + self._key = key + self._unique_id = f"{entry_id}_{key}" + self.last_update_success = False - self.conf = conf - self.host = conf.get(CONF_HOST) - self.port = conf.get(CONF_PORT) - self.urlbase = conf.get(CONF_URLBASE) - if self.urlbase: - self.urlbase = "{}/".format(self.urlbase.strip("/")) - self.apikey = conf.get(CONF_API_KEY) - self.included = conf.get(CONF_INCLUDED) - self.days = int(conf.get(CONF_DAYS)) - self.ssl = "https" if conf.get(CONF_SSL) else "http" - self._state = None - self.data = [] - self.type = sensor_type - self._name = SENSOR_TYPES[self.type][0] - if self.type == "diskspace": - self._unit = conf.get(CONF_UNIT) - else: - self._unit = SENSOR_TYPES[self.type][1] - self._icon = SENSOR_TYPES[self.type][2] - self._available = False + super().__init__( + sonarr=sonarr, + entry_id=entry_id, + device_id=entry_id, + name=name, + icon=icon, + enabled_default=enabled_default, + ) @property - def name(self): - """Return the name of the sensor.""" - return "{} {}".format("Sonarr", self._name) + def unique_id(self) -> str: + """Return the unique ID for this sensor.""" + return self._unique_id @property - def state(self): - """Return sensor state.""" - return self._state - - @property - def available(self): + def available(self) -> bool: """Return sensor availability.""" - return self._available + return self.last_update_success @property - def unit_of_measurement(self): - """Return the unit of the sensor.""" - return self._unit + def unit_of_measurement(self) -> str: + """Return the unit this state is expressed in.""" + return self._unit_of_measurement + + +class SonarrCommandsSensor(SonarrSensor): + """Defines a Sonarr Commands sensor.""" + + def __init__(self, sonarr: Sonarr, entry_id: str) -> None: + """Initialize Sonarr Commands sensor.""" + self._commands = [] + + super().__init__( + sonarr=sonarr, + entry_id=entry_id, + icon="mdi:code-braces", + key="commands", + name=f"{sonarr.app.info.app_name} Commands", + unit_of_measurement="Commands", + enabled_default=False, + ) + + @sonarr_exception_handler + async def async_update(self) -> None: + """Update entity.""" + self._commands = await self.sonarr.commands() @property - def device_state_attributes(self): - """Return the state attributes of the sensor.""" - attributes = {} - if self.type == "upcoming": - for show in self.data: - if show["series"]["title"] in attributes: - continue + def device_state_attributes(self) -> Optional[Dict[str, Any]]: + """Return the state attributes of the entity.""" + attrs = {} - attributes[show["series"]["title"]] = "S{:02d}E{:02d}".format( - show["seasonNumber"], show["episodeNumber"] - ) - elif self.type == "queue": - for show in self.data: - remaining = 1 if show["size"] == 0 else show["sizeleft"] / show["size"] - attributes[ - show["series"]["title"] - + " S{:02d}E{:02d}".format( - show["episode"]["seasonNumber"], - show["episode"]["episodeNumber"], - ) - ] = "{:.2f}%".format(100 * (1 - (remaining))) - elif self.type == "wanted": - for show in self.data: - attributes[ - show["series"]["title"] - + " S{:02d}E{:02d}".format( - show["seasonNumber"], show["episodeNumber"] - ) - ] = show["airDate"] - elif self.type == "commands": - for command in self.data: - attributes[command["name"]] = command["state"] - elif self.type == "diskspace": - for data in self.data: - attributes[data["path"]] = "{:.2f}/{:.2f}{} ({:.2f}%)".format( - to_unit(data["freeSpace"], self._unit), - to_unit(data["totalSpace"], self._unit), - self._unit, - ( - to_unit(data["freeSpace"], self._unit) - / to_unit(data["totalSpace"], self._unit) - * 100 - ), - ) - elif self.type == "series": - for show in self.data: - if "episodeFileCount" not in show or "episodeCount" not in show: - attributes[show["title"]] = "N/A" - else: - attributes[show["title"]] = "{}/{} Episodes".format( - show["episodeFileCount"], show["episodeCount"] - ) - elif self.type == "status": - attributes = self.data - return attributes + for command in self._commands: + attrs[command.name] = command.state + + return attrs @property - def icon(self): - """Return the icon of the sensor.""" - return self._icon + def state(self) -> Union[None, str, int, float]: + """Return the state of the sensor.""" + return len(self._commands) - def update(self): - """Update the data for the sensor.""" + +class SonarrDiskspaceSensor(SonarrSensor): + """Defines a Sonarr Disk Space sensor.""" + + def __init__(self, sonarr: Sonarr, entry_id: str) -> None: + """Initialize Sonarr Disk Space sensor.""" + self._disks = [] + self._total_free = 0 + + super().__init__( + sonarr=sonarr, + entry_id=entry_id, + icon="mdi:harddisk", + key="diskspace", + name=f"{sonarr.app.info.app_name} Disk Space", + unit_of_measurement=DATA_GIGABYTES, + enabled_default=False, + ) + + def _to_unit(self, value): + """Return a value converted to unit of measurement.""" + return value / 1024 ** BYTE_SIZES.index(self._unit_of_measurement) + + @sonarr_exception_handler + async def async_update(self) -> None: + """Update entity.""" + app = await self.sonarr.update() + self._disks = app.disks + self._total_free = sum([disk.free for disk in self._disks]) + + @property + def device_state_attributes(self) -> Optional[Dict[str, Any]]: + """Return the state attributes of the entity.""" + attrs = {} + + for disk in self._disks: + free = self._to_unit(disk.free) + total = self._to_unit(disk.total) + usage = free / total * 100 + + attrs[ + disk.path + ] = f"{free:.2f}/{total:.2f}{self._unit_of_measurement} ({usage:.2f}%)" + + return attrs + + @property + def state(self) -> Union[None, str, int, float]: + """Return the state of the sensor.""" + free = self._to_unit(self._total_free) + return f"{free:.2f}" + + +class SonarrQueueSensor(SonarrSensor): + """Defines a Sonarr Queue sensor.""" + + def __init__(self, sonarr: Sonarr, entry_id: str) -> None: + """Initialize Sonarr Queue sensor.""" + self._queue = [] + + super().__init__( + sonarr=sonarr, + entry_id=entry_id, + icon="mdi:download", + key="queue", + name=f"{sonarr.app.info.app_name} Queue", + unit_of_measurement="Episodes", + enabled_default=False, + ) + + @sonarr_exception_handler + async def async_update(self) -> None: + """Update entity.""" + self._queue = await self.sonarr.queue() + + @property + def device_state_attributes(self) -> Optional[Dict[str, Any]]: + """Return the state attributes of the entity.""" + attrs = {} + + for item in self._queue: + remaining = 1 if item.size == 0 else item.size_remaining / item.size + remaining_pct = 100 * (1 - remaining) + name = f"{item.episode.series.title} {item.episode.identifier}" + attrs[name] = f"{remaining_pct:.2f}%" + + return attrs + + @property + def state(self) -> Union[None, str, int, float]: + """Return the state of the sensor.""" + return len(self._queue) + + +class SonarrSeriesSensor(SonarrSensor): + """Defines a Sonarr Series sensor.""" + + def __init__(self, sonarr: Sonarr, entry_id: str) -> None: + """Initialize Sonarr Series sensor.""" + self._items = [] + + super().__init__( + sonarr=sonarr, + entry_id=entry_id, + icon="mdi:television", + key="series", + name=f"{sonarr.app.info.app_name} Shows", + unit_of_measurement="Series", + enabled_default=False, + ) + + @sonarr_exception_handler + async def async_update(self) -> None: + """Update entity.""" + self._items = await self.sonarr.series() + + @property + def device_state_attributes(self) -> Optional[Dict[str, Any]]: + """Return the state attributes of the entity.""" + attrs = {} + + for item in self._items: + attrs[item.series.title] = f"{item.downloaded}/{item.episodes} Episodes" + + return attrs + + @property + def state(self) -> Union[None, str, int, float]: + """Return the state of the sensor.""" + return len(self._items) + + +class SonarrUpcomingSensor(SonarrSensor): + """Defines a Sonarr Upcoming sensor.""" + + def __init__(self, sonarr: Sonarr, entry_id: str, days: int = 1) -> None: + """Initialize Sonarr Upcoming sensor.""" + self._days = days + self._upcoming = [] + + super().__init__( + sonarr=sonarr, + entry_id=entry_id, + icon="mdi:television", + key="upcoming", + name=f"{sonarr.app.info.app_name} Upcoming", + unit_of_measurement="Episodes", + ) + + async def async_added_to_hass(self): + """Listen for signals.""" + await super().async_added_to_hass() + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"sonarr.{self._entry_id}.entry_options_update", + self.async_update_entry_options, + ) + ) + + @sonarr_exception_handler + async def async_update(self) -> None: + """Update entity.""" local = dt_util.start_of_local_day().replace(microsecond=0) start = dt_util.as_utc(local) - end = start + timedelta(days=self.days) - try: - res = requests.get( - ENDPOINTS[self.type].format( - self.ssl, - self.host, - self.port, - self.urlbase, - start.isoformat().replace("+00:00", "Z"), - end.isoformat().replace("+00:00", "Z"), - ), - headers={"X-Api-Key": self.apikey}, - timeout=10, + end = start + timedelta(days=self._days) + self._upcoming = await self.sonarr.calendar( + start=start.isoformat(), end=end.isoformat() + ) + + async def async_update_entry_options(self, options: dict) -> None: + """Update sensor settings when config entry options are update.""" + self._days = options[CONF_UPCOMING_DAYS] + + @property + def device_state_attributes(self) -> Optional[Dict[str, Any]]: + """Return the state attributes of the entity.""" + attrs = {} + + for episode in self._upcoming: + attrs[episode.series.title] = episode.identifier + + return attrs + + @property + def state(self) -> Union[None, str, int, float]: + """Return the state of the sensor.""" + return len(self._upcoming) + + +class SonarrWantedSensor(SonarrSensor): + """Defines a Sonarr Wanted sensor.""" + + def __init__(self, sonarr: Sonarr, entry_id: str, max_items: int = 10) -> None: + """Initialize Sonarr Wanted sensor.""" + self._max_items = max_items + self._results = None + self._total = None + + super().__init__( + sonarr=sonarr, + entry_id=entry_id, + icon="mdi:television", + key="wanted", + name=f"{sonarr.app.info.app_name} Wanted", + unit_of_measurement="Episodes", + enabled_default=False, + ) + + async def async_added_to_hass(self): + """Listen for signals.""" + await super().async_added_to_hass() + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"sonarr.{self._entry_id}.entry_options_update", + self.async_update_entry_options, ) - except OSError: - _LOGGER.warning("Host %s is not available", self.host) - self._available = False - self._state = None - return + ) - if res.status_code == HTTP_OK: - if self.type in ["upcoming", "queue", "series", "commands"]: - self.data = res.json() - self._state = len(self.data) - elif self.type == "wanted": - data = res.json() - res = requests.get( - "{}?pageSize={}".format( - ENDPOINTS[self.type].format( - self.ssl, self.host, self.port, self.urlbase - ), - data["totalRecords"], - ), - headers={"X-Api-Key": self.apikey}, - timeout=10, - ) - self.data = res.json()["records"] - self._state = len(self.data) - elif self.type == "diskspace": - # If included paths are not provided, use all data - if self.included == []: - self.data = res.json() - else: - # Filter to only show lists that are included - self.data = list( - filter(lambda x: x["path"] in self.included, res.json()) - ) - self._state = "{:.2f}".format( - to_unit(sum([data["freeSpace"] for data in self.data]), self._unit) - ) - elif self.type == "status": - self.data = res.json() - self._state = self.data["version"] - self._available = True + @sonarr_exception_handler + async def async_update(self) -> None: + """Update entity.""" + self._results = await self.sonarr.wanted(page_size=self._max_items) + self._total = self._results.total + async def async_update_entry_options(self, options: dict) -> None: + """Update sensor settings when config entry options are update.""" + self._max_items = options[CONF_WANTED_MAX_ITEMS] -def to_unit(value, unit): - """Convert bytes to give unit.""" - return value / 1024 ** BYTE_SIZES.index(unit) + @property + def device_state_attributes(self) -> Optional[Dict[str, Any]]: + """Return the state attributes of the entity.""" + attrs = {} + + if self._results is not None: + for episode in self._results.episodes: + name = f"{episode.series.title} {episode.identifier}" + attrs[name] = episode.airdate + + return attrs + + @property + def state(self) -> Union[None, str, int, float]: + """Return the state of the sensor.""" + return self._total diff --git a/homeassistant/components/sonarr/strings.json b/homeassistant/components/sonarr/strings.json new file mode 100644 index 00000000000..481a3d381f0 --- /dev/null +++ b/homeassistant/components/sonarr/strings.json @@ -0,0 +1,37 @@ +{ + "title": "Sonarr", + "config": { + "flow_title": "Sonarr: {name}", + "step": { + "user": { + "title": "Connect to Sonarr", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "api_key": "[%key:common::config_flow::data::api_key%]", + "base_path": "Path to API", + "port": "[%key:common::config_flow::data::port%]", + "ssl": "Sonarr uses a SSL certificate", + "verify_ssl": "Sonarr uses a proper certificate" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Number of upcoming days to display", + "wanted_max_items": "Max number of wanted items to display" + } + } + } + } +} diff --git a/homeassistant/components/sonarr/translations/ca.json b/homeassistant/components/sonarr/translations/ca.json new file mode 100644 index 00000000000..955d94e9202 --- /dev/null +++ b/homeassistant/components/sonarr/translations/ca.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat", + "unknown": "Error inesperat" + }, + "error": { + "cannot_connect": "No s'ha pogut connectar", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + }, + "flow_title": "Sonarr: {name}", + "step": { + "user": { + "data": { + "api_key": "Clau API", + "base_path": "Ruta a l'API", + "host": "Amfitri\u00f3", + "port": "Port", + "ssl": "Sonarr utilitza un certificat SSL", + "verify_ssl": "Sonarr utilitza un certificat adequat" + }, + "title": "Connexi\u00f3 amb Sonarr" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Nombre de dies propers a mostrar", + "wanted_max_items": "Nombre m\u00e0xim d'elements a mostrar" + } + } + } + }, + "title": "Sonarr" +} \ No newline at end of file diff --git a/homeassistant/components/sonarr/translations/en.json b/homeassistant/components/sonarr/translations/en.json new file mode 100644 index 00000000000..9e62ea16d77 --- /dev/null +++ b/homeassistant/components/sonarr/translations/en.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured", + "unknown": "Unexpected error" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication" + }, + "flow_title": "Sonarr: {name}", + "step": { + "user": { + "data": { + "api_key": "API Key", + "base_path": "Path to API", + "host": "Host", + "port": "Port", + "ssl": "Sonarr uses a SSL certificate", + "verify_ssl": "Sonarr uses a proper certificate" + }, + "title": "Connect to Sonarr" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Number of upcoming days to display", + "wanted_max_items": "Max number of wanted items to display" + } + } + } + }, + "title": "Sonarr" +} \ No newline at end of file diff --git a/homeassistant/components/sonarr/translations/es.json b/homeassistant/components/sonarr/translations/es.json new file mode 100644 index 00000000000..29db7cfbd77 --- /dev/null +++ b/homeassistant/components/sonarr/translations/es.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "El servicio ya est\u00e1 configurado", + "unknown": "Error inesperado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" + }, + "flow_title": "Sonarr: {name}", + "step": { + "user": { + "data": { + "api_key": "Clave API", + "base_path": "Ruta a la API", + "host": "Host", + "port": "Puerto", + "ssl": "Sonarr usa un certificado SSL", + "verify_ssl": "Sonarr usa un certificado adecuado" + }, + "title": "Conectar a Sonarr" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "N\u00famero de d\u00edas pr\u00f3ximos a mostrar", + "wanted_max_items": "N\u00famero m\u00e1ximo de elementos a mostrar" + } + } + } + }, + "title": "Sonarr" +} \ No newline at end of file diff --git a/homeassistant/components/sonarr/translations/fr.json b/homeassistant/components/sonarr/translations/fr.json new file mode 100644 index 00000000000..f613debcfbc --- /dev/null +++ b/homeassistant/components/sonarr/translations/fr.json @@ -0,0 +1,3 @@ +{ + "title": "Sonarr" +} \ No newline at end of file diff --git a/homeassistant/components/sonarr/translations/ko.json b/homeassistant/components/sonarr/translations/ko.json new file mode 100644 index 00000000000..d08f188303a --- /dev/null +++ b/homeassistant/components/sonarr/translations/ko.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "flow_title": "Sonarr: {name}", + "step": { + "user": { + "data": { + "api_key": "API \ud0a4", + "base_path": "API \uacbd\ub85c", + "host": "\ud638\uc2a4\ud2b8", + "port": "\ud3ec\ud2b8", + "ssl": "Sonarr \ub294 SSL \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4", + "verify_ssl": "Sonarr \ub294 \uc62c\ubc14\ub978 \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4" + }, + "title": "Sonarr \uc5d0 \uc5f0\uacb0\ud558\uae30" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "\ud45c\uc2dc\ud560 \uc608\uc815\uc77c \uc218", + "wanted_max_items": "\ud45c\uc2dc\ud560 Wanted \ud56d\ubaa9 \uc218" + } + } + } + }, + "title": "Sonarr" +} \ No newline at end of file diff --git a/homeassistant/components/sonarr/translations/no.json b/homeassistant/components/sonarr/translations/no.json new file mode 100644 index 00000000000..0b98c67d820 --- /dev/null +++ b/homeassistant/components/sonarr/translations/no.json @@ -0,0 +1,29 @@ +{ + "config": { + "flow_title": "", + "step": { + "user": { + "data": { + "api_key": "API N\u00f8kkel", + "base_path": "Bane til API", + "host": "Vert", + "port": "", + "ssl": "Sonarr bruker et SSL-sertifikat", + "verify_ssl": "Sonarr bruker et riktig sertifikat" + }, + "title": "Koble til Sonarr" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Antall kommende dager som skal vises", + "wanted_max_items": "Maks antall \u00f8nskede elementer som skal vises" + } + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/sonarr/translations/pl.json b/homeassistant/components/sonarr/translations/pl.json new file mode 100644 index 00000000000..e2c60427b7e --- /dev/null +++ b/homeassistant/components/sonarr/translations/pl.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana.", + "unknown": "Nieoczekiwany b\u0142\u0105d." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "invalid_auth": "Niepoprawne uwierzytelnienie." + }, + "flow_title": "Sonarr: {name}", + "step": { + "user": { + "data": { + "api_key": "Klucz API", + "base_path": "\u015acie\u017cka do API", + "host": "Nazwa hosta lub adres IP", + "port": "Port", + "ssl": "Sonarr u\u017cywa certyfikatu SSL", + "verify_ssl": "Sonarr u\u017cywa prawid\u0142owego certyfikatu" + }, + "title": "Po\u0142\u0105cz z Sonarr" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Liczba nadchodz\u0105cych dni do wy\u015bwietlenia", + "wanted_max_items": "Maksymalna liczba wy\u015bwietlanych element\u00f3w" + } + } + } + }, + "title": "Sonarr" +} \ No newline at end of file diff --git a/homeassistant/components/sonarr/translations/ru.json b/homeassistant/components/sonarr/translations/ru.json new file mode 100644 index 00000000000..d7ffd002981 --- /dev/null +++ b/homeassistant/components/sonarr/translations/ru.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "error": { + "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." + }, + "flow_title": "Sonarr: {name}", + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "base_path": "\u041f\u0443\u0442\u044c \u043a API", + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "Sonarr \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL", + "verify_ssl": "Sonarr \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442" + }, + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a Sonarr" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u0440\u0435\u0434\u0441\u0442\u043e\u044f\u0449\u0438\u0445 \u0434\u043d\u0435\u0439 \u0434\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f", + "wanted_max_items": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432 \u0434\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f" + } + } + } + }, + "title": "Sonarr" +} \ No newline at end of file diff --git a/homeassistant/components/sonarr/translations/sv.json b/homeassistant/components/sonarr/translations/sv.json new file mode 100644 index 00000000000..bbdafea6798 --- /dev/null +++ b/homeassistant/components/sonarr/translations/sv.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Anslut till Sonarr" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sonarr/translations/zh-Hant.json b/homeassistant/components/sonarr/translations/zh-Hant.json new file mode 100644 index 00000000000..dc03a007099 --- /dev/null +++ b/homeassistant/components/sonarr/translations/zh-Hant.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + }, + "flow_title": "Sonarr\uff1a{name}", + "step": { + "user": { + "data": { + "api_key": "API \u5bc6\u9470", + "base_path": "API \u8def\u5f91", + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0", + "ssl": "Sonarr \u4f7f\u7528 SSL \u8a8d\u8b49", + "verify_ssl": "Sonarr \u4f7f\u7528\u5c0d\u61c9\u8a8d\u8b49" + }, + "title": "\u9023\u7dda\u81f3 Sonarr" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "\u5373\u5c07\u5230\u4f86\u986f\u793a\u5929\u6578", + "wanted_max_items": "\u9700\u6c42\u9805\u76ee\u6700\u5927\u986f\u793a\u6578" + } + } + } + }, + "title": "Sonarr" +} \ No newline at end of file diff --git a/homeassistant/components/songpal/media_player.py b/homeassistant/components/songpal/media_player.py index 3777ecd8325..578cdaed367 100644 --- a/homeassistant/components/songpal/media_player.py +++ b/homeassistant/components/songpal/media_player.py @@ -316,11 +316,11 @@ class SongpalEntity(MediaPlayerEntity): async def async_volume_up(self): """Set volume up.""" - return await self._volume_control.set_volume("+1") + return await self._volume_control.set_volume(self._volume + 1) async def async_volume_down(self): """Set volume down.""" - return await self._volume_control.set_volume("-1") + return await self._volume_control.set_volume(self._volume - 1) async def async_turn_on(self): """Turn the device on.""" diff --git a/homeassistant/components/songpal/translations/ca.json b/homeassistant/components/songpal/translations/ca.json index 9f7c18187c3..10060f63ed4 100644 --- a/homeassistant/components/songpal/translations/ca.json +++ b/homeassistant/components/songpal/translations/ca.json @@ -5,20 +5,17 @@ "not_songpal_device": "No \u00e9s un dispositiu Songpal" }, "error": { - "cannot_connect": "No s'ha pogut connectar", - "connection": "Error de connexi\u00f3: comprova l'endpoint seleccionat" + "cannot_connect": "No s'ha pogut connectar" }, "flow_title": "Sony Songpal {name} ({host})", "step": { "init": { - "description": "Vols configurar {name} ({host})?", - "title": "Sony Songpal" + "description": "Vols configurar {name} ({host})?" }, "user": { "data": { "endpoint": "Endpoint" - }, - "title": "Sony Songpal" + } } } } diff --git a/homeassistant/components/songpal/translations/de.json b/homeassistant/components/songpal/translations/de.json index b351dab97db..851e0b5f77c 100644 --- a/homeassistant/components/songpal/translations/de.json +++ b/homeassistant/components/songpal/translations/de.json @@ -5,19 +5,17 @@ "not_songpal_device": "Kein Songpal-Ger\u00e4t" }, "error": { - "connection": "Verbindungsfehler: Bitte \u00fcberpr\u00fcfen Sie Ihren Endpunkt" + "cannot_connect": "Verbindung fehlgeschlagen" }, "flow_title": "Sony Songpal {name} ({host})", "step": { "init": { - "description": "M\u00f6chten Sie {name} ({host}) einrichten?", - "title": "Sony Songpal" + "description": "M\u00f6chten Sie {name} ({host}) einrichten?" }, "user": { "data": { "endpoint": "Endpunkt" - }, - "title": "Sony Songpal" + } } } } diff --git a/homeassistant/components/songpal/translations/en.json b/homeassistant/components/songpal/translations/en.json index f84510040ac..a44ba6e8668 100644 --- a/homeassistant/components/songpal/translations/en.json +++ b/homeassistant/components/songpal/translations/en.json @@ -5,20 +5,17 @@ "not_songpal_device": "Not a Songpal device" }, "error": { - "cannot_connect": "Failed to connect", - "connection": "Connection error: please check your endpoint" + "cannot_connect": "Failed to connect" }, "flow_title": "Sony Songpal {name} ({host})", "step": { "init": { - "description": "Do you want to set up {name} ({host})?", - "title": "Sony Songpal" + "description": "Do you want to set up {name} ({host})?" }, "user": { "data": { "endpoint": "Endpoint" - }, - "title": "Sony Songpal" + } } } } diff --git a/homeassistant/components/songpal/translations/es.json b/homeassistant/components/songpal/translations/es.json index bb82d0a006d..fa62eb3ba1e 100644 --- a/homeassistant/components/songpal/translations/es.json +++ b/homeassistant/components/songpal/translations/es.json @@ -5,20 +5,17 @@ "not_songpal_device": "No es un dispositivo Songpal" }, "error": { - "cannot_connect": "Error al conectar", - "connection": "Error de conexi\u00f3n: comprueba tu endpoint" + "cannot_connect": "No se pudo conectar" }, "flow_title": "Sony Songpal {name} ({host})", "step": { "init": { - "description": "\u00bfQuieres configurar {name} ({host})?", - "title": "Sony Songpal" + "description": "\u00bfQuieres configurar {name} ({host})?" }, "user": { "data": { "endpoint": "Endpoint" - }, - "title": "Sony Songpal" + } } } } diff --git a/homeassistant/components/songpal/translations/fi.json b/homeassistant/components/songpal/translations/fi.json index 3e06c6f1461..c88c95f42c6 100644 --- a/homeassistant/components/songpal/translations/fi.json +++ b/homeassistant/components/songpal/translations/fi.json @@ -4,20 +4,15 @@ "already_configured": "Laite on jo m\u00e4\u00e4ritetty", "not_songpal_device": "Ei Songpal-laite" }, - "error": { - "connection": "Yhteysvirhe: tarkista p\u00e4\u00e4tepisteesi" - }, "flow_title": "Sony Songpal {name} ({host})", "step": { "init": { - "description": "Haluatko m\u00e4\u00e4ritt\u00e4\u00e4 kohteen {name} ({host})?", - "title": "Sony Songpal" + "description": "Haluatko m\u00e4\u00e4ritt\u00e4\u00e4 kohteen {name} ({host})?" }, "user": { "data": { "endpoint": "P\u00e4\u00e4tepiste" - }, - "title": "Sony Songpal" + } } } } diff --git a/homeassistant/components/songpal/translations/fr.json b/homeassistant/components/songpal/translations/fr.json index f8a278a288e..aadbf7b3e56 100644 --- a/homeassistant/components/songpal/translations/fr.json +++ b/homeassistant/components/songpal/translations/fr.json @@ -7,11 +7,7 @@ "flow_title": "Sony Songpal {name} ({host})", "step": { "init": { - "description": "Voulez-vous configurer {name} ({host})?", - "title": "Sony Songpal" - }, - "user": { - "title": "Sony Songpal" + "description": "Voulez-vous configurer {name} ({host})?" } } } diff --git a/homeassistant/components/songpal/translations/hu.json b/homeassistant/components/songpal/translations/hu.json index b70fdd2d9b5..cd4c501ecf7 100644 --- a/homeassistant/components/songpal/translations/hu.json +++ b/homeassistant/components/songpal/translations/hu.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/songpal/translations/it.json b/homeassistant/components/songpal/translations/it.json index 6e0eb1fdc94..fff030a6d08 100644 --- a/homeassistant/components/songpal/translations/it.json +++ b/homeassistant/components/songpal/translations/it.json @@ -5,19 +5,17 @@ "not_songpal_device": "Non \u00e8 un dispositivo Songpal" }, "error": { - "connection": "Errore di connessione: controlla il tuo endpoint" + "cannot_connect": "Impossibile connettersi" }, "flow_title": "Sony Songpal {name} ({host})", "step": { "init": { - "description": "Vuoi impostare {name} ({host})?", - "title": "Sony Songpal" + "description": "Vuoi impostare {name} ({host})?" }, "user": { "data": { "endpoint": "Endpoint" - }, - "title": "Sony Songpal" + } } } } diff --git a/homeassistant/components/songpal/translations/ko.json b/homeassistant/components/songpal/translations/ko.json index b71ca83bf38..abe7f9b384c 100644 --- a/homeassistant/components/songpal/translations/ko.json +++ b/homeassistant/components/songpal/translations/ko.json @@ -5,20 +5,17 @@ "not_songpal_device": "Songpal \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "connection": "\uc5f0\uacb0 \uc624\ub958: \uc5d4\ub4dc\ud3ec\uc778\ud2b8\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694" + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, - "flow_title": "Sony Songpal {name} ({host})", + "flow_title": "Sony Songpal: {name} ({host})", "step": { "init": { - "description": "{name} ({host}) \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "Sony Songpal" + "description": "{name} ({host}) \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" }, "user": { "data": { "endpoint": "\uc5d4\ub4dc\ud3ec\uc778\ud2b8" - }, - "title": "Sony Songpal" + } } } } diff --git a/homeassistant/components/songpal/translations/lb.json b/homeassistant/components/songpal/translations/lb.json index c3d31b81e53..f322bff8006 100644 --- a/homeassistant/components/songpal/translations/lb.json +++ b/homeassistant/components/songpal/translations/lb.json @@ -5,20 +5,17 @@ "not_songpal_device": "Keen Songpal Apparat" }, "error": { - "cannot_connect": "Feeler beim verbannen", - "connection": "Feeler beim verbannen Iwwerpr\u00e9if w.e.g. den Endpunkt" + "cannot_connect": "Feeler beim verbannen" }, "flow_title": "Sony Songpal {name} ({host})", "step": { "init": { - "description": "Soll {name} ({host}) konfigur\u00e9iert ginn?", - "title": "Sony Songpal" + "description": "Soll {name} ({host}) konfigur\u00e9iert ginn?" }, "user": { "data": { "endpoint": "Endpunkt" - }, - "title": "Sony Songpal" + } } } } diff --git a/homeassistant/components/songpal/translations/no.json b/homeassistant/components/songpal/translations/no.json index adbee2adc9f..4c3ef9e6c0d 100644 --- a/homeassistant/components/songpal/translations/no.json +++ b/homeassistant/components/songpal/translations/no.json @@ -1,23 +1,17 @@ { "config": { "abort": { - "already_configured": "Enheten er allerede konfigurert", "not_songpal_device": "Ikke en Songpal-enhet" }, - "error": { - "connection": "Tilkoblingsfeil: vennligst sjekk endepunktet ditt" - }, "flow_title": "", "step": { "init": { - "description": "Vil du sette opp {name} ({host})?", - "title": "" + "description": "Vil du sette opp {name} ({host})?" }, "user": { "data": { "endpoint": "Endepunkt" - }, - "title": "" + } } } } diff --git a/homeassistant/components/songpal/translations/pl.json b/homeassistant/components/songpal/translations/pl.json index 8d6c913c844..cc420f0f83a 100644 --- a/homeassistant/components/songpal/translations/pl.json +++ b/homeassistant/components/songpal/translations/pl.json @@ -1,24 +1,21 @@ { "config": { "abort": { - "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]", - "not_songpal_device": "To nie jest urz\u0105dzenie Songpal" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "not_songpal_device": "To nie jest urz\u0105dzenie Songpal." }, "error": { - "cannot_connect": "[%key_id:common::config_flow::error::cannot_connect%]", - "connection": "B\u0142\u0105d po\u0142\u0105czenia: sprawd\u017a punkt ko\u0144cowy" + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." }, "flow_title": "Sony Songpal {name} ({host})", "step": { "init": { - "description": "Czy chcesz skonfigurowa\u0107 {name} ({host})?", - "title": "Sony Songpal" + "description": "Czy chcesz skonfigurowa\u0107 {name} ({host})?" }, "user": { "data": { "endpoint": "Punkt ko\u0144cowy" - }, - "title": "Sony Songpal" + } } } } diff --git a/homeassistant/components/songpal/translations/pt-BR.json b/homeassistant/components/songpal/translations/pt-BR.json new file mode 100644 index 00000000000..110e7413121 --- /dev/null +++ b/homeassistant/components/songpal/translations/pt-BR.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha ao conectar" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/songpal/translations/ru.json b/homeassistant/components/songpal/translations/ru.json index 3f9fc7ecc08..9bd54b442bc 100644 --- a/homeassistant/components/songpal/translations/ru.json +++ b/homeassistant/components/songpal/translations/ru.json @@ -5,20 +5,17 @@ "not_songpal_device": "\u041d\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Songpal." }, "error": { - "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", - "connection": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u0443\u044e \u043a\u043e\u043d\u0435\u0447\u043d\u0443\u044e \u0442\u043e\u0447\u043a\u0443." + "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f." }, "flow_title": "Sony Songpal {name} ({host})", "step": { "init": { - "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name} ({host})?", - "title": "Sony Songpal" + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name} ({host})?" }, "user": { "data": { "endpoint": "\u041a\u043e\u043d\u0435\u0447\u043d\u0430\u044f \u0442\u043e\u0447\u043a\u0430" - }, - "title": "Sony Songpal" + } } } } diff --git a/homeassistant/components/songpal/translations/sv.json b/homeassistant/components/songpal/translations/sv.json index 9913f89f3d9..ea2b5e2d715 100644 --- a/homeassistant/components/songpal/translations/sv.json +++ b/homeassistant/components/songpal/translations/sv.json @@ -4,20 +4,15 @@ "already_configured": "Enheten \u00e4r redan konfigurerad", "not_songpal_device": "Inte en Songpal-enhet" }, - "error": { - "connection": "Anslutningsfel: Kontrollera destinationen" - }, "flow_title": "Sony Songpal {name} ({host})", "step": { "init": { - "description": "Do vill du konfigurera {name} ({host})?", - "title": "Sony Songpal" + "description": "Do vill du konfigurera {name} ({host})?" }, "user": { "data": { "endpoint": "Destination." - }, - "title": "Sony Songpal" + } } } } diff --git a/homeassistant/components/songpal/translations/zh-Hant.json b/homeassistant/components/songpal/translations/zh-Hant.json index c505979b85e..b73aaade30c 100644 --- a/homeassistant/components/songpal/translations/zh-Hant.json +++ b/homeassistant/components/songpal/translations/zh-Hant.json @@ -5,20 +5,17 @@ "not_songpal_device": "\u4e26\u975e Songpal \u8a2d\u5099" }, "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557", - "connection": "\u9023\u7dda\u932f\u8aa4\uff1a\u8acb\u6aa2\u67e5\u7aef\u9ede" + "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "flow_title": "Sony Songpal {name} ({host})", "step": { "init": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name} ({host})\uff1f", - "title": "Sony Songpal" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name} ({host})\uff1f" }, "user": { "data": { "endpoint": "\u7aef\u9ede" - }, - "title": "Sony Songpal" + } } } } diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index c3a977e32e1..f19816e865d 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -4,9 +4,11 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.const import CONF_HOSTS +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv +from homeassistant.loader import bind_hass -from .const import DOMAIN +from .const import DATA_SONOS, DOMAIN CONF_ADVERTISE_ADDR = "advertise_addr" CONF_INTERFACE_ADDR = "interface_addr" @@ -53,3 +55,21 @@ async def async_setup_entry(hass, entry): hass.config_entries.async_forward_entry_setup(entry, MP_DOMAIN) ) return True + + +@bind_hass +def get_coordinator_id(hass, entity_id): + """Obtain the unique_id of a device's coordinator. + + This function is safe to run inside the event loop. + """ + if DATA_SONOS not in hass.data: + raise HomeAssistantError("Sonos integration not set up") + + device = next( + (x for x in hass.data[DATA_SONOS].entities if x.entity_id == entity_id), None + ) + + if device.is_coordinator: + return device.unique_id + return device.coordinator.unique_id diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index 5858f2bca9b..0d88249f740 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -1,3 +1,4 @@ """Const for Sonos.""" DOMAIN = "sonos" +DATA_SONOS = "sonos_media_player" diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index e5ce9ede290..7ce4af02e45 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,11 +3,11 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["pysonos==0.0.30"], + "requirements": ["pysonos==0.0.31"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:ZonePlayer:1" } ], - "codeowners": ["@amelchio"] + "codeowners": [] } diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index b4dc9530b90..15a168047e9 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -40,12 +40,14 @@ from homeassistant.const import ( ) from homeassistant.core import ServiceCall, callback from homeassistant.helpers import config_validation as cv, entity_platform, service +import homeassistant.helpers.device_registry as dr from homeassistant.util.dt import utcnow from . import ( CONF_ADVERTISE_ADDR, CONF_HOSTS, CONF_INTERFACE_ADDR, + DATA_SONOS, DOMAIN as SONOS_DOMAIN, ) @@ -69,8 +71,6 @@ SUPPORT_SONOS = ( | SUPPORT_CLEAR_PLAYLIST ) -DATA_SONOS = "sonos_media_player" - SOURCE_LINEIN = "Line-in" SOURCE_TV = "TV" @@ -151,15 +151,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities): try: _LOGGER.debug("Reached _discovered_player, soco=%s", soco) - if soco not in hass.data[DATA_SONOS].discovered: + if soco.uid not in hass.data[DATA_SONOS].discovered: _LOGGER.debug("Adding new entity") - hass.data[DATA_SONOS].discovered.append(soco) + hass.data[DATA_SONOS].discovered.append(soco.uid) hass.add_job(async_add_entities, [SonosEntity(soco)]) else: entity = _get_entity_from_soco_uid(hass, soco.uid) - if entity: + if entity and (entity.soco == soco or not entity.available): _LOGGER.debug("Seen %s", entity) - hass.add_job(entity.async_seen()) + hass.add_job(entity.async_seen(soco)) + except SoCoException as ex: _LOGGER.debug("SoCoException, ex=%s", ex) @@ -392,10 +393,12 @@ class SonosEntity(MediaPlayerEntity): speaker_info = self.soco.get_speaker_info(True) self._name = speaker_info["zone_name"] self._model = speaker_info["model_name"] + self._sw_version = speaker_info["software_version"] + self._mac_address = speaker_info["mac_address"] async def async_added_to_hass(self): """Subscribe sonos events.""" - await self.async_seen() + await self.async_seen(self.soco) self.hass.data[DATA_SONOS].entities.append(self) @@ -427,6 +430,8 @@ class SonosEntity(MediaPlayerEntity): "identifiers": {(SONOS_DOMAIN, self._unique_id)}, "name": self._name, "model": self._model.replace("Sonos ", ""), + "sw_version": self._sw_version, + "connections": {(dr.CONNECTION_NETWORK_MAC, self._mac_address)}, "manufacturer": "Sonos", } @@ -459,10 +464,12 @@ class SonosEntity(MediaPlayerEntity): """Return coordinator of this player.""" return self._coordinator - async def async_seen(self): + async def async_seen(self, player): """Record that this player was seen right now.""" was_available = self.available + self._player = player + if self._seen_timer: self._seen_timer() diff --git a/homeassistant/components/sonos/translations/bg.json b/homeassistant/components/sonos/translations/bg.json index 0c9d9a8d8e5..1b8c4d5cbee 100644 --- a/homeassistant/components/sonos/translations/bg.json +++ b/homeassistant/components/sonos/translations/bg.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Sonos?", - "title": "Sonos" + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Sonos?" } } } diff --git a/homeassistant/components/sonos/translations/ca.json b/homeassistant/components/sonos/translations/ca.json index 683149c6fd8..c7bbae58a41 100644 --- a/homeassistant/components/sonos/translations/ca.json +++ b/homeassistant/components/sonos/translations/ca.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Vols configurar Sonos?", - "title": "Sonos" + "description": "Vols configurar Sonos?" } } } diff --git a/homeassistant/components/sonos/translations/cs.json b/homeassistant/components/sonos/translations/cs.json index 9f87f719b5e..20aab9ac861 100644 --- a/homeassistant/components/sonos/translations/cs.json +++ b/homeassistant/components/sonos/translations/cs.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Chcete nastavit Sonos?", - "title": "Sonos" + "description": "Chcete nastavit Sonos?" } } } diff --git a/homeassistant/components/sonos/translations/da.json b/homeassistant/components/sonos/translations/da.json index 5c8c3cdeb3e..0a6600dca08 100644 --- a/homeassistant/components/sonos/translations/da.json +++ b/homeassistant/components/sonos/translations/da.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Vil du ops\u00e6tte Sonos?", - "title": "Sonos" + "description": "Vil du ops\u00e6tte Sonos?" } } } diff --git a/homeassistant/components/sonos/translations/de.json b/homeassistant/components/sonos/translations/de.json index b08900034ed..93b25cf0b97 100644 --- a/homeassistant/components/sonos/translations/de.json +++ b/homeassistant/components/sonos/translations/de.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "M\u00f6chtest du Sonos einrichten?", - "title": "Sonos" + "description": "M\u00f6chtest du Sonos einrichten?" } } } diff --git a/homeassistant/components/sonos/translations/en.json b/homeassistant/components/sonos/translations/en.json index 69b8bd6c3b8..3cd46eae627 100644 --- a/homeassistant/components/sonos/translations/en.json +++ b/homeassistant/components/sonos/translations/en.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Do you want to set up Sonos?", - "title": "Sonos" + "description": "Do you want to set up Sonos?" } } } diff --git a/homeassistant/components/sonos/translations/es-419.json b/homeassistant/components/sonos/translations/es-419.json index ba750323426..873fee687e1 100644 --- a/homeassistant/components/sonos/translations/es-419.json +++ b/homeassistant/components/sonos/translations/es-419.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u00bfDesea configurar Sonos?", - "title": "Sonos" + "description": "\u00bfDesea configurar Sonos?" } } } diff --git a/homeassistant/components/sonos/translations/es.json b/homeassistant/components/sonos/translations/es.json index 296dc3f222e..e1cc4b22ef7 100644 --- a/homeassistant/components/sonos/translations/es.json +++ b/homeassistant/components/sonos/translations/es.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u00bfQuieres configurar Sonos?", - "title": "Sonos" + "description": "\u00bfQuieres configurar Sonos?" } } } diff --git a/homeassistant/components/sonos/translations/fi.json b/homeassistant/components/sonos/translations/fi.json index a01f678b03c..314809cfcd8 100644 --- a/homeassistant/components/sonos/translations/fi.json +++ b/homeassistant/components/sonos/translations/fi.json @@ -2,8 +2,7 @@ "config": { "step": { "confirm": { - "description": "Haluatko m\u00e4\u00e4ritt\u00e4\u00e4 Sonosin?", - "title": "Sonos" + "description": "Haluatko m\u00e4\u00e4ritt\u00e4\u00e4 Sonosin?" } } } diff --git a/homeassistant/components/sonos/translations/fr.json b/homeassistant/components/sonos/translations/fr.json index d15390fdcf0..2bae0a69826 100644 --- a/homeassistant/components/sonos/translations/fr.json +++ b/homeassistant/components/sonos/translations/fr.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Voulez-vous configurer Sonos?", - "title": "Sonos" + "description": "Voulez-vous configurer Sonos?" } } } diff --git a/homeassistant/components/sonos/translations/he.json b/homeassistant/components/sonos/translations/he.json index 609cc3747be..91cbe81a2a6 100644 --- a/homeassistant/components/sonos/translations/he.json +++ b/homeassistant/components/sonos/translations/he.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea Sonos?", - "title": "Sonos" + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea Sonos?" } } } diff --git a/homeassistant/components/sonos/translations/hu.json b/homeassistant/components/sonos/translations/hu.json index 771f48d1d9b..aa10087a884 100644 --- a/homeassistant/components/sonos/translations/hu.json +++ b/homeassistant/components/sonos/translations/hu.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a Sonos-t?", - "title": "Sonos" + "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a Sonos-t?" } } } diff --git a/homeassistant/components/sonos/translations/id.json b/homeassistant/components/sonos/translations/id.json index b5b421d2d79..ef88cab5814 100644 --- a/homeassistant/components/sonos/translations/id.json +++ b/homeassistant/components/sonos/translations/id.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Apakah Anda ingin mengatur Sonos?", - "title": "Sonos" + "description": "Apakah Anda ingin mengatur Sonos?" } } } diff --git a/homeassistant/components/sonos/translations/it.json b/homeassistant/components/sonos/translations/it.json index 1307f4cfe7b..70353a2613a 100644 --- a/homeassistant/components/sonos/translations/it.json +++ b/homeassistant/components/sonos/translations/it.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Vuoi configurare Sonos?", - "title": "Sonos" + "description": "Vuoi configurare Sonos?" } } } diff --git a/homeassistant/components/sonos/translations/ko.json b/homeassistant/components/sonos/translations/ko.json index baa5200f264..c92b50a0f83 100644 --- a/homeassistant/components/sonos/translations/ko.json +++ b/homeassistant/components/sonos/translations/ko.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Sonos \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "Sonos" + "description": "Sonos \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" } } } diff --git a/homeassistant/components/sonos/translations/lb.json b/homeassistant/components/sonos/translations/lb.json index f2c1675a14c..902d7d8b3cc 100644 --- a/homeassistant/components/sonos/translations/lb.json +++ b/homeassistant/components/sonos/translations/lb.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Soll Sonos konfigur\u00e9iert ginn?", - "title": "Sonos" + "description": "Soll Sonos konfigur\u00e9iert ginn?" } } } diff --git a/homeassistant/components/sonos/translations/nl.json b/homeassistant/components/sonos/translations/nl.json index 5efd65b1c79..e52111fc50f 100644 --- a/homeassistant/components/sonos/translations/nl.json +++ b/homeassistant/components/sonos/translations/nl.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Wilt u Sonos instellen?", - "title": "Sonos" + "description": "Wilt u Sonos instellen?" } } } diff --git a/homeassistant/components/sonos/translations/nn.json b/homeassistant/components/sonos/translations/nn.json index cb08944d26b..0a43eda6343 100644 --- a/homeassistant/components/sonos/translations/nn.json +++ b/homeassistant/components/sonos/translations/nn.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Vil du sette opp Sonos?", - "title": "Sonos" + "description": "Vil du sette opp Sonos?" } } } diff --git a/homeassistant/components/sonos/translations/no.json b/homeassistant/components/sonos/translations/no.json index db792405988..e5e4792eaeb 100644 --- a/homeassistant/components/sonos/translations/no.json +++ b/homeassistant/components/sonos/translations/no.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u00d8nsker du \u00e5 sette opp Sonos?", - "title": "" + "description": "\u00d8nsker du \u00e5 sette opp Sonos?" } } } diff --git a/homeassistant/components/sonos/translations/pl.json b/homeassistant/components/sonos/translations/pl.json index 56a0e6d5341..d37bf4c1ea9 100644 --- a/homeassistant/components/sonos/translations/pl.json +++ b/homeassistant/components/sonos/translations/pl.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Czy chcesz skonfigurowa\u0107 Sonos?", - "title": "Sonos" + "description": "Czy chcesz skonfigurowa\u0107 Sonos?" } } } diff --git a/homeassistant/components/sonos/translations/pt-BR.json b/homeassistant/components/sonos/translations/pt-BR.json index 77e2c75540a..f2467135d35 100644 --- a/homeassistant/components/sonos/translations/pt-BR.json +++ b/homeassistant/components/sonos/translations/pt-BR.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Voc\u00ea quer configurar o Sonos?", - "title": "Sonos" + "description": "Voc\u00ea quer configurar o Sonos?" } } } diff --git a/homeassistant/components/sonos/translations/pt.json b/homeassistant/components/sonos/translations/pt.json index 765d5803675..51fbd16a20d 100644 --- a/homeassistant/components/sonos/translations/pt.json +++ b/homeassistant/components/sonos/translations/pt.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Deseja configurar o Sonos?", - "title": "Sonos" + "description": "Deseja configurar o Sonos?" } } } diff --git a/homeassistant/components/sonos/translations/ro.json b/homeassistant/components/sonos/translations/ro.json index 338539bf25b..4c6b00fb697 100644 --- a/homeassistant/components/sonos/translations/ro.json +++ b/homeassistant/components/sonos/translations/ro.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Dori\u021bi s\u0103 configura\u021bi Sonos?", - "title": "Sonos" + "description": "Dori\u021bi s\u0103 configura\u021bi Sonos?" } } } diff --git a/homeassistant/components/sonos/translations/ru.json b/homeassistant/components/sonos/translations/ru.json index fd5f3f6ef09..fa153fda3ce 100644 --- a/homeassistant/components/sonos/translations/ru.json +++ b/homeassistant/components/sonos/translations/ru.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Sonos?", - "title": "Sonos" + "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 Sonos?" } } } diff --git a/homeassistant/components/sonos/translations/sl.json b/homeassistant/components/sonos/translations/sl.json index b3d4cc352f5..23a9837e1dc 100644 --- a/homeassistant/components/sonos/translations/sl.json +++ b/homeassistant/components/sonos/translations/sl.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Ali \u017eelite nastaviti Sonos?", - "title": "Sonos" + "description": "Ali \u017eelite nastaviti Sonos?" } } } diff --git a/homeassistant/components/sonos/translations/sv.json b/homeassistant/components/sonos/translations/sv.json index 789c9b84a65..4f2202ff8f5 100644 --- a/homeassistant/components/sonos/translations/sv.json +++ b/homeassistant/components/sonos/translations/sv.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Vill du konfigurera Sonos?", - "title": "Sonos" + "description": "Vill du konfigurera Sonos?" } } } diff --git a/homeassistant/components/sonos/translations/vi.json b/homeassistant/components/sonos/translations/vi.json index b047e636eea..126a574f475 100644 --- a/homeassistant/components/sonos/translations/vi.json +++ b/homeassistant/components/sonos/translations/vi.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "B\u1ea1n c\u00f3 mu\u1ed1n thi\u1ebft l\u1eadp Sonos kh\u00f4ng?", - "title": "Sonos" + "description": "B\u1ea1n c\u00f3 mu\u1ed1n thi\u1ebft l\u1eadp Sonos kh\u00f4ng?" } } } diff --git a/homeassistant/components/sonos/translations/zh-Hans.json b/homeassistant/components/sonos/translations/zh-Hans.json index 7ea329dc903..1b703cd1ac6 100644 --- a/homeassistant/components/sonos/translations/zh-Hans.json +++ b/homeassistant/components/sonos/translations/zh-Hans.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u60a8\u60f3\u8981\u914d\u7f6e Sonos \u5417\uff1f", - "title": "Sonos" + "description": "\u60a8\u60f3\u8981\u914d\u7f6e Sonos \u5417\uff1f" } } } diff --git a/homeassistant/components/sonos/translations/zh-Hant.json b/homeassistant/components/sonos/translations/zh-Hant.json index 3b56f9ece6b..8ffa1577abe 100644 --- a/homeassistant/components/sonos/translations/zh-Hant.json +++ b/homeassistant/components/sonos/translations/zh-Hant.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a Sonos\uff1f", - "title": "Sonos" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Sonos\uff1f" } } } diff --git a/homeassistant/components/soundtouch/manifest.json b/homeassistant/components/soundtouch/manifest.json index c9cc4f32734..58bdab1a2d7 100644 --- a/homeassistant/components/soundtouch/manifest.json +++ b/homeassistant/components/soundtouch/manifest.json @@ -3,5 +3,6 @@ "name": "Bose Soundtouch", "documentation": "https://www.home-assistant.io/integrations/soundtouch", "requirements": ["libsoundtouch==0.8"], + "after_dependencies": ["zeroconf"], "codeowners": [] } diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index 9e5feb1c582..619bcdb471f 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -6,7 +6,7 @@ import voluptuous as vol from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN from homeassistant.components.spotify import config_flow from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_CREDENTIALS +from homeassistant.const import ATTR_CREDENTIALS, CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv @@ -16,14 +16,7 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( ) from homeassistant.helpers.typing import ConfigType -from .const import ( - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, - DATA_SPOTIFY_CLIENT, - DATA_SPOTIFY_ME, - DATA_SPOTIFY_SESSION, - DOMAIN, -) +from .const import DATA_SPOTIFY_CLIENT, DATA_SPOTIFY_ME, DATA_SPOTIFY_SESSION, DOMAIN CONFIG_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/spotify/const.py b/homeassistant/components/spotify/const.py index 37bd1a2bf81..f508c9b2938 100644 --- a/homeassistant/components/spotify/const.py +++ b/homeassistant/components/spotify/const.py @@ -2,9 +2,6 @@ DOMAIN = "spotify" -CONF_CLIENT_ID = "client_id" -CONF_CLIENT_SECRET = "client_secret" - DATA_SPOTIFY_CLIENT = "spotify_client" DATA_SPOTIFY_ME = "spotify_me" DATA_SPOTIFY_SESSION = "spotify_session" diff --git a/homeassistant/components/spotify/translations/pl.json b/homeassistant/components/spotify/translations/pl.json index f3c8413a02a..9dbd73661ee 100644 --- a/homeassistant/components/spotify/translations/pl.json +++ b/homeassistant/components/spotify/translations/pl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Spotify.", - "authorize_url_timeout": "[%key_id:common::config_flow::abort::oauth2_authorize_url_timeout%]", + "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", "missing_configuration": "Integracja ze Spotify nie jest skonfigurowana. Post\u0119puj zgodnie z dokumentacj\u0105." }, "create_entry": { @@ -10,7 +10,7 @@ }, "step": { "pick_implementation": { - "title": "[%key_id:common::config_flow::title::oauth2_pick_implementation%]" + "title": "Wybierz metod\u0119 uwierzytelniania" } } } diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index 9bfc5b35288..a5caa1b1592 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -2,6 +2,6 @@ "domain": "sql", "name": "SQL", "documentation": "https://www.home-assistant.io/integrations/sql", - "requirements": ["sqlalchemy==1.3.16"], + "requirements": ["sqlalchemy==1.3.17"], "codeowners": ["@dgomes"] } diff --git a/homeassistant/components/squeezebox/manifest.json b/homeassistant/components/squeezebox/manifest.json index ae076c88b4a..98456de67b5 100644 --- a/homeassistant/components/squeezebox/manifest.json +++ b/homeassistant/components/squeezebox/manifest.json @@ -3,5 +3,5 @@ "name": "Logitech Squeezebox", "documentation": "https://www.home-assistant.io/integrations/squeezebox", "codeowners": ["@rajlaud"], - "requirements": ["pysqueezebox==0.1.4"] + "requirements": ["pysqueezebox==0.2.1"] } diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index a2683346b63..54fac55198e 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -2,6 +2,7 @@ "domain": "ssdp", "name": "Simple Service Discovery Protocol (SSDP)", "documentation": "https://www.home-assistant.io/integrations/ssdp", - "requirements": ["defusedxml==0.6.0", "netdisco==2.6.0"], + "requirements": ["defusedxml==0.6.0", "netdisco==2.7.0"], + "after_dependencies": ["zeroconf"], "codeowners": [] } diff --git a/homeassistant/components/starline/translations/bg.json b/homeassistant/components/starline/translations/bg.json index 1f3fb32b8e3..af8242ce712 100644 --- a/homeassistant/components/starline/translations/bg.json +++ b/homeassistant/components/starline/translations/bg.json @@ -11,7 +11,7 @@ "app_id": "ID \u043d\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435", "app_secret": "\u0422\u0430\u0439\u043d\u0430" }, - "description": "\u0418\u0414 \u043d\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0438 \u0442\u0430\u0435\u043d \u043a\u043e\u0434 \u043e\u0442 StarLine \u0430\u043a\u0430\u0443\u043d\u0442 \u043d\u0430 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a", + "description": "\u0418\u0414 \u043d\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0438 \u0442\u0430\u0435\u043d \u043a\u043e\u0434 \u043e\u0442 [StarLine \u0430\u043a\u0430\u0443\u043d\u0442 \u043d\u0430 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a](https://my.starline.ru/developer)", "title": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0434\u0430\u043d\u043d\u0438 \u0437\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e" }, "auth_captcha": { diff --git a/homeassistant/components/starline/translations/ca.json b/homeassistant/components/starline/translations/ca.json index 722b65da2a8..58545ec9bfd 100644 --- a/homeassistant/components/starline/translations/ca.json +++ b/homeassistant/components/starline/translations/ca.json @@ -11,7 +11,7 @@ "app_id": "ID d'aplicaci\u00f3", "app_secret": "Secret" }, - "description": "ID d'aplicaci\u00f3 i codi secret de compte de desenvolupador de StarLine", + "description": "ID d'aplicaci\u00f3 i codi secret del [compte de desenvolupador de StarLine](https://my.starline.ru/developer)", "title": "Credencials d'aplicaci\u00f3" }, "auth_captcha": { diff --git a/homeassistant/components/starline/translations/en.json b/homeassistant/components/starline/translations/en.json index cf71829bf29..867cdccb19a 100644 --- a/homeassistant/components/starline/translations/en.json +++ b/homeassistant/components/starline/translations/en.json @@ -11,7 +11,7 @@ "app_id": "App ID", "app_secret": "Secret" }, - "description": "Application ID and secret code from StarLine developer account", + "description": "Application ID and secret code from [StarLine developer account](https://my.starline.ru/developer)", "title": "Application credentials" }, "auth_captcha": { diff --git a/homeassistant/components/starline/translations/it.json b/homeassistant/components/starline/translations/it.json index 01364b9c3ce..57bc2213103 100644 --- a/homeassistant/components/starline/translations/it.json +++ b/homeassistant/components/starline/translations/it.json @@ -11,7 +11,7 @@ "app_id": "ID applicazione", "app_secret": "Segreto" }, - "description": "ID applicazione e codice segreto da Account sviluppatore StarLine ", + "description": "ID applicazione e codice segreto dall'[account sviluppatore StarLine](https://my.starline.ru/developer)", "title": "Credenziali dell'applicazione" }, "auth_captcha": { diff --git a/homeassistant/components/starline/translations/ko.json b/homeassistant/components/starline/translations/ko.json index a4afdd6e22f..e9355a4d649 100644 --- a/homeassistant/components/starline/translations/ko.json +++ b/homeassistant/components/starline/translations/ko.json @@ -11,7 +11,7 @@ "app_id": "\uc560\ud50c\ub9ac\ucf00\uc774\uc158 ID", "app_secret": "\ubcf4\uc548\ud0a4" }, - "description": "StarLine \uac1c\ubc1c\uc790 \uacc4\uc815\uc758 \uc560\ud50c\ub9ac\ucf00\uc774\uc158 ID \ubc0f \ube44\ubc00\ubc88\ud638", + "description": "[StarLine \uac1c\ubc1c\uc790 \uacc4\uc815](https://my.starline.ru/developer) \uc758 \uc560\ud50c\ub9ac\ucf00\uc774\uc158 ID \ubc0f \ubcf4\uc548\ud0a4 \ucf54\ub4dc", "title": "\uc560\ud50c\ub9ac\ucf00\uc774\uc158 \uc790\uaca9 \uc99d\uba85" }, "auth_captcha": { diff --git a/homeassistant/components/starline/translations/nl.json b/homeassistant/components/starline/translations/nl.json index e92372f3d86..9763a0422a5 100644 --- a/homeassistant/components/starline/translations/nl.json +++ b/homeassistant/components/starline/translations/nl.json @@ -11,7 +11,7 @@ "app_id": "Toepassings-ID", "app_secret": "Geheime code" }, - "description": "Toepassings-ID en de geheime code van StarLine developer account", + "description": "Applicatie-ID en geheime code van [StarLine-ontwikkelaarsaccount] (https://my.starline.ru/developer)", "title": "Inloggegevens van de applicatie" }, "auth_captcha": { diff --git a/homeassistant/components/starline/translations/no.json b/homeassistant/components/starline/translations/no.json index a96b788c90d..36545f3efd7 100644 --- a/homeassistant/components/starline/translations/no.json +++ b/homeassistant/components/starline/translations/no.json @@ -11,7 +11,7 @@ "app_id": "App-ID", "app_secret": "Hemmelig" }, - "description": "Applikasjons-ID og hemmelig kode fra StarLine utviklerkonto ", + "description": "Applikasjons-ID og hemmelig kode fra [StarLine utviklerkonto](https://my.starline.ru/developer)", "title": "Programlegitimasjon" }, "auth_captcha": { diff --git a/homeassistant/components/starline/translations/pl.json b/homeassistant/components/starline/translations/pl.json index 69691db21f8..5e5a293fc82 100644 --- a/homeassistant/components/starline/translations/pl.json +++ b/homeassistant/components/starline/translations/pl.json @@ -30,8 +30,8 @@ }, "auth_user": { "data": { - "password": "[%key_id:common::config_flow::data::password%]", - "username": "[%key_id:common::config_flow::data::username%]" + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" }, "description": "Adres e-mail i has\u0142o do konta StarLine", "title": "Po\u015bwiadczenia u\u017cytkownika" diff --git a/homeassistant/components/starline/translations/ru.json b/homeassistant/components/starline/translations/ru.json index 156f7fb8262..ea6833f5842 100644 --- a/homeassistant/components/starline/translations/ru.json +++ b/homeassistant/components/starline/translations/ru.json @@ -11,7 +11,7 @@ "app_id": "ID \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f", "app_secret": "\u0421\u0435\u043a\u0440\u0435\u0442\u043d\u044b\u0439 \u043a\u043e\u0434" }, - "description": "ID \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438 \u0441\u0435\u043a\u0440\u0435\u0442\u043d\u044b\u0439 \u043a\u043e\u0434 \u0438\u0437 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430 StarLine", + "description": "ID \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438 \u0441\u0435\u043a\u0440\u0435\u0442\u043d\u044b\u0439 \u043a\u043e\u0434 [\u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430 StarLine](https://my.starline.ru/developer)", "title": "\u0423\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f" }, "auth_captcha": { diff --git a/homeassistant/components/starline/translations/zh-Hant.json b/homeassistant/components/starline/translations/zh-Hant.json index d1635d0bc27..81a65ac0405 100644 --- a/homeassistant/components/starline/translations/zh-Hant.json +++ b/homeassistant/components/starline/translations/zh-Hant.json @@ -11,7 +11,7 @@ "app_id": "App ID", "app_secret": "\u5bc6\u78bc" }, - "description": "Application ID and secret code \u7531 StarLine \u958b\u767c\u8005\u5e33\u865f \u6240\u53d6\u5f97\u7684\u61c9\u7528\u7a0b\u5f0f ID \u8207\u5bc6\u78bc", + "description": "\u7531 [StarLine \u958b\u767c\u8005\u5e33\u865f] (https://my.starline.ru/developer) \u6240\u53d6\u5f97\u4e4b\u61c9\u7528\u7a0b\u5f0f ID \u8207\u5bc6\u78bc", "title": "\u61c9\u7528\u6191\u8b49" }, "auth_captcha": { diff --git a/homeassistant/components/stookalert/binary_sensor.py b/homeassistant/components/stookalert/binary_sensor.py index a07f208ac9d..df0c1db369c 100644 --- a/homeassistant/components/stookalert/binary_sensor.py +++ b/homeassistant/components/stookalert/binary_sensor.py @@ -13,6 +13,7 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(minutes=60) CONF_PROVINCE = "province" +DEFAULT_DEVICE_CLASS = "safety" DEFAULT_NAME = "Stookalert" ATTRIBUTION = "Data provided by rivm.nl" PROVINCES = [ @@ -74,6 +75,11 @@ class StookalertBinarySensor(BinarySensorEntity): """Return True if the Alert is active.""" return self._api_handler.state == 1 + @property + def device_class(self): + """Return the device class of this binary sensor.""" + return DEFAULT_DEVICE_CLASS + def update(self): """Update the data from the Stookalert handler.""" self._api_handler.get_alerts() diff --git a/homeassistant/components/stream/manifest.json b/homeassistant/components/stream/manifest.json index e90d93cbfe3..d434d1ce1ed 100644 --- a/homeassistant/components/stream/manifest.json +++ b/homeassistant/components/stream/manifest.json @@ -2,7 +2,7 @@ "domain": "stream", "name": "Stream", "documentation": "https://www.home-assistant.io/integrations/stream", - "requirements": ["av==8.0.1"], + "requirements": ["av==8.0.2"], "dependencies": ["http"], "codeowners": ["@hunterjm"], "quality_scale": "internal" diff --git a/homeassistant/components/switch/translations/ca.json b/homeassistant/components/switch/translations/ca.json index e7d31e28da0..e39386a680f 100644 --- a/homeassistant/components/switch/translations/ca.json +++ b/homeassistant/components/switch/translations/ca.json @@ -16,9 +16,9 @@ }, "state": { "_": { - "off": "Apagat", - "on": "Enc\u00e8s" + "off": "OFF", + "on": "ON" } }, - "title": "Interruptors" + "title": "Interruptor" } \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index b2ff2d2e8ef..89dc39e427c 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -1,14 +1,20 @@ """The Synology DSM component.""" +import asyncio from datetime import timedelta +import logging +from typing import Dict from synology_dsm import SynologyDSM +from synology_dsm.api.core.security import SynoCoreSecurity from synology_dsm.api.core.utilization import SynoCoreUtilization from synology_dsm.api.dsm.information import SynoDSMInformation +from synology_dsm.api.dsm.network import SynoDSMNetwork from synology_dsm.api.storage.storage import SynoStorage import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_DISKS, CONF_HOST, CONF_MAC, @@ -18,8 +24,14 @@ from homeassistant.const import ( CONF_SSL, CONF_USERNAME, ) +from homeassistant.core import callback +from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import HomeAssistantType @@ -28,8 +40,19 @@ from .const import ( DEFAULT_SCAN_INTERVAL, DEFAULT_SSL, DOMAIN, + ENTITY_CLASS, + ENTITY_ENABLE, + ENTITY_ICON, + ENTITY_NAME, + ENTITY_UNIT, + PLATFORMS, + STORAGE_DISK_BINARY_SENSORS, + STORAGE_DISK_SENSORS, + STORAGE_VOL_SENSORS, SYNO_API, + TEMP_SENSORS_KEYS, UNDO_UPDATE_LISTENER, + UTILISATION_SENSORS, ) CONFIG_SCHEMA = vol.Schema( @@ -49,6 +72,11 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +ATTRIBUTION = "Data provided by Synology" + + +_LOGGER = logging.getLogger(__name__) + async def async_setup(hass, config): """Set up Synology DSM sensors from legacy config file.""" @@ -71,6 +99,65 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Set up Synology DSM sensors.""" api = SynoApi(hass, entry) + # Migrate old unique_id + @callback + def _async_migrator(entity_entry: entity_registry.RegistryEntry): + """Migrate away from ID using label.""" + # Reject if new unique_id + if "SYNO." in entity_entry.unique_id: + return None + + entries = { + **STORAGE_DISK_BINARY_SENSORS, + **STORAGE_DISK_SENSORS, + **STORAGE_VOL_SENSORS, + **UTILISATION_SENSORS, + } + infos = entity_entry.unique_id.split("_") + serial = infos.pop(0) + label = infos.pop(0) + device_id = "_".join(infos) + + # Removed entity + if ( + "Type" in entity_entry.unique_id + or "Device" in entity_entry.unique_id + or "Name" in entity_entry.unique_id + ): + return None + + entity_type = None + for entity_key, entity_attrs in entries.items(): + if ( + device_id + and entity_attrs[ENTITY_NAME] == "Status" + and "Status" in entity_entry.unique_id + and "(Smart)" not in entity_entry.unique_id + ): + if "sd" in device_id and "disk" in entity_key: + entity_type = entity_key + continue + if "volume" in device_id and "volume" in entity_key: + entity_type = entity_key + continue + + if entity_attrs[ENTITY_NAME] == label: + entity_type = entity_key + + new_unique_id = "_".join([serial, entity_type]) + if device_id: + new_unique_id += f"_{device_id}" + + _LOGGER.info( + "Migrating unique_id from [%s] to [%s]", + entity_entry.unique_id, + new_unique_id, + ) + return {"new_unique_id": new_unique_id} + + await entity_registry.async_migrate_entries(hass, entry.entry_id, _async_migrator) + + # Continue setup await api.async_setup() undo_listener = entry.add_update_listener(_async_update_listener) @@ -88,16 +175,24 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): entry, data={**entry.data, CONF_MAC: network.macs} ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "sensor") - ) + 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: HomeAssistantType, entry: ConfigEntry): """Unload Synology DSM sensors.""" - unload_ok = await hass.config_entries.async_forward_entry_unload(entry, "sensor") + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS + ] + ) + ) if unload_ok: entry_data = hass.data[DOMAIN][entry.unique_id] @@ -121,10 +216,19 @@ class SynoApi: self._hass = hass self._entry = entry + # DSM APIs self.dsm: SynologyDSM = None self.information: SynoDSMInformation = None - self.utilisation: SynoCoreUtilization = None + self.network: SynoDSMNetwork = None + self.security: SynoCoreSecurity = None self.storage: SynoStorage = None + self.utilisation: SynoCoreUtilization = None + + # Should we fetch them + self._fetching_entities = {} + self._with_security = True + self._with_storage = True + self._with_utilisation = True self._unsub_dispatcher = None @@ -144,12 +248,14 @@ class SynoApi: device_token=self._entry.data.get("device_token"), ) + self._async_setup_api_requests() + await self._hass.async_add_executor_job(self._fetch_device_configuration) - await self.update() + await self.async_update() self._unsub_dispatcher = async_track_time_interval( self._hass, - self.update, + self.async_update, timedelta( minutes=self._entry.options.get( CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL @@ -157,17 +263,217 @@ class SynoApi: ), ) + @callback + def subscribe(self, api_key, unique_id): + """Subscribe an entity from API fetches.""" + if api_key not in self._fetching_entities: + self._fetching_entities[api_key] = set() + self._fetching_entities[api_key].add(unique_id) + + @callback + def unsubscribe() -> None: + """Unsubscribe an entity from API fetches (when disable).""" + self._fetching_entities[api_key].remove(unique_id) + + return unsubscribe + + @callback + def _async_setup_api_requests(self): + """Determine if we should fetch each API, if one entity needs it.""" + # Entities not added yet, fetch all + if not self._fetching_entities: + return + + # Determine if we should fetch an API + self._with_security = bool( + self._fetching_entities.get(SynoCoreSecurity.API_KEY) + ) + self._with_storage = bool(self._fetching_entities.get(SynoStorage.API_KEY)) + self._with_utilisation = bool( + self._fetching_entities.get(SynoCoreUtilization.API_KEY) + ) + + # Reset not used API + if not self._with_security: + self.dsm.reset(self.security) + self.security = None + + if not self._with_storage: + self.dsm.reset(self.storage) + self.storage = None + + if not self._with_utilisation: + self.dsm.reset(self.utilisation) + self.utilisation = None + def _fetch_device_configuration(self): """Fetch initial device config.""" self.information = self.dsm.information - self.utilisation = self.dsm.utilisation - self.storage = self.dsm.storage + self.network = self.dsm.network + + if self._with_security: + self.security = self.dsm.security + + if self._with_storage: + self.storage = self.dsm.storage + + if self._with_utilisation: + self.utilisation = self.dsm.utilisation async def async_unload(self): """Stop interacting with the NAS and prepare for removal from hass.""" self._unsub_dispatcher() - async def update(self, now=None): + async def async_update(self, now=None): """Update function for updating API information.""" + self._async_setup_api_requests() await self._hass.async_add_executor_job(self.dsm.update) async_dispatcher_send(self._hass, self.signal_sensor_update) + + +class SynologyDSMEntity(Entity): + """Representation of a Synology NAS entry.""" + + def __init__( + self, api: SynoApi, entity_type: str, entity_info: Dict[str, str], + ): + """Initialize the Synology DSM entity.""" + self._api = api + self._api_key = entity_type.split(":")[0] + self.entity_type = entity_type.split(":")[-1] + self._name = f"{api.network.hostname} {entity_info[ENTITY_NAME]}" + self._class = entity_info[ENTITY_CLASS] + self._enable_default = entity_info[ENTITY_ENABLE] + self._icon = entity_info[ENTITY_ICON] + self._unit = entity_info[ENTITY_UNIT] + self._unique_id = f"{self._api.information.serial}_{entity_type}" + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._unique_id + + @property + def name(self) -> str: + """Return the name.""" + return self._name + + @property + def icon(self) -> str: + """Return the icon.""" + return self._icon + + @property + def unit_of_measurement(self) -> str: + """Return the unit the value is expressed in.""" + if self.entity_type in TEMP_SENSORS_KEYS: + return self.hass.config.units.temperature_unit + return self._unit + + @property + def device_class(self) -> str: + """Return the class of this device.""" + return self._class + + @property + def device_state_attributes(self) -> Dict[str, any]: + """Return the state attributes.""" + return {ATTR_ATTRIBUTION: ATTRIBUTION} + + @property + def device_info(self) -> Dict[str, any]: + """Return the device information.""" + return { + "identifiers": {(DOMAIN, self._api.information.serial)}, + "name": "Synology NAS", + "manufacturer": "Synology", + "model": self._api.information.model, + "sw_version": self._api.information.version_string, + } + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return self._enable_default + + @property + def should_poll(self) -> bool: + """No polling needed.""" + return False + + async def async_update(self): + """Only used by the generic entity update service.""" + if not self.enabled: + return + + await self._api.async_update() + + async def async_added_to_hass(self): + """Register state update callback.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, self._api.signal_sensor_update, self.async_write_ha_state + ) + ) + + self.async_on_remove(self._api.subscribe(self._api_key, self.unique_id)) + + +class SynologyDSMDeviceEntity(SynologyDSMEntity): + """Representation of a Synology NAS disk or volume entry.""" + + def __init__( + self, + api: SynoApi, + entity_type: str, + entity_info: Dict[str, str], + device_id: str = None, + ): + """Initialize the Synology DSM disk or volume entity.""" + super().__init__(api, entity_type, entity_info) + self._device_id = device_id + self._device_name = None + self._device_manufacturer = None + self._device_model = None + self._device_firmware = None + self._device_type = None + + if "volume" in entity_type: + volume = self._api.storage._get_volume(self._device_id) + # Volume does not have a name + self._device_name = volume["id"].replace("_", " ").capitalize() + self._device_manufacturer = "Synology" + self._device_model = self._api.information.model + self._device_firmware = self._api.information.version_string + self._device_type = ( + volume["device_type"] + .replace("_", " ") + .replace("raid", "RAID") + .replace("shr", "SHR") + ) + elif "disk" in entity_type: + disk = self._api.storage._get_disk(self._device_id) + self._device_name = disk["name"] + self._device_manufacturer = disk["vendor"] + self._device_model = disk["model"].strip() + self._device_firmware = disk["firm"] + self._device_type = disk["diskType"] + self._name = f"{self._api.network.hostname} {self._device_name} {entity_info[ENTITY_NAME]}" + self._unique_id += f"_{self._device_id}" + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return bool(self._api.storage) + + @property + def device_info(self) -> Dict[str, any]: + """Return the device information.""" + return { + "identifiers": {(DOMAIN, self._api.information.serial, self._device_id)}, + "name": f"Synology NAS ({self._device_name} - {self._device_type})", + "manufacturer": self._device_manufacturer, + "model": self._device_model, + "sw_version": self._device_firmware, + "via_device": (DOMAIN, self._api.information.serial), + } diff --git a/homeassistant/components/synology_dsm/binary_sensor.py b/homeassistant/components/synology_dsm/binary_sensor.py new file mode 100644 index 00000000000..3dfc21b8a7b --- /dev/null +++ b/homeassistant/components/synology_dsm/binary_sensor.py @@ -0,0 +1,66 @@ +"""Support for Synology DSM binary sensors.""" +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_DISKS +from homeassistant.helpers.typing import HomeAssistantType + +from . import SynologyDSMDeviceEntity, SynologyDSMEntity +from .const import ( + DOMAIN, + SECURITY_BINARY_SENSORS, + STORAGE_DISK_BINARY_SENSORS, + SYNO_API, +) + + +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: + """Set up the Synology NAS binary sensor.""" + + api = hass.data[DOMAIN][entry.unique_id][SYNO_API] + + entities = [ + SynoDSMSecurityBinarySensor( + api, sensor_type, SECURITY_BINARY_SENSORS[sensor_type] + ) + for sensor_type in SECURITY_BINARY_SENSORS + ] + + # Handle all disks + if api.storage.disks_ids: + for disk in entry.data.get(CONF_DISKS, api.storage.disks_ids): + entities += [ + SynoDSMStorageBinarySensor( + api, sensor_type, STORAGE_DISK_BINARY_SENSORS[sensor_type], disk + ) + for sensor_type in STORAGE_DISK_BINARY_SENSORS + ] + + async_add_entities(entities) + + +class SynoDSMSecurityBinarySensor(SynologyDSMEntity, BinarySensorEntity): + """Representation a Synology Security binary sensor.""" + + @property + def is_on(self) -> bool: + """Return the state.""" + return getattr(self._api.security, self.entity_type) != "safe" + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return bool(self._api.security) + + +class SynoDSMStorageBinarySensor(SynologyDSMDeviceEntity, BinarySensorEntity): + """Representation a Synology Storage binary sensor.""" + + @property + def is_on(self) -> bool: + """Return the state.""" + attr = getattr(self._api.storage, self.entity_type)(self._device_id) + if attr is None: + return None + return attr diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index e0a166e908b..d19e919e41b 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -1,4 +1,9 @@ """Constants for Synology DSM.""" + +from synology_dsm.api.core.security import SynoCoreSecurity +from synology_dsm.api.core.utilization import SynoCoreUtilization +from synology_dsm.api.storage.storage import SynoStorage + from homeassistant.const import ( DATA_MEGABYTES, DATA_RATE_KILOBYTES_PER_SECOND, @@ -7,7 +12,7 @@ from homeassistant.const import ( ) DOMAIN = "synology_dsm" -BASE_NAME = "Synology" +PLATFORMS = ["binary_sensor", "sensor"] # Entry keys SYNO_API = "syno_api" @@ -15,47 +20,231 @@ UNDO_UPDATE_LISTENER = "undo_update_listener" # Configuration CONF_VOLUMES = "volumes" + DEFAULT_SSL = True DEFAULT_PORT = 5000 DEFAULT_PORT_SSL = 5001 # Options DEFAULT_SCAN_INTERVAL = 15 # min + +ENTITY_NAME = "name" +ENTITY_UNIT = "unit" +ENTITY_ICON = "icon" +ENTITY_CLASS = "device_class" +ENTITY_ENABLE = "enable" + +# Entity keys should start with the API_KEY to fetch + +# Binary sensors +STORAGE_DISK_BINARY_SENSORS = { + f"{SynoStorage.API_KEY}:disk_exceed_bad_sector_thr": { + ENTITY_NAME: "Exceeded Max Bad Sectors", + ENTITY_UNIT: None, + ENTITY_ICON: "mdi:test-tube", + ENTITY_CLASS: None, + ENTITY_ENABLE: True, + }, + f"{SynoStorage.API_KEY}:disk_below_remain_life_thr": { + ENTITY_NAME: "Below Min Remaining Life", + ENTITY_UNIT: None, + ENTITY_ICON: "mdi:test-tube", + ENTITY_CLASS: None, + ENTITY_ENABLE: True, + }, +} + +SECURITY_BINARY_SENSORS = { + f"{SynoCoreSecurity.API_KEY}:status": { + ENTITY_NAME: "Security status", + ENTITY_UNIT: None, + ENTITY_ICON: "mdi:checkbox-marked-circle-outline", + ENTITY_CLASS: "safety", + ENTITY_ENABLE: True, + }, +} + +# Sensors UTILISATION_SENSORS = { - "cpu_other_load": ["CPU Load (Other)", UNIT_PERCENTAGE, "mdi:chip"], - "cpu_user_load": ["CPU Load (User)", UNIT_PERCENTAGE, "mdi:chip"], - "cpu_system_load": ["CPU Load (System)", UNIT_PERCENTAGE, "mdi:chip"], - "cpu_total_load": ["CPU Load (Total)", UNIT_PERCENTAGE, "mdi:chip"], - "cpu_1min_load": ["CPU Load (1 min)", UNIT_PERCENTAGE, "mdi:chip"], - "cpu_5min_load": ["CPU Load (5 min)", UNIT_PERCENTAGE, "mdi:chip"], - "cpu_15min_load": ["CPU Load (15 min)", UNIT_PERCENTAGE, "mdi:chip"], - "memory_real_usage": ["Memory Usage (Real)", UNIT_PERCENTAGE, "mdi:memory"], - "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"], + f"{SynoCoreUtilization.API_KEY}:cpu_other_load": { + ENTITY_NAME: "CPU Load (Other)", + ENTITY_UNIT: UNIT_PERCENTAGE, + ENTITY_ICON: "mdi:chip", + ENTITY_CLASS: None, + ENTITY_ENABLE: False, + }, + f"{SynoCoreUtilization.API_KEY}:cpu_user_load": { + ENTITY_NAME: "CPU Load (User)", + ENTITY_UNIT: UNIT_PERCENTAGE, + ENTITY_ICON: "mdi:chip", + ENTITY_CLASS: None, + ENTITY_ENABLE: True, + }, + f"{SynoCoreUtilization.API_KEY}:cpu_system_load": { + ENTITY_NAME: "CPU Load (System)", + ENTITY_UNIT: UNIT_PERCENTAGE, + ENTITY_ICON: "mdi:chip", + ENTITY_CLASS: None, + ENTITY_ENABLE: False, + }, + f"{SynoCoreUtilization.API_KEY}:cpu_total_load": { + ENTITY_NAME: "CPU Load (Total)", + ENTITY_UNIT: UNIT_PERCENTAGE, + ENTITY_ICON: "mdi:chip", + ENTITY_CLASS: None, + ENTITY_ENABLE: True, + }, + f"{SynoCoreUtilization.API_KEY}:cpu_1min_load": { + ENTITY_NAME: "CPU Load (1 min)", + ENTITY_UNIT: UNIT_PERCENTAGE, + ENTITY_ICON: "mdi:chip", + ENTITY_CLASS: None, + ENTITY_ENABLE: False, + }, + f"{SynoCoreUtilization.API_KEY}:cpu_5min_load": { + ENTITY_NAME: "CPU Load (5 min)", + ENTITY_UNIT: UNIT_PERCENTAGE, + ENTITY_ICON: "mdi:chip", + ENTITY_CLASS: None, + ENTITY_ENABLE: True, + }, + f"{SynoCoreUtilization.API_KEY}:cpu_15min_load": { + ENTITY_NAME: "CPU Load (15 min)", + ENTITY_UNIT: UNIT_PERCENTAGE, + ENTITY_ICON: "mdi:chip", + ENTITY_CLASS: None, + ENTITY_ENABLE: True, + }, + f"{SynoCoreUtilization.API_KEY}:memory_real_usage": { + ENTITY_NAME: "Memory Usage (Real)", + ENTITY_UNIT: UNIT_PERCENTAGE, + ENTITY_ICON: "mdi:memory", + ENTITY_CLASS: None, + ENTITY_ENABLE: True, + }, + f"{SynoCoreUtilization.API_KEY}:memory_size": { + ENTITY_NAME: "Memory Size", + ENTITY_UNIT: DATA_MEGABYTES, + ENTITY_ICON: "mdi:memory", + ENTITY_CLASS: None, + ENTITY_ENABLE: False, + }, + f"{SynoCoreUtilization.API_KEY}:memory_cached": { + ENTITY_NAME: "Memory Cached", + ENTITY_UNIT: DATA_MEGABYTES, + ENTITY_ICON: "mdi:memory", + ENTITY_CLASS: None, + ENTITY_ENABLE: False, + }, + f"{SynoCoreUtilization.API_KEY}:memory_available_swap": { + ENTITY_NAME: "Memory Available (Swap)", + ENTITY_UNIT: DATA_MEGABYTES, + ENTITY_ICON: "mdi:memory", + ENTITY_CLASS: None, + ENTITY_ENABLE: True, + }, + f"{SynoCoreUtilization.API_KEY}:memory_available_real": { + ENTITY_NAME: "Memory Available (Real)", + ENTITY_UNIT: DATA_MEGABYTES, + ENTITY_ICON: "mdi:memory", + ENTITY_CLASS: None, + ENTITY_ENABLE: True, + }, + f"{SynoCoreUtilization.API_KEY}:memory_total_swap": { + ENTITY_NAME: "Memory Total (Swap)", + ENTITY_UNIT: DATA_MEGABYTES, + ENTITY_ICON: "mdi:memory", + ENTITY_CLASS: None, + ENTITY_ENABLE: True, + }, + f"{SynoCoreUtilization.API_KEY}:memory_total_real": { + ENTITY_NAME: "Memory Total (Real)", + ENTITY_UNIT: DATA_MEGABYTES, + ENTITY_ICON: "mdi:memory", + ENTITY_CLASS: None, + ENTITY_ENABLE: True, + }, + f"{SynoCoreUtilization.API_KEY}:network_up": { + ENTITY_NAME: "Network Up", + ENTITY_UNIT: DATA_RATE_KILOBYTES_PER_SECOND, + ENTITY_ICON: "mdi:upload", + ENTITY_CLASS: None, + ENTITY_ENABLE: True, + }, + f"{SynoCoreUtilization.API_KEY}:network_down": { + ENTITY_NAME: "Network Down", + ENTITY_UNIT: DATA_RATE_KILOBYTES_PER_SECOND, + ENTITY_ICON: "mdi:download", + ENTITY_CLASS: None, + ENTITY_ENABLE: True, + }, } STORAGE_VOL_SENSORS = { - "volume_status": ["Status", None, "mdi:checkbox-marked-circle-outline"], - "volume_device_type": ["Type", None, "mdi:harddisk"], - "volume_size_total": ["Total Size", DATA_TERABYTES, "mdi:chart-pie"], - "volume_size_used": ["Used Space", DATA_TERABYTES, "mdi:chart-pie"], - "volume_percentage_used": ["Volume Used", UNIT_PERCENTAGE, "mdi:chart-pie"], - "volume_disk_temp_avg": ["Average Disk Temp", None, "mdi:thermometer"], - "volume_disk_temp_max": ["Maximum Disk Temp", None, "mdi:thermometer"], + f"{SynoStorage.API_KEY}:volume_status": { + ENTITY_NAME: "Status", + ENTITY_UNIT: None, + ENTITY_ICON: "mdi:checkbox-marked-circle-outline", + ENTITY_CLASS: None, + ENTITY_ENABLE: True, + }, + f"{SynoStorage.API_KEY}:volume_size_total": { + ENTITY_NAME: "Total Size", + ENTITY_UNIT: DATA_TERABYTES, + ENTITY_ICON: "mdi:chart-pie", + ENTITY_CLASS: None, + ENTITY_ENABLE: False, + }, + f"{SynoStorage.API_KEY}:volume_size_used": { + ENTITY_NAME: "Used Space", + ENTITY_UNIT: DATA_TERABYTES, + ENTITY_ICON: "mdi:chart-pie", + ENTITY_CLASS: None, + ENTITY_ENABLE: True, + }, + f"{SynoStorage.API_KEY}:volume_percentage_used": { + ENTITY_NAME: "Volume Used", + ENTITY_UNIT: UNIT_PERCENTAGE, + ENTITY_ICON: "mdi:chart-pie", + ENTITY_CLASS: None, + ENTITY_ENABLE: True, + }, + f"{SynoStorage.API_KEY}:volume_disk_temp_avg": { + ENTITY_NAME: "Average Disk Temp", + ENTITY_UNIT: None, + ENTITY_ICON: "mdi:thermometer", + ENTITY_CLASS: "temperature", + ENTITY_ENABLE: True, + }, + f"{SynoStorage.API_KEY}:volume_disk_temp_max": { + ENTITY_NAME: "Maximum Disk Temp", + ENTITY_UNIT: None, + ENTITY_ICON: "mdi:thermometer", + ENTITY_CLASS: "temperature", + ENTITY_ENABLE: False, + }, } STORAGE_DISK_SENSORS = { - "disk_name": ["Name", None, "mdi:harddisk"], - "disk_device": ["Device", None, "mdi:dots-horizontal"], - "disk_smart_status": ["Status (Smart)", None, "mdi:checkbox-marked-circle-outline"], - "disk_status": ["Status", None, "mdi:checkbox-marked-circle-outline"], - "disk_exceed_bad_sector_thr": ["Exceeded Max Bad Sectors", None, "mdi:test-tube"], - "disk_below_remain_life_thr": ["Below Min Remaining Life", None, "mdi:test-tube"], - "disk_temp": ["Temperature", None, "mdi:thermometer"], + f"{SynoStorage.API_KEY}:disk_smart_status": { + ENTITY_NAME: "Status (Smart)", + ENTITY_UNIT: None, + ENTITY_ICON: "mdi:checkbox-marked-circle-outline", + ENTITY_CLASS: None, + ENTITY_ENABLE: False, + }, + f"{SynoStorage.API_KEY}:disk_status": { + ENTITY_NAME: "Status", + ENTITY_UNIT: None, + ENTITY_ICON: "mdi:checkbox-marked-circle-outline", + ENTITY_CLASS: None, + ENTITY_ENABLE: True, + }, + f"{SynoStorage.API_KEY}:disk_temp": { + ENTITY_NAME: "Temperature", + ENTITY_UNIT: None, + ENTITY_ICON: "mdi:thermometer", + ENTITY_CLASS: "temperature", + ENTITY_ENABLE: True, + }, } diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index 81873cad4cd..22171fdf2f5 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -1,9 +1,6 @@ -"""Support for Synology DSM Sensors.""" -from typing import Dict - +"""Support for Synology DSM sensors.""" from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_DISKS, DATA_MEGABYTES, DATA_RATE_KILOBYTES_PER_SECOND, @@ -11,14 +8,11 @@ from homeassistant.const import ( PRECISION_TENTHS, TEMP_CELSIUS, ) -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity from homeassistant.helpers.temperature import display_temp from homeassistant.helpers.typing import HomeAssistantType -from . import SynoApi +from . import SynologyDSMDeviceEntity, SynologyDSMEntity from .const import ( - BASE_NAME, CONF_VOLUMES, DOMAIN, STORAGE_DISK_SENSORS, @@ -28,8 +22,6 @@ from .const import ( UTILISATION_SENSORS, ) -ATTRIBUTION = "Data provided by Synology" - async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities @@ -38,16 +30,16 @@ async def async_setup_entry( api = hass.data[DOMAIN][entry.unique_id][SYNO_API] - sensors = [ - SynoNasUtilSensor(api, sensor_type, UTILISATION_SENSORS[sensor_type]) + entities = [ + SynoDSMUtilSensor(api, sensor_type, UTILISATION_SENSORS[sensor_type]) for sensor_type in UTILISATION_SENSORS ] # Handle all volumes if api.storage.volumes_ids: for volume in entry.data.get(CONF_VOLUMES, api.storage.volumes_ids): - sensors += [ - SynoNasStorageSensor( + entities += [ + SynoDSMStorageSensor( api, sensor_type, STORAGE_VOL_SENSORS[sensor_type], volume ) for sensor_type in STORAGE_VOL_SENSORS @@ -56,106 +48,23 @@ async def async_setup_entry( # Handle all disks if api.storage.disks_ids: for disk in entry.data.get(CONF_DISKS, api.storage.disks_ids): - sensors += [ - SynoNasStorageSensor( + entities += [ + SynoDSMStorageSensor( api, sensor_type, STORAGE_DISK_SENSORS[sensor_type], disk ) for sensor_type in STORAGE_DISK_SENSORS ] - async_add_entities(sensors) + async_add_entities(entities) -class SynoNasSensor(Entity): - """Representation of a Synology NAS sensor.""" - - def __init__( - self, - api: SynoApi, - sensor_type: str, - sensor_info: Dict[str, str], - monitored_device: str = None, - ): - """Initialize the sensor.""" - self._api = api - self.sensor_type = sensor_type - self._name = f"{BASE_NAME} {sensor_info[0]}" - self._unit = sensor_info[1] - self._icon = sensor_info[2] - self.monitored_device = monitored_device - self._unique_id = f"{self._api.information.serial}_{sensor_info[0]}" - - if self.monitored_device: - self._name += f" ({self.monitored_device})" - self._unique_id += f"_{self.monitored_device}" - - self._unsub_dispatcher = None - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return self._unique_id - - @property - def name(self) -> str: - """Return the name.""" - return self._name - - @property - def icon(self) -> str: - """Return the icon.""" - return self._icon - - @property - def unit_of_measurement(self) -> str: - """Return the unit the value is expressed in.""" - if self.sensor_type in TEMP_SENSORS_KEYS: - return self.hass.config.units.temperature_unit - return self._unit - - @property - def device_state_attributes(self) -> Dict[str, any]: - """Return the state attributes.""" - return {ATTR_ATTRIBUTION: ATTRIBUTION} - - @property - def device_info(self) -> Dict[str, any]: - """Return the device information.""" - return { - "identifiers": {(DOMAIN, self._api.information.serial)}, - "name": "Synology NAS", - "manufacturer": "Synology", - "model": self._api.information.model, - "sw_version": self._api.information.version_string, - } - - @property - def should_poll(self) -> bool: - """No polling needed.""" - return False - - async def async_update(self): - """Only used by the generic entity update service.""" - await self._api.update() - - async def async_added_to_hass(self): - """Register state update callback.""" - self._unsub_dispatcher = async_dispatcher_connect( - self.hass, self._api.signal_sensor_update, self.async_write_ha_state - ) - - async def async_will_remove_from_hass(self): - """Clean up after entity before removal.""" - self._unsub_dispatcher() - - -class SynoNasUtilSensor(SynoNasSensor): +class SynoDSMUtilSensor(SynologyDSMEntity): """Representation a Synology Utilisation sensor.""" @property def state(self): """Return the state.""" - attr = getattr(self._api.utilisation, self.sensor_type) + attr = getattr(self._api.utilisation, self.entity_type) if callable(attr): attr = attr() if attr is None: @@ -171,14 +80,19 @@ class SynoNasUtilSensor(SynoNasSensor): return attr + @property + def available(self) -> bool: + """Return True if entity is available.""" + return bool(self._api.utilisation) -class SynoNasStorageSensor(SynoNasSensor): + +class SynoDSMStorageSensor(SynologyDSMDeviceEntity): """Representation a Synology Storage sensor.""" @property def state(self): """Return the state.""" - attr = getattr(self._api.storage, self.sensor_type)(self.monitored_device) + attr = getattr(self._api.storage, self.entity_type)(self._device_id) if attr is None: return None @@ -187,21 +101,7 @@ class SynoNasStorageSensor(SynoNasSensor): return round(attr / 1024.0 ** 4, 2) # Temperature - if self.sensor_type in TEMP_SENSORS_KEYS: + if self.entity_type in TEMP_SENSORS_KEYS: return display_temp(self.hass, attr, TEMP_CELSIUS, PRECISION_TENTHS) return attr - - @property - def device_info(self) -> Dict[str, any]: - """Return the device information.""" - return { - "identifiers": { - (DOMAIN, self._api.information.serial, self.monitored_device) - }, - "name": f"Synology NAS ({self.monitored_device})", - "manufacturer": "Synology", - "model": self._api.information.model, - "sw_version": self._api.information.version_string, - "via_device": (DOMAIN, self._api.information.serial), - } diff --git a/homeassistant/components/synology_dsm/strings.json b/homeassistant/components/synology_dsm/strings.json index 0024a7db612..c46f645719f 100644 --- a/homeassistant/components/synology_dsm/strings.json +++ b/homeassistant/components/synology_dsm/strings.json @@ -49,4 +49,4 @@ } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/synology_dsm/translations/ca.json b/homeassistant/components/synology_dsm/translations/ca.json index 26f5d0c76b7..fef0bca2ce4 100644 --- a/homeassistant/components/synology_dsm/translations/ca.json +++ b/homeassistant/components/synology_dsm/translations/ca.json @@ -20,9 +20,8 @@ }, "link": { "data": { - "api_version": "Versi\u00f3 DSM", "password": "Contrasenya", - "port": "Port (opcional)", + "port": "Port", "ssl": "Utilitza SSL/TLS per connectar-te al servidor NAS", "username": "Nom d'usuari" }, @@ -31,7 +30,6 @@ }, "user": { "data": { - "api_version": "Versi\u00f3 DSM", "host": "Amfitri\u00f3", "password": "Contrasenya", "port": "Port", diff --git a/homeassistant/components/synology_dsm/translations/de.json b/homeassistant/components/synology_dsm/translations/de.json index 54ee7a9b623..b5e7064066c 100644 --- a/homeassistant/components/synology_dsm/translations/de.json +++ b/homeassistant/components/synology_dsm/translations/de.json @@ -20,7 +20,6 @@ }, "link": { "data": { - "api_version": "DSM-Version", "password": "Passwort", "port": "Port (optional)", "ssl": "Verwenden Sie SSL/TLS, um eine Verbindung zu Ihrem NAS herzustellen", @@ -31,7 +30,6 @@ }, "user": { "data": { - "api_version": "DSM-Version", "host": "Host", "password": "Passwort", "port": "Port (optional)", diff --git a/homeassistant/components/synology_dsm/translations/en.json b/homeassistant/components/synology_dsm/translations/en.json index ed3dae27151..48a8118528a 100644 --- a/homeassistant/components/synology_dsm/translations/en.json +++ b/homeassistant/components/synology_dsm/translations/en.json @@ -20,7 +20,6 @@ }, "link": { "data": { - "api_version": "DSM version", "password": "Password", "port": "Port", "ssl": "Use SSL/TLS to connect to your NAS", @@ -31,7 +30,6 @@ }, "user": { "data": { - "api_version": "DSM version", "host": "Host", "password": "Password", "port": "Port", diff --git a/homeassistant/components/synology_dsm/translations/es-419.json b/homeassistant/components/synology_dsm/translations/es-419.json index cad627e0dfb..a7c83782df5 100644 --- a/homeassistant/components/synology_dsm/translations/es-419.json +++ b/homeassistant/components/synology_dsm/translations/es-419.json @@ -20,7 +20,6 @@ }, "link": { "data": { - "api_version": "Versi\u00f3n DSM", "password": "Contrase\u00f1a", "port": "Puerto (opcional)", "ssl": "Utilice SSL/TLS para conectarse a su NAS", @@ -31,7 +30,6 @@ }, "user": { "data": { - "api_version": "Versi\u00f3n DSM", "host": "Host", "password": "Contrase\u00f1a", "port": "Puerto (opcional)", diff --git a/homeassistant/components/synology_dsm/translations/es.json b/homeassistant/components/synology_dsm/translations/es.json index 6efe92020d2..390f67d4667 100644 --- a/homeassistant/components/synology_dsm/translations/es.json +++ b/homeassistant/components/synology_dsm/translations/es.json @@ -20,7 +20,6 @@ }, "link": { "data": { - "api_version": "Versi\u00f3n del DSM", "password": "Contrase\u00f1a", "port": "Puerto (opcional)", "ssl": "Usar SSL/TLS para conectar con tu NAS", @@ -31,7 +30,6 @@ }, "user": { "data": { - "api_version": "Versi\u00f3n del DSM", "host": "Host", "password": "Contrase\u00f1a", "port": "Puerto (opcional)", diff --git a/homeassistant/components/synology_dsm/translations/fr.json b/homeassistant/components/synology_dsm/translations/fr.json index 4859b10073c..bdb10425424 100644 --- a/homeassistant/components/synology_dsm/translations/fr.json +++ b/homeassistant/components/synology_dsm/translations/fr.json @@ -20,7 +20,6 @@ }, "link": { "data": { - "api_version": "Version du DSM", "password": "Mot de passe", "port": "Port (facultatif)", "ssl": "Utilisez SSL/TLS pour vous connecter \u00e0 votre NAS", @@ -31,7 +30,6 @@ }, "user": { "data": { - "api_version": "Version du DSM", "host": "H\u00f4te", "password": "Mot de passe", "port": "Port (facultatif)", diff --git a/homeassistant/components/synology_dsm/translations/hu.json b/homeassistant/components/synology_dsm/translations/hu.json index 0bb810d66e2..23cb168b9a5 100644 --- a/homeassistant/components/synology_dsm/translations/hu.json +++ b/homeassistant/components/synology_dsm/translations/hu.json @@ -3,11 +3,16 @@ "step": { "link": { "data": { + "password": "Jelsz\u00f3", + "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" } }, "user": { "data": { + "host": "Hoszt", + "password": "Jelsz\u00f3", + "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" } } diff --git a/homeassistant/components/synology_dsm/translations/it.json b/homeassistant/components/synology_dsm/translations/it.json index 3bdd7ab0faf..1bb6dc026d8 100644 --- a/homeassistant/components/synology_dsm/translations/it.json +++ b/homeassistant/components/synology_dsm/translations/it.json @@ -20,9 +20,8 @@ }, "link": { "data": { - "api_version": "Versione DSM", "password": "Password", - "port": "Porta (opzionale)", + "port": "Porta", "ssl": "Utilizzare SSL/TLS per connettersi al NAS", "username": "Nome utente" }, @@ -31,10 +30,9 @@ }, "user": { "data": { - "api_version": "Versione DSM", "host": "Host", "password": "Password", - "port": "Porta (opzionale)", + "port": "Porta", "ssl": "Utilizzare SSL/TLS per connettersi al NAS", "username": "Nome utente" }, diff --git a/homeassistant/components/synology_dsm/translations/ko.json b/homeassistant/components/synology_dsm/translations/ko.json index 252c68f04ad..81b7d5f1435 100644 --- a/homeassistant/components/synology_dsm/translations/ko.json +++ b/homeassistant/components/synology_dsm/translations/ko.json @@ -10,7 +10,7 @@ "otp_failed": "2\ub2e8\uacc4 \uc778\uc99d\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4. \uc0c8\ub85c\uc6b4 \ud328\uc2a4 \ucf54\ub4dc\ub85c \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694", "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uc785\ub2c8\ub2e4. \uc790\uc138\ud55c \uc815\ubcf4\ub294 \ub85c\uadf8\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694" }, - "flow_title": "Synology DSM {name} ({host})", + "flow_title": "Synology DSM: {name} ({host})", "step": { "2sa": { "data": { @@ -20,7 +20,6 @@ }, "link": { "data": { - "api_version": "DSM \ubc84\uc804", "password": "\ube44\ubc00\ubc88\ud638", "port": "\ud3ec\ud2b8", "ssl": "SSL/TLS \ub97c \uc0ac\uc6a9\ud558\uc5ec NAS \uc5d0 \uc5f0\uacb0", @@ -31,7 +30,6 @@ }, "user": { "data": { - "api_version": "DSM \ubc84\uc804", "host": "\ud638\uc2a4\ud2b8", "password": "\ube44\ubc00\ubc88\ud638", "port": "\ud3ec\ud2b8", diff --git a/homeassistant/components/synology_dsm/translations/lb.json b/homeassistant/components/synology_dsm/translations/lb.json index 42bbc08b6cf..0e3d8300248 100644 --- a/homeassistant/components/synology_dsm/translations/lb.json +++ b/homeassistant/components/synology_dsm/translations/lb.json @@ -20,7 +20,6 @@ }, "link": { "data": { - "api_version": "DSM Versioun", "password": "Passwuert", "port": "Port (Optionell)", "ssl": "Benotz SSL/TLS fir d'Verbindung mam NAS", @@ -31,7 +30,6 @@ }, "user": { "data": { - "api_version": "DSM Versioun", "host": "Apparat", "password": "Passwuert", "port": "Port (Optionell)", diff --git a/homeassistant/components/synology_dsm/translations/nl.json b/homeassistant/components/synology_dsm/translations/nl.json index 8b69639140b..5798dce567d 100644 --- a/homeassistant/components/synology_dsm/translations/nl.json +++ b/homeassistant/components/synology_dsm/translations/nl.json @@ -20,7 +20,6 @@ }, "link": { "data": { - "api_version": "DSM-versie", "password": "Wachtwoord", "port": "Poort (optioneel)", "ssl": "Gebruik SSL/TLS om verbinding te maken met uw NAS", @@ -31,7 +30,6 @@ }, "user": { "data": { - "api_version": "DSM-versie", "host": "Host", "password": "Wachtwoord", "port": "Poort (optioneel)", diff --git a/homeassistant/components/synology_dsm/translations/no.json b/homeassistant/components/synology_dsm/translations/no.json index 40d7907d61d..678484d5226 100644 --- a/homeassistant/components/synology_dsm/translations/no.json +++ b/homeassistant/components/synology_dsm/translations/no.json @@ -20,7 +20,6 @@ }, "link": { "data": { - "api_version": "DSM-versjon", "password": "Passord", "port": "Port (valgfritt)", "ssl": "Bruk SSL/TLS til \u00e5 koble til NAS-en", @@ -31,7 +30,6 @@ }, "user": { "data": { - "api_version": "DSM-versjon", "host": "Vert", "password": "Passord", "port": "Port (valgfritt)", diff --git a/homeassistant/components/synology_dsm/translations/pl.json b/homeassistant/components/synology_dsm/translations/pl.json index 33644773220..60c7ee849f1 100644 --- a/homeassistant/components/synology_dsm/translations/pl.json +++ b/homeassistant/components/synology_dsm/translations/pl.json @@ -20,23 +20,21 @@ }, "link": { "data": { - "api_version": "Wersja DSM", - "password": "[%key_id:common::config_flow::data::password%]", - "port": "[%key_id:common::config_flow::data::port%] (opcjonalnie)", + "password": "Has\u0142o", + "port": "Port", "ssl": "U\u017cyj SSL/TLS, aby po\u0142\u0105czy\u0107 si\u0119 z serwerem NAS", - "username": "[%key_id:common::config_flow::data::username%]" + "username": "Nazwa u\u017cytkownika" }, "description": "Czy chcesz skonfigurowa\u0107 {name} ({host})?", "title": "Synology DSM" }, "user": { "data": { - "api_version": "Wersja DSM", - "host": "[%key_id:common::config_flow::data::host%]", - "password": "[%key_id:common::config_flow::data::password%]", - "port": "[%key_id:common::config_flow::data::port%] (opcjonalnie)", + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o", + "port": "Port", "ssl": "U\u017cyj SSL/TLS, aby po\u0142\u0105czy\u0107 si\u0119 z serwerem NAS", - "username": "[%key_id:common::config_flow::data::username%]" + "username": "Nazwa u\u017cytkownika" }, "title": "Synology DSM" } diff --git a/homeassistant/components/synology_dsm/translations/pt-BR.json b/homeassistant/components/synology_dsm/translations/pt-BR.json new file mode 100644 index 00000000000..e633eb9128d --- /dev/null +++ b/homeassistant/components/synology_dsm/translations/pt-BR.json @@ -0,0 +1,32 @@ +{ + "config": { + "error": { + "otp_failed": "Falha na autentica\u00e7\u00e3o em duas etapas, tente novamente com um novo c\u00f3digo", + "unknown": "Erro desconhecido: verifique os logs para obter mais detalhes" + }, + "flow_title": "Synology DSM {name} ({host})", + "step": { + "2sa": { + "data": { + "otp_code": "C\u00f3digo" + } + }, + "link": { + "data": { + "ssl": "Use SSL/TLS para conectar-se ao seu NAS" + }, + "description": "Voc\u00ea quer configurar o {name} ({host})?", + "title": "Synology DSM" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Minutos entre os escaneamentos" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/ru.json b/homeassistant/components/synology_dsm/translations/ru.json index 9d1ca6ca39f..c0e5da7a1f1 100644 --- a/homeassistant/components/synology_dsm/translations/ru.json +++ b/homeassistant/components/synology_dsm/translations/ru.json @@ -20,7 +20,6 @@ }, "link": { "data": { - "api_version": "\u0412\u0435\u0440\u0441\u0438\u044f DSM", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442", "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c SSL / TLS \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", @@ -31,7 +30,6 @@ }, "user": { "data": { - "api_version": "\u0412\u0435\u0440\u0441\u0438\u044f DSM", "host": "\u0425\u043e\u0441\u0442", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442", diff --git a/homeassistant/components/synology_dsm/translations/sl.json b/homeassistant/components/synology_dsm/translations/sl.json index 1ad2e70a148..8f58af96960 100644 --- a/homeassistant/components/synology_dsm/translations/sl.json +++ b/homeassistant/components/synology_dsm/translations/sl.json @@ -20,7 +20,6 @@ }, "link": { "data": { - "api_version": "Razli\u010dica DSM", "password": "Geslo", "port": "Vrata (Izbirno)", "ssl": "Uporabite SSL/TLS za povezavo z va\u0161im NAS-om", @@ -31,7 +30,6 @@ }, "user": { "data": { - "api_version": "Razli\u010dica DSM", "host": "Gostitelj", "password": "Geslo", "port": "Vrata (Izbirno)", diff --git a/homeassistant/components/synology_dsm/translations/zh-Hant.json b/homeassistant/components/synology_dsm/translations/zh-Hant.json index 07d469bedd2..eec37c812f8 100644 --- a/homeassistant/components/synology_dsm/translations/zh-Hant.json +++ b/homeassistant/components/synology_dsm/translations/zh-Hant.json @@ -20,7 +20,6 @@ }, "link": { "data": { - "api_version": "DSM \u7248\u672c", "password": "\u5bc6\u78bc", "port": "\u901a\u8a0a\u57e0", "ssl": "\u4f7f\u7528 SSL/TLS \u9023\u7dda\u81f3 NAS", @@ -31,7 +30,6 @@ }, "user": { "data": { - "api_version": "DSM \u7248\u672c", "host": "\u4e3b\u6a5f\u7aef", "password": "\u5bc6\u78bc", "port": "\u901a\u8a0a\u57e0", diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index ceca0e0170e..231dc419b6b 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -222,7 +222,10 @@ class TadoZoneSensor(TadoZoneEntity, Entity): self._state = self._tado_zone_data.preparation elif self.zone_variable == "open window": - self._state = self._tado_zone_data.open_window + self._state = bool( + self._tado_zone_data.open_window + or self._tado_zone_data.open_window_detected + ) self._state_attributes = self._tado_zone_data.open_window_attr diff --git a/homeassistant/components/tado/translations/hu.json b/homeassistant/components/tado/translations/hu.json new file mode 100644 index 00000000000..dee4ed9ee0f --- /dev/null +++ b/homeassistant/components/tado/translations/hu.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tado/translations/pl.json b/homeassistant/components/tado/translations/pl.json index afe884ced51..f95374e0329 100644 --- a/homeassistant/components/tado/translations/pl.json +++ b/homeassistant/components/tado/translations/pl.json @@ -1,21 +1,21 @@ { "config": { "abort": { - "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "[%key_id:common::config_flow::error::invalid_auth%]", + "invalid_auth": "Niepoprawne uwierzytelnienie.", "no_homes": "Brak dom\u00f3w powi\u0105zanych z tym kontem Tado.", - "unknown": "[%key_id:common::config_flow::error::unknown%]" + "unknown": "Nieoczekiwany b\u0142\u0105d." }, "step": { "user": { "data": { - "password": "[%key_id:common::config_flow::data::password%]", - "username": "[%key_id:common::config_flow::data::username%]" + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" }, - "title": "Po\u0142\u0105cz z kontem Tado" + "title": "Po\u0142\u0105czenie z kontem Tado" } } }, diff --git a/homeassistant/components/tado/translations/pt-BR.json b/homeassistant/components/tado/translations/pt-BR.json new file mode 100644 index 00000000000..af32cb3c3a6 --- /dev/null +++ b/homeassistant/components/tado/translations/pt-BR.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar, tente novamente", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "no_homes": "N\u00e3o h\u00e1 casas vinculadas a esta conta Tado.", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "title": "Conecte-se \u00e0 sua conta Tado" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "fallback": "Ative o modo de fallback." + }, + "title": "Ajuste as op\u00e7\u00f5es do Tado." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tahoma/__init__.py b/homeassistant/components/tahoma/__init__.py index 1e1bbd31eae..1a6b326f4e5 100644 --- a/homeassistant/components/tahoma/__init__.py +++ b/homeassistant/components/tahoma/__init__.py @@ -62,9 +62,12 @@ TAHOMA_TYPES = { "rts:DualCurtainRTSComponent": "cover", "rts:ExteriorVenetianBlindRTSComponent": "cover", "rts:GarageDoor4TRTSComponent": "switch", + "rts:LightRTSComponent": "switch", "rts:RollerShutterRTSComponent": "cover", "rts:OnOffRTSComponent": "switch", "rts:VenetianBlindRTSComponent": "cover", + "somfythermostat:SomfyThermostatTemperatureSensor": "sensor", + "somfythermostat:SomfyThermostatHumiditySensor": "sensor", } diff --git a/homeassistant/components/tahoma/sensor.py b/homeassistant/components/tahoma/sensor.py index 20364a243b3..8ceb07e1a9f 100644 --- a/homeassistant/components/tahoma/sensor.py +++ b/homeassistant/components/tahoma/sensor.py @@ -56,6 +56,13 @@ class TahomaSensor(TahomaDevice, Entity): return None if self.tahoma_device.type == "rtds:RTDSMotionSensor": return None + if ( + self.tahoma_device.type + == "somfythermostat:SomfyThermostatTemperatureSensor" + ): + return TEMP_CELSIUS + if self.tahoma_device.type == "somfythermostat:SomfyThermostatHumiditySensor": + return UNIT_PERCENTAGE def update(self): """Update the state.""" @@ -86,6 +93,19 @@ class TahomaSensor(TahomaDevice, Entity): float(self.tahoma_device.active_states["core:TemperatureState"]), 1 ) self._available = True + if ( + self.tahoma_device.type + == "somfythermostat:SomfyThermostatTemperatureSensor" + ): + self.current_value = float( + f"{self.tahoma_device.active_states['core:TemperatureState']:.2f}" + ) + self._available = True + if self.tahoma_device.type == "somfythermostat:SomfyThermostatHumiditySensor": + self.current_value = float( + f"{self.tahoma_device.active_states['core:RelativeHumidityState']:.2f}" + ) + self._available = True _LOGGER.debug("Update %s, value: %d", self._name, self.current_value) diff --git a/homeassistant/components/tellduslive/translations/ca.json b/homeassistant/components/tellduslive/translations/ca.json index c729dea0dd0..88e4dfbbdae 100644 --- a/homeassistant/components/tellduslive/translations/ca.json +++ b/homeassistant/components/tellduslive/translations/ca.json @@ -18,7 +18,7 @@ "data": { "host": "Amfitri\u00f3" }, - "description": "buit", + "description": "Buit", "title": "Selecci\u00f3 de l'endpoint" } } diff --git a/homeassistant/components/tellduslive/translations/fi.json b/homeassistant/components/tellduslive/translations/fi.json new file mode 100644 index 00000000000..95b8fe62dbb --- /dev/null +++ b/homeassistant/components/tellduslive/translations/fi.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Valitse p\u00e4\u00e4tepiste." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/translations/it.json b/homeassistant/components/tellduslive/translations/it.json index 7776c23dbaa..177c22e5813 100644 --- a/homeassistant/components/tellduslive/translations/it.json +++ b/homeassistant/components/tellduslive/translations/it.json @@ -11,7 +11,7 @@ }, "step": { "auth": { - "description": "Per collegare il tuo account TelldusLive:\n 1. Clicca sul link sottostante\n 2. Accedi a Telldus Live\n 3. Autorizzare **{app_name}**** (cliccare **S\u00ec**).\n 4. Torna qui e clicca su **SUBMIT**.\n\n [Collega account TelldusLive]({auth_url})", + "description": "Per collegare il tuo account TelldusLive:\n 1. Clicca sul link sottostante\n 2. Accedi a Telldus Live\n 3. Autorizzare **{app_name}** (cliccare **S\u00ec**).\n 4. Torna qui e clicca su **SUBMIT**.\n\n [Link per account TelldusLive]({auth_url})", "title": "Autenticati con TelldusLive" }, "user": { diff --git a/homeassistant/components/tellduslive/translations/ko.json b/homeassistant/components/tellduslive/translations/ko.json index fa3cb937baf..3430b256dda 100644 --- a/homeassistant/components/tellduslive/translations/ko.json +++ b/homeassistant/components/tellduslive/translations/ko.json @@ -11,7 +11,7 @@ }, "step": { "auth": { - "description": "TelldusLive \uacc4\uc815\uc744 \uc5f0\uacb0\ud558\ub824\uba74:\n 1. \ud558\ub2e8\uc758 \ub9c1\ud06c\ub97c \ud074\ub9ad\ud574\uc8fc\uc138\uc694\n 2. Telldus Live \uc5d0 \ub85c\uadf8\uc778 \ud558\uc138\uc694\n 3. Authorize **{app_name}** (**Yes** \ub97c \ud074\ub9ad\ud558\uc138\uc694).\n 4. \ub2e4\uc2dc \uc5ec\uae30\ub85c \ub3cc\uc544\uc640\uc11c **SUBMIT** \uc744 \ud074\ub9ad\ud558\uc138\uc694.\n\n [TelldusLive \uacc4\uc815 \uc5f0\uacb0\ud558\uae30]({auth_url})", + "description": "TelldusLive \uacc4\uc815\uc744 \uc5f0\uacb0\ud558\ub824\uba74:\n 1. \ud558\ub2e8\uc758 \ub9c1\ud06c\ub97c \ud074\ub9ad\ud574\uc8fc\uc138\uc694\n 2. Telldus Live \uc5d0 \ub85c\uadf8\uc778 \ud558\uc138\uc694\n 3. Authorize **{app_name}** (**Yes** \ub97c \ud074\ub9ad\ud558\uc138\uc694).\n 4. \ub2e4\uc2dc \uc5ec\uae30\ub85c \ub3cc\uc544\uc640\uc11c **\ud655\uc778**\uc744 \ud074\ub9ad\ud558\uc138\uc694.\n\n [TelldusLive \uacc4\uc815 \uc5f0\uacb0\ud558\uae30]({auth_url})", "title": "TelldusLive \uc778\uc99d\ud558\uae30" }, "user": { diff --git a/homeassistant/components/tellduslive/translations/pl.json b/homeassistant/components/tellduslive/translations/pl.json index acf9df68b03..49118f70dd8 100644 --- a/homeassistant/components/tellduslive/translations/pl.json +++ b/homeassistant/components/tellduslive/translations/pl.json @@ -3,8 +3,8 @@ "abort": { "already_setup": "TelldusLive jest ju\u017c skonfigurowany.", "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji.", - "authorize_url_timeout": "[%key_id:common::config_flow::abort::oauth2_authorize_url_timeout%]", - "unknown": "[%key_id:common::config_flow::error::unknown%]" + "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", + "unknown": "Nieoczekiwany b\u0142\u0105d." }, "error": { "auth_error": "B\u0142\u0105d uwierzytelniania, spr\u00f3buj ponownie" @@ -16,7 +16,7 @@ }, "user": { "data": { - "host": "[%key_id:common::config_flow::data::host%]" + "host": "Nazwa hosta lub adres IP" }, "description": "Puste", "title": "Wybierz punkt ko\u0144cowy." diff --git a/homeassistant/components/tesla/manifest.json b/homeassistant/components/tesla/manifest.json index 3d21fbae960..39aa00cfb60 100644 --- a/homeassistant/components/tesla/manifest.json +++ b/homeassistant/components/tesla/manifest.json @@ -3,6 +3,6 @@ "name": "Tesla", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tesla", - "requirements": ["teslajsonpy==0.8.0"], + "requirements": ["teslajsonpy==0.8.1"], "codeowners": ["@zabuldon", "@alandtse"] } diff --git a/homeassistant/components/tesla/translations/hu.json b/homeassistant/components/tesla/translations/hu.json index 0b9dbbf06a7..1e8efeec1a1 100644 --- a/homeassistant/components/tesla/translations/hu.json +++ b/homeassistant/components/tesla/translations/hu.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "Jelsz\u00f3", - "username": "Email c\u00edm" + "username": "E-mail" }, "description": "K\u00e9rlek, add meg az adataidat.", "title": "Tesla - Konfigur\u00e1ci\u00f3" diff --git a/homeassistant/components/tesla/translations/it.json b/homeassistant/components/tesla/translations/it.json index 3a77ce669eb..0c4ae68fe0f 100644 --- a/homeassistant/components/tesla/translations/it.json +++ b/homeassistant/components/tesla/translations/it.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "Password", - "username": "Indirizzo E-Mail" + "username": "E-mail" }, "description": "Si prega di inserire le tue informazioni.", "title": "Tesla - Configurazione" diff --git a/homeassistant/components/tesla/translations/pl.json b/homeassistant/components/tesla/translations/pl.json index 9054268f4a5..e2a6b6a09c8 100644 --- a/homeassistant/components/tesla/translations/pl.json +++ b/homeassistant/components/tesla/translations/pl.json @@ -9,8 +9,8 @@ "step": { "user": { "data": { - "password": "[%key_id:common::config_flow::data::password%]", - "username": "[%key_id:common::config_flow::data::email%]" + "password": "Has\u0142o", + "username": "Adres e-mail" }, "description": "Wprowad\u017a dane", "title": "Tesla - konfiguracja" diff --git a/homeassistant/components/tesla/translations/pt-BR.json b/homeassistant/components/tesla/translations/pt-BR.json index c06b4fabf99..7c0a910ca9a 100644 --- a/homeassistant/components/tesla/translations/pt-BR.json +++ b/homeassistant/components/tesla/translations/pt-BR.json @@ -16,5 +16,14 @@ "title": "Tesla - Configura\u00e7\u00e3o" } } + }, + "options": { + "step": { + "init": { + "data": { + "enable_wake_on_start": "For\u00e7ar carros a acordar na inicializa\u00e7\u00e3o" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 7fc8820e92d..8dd5c507d7d 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -18,6 +18,7 @@ ICON = "mdi:currency-usd" ICON_RT = "mdi:power-plug" SCAN_INTERVAL = timedelta(minutes=1) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) +PARALLEL_UPDATES = 0 async def async_setup_entry(hass, entry, async_add_entities): diff --git a/homeassistant/components/tibber/translations/hu.json b/homeassistant/components/tibber/translations/hu.json new file mode 100644 index 00000000000..0b0581d4923 --- /dev/null +++ b/homeassistant/components/tibber/translations/hu.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_access_token": "\u00c9rv\u00e9nytelen hozz\u00e1f\u00e9r\u00e9si token" + }, + "step": { + "user": { + "data": { + "access_token": "Hozz\u00e1f\u00e9r\u00e9si token" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tibber/translations/ko.json b/homeassistant/components/tibber/translations/ko.json index 92b777e35bb..6229d2fd138 100644 --- a/homeassistant/components/tibber/translations/ko.json +++ b/homeassistant/components/tibber/translations/ko.json @@ -13,7 +13,7 @@ "data": { "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070" }, - "description": "https://developer.tibber.com/settings/accesstoken \uc5d0\uc11c \uc561\uc138\uc2a4 \ud1a0\ud070\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694", + "description": "https://developer.tibber.com/settings/accesstoken \uc5d0\uc11c \uc0dd\uc131\ud55c \uc561\uc138\uc2a4 \ud1a0\ud070\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694", "title": "Tibber" } } diff --git a/homeassistant/components/tibber/translations/pl.json b/homeassistant/components/tibber/translations/pl.json index 9e6417c33d7..8ef96358301 100644 --- a/homeassistant/components/tibber/translations/pl.json +++ b/homeassistant/components/tibber/translations/pl.json @@ -1,17 +1,17 @@ { "config": { "abort": { - "already_configured": "[%key_id:common::config_flow::abort::already_configured_account%]" + "already_configured": "Konto jest ju\u017c skonfigurowane." }, "error": { "connection_error": "B\u0142\u0105d po\u0142\u0105czenia z Tibber.", - "invalid_access_token": "[%key_id:common::config_flow::error::invalid_access_token%]", + "invalid_access_token": "Niepoprawny token dost\u0119pu.", "timeout": "Przekroczono limit czasu \u0142\u0105czenia z Tibber." }, "step": { "user": { "data": { - "access_token": "[%key_id:common::config_flow::data::access_token%]" + "access_token": "Token dost\u0119pu" }, "description": "Wprowad\u017a token dost\u0119pu z https://developer.tibber.com/settings/accesstoken", "title": "Tibber" diff --git a/homeassistant/components/timer/translations/ca.json b/homeassistant/components/timer/translations/ca.json index b5e9555940d..17e8463080a 100644 --- a/homeassistant/components/timer/translations/ca.json +++ b/homeassistant/components/timer/translations/ca.json @@ -2,7 +2,7 @@ "state": { "_": { "active": "Actiu", - "idle": "inactiu", + "idle": "Inactiu", "paused": "Pausat" } } diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index 0ce8101f49c..014dbd37a4e 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -9,7 +9,7 @@ from homeassistant.components.calendar import PLATFORM_SCHEMA, CalendarEventDevi from homeassistant.const import CONF_ID, CONF_NAME, CONF_TOKEN import homeassistant.helpers.config_validation as cv from homeassistant.helpers.template import DATE_STR_FORMAT -from homeassistant.util import Throttle, dt +from homeassistant.util import dt from .const import ( ALL_DAY, @@ -84,7 +84,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) +SCAN_INTERVAL = timedelta(minutes=15) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -302,8 +302,7 @@ class TodoistProjectData: platform itself, but are not used by this component at all. The 'update' method polls the Todoist API for new projects/tasks, as well - as any updates to current projects/tasks. This is throttled to every - MIN_TIME_BETWEEN_UPDATES minutes. + as any updates to current projects/tasks. This occurs every SCAN_INTERVAL minutes. """ def __init__( @@ -514,7 +513,6 @@ class TodoistProjectData: events.append(event) return events - @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data.""" if self._id is None: diff --git a/homeassistant/components/toon/__init__.py b/homeassistant/components/toon/__init__.py index 595d3cc1ede..b970ed2221b 100644 --- a/homeassistant/components/toon/__init__.py +++ b/homeassistant/components/toon/__init__.py @@ -6,7 +6,13 @@ from typing import Any, Dict from toonapilib import Toon import voluptuous as vol -from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_PASSWORD, + CONF_SCAN_INTERVAL, + CONF_USERNAME, +) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send @@ -16,8 +22,6 @@ from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import config_flow # noqa: F401 from .const import ( - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, CONF_DISPLAY, CONF_TENANT, DATA_TOON, diff --git a/homeassistant/components/toon/config_flow.py b/homeassistant/components/toon/config_flow.py index c8b4b537853..b584b7bd6cb 100644 --- a/homeassistant/components/toon/config_flow.py +++ b/homeassistant/components/toon/config_flow.py @@ -13,17 +13,15 @@ from toonapilib.toonapilibexceptions import ( import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import callback - -from .const import ( +from homeassistant.const import ( CONF_CLIENT_ID, CONF_CLIENT_SECRET, - CONF_DISPLAY, - CONF_TENANT, - DATA_TOON_CONFIG, - DOMAIN, + CONF_PASSWORD, + CONF_USERNAME, ) +from homeassistant.core import callback + +from .const import CONF_DISPLAY, CONF_TENANT, DATA_TOON_CONFIG, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -94,10 +92,10 @@ class ToonFlowHandler(config_entries.ConfigFlow): displays = toon.display_names except InvalidConsumerKey: - return self.async_abort(reason="client_id") + return self.async_abort(reason=CONF_CLIENT_ID) except InvalidConsumerSecret: - return self.async_abort(reason="client_secret") + return self.async_abort(reason=CONF_CLIENT_SECRET) except InvalidCredentials: return await self._show_authenticaticate_form({"base": "credentials"}) diff --git a/homeassistant/components/toon/const.py b/homeassistant/components/toon/const.py index 239359c1fdf..5f26035065e 100644 --- a/homeassistant/components/toon/const.py +++ b/homeassistant/components/toon/const.py @@ -8,8 +8,6 @@ DATA_TOON_CLIENT = "toon_client" DATA_TOON_CONFIG = "toon_config" DATA_TOON_UPDATED = "toon_updated" -CONF_CLIENT_ID = "client_id" -CONF_CLIENT_SECRET = "client_secret" CONF_DISPLAY = "display" CONF_TENANT = "tenant" diff --git a/homeassistant/components/toon/translations/it.json b/homeassistant/components/toon/translations/it.json index 62f4031660d..af7d61d0ef9 100644 --- a/homeassistant/components/toon/translations/it.json +++ b/homeassistant/components/toon/translations/it.json @@ -4,7 +4,7 @@ "client_id": "L'ID client dalla configurazione non \u00e8 valido.", "client_secret": "Il client segreto della configurazione non \u00e8 valido.", "no_agreements": "Questo account non ha display Toon.", - "no_app": "\u00c8 necessario configurare Toon prima di poter eseguire l'autenticazione con esso. [Si prega di leggere le istruzioni] (https://www.home-assistant.io/components/toon/).", + "no_app": "\u00c8 necessario configurare Toon prima di poter eseguire l'autenticazione con esso. [Si prega di leggere le istruzioni](https://www.home-assistant.io/components/toon/).", "unknown_auth_fail": "Si \u00e8 verificato un errore imprevisto durante l'autenticazione." }, "error": { diff --git a/homeassistant/components/toon/translations/ko.json b/homeassistant/components/toon/translations/ko.json index 200eb2d810b..7bbde56dbe1 100644 --- a/homeassistant/components/toon/translations/ko.json +++ b/homeassistant/components/toon/translations/ko.json @@ -2,7 +2,7 @@ "config": { "abort": { "client_id": "\ud074\ub77c\uc774\uc5b8\ud2b8 ID \uac00 \uc720\ud6a8\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", - "client_secret": "\ud074\ub77c\uc774\uc5b8\ud2b8 \ube44\ubc00\ubc88\ud638\uac00 \uc720\ud6a8\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", + "client_secret": "\ud074\ub77c\uc774\uc5b8\ud2b8 \uc2dc\ud06c\ub9bf\uc774 \uc720\ud6a8\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", "no_agreements": "\uc774 \uacc4\uc815\uc5d0\ub294 Toon \ub514\uc2a4\ud50c\ub808\uc774\uac00 \uc5c6\uc2b5\ub2c8\ub2e4.", "no_app": "Toon \uc744 \uc778\uc99d\ud558\ub824\uba74 \uba3c\uc800 Toon \uc744 \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/toon/) \ub97c \uc77d\uc5b4\ubcf4\uc138\uc694.", "unknown_auth_fail": "\uc778\uc99d\ud558\ub294 \ub3d9\uc548 \uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4." diff --git a/homeassistant/components/toon/translations/pl.json b/homeassistant/components/toon/translations/pl.json index bbd9b03736d..96002ed932c 100644 --- a/homeassistant/components/toon/translations/pl.json +++ b/homeassistant/components/toon/translations/pl.json @@ -14,9 +14,9 @@ "step": { "authenticate": { "data": { - "password": "[%key_id:common::config_flow::data::password%]", + "password": "Has\u0142o", "tenant": "Najemca", - "username": "[%key_id:common::config_flow::data::username%]" + "username": "Nazwa u\u017cytkownika" }, "description": "Uwierzytelnij konto Eneco Toon (nie konto programisty).", "title": "Po\u0142\u0105cz konto Toon" diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py index 632673233ec..d7c17a1ccff 100644 --- a/homeassistant/components/totalconnect/alarm_control_panel.py +++ b/homeassistant/components/totalconnect/alarm_control_panel.py @@ -17,6 +17,7 @@ from homeassistant.const import ( STATE_ALARM_DISARMING, STATE_ALARM_TRIGGERED, ) +from homeassistant.exceptions import HomeAssistantError from .const import DOMAIN @@ -114,16 +115,20 @@ class TotalConnectAlarm(alarm.AlarmControlPanelEntity): def alarm_disarm(self, code=None): """Send disarm command.""" - self._client.disarm(self._location_id) + if self._client.disarm(self._location_id) is not True: + raise HomeAssistantError(f"TotalConnect failed to disarm {self._name}.") def alarm_arm_home(self, code=None): """Send arm home command.""" - self._client.arm_stay(self._location_id) + if self._client.arm_stay(self._location_id) is not True: + raise HomeAssistantError(f"TotalConnect failed to arm home {self._name}.") def alarm_arm_away(self, code=None): """Send arm away command.""" - self._client.arm_away(self._location_id) + if self._client.arm_away(self._location_id) is not True: + raise HomeAssistantError(f"TotalConnect failed to arm away {self._name}.") def alarm_arm_night(self, code=None): """Send arm night command.""" - self._client.arm_stay_night(self._location_id) + if self._client.arm_stay_night(self._location_id) is not True: + raise HomeAssistantError(f"TotalConnect failed to arm night {self._name}.") diff --git a/homeassistant/components/totalconnect/manifest.json b/homeassistant/components/totalconnect/manifest.json index fc19c889d8b..665a42aba1a 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.54.1"], + "requirements": ["total_connect_client==0.55"], "dependencies": [], "codeowners": ["@austinmroczek"], "config_flow": true diff --git a/homeassistant/components/totalconnect/translations/hu.json b/homeassistant/components/totalconnect/translations/hu.json new file mode 100644 index 00000000000..dee4ed9ee0f --- /dev/null +++ b/homeassistant/components/totalconnect/translations/hu.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/pl.json b/homeassistant/components/totalconnect/translations/pl.json index 426dfebd60e..58a0f7018da 100644 --- a/homeassistant/components/totalconnect/translations/pl.json +++ b/homeassistant/components/totalconnect/translations/pl.json @@ -9,8 +9,8 @@ "step": { "user": { "data": { - "password": "[%key_id:common::config_flow::data::password%]", - "username": "[%key_id:common::config_flow::data::username%]" + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" }, "title": "Total Connect" } diff --git a/homeassistant/components/totalconnect/translations/pt-BR.json b/homeassistant/components/totalconnect/translations/pt-BR.json new file mode 100644 index 00000000000..30b433108ec --- /dev/null +++ b/homeassistant/components/totalconnect/translations/pt-BR.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, + "error": { + "login": "Erro de login: verifique seu nome de usu\u00e1rio e senha" + }, + "step": { + "user": { + "title": "Total Connect" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/translations/bg.json b/homeassistant/components/tplink/translations/bg.json index 288ae9de670..cdceae66cbf 100644 --- a/homeassistant/components/tplink/translations/bg.json +++ b/homeassistant/components/tplink/translations/bg.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 TP-Link \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430?", - "title": "TP-Link Smart Home" + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 TP-Link \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430?" } } } diff --git a/homeassistant/components/tplink/translations/ca.json b/homeassistant/components/tplink/translations/ca.json index 41470c2bd6a..62baf289fd4 100644 --- a/homeassistant/components/tplink/translations/ca.json +++ b/homeassistant/components/tplink/translations/ca.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Vols configurar dispositius intel\u00b7ligents TP-Link?", - "title": "TP-Link Smart Home" + "description": "Vols configurar dispositius intel\u00b7ligents TP-Link?" } } } diff --git a/homeassistant/components/tplink/translations/da.json b/homeassistant/components/tplink/translations/da.json index fa55b4ff92f..e6fc3c895a3 100644 --- a/homeassistant/components/tplink/translations/da.json +++ b/homeassistant/components/tplink/translations/da.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Vil du konfigurere TP-Link-smartenheder?", - "title": "TP-Link Smart Home" + "description": "Vil du konfigurere TP-Link-smartenheder?" } } } diff --git a/homeassistant/components/tplink/translations/de.json b/homeassistant/components/tplink/translations/de.json index bd391ed762c..64bdfc9bf77 100644 --- a/homeassistant/components/tplink/translations/de.json +++ b/homeassistant/components/tplink/translations/de.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "M\u00f6chtest du TP-Link Smart Devices einrichten?", - "title": "TP-Link Smart Home" + "description": "M\u00f6chtest du TP-Link Smart Devices einrichten?" } } } diff --git a/homeassistant/components/tplink/translations/en.json b/homeassistant/components/tplink/translations/en.json index 49c7f40d4ad..3c1f7da52e0 100644 --- a/homeassistant/components/tplink/translations/en.json +++ b/homeassistant/components/tplink/translations/en.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Do you want to setup TP-Link smart devices?", - "title": "TP-Link Smart Home" + "description": "Do you want to setup TP-Link smart devices?" } } } diff --git a/homeassistant/components/tplink/translations/es-419.json b/homeassistant/components/tplink/translations/es-419.json index c349c395733..4113e802e1a 100644 --- a/homeassistant/components/tplink/translations/es-419.json +++ b/homeassistant/components/tplink/translations/es-419.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u00bfDesea configurar dispositivos inteligentes TP-Link?", - "title": "TP-Link Smart Home" + "description": "\u00bfDesea configurar dispositivos inteligentes TP-Link?" } } } diff --git a/homeassistant/components/tplink/translations/es.json b/homeassistant/components/tplink/translations/es.json index 6fa2fd5fd16..408be2c1360 100644 --- a/homeassistant/components/tplink/translations/es.json +++ b/homeassistant/components/tplink/translations/es.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u00bfQuieres configurar dispositivos inteligentes de TP-Link?", - "title": "TP-Link Smart Home" + "description": "\u00bfQuieres configurar dispositivos inteligentes de TP-Link?" } } } diff --git a/homeassistant/components/tplink/translations/fr.json b/homeassistant/components/tplink/translations/fr.json index b67464cb97c..43ea1d1b111 100644 --- a/homeassistant/components/tplink/translations/fr.json +++ b/homeassistant/components/tplink/translations/fr.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Voulez-vous configurer TP-Link smart devices?", - "title": "TP-Link Smart Home" + "description": "Voulez-vous configurer TP-Link smart devices?" } } } diff --git a/homeassistant/components/tplink/translations/he.json b/homeassistant/components/tplink/translations/he.json index f947ce87933..55d53f6e676 100644 --- a/homeassistant/components/tplink/translations/he.json +++ b/homeassistant/components/tplink/translations/he.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u05d4\u05d0\u05dd \u05d0\u05ea\u05d4 \u05e8\u05d5\u05e6\u05d4 \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d7\u05db\u05de\u05d9\u05dd \u05e9\u05dc TP-Link ?", - "title": "\u05d1\u05d9\u05ea \u05d7\u05db\u05dd \u05e9\u05dc TP-Link" + "description": "\u05d4\u05d0\u05dd \u05d0\u05ea\u05d4 \u05e8\u05d5\u05e6\u05d4 \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d7\u05db\u05de\u05d9\u05dd \u05e9\u05dc TP-Link ?" } } } diff --git a/homeassistant/components/tplink/translations/it.json b/homeassistant/components/tplink/translations/it.json index fd46e40d33f..d5fc0a46a00 100644 --- a/homeassistant/components/tplink/translations/it.json +++ b/homeassistant/components/tplink/translations/it.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Vuoi configurare i dispositivi intelligenti TP-Link?", - "title": "TP-Link Smart Home" + "description": "Vuoi configurare i dispositivi intelligenti TP-Link?" } } } diff --git a/homeassistant/components/tplink/translations/ko.json b/homeassistant/components/tplink/translations/ko.json index 45e5c525e35..dc8a6a5a8fc 100644 --- a/homeassistant/components/tplink/translations/ko.json +++ b/homeassistant/components/tplink/translations/ko.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "TP-Link \uc2a4\ub9c8\ud2b8 \uae30\uae30\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "TP-Link Smart Home" + "description": "TP-Link \uc2a4\ub9c8\ud2b8 \uae30\uae30\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" } } } diff --git a/homeassistant/components/tplink/translations/lb.json b/homeassistant/components/tplink/translations/lb.json index 740b1684d6e..80369964f11 100644 --- a/homeassistant/components/tplink/translations/lb.json +++ b/homeassistant/components/tplink/translations/lb.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Soll TP-Link Smart Home konfigur\u00e9iert ginn?", - "title": "TP-Link Smart Home" + "description": "Soll TP-Link Smart Home konfigur\u00e9iert ginn?" } } } diff --git a/homeassistant/components/tplink/translations/nl.json b/homeassistant/components/tplink/translations/nl.json index 0d6ace9da78..f6a8dbbe02a 100644 --- a/homeassistant/components/tplink/translations/nl.json +++ b/homeassistant/components/tplink/translations/nl.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Wil je TP-Link slimme apparaten instellen?", - "title": "TP-Link Smart Home" + "description": "Wil je TP-Link slimme apparaten instellen?" } } } diff --git a/homeassistant/components/tplink/translations/no.json b/homeassistant/components/tplink/translations/no.json index f2ba2085918..d480a5d996d 100644 --- a/homeassistant/components/tplink/translations/no.json +++ b/homeassistant/components/tplink/translations/no.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Vil du konfigurere TP-Link smart enheter?", - "title": "" + "description": "Vil du konfigurere TP-Link smart enheter?" } } } diff --git a/homeassistant/components/tplink/translations/pl.json b/homeassistant/components/tplink/translations/pl.json index 33b963e9813..924f596ca4f 100644 --- a/homeassistant/components/tplink/translations/pl.json +++ b/homeassistant/components/tplink/translations/pl.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Czy chcesz skonfigurowa\u0107 urz\u0105dzenia TP-Link smart?", - "title": "TP-Link Smart Home" + "description": "Czy chcesz skonfigurowa\u0107 urz\u0105dzenia TP-Link smart?" } } } diff --git a/homeassistant/components/tplink/translations/pt-BR.json b/homeassistant/components/tplink/translations/pt-BR.json index f86d5d5b2a2..f4852405726 100644 --- a/homeassistant/components/tplink/translations/pt-BR.json +++ b/homeassistant/components/tplink/translations/pt-BR.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Deseja configurar dispositivos inteligentes TP-Link?", - "title": "TP-Link Smart Home" + "description": "Deseja configurar dispositivos inteligentes TP-Link?" } } } diff --git a/homeassistant/components/tplink/translations/pt.json b/homeassistant/components/tplink/translations/pt.json index 27c9fd6fbb1..9df803c325e 100644 --- a/homeassistant/components/tplink/translations/pt.json +++ b/homeassistant/components/tplink/translations/pt.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Deseja configurar os dispositivos inteligentes TP-Link?", - "title": "TP-Link Smart Home" + "description": "Deseja configurar os dispositivos inteligentes TP-Link?" } } } diff --git a/homeassistant/components/tplink/translations/ru.json b/homeassistant/components/tplink/translations/ru.json index 5797fb63bd7..7f84067d9a2 100644 --- a/homeassistant/components/tplink/translations/ru.json +++ b/homeassistant/components/tplink/translations/ru.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c TP-Link Smart Home?", - "title": "TP-Link Smart Home" + "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 TP-Link Smart Home?" } } } diff --git a/homeassistant/components/tplink/translations/sl.json b/homeassistant/components/tplink/translations/sl.json index 9bbf88cae15..6c49eaf8d0a 100644 --- a/homeassistant/components/tplink/translations/sl.json +++ b/homeassistant/components/tplink/translations/sl.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u017delite namestiti pametne naprave TP-Link?", - "title": "TP-Link Pametni Dom" + "description": "\u017delite namestiti pametne naprave TP-Link?" } } } diff --git a/homeassistant/components/tplink/translations/sv.json b/homeassistant/components/tplink/translations/sv.json index c60162e6e31..70ab1740f0d 100644 --- a/homeassistant/components/tplink/translations/sv.json +++ b/homeassistant/components/tplink/translations/sv.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Vill du konfigurera TP-Link smart enheter?", - "title": "TP-Link Smart Home" + "description": "Vill du konfigurera TP-Link smart enheter?" } } } diff --git a/homeassistant/components/tplink/translations/zh-Hans.json b/homeassistant/components/tplink/translations/zh-Hans.json index 22ff788e9bb..710cc0fe166 100644 --- a/homeassistant/components/tplink/translations/zh-Hans.json +++ b/homeassistant/components/tplink/translations/zh-Hans.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u60a8\u60f3\u8981\u914d\u7f6e TP-Link \u667a\u80fd\u8bbe\u5907\u5417\uff1f", - "title": "TP-Link Smart Home" + "description": "\u60a8\u60f3\u8981\u914d\u7f6e TP-Link \u667a\u80fd\u8bbe\u5907\u5417\uff1f" } } } diff --git a/homeassistant/components/tplink/translations/zh-Hant.json b/homeassistant/components/tplink/translations/zh-Hant.json index 08a4779e593..9fafcbbce7d 100644 --- a/homeassistant/components/tplink/translations/zh-Hant.json +++ b/homeassistant/components/tplink/translations/zh-Hant.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a TP-Link \u667a\u80fd\u8a2d\u5099\uff1f", - "title": "TP-Link Smart Home" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a TP-Link \u667a\u80fd\u8a2d\u5099\uff1f" } } } diff --git a/homeassistant/components/traccar/translations/pl.json b/homeassistant/components/traccar/translations/pl.json index 5b1ad10c74d..fecfc94c36c 100644 --- a/homeassistant/components/traccar/translations/pl.json +++ b/homeassistant/components/traccar/translations/pl.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Niezb\u0119dna jest tylko jedna instancja." }, "create_entry": { - "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant'a, musisz skonfigurowa\u0107 webhook w Traccar. \n\n U\u017cyj nast\u0119puj\u0105cego URL: `{webhook_url}` \n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}), by pozna\u0107 szczeg\u00f3\u0142y." + "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistanta, musisz skonfigurowa\u0107 webhook w Traccar. \n\n U\u017cyj nast\u0119puj\u0105cego URL: `{webhook_url}` \n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}), by pozna\u0107 szczeg\u00f3\u0142y." }, "step": { "user": { diff --git a/homeassistant/components/tradfri/translations/fi.json b/homeassistant/components/tradfri/translations/fi.json index 5b7b417977d..31984784ee6 100644 --- a/homeassistant/components/tradfri/translations/fi.json +++ b/homeassistant/components/tradfri/translations/fi.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Silta on jo m\u00e4\u00e4ritetty" + }, "step": { "auth": { "data": { diff --git a/homeassistant/components/tradfri/translations/pl.json b/homeassistant/components/tradfri/translations/pl.json index 83190ee37e3..028956ec6b3 100644 --- a/homeassistant/components/tradfri/translations/pl.json +++ b/homeassistant/components/tradfri/translations/pl.json @@ -12,7 +12,7 @@ "step": { "auth": { "data": { - "host": "[%key_id:common::config_flow::data::host%]", + "host": "Nazwa hosta lub adres IP", "security_code": "Kod bezpiecze\u0144stwa" }, "description": "Mo\u017cesz znale\u017a\u0107 kod bezpiecze\u0144stwa z ty\u0142u bramki.", diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index 32177e91160..a09fb6a7456 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -362,6 +362,8 @@ class TransmissionData: def start_torrents(self): """Start all torrents.""" + if len(self.torrents) <= 0: + return self._api.start_all() def stop_torrents(self): diff --git a/homeassistant/components/transmission/translations/hu.json b/homeassistant/components/transmission/translations/hu.json index fd9bb9e64cb..79fdbe9c7f0 100644 --- a/homeassistant/components/transmission/translations/hu.json +++ b/homeassistant/components/transmission/translations/hu.json @@ -8,7 +8,7 @@ "step": { "user": { "data": { - "host": "Kiszolg\u00e1l\u00f3", + "host": "Hoszt", "name": "N\u00e9v", "password": "Jelsz\u00f3", "port": "Port", diff --git a/homeassistant/components/transmission/translations/pl.json b/homeassistant/components/transmission/translations/pl.json index 9d2b40f1cf2..52efb32b551 100644 --- a/homeassistant/components/transmission/translations/pl.json +++ b/homeassistant/components/transmission/translations/pl.json @@ -11,11 +11,11 @@ "step": { "user": { "data": { - "host": "[%key_id:common::config_flow::data::host%]", + "host": "Nazwa hosta lub adres IP", "name": "Nazwa", - "password": "[%key_id:common::config_flow::data::password%]", - "port": "[%key_id:common::config_flow::data::port%]", - "username": "[%key_id:common::config_flow::data::username%]" + "password": "Has\u0142o", + "port": "Port", + "username": "Nazwa u\u017cytkownika" }, "title": "Konfiguracja klienta Transmission" } diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 9a33c0514d5..6dc2e9b7d45 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -399,7 +399,7 @@ class SpeechManager: @callback def async_remove_from_mem(): """Cleanup memcache.""" - self.mem_cache.pop(key) + self.mem_cache.pop(key, None) self.hass.loop.call_later(self.time_memory, async_remove_from_mem) @@ -530,7 +530,7 @@ class TextToSpeechUrlView(HomeAssistantView): """Initialize a tts view.""" self.tts = tts - async def post(self, request): + async def post(self, request: web.Request) -> web.Response: """Generate speech and provide url.""" try: data = await request.json() @@ -570,7 +570,7 @@ class TextToSpeechView(HomeAssistantView): """Initialize a tts view.""" self.tts = tts - async def get(self, request, filename): + async def get(self, request: web.Request, filename: str) -> web.Response: """Start a get request.""" try: content, data = await self.tts.async_read_tts(filename) diff --git a/homeassistant/components/tuya/translations/ca.json b/homeassistant/components/tuya/translations/ca.json index 2cf09455b7b..89398296e9f 100644 --- a/homeassistant/components/tuya/translations/ca.json +++ b/homeassistant/components/tuya/translations/ca.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_in_progress": "La configuraci\u00f3 de Tuya ja est\u00e0 en curs.", "auth_failed": "Autenticaci\u00f3 inv\u00e0lida", "conn_error": "No s'ha pogut connectar", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." @@ -15,6 +14,7 @@ "data": { "country_code": "El teu codi de pa\u00eds (per exemple, 1 per l'EUA o 86 per la Xina)", "password": "Contrasenya", + "platform": "L\u2019aplicaci\u00f3 on es registra el vostre compte", "username": "Nom d'usuari" }, "description": "Introdueix la teva credencial de Tuya.", diff --git a/homeassistant/components/tuya/translations/en.json b/homeassistant/components/tuya/translations/en.json index c3de7cbe020..c66eb2d274f 100644 --- a/homeassistant/components/tuya/translations/en.json +++ b/homeassistant/components/tuya/translations/en.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_in_progress": "Tuya configuration is already in progress.", "auth_failed": "Invalid authentication", "conn_error": "Failed to connect", "single_instance_allowed": "Already configured. Only a single configuration possible." diff --git a/homeassistant/components/tuya/translations/es.json b/homeassistant/components/tuya/translations/es.json index 163b2ed73c8..f0a607331f3 100644 --- a/homeassistant/components/tuya/translations/es.json +++ b/homeassistant/components/tuya/translations/es.json @@ -1,9 +1,8 @@ { "config": { "abort": { - "already_in_progress": "La configuraci\u00f3n de Tuya ya est\u00e1 en progreso.", "auth_failed": "Autenticaci\u00f3n no v\u00e1lida", - "conn_error": "Fallo al conectarse", + "conn_error": "No se pudo conectar", "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n." }, "error": { diff --git a/homeassistant/components/tuya/translations/fi.json b/homeassistant/components/tuya/translations/fi.json index f0c2c5e71d3..a2efee7e5ec 100644 --- a/homeassistant/components/tuya/translations/fi.json +++ b/homeassistant/components/tuya/translations/fi.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_in_progress": "Tuya-m\u00e4\u00e4ritykset ovat jo k\u00e4ynniss\u00e4.", "conn_error": "Yhdist\u00e4minen ep\u00e4onnistui" }, "error": { diff --git a/homeassistant/components/tuya/translations/fr.json b/homeassistant/components/tuya/translations/fr.json index 9860353504b..6e181e2d646 100644 --- a/homeassistant/components/tuya/translations/fr.json +++ b/homeassistant/components/tuya/translations/fr.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_in_progress": "La configuration de Tuya est d\u00e9j\u00e0 en cours." - }, "flow_title": "Configuration Tuya", "step": { "user": { diff --git a/homeassistant/components/tuya/translations/hu.json b/homeassistant/components/tuya/translations/hu.json new file mode 100644 index 00000000000..69681ee800a --- /dev/null +++ b/homeassistant/components/tuya/translations/hu.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "auth_failed": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "conn_error": "Sikertelen csatlakoz\u00e1s", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "error": { + "auth_failed": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/it.json b/homeassistant/components/tuya/translations/it.json index 581121cc6f9..fc2c8fc49b3 100644 --- a/homeassistant/components/tuya/translations/it.json +++ b/homeassistant/components/tuya/translations/it.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_in_progress": "La configurazione di Tuya \u00e8 gi\u00e0 in corso.", "auth_failed": "Autenticazione non valida", "conn_error": "Impossibile connettersi", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." diff --git a/homeassistant/components/tuya/translations/ko.json b/homeassistant/components/tuya/translations/ko.json index 201e45ec357..a028fc2ed56 100644 --- a/homeassistant/components/tuya/translations/ko.json +++ b/homeassistant/components/tuya/translations/ko.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_in_progress": "Tuya \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4.", "auth_failed": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "conn_error": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." diff --git a/homeassistant/components/tuya/translations/lb.json b/homeassistant/components/tuya/translations/lb.json index d09427d71d8..32595b961a1 100644 --- a/homeassistant/components/tuya/translations/lb.json +++ b/homeassistant/components/tuya/translations/lb.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_in_progress": "Tuya Konfiguratioun ass schonn am gaang.", "auth_failed": "Ong\u00eblteg Authentifikatioun", "conn_error": "Feeler beim verbannen", "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech." diff --git a/homeassistant/components/tuya/translations/nl.json b/homeassistant/components/tuya/translations/nl.json new file mode 100644 index 00000000000..0c1a78eb2a5 --- /dev/null +++ b/homeassistant/components/tuya/translations/nl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "auth_failed": "Verkeerde gegevens", + "conn_error": "Niet gelukt om te verbinden.", + "single_instance_allowed": "Al geconfigureerd. Er is maar een configuratie mogelijk." + }, + "error": { + "auth_failed": "Verkeerde gegevens" + }, + "flow_title": "Tuya-configuratie", + "step": { + "user": { + "data": { + "country_code": "De landcode van uw account (bijvoorbeeld 1 voor de VS of 86 voor China)", + "password": "Wachtwoord", + "platform": "De app waar uw account is geregistreerd", + "username": "Gebruikersnaam" + }, + "description": "Voer uw Tuya-referentie in.", + "title": "Tuya" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/no.json b/homeassistant/components/tuya/translations/no.json index 253401e721e..7b336c3f9c6 100644 --- a/homeassistant/components/tuya/translations/no.json +++ b/homeassistant/components/tuya/translations/no.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_in_progress": "Tuya konfigurasjon er allerede i gang." - }, "flow_title": "Tuya konfigurasjon", "step": { "user": { diff --git a/homeassistant/components/tuya/translations/pl.json b/homeassistant/components/tuya/translations/pl.json index 5f5f552d4c2..7278806b5f6 100644 --- a/homeassistant/components/tuya/translations/pl.json +++ b/homeassistant/components/tuya/translations/pl.json @@ -1,18 +1,24 @@ { "config": { "abort": { - "auth_failed": "[%key_id:common::config_flow::error::invalid_auth%]", - "conn_error": "[%key_id:common::config_flow::error::cannot_connect%]" + "auth_failed": "Niepoprawne uwierzytelnienie.", + "conn_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, "error": { - "auth_failed": "[%key_id:common::config_flow::error::invalid_auth%]" + "auth_failed": "Niepoprawne uwierzytelnienie." }, + "flow_title": "Konfiguracja integracji Tuya", "step": { "user": { "data": { - "password": "[%key_id:common::config_flow::data::password%]", - "username": "[%key_id:common::config_flow::data::username%]" - } + "country_code": "Kod kraju twojego konta (np. 1 dla USA lub 86 dla Chin)", + "password": "Has\u0142o", + "platform": "Aplikacja, w kt\u00f3rej zarejestrowane jest Twoje konto", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce", + "title": "Tuya" } } } diff --git a/homeassistant/components/tuya/translations/pt-BR.json b/homeassistant/components/tuya/translations/pt-BR.json new file mode 100644 index 00000000000..8dc537e7549 --- /dev/null +++ b/homeassistant/components/tuya/translations/pt-BR.json @@ -0,0 +1,17 @@ +{ + "config": { + "flow_title": "Configura\u00e7\u00e3o Tuya", + "step": { + "user": { + "data": { + "country_code": "O c\u00f3digo do pa\u00eds da sua conta (por exemplo, 1 para os EUA ou 86 para a China)", + "password": "Senha", + "platform": "O aplicativo onde sua conta \u00e9 registrada", + "username": "Nome de usu\u00e1rio" + }, + "description": "Digite sua credencial Tuya.", + "title": "Tuya" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/ru.json b/homeassistant/components/tuya/translations/ru.json index 1452621b198..dab853f3310 100644 --- a/homeassistant/components/tuya/translations/ru.json +++ b/homeassistant/components/tuya/translations/ru.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", "auth_failed": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", "conn_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." @@ -13,9 +12,9 @@ "step": { "user": { "data": { - "country_code": "\u041a\u043e\u0434 \u0441\u0442\u0440\u0430\u043d\u044b \u0412\u0430\u0448\u0435\u0433\u043e \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0430 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, 1 \u0434\u043b\u044f \u0421\u0428\u0410 \u0438\u043b\u0438 86 \u0434\u043b\u044f \u041a\u0438\u0442\u0430\u044f)", + "country_code": "\u041a\u043e\u0434 \u0441\u0442\u0440\u0430\u043d\u044b \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0430 (1 \u0434\u043b\u044f \u0421\u0428\u0410 \u0438\u043b\u0438 86 \u0434\u043b\u044f \u041a\u0438\u0442\u0430\u044f)", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "platform": "\u041f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d \u0412\u0430\u0448 \u0430\u043a\u043a\u0430\u0443\u043d\u0442", + "platform": "\u041f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d \u0430\u043a\u043a\u0430\u0443\u043d\u0442", "username": "\u041b\u043e\u0433\u0438\u043d" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Tuya.", diff --git a/homeassistant/components/tuya/translations/zh-Hant.json b/homeassistant/components/tuya/translations/zh-Hant.json index 91c0936404d..2d7d2fe1004 100644 --- a/homeassistant/components/tuya/translations/zh-Hant.json +++ b/homeassistant/components/tuya/translations/zh-Hant.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_in_progress": "Tuya \u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", "auth_failed": "\u9a57\u8b49\u78bc\u7121\u6548", "conn_error": "\u9023\u7dda\u5931\u6557", "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" diff --git a/homeassistant/components/twentemilieu/translations/pl.json b/homeassistant/components/twentemilieu/translations/pl.json index e8cd67db9db..bfa38f9ef8a 100644 --- a/homeassistant/components/twentemilieu/translations/pl.json +++ b/homeassistant/components/twentemilieu/translations/pl.json @@ -4,7 +4,7 @@ "address_exists": "Adres jest ju\u017c skonfigurowany." }, "error": { - "connection_error": "[%key_id:common::config_flow::error::cannot_connect%]", + "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", "invalid_address": "Nie znaleziono adresu w obszarze us\u0142ugi Twente Milieu." }, "step": { diff --git a/homeassistant/components/twilio/translations/it.json b/homeassistant/components/twilio/translations/it.json index b50d82bfeb1..8bf92bf60a2 100644 --- a/homeassistant/components/twilio/translations/it.json +++ b/homeassistant/components/twilio/translations/it.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u00c8 necessaria una sola istanza." }, "create_entry": { - "default": "Per inviare eventi a Home Assistant, dovrai configurare [Webhooks con Twilio]({twilio_url})\n\n Compila le seguenti informazioni: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n - Content Type: application/x-www-form-urlencoded\n\n Vedi [la documentazione]({docs_url}) su come configurare le automazioni per gestire i dati in arrivo." + "default": "Per inviare eventi a Home Assistant, dovrai configurare [Webhooks con Twilio]({twilio_url})\n\n Compila le seguenti informazioni: \n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/x-www-form-urlencoded\n\n Vedi [la documentazione]({docs_url}) su come configurare le automazioni per gestire i dati in arrivo." }, "step": { "user": { diff --git a/homeassistant/components/twilio/translations/pl.json b/homeassistant/components/twilio/translations/pl.json index 8d2f02ade2a..8c7a4da4cdd 100644 --- a/homeassistant/components/twilio/translations/pl.json +++ b/homeassistant/components/twilio/translations/pl.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Wymagana jest tylko jedna instancja." }, "create_entry": { - "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant'a, musisz skonfigurowa\u0107 [Twilio Webhook]({twilio_url}). \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n - Typ zawarto\u015bci: application/x-www-form-urlencoded \n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}) na temat konfiguracji automatyzacji, by obs\u0142u\u017cy\u0107 przychodz\u0105ce dane." + "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistanta, musisz skonfigurowa\u0107 [Twilio Webhook]({twilio_url}). \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n - Typ zawarto\u015bci: application/x-www-form-urlencoded \n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}) na temat konfiguracji automatyzacji, by obs\u0142u\u017cy\u0107 przychodz\u0105ce dane." }, "step": { "user": { diff --git a/homeassistant/components/twitch/sensor.py b/homeassistant/components/twitch/sensor.py index 68b7d5dce21..52212fca060 100644 --- a/homeassistant/components/twitch/sensor.py +++ b/homeassistant/components/twitch/sensor.py @@ -6,7 +6,7 @@ from twitch import TwitchClient import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_TOKEN +from homeassistant.const import CONF_CLIENT_ID, CONF_TOKEN import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -23,7 +23,6 @@ ATTR_FOLLOWING = "followers" ATTR_VIEWS = "views" CONF_CHANNELS = "channels" -CONF_CLIENT_ID = "client_id" ICON = "mdi:twitch" diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index ebad63acb4e..602795404bb 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -301,7 +301,7 @@ class UniFiDeviceTracker(UniFiBase, ScannerEntity): self.cancel_scheduled_update = async_track_point_in_utc_time( self.hass, _no_heartbeat, - dt_util.utcnow() + timedelta(seconds=self.device.next_interval + 10), + dt_util.utcnow() + timedelta(seconds=self.device.next_interval + 60), ) elif ( diff --git a/homeassistant/components/unifi/strings.json b/homeassistant/components/unifi/strings.json index df1cb753b53..95d273278bd 100644 --- a/homeassistant/components/unifi/strings.json +++ b/homeassistant/components/unifi/strings.json @@ -24,7 +24,6 @@ }, "options": { "step": { - "init": { "data": {} }, "device_tracker": { "data": { "detection_time": "Time in seconds from last seen until considered away", diff --git a/homeassistant/components/unifi/translations/bg.json b/homeassistant/components/unifi/translations/bg.json index f99f5ed67e5..afe2db0cc5b 100644 --- a/homeassistant/components/unifi/translations/bg.json +++ b/homeassistant/components/unifi/translations/bg.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "\u0421\u0430\u0439\u0442\u044a\u0442 \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", - "user_privilege": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044f\u0442 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0435 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440" + "already_configured": "\u0421\u0430\u0439\u0442\u044a\u0442 \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" }, "error": { "faulty_credentials": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0434\u0430\u043d\u043d\u0438", diff --git a/homeassistant/components/unifi/translations/ca.json b/homeassistant/components/unifi/translations/ca.json index 0fdbf0cde10..de2a8ffc562 100644 --- a/homeassistant/components/unifi/translations/ca.json +++ b/homeassistant/components/unifi/translations/ca.json @@ -1,9 +1,7 @@ { "config": { "abort": { - "already_configured": "El lloc del controlador ja est\u00e0 configurat", - "no_local_user": "No s'ha trobat cap usuari local, configura un compte local al controlador i torna-ho a provar", - "user_privilege": "L'usuari ha de ser administrador" + "already_configured": "El lloc del controlador ja est\u00e0 configurat" }, "error": { "faulty_credentials": "[%key::common::config_flow::error::invalid_auth%]", @@ -29,7 +27,6 @@ "client_control": { "data": { "block_client": "Clients controlats amb acc\u00e9s a la xarxa", - "new_client": "Afegeix un client nou per al control d'acc\u00e9s a la xarxa", "poe_clients": "Permet control POE dels clients" }, "description": "Configura els controls del client \n\nConfigura interruptors per als n\u00fameros de s\u00e8rie als quals vulguis controlar l'acc\u00e9s a la xarxa.", @@ -38,13 +35,14 @@ "device_tracker": { "data": { "detection_time": "Temps (en segons) des de s'ha vist per \u00faltima vegada fins que es considera a fora", + "ignore_wired_bug": "Desactiva la l\u00f2gica d'errors amb UniFi", "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" + "title": "Opcions d'UniFi 1/3" }, "simple_options": { "data": { @@ -56,10 +54,10 @@ }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "Crea sensors d'\u00fas d'ample de banda per a clients de la xarxa" + "allow_bandwidth_sensors": "Sensors d'utilitzaci\u00f3 d'ample de banda per a clients de la xarxa" }, "description": "Configuraci\u00f3 dels sensors d'estad\u00edstiques", - "title": "Opcions d'UniFi" + "title": "Opcions d'UniFi 3/3" } } } diff --git a/homeassistant/components/unifi/translations/cs.json b/homeassistant/components/unifi/translations/cs.json index 8fe1c060992..fdbfbc343de 100644 --- a/homeassistant/components/unifi/translations/cs.json +++ b/homeassistant/components/unifi/translations/cs.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "\u0158adi\u010d je ji\u017e nakonfigurov\u00e1n", - "user_privilege": "U\u017eivatel mus\u00ed b\u00fdt spr\u00e1vcem" + "already_configured": "\u0158adi\u010d je ji\u017e nakonfigurov\u00e1n" }, "error": { "faulty_credentials": "Chybn\u00e9 p\u0159ihla\u0161ovac\u00ed \u00fadaje", diff --git a/homeassistant/components/unifi/translations/da.json b/homeassistant/components/unifi/translations/da.json index a15d25a283d..15ec878f1ce 100644 --- a/homeassistant/components/unifi/translations/da.json +++ b/homeassistant/components/unifi/translations/da.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Controller site er allerede konfigureret", - "user_privilege": "Bruger skal v\u00e6re administrator" + "already_configured": "Controller site er allerede konfigureret" }, "error": { "faulty_credentials": "Ugyldige legitimationsoplysninger", diff --git a/homeassistant/components/unifi/translations/de.json b/homeassistant/components/unifi/translations/de.json index 4ef34b3915b..133a5355deb 100644 --- a/homeassistant/components/unifi/translations/de.json +++ b/homeassistant/components/unifi/translations/de.json @@ -1,9 +1,7 @@ { "config": { "abort": { - "already_configured": "Controller-Site ist bereits konfiguriert", - "no_local_user": "Kein lokaler Benutzer gefunden, konfigurieren Sie ein lokales Konto auf dem Controller und versuchen Sie es erneut", - "user_privilege": "Der Benutzer muss Administrator sein" + "already_configured": "Controller-Site ist bereits konfiguriert" }, "error": { "faulty_credentials": "Ung\u00fcltige Anmeldeinformationen", @@ -29,7 +27,6 @@ "client_control": { "data": { "block_client": "Clients mit Netzwerkzugriffskontrolle", - "new_client": "F\u00fcgen Sie einen neuen Client f\u00fcr die Netzwerkzugangskontrolle hinzu", "poe_clients": "POE-Kontrolle von Clients zulassen" }, "description": "Konfigurieren Sie Client-Steuerelemente \n\nErstellen Sie Switches f\u00fcr Seriennummern, f\u00fcr die Sie den Netzwerkzugriff steuern m\u00f6chten.", diff --git a/homeassistant/components/unifi/translations/en.json b/homeassistant/components/unifi/translations/en.json index 279904ebd95..691f4fb6b01 100644 --- a/homeassistant/components/unifi/translations/en.json +++ b/homeassistant/components/unifi/translations/en.json @@ -1,9 +1,7 @@ { "config": { "abort": { - "already_configured": "Controller site is already configured", - "no_local_user": "No local user found, configure a local account on controller and try again", - "user_privilege": "User needs to be administrator" + "already_configured": "Controller site is already configured" }, "error": { "faulty_credentials": "Invalid authentication", @@ -29,7 +27,6 @@ "client_control": { "data": { "block_client": "Network access controlled clients", - "new_client": "Add new client for network access control", "poe_clients": "Allow POE control of clients" }, "description": "Configure client controls\n\nCreate switches for serial numbers you want to control network access for.", diff --git a/homeassistant/components/unifi/translations/es-419.json b/homeassistant/components/unifi/translations/es-419.json index 7f92a0b45af..0c4e9a73614 100644 --- a/homeassistant/components/unifi/translations/es-419.json +++ b/homeassistant/components/unifi/translations/es-419.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "El sitio del controlador ya est\u00e1 configurado", - "user_privilege": "El usuario necesita ser administrador" + "already_configured": "El sitio del controlador ya est\u00e1 configurado" }, "error": { "faulty_credentials": "Credenciales de usuario incorrectas", @@ -28,7 +27,6 @@ "client_control": { "data": { "block_client": "Acceso controlado a la red de clientes", - "new_client": "Agregar nuevo cliente para control de acceso a la red", "poe_clients": "Permitir control POE de clientes" }, "description": "Configurar controles de cliente \n\nCree conmutadores para los n\u00fameros de serie para los que desea controlar el acceso a la red.", diff --git a/homeassistant/components/unifi/translations/es.json b/homeassistant/components/unifi/translations/es.json index 1d2bb2977cb..1867bd89ffd 100644 --- a/homeassistant/components/unifi/translations/es.json +++ b/homeassistant/components/unifi/translations/es.json @@ -1,9 +1,7 @@ { "config": { "abort": { - "already_configured": "El sitio del controlador ya est\u00e1 configurado", - "no_local_user": "No se encontr\u00f3 ning\u00fan usuario local, configure una cuenta local en el controlador e int\u00e9ntelo de nuevo", - "user_privilege": "El usuario debe ser administrador" + "already_configured": "El sitio del controlador ya est\u00e1 configurado" }, "error": { "faulty_credentials": "Autenticaci\u00f3n no v\u00e1lida", @@ -29,7 +27,6 @@ "client_control": { "data": { "block_client": "Clientes con acceso controlado a la red", - "new_client": "A\u00f1adir nuevo cliente para el control de acceso a la red", "poe_clients": "Permitir control PoE de clientes" }, "description": "Configurar controles de cliente\n\nCrea conmutadores para los n\u00fameros de serie para los que deseas controlar el acceso a la red.", diff --git a/homeassistant/components/unifi/translations/fi.json b/homeassistant/components/unifi/translations/fi.json index 3b9bceb1d0f..6b6d9abf9e5 100644 --- a/homeassistant/components/unifi/translations/fi.json +++ b/homeassistant/components/unifi/translations/fi.json @@ -1,7 +1,8 @@ { "config": { - "abort": { - "user_privilege": "K\u00e4ytt\u00e4j\u00e4n on oltava j\u00e4rjestelm\u00e4nvalvoja" + "error": { + "faulty_credentials": "Virheellinen tunnistautuminen", + "service_unavailable": "Yhdist\u00e4minen ep\u00e4onnistui" }, "step": { "user": { @@ -9,7 +10,8 @@ "host": "Palvelin", "password": "Salasana", "port": "Portti", - "site": "Sivuston ID" + "site": "Sivuston ID", + "username": "K\u00e4ytt\u00e4j\u00e4tunnus" } } } diff --git a/homeassistant/components/unifi/translations/fr.json b/homeassistant/components/unifi/translations/fr.json index 8e2a8be41b1..f1846177791 100644 --- a/homeassistant/components/unifi/translations/fr.json +++ b/homeassistant/components/unifi/translations/fr.json @@ -1,9 +1,7 @@ { "config": { "abort": { - "already_configured": "Le contr\u00f4leur est d\u00e9j\u00e0 configur\u00e9", - "no_local_user": "Aucun utilisateur local trouv\u00e9, configurez un compte local sur le contr\u00f4leur et r\u00e9essayez", - "user_privilege": "L'utilisateur doit \u00eatre administrateur" + "already_configured": "Le contr\u00f4leur est d\u00e9j\u00e0 configur\u00e9" }, "error": { "faulty_credentials": "Mauvaises informations d'identification de l'utilisateur", @@ -28,14 +26,14 @@ "step": { "client_control": { "data": { - "block_client": "Clients contr\u00f4l\u00e9s par acc\u00e8s r\u00e9seau", - "new_client": "Ajouter un nouveau client pour le contr\u00f4le d'acc\u00e8s au r\u00e9seau" + "block_client": "Clients contr\u00f4l\u00e9s par acc\u00e8s r\u00e9seau" }, "title": "Options UniFi 2/3" }, "device_tracker": { "data": { "detection_time": "Temps en secondes depuis la derni\u00e8re vue avant de consid\u00e9rer comme absent", + "ssid_filter": "S\u00e9lectionnez les SSID pour suivre les clients sans fil", "track_clients": "Suivre les clients du r\u00e9seau", "track_devices": "Suivre les p\u00e9riph\u00e9riques r\u00e9seau (p\u00e9riph\u00e9riques Ubiquiti)", "track_wired_clients": "Inclure les clients du r\u00e9seau filaire" diff --git a/homeassistant/components/unifi/translations/hu.json b/homeassistant/components/unifi/translations/hu.json index 49bab5225d9..91d031334dd 100644 --- a/homeassistant/components/unifi/translations/hu.json +++ b/homeassistant/components/unifi/translations/hu.json @@ -1,11 +1,8 @@ { "config": { - "abort": { - "user_privilege": "A felhaszn\u00e1l\u00f3nak rendszergazd\u00e1nak kell lennie" - }, "error": { - "faulty_credentials": "Rossz felhaszn\u00e1l\u00f3i hiteles\u00edt\u0151 adatok", - "service_unavailable": "Nincs el\u00e9rhet\u0151 szolg\u00e1ltat\u00e1s" + "faulty_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "service_unavailable": "Sikertelen csatlakoz\u00e1s" }, "step": { "user": { diff --git a/homeassistant/components/unifi/translations/it.json b/homeassistant/components/unifi/translations/it.json index 9eddddde302..8a06ca440c5 100644 --- a/homeassistant/components/unifi/translations/it.json +++ b/homeassistant/components/unifi/translations/it.json @@ -1,9 +1,7 @@ { "config": { "abort": { - "already_configured": "Il sito del Controller \u00e8 gi\u00e0 configurato", - "no_local_user": "Nessun utente locale trovato, configura un account locale sul controller e riprova", - "user_privilege": "L'utente deve essere amministratore" + "already_configured": "Il sito del Controller \u00e8 gi\u00e0 configurato" }, "error": { "faulty_credentials": "Autenticazione non valida", @@ -29,7 +27,6 @@ "client_control": { "data": { "block_client": "Client controllati per l'accesso alla rete", - "new_client": "Aggiungere un nuovo client per il controllo dell'accesso alla rete", "poe_clients": "Consentire il controllo POE dei client" }, "description": "Configurare i controlli client \n\nCreare interruttori per i numeri di serie dei quali si desidera controllare l'accesso alla rete.", diff --git a/homeassistant/components/unifi/translations/ko.json b/homeassistant/components/unifi/translations/ko.json index 6878b537209..a3d2c8f3b69 100644 --- a/homeassistant/components/unifi/translations/ko.json +++ b/homeassistant/components/unifi/translations/ko.json @@ -1,9 +1,7 @@ { "config": { "abort": { - "already_configured": "\ucee8\ud2b8\ub864\ub7ec \uc0ac\uc774\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "no_local_user": "\ub85c\uceec \uc0ac\uc6a9\uc790\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ucee8\ud2b8\ub864\ub7ec\uc5d0\uc11c \ub85c\uceec \uacc4\uc815\uc744 \uad6c\uc131\ud55c \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", - "user_privilege": "\uc0ac\uc6a9\uc790\ub294 \uad00\ub9ac\uc790\uc5ec\uc57c \ud569\ub2c8\ub2e4" + "already_configured": "\ucee8\ud2b8\ub864\ub7ec \uc0ac\uc774\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { "faulty_credentials": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", @@ -29,7 +27,6 @@ "client_control": { "data": { "block_client": "\ub124\ud2b8\uc6cc\ud06c \uc561\uc138\uc2a4 \uc81c\uc5b4 \ud074\ub77c\uc774\uc5b8\ud2b8", - "new_client": "\ub124\ud2b8\uc6cc\ud06c \uc561\uc138\uc2a4 \uc81c\uc5b4\ub97c \uc704\ud55c \uc0c8\ub85c\uc6b4 \ud074\ub77c\uc774\uc5b8\ud2b8 \ucd94\uac00", "poe_clients": "\ud074\ub77c\uc774\uc5b8\ud2b8\uc758 POE \uc81c\uc5b4 \ud5c8\uc6a9" }, "description": "\ud074\ub77c\uc774\uc5b8\ud2b8 \ucee8\ud2b8\ub864 \uad6c\uc131 \n\n\ub124\ud2b8\uc6cc\ud06c \uc561\uc138\uc2a4\ub97c \uc81c\uc5b4\ud558\ub824\ub294 \uc2dc\ub9ac\uc5bc \ubc88\ud638\uc5d0 \ub300\ud55c \uc2a4\uc704\uce58\ub97c \ub9cc\ub4ed\ub2c8\ub2e4.", diff --git a/homeassistant/components/unifi/translations/lb.json b/homeassistant/components/unifi/translations/lb.json index d93a1d8d882..83011f8cceb 100644 --- a/homeassistant/components/unifi/translations/lb.json +++ b/homeassistant/components/unifi/translations/lb.json @@ -1,9 +1,7 @@ { "config": { "abort": { - "already_configured": "Kontroller Site ass scho konfigur\u00e9iert", - "no_local_user": "Kee lokale Benotzer fonnt, erstell ee lokale Kont um Kontroller a prob\u00e9ier nach eemol", - "user_privilege": "Benotzer muss een Administrator sinn" + "already_configured": "Kontroller Site ass scho konfigur\u00e9iert" }, "error": { "faulty_credentials": "Ong\u00eblteg Login Informatioune", @@ -29,7 +27,6 @@ "client_control": { "data": { "block_client": "Netzwierk Zougang kontroll\u00e9iert Clienten", - "new_client": "Neie Client fir Netzwierk Zougang Kontroll b\u00e4isetzen", "poe_clients": "POE Kontroll vun Clienten erlaben" }, "description": "Client Kontroll konfigur\u00e9ieren\n\nErstell Schalter fir Serienummer d\u00e9i sollen fir Netzwierk Zougangs Kontroll kontroll\u00e9iert ginn.", diff --git a/homeassistant/components/unifi/translations/nl.json b/homeassistant/components/unifi/translations/nl.json index d3ea6b3eaae..5d64d73d1de 100644 --- a/homeassistant/components/unifi/translations/nl.json +++ b/homeassistant/components/unifi/translations/nl.json @@ -1,9 +1,7 @@ { "config": { "abort": { - "already_configured": "Controller site is al geconfigureerd", - "no_local_user": "Geen lokale gebruiker gevonden, configureer een lokaal account op de controller en probeer het opnieuw", - "user_privilege": "Gebruiker moet beheerder zijn" + "already_configured": "Controller site is al geconfigureerd" }, "error": { "faulty_credentials": "Foutieve gebruikersgegevens", @@ -29,7 +27,6 @@ "client_control": { "data": { "block_client": "Cli\u00ebnten met netwerktoegang", - "new_client": "Voeg een nieuwe client toe voor netwerktoegangsbeheer", "poe_clients": "Sta POE-controle van gebruikers toe" }, "description": "Configureer clientbesturingen \n\n Maak schakelaars voor serienummers waarvoor u de netwerktoegang wilt beheren.", diff --git a/homeassistant/components/unifi/translations/no.json b/homeassistant/components/unifi/translations/no.json index c244cac1696..d9260f92640 100644 --- a/homeassistant/components/unifi/translations/no.json +++ b/homeassistant/components/unifi/translations/no.json @@ -1,9 +1,7 @@ { "config": { "abort": { - "already_configured": "Kontroller nettstedet er allerede konfigurert", - "no_local_user": "Ingen lokale brukere funnet. Konfigurer en lokal konto p\u00e5 kontrolleren og pr\u00f8v igjen", - "user_privilege": "Bruker m\u00e5 v\u00e6re administrator" + "already_configured": "Kontroller nettstedet er allerede konfigurert" }, "error": { "faulty_credentials": "Ugyldig brukerlegitimasjon", @@ -29,7 +27,6 @@ "client_control": { "data": { "block_client": "Nettverkskontrollerte klienter", - "new_client": "Legg til ny klient for nettverkstilgangskontroll", "poe_clients": "Tillat POE-kontroll av klienter" }, "description": "Konfigurere klient-kontroller\n\nOpprette brytere for serienumre du \u00f8nsker \u00e5 kontrollere tilgang til nettverk for.", diff --git a/homeassistant/components/unifi/translations/pl.json b/homeassistant/components/unifi/translations/pl.json index 27155ff7b5c..c062d392911 100644 --- a/homeassistant/components/unifi/translations/pl.json +++ b/homeassistant/components/unifi/translations/pl.json @@ -1,23 +1,21 @@ { "config": { "abort": { - "already_configured": "Witryna kontrolera jest ju\u017c skonfigurowana.", - "no_local_user": "Nie znaleziono lokalnego u\u017cytkownika, skonfiguruj konto lokalne na kontrolerze i spr\u00f3buj ponownie.", - "user_privilege": "U\u017cytkownik musi by\u0107 administratorem" + "already_configured": "Witryna kontrolera jest ju\u017c skonfigurowana." }, "error": { - "faulty_credentials": "B\u0142\u0119dne dane uwierzytelniaj\u0105ce", - "service_unavailable": "Brak dost\u0119pnych us\u0142ug", + "faulty_credentials": "Niepoprawne uwierzytelnienie.", + "service_unavailable": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", "unknown_client_mac": "Brak klienta z tym adresem MAC" }, "step": { "user": { "data": { - "host": "[%key_id:common::config_flow::data::host%]", - "password": "[%key_id:common::config_flow::data::password%]", - "port": "[%key_id:common::config_flow::data::port%]", + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o", + "port": "Port", "site": "Identyfikator witryny", - "username": "[%key_id:common::config_flow::data::username%]", + "username": "Nazwa u\u017cytkownika", "verify_ssl": "Kontroler u\u017cywa prawid\u0142owego certyfikatu" }, "title": "Konfiguracja kontrolera UniFi" @@ -29,7 +27,6 @@ "client_control": { "data": { "block_client": "Klienci z kontrol\u0105 dost\u0119pu do sieci", - "new_client": "Dodaj nowego klienta do kontroli dost\u0119pu do sieci", "poe_clients": "Zezwalaj na kontrol\u0119 POE klient\u00f3w" }, "description": "Konfigurowanie kontroli klienta\n\nUtw\u00f3rz prze\u0142\u0105czniki dla numer\u00f3w seryjnych, dla kt\u00f3rych chcesz kontrolowa\u0107 dost\u0119p do sieci.", diff --git a/homeassistant/components/unifi/translations/pt-BR.json b/homeassistant/components/unifi/translations/pt-BR.json index 6372ed941db..67b39f07f66 100644 --- a/homeassistant/components/unifi/translations/pt-BR.json +++ b/homeassistant/components/unifi/translations/pt-BR.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "O site de controle j\u00e1 est\u00e1 configurado", - "user_privilege": "O usu\u00e1rio precisa ser administrador" + "already_configured": "O site de controle j\u00e1 est\u00e1 configurado" }, "error": { "faulty_credentials": "Credenciais do usu\u00e1rio inv\u00e1lidas", - "service_unavailable": "Servi\u00e7o indispon\u00edvel" + "service_unavailable": "Servi\u00e7o indispon\u00edvel", + "unknown_client_mac": "Nenhum cliente dispon\u00edvel nesse endere\u00e7o MAC" }, "step": { "user": { @@ -24,6 +24,13 @@ }, "options": { "step": { + "client_control": { + "data": { + "block_client": "Clientes com acesso controlado \u00e0 rede" + }, + "description": "Configurar controles do cliente \n\nCrie comutadores para os n\u00fameros de s\u00e9rie para os quais deseja controlar o acesso \u00e0 rede.", + "title": "UniFi op\u00e7\u00f5es de 2/3" + }, "device_tracker": { "data": { "detection_time": "Tempo em segundos desde a \u00faltima vez que foi visto at\u00e9 ser considerado afastado", @@ -37,6 +44,9 @@ "one": "um", "other": "uns" } + }, + "simple_options": { + "description": "Configurar integra\u00e7\u00e3o UniFi" } } } diff --git a/homeassistant/components/unifi/translations/pt.json b/homeassistant/components/unifi/translations/pt.json index 123385313e9..354870a0d51 100644 --- a/homeassistant/components/unifi/translations/pt.json +++ b/homeassistant/components/unifi/translations/pt.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "O site do controlador j\u00e1 se encontra configurado", - "user_privilege": "Utilizador tem que ser administrador" + "already_configured": "O site do controlador j\u00e1 se encontra configurado" }, "error": { "faulty_credentials": "Credenciais do utilizador erradas", diff --git a/homeassistant/components/unifi/translations/ro.json b/homeassistant/components/unifi/translations/ro.json index 090aeab1a7c..03a05564c27 100644 --- a/homeassistant/components/unifi/translations/ro.json +++ b/homeassistant/components/unifi/translations/ro.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "user_privilege": "Utilizatorul trebuie s\u0103 fie administrator" - }, "error": { "faulty_credentials": "Credentiale utilizator invalide", "service_unavailable": "Nici un serviciu disponibil" diff --git a/homeassistant/components/unifi/translations/ru.json b/homeassistant/components/unifi/translations/ru.json index f2e404a04fc..eecd5b53c56 100644 --- a/homeassistant/components/unifi/translations/ru.json +++ b/homeassistant/components/unifi/translations/ru.json @@ -1,9 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "no_local_user": "\u041d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c, \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u0443\u044e \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c \u043d\u0430 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0435 \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443.", - "user_privilege": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u043e\u043c." + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "error": { "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", @@ -29,7 +27,6 @@ "client_control": { "data": { "block_client": "\u041a\u043b\u0438\u0435\u043d\u0442\u044b \u0441 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u043c \u0441\u0435\u0442\u0435\u0432\u043e\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430", - "new_client": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043d\u043e\u0432\u043e\u0433\u043e \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u0434\u043b\u044f \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044f \u0441\u0435\u0442\u0435\u0432\u043e\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430", "poe_clients": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c POE \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f.\n\n\u0421\u043e\u0437\u0434\u0430\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u0438 \u0434\u043b\u044f \u0441\u0435\u0440\u0438\u0439\u043d\u044b\u0445 \u043d\u043e\u043c\u0435\u0440\u043e\u0432, \u0434\u043b\u044f \u043a\u043e\u0442\u043e\u0440\u044b\u0445 \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0441\u0435\u0442\u0438.", @@ -38,6 +35,7 @@ "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\".", + "ignore_wired_bug": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043b\u043e\u0433\u0438\u043a\u0443 \u043e\u0448\u0438\u0431\u043a\u0438 \u0434\u043b\u044f \u043d\u0435 \u0431\u0435\u0441\u043f\u0440\u043e\u0432\u043e\u0434\u043d\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 UniFi", "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)", diff --git a/homeassistant/components/unifi/translations/sl.json b/homeassistant/components/unifi/translations/sl.json index 7a5a79e252c..e0acdc13079 100644 --- a/homeassistant/components/unifi/translations/sl.json +++ b/homeassistant/components/unifi/translations/sl.json @@ -1,9 +1,7 @@ { "config": { "abort": { - "already_configured": "Nadzornik je \u017ee konfiguriran", - "no_local_user": "Nobenega lokalnega uporabnika ni mogo\u010de najti, konfigurirajte lokalni ra\u010dun na krmilniku in poskusite znova", - "user_privilege": "Uporabnik mora biti skrbnik" + "already_configured": "Nadzornik je \u017ee konfiguriran" }, "error": { "faulty_credentials": "Napa\u010dni uporabni\u0161ki podatki", @@ -29,7 +27,6 @@ "client_control": { "data": { "block_client": "Odjemalci pod nadzorom dostopa do omre\u017eja", - "new_client": "Dodajte novega odjemalca za nadzor dostopa do omre\u017eja", "poe_clients": "Dovoli POE nadzor strank" }, "description": "Konfigurirajte nadzor odjemalcev \n\n Ustvarite stikala za serijske \u0161tevilke, za katere \u017eelite nadzirati dostop do omre\u017eja.", diff --git a/homeassistant/components/unifi/translations/sv.json b/homeassistant/components/unifi/translations/sv.json index d3dd18a28e0..a0388cb7611 100644 --- a/homeassistant/components/unifi/translations/sv.json +++ b/homeassistant/components/unifi/translations/sv.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Controller-platsen \u00e4r redan konfigurerad", - "user_privilege": "Anv\u00e4ndaren m\u00e5ste vara administrat\u00f6r" + "already_configured": "Controller-platsen \u00e4r redan konfigurerad" }, "error": { "faulty_credentials": "Felaktiga anv\u00e4ndaruppgifter", diff --git a/homeassistant/components/unifi/translations/zh-Hans.json b/homeassistant/components/unifi/translations/zh-Hans.json index 402d8277bc7..7fe1b741bd5 100644 --- a/homeassistant/components/unifi/translations/zh-Hans.json +++ b/homeassistant/components/unifi/translations/zh-Hans.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "\u63a7\u5236\u5668\u7ad9\u70b9\u5df2\u914d\u7f6e\u5b8c\u6210", - "user_privilege": "\u7528\u6237\u987b\u4e3a\u7ba1\u7406\u5458" + "already_configured": "\u63a7\u5236\u5668\u7ad9\u70b9\u5df2\u914d\u7f6e\u5b8c\u6210" }, "error": { "faulty_credentials": "\u9519\u8bef\u7684\u7528\u6237\u51ed\u636e", diff --git a/homeassistant/components/unifi/translations/zh-Hant.json b/homeassistant/components/unifi/translations/zh-Hant.json index c5d44c13e73..7ba51c9b621 100644 --- a/homeassistant/components/unifi/translations/zh-Hant.json +++ b/homeassistant/components/unifi/translations/zh-Hant.json @@ -1,9 +1,7 @@ { "config": { "abort": { - "already_configured": "\u63a7\u5236\u5668\u4f4d\u5740\u5df2\u7d93\u8a2d\u5b9a", - "no_local_user": "\u627e\u4e0d\u5230\u672c\u5730\u4f7f\u7528\u8005\u3001\u65bc\u63a7\u5236\u5668\u4e0a\u8a2d\u5b9a\u4e00\u7d44\u672c\u5730\u5e33\u865f\u4e26\u518d\u8a66\u4e00\u6b21", - "user_privilege": "\u4f7f\u7528\u8005\u5fc5\u9808\u70ba\u7ba1\u7406\u54e1\u8eab\u4efd" + "already_configured": "\u63a7\u5236\u5668\u4f4d\u5740\u5df2\u7d93\u8a2d\u5b9a" }, "error": { "faulty_credentials": "\u9a57\u8b49\u78bc\u7121\u6548", @@ -29,7 +27,6 @@ "client_control": { "data": { "block_client": "\u7db2\u8def\u5b58\u53d6\u63a7\u5236\u5ba2\u6236\u7aef", - "new_client": "\u65b0\u589e\u9396\u8981\u63a7\u5236\u7db2\u8def\u5b58\u53d6\u7684\u5ba2\u6236\u7aef", "poe_clients": "\u5141\u8a31 POE \u63a7\u5236\u5ba2\u6236\u7aef" }, "description": "\u8a2d\u5b9a\u5ba2\u6236\u7aef\u63a7\u5236\n\n\u65b0\u589e\u9396\u8981\u63a7\u5236\u7db2\u8def\u5b58\u53d6\u7684\u958b\u95dc\u5e8f\u865f\u3002", diff --git a/homeassistant/components/upb/translations/ca.json b/homeassistant/components/upb/translations/ca.json index b54e2816572..92785937145 100644 --- a/homeassistant/components/upb/translations/ca.json +++ b/homeassistant/components/upb/translations/ca.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "No s'ha pogut connectar a UPB PIM, torna-ho a provar.", - "invalid_upb_file": "El fitxer d\u2019exportaci\u00f3 UPB UPStart no hi \u00e9s o \u00e9s erroni, comprova el nom i la ruta del fitxer.", + "invalid_upb_file": "El fitxer d'exportaci\u00f3 UPB UPStart no hi \u00e9s o \u00e9s erroni, comprova el nom i la ruta del fitxer.", "unknown": "Error inesperat." }, "step": { @@ -15,6 +15,7 @@ "file_path": "Ruta i nom del fitxer d'exportaci\u00f3 UPStart UPB.", "protocol": "Protocol" }, + "description": "Connexi\u00f3 amb un m\u00f2dul Universal Powerline Bus Powerline Interface (UPB PIM). La cadena de car\u00e0cters (string) de l'adre\u00e7a ha de tenir el format: 'adre\u00e7a[:port]' per a 'TCP'. El port \u00e9s opcional, per defecte \u00e9s el 2101. Exemple: '192.168.1.42'. Per al protocol s\u00e8rie, l'adre\u00e7a ha de tenir el format 'tty[:baud]'. La velocitat en bauds \u00e9s opcional (4800 per defecte). Exemple: '/dev/ttyS1'.", "title": "Connexi\u00f3 amb UPB PIM" } } diff --git a/homeassistant/components/upb/translations/lb.json b/homeassistant/components/upb/translations/lb.json index 26a76f8ab2e..71f05f72581 100644 --- a/homeassistant/components/upb/translations/lb.json +++ b/homeassistant/components/upb/translations/lb.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "address_already_configured": "een UPB PIM mat d\u00ebser Adress ass scho konfigur\u00e9iert." + }, "error": { "cannot_connect": "Feeler beim verbannen mat UPB PIM, prob\u00e9iert w.e.g. nach emol.", "invalid_upb_file": "UPB UPStart export fichier feelt oder ong\u00eblteg, iwwerpr\u00e9if den numm a pad vum fichier.", @@ -9,6 +12,7 @@ "user": { "data": { "address": "Adress (Kuck Beschr\u00e9iwung uewen)", + "file_path": "Pad a Numm vum UPStart UPB Export Fichier.", "protocol": "Protokoll" }, "description": "Verbann een Universal Bus Powerline Interface Module (UPB PIM). D'Adresse muss an der der From 'adress[:port]' fir 'tcp' sinn. De Port ass optionell an als standard op 2101 gesat. Beispill: '192.168.1.42'. Fir de serielle Protokoll muss d'Adress an der form 'tty[:baud]' sinn. Baudrate ass optionell an standardm\u00e9isseg o p 4800. Beispill: '/dev/ttyS1'.", diff --git a/homeassistant/components/upb/translations/pl.json b/homeassistant/components/upb/translations/pl.json index a236c6fded2..abbda657dc5 100644 --- a/homeassistant/components/upb/translations/pl.json +++ b/homeassistant/components/upb/translations/pl.json @@ -1,7 +1,8 @@ { "config": { "error": { - "unknown": "[%key_id:common::config_flow::error::unknown%]" + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z UPB PIM, spr\u00f3buj ponownie.", + "unknown": "Nieoczekiwany b\u0142\u0105d." } } } \ No newline at end of file diff --git a/homeassistant/components/upb/translations/pt-BR.json b/homeassistant/components/upb/translations/pt-BR.json new file mode 100644 index 00000000000..7ce0f1c613a --- /dev/null +++ b/homeassistant/components/upb/translations/pt-BR.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "unknown": "Erro inesperado." + }, + "step": { + "user": { + "data": { + "address": "Endere\u00e7o (veja a descri\u00e7\u00e3o acima)", + "protocol": "Protocolo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py index 869e3c55271..0b53850733f 100644 --- a/homeassistant/components/updater/__init__.py +++ b/homeassistant/components/updater/__init__.py @@ -1,4 +1,5 @@ """Support to check for available updates.""" +import asyncio from datetime import timedelta from distutils.version import StrictVersion import logging @@ -105,7 +106,8 @@ async def async_setup(hass, config): update_interval=timedelta(days=1), ) - await coordinator.async_refresh() + # This can take up to 15s which can delay startup + asyncio.create_task(coordinator.async_refresh()) hass.async_create_task( discovery.async_load_platform(hass, "binary_sensor", DOMAIN, {}, config) diff --git a/homeassistant/components/updater/binary_sensor.py b/homeassistant/components/updater/binary_sensor.py index 5088856ce6d..5d45b368500 100644 --- a/homeassistant/components/updater/binary_sensor.py +++ b/homeassistant/components/updater/binary_sensor.py @@ -33,6 +33,8 @@ class UpdaterBinary(BinarySensorEntity): @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" + if not self.coordinator.data: + return None return self.coordinator.data.update_available @property @@ -48,6 +50,8 @@ class UpdaterBinary(BinarySensorEntity): @property def device_state_attributes(self) -> dict: """Return the optional state attributes.""" + if not self.coordinator.data: + return None data = {} if self.coordinator.data.release_notes: data[ATTR_RELEASE_NOTES] = self.coordinator.data.release_notes @@ -57,11 +61,9 @@ class UpdaterBinary(BinarySensorEntity): async def async_added_to_hass(self): """Register update dispatcher.""" - self.coordinator.async_add_listener(self.async_write_ha_state) - - async def async_will_remove_from_hass(self): - """When removed from hass.""" - self.coordinator.async_remove_listener(self.async_write_ha_state) + self.async_on_remove( + self.coordinator.async_add_listener(self.async_write_ha_state) + ) async def async_update(self): """Update the entity. diff --git a/homeassistant/components/upnp/translations/bg.json b/homeassistant/components/upnp/translations/bg.json index 7e85d64daa1..892d193dd6b 100644 --- a/homeassistant/components/upnp/translations/bg.json +++ b/homeassistant/components/upnp/translations/bg.json @@ -2,29 +2,12 @@ "config": { "abort": { "already_configured": "UPnP/IGD \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", - "incomplete_device": "\u0418\u0433\u043d\u043e\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043d\u0435\u043f\u044a\u043b\u043d\u043e UPnP \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", "no_devices_discovered": "\u041d\u044f\u043c\u0430 \u043e\u0442\u043a\u0440\u0438\u0442\u0438 UPnP/IGD", - "no_devices_found": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 UPnP/IGD \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430.", - "no_sensors_or_port_mapping": "\u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u0439\u0442\u0435 \u0441\u0435\u043d\u0437\u043e\u0440\u0438\u0442\u0435 \u0438\u043b\u0438 \u043f\u0440\u0435\u043d\u0430\u0441\u043e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 \u043f\u043e\u0440\u0442\u0430", - "single_instance_allowed": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 UPnP/IGD." + "no_devices_found": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 UPnP/IGD \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430." }, "error": { "one": "\u0433\u0440\u0435\u0448\u043a\u0430", "other": "\u0433\u0440\u0435\u0448\u043a\u0438" - }, - "step": { - "confirm": { - "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 UPnP/IGD?", - "title": "UPnP/IGD" - }, - "user": { - "data": { - "enable_port_mapping": "\u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043f\u0440\u0435\u043d\u0430\u0441\u043e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 \u043f\u043e\u0440\u0442\u0430 \u0437\u0430 Home Assistant", - "enable_sensors": "\u0414\u043e\u0431\u0430\u0432\u044f\u043d\u0435 \u043d\u0430 \u0442\u0440\u0430\u0444\u0438\u0447\u043d\u0438 \u0441\u0435\u043d\u0437\u043e\u0440\u0438", - "igd": "UPnP/IGD" - }, - "title": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u043e\u043f\u0446\u0438\u0438 \u0437\u0430 UPnP/IGD" - } } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/ca.json b/homeassistant/components/upnp/translations/ca.json index e0824e5f9bb..e2bf1e92d99 100644 --- a/homeassistant/components/upnp/translations/ca.json +++ b/homeassistant/components/upnp/translations/ca.json @@ -2,11 +2,9 @@ "config": { "abort": { "already_configured": "UPnP/IGD ja est\u00e0 configurat", - "incomplete_device": "Ignorant el dispositiu incomplet UPnP", + "incomplete_discovery": "Descoberta incompleta", "no_devices_discovered": "No s'ha trobat cap UPnP/IGD", - "no_devices_found": "No s'han trobat dispositius UPnP/IGD a la xarxa.", - "no_sensors_or_port_mapping": "Activa, com a m\u00ednim, els sensors o l'assignaci\u00f3 de ports", - "single_instance_allowed": "Nom\u00e9s cal una sola configuraci\u00f3 de UPnP/IGD." + "no_devices_found": "No s'han trobat dispositius UPnP/IGD a la xarxa." }, "error": { "one": "un", @@ -14,22 +12,14 @@ }, "flow_title": "UPnP/IGD: {name}", "step": { - "confirm": { - "description": "Vols configurar UPnP/IGD?", - "title": "UPnP/IGD" - }, "ssdp_confirm": { "description": "Vols configurar aquest dispositiu UPnP/IGD?" }, "user": { "data": { - "enable_port_mapping": "Activa l'assignaci\u00f3 de ports per a Home Assistant", - "enable_sensors": "Afegeix sensors de tr\u00e0nsit", - "igd": "UPnP/IGD", "scan_interval": "Interval d'actualitzaci\u00f3 (en segons, m\u00ednim 30)", "usn": "Dispositiu" - }, - "title": "Opcions de configuraci\u00f3 d'UPnP/IGD" + } } } } diff --git a/homeassistant/components/upnp/translations/cs.json b/homeassistant/components/upnp/translations/cs.json index 9e3878bedd4..0c38ba9ab80 100644 --- a/homeassistant/components/upnp/translations/cs.json +++ b/homeassistant/components/upnp/translations/cs.json @@ -2,25 +2,8 @@ "config": { "abort": { "already_configured": "UPnP/IGD je ji\u017e nakonfigurov\u00e1no", - "incomplete_device": "Ignorov\u00e1n\u00ed ne\u00fapln\u00e9ho za\u0159\u00edzen\u00ed UPnP", "no_devices_discovered": "Nebyly zji\u0161t\u011bny \u017e\u00e1dn\u00e9 UPnP/IGD", - "no_devices_found": "V s\u00edti nejsou nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed UPnP/IGD.", - "no_sensors_or_port_mapping": "Povolte senzory nebo mapov\u00e1n\u00ed port\u016f", - "single_instance_allowed": "Povolena je pouze jedna instance UPnP/IGD." - }, - "step": { - "confirm": { - "description": "Chcete nastavit UPnP/IGD?", - "title": "UPnP/IGD" - }, - "user": { - "data": { - "enable_port_mapping": "Povolit mapov\u00e1n\u00ed port\u016f pro Home Assistant", - "enable_sensors": "P\u0159idejte dopravn\u00ed senzory", - "igd": "UPnP/IGD" - }, - "title": "Mo\u017enosti konfigurace pro UPnP/IGD" - } + "no_devices_found": "V s\u00edti nejsou nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed UPnP/IGD." } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/da.json b/homeassistant/components/upnp/translations/da.json index e45e84fffaf..f1880412006 100644 --- a/homeassistant/components/upnp/translations/da.json +++ b/homeassistant/components/upnp/translations/da.json @@ -2,29 +2,12 @@ "config": { "abort": { "already_configured": "UPnP/IGD er allerede konfigureret", - "incomplete_device": "Ignorerer ufuldst\u00e6ndig UPnP-enhed", "no_devices_discovered": "Ingen UPnP/IGD-enheder fundet.", - "no_devices_found": "Ingen UPnP/IGD enheder kunne findes p\u00e5 netv\u00e6rket.", - "no_sensors_or_port_mapping": "Aktiv\u00e9r enten sensorer eller porttilknytning", - "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af UPnP/IGD." + "no_devices_found": "Ingen UPnP/IGD enheder kunne findes p\u00e5 netv\u00e6rket." }, "error": { "one": "En", "other": "Anden" - }, - "step": { - "confirm": { - "description": "Er du sikker p\u00e5 at du vil konfigurere UPnP/IGD?", - "title": "UPnP/IGD" - }, - "user": { - "data": { - "enable_port_mapping": "Aktiv\u00e9r porttilknytning til Home Assistent", - "enable_sensors": "Tilf\u00f8j trafiksensorer", - "igd": "UPnP/IGD" - }, - "title": "Konfigurationsindstillinger for UPnP/IGD" - } } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/de.json b/homeassistant/components/upnp/translations/de.json index bfe95b10a39..c2defe875a3 100644 --- a/homeassistant/components/upnp/translations/de.json +++ b/homeassistant/components/upnp/translations/de.json @@ -2,11 +2,9 @@ "config": { "abort": { "already_configured": "UPnP/IGD ist bereits konfiguriert", - "incomplete_device": "Unvollst\u00e4ndiges UPnP-Ger\u00e4t wird ignoriert", + "incomplete_discovery": "Unvollst\u00e4ndige Suche", "no_devices_discovered": "Keine UPnP/IGDs entdeckt", - "no_devices_found": "Keine UPnP/IGD-Ger\u00e4te im Netzwerk gefunden.", - "no_sensors_or_port_mapping": "Aktiviere mindestens Sensoren oder Port-Mapping", - "single_instance_allowed": "Es ist nur eine einzige Konfiguration von UPnP/IGD erforderlich." + "no_devices_found": "Keine UPnP/IGD-Ger\u00e4te im Netzwerk gefunden." }, "error": { "one": "Ein", @@ -14,21 +12,13 @@ }, "flow_title": "UPnP/IGD: {name}", "step": { - "confirm": { - "description": "M\u00f6chtest du UPnP/IGD einrichten?", - "title": "UPnP/IGD" - }, "ssdp_confirm": { "description": "M\u00f6chten Sie dieses UPnP/IGD-Ger\u00e4t einrichten?" }, "user": { "data": { - "enable_port_mapping": "Aktiviere Port-Mapping f\u00fcr Home Assistant", - "enable_sensors": "Verkehrssensoren hinzuf\u00fcgen", - "igd": "UPnP/IGD", "usn": "Ger\u00e4t" - }, - "title": "Konfigurations-Optionen" + } } } } diff --git a/homeassistant/components/upnp/translations/en.json b/homeassistant/components/upnp/translations/en.json index d5436028cba..737954c2133 100644 --- a/homeassistant/components/upnp/translations/en.json +++ b/homeassistant/components/upnp/translations/en.json @@ -2,30 +2,20 @@ "config": { "abort": { "already_configured": "UPnP/IGD is already configured", - "incomplete_device": "Ignoring incomplete UPnP device", + "incomplete_discovery": "Incomplete discovery", "no_devices_discovered": "No UPnP/IGDs discovered", - "no_devices_found": "No UPnP/IGD devices found on the network.", - "no_sensors_or_port_mapping": "Enable at least sensors or port mapping", - "single_instance_allowed": "Only a single configuration of UPnP/IGD is necessary." + "no_devices_found": "No UPnP/IGD devices found on the network." }, "flow_title": "UPnP/IGD: {name}", "step": { - "confirm": { - "description": "Do you want to set up UPnP/IGD?", - "title": "UPnP/IGD" - }, "ssdp_confirm": { "description": "Do you want to set up this UPnP/IGD device?" }, "user": { "data": { - "enable_port_mapping": "Enable port mapping for Home Assistant", - "enable_sensors": "Add traffic sensors", - "igd": "UPnP/IGD", "scan_interval": "Update interval (seconds, minimal 30)", "usn": "Device" - }, - "title": "Configuration options" + } } } } diff --git a/homeassistant/components/upnp/translations/es-419.json b/homeassistant/components/upnp/translations/es-419.json index e516d978af0..8522ad16805 100644 --- a/homeassistant/components/upnp/translations/es-419.json +++ b/homeassistant/components/upnp/translations/es-419.json @@ -2,25 +2,8 @@ "config": { "abort": { "already_configured": "UPnP/IGD ya est\u00e1 configurado", - "incomplete_device": "Ignorar un dispositivo UPnP incompleto", "no_devices_discovered": "No se han descubierto UPnP/IGDs", - "no_devices_found": "No se encuentran dispositivos UPnP/IGD en la red.", - "no_sensors_or_port_mapping": "Habilitar al menos sensores o mapeo de puertos", - "single_instance_allowed": "S\u00f3lo se necesita una \u00fanica configuraci\u00f3n de UPnP/IGD." - }, - "step": { - "confirm": { - "description": "\u00bfDesea configurar UPnP/IGD?", - "title": "UPnP/IGD" - }, - "user": { - "data": { - "enable_port_mapping": "Habilitar la asignaci\u00f3n de puertos para Home Assistant", - "enable_sensors": "A\u00f1adir sensores de tr\u00e1fico", - "igd": "UPnP/IGD" - }, - "title": "Opciones de configuraci\u00f3n para UPnP/IGD" - } + "no_devices_found": "No se encuentran dispositivos UPnP/IGD en la red." } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/es.json b/homeassistant/components/upnp/translations/es.json index 6ca30b11339..308b10cb6db 100644 --- a/homeassistant/components/upnp/translations/es.json +++ b/homeassistant/components/upnp/translations/es.json @@ -2,11 +2,9 @@ "config": { "abort": { "already_configured": "UPnP / IGD ya est\u00e1 configurado", - "incomplete_device": "Ignorando el dispositivo UPnP incompleto", + "incomplete_discovery": "Descubrimiento incompleto", "no_devices_discovered": "No se descubrieron UPnP / IGDs", - "no_devices_found": "No se encuentran dispositivos UPnP/IGD en la red.", - "no_sensors_or_port_mapping": "Habilitar al menos sensores o mapeo de puertos", - "single_instance_allowed": "S\u00f3lo se necesita una configuraci\u00f3n de UPnP/IGD." + "no_devices_found": "No se encuentran dispositivos UPnP/IGD en la red." }, "error": { "one": "UNO", @@ -14,22 +12,14 @@ }, "flow_title": "UPnP / IGD: {name}", "step": { - "confirm": { - "description": "\u00bfDesea configurar UPnP/IGD?", - "title": "UPnP/IGD" - }, "ssdp_confirm": { "description": "\u00bfQuieres configurar este dispositivo UPnP/IGD?" }, "user": { "data": { - "enable_port_mapping": "Habilitar la asignaci\u00f3n de puertos para Home Assistant", - "enable_sensors": "A\u00f1adir sensores de tr\u00e1fico", - "igd": "UPnP / IGD", "scan_interval": "Intervalo de actualizaci\u00f3n (segundos, m\u00ednimo 30)", "usn": "Dispositivo" - }, - "title": "Opciones de configuraci\u00f3n" + } } } } diff --git a/homeassistant/components/upnp/translations/fi.json b/homeassistant/components/upnp/translations/fi.json index 0ad39900b72..dcd927ffd24 100644 --- a/homeassistant/components/upnp/translations/fi.json +++ b/homeassistant/components/upnp/translations/fi.json @@ -1,14 +1,8 @@ { "config": { "step": { - "confirm": { - "description": "Haluatko m\u00e4\u00e4ritt\u00e4\u00e4 UPnP/IGD:n?", - "title": "UPnP/IGD" - }, "user": { "data": { - "enable_sensors": "Lis\u00e4\u00e4 liikenneanturit", - "igd": "UPnP/IGD", "scan_interval": "P\u00e4ivitysv\u00e4li (sekuntia, v\u00e4hint\u00e4\u00e4n 30)", "usn": "Laite" } diff --git a/homeassistant/components/upnp/translations/fr.json b/homeassistant/components/upnp/translations/fr.json index 6baf9087b8e..e07bfbc6330 100644 --- a/homeassistant/components/upnp/translations/fr.json +++ b/homeassistant/components/upnp/translations/fr.json @@ -2,11 +2,8 @@ "config": { "abort": { "already_configured": "UPnP / IGD est d\u00e9j\u00e0 configur\u00e9", - "incomplete_device": "Ignorer un p\u00e9riph\u00e9rique UPnP incomplet", "no_devices_discovered": "Aucun UPnP / IGD d\u00e9couvert", - "no_devices_found": "Aucun p\u00e9riph\u00e9rique UPnP / IGD trouv\u00e9 sur le r\u00e9seau.", - "no_sensors_or_port_mapping": "Activer au moins les capteurs ou la cartographie des ports", - "single_instance_allowed": "Une seule configuration UPnP / IGD est n\u00e9cessaire." + "no_devices_found": "Aucun p\u00e9riph\u00e9rique UPnP / IGD trouv\u00e9 sur le r\u00e9seau." }, "error": { "one": "Vide", @@ -14,21 +11,13 @@ }, "flow_title": "UPnP/IGD: {name}", "step": { - "confirm": { - "description": "Voulez-vous configurer UPnP / IGD?", - "title": "UPnP / IGD" - }, "ssdp_confirm": { "description": "Voulez-vous configurer ce p\u00e9riph\u00e9rique UPnP/IGD?" }, "user": { "data": { - "enable_port_mapping": "Activer le mappage de port pour Home Assistant", - "enable_sensors": "Ajouter des capteurs de trafic", - "igd": "UPnP / IGD", "usn": "Appareil" - }, - "title": "Options de configuration pour UPnP / IGD" + } } } } diff --git a/homeassistant/components/upnp/translations/hu.json b/homeassistant/components/upnp/translations/hu.json index a8dc4ce854b..1baf3f0c11a 100644 --- a/homeassistant/components/upnp/translations/hu.json +++ b/homeassistant/components/upnp/translations/hu.json @@ -2,28 +2,12 @@ "config": { "abort": { "already_configured": "Az UPnP / IGD m\u00e1r konfigur\u00e1l\u00e1sra ker\u00fclt", - "incomplete_device": "A hi\u00e1nyos UPnP-eszk\u00f6z figyelmen k\u00edv\u00fcl hagy\u00e1sa", "no_devices_discovered": "Nem tal\u00e1ltam UPnP / IGD-ket", - "no_devices_found": "Nincsenek UPnPIGD eszk\u00f6z\u00f6k a h\u00e1l\u00f3zaton.", - "single_instance_allowed": "Csak egy UPnP / IGD konfigur\u00e1ci\u00f3 sz\u00fcks\u00e9ges." + "no_devices_found": "Nincsenek UPnPIGD eszk\u00f6z\u00f6k a h\u00e1l\u00f3zaton." }, "error": { "one": "hiba", "other": "" - }, - "step": { - "confirm": { - "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a UPnP/IGD-t?", - "title": "UPnP/IGD" - }, - "user": { - "data": { - "enable_port_mapping": "Enged\u00e9lyezd a port mappinget a Home Assistant sz\u00e1m\u00e1ra", - "enable_sensors": "Forgalom \u00e9rz\u00e9kel\u0151k hozz\u00e1ad\u00e1sa", - "igd": "UPnP/IGD" - }, - "title": "Az UPnP/IGD be\u00e1ll\u00edt\u00e1si lehet\u0151s\u00e9gei" - } } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/it.json b/homeassistant/components/upnp/translations/it.json index df13cc79d6f..dacb5023615 100644 --- a/homeassistant/components/upnp/translations/it.json +++ b/homeassistant/components/upnp/translations/it.json @@ -2,11 +2,9 @@ "config": { "abort": { "already_configured": "UPnP/IGD \u00e8 gi\u00e0 configurato", - "incomplete_device": "Ignorare il dispositivo UPnP incompleto", + "incomplete_discovery": "Individuazione incompleta", "no_devices_discovered": "Nessun UPnP/IGD trovato", - "no_devices_found": "Nessun dispositivo UPnP/IGD trovato in rete.", - "no_sensors_or_port_mapping": "Abilita almeno i sensori o la mappatura delle porte", - "single_instance_allowed": "\u00c8 necessaria una sola configurazione di UPnP/IGD." + "no_devices_found": "Nessun dispositivo UPnP/IGD trovato in rete." }, "error": { "one": "Vuoto", @@ -14,22 +12,18 @@ }, "flow_title": "UPnP/IGD: {name}", "step": { - "confirm": { - "description": "Vuoi configurare UPnP/IGD?", - "title": "UPnP/IGD" + "init": { + "one": "uno", + "other": "altro" }, "ssdp_confirm": { "description": "Vuoi configurare questo dispositivo UPnP/IGD?" }, "user": { "data": { - "enable_port_mapping": "Abilita il port mapping per Home Assistant", - "enable_sensors": "Aggiungi sensori di traffico", - "igd": "UPnP/IGD", "scan_interval": "Intervallo di aggiornamento (secondi, minimo 30)", "usn": "Dispositivo" - }, - "title": "Opzioni di configurazione" + } } } } diff --git a/homeassistant/components/upnp/translations/ko.json b/homeassistant/components/upnp/translations/ko.json index b3a0d822a2b..279d9f7f7ce 100644 --- a/homeassistant/components/upnp/translations/ko.json +++ b/homeassistant/components/upnp/translations/ko.json @@ -2,30 +2,20 @@ "config": { "abort": { "already_configured": "UPnP/IGD \uac00 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", - "incomplete_device": "\ubd88\uc644\uc804\ud55c UPnP \uae30\uae30 \ubb34\uc2dc\ud558\uae30", + "incomplete_discovery": "\uae30\uae30 \uac80\uc0c9\uc774 \uc644\uc804\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", "no_devices_discovered": "\ubc1c\uacac\ub41c UPnP/IGD \uac00 \uc5c6\uc2b5\ub2c8\ub2e4", - "no_devices_found": "UPnP/IGD \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", - "no_sensors_or_port_mapping": "\ucd5c\uc18c\ud55c \uc13c\uc11c \ud639\uc740 \ud3ec\ud2b8 \ub9e4\ud551\uc744 \ud65c\uc131\ud654 \ud574\uc57c \ud569\ub2c8\ub2e4", - "single_instance_allowed": "\ud558\ub098\uc758 UPnP/IGD \ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + "no_devices_found": "UPnP/IGD \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4." }, "flow_title": "UPnP/IGD: {name}", "step": { - "confirm": { - "description": "UPnP/IGD \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "UPnP/IGD" - }, "ssdp_confirm": { "description": "\uc774 UPnP/IGD \uae30\uae30\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" }, "user": { "data": { - "enable_port_mapping": "Home Assistant \ud3ec\ud2b8 \ub9e4\ud551 \ud65c\uc131\ud654", - "enable_sensors": "\ud2b8\ub798\ud53d \uc13c\uc11c \ucd94\uac00", - "igd": "UPnP/IGD", "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \uac04\uaca9 (\ucd08, \ucd5c\uc18c\uac12 30)", "usn": "\uae30\uae30" - }, - "title": "\uc635\uc158 \uad6c\uc131\ud558\uae30" + } } } } diff --git a/homeassistant/components/upnp/translations/lb.json b/homeassistant/components/upnp/translations/lb.json index e30bae93d06..710b788ae32 100644 --- a/homeassistant/components/upnp/translations/lb.json +++ b/homeassistant/components/upnp/translations/lb.json @@ -2,11 +2,9 @@ "config": { "abort": { "already_configured": "UPnP/IGD ass scho konfigur\u00e9iert", - "incomplete_device": "Ignor\u00e9iert onvollst\u00e4nnegen UPnP-Apparat", + "incomplete_discovery": "Entdeckung net komplett", "no_devices_discovered": "Keng UPnP/IGDs entdeckt", - "no_devices_found": "Keng UPnP/IGD Apparater am Netzwierk fonnt.", - "no_sensors_or_port_mapping": "Aktiv\u00e9ier op mannst Sensoren oder Port Mapping", - "single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun UPnP/IGD ass n\u00e9ideg." + "no_devices_found": "Keng UPnP/IGD Apparater am Netzwierk fonnt." }, "error": { "one": "Een", @@ -14,22 +12,14 @@ }, "flow_title": "UPnP/IGD: {name}", "step": { - "confirm": { - "description": "Soll UPnP/IGD konfigur\u00e9iert ginn?", - "title": "UPnP/IGD" - }, "ssdp_confirm": { "description": "Soll d\u00ebsen UPnP/IGD Apparat konfigur\u00e9iert ginn?" }, "user": { "data": { - "enable_port_mapping": "Port Mapping fir Home Assistant aktiv\u00e9ieren", - "enable_sensors": "Trafic Sensoren dob\u00e4isetzen", - "igd": "UPnP/IGD", "scan_interval": "Update Intervall (Sekonnen, minimum 30)", "usn": "Apparat" - }, - "title": "Konfiguratiouns Optiounen" + } } } } diff --git a/homeassistant/components/upnp/translations/nl.json b/homeassistant/components/upnp/translations/nl.json index c36e6252867..9b579127f8c 100644 --- a/homeassistant/components/upnp/translations/nl.json +++ b/homeassistant/components/upnp/translations/nl.json @@ -2,11 +2,9 @@ "config": { "abort": { "already_configured": "UPnP/IGD is al geconfigureerd", - "incomplete_device": "Onvolledig UPnP-apparaat negeren", + "incomplete_discovery": "Onvolledige ontdekking", "no_devices_discovered": "Geen UPnP'/IGD's ontdekt", - "no_devices_found": "Geen UPnP/IGD apparaten gevonden op het netwerk.", - "no_sensors_or_port_mapping": "Schakel ten minste sensoren of poorttoewijzing in", - "single_instance_allowed": "Er is slechts \u00e9\u00e9n configuratie van UPnP/IGD nodig." + "no_devices_found": "Geen UPnP/IGD apparaten gevonden op het netwerk." }, "error": { "one": "Een", @@ -14,21 +12,13 @@ }, "flow_title": "[%%]: {naam}", "step": { - "confirm": { - "description": "Wilt u UPnP/IGD instellen?", - "title": "UPnP/IGD" - }, "ssdp_confirm": { "description": "Wilt u [%%] instellen?" }, "user": { "data": { - "enable_port_mapping": "Poorttoewijzing voor Home Assistant inschakelen", - "enable_sensors": "Voeg verkeerssensoren toe", - "igd": "UPnP/IGD", "usn": "Apparaat" - }, - "title": "Configuratiemogelijkheden voor de UPnP/IGD" + } } } } diff --git a/homeassistant/components/upnp/translations/nn.json b/homeassistant/components/upnp/translations/nn.json index f521406b087..83565f89436 100644 --- a/homeassistant/components/upnp/translations/nn.json +++ b/homeassistant/components/upnp/translations/nn.json @@ -1,21 +1,8 @@ { "config": { - "abort": { - "no_sensors_or_port_mapping": "I det minste, aktiver sensor eller portkartlegging" - }, "error": { "one": "Ein", "other": "Andre" - }, - "step": { - "confirm": { - "title": "UPnP/IGD" - }, - "user": { - "data": { - "igd": "UPnP/IGD" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/no.json b/homeassistant/components/upnp/translations/no.json index 99eb7925b5c..28c2e099192 100644 --- a/homeassistant/components/upnp/translations/no.json +++ b/homeassistant/components/upnp/translations/no.json @@ -2,11 +2,9 @@ "config": { "abort": { "already_configured": "UPnP / IGD er allerede konfigurert", - "incomplete_device": "Ignorerer ufullstendig UPnP-enhet", + "incomplete_discovery": "Ufullstendig oppdagelse", "no_devices_discovered": "Ingen UPnP / IGDs oppdaget", - "no_devices_found": "Ingen UPnP / IGD-enheter funnet p\u00e5 nettverket.", - "no_sensors_or_port_mapping": "Aktiver minst sensorer eller port mapping", - "single_instance_allowed": "Bare en konfigurasjon av UPnP / IGD er n\u00f8dvendig." + "no_devices_found": "Ingen UPnP / IGD-enheter funnet p\u00e5 nettverket." }, "error": { "few": "f\u00e5", @@ -18,22 +16,14 @@ }, "flow_title": "UPnP/IGD: {name}", "step": { - "confirm": { - "description": "\u00d8nsker du \u00e5 sette opp UPnP / IGD?", - "title": "" - }, "ssdp_confirm": { "description": "\u00d8nsker du \u00e5 sette opp denne UPnP/IGD-enheten?" }, "user": { "data": { - "enable_port_mapping": "Aktiver port mapping for Home Assistant", - "enable_sensors": "Legg til trafikk sensorer", - "igd": "UPnP / IGD", "scan_interval": "Oppdateringsintervall (sekunder, minimum 30)", "usn": "Enhet" - }, - "title": "Konfigurasjonsalternativer" + } } } } diff --git a/homeassistant/components/upnp/translations/pl.json b/homeassistant/components/upnp/translations/pl.json index 08e20e75679..55d0abbba48 100644 --- a/homeassistant/components/upnp/translations/pl.json +++ b/homeassistant/components/upnp/translations/pl.json @@ -2,29 +2,20 @@ "config": { "abort": { "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.", - "no_sensors_or_port_mapping": "W\u0142\u0105cz przynajmniej sensory lub mapowanie port\u00f3w", - "single_instance_allowed": "Wymagana jest tylko jedna konfiguracja UPnP/IGD." + "incomplete_discovery": "Wykrywanie niekompletne", + "no_devices_discovered": "Nie wykryto urz\u0105dze\u0144 UPnP/IGD.", + "no_devices_found": "Nie znaleziono w sieci urz\u0105dze\u0144 UPnP/IGD." }, "flow_title": "UPnP/IGD: {name}", "step": { - "confirm": { - "description": "Czy chcesz skonfigurowa\u0107 UPnP/IGD?", - "title": "UPnP/IGD" - }, "ssdp_confirm": { "description": "Czy chcesz skonfigurowa\u0107 to urz\u0105dzenie UPnP/IGD?" }, "user": { "data": { - "enable_port_mapping": "W\u0142\u0105cz mapowanie port\u00f3w dla Home Assistant'a", - "enable_sensors": "Dodaj sensor ruchu sieciowego", - "igd": "UPnP/IGD", + "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji (sekundy, minimum 30)", "usn": "Urz\u0105dzenie" - }, - "title": "Opcje konfiguracji dla UPnP/IGD" + } } } } diff --git a/homeassistant/components/upnp/translations/pt-BR.json b/homeassistant/components/upnp/translations/pt-BR.json index d472fa18834..f9c75ffec25 100644 --- a/homeassistant/components/upnp/translations/pt-BR.json +++ b/homeassistant/components/upnp/translations/pt-BR.json @@ -2,24 +2,16 @@ "config": { "abort": { "already_configured": "UPnP / IGD j\u00e1 est\u00e1 configurado", - "incomplete_device": "Ignorando o dispositivo UPnP incompleto", + "incomplete_discovery": "Descoberta incompleta", "no_devices_discovered": "Nenhum UPnP/IGD descoberto", - "no_devices_found": "Nenhum dispositivo UPnP/IGD encontrado na rede.", - "no_sensors_or_port_mapping": "Ative pelo menos sensores ou mapeamento de porta", - "single_instance_allowed": "Apenas uma configura\u00e7\u00e3o do UPnP/IGD \u00e9 necess\u00e1ria." + "no_devices_found": "Nenhum dispositivo UPnP/IGD encontrado na rede." }, "step": { - "confirm": { - "description": "Deseja configurar o UPnP/IGD?", - "title": "UPnP/IGD" - }, "user": { "data": { - "enable_port_mapping": "Ativar o mapeamento de porta para o Home Assistant", - "enable_sensors": "Adicionar sensores de tr\u00e1fego", - "igd": "UPnP/IGD" - }, - "title": "Op\u00e7\u00f5es de configura\u00e7\u00e3o para o UPnP/IGD" + "scan_interval": "Intervalo de atualiza\u00e7\u00e3o (segundos, m\u00ednimo 30)", + "usn": "Dispositivo" + } } } } diff --git a/homeassistant/components/upnp/translations/pt.json b/homeassistant/components/upnp/translations/pt.json index ea501c2c263..cb08c0f52a6 100644 --- a/homeassistant/components/upnp/translations/pt.json +++ b/homeassistant/components/upnp/translations/pt.json @@ -2,29 +2,12 @@ "config": { "abort": { "already_configured": "UPnP/IGD j\u00e1 est\u00e1 configurado", - "incomplete_device": "Dispositivos UPnP incompletos ignorados", "no_devices_discovered": "Nenhum UPnP/IGDs descoberto", - "no_devices_found": "Nenhum dispositivo UPnP / IGD encontrado na rede.", - "no_sensors_or_port_mapping": "Ative pelo menos os sensores ou o mapeamento de porta", - "single_instance_allowed": "Apenas uma \u00fanica configura\u00e7\u00e3o do UPnP/IGD \u00e9 necess\u00e1ria." + "no_devices_found": "Nenhum dispositivo UPnP / IGD encontrado na rede." }, "error": { "one": "um", "other": "v\u00e1rios" - }, - "step": { - "confirm": { - "description": "Deseja configurar o UPnP / IGD?", - "title": "UPnP/IGD" - }, - "user": { - "data": { - "enable_port_mapping": "Ativar o mapeamento de porta para o Home Assistant", - "enable_sensors": "Adicionar sensores de tr\u00e1fego", - "igd": "UPnP/IGD" - }, - "title": "Op\u00e7\u00f5es de configura\u00e7\u00e3o para o UPnP/IGD" - } } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/ro.json b/homeassistant/components/upnp/translations/ro.json index 33342807995..230e285cfa4 100644 --- a/homeassistant/components/upnp/translations/ro.json +++ b/homeassistant/components/upnp/translations/ro.json @@ -8,16 +8,6 @@ "few": "", "one": "Unul", "other": "" - }, - "step": { - "user": { - "data": { - "enable_port_mapping": "Activa\u021bi maparea porturilor pentru Home Assistant", - "enable_sensors": "Ad\u0103uga\u021bi senzori de trafic", - "igd": "UPnP/IGD" - }, - "title": "Op\u021biuni de configurare pentru UPnP/IGD" - } } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/ru.json b/homeassistant/components/upnp/translations/ru.json index eaf75d85b09..cea6b66db2b 100644 --- a/homeassistant/components/upnp/translations/ru.json +++ b/homeassistant/components/upnp/translations/ru.json @@ -2,11 +2,9 @@ "config": { "abort": { "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "incomplete_device": "\u0418\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043d\u0435\u043f\u043e\u043b\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 UPnP.", - "no_devices_discovered": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e UPnP / IGD.", - "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 \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." + "incomplete_discovery": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043f\u0440\u043e\u0446\u0435\u0441\u0441.", + "no_devices_discovered": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 UPnP / IGD \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u044b.", + "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." }, "error": { "few": "\u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e", @@ -16,22 +14,14 @@ }, "flow_title": "UPnP/IGD: {name}", "step": { - "confirm": { - "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c UPnP / IGD?", - "title": "UPnP / IGD" - }, "ssdp_confirm": { "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u044d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e UPnP / IGD?" }, "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 \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", "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445, \u043c\u0438\u043d\u0438\u043c\u0443\u043c 30)", "usn": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" - }, - "title": "UPnP / IGD" + } } } } diff --git a/homeassistant/components/upnp/translations/sl.json b/homeassistant/components/upnp/translations/sl.json index 1813461eadd..88b339b7372 100644 --- a/homeassistant/components/upnp/translations/sl.json +++ b/homeassistant/components/upnp/translations/sl.json @@ -2,11 +2,8 @@ "config": { "abort": { "already_configured": "UPnP/IGD je \u017ee konfiguriran", - "incomplete_device": "Ignoriranje nepopolnih UPnP naprav", "no_devices_discovered": "Ni odkritih UPnP/IGD naprav", - "no_devices_found": "Naprav UPnP/IGD ni mogo\u010de najti v omre\u017eju.", - "no_sensors_or_port_mapping": "Omogo\u010dite vsaj senzorje ali preslikavo vrat (port mapping)", - "single_instance_allowed": "Potrebna je samo ena konfiguracija UPnp/IGD." + "no_devices_found": "Naprav UPnP/IGD ni mogo\u010de najti v omre\u017eju." }, "error": { "few": "nekaj", @@ -16,21 +13,13 @@ }, "flow_title": "UPnP/IGD: {name}", "step": { - "confirm": { - "description": "Ali \u017eelite nastaviti UPnp/IGD?", - "title": "UPnP/IGD" - }, "ssdp_confirm": { "description": "Ali \u017eelite nastaviti to UPnP/IGD napravo?" }, "user": { "data": { - "enable_port_mapping": "Omogo\u010dajo preslikavo vrat (port mapping) za Home Assistant-a", - "enable_sensors": "Dodaj prometne senzorje", - "igd": "UPnP/IGD", "usn": "Naprava" - }, - "title": "Mo\u017enosti konfiguracije za UPnP/IGD" + } } } } diff --git a/homeassistant/components/upnp/translations/sv.json b/homeassistant/components/upnp/translations/sv.json index f53c9294ea9..a7c95b6fc08 100644 --- a/homeassistant/components/upnp/translations/sv.json +++ b/homeassistant/components/upnp/translations/sv.json @@ -2,29 +2,18 @@ "config": { "abort": { "already_configured": "UPnP/IGD \u00e4r redan konfigurerad", - "incomplete_device": "Ignorera ofullst\u00e4ndig UPnP-enhet", "no_devices_discovered": "Inga UPnP/IGDs uppt\u00e4cktes", - "no_devices_found": "Inga UPnP/IGD-enheter hittades p\u00e5 n\u00e4tverket.", - "no_sensors_or_port_mapping": "Aktivera minst sensorer eller portmappning", - "single_instance_allowed": "Endast en enda konfiguration av UPnP/IGD \u00e4r n\u00f6dv\u00e4ndig." + "no_devices_found": "Inga UPnP/IGD-enheter hittades p\u00e5 n\u00e4tverket." }, "error": { "one": "En", "other": "Andra" }, "step": { - "confirm": { - "description": "Vill du konfigurera UPnP/IGD?", - "title": "UPnP/IGD" - }, "user": { "data": { - "enable_port_mapping": "Aktivera portmappning f\u00f6r Home Assistant", - "enable_sensors": "L\u00e4gg till trafiksensorer", - "igd": "UPnP/IGD", "usn": "Enheten" - }, - "title": "Konfigurationsalternativ f\u00f6r UPnP/IGD" + } } } } diff --git a/homeassistant/components/upnp/translations/uk.json b/homeassistant/components/upnp/translations/uk.json index 44268a5b5b5..c1a9f1fadcf 100644 --- a/homeassistant/components/upnp/translations/uk.json +++ b/homeassistant/components/upnp/translations/uk.json @@ -2,17 +2,7 @@ "config": { "abort": { "already_configured": "UPnP/IGD \u0432\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0454\u043d\u043e", - "no_devices_discovered": "\u041d\u0435 \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043e UPnP/IGD", - "no_sensors_or_port_mapping": "\u0423\u0432\u0456\u043c\u043a\u043d\u0456\u0442\u044c \u043f\u0440\u0438\u043d\u0430\u0439\u043c\u043d\u0456 \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0430\u0431\u043e \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u043d\u044f \u043f\u043e\u0440\u0442\u0456\u0432" - }, - "step": { - "user": { - "data": { - "enable_port_mapping": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u043d\u044f \u043f\u043e\u0440\u0442\u0456\u0432 \u0434\u043b\u044f Home Assistant", - "enable_sensors": "\u0414\u043e\u0434\u0430\u0442\u0438 \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0442\u0440\u0430\u0444\u0456\u043a\u0443" - }, - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0457\u0442\u0438 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 UPnP/IGD" - } + "no_devices_discovered": "\u041d\u0435 \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043e UPnP/IGD" } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/zh-Hans.json b/homeassistant/components/upnp/translations/zh-Hans.json index 80216d7a18e..7919d0f4591 100644 --- a/homeassistant/components/upnp/translations/zh-Hans.json +++ b/homeassistant/components/upnp/translations/zh-Hans.json @@ -2,25 +2,14 @@ "config": { "abort": { "already_configured": "UPnP/IGD \u5df2\u914d\u7f6e\u5b8c\u6210", - "incomplete_device": "\u5ffd\u7565\u4e0d\u5b8c\u6574\u7684 UPnP \u8bbe\u5907", "no_devices_discovered": "\u672a\u53d1\u73b0 UPnP/IGD", - "no_devices_found": "\u6ca1\u6709\u5728\u7f51\u7edc\u4e0a\u627e\u5230 UPnP/IGD \u8bbe\u5907\u3002", - "no_sensors_or_port_mapping": "\u81f3\u5c11\u542f\u7528\u4f20\u611f\u5668\u6216\u7aef\u53e3\u6620\u5c04", - "single_instance_allowed": "UPnP/IGD \u53ea\u9700\u8981\u914d\u7f6e\u4e00\u6b21\u3002" + "no_devices_found": "\u6ca1\u6709\u5728\u7f51\u7edc\u4e0a\u627e\u5230 UPnP/IGD \u8bbe\u5907\u3002" }, "step": { - "confirm": { - "description": "\u60a8\u60f3\u8981\u914d\u7f6e UPnP/IGD \u5417\uff1f", - "title": "UPnP/IGD" - }, "user": { "data": { - "enable_port_mapping": "\u4e3a Home Assistant \u542f\u7528\u7aef\u53e3\u6620\u5c04", - "enable_sensors": "\u6dfb\u52a0\u6d41\u91cf\u4f20\u611f\u5668", - "igd": "UPnP/IGD", "usn": "\u8bbe\u5907" - }, - "title": "UPnP/IGD \u7684\u914d\u7f6e\u9009\u9879" + } } } } diff --git a/homeassistant/components/upnp/translations/zh-Hant.json b/homeassistant/components/upnp/translations/zh-Hant.json index c45157ff77d..c89ff8af971 100644 --- a/homeassistant/components/upnp/translations/zh-Hant.json +++ b/homeassistant/components/upnp/translations/zh-Hant.json @@ -2,30 +2,20 @@ "config": { "abort": { "already_configured": "UPnP/IGD \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "incomplete_device": "\u5ffd\u7565\u4e0d\u76f8\u5bb9 UPnP \u8a2d\u5099", + "incomplete_discovery": "\u672a\u5b8c\u6210\u63a2\u7d22", "no_devices_discovered": "\u672a\u641c\u5c0b\u5230 UPnP/IGD", - "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 UPnP/IGD \u8a2d\u5099\u3002", - "no_sensors_or_port_mapping": "\u81f3\u5c11\u958b\u555f\u611f\u61c9\u5668\u6216\u901a\u8a0a\u57e0\u8f49\u767c", - "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21 UPnP/IGD \u5373\u53ef\u3002" + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 UPnP/IGD \u8a2d\u5099\u3002" }, "flow_title": "UPnP/IGD\uff1a{name}", "step": { - "confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a UPnP/IGD\uff1f", - "title": "UPnP/IGD" - }, "ssdp_confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a UPnP/IGD \u8a2d\u5099\uff1f" }, "user": { "data": { - "enable_port_mapping": "\u958b\u555f Home Assistant \u901a\u8a0a\u57e0\u8f49\u767c", - "enable_sensors": "\u65b0\u589e\u6d41\u91cf\u611f\u61c9\u5668", - "igd": "UPnP/IGD", "scan_interval": "\u66f4\u65b0\u9593\u9694\uff08\u79d2\u3001\u6700\u5c11 30 \u79d2\uff09", "usn": "\u8a2d\u5099" - }, - "title": "\u8a2d\u5b9a\u9078\u9805" + } } } } diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 111fa64f988..ca5caec7ac1 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -147,6 +147,11 @@ class _BaseVacuum(Entity): """Return the battery level of the vacuum cleaner.""" return None + @property + def battery_icon(self): + """Return the battery icon for the vacuum cleaner.""" + raise NotImplementedError() + @property def fan_speed(self): """Return the fan speed of the vacuum cleaner.""" @@ -157,6 +162,26 @@ class _BaseVacuum(Entity): """Get the list of available fan speed steps of the vacuum cleaner.""" raise NotImplementedError() + @property + def capability_attributes(self): + """Return capability attributes.""" + if self.supported_features & SUPPORT_FAN_SPEED: + return {ATTR_FAN_SPEED_LIST: self.fan_speed_list} + + @property + def state_attributes(self): + """Return the state attributes of the vacuum cleaner.""" + data = {} + + if self.supported_features & SUPPORT_BATTERY: + data[ATTR_BATTERY_LEVEL] = self.battery_level + data[ATTR_BATTERY_ICON] = self.battery_icon + + if self.supported_features & SUPPORT_FAN_SPEED: + data[ATTR_FAN_SPEED] = self.fan_speed + + return data + def stop(self, **kwargs): """Stop the vacuum cleaner.""" raise NotImplementedError() @@ -246,27 +271,14 @@ class VacuumEntity(_BaseVacuum, ToggleEntity): battery_level=self.battery_level, charging=charging ) - @property - def capability_attributes(self): - """Return capability attributes.""" - if self.supported_features & SUPPORT_FAN_SPEED: - return {ATTR_FAN_SPEED_LIST: self.fan_speed_list} - @property def state_attributes(self): """Return the state attributes of the vacuum cleaner.""" - data = {} + data = super().state_attributes - if self.status is not None: + if self.supported_features & SUPPORT_STATUS: data[ATTR_STATUS] = self.status - if self.battery_level is not None: - data[ATTR_BATTERY_LEVEL] = self.battery_level - data[ATTR_BATTERY_ICON] = self.battery_icon - - if self.fan_speed is not None: - data[ATTR_FAN_SPEED] = self.fan_speed - return data def turn_on(self, **kwargs): @@ -337,27 +349,6 @@ class StateVacuumEntity(_BaseVacuum): battery_level=self.battery_level, charging=charging ) - @property - def capability_attributes(self): - """Return capability attributes.""" - if self.supported_features & SUPPORT_FAN_SPEED: - return {ATTR_FAN_SPEED_LIST: self.fan_speed_list} - - @property - def state_attributes(self): - """Return the state attributes of the vacuum cleaner.""" - data = {} - - if self.battery_level is not None: - data[ATTR_BATTERY_LEVEL] = self.battery_level - data[ATTR_BATTERY_ICON] = self.battery_icon - - if self.fan_speed is not None: - data[ATTR_FAN_SPEED] = self.fan_speed - data[ATTR_FAN_SPEED_LIST] = self.fan_speed_list - - return data - def start(self): """Start or resume the cleaning task.""" raise NotImplementedError() diff --git a/homeassistant/components/vacuum/translations/ca.json b/homeassistant/components/vacuum/translations/ca.json index f52d7e2536b..5f8f234d808 100644 --- a/homeassistant/components/vacuum/translations/ca.json +++ b/homeassistant/components/vacuum/translations/ca.json @@ -19,10 +19,10 @@ "docked": "Aparcat", "error": "Error", "idle": "Inactiu", - "off": "Apagat", - "on": "Enc\u00e8s", - "paused": "Pausat", - "returning": "Retornant a la base" + "off": "OFF", + "on": "ON", + "paused": "Pausat/da", + "returning": "Retornant a base" } }, "title": "Aspirador" diff --git a/homeassistant/components/velux/cover.py b/homeassistant/components/velux/cover.py index 901946b3245..e8e210c1e53 100644 --- a/homeassistant/components/velux/cover.py +++ b/homeassistant/components/velux/cover.py @@ -1,9 +1,15 @@ """Support for Velux covers.""" from pyvlx import OpeningDevice, Position -from pyvlx.opening_device import Awning, Blind, GarageDoor, RollerShutter, Window +from pyvlx.opening_device import Awning, Blind, GarageDoor, Gate, RollerShutter, Window from homeassistant.components.cover import ( ATTR_POSITION, + DEVICE_CLASS_AWNING, + DEVICE_CLASS_BLIND, + DEVICE_CLASS_GARAGE, + DEVICE_CLASS_GATE, + DEVICE_CLASS_SHUTTER, + DEVICE_CLASS_WINDOW, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, @@ -19,7 +25,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= """Set up cover(s) for Velux platform.""" entities = [] for node in hass.data[DATA_VELUX].pyvlx.nodes: - if isinstance(node, OpeningDevice): entities.append(VeluxCover(node)) async_add_entities(entities) @@ -73,18 +78,20 @@ class VeluxCover(CoverEntity): @property def device_class(self): - """Define this cover as either window/blind/awning/shutter.""" - if isinstance(self.node, Window): - return "window" - if isinstance(self.node, Blind): - return "blind" - if isinstance(self.node, RollerShutter): - return "shutter" + """Define this cover as either awning, blind, garage, gate, shutter or window.""" if isinstance(self.node, Awning): - return "awning" + return DEVICE_CLASS_AWNING + if isinstance(self.node, Blind): + return DEVICE_CLASS_BLIND if isinstance(self.node, GarageDoor): - return "garage" - return "window" + return DEVICE_CLASS_GARAGE + if isinstance(self.node, Gate): + return DEVICE_CLASS_GATE + if isinstance(self.node, RollerShutter): + return DEVICE_CLASS_SHUTTER + if isinstance(self.node, Window): + return DEVICE_CLASS_WINDOW + return DEVICE_CLASS_WINDOW @property def is_closed(self): diff --git a/homeassistant/components/vesync/translations/hu.json b/homeassistant/components/vesync/translations/hu.json index 4735140216f..ac1da8b714d 100644 --- a/homeassistant/components/vesync/translations/hu.json +++ b/homeassistant/components/vesync/translations/hu.json @@ -7,7 +7,7 @@ "user": { "data": { "password": "Jelsz\u00f3", - "username": "Email c\u00edm" + "username": "E-mail" }, "title": "\u00cdrja be a felhaszn\u00e1l\u00f3nevet \u00e9s a jelsz\u00f3t" } diff --git a/homeassistant/components/vesync/translations/it.json b/homeassistant/components/vesync/translations/it.json index 42e5b1bb1e4..f18dd28c996 100644 --- a/homeassistant/components/vesync/translations/it.json +++ b/homeassistant/components/vesync/translations/it.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "Password", - "username": "Indirizzo E-mail" + "username": "E-mail" }, "title": "Immettere nome utente e password" } diff --git a/homeassistant/components/vesync/translations/pl.json b/homeassistant/components/vesync/translations/pl.json index 74fbc7c753b..aa5d4dc587f 100644 --- a/homeassistant/components/vesync/translations/pl.json +++ b/homeassistant/components/vesync/translations/pl.json @@ -9,8 +9,8 @@ "step": { "user": { "data": { - "password": "[%key_id:common::config_flow::data::password%]", - "username": "[%key_id:common::config_flow::data::email%]" + "password": "Has\u0142o", + "username": "Adres e-mail" }, "title": "Wprowad\u017a nazw\u0119 u\u017cytkownika i has\u0142o." } diff --git a/homeassistant/components/vesync/translations/pt-BR.json b/homeassistant/components/vesync/translations/pt-BR.json new file mode 100644 index 00000000000..fada7da0ac7 --- /dev/null +++ b/homeassistant/components/vesync/translations/pt-BR.json @@ -0,0 +1,12 @@ +{ + "config": { + "error": { + "invalid_login": "Usu\u00e1rio ou senha inv\u00e1lidos" + }, + "step": { + "user": { + "title": "Digite o nome de usu\u00e1rio e a senha" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/translations/ca.json b/homeassistant/components/vilfo/translations/ca.json index 8bd6185c094..639167e4cec 100644 --- a/homeassistant/components/vilfo/translations/ca.json +++ b/homeassistant/components/vilfo/translations/ca.json @@ -11,8 +11,8 @@ "step": { "user": { "data": { - "access_token": "Token d'acc\u00e9s per l'API de l'encaminador Vilfo", - "host": "Nom d'amfitri\u00f3 o IP de l'encaminador" + "access_token": "Token d'acc\u00e9s", + "host": "Amfitri\u00f3" }, "description": "Configura la integraci\u00f3 de l'encaminador Vilfo. Necessites la seva IP o nom d'amfitri\u00f3 i el token d'acc\u00e9s de l'API. Per a m\u00e9s informaci\u00f3, visita: https://www.home-assistant.io/integrations/vilfo", "title": "Connexi\u00f3 amb l'encaminador Vilfo" diff --git a/homeassistant/components/vilfo/translations/fr.json b/homeassistant/components/vilfo/translations/fr.json index 867b78ac411..1a587121b55 100644 --- a/homeassistant/components/vilfo/translations/fr.json +++ b/homeassistant/components/vilfo/translations/fr.json @@ -1,8 +1,17 @@ { "config": { + "abort": { + "already_configured": "Ce routeur Vilfo est d\u00e9j\u00e0 configur\u00e9." + }, + "error": { + "cannot_connect": "\u00c9chec de connexion. Veuillez v\u00e9rifier les informations que vous avez fournies et r\u00e9essayer.", + "invalid_auth": "Authentification non valide. Veuillez v\u00e9rifier le jeton d'acc\u00e8s et r\u00e9essayer.", + "unknown": "Une erreur inattendue s'est produite lors de la configuration de l'int\u00e9gration." + }, "step": { "user": { "data": { + "access_token": "Jeton d'Acc\u00e8s", "host": "Nom d'h\u00f4te ou IP du routeur" }, "title": "Connectez-vous au routeur Vilfo" diff --git a/homeassistant/components/vilfo/translations/hu.json b/homeassistant/components/vilfo/translations/hu.json index 0368349f75a..a75149507fc 100644 --- a/homeassistant/components/vilfo/translations/hu.json +++ b/homeassistant/components/vilfo/translations/hu.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "host": "Router hostname vagy IP" + "access_token": "Hozz\u00e1f\u00e9r\u00e9si token", + "host": "Hoszt" }, "title": "Csatlakoz\u00e1s a Vilfo routerhez" } diff --git a/homeassistant/components/vilfo/translations/it.json b/homeassistant/components/vilfo/translations/it.json index 2f39499260e..8d41d74c79d 100644 --- a/homeassistant/components/vilfo/translations/it.json +++ b/homeassistant/components/vilfo/translations/it.json @@ -11,8 +11,8 @@ "step": { "user": { "data": { - "access_token": "Token di accesso per il Vilfo Router API", - "host": "Nome host o IP del router" + "access_token": "Token di accesso", + "host": "Host" }, "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" diff --git a/homeassistant/components/vilfo/translations/pl.json b/homeassistant/components/vilfo/translations/pl.json index 939d840035f..29a8a056361 100644 --- a/homeassistant/components/vilfo/translations/pl.json +++ b/homeassistant/components/vilfo/translations/pl.json @@ -6,13 +6,13 @@ "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": "[%key_id:common::config_flow::error::unknown%]" + "unknown": "Nieoczekiwany b\u0142\u0105d." }, "step": { "user": { "data": { - "access_token": "Token dost\u0119pu do interfejsu API routera Vilfo", - "host": "[%key_id:common::config_flow::data::host%]" + "access_token": "Token dost\u0119pu", + "host": "Nazwa hosta lub adres IP" }, "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\u0105czenie z routerem Vilfo" diff --git a/homeassistant/components/vilfo/translations/pt-BR.json b/homeassistant/components/vilfo/translations/pt-BR.json new file mode 100644 index 00000000000..3105455cb8b --- /dev/null +++ b/homeassistant/components/vilfo/translations/pt-BR.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Este roteador Vilfo j\u00e1 est\u00e1 configurado." + }, + "error": { + "cannot_connect": "Falha ao conectar. Por favor, verifique as informa\u00e7\u00f5es fornecidas por voc\u00ea e tente novamente.", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida. Verifique o token de acesso e tente novamente.", + "unknown": "Ocorreu um erro inesperado ao configurar a integra\u00e7\u00e3o." + }, + "step": { + "user": { + "title": "Conecte-se ao roteador Vilfo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index 379f6c48ace..da4c7cc8b13 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -229,7 +229,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): user_input[CONF_DEVICE_CLASS], session=async_get_clientsession(self.hass, False), ): - errors["base"] = "cant_connect" + errors["base"] = "cannot_connect" if not errors: unique_id = await VizioAsync.get_unique_id( @@ -323,7 +323,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_abort(reason="updated_entry") - return self.async_abort(reason="already_setup") + return self.async_abort(reason="already_configured_device") self._must_show_form = True # Store config key/value pairs that are not configurable in user step so they @@ -354,7 +354,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): continue if _host_is_same(entry.data[CONF_HOST], discovery_info[CONF_HOST]): - return self.async_abort(reason="already_setup") + return self.async_abort(reason="already_configured_device") # Set default name to discovered device name by stripping zeroconf service # (`type`) from `name` @@ -400,7 +400,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", data_schema=_get_config_schema(self._data), - errors={"base": "cant_connect"}, + errors={"base": "cannot_connect"}, ) # Complete pairing process if PIN has been provided diff --git a/homeassistant/components/vizio/const.py b/homeassistant/components/vizio/const.py index 43cb993cec3..72bd9b6b08a 100644 --- a/homeassistant/components/vizio/const.py +++ b/homeassistant/components/vizio/const.py @@ -65,6 +65,7 @@ SUPPORTED_COMMANDS = { VIZIO_SOUND_MODE = "eq" VIZIO_AUDIO_SETTINGS = "audio" +VIZIO_MUTE_ON = "on" # Since Vizio component relies on device class, this dict will ensure that changes to # the values of DEVICE_CLASS_SPEAKER or DEVICE_CLASS_TV don't require changes to pyvizio. diff --git a/homeassistant/components/vizio/manifest.json b/homeassistant/components/vizio/manifest.json index 9c1f1960d74..2f50e26f310 100644 --- a/homeassistant/components/vizio/manifest.json +++ b/homeassistant/components/vizio/manifest.json @@ -2,7 +2,7 @@ "domain": "vizio", "name": "VIZIO SmartCast", "documentation": "https://www.home-assistant.io/integrations/vizio", - "requirements": ["pyvizio==0.1.47"], + "requirements": ["pyvizio==0.1.48"], "codeowners": ["@raman325"], "config_flow": true, "zeroconf": ["_viziocast._tcp.local."], diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index 67bb3d3633c..942411b0536 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -44,6 +44,7 @@ from .const import ( SUPPORTED_COMMANDS, VIZIO_AUDIO_SETTINGS, VIZIO_DEVICE_CLASSES, + VIZIO_MUTE_ON, VIZIO_SOUND_MODE, ) @@ -137,7 +138,7 @@ class VizioDevice(MediaPlayerEntity): self._current_app = None self._current_app_config = None self._current_sound_mode = None - self._available_sound_modes = None + self._available_sound_modes = [] self._available_inputs = [] self._available_apps = [] self._conf_apps = config_entry.options.get(CONF_APPS, {}) @@ -192,12 +193,9 @@ class VizioDevice(MediaPlayerEntity): self._volume_level = None self._is_volume_muted = None self._current_input = None - self._available_inputs = None self._current_app = None self._current_app_config = None - self._available_apps = None self._current_sound_mode = None - self._available_sound_modes = None return self._state = STATE_ON @@ -205,25 +203,26 @@ class VizioDevice(MediaPlayerEntity): audio_settings = await self._device.get_all_settings( VIZIO_AUDIO_SETTINGS, log_api_exception=False ) - if audio_settings is not None: + if audio_settings: self._volume_level = float(audio_settings["volume"]) / self._max_volume if "mute" in audio_settings: - self._is_volume_muted = audio_settings["mute"].lower() == "on" + self._is_volume_muted = audio_settings["mute"].lower() == VIZIO_MUTE_ON else: self._is_volume_muted = None if VIZIO_SOUND_MODE in audio_settings: self._supported_commands |= SUPPORT_SELECT_SOUND_MODE self._current_sound_mode = audio_settings[VIZIO_SOUND_MODE] - if self._available_sound_modes is None: + if not self._available_sound_modes: self._available_sound_modes = await self._device.get_setting_options( VIZIO_AUDIO_SETTINGS, VIZIO_SOUND_MODE ) else: - self._supported_commands ^= SUPPORT_SELECT_SOUND_MODE + # Explicitly remove SUPPORT_SELECT_SOUND_MODE from supported features + self._supported_commands &= ~SUPPORT_SELECT_SOUND_MODE input_ = await self._device.get_current_input(log_api_exception=False) - if input_ is not None: + if input_: self._current_input = input_ inputs = await self._device.get_inputs_list(log_api_exception=False) @@ -242,8 +241,7 @@ class VizioDevice(MediaPlayerEntity): # Create list of available known apps from known app list after # filtering by CONF_INCLUDE/CONF_EXCLUDE - if not self._available_apps: - self._available_apps = self._apps_list(self._device.get_apps_list()) + self._available_apps = self._apps_list(self._device.get_apps_list()) self._current_app_config = await self._device.get_current_app_config( log_api_exception=False diff --git a/homeassistant/components/vizio/strings.json b/homeassistant/components/vizio/strings.json index 9cceb6c35a2..0de1a380d12 100644 --- a/homeassistant/components/vizio/strings.json +++ b/homeassistant/components/vizio/strings.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "title": "Setup VIZIO SmartCast Device", + "title": "VIZIO SmartCast Device", "description": "An [%key:common::config_flow::data::access_token%] is only needed for TVs. If you are configuring a TV and do not have an [%key:common::config_flow::data::access_token%] yet, leave it blank to go through a pairing process.", "data": { "name": "Name", @@ -20,28 +20,28 @@ }, "pairing_complete": { "title": "Pairing Complete", - "description": "Your VIZIO SmartCast device is now connected to Home Assistant." + "description": "Your [%key:component::vizio::config::step::user::title%] is now connected to Home Assistant." }, "pairing_complete_import": { "title": "Pairing Complete", - "description": "Your VIZIO SmartCast TV is now connected to Home Assistant.\n\nYour [%key:common::config_flow::data::access_token%] is '**{access_token}**'." + "description": "Your [%key:component::vizio::config::step::user::title%] is now connected to Home Assistant.\n\nYour [%key:common::config_flow::data::access_token%] is '**{access_token}**'." } }, "error": { - "host_exists": "VIZIO device with specified host already configured.", - "name_exists": "VIZIO device with specified name already configured.", + "host_exists": "[%key:component::vizio::config::step::user::title%] with specified host already configured.", + "name_exists": "[%key:component::vizio::config::step::user::title%] with specified name already configured.", "complete_pairing_failed": "Unable to complete pairing. Ensure the PIN you provided is correct and the TV is still powered and connected to the network before resubmitting.", - "cant_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "abort": { - "already_setup": "This entry has already been setup.", + "already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]", "updated_entry": "This entry has already been setup but the name, apps, and/or options defined in the configuration do not match the previously imported configuration, so the configuration entry has been updated accordingly." } }, "options": { "step": { "init": { - "title": "Update VIZIO SmartCast Options", + "title": "Update [%key:component::vizio::config::step::user::title%] Options", "description": "If you have a Smart TV, you can optionally filter your source list by choosing which apps to include or exclude in your source list.", "data": { "volume_step": "Volume Step Size", diff --git a/homeassistant/components/vizio/translations/ca.json b/homeassistant/components/vizio/translations/ca.json index 2194f33dfff..42d363c1d41 100644 --- a/homeassistant/components/vizio/translations/ca.json +++ b/homeassistant/components/vizio/translations/ca.json @@ -1,15 +1,14 @@ { "config": { "abort": { - "already_setup": "Aquesta entrada ja ha estat configurada.", - "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." + "already_configured_device": "El dispositiu ja est\u00e0 configurat", + "updated_entry": "Aquesta entrada ja s'ha configurat per\u00f2 el nom, les aplicacions i/o les opcions definides a la configuraci\u00f3 no coincideixen amb els valors importats anteriorment, en conseq\u00fc\u00e8ncia, l'entrada de configuraci\u00f3 s'ha actualitzat." }, "error": { - "cant_connect": "No s'ha pogut connectar", - "complete_pairing failed": "No s'ha pogut completar l'emparellament. Verifica que el PIN proporcionat sigui el correcte i que el televisor segueix connectat a la xarxa abans de provar-ho de nou.", + "cannot_connect": "No s'ha pogut connectar", "complete_pairing_failed": "No s'ha pogut completar l'emparellament. Verifica que el PIN proporcionat sigui el correcte i que el televisor segueix connectat a la xarxa abans de provar-ho de nou.", - "host_exists": "Dispositiu Vizio amb aquest nom d'amfitri\u00f3 ja configurat.", - "name_exists": "Dispositiu Vizio amb aquest nom ja configurat." + "host_exists": "Dispositiu VIZIO SmartCast amb aquest nom d'amfitri\u00f3 ja configurat.", + "name_exists": "Dispositiu VIZIO SmartCast amb aquest nom ja configurat." }, "step": { "pair_tv": { @@ -20,22 +19,22 @@ "title": "Proc\u00e9s d'aparellament complet" }, "pairing_complete": { - "description": "El dispositiu Vizio SmartCast est\u00e0 connectat a Home Assistant.", + "description": "El Dispositiu VIZIO SmartCast est\u00e0 connectat a Home Assistant.", "title": "Emparellament completat" }, "pairing_complete_import": { - "description": "El dispositiu VIZIO SmartCast TV est\u00e0 connectat a Home Assistant.\n\nEl teu [%key::common::config_flow::data::access_token%] \u00e9s '**{access_token}**'.", + "description": "El Dispositiu VIZIO SmartCast est\u00e0 connectat a Home Assistant.\n\nEl teu [%key::common::config_flow::data::access_token%] \u00e9s '**{access_token}**'.", "title": "Emparellament completat" }, "user": { "data": { "access_token": "[%key::common::config_flow::data::access_token%]", "device_class": "Tipus de dispositiu", - "host": ":", + "host": "Amfitri\u00f3", "name": "Nom" }, "description": "Nom\u00e9s es necessita el [%key::common::config_flow::data::access_token%] per als televisors. Si est\u00e0s configurant un televisor i encara no tens un [%key::common::config_flow::data::access_token%], deixa-ho en blanc per poder fer el proc\u00e9s d'emparellament.", - "title": "Configuraci\u00f3 del client de Vizio SmartCast" + "title": "Dispositiu VIZIO SmartCast" } } }, @@ -48,7 +47,7 @@ "volume_step": "Mida del pas de volum" }, "description": "Si tens una Smart TV, pots filtrar de manera opcional la teva llista de canals escollint quines aplicacions vols incloure o excloure de la llista.", - "title": "Actualitzaci\u00f3 de les opcions de Vizo SmartCast" + "title": "Actualitzaci\u00f3 de les opcions del Dispositiu VIZIO SmartCast" } } } diff --git a/homeassistant/components/vizio/translations/da.json b/homeassistant/components/vizio/translations/da.json index 6de25c240de..71f9648cf0f 100644 --- a/homeassistant/components/vizio/translations/da.json +++ b/homeassistant/components/vizio/translations/da.json @@ -1,11 +1,9 @@ { "config": { "abort": { - "already_setup": "Denne post er allerede blevet konfigureret.", "updated_entry": "Denne post er allerede konfigureret, men navnet og/eller indstillingerne, der er defineret i konfigurationen, stemmer ikke overens med den tidligere importerede konfiguration, s\u00e5 konfigurationsposten er blevet opdateret i overensstemmelse hermed." }, "error": { - "cant_connect": "Kunne ikke oprette forbindelse til enheden. [Gennemg\u00e5 dokumentationen] (https://www.home-assistant.io/integrations/vizio/), og bekr\u00e6ft, at: \n - Enheden er t\u00e6ndt \n - Enheden er tilsluttet netv\u00e6rket \n - De angivne v\u00e6rdier er korrekte \n f\u00f8r du fors\u00f8ger at indsende igen.", "host_exists": "Vizio-enhed med den specificerede v\u00e6rt er allerede konfigureret.", "name_exists": "Vizio-enhed med det specificerede navn er allerede konfigureret." }, diff --git a/homeassistant/components/vizio/translations/de.json b/homeassistant/components/vizio/translations/de.json index b5e8a5acc60..28397d08b8f 100644 --- a/homeassistant/components/vizio/translations/de.json +++ b/homeassistant/components/vizio/translations/de.json @@ -1,12 +1,9 @@ { "config": { "abort": { - "already_setup": "Dieser Eintrag wurde bereits eingerichtet.", "updated_entry": "Dieser Eintrag wurde bereits eingerichtet, aber der Name, die Apps und / oder die in der Konfiguration definierten Optionen stimmen nicht mit der zuvor importierten Konfiguration \u00fcberein, sodass der Konfigurationseintrag entsprechend aktualisiert wurde." }, "error": { - "cant_connect": "Es konnte keine Verbindung zum Ger\u00e4t hergestellt werden. [\u00dcberpr\u00fcfen Sie die Dokumentation] (https://www.home-assistant.io/integrations/vizio/) und \u00fcberpr\u00fcfen Sie Folgendes erneut:\n- Das Ger\u00e4t ist eingeschaltet\n- Das Ger\u00e4t ist mit dem Netzwerk verbunden\n- Die von Ihnen eingegebenen Werte sind korrekt\nbevor sie versuchen, erneut zu \u00fcbermitteln.", - "complete_pairing failed": "Das Pairing kann nicht abgeschlossen werden. Stellen Sie sicher, dass die von Ihnen angegebene PIN korrekt ist und das Fernsehger\u00e4t weiterhin mit Strom versorgt und mit dem Netzwerk verbunden ist, bevor Sie es erneut versuchen.", "host_exists": "VIZIO-Ger\u00e4t mit angegebenem Host bereits konfiguriert.", "name_exists": "VIZIO-Ger\u00e4t mit angegebenem Namen bereits konfiguriert." }, diff --git a/homeassistant/components/vizio/translations/en.json b/homeassistant/components/vizio/translations/en.json index d09e29037ee..42dcfe6d96d 100644 --- a/homeassistant/components/vizio/translations/en.json +++ b/homeassistant/components/vizio/translations/en.json @@ -1,15 +1,14 @@ { "config": { "abort": { - "already_setup": "This entry has already been setup.", + "already_configured_device": "Device is already configured", "updated_entry": "This entry has already been setup but the name, apps, and/or options defined in the configuration do not match the previously imported configuration, so the configuration entry has been updated accordingly." }, "error": { - "cant_connect": "Failed to connect", - "complete_pairing failed": "Unable to complete pairing. Ensure the PIN you provided is correct and the TV is still powered and connected to the network before resubmitting.", + "cannot_connect": "Failed to connect", "complete_pairing_failed": "Unable to complete pairing. Ensure the PIN you provided is correct and the TV is still powered and connected to the network before resubmitting.", - "host_exists": "VIZIO device with specified host already configured.", - "name_exists": "VIZIO device with specified name already configured." + "host_exists": "VIZIO SmartCast Device with specified host already configured.", + "name_exists": "VIZIO SmartCast Device with specified name already configured." }, "step": { "pair_tv": { @@ -20,11 +19,11 @@ "title": "Complete Pairing Process" }, "pairing_complete": { - "description": "Your VIZIO SmartCast device is now connected to Home Assistant.", + "description": "Your VIZIO SmartCast Device is now connected to Home Assistant.", "title": "Pairing Complete" }, "pairing_complete_import": { - "description": "Your VIZIO SmartCast TV is now connected to Home Assistant.\n\nYour Access Token is '**{access_token}**'.", + "description": "Your VIZIO SmartCast Device is now connected to Home Assistant.\n\nYour Access Token is '**{access_token}**'.", "title": "Pairing Complete" }, "user": { @@ -35,7 +34,7 @@ "name": "Name" }, "description": "An Access Token is only needed for TVs. If you are configuring a TV and do not have an Access Token yet, leave it blank to go through a pairing process.", - "title": "Setup VIZIO SmartCast Device" + "title": "VIZIO SmartCast Device" } } }, @@ -48,7 +47,7 @@ "volume_step": "Volume Step Size" }, "description": "If you have a Smart TV, you can optionally filter your source list by choosing which apps to include or exclude in your source list.", - "title": "Update VIZIO SmartCast Options" + "title": "Update VIZIO SmartCast Device Options" } } } diff --git a/homeassistant/components/vizio/translations/es-419.json b/homeassistant/components/vizio/translations/es-419.json index d60f839d653..0ee2dcaaf86 100644 --- a/homeassistant/components/vizio/translations/es-419.json +++ b/homeassistant/components/vizio/translations/es-419.json @@ -1,12 +1,9 @@ { "config": { "abort": { - "already_setup": "Esta entrada ya se ha configurado.", "updated_entry": "Esta entrada ya se configur\u00f3, pero el nombre, las aplicaciones o las opciones definidas en la configuraci\u00f3n no coinciden con la configuraci\u00f3n importada anteriormente, por lo que la entrada de configuraci\u00f3n se actualiz\u00f3 en consecuencia." }, "error": { - "cant_connect": "No se pudo conectar al dispositivo. [Revise los documentos] (https://www.home-assistant.io/integrations/vizio/) y vuelva a verificar que: \n - El dispositivo est\u00e1 encendido \n - El dispositivo est\u00e1 conectado a la red. \n - Los valores que complet\u00f3 son precisos \n antes de intentar volver a enviar.", - "complete_pairing failed": "No se puede completar el emparejamiento. Aseg\u00farese de que el PIN que proporcion\u00f3 sea correcto y que el televisor siga encendido y conectado a la red antes de volver a enviarlo.", "host_exists": "Dispositivo VIZIO con el host especificado ya configurado.", "name_exists": "Dispositivo VIZIO con el nombre especificado ya configurado." }, diff --git a/homeassistant/components/vizio/translations/es.json b/homeassistant/components/vizio/translations/es.json index 41246de7531..ad496be3836 100644 --- a/homeassistant/components/vizio/translations/es.json +++ b/homeassistant/components/vizio/translations/es.json @@ -1,12 +1,11 @@ { "config": { "abort": { - "already_setup": "Esta entrada ya ha sido configurada.", + "already_configured_device": "El dispositivo ya est\u00e1 configurado", "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." }, "error": { - "cant_connect": "Error al conectar", - "complete_pairing failed": "No se pudo completar el emparejamiento. Aseg\u00farate de que el PIN que has proporcionado es correcto y que el televisor sigue encendido y conectado a la red antes de volver a enviarlo.", + "cannot_connect": "No se pudo conectar", "complete_pairing_failed": "No se pudo completar el emparejamiento. Aseg\u00farate de que el PIN que has proporcionado es correcto y que el televisor sigue encendido y conectado a la red antes de volver a enviarlo.", "host_exists": "El host ya est\u00e1 configurado.", "name_exists": "Nombre ya configurado." diff --git a/homeassistant/components/vizio/translations/fr.json b/homeassistant/components/vizio/translations/fr.json index b28b9881024..906f1580c35 100644 --- a/homeassistant/components/vizio/translations/fr.json +++ b/homeassistant/components/vizio/translations/fr.json @@ -1,12 +1,9 @@ { "config": { "abort": { - "already_setup": "Cette entr\u00e9e a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9e.", "updated_entry": "Cette entr\u00e9e a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9e mais le nom et/ou les options d\u00e9finis dans la configuration ne correspondent pas \u00e0 la configuration pr\u00e9c\u00e9demment import\u00e9e, de sorte que l'entr\u00e9e de configuration a \u00e9t\u00e9 mise \u00e0 jour en cons\u00e9quence." }, "error": { - "cant_connect": "Impossible de se connecter \u00e0 l'appareil. [V\u00e9rifier les documents](https://www.home-assistant.io/integrations/vizio/) et rev\u00e9rifier que:\n- L'appareil est sous tension\n- L'appareil est connect\u00e9 au r\u00e9seau\n- Les valeurs que vous avez saisies sont exactes\navant d'essayer de le soumettre \u00e0 nouveau.", - "complete_pairing failed": "Impossible de terminer l'appariement. Assurez-vous que le code PIN que vous avez fourni est correct et que le t\u00e9l\u00e9viseur est toujours aliment\u00e9 et connect\u00e9 au r\u00e9seau avant de soumettre \u00e0 nouveau.", "host_exists": "H\u00f4te d\u00e9j\u00e0 configur\u00e9.", "name_exists": "Nom d\u00e9j\u00e0 configur\u00e9." }, diff --git a/homeassistant/components/vizio/translations/hu.json b/homeassistant/components/vizio/translations/hu.json index 37ef8b3740f..469649275e1 100644 --- a/homeassistant/components/vizio/translations/hu.json +++ b/homeassistant/components/vizio/translations/hu.json @@ -1,11 +1,9 @@ { "config": { "abort": { - "already_setup": "Ez a bejegyz\u00e9s m\u00e1r be van \u00e1ll\u00edtva.", "updated_entry": "Ez a bejegyz\u00e9s m\u00e1r be van \u00e1ll\u00edtva, de a konfigur\u00e1ci\u00f3ban defini\u00e1lt n\u00e9v, appok \u00e9s/vagy be\u00e1ll\u00edt\u00e1sok nem egyeznek meg a kor\u00e1bban import\u00e1lt konfigur\u00e1ci\u00f3val, \u00edgy a konfigur\u00e1ci\u00f3s bejegyz\u00e9s ennek megfelel\u0151en friss\u00fclt." }, "error": { - "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." }, @@ -14,6 +12,7 @@ "data": { "access_token": "Hozz\u00e1f\u00e9r\u00e9si token", "device_class": "Eszk\u00f6zt\u00edpus", + "host": "Hoszt", "name": "N\u00e9v" }, "title": "A Vizio SmartCast Client be\u00e1ll\u00edt\u00e1sa" diff --git a/homeassistant/components/vizio/translations/it.json b/homeassistant/components/vizio/translations/it.json index 0212b8a1281..64f3d8ddd93 100644 --- a/homeassistant/components/vizio/translations/it.json +++ b/homeassistant/components/vizio/translations/it.json @@ -1,15 +1,14 @@ { "config": { "abort": { - "already_setup": "Questa voce \u00e8 gi\u00e0 stata configurata.", + "already_configured_device": "Dispositivo gi\u00e0 configurato", "updated_entry": "Questa voce \u00e8 gi\u00e0 stata configurata, ma il nome, le app e/o le opzioni definite nella configurazione non corrispondono alla configurazione importata in precedenza, pertanto la voce di configurazione \u00e8 stata aggiornata di conseguenza." }, "error": { - "cant_connect": "Impossibile connettersi", - "complete_pairing failed": "Impossibile completare l'associazione. Assicurarsi che il PIN fornito sia corretto e che il televisore sia ancora alimentato e connesso alla rete prima di inviarlo di nuovo.", + "cannot_connect": "Impossibile connettersi", "complete_pairing_failed": "Impossibile completare l'associazione. Assicurarsi che il PIN fornito sia corretto e che la TV sia ancora accesa e collegata alla rete prima di inviare nuovamente.", - "host_exists": "Dispositivo VIZIO con host specificato gi\u00e0 configurato.", - "name_exists": "Dispositivo VIZIO con il nome specificato gi\u00e0 configurato." + "host_exists": "Il Dispositivo SmartCast VIZIO con host specificato \u00e8 gi\u00e0 configurato.", + "name_exists": "Il Dispositivo SmartCast VIZIO con il nome specificato \u00e8 gi\u00e0 configurato." }, "step": { "pair_tv": { @@ -20,22 +19,22 @@ "title": "Processo di associazione completo" }, "pairing_complete": { - "description": "Il dispositivo VIZIO SmartCast \u00e8 ora connesso a Home Assistant.", + "description": "Il tuo Dispositivo SmartCast VIZIO \u00e8 ora connesso a Home Assistant.", "title": "Associazione completata" }, "pairing_complete_import": { - "description": "Il dispositivo VIZIO SmartCast TV \u00e8 ora connesso a Home Assistant. \n\nIl tuo Token di accesso \u00e8 '**{access_token}**'.", + "description": "Il tuo Dispositivo SmartCast VIZIO \u00e8 ora connesso a Home Assistant.\n\nIl tuo Token di accesso \u00e8 '**{access_token}**'.", "title": "Associazione completata" }, "user": { "data": { "access_token": "Token di accesso", "device_class": "Tipo di dispositivo", - "host": "< Host / IP >: ", + "host": "Host", "name": "Nome" }, "description": "Un Token di accesso \u00e8 necessario solo per i televisori. Se si sta configurando un televisore e non si dispone ancora di un Token di accesso, lasciarlo vuoto per passare attraverso un processo di associazione.", - "title": "Configurazione del dispositivo SmartCast VIZIO" + "title": "Dispositivo SmartCast VIZIO" } } }, @@ -48,7 +47,7 @@ "volume_step": "Dimensione del passo del volume" }, "description": "Se si dispone di una Smart TV, \u00e8 possibile filtrare l'elenco di origine scegliendo le app da includere o escludere in esso.", - "title": "Aggiornamento delle opzioni di VIZIO SmartCast" + "title": "Aggiornamento delle Opzioni del Dispositivo SmartCast VIZIO" } } } diff --git a/homeassistant/components/vizio/translations/ko.json b/homeassistant/components/vizio/translations/ko.json index db690798908..8b84c96b102 100644 --- a/homeassistant/components/vizio/translations/ko.json +++ b/homeassistant/components/vizio/translations/ko.json @@ -1,12 +1,11 @@ { "config": { "abort": { - "already_setup": "\uc774 \ud56d\ubaa9\uc740 \uc774\ubbf8 \uc124\uc815\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "already_configured_device": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "updated_entry": "\uc774 \ud56d\ubaa9\uc740 \uc774\ubbf8 \uc124\uc815\ub418\uc5c8\uc9c0\ub9cc \uad6c\uc131\uc5d0 \uc815\uc758\ub41c \uc774\ub984, \uc571 \ud639\uc740 \uc635\uc158\uc774 \uc774\uc804\uc5d0 \uac00\uc838\uc628 \uad6c\uc131 \ub0b4\uc6a9\uacfc \uc77c\uce58\ud558\uc9c0 \uc54a\uc73c\ubbc0\ub85c \uad6c\uc131 \ud56d\ubaa9\uc774 \uadf8\uc5d0 \ub530\ub77c \uc5c5\ub370\uc774\ud2b8\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, "error": { - "cant_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "complete_pairing failed": "\ud398\uc5b4\ub9c1\uc744 \uc644\ub8cc\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc785\ub825\ud55c PIN \uc774 \uc62c\ubc14\ub978\uc9c0 \ud655\uc778\ud558\uace0 \ub2e4\uc74c \uacfc\uc815\uc744 \uc9c4\ud589\ud558\uae30 \uc804\uc5d0 TV \uc758 \uc804\uc6d0\uc774 \ucf1c\uc838 \uc788\uace0 \ub124\ud2b8\uc6cc\ud06c\uc5d0 \uc5f0\uacb0\ub418\uc5b4 \uc788\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "complete_pairing_failed": "\ud398\uc5b4\ub9c1\uc744 \uc644\ub8cc\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc81c\ucd9c\ud558\uae30 \uc804\uc5d0 \uc785\ub825\ud55c PIN \uc774 \uc62c\ubc14\ub978\uc9c0, TV \uc758 \uc804\uc6d0\uc774 \ucf1c\uc838 \uc788\uace0 \ub124\ud2b8\uc6cc\ud06c\uc5d0 \uc5f0\uacb0\ub418\uc5b4 \uc788\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", "host_exists": "\uc124\uc815\ub41c \ud638\uc2a4\ud2b8\uc758 VIZIO \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "name_exists": "\uc124\uc815\ub41c \uc774\ub984\uc758 VIZIO \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." @@ -16,7 +15,7 @@ "data": { "pin": "PIN" }, - "description": "TV \uc5d0 \ucf54\ub4dc\uac00 \ud45c\uc2dc\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud574\ub2f9 \ucf54\ub4dc\ub97c \uc591\uc2dd\uc5d0 \uc785\ub825\ud55c \ud6c4 \ub2e4\uc74c \ub2e8\uacc4\ub97c \uacc4\uc18d\ud558\uc5ec \ud398\uc5b4\ub9c1\uc744 \uc644\ub8cc\ud574\uc8fc\uc138\uc694.", + "description": "TV \uc5d0 \ucf54\ub4dc\uac00 \ud45c\uc2dc\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud574\ub2f9 \ucf54\ub4dc\ub97c \uc785\ub825\ub780\uc5d0 \uc785\ub825\ud55c \ud6c4 \ub2e4\uc74c \ub2e8\uacc4\ub97c \uacc4\uc18d\ud558\uc5ec \ud398\uc5b4\ub9c1\uc744 \uc644\ub8cc\ud574\uc8fc\uc138\uc694.", "title": "\ud398\uc5b4\ub9c1 \uacfc\uc815 \ub05d\ub0b4\uae30" }, "pairing_complete": { @@ -43,7 +42,7 @@ "step": { "init": { "data": { - "apps_to_include_or_exclude": "\ud3ec\ud568 \ub610\ub294 \uc81c\uc678 \ud560 \uc571", + "apps_to_include_or_exclude": "\ud3ec\ud568 \ub610\ub294 \uc81c\uc678\ud560 \uc571", "include_or_exclude": "\uc571\uc744 \ud3ec\ud568 \ub610\ub294 \uc81c\uc678\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "volume_step": "\ubcfc\ub968 \ub2e8\uacc4 \ud06c\uae30" }, diff --git a/homeassistant/components/vizio/translations/lb.json b/homeassistant/components/vizio/translations/lb.json index 8a799834b3a..1fefd98f9f7 100644 --- a/homeassistant/components/vizio/translations/lb.json +++ b/homeassistant/components/vizio/translations/lb.json @@ -1,12 +1,11 @@ { "config": { "abort": { - "already_setup": "D\u00ebs Entr\u00e9e ass scho konfigur\u00e9iert.", + "already_configured_device": "Apparat ass scho konfigur\u00e9iert", "updated_entry": "D\u00ebs Entr\u00e9e ass scho konfigur\u00e9iert mee d\u00e9i defin\u00e9ierten Numm an/oder Optiounen an der Konfiguratioun st\u00ebmmen net mat deene virdrun import\u00e9ierten Optiounen iwwereneen, esou gouf d'Entr\u00e9e deementspriechend aktualis\u00e9iert." }, "error": { - "cant_connect": "Konnt sech net mam Apparat verbannen. [Iwwerpr\u00e9ift Dokumentatioun] (https://www.home-assistant.io/integrations/vizio/) a stellt s\u00e9cher dass:\n- Den Apparat ass un\n- Den Apparat ass mam Netzwierk verbonnen\n- D'Optiounen d\u00e9i dir aginn hutt si korrekt\nier dir d'Verbindung nees prob\u00e9iert", - "complete_pairing failed": "Feeler beim ofschl\u00e9isse vun der Kopplung. Iwwerpr\u00e9if dass de PIN korrekt an da de Fernsee nach \u00ebmmer ugeschalt a mam Netzwierk verbonnen ass ier de n\u00e4chste Versuch gestart g\u00ebtt.", + "cannot_connect": "Feeler beim verbannen", "complete_pairing_failed": "Feeler beim ofschl\u00e9isse vun der Kopplung. Iwwerpr\u00e9if dass de PIN korrekt an da de Fernsee nach \u00ebmmer ugeschalt a mam Netzwierk verbonnen ass ier de n\u00e4chste Versuch gestart g\u00ebtt.", "host_exists": "VIZIO Apparat mat d\u00ebsem Host ass scho konfigur\u00e9iert.", "name_exists": "VIZIO Apparat mat d\u00ebsen Numm ass scho konfigur\u00e9iert." diff --git a/homeassistant/components/vizio/translations/nl.json b/homeassistant/components/vizio/translations/nl.json index 33777175c3e..476f8265102 100644 --- a/homeassistant/components/vizio/translations/nl.json +++ b/homeassistant/components/vizio/translations/nl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_setup": "Dit item is al ingesteld.", + "already_configured_device": "Dit apparaat is 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." }, "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.", + "cannot_connect": "Verbinding mislukt", "host_exists": "Vizio apparaat met opgegeven host al geconfigureerd.", "name_exists": "Vizio apparaat met opgegeven naam al geconfigureerd." }, diff --git a/homeassistant/components/vizio/translations/no.json b/homeassistant/components/vizio/translations/no.json index d7446a415b6..ab585dcdcf3 100644 --- a/homeassistant/components/vizio/translations/no.json +++ b/homeassistant/components/vizio/translations/no.json @@ -1,12 +1,9 @@ { "config": { "abort": { - "already_setup": "Denne oppf\u00f8ringen er allerede konfigurert.", "updated_entry": "Dette innlegget har allerede v\u00e6rt oppsett, men navnet, apps, og/eller alternativer som er definert i konfigurasjon som ikke stemmer med det som tidligere er importert konfigurasjon, s\u00e5 konfigurasjonen innlegget har blitt oppdatert i henhold til dette." }, "error": { - "cant_connect": "Kunne ikke koble til enheten. [Se gjennom dokumentene](https://www.home-assistant.io/integrations/vizio/) og bekreft at: \n - Enheten er sl\u00e5tt p\u00e5 \n - Enheten er koblet til nettverket \n - Verdiene du fylte ut er n\u00f8yaktige \n f\u00f8r du pr\u00f8ver \u00e5 sende inn p\u00e5 nytt.", - "complete_pairing failed": "Kan ikke fullf\u00f8re sammenkoblingen. Forsikre deg om at PIN-koden du oppga er riktig, og at TV-en fortsatt er p\u00e5 og tilkoblet nettverket f\u00f8r du sender inn p\u00e5 nytt.", "complete_pairing_failed": "Kan ikke fullf\u00f8re sammenkoblingen. Forsikre deg om at PIN-koden du oppga er riktig, og at TV-en fortsatt er p\u00e5 og tilkoblet nettverket f\u00f8r du sender inn p\u00e5 nytt.", "host_exists": "VIZIO-enhet med spesifisert vert allerede konfigurert.", "name_exists": "VIZIO-enhet med spesifisert navn allerede konfigurert." @@ -20,11 +17,9 @@ "title": "Fullf\u00f8r sammenkoblingsprosess" }, "pairing_complete": { - "description": "Din VIZIO SmartCast enheten er n\u00e5 koblet til Home Assistant.", "title": "Sammenkoblingen fullf\u00f8rt" }, "pairing_complete_import": { - "description": "VIZIO SmartCast TV er n\u00e5 koblet til Home Assistant\n\nTilgangstokenet er '**{access_token}**'.", "title": "Sammenkoblingen fullf\u00f8rt" }, "user": { @@ -34,8 +29,7 @@ "host": "Vert", "name": "Navn" }, - "description": "En tilgangstoken er bare n\u00f8dvendig for TV-er. Hvis du konfigurerer en TV og ikke har tilgangstoken enda, m\u00e5 du la den st\u00e5 tom for \u00e5 g\u00e5 gjennom en sammenkoblingsprosess.", - "title": "Konfigurer VIZIO SmartCast-enhet" + "title": "VIZIO SmartCast-enhet" } } }, @@ -47,8 +41,7 @@ "include_or_exclude": "Inkluder eller ekskludere apper?", "volume_step": "St\u00f8rrelse p\u00e5 volum trinn" }, - "description": "Hvis du har en Smart-TV, kan du eventuelt filtrere kildelisten ved \u00e5 velge hvilke apper som skal inkluderes eller utelates i kildelisten.", - "title": "Oppdater VIZIO SmartCast-alternativer" + "description": "Hvis du har en Smart-TV, kan du eventuelt filtrere kildelisten ved \u00e5 velge hvilke apper som skal inkluderes eller utelates i kildelisten." } } } diff --git a/homeassistant/components/vizio/translations/pl.json b/homeassistant/components/vizio/translations/pl.json index 699ffdbc437..9d22796ea44 100644 --- a/homeassistant/components/vizio/translations/pl.json +++ b/homeassistant/components/vizio/translations/pl.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_setup": "Ten komponent jest ju\u017c skonfigurowany.", + "already_configured_device": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", "updated_entry": "Ten wpis zosta\u0142 ju\u017c skonfigurowany, ale nazwa i/lub opcje zdefiniowane w konfiguracji nie pasuj\u0105 do wcze\u015bniej zaimportowanych warto\u015bci, wi\u0119c wpis konfiguracji zosta\u0142 odpowiednio zaktualizowany." }, "error": { - "cant_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z urz\u0105dzeniem. [Przejrzyj dokumentacj\u0119] (https://www.home-assistant.io/integrations/vizio/) i ponownie sprawd\u017a, czy: \n - urz\u0105dzenie jest w\u0142\u0105czone,\n - urz\u0105dzenie jest pod\u0142\u0105czone do sieci,\n - wprowadzone warto\u015bci s\u0105 prawid\u0142owe,\n przed pr\u00f3b\u0105 ponownego przes\u0142ania.", - "complete_pairing failed": "Nie mo\u017cna uko\u0144czy\u0107 parowania. Upewnij si\u0119, \u017ce podany kod PIN jest prawid\u0142owy, a telewizor jest zasilany i pod\u0142\u0105czony do sieci przed ponownym przes\u0142aniem.", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", + "complete_pairing_failed": "Nie mo\u017cna uko\u0144czy\u0107 parowania. Upewnij si\u0119, \u017ce podany kod PIN jest prawid\u0142owy, a telewizor jest zasilany i pod\u0142\u0105czony do sieci przed ponownym przes\u0142aniem.", "host_exists": "Urz\u0105dzenie Vizio z okre\u015blonym hostem jest ju\u017c skonfigurowane.", "name_exists": "Urz\u0105dzenie Vizio o okre\u015blonej nazwie jest ju\u017c skonfigurowane." }, @@ -19,18 +19,18 @@ "title": "Ko\u0144czenie procesu parowania" }, "pairing_complete": { - "description": "Twoje urz\u0105dzenie VIZIO SmartCast jest teraz po\u0142\u0105czone z Home Assistant'em.", + "description": "Twoje urz\u0105dzenie VIZIO SmartCast jest teraz po\u0142\u0105czone z Home Assistantem.", "title": "Parowanie zako\u0144czone" }, "pairing_complete_import": { - "description": "Twoje urz\u0105dzenie VIZIO SmartCast jest teraz po\u0142\u0105czone z Home Assistant'em.\n\nToken dost\u0119pu to '**{access_token}**'.", + "description": "Twoje urz\u0105dzenie VIZIO SmartCast jest teraz po\u0142\u0105czone z Home Assistantem.\n\nToken dost\u0119pu to '**{access_token}**'.", "title": "Parowanie zako\u0144czone" }, "user": { "data": { - "access_token": "[%key_id:common::config_flow::data::access_token%]", + "access_token": "Token dost\u0119pu", "device_class": "Typ urz\u0105dzenia", - "host": ":", + "host": "Nazwa hosta lub adres IP", "name": "Nazwa" }, "description": "Token dost\u0119pu potrzebny jest tylko dla telewizor\u00f3w. Je\u015bli konfigurujesz telewizor i nie masz jeszcze tokenu dost\u0119pu, pozostaw go pusty, aby przej\u015b\u0107 przez proces parowania.", diff --git a/homeassistant/components/vizio/translations/pt-BR.json b/homeassistant/components/vizio/translations/pt-BR.json new file mode 100644 index 00000000000..425d1b91f5e --- /dev/null +++ b/homeassistant/components/vizio/translations/pt-BR.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "complete_pairing_failed": "N\u00e3o foi poss\u00edvel concluir o pareamento. Verifique se o PIN que voc\u00ea forneceu est\u00e1 correto e a TV ainda est\u00e1 ligada e conectada \u00e0 internet antes de reenviar." + }, + "step": { + "pair_tv": { + "data": { + "pin": "PIN" + }, + "description": "Sua TV deve estar exibindo um c\u00f3digo. Digite esse c\u00f3digo no formul\u00e1rio e continue na pr\u00f3xima etapa para concluir o pareamento.", + "title": "Processo de pareamento completo" + }, + "pairing_complete": { + "title": "Pareamento completo" + }, + "user": { + "data": { + "device_class": "Tipo de dispositivo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/translations/ru.json b/homeassistant/components/vizio/translations/ru.json index 20d051813c5..d21b0c39615 100644 --- a/homeassistant/components/vizio/translations/ru.json +++ b/homeassistant/components/vizio/translations/ru.json @@ -1,12 +1,11 @@ { "config": { "abort": { - "already_setup": "\u042d\u0442\u0430 \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0431\u044b\u043b\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430.", + "already_configured_device": "\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.", "updated_entry": "\u042d\u0442\u0430 \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430, \u043d\u043e \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b, \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0435 \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438, \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0442 \u0440\u0430\u043d\u0435\u0435 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f\u043c, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0437\u0430\u043f\u0438\u0441\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0431\u044b\u043b\u0430 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430." }, "error": { - "cant_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", - "complete_pairing failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435. \u041f\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043c \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u044c \u043f\u043e\u043f\u044b\u0442\u043a\u0443, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0432\u0432\u0435\u0434\u0435\u043d\u043d\u044b\u0439 \u0412\u0430\u043c\u0438 PIN-\u043a\u043e\u0434 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439, \u0430 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0432\u043a\u043b\u044e\u0447\u0435\u043d \u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d \u043a \u0441\u0435\u0442\u0438.", + "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", "complete_pairing_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435. \u041f\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043c \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u044c \u043f\u043e\u043f\u044b\u0442\u043a\u0443, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0432\u0432\u0435\u0434\u0435\u043d\u043d\u044b\u0439 \u0412\u0430\u043c\u0438 PIN-\u043a\u043e\u0434 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439, \u0430 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0432\u043a\u043b\u044e\u0447\u0435\u043d \u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d \u043a \u0441\u0435\u0442\u0438.", "host_exists": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0445\u043e\u0441\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "name_exists": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441 \u0442\u0430\u043a\u0438\u043c \u0436\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." @@ -24,7 +23,7 @@ "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u043e" }, "pairing_complete_import": { - "description": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e VIZIO SmartCast TV \u0442\u0435\u043f\u0435\u0440\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043a Home Assistant. \n\n\u0412\u0430\u0448 \u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 - '**{access_token}**'.", + "description": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e VIZIO SmartCast \u0442\u0435\u043f\u0435\u0440\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043a Home Assistant. \n\n\u0412\u0430\u0448 \u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 - '**{access_token}**'.", "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u043e" }, "user": { diff --git a/homeassistant/components/vizio/translations/sl.json b/homeassistant/components/vizio/translations/sl.json index e64c3b6cc86..e1f11c9a13e 100644 --- a/homeassistant/components/vizio/translations/sl.json +++ b/homeassistant/components/vizio/translations/sl.json @@ -1,12 +1,9 @@ { "config": { "abort": { - "already_setup": "Ta vnos je \u017ee nastavljen.", "updated_entry": "Ta vnos je bil \u017ee nastavljen, vendar se ime, aplikacije in/ali mo\u017enosti, dolo\u010dene v konfiguraciji, ne ujemajo s predhodno uvo\u017eeno konfiguracijo, zato je bil konfiguracijski vnos 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.", - "complete_pairing failed": "Seznanjanja ni mogo\u010de dokon\u010dati. Zagotovite, da je PIN, ki ste ga vnesli, pravilen in da je televizor \u0161e vedno vklopljen in priklju\u010den na omre\u017eje, preden ponovno poizkusite.", "host_exists": "Naprava Vizio z dolo\u010denim gostiteljem je \u017ee konfigurirana.", "name_exists": "Naprava Vizio z navedenim imenom je \u017ee konfigurirana." }, diff --git a/homeassistant/components/vizio/translations/sv.json b/homeassistant/components/vizio/translations/sv.json index 5c5a25dd129..0762066c8b4 100644 --- a/homeassistant/components/vizio/translations/sv.json +++ b/homeassistant/components/vizio/translations/sv.json @@ -1,11 +1,9 @@ { "config": { "abort": { - "already_setup": "Den h\u00e4r posten har redan st\u00e4llts in.", "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." }, "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." }, diff --git a/homeassistant/components/vizio/translations/zh-Hant.json b/homeassistant/components/vizio/translations/zh-Hant.json index a3508481a1b..0ddf8f8b88f 100644 --- a/homeassistant/components/vizio/translations/zh-Hant.json +++ b/homeassistant/components/vizio/translations/zh-Hant.json @@ -1,15 +1,14 @@ { "config": { "abort": { - "already_setup": "\u6b64\u7269\u4ef6\u5df2\u8a2d\u5b9a\u904e\u3002", + "already_configured_device": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "updated_entry": "\u6b64\u7269\u4ef6\u5df2\u7d93\u8a2d\u5b9a\uff0c\u4f46\u8a2d\u5b9a\u4e4b\u540d\u7a31\u3001App \u53ca/\u6216\u9078\u9805\u8207\u5148\u524d\u532f\u5165\u7684\u7269\u4ef6\u9078\u9805\u503c\u4e0d\u5408\uff0c\u56e0\u6b64\u8a2d\u5b9a\u5c07\u6703\u8ddf\u8457\u66f4\u65b0\u3002" }, "error": { - "cant_connect": "\u9023\u7dda\u5931\u6557", - "complete_pairing failed": "\u7121\u6cd5\u5b8c\u6210\u914d\u5c0d\uff0c\u50b3\u9001\u524d\u3001\u8acb\u78ba\u5b9a\u6240\u8f38\u5165\u7684 PIN \u78bc\u3001\u540c\u6642\u96fb\u8996\u5df2\u7d93\u958b\u555f\u4e26\u9023\u7dda\u81f3\u7db2\u8def\u3002", + "cannot_connect": "\u9023\u7dda\u5931\u6557", "complete_pairing_failed": "\u7121\u6cd5\u5b8c\u6210\u914d\u5c0d\uff0c\u50b3\u9001\u524d\u3001\u8acb\u78ba\u5b9a\u6240\u8f38\u5165\u7684 PIN \u78bc\u3001\u540c\u6642\u96fb\u8996\u5df2\u7d93\u958b\u555f\u4e26\u9023\u7dda\u81f3\u7db2\u8def\u3002", - "host_exists": "\u4f9d\u4e3b\u6a5f\u7aef\u4e4b VIZIO \u5143\u4ef6\u8a2d\u5b9a\u5df2\u8a2d\u5b9a\u5b8c\u6210\u3002", - "name_exists": "\u4f9d\u540d\u7a31\u4e4b VIZIO \u5143\u4ef6\u8a2d\u5b9a\u5df2\u8a2d\u5b9a\u5b8c\u6210\u3002" + "host_exists": "\u4f9d\u4e3b\u6a5f\u7aef\u4e4b VIZIO SmartCast \u8a2d\u5099 \u8a2d\u5b9a\u5df2\u8a2d\u5b9a\u5b8c\u6210\u3002", + "name_exists": "\u4f9d\u540d\u7a31\u4e4b VIZIO SmartCast \u8a2d\u5099 \u8a2d\u5b9a\u5df2\u8a2d\u5b9a\u5b8c\u6210\u3002" }, "step": { "pair_tv": { @@ -20,11 +19,11 @@ "title": "\u5b8c\u6210\u914d\u5c0d\u904e\u7a0b" }, "pairing_complete": { - "description": "VIZIO SmartCast \u8a2d\u5099\u5df2\u7d93\u9023\u7dda\u81f3 Home Assistant\u3002", + "description": "VIZIO SmartCast \u8a2d\u5099 \u5df2\u7d93\u9023\u7dda\u81f3 Home Assistant\u3002", "title": "\u914d\u5c0d\u5b8c\u6210" }, "pairing_complete_import": { - "description": "VIZIO SmartCast TV \u8a2d\u5099\u5df2\u9023\u7dda\u81f3 Home Assistant\u3002\n\n\u5b58\u53d6\u5bc6\u9470\u70ba '**{access_token}**'\u3002", + "description": "VIZIO SmartCast \u8a2d\u5099 \u5df2\u9023\u7dda\u81f3 Home Assistant\u3002\n\n\u5b58\u53d6\u5bc6\u9470\u70ba '**{access_token}**'\u3002", "title": "\u914d\u5c0d\u5b8c\u6210" }, "user": { @@ -35,7 +34,7 @@ "name": "\u540d\u7a31" }, "description": "\u6b64\u96fb\u8996\u50c5\u9700\u5b58\u53d6\u5bc6\u9470\u5047\u5982\u60a8\u6b63\u5728\u8a2d\u5b9a\u96fb\u8996\u3001\u5c1a\u672a\u53d6\u5f97\u5b58\u53d6\u5bc6\u9470 \uff0c\u4fdd\u6301\u7a7a\u767d\u4ee5\u9032\u884c\u914d\u5c0d\u904e\u7a0b\u3002", - "title": "\u8a2d\u5b9a VIZIO SmartCast \u8a2d\u5099" + "title": "VIZIO SmartCast \u8a2d\u5099" } } }, @@ -48,7 +47,7 @@ "volume_step": "\u97f3\u91cf\u5927\u5c0f" }, "description": "\u5047\u5982\u60a8\u64c1\u6709 Smart TV\u3001\u53ef\u7531\u4f86\u6e90\u5217\u8868\u4e2d\u9078\u64c7\u6240\u8981\u904e\u6ffe\u5305\u542b\u6216\u6392\u9664\u7684 App\u3002\u3002", - "title": "\u66f4\u65b0 VIZIO SmartCast \u9078\u9805" + "title": "\u66f4\u65b0 VIZIO SmartCast \u8a2d\u5099 \u9078\u9805" } } } diff --git a/homeassistant/components/watson_tts/tts.py b/homeassistant/components/watson_tts/tts.py index 74803464484..67dfa32e673 100644 --- a/homeassistant/components/watson_tts/tts.py +++ b/homeassistant/components/watson_tts/tts.py @@ -22,39 +22,47 @@ CONF_TEXT_TYPE = "text" # List from https://tinyurl.com/watson-tts-docs SUPPORTED_VOICES = [ - "de-DE_BirgitVoice", - "de-DE_BirgitV2Voice", + "ar-AR_OmarVoice", "de-DE_BirgitV3Voice", - "de-DE_DieterVoice", - "de-DE_DieterV2Voice", + "de-DE_BirgitVoice", "de-DE_DieterV3Voice", - "en-GB_KateVoice", + "de-DE_DieterVoice", + "de-DE_ErikaV3Voice", "en-GB_KateV3Voice", - "en-US_AllisonVoice", - "en-US_AllisonV2Voice", + "en-GB_KateVoice", "en-US_AllisonV3Voice", - "en-US_LisaVoice", - "en-US_LisaV2Voice", + "en-US_AllisonVoice", + "en-US_EmilyV3Voice", + "en-US_HenryV3Voice", + "en-US_KevinV3Voice", "en-US_LisaV3Voice", - "en-US_MichaelVoice", - "en-US_MichaelV2Voice", + "en-US_LisaVoice", "en-US_MichaelV3Voice", - "es-ES_EnriqueVoice", + "en-US_MichaelVoice", + "en-US_OliviaV3Voice", "es-ES_EnriqueV3Voice", - "es-ES_LauraVoice", + "es-ES_EnriqueVoice", "es-ES_LauraV3Voice", - "es-LA_SofiaVoice", + "es-ES_LauraVoice", "es-LA_SofiaV3Voice", - "es-US_SofiaVoice", + "es-LA_SofiaVoice", "es-US_SofiaV3Voice", - "fr-FR_ReneeVoice", + "es-US_SofiaVoice", "fr-FR_ReneeV3Voice", - "it-IT_FrancescaVoice", - "it-IT_FrancescaV2Voice", + "fr-FR_ReneeVoice", "it-IT_FrancescaV3Voice", + "it-IT_FrancescaVoice", + "ja-JP_EmiV3Voice", "ja-JP_EmiVoice", - "pt-BR_IsabelaVoice", + "ko-KR_YoungmiVoice", + "ko-KR_YunaVoice", + "nl-NL_EmmaVoice", + "nl-NL_LiamVoice", "pt-BR_IsabelaV3Voice", + "pt-BR_IsabelaVoice", + "zh-CN_LiNaVoice", + "zh-CN_WangWeiVoice", + "zh-CN_ZhangJingVoice", ] SUPPORTED_OUTPUT_FORMATS = [ diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 01ca8ed6790..8efb8519636 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -114,7 +114,7 @@ class WeatherEntity(Entity): @property def precision(self): - """Return the forecast.""" + """Return the precision of the temperature value.""" return ( PRECISION_TENTHS if self.temperature_unit == TEMP_CELSIUS diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py index 0790ece9333..ebac158c452 100644 --- a/homeassistant/components/webostv/__init__.py +++ b/homeassistant/components/webostv/__init__.py @@ -9,6 +9,7 @@ from websockets.exceptions import ConnectionClosed from homeassistant.components.webostv.const import ( ATTR_BUTTON, ATTR_COMMAND, + ATTR_PAYLOAD, CONF_ON_ACTION, CONF_SOURCES, DEFAULT_NAME, @@ -59,7 +60,9 @@ CALL_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids}) BUTTON_SCHEMA = CALL_SCHEMA.extend({vol.Required(ATTR_BUTTON): cv.string}) -COMMAND_SCHEMA = CALL_SCHEMA.extend({vol.Required(ATTR_COMMAND): cv.string}) +COMMAND_SCHEMA = CALL_SCHEMA.extend( + {vol.Required(ATTR_COMMAND): cv.string, vol.Optional(ATTR_PAYLOAD): dict} +) SOUND_OUTPUT_SCHEMA = CALL_SCHEMA.extend({vol.Required(ATTR_SOUND_OUTPUT): cv.string}) diff --git a/homeassistant/components/webostv/const.py b/homeassistant/components/webostv/const.py index 3e1e790fc02..bea485a7d68 100644 --- a/homeassistant/components/webostv/const.py +++ b/homeassistant/components/webostv/const.py @@ -5,6 +5,7 @@ DEFAULT_NAME = "LG webOS Smart TV" ATTR_BUTTON = "button" ATTR_COMMAND = "command" +ATTR_PAYLOAD = "payload" ATTR_SOUND_OUTPUT = "sound_output" CONF_ON_ACTION = "turn_on_action" diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 3e443929012..556ff7a287b 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -24,6 +24,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_STEP, ) from homeassistant.components.webostv.const import ( + ATTR_PAYLOAD, ATTR_SOUND_OUTPUT, CONF_ON_ACTION, CONF_SOURCES, @@ -450,6 +451,6 @@ class LgWebOSMediaPlayerEntity(MediaPlayerEntity): await self._client.button(button) @cmd - async def async_command(self, command): + async def async_command(self, command, **kwargs): """Send a command.""" - await self._client.request(command) + await self._client.request(command, payload=kwargs.get(ATTR_PAYLOAD)) diff --git a/homeassistant/components/webostv/services.yaml b/homeassistant/components/webostv/services.yaml index 70aa94b6ea6..b88b3d839d7 100644 --- a/homeassistant/components/webostv/services.yaml +++ b/homeassistant/components/webostv/services.yaml @@ -24,7 +24,12 @@ command: description: >- Endpoint of the command. Known valid endpoints are listed in https://github.com/TheRealLink/pylgtv/blob/master/pylgtv/endpoints.py - example: "media.controls/rewind" + example: "system.launcher/open" + payload: + description: >- + An optional payload to provide to the endpoint in the format of key value pair(s). + example: >- + target: https://www.google.com select_sound_output: description: "Send the TV the command to change sound output." diff --git a/homeassistant/components/websocket_api/decorators.py b/homeassistant/components/websocket_api/decorators.py index 87b5d5baf92..d4a4cff1a8f 100644 --- a/homeassistant/components/websocket_api/decorators.py +++ b/homeassistant/components/websocket_api/decorators.py @@ -1,4 +1,5 @@ """Decorators for the Websocket API.""" +import asyncio from functools import wraps import logging from typing import Awaitable, Callable @@ -31,7 +32,9 @@ def async_response( @wraps(func) def schedule_handler(hass, connection, msg): """Schedule the handler.""" - hass.async_create_task(_handle_async_response(func, hass, connection, msg)) + # As the webserver is now started before the start + # event we do not want to block for websocket responders + asyncio.create_task(_handle_async_response(func, hass, connection, msg)) return schedule_handler diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index b22eff150ba..ab412e06583 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -42,7 +42,7 @@ class WebsocketAPIView(HomeAssistantView): url = URL requires_auth = False - async def get(self, request): + async def get(self, request: web.Request) -> web.WebSocketResponse: """Handle an incoming websocket connection.""" return await WebSocketHandler(request.app["hass"], request).async_handle() @@ -148,7 +148,7 @@ class WebSocketHandler: self._handle_task.cancel() self._writer_task.cancel() - async def async_handle(self): + async def async_handle(self) -> web.WebSocketResponse: """Handle a websocket response.""" request = self.request wsock = self.wsock = web.WebSocketResponse(heartbeat=55) @@ -165,7 +165,9 @@ class WebSocketHandler: EVENT_HOMEASSISTANT_STOP, handle_hass_stop ) - self._writer_task = self.hass.async_create_task(self._writer()) + # As the webserver is now started before the start + # event we do not want to block for websocket responses + self._writer_task = asyncio.create_task(self._writer()) auth = AuthPhase(self._logger, self.hass, self._send_message, request) connection = None diff --git a/homeassistant/components/websocket_api/sensor.py b/homeassistant/components/websocket_api/sensor.py index 6be07dfb1f4..c026978634f 100644 --- a/homeassistant/components/websocket_api/sensor.py +++ b/homeassistant/components/websocket_api/sensor.py @@ -24,7 +24,7 @@ class APICount(Entity): def __init__(self): """Initialize the API count.""" - self.count = None + self.count = 0 async def async_added_to_hass(self): """Added to hass.""" @@ -38,7 +38,6 @@ class APICount(Entity): SIGNAL_WEBSOCKET_DISCONNECTED, self._update_count ) ) - self._update_count() @property def name(self): diff --git a/homeassistant/components/wemo/binary_sensor.py b/homeassistant/components/wemo/binary_sensor.py index 805a730e3d9..b5ef3dc528b 100644 --- a/homeassistant/components/wemo/binary_sensor.py +++ b/homeassistant/components/wemo/binary_sensor.py @@ -3,6 +3,7 @@ import asyncio import logging import async_timeout +from pywemo.ouimeaux_device.api.service import ActionException from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -40,7 +41,7 @@ class WemoBinarySensor(BinarySensorEntity): self._update_lock = None self._model_name = self.wemo.model_name self._name = self.wemo.name - self._serialnumber = self.wemo.serialnumber + self._serial_number = self.wemo.serialnumber def _subscription_callback(self, _device, _type, _params): """Update the state by the Wemo sensor.""" @@ -98,14 +99,15 @@ class WemoBinarySensor(BinarySensorEntity): if not self._available: _LOGGER.info("Reconnected to %s", self.name) self._available = True - except AttributeError as err: + except (AttributeError, ActionException) as err: _LOGGER.warning("Could not update status for %s (%s)", self.name, err) self._available = False + self.wemo.reconnect_with_device() @property def unique_id(self): """Return the id of this WeMo sensor.""" - return self._serialnumber + return self._serial_number @property def name(self): @@ -126,8 +128,8 @@ class WemoBinarySensor(BinarySensorEntity): def device_info(self): """Return the device info.""" return { - "name": self.wemo.name, - "identifiers": {(WEMO_DOMAIN, self.wemo.serialnumber)}, - "model": self.wemo.model_name, + "name": self._name, + "identifiers": {(WEMO_DOMAIN, self._serial_number)}, + "model": self._model_name, "manufacturer": "Belkin", } diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index 24ed68c792d..f040a9f3845 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -4,6 +4,7 @@ from datetime import timedelta import logging import async_timeout +from pywemo.ouimeaux_device.api.service import ActionException import voluptuous as vol from homeassistant.components.fan import ( @@ -128,7 +129,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): # Register service(s) hass.services.async_register( - WEMO_DOMAIN, SERVICE_SET_HUMIDITY, service_handle, schema=SET_HUMIDITY_SCHEMA + WEMO_DOMAIN, SERVICE_SET_HUMIDITY, service_handle, schema=SET_HUMIDITY_SCHEMA, ) hass.services.async_register( @@ -198,9 +199,9 @@ class WemoHumidifier(FanEntity): def device_info(self): """Return the device info.""" return { - "name": self.wemo.name, - "identifiers": {(WEMO_DOMAIN, self.wemo.serialnumber)}, - "model": self.wemo.model_name, + "name": self._name, + "identifiers": {(WEMO_DOMAIN, self._serialnumber)}, + "model": self._model_name, "manufacturer": "Belkin", } @@ -287,38 +288,67 @@ class WemoHumidifier(FanEntity): if not self._available: _LOGGER.info("Reconnected to %s", self.name) self._available = True - except AttributeError as err: + except (AttributeError, ActionException) as err: _LOGGER.warning("Could not update status for %s (%s)", self.name, err) self._available = False + self.wemo.reconnect_with_device() def turn_on(self, speed: str = None, **kwargs) -> None: """Turn the switch on.""" if speed is None: - self.wemo.set_state(self._last_fan_on_mode) + try: + self.wemo.set_state(self._last_fan_on_mode) + except ActionException as err: + _LOGGER.warning("Error while turning on device %s (%s)", self.name, err) + self._available = False else: self.set_speed(speed) def turn_off(self, **kwargs) -> None: """Turn the switch off.""" - self.wemo.set_state(WEMO_FAN_OFF) + try: + self.wemo.set_state(WEMO_FAN_OFF) + except ActionException as err: + _LOGGER.warning("Error while turning off device %s (%s)", self.name, err) + self._available = False def set_speed(self, speed: str) -> None: """Set the fan_mode of the Humidifier.""" - self.wemo.set_state(HASS_FAN_SPEED_TO_WEMO.get(speed)) + try: + self.wemo.set_state(HASS_FAN_SPEED_TO_WEMO.get(speed)) + except ActionException as err: + _LOGGER.warning( + "Error while setting speed of device %s (%s)", self.name, err + ) + self._available = False def set_humidity(self, humidity: float) -> None: """Set the target humidity level for the Humidifier.""" if humidity < 50: - self.wemo.set_humidity(WEMO_HUMIDITY_45) + target_humidity = WEMO_HUMIDITY_45 elif 50 <= humidity < 55: - self.wemo.set_humidity(WEMO_HUMIDITY_50) + target_humidity = WEMO_HUMIDITY_50 elif 55 <= humidity < 60: - self.wemo.set_humidity(WEMO_HUMIDITY_55) + target_humidity = WEMO_HUMIDITY_55 elif 60 <= humidity < 100: - self.wemo.set_humidity(WEMO_HUMIDITY_60) + target_humidity = WEMO_HUMIDITY_60 elif humidity >= 100: - self.wemo.set_humidity(WEMO_HUMIDITY_100) + target_humidity = WEMO_HUMIDITY_100 + + try: + self.wemo.set_humidity(target_humidity) + except ActionException as err: + _LOGGER.warning( + "Error while setting humidity of device: %s (%s)", self.name, err + ) + self._available = False def reset_filter_life(self) -> None: """Reset the filter life to 100%.""" - self.wemo.reset_filter_life() + try: + self.wemo.reset_filter_life() + except ActionException as err: + _LOGGER.warning( + "Error while resetting filter life on device: %s (%s)", self.name, err + ) + self._available = False diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index 02b5e79d1c6..2a05d42f1f7 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -4,6 +4,7 @@ from datetime import timedelta import logging import async_timeout +from pywemo.ouimeaux_device.api.service import ActionException from homeassistant import util from homeassistant.components.light import ( @@ -92,6 +93,7 @@ class WemoLight(LightEntity): self._is_on = None self._name = self.wemo.name self._unique_id = self.wemo.uniqueID + self._model_name = type(self.wemo).__name__ async def async_added_to_hass(self): """Wemo light added to Home Assistant.""" @@ -112,9 +114,9 @@ class WemoLight(LightEntity): def device_info(self): """Return the device info.""" return { - "name": self.wemo.name, - "identifiers": {(WEMO_DOMAIN, self.wemo.uniqueID)}, - "model": type(self.wemo).__name__, + "name": self._name, + "identifiers": {(WEMO_DOMAIN, self._unique_id)}, + "model": self._model_name, "manufacturer": "Belkin", } @@ -150,45 +152,65 @@ class WemoLight(LightEntity): def turn_on(self, **kwargs): """Turn the light on.""" - transitiontime = int(kwargs.get(ATTR_TRANSITION, 0)) + xy_color = None + brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness or 255) + color_temp = kwargs.get(ATTR_COLOR_TEMP) hs_color = kwargs.get(ATTR_HS_COLOR) + transition_time = int(kwargs.get(ATTR_TRANSITION, 0)) if hs_color is not None: xy_color = color_util.color_hs_to_xy(*hs_color) - self.wemo.set_color(xy_color, transition=transitiontime) - if ATTR_COLOR_TEMP in kwargs: - colortemp = kwargs[ATTR_COLOR_TEMP] - self.wemo.set_temperature(mireds=colortemp, transition=transitiontime) + turn_on_kwargs = { + "level": brightness, + "transition": transition_time, + "force_update": False, + } - if ATTR_BRIGHTNESS in kwargs: - brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness or 255) - self.wemo.turn_on(level=brightness, transition=transitiontime) - else: - self.wemo.turn_on(transition=transitiontime) + try: + if xy_color is not None: + self.wemo.set_color(xy_color, transition=transition_time) + + if color_temp is not None: + self.wemo.set_temperature(mireds=color_temp, transition=transition_time) + + self.wemo.turn_on(**turn_on_kwargs) + except ActionException as err: + _LOGGER.warning("Error while turning on device %s (%s)", self.name, err) + self._available = False def turn_off(self, **kwargs): """Turn the light off.""" - transitiontime = int(kwargs.get(ATTR_TRANSITION, 0)) - self.wemo.turn_off(transition=transitiontime) + transition_time = int(kwargs.get(ATTR_TRANSITION, 0)) + + try: + self.wemo.turn_off(transition=transition_time) + except ActionException as err: + _LOGGER.warning("Error while turning off device %s (%s)", self.name, err) + self._available = False def _update(self, force_update=True): """Synchronize state with bridge.""" - self._update_lights(no_throttle=force_update) - self._state = self.wemo.state - - self._is_on = self._state.get("onoff") != 0 - self._brightness = self._state.get("level", 255) - self._color_temp = self._state.get("temperature_mireds") - self._available = True - - xy_color = self._state.get("color_xy") - - if xy_color: - self._hs_color = color_util.color_xy_to_hs(*xy_color) + try: + self._update_lights(no_throttle=force_update) + self._state = self.wemo.state + except (AttributeError, ActionException) as err: + _LOGGER.warning("Could not update status for %s (%s)", self.name, err) + self._available = False + self.wemo.reconnect_with_device() else: - self._hs_color = None + self._is_on = self._state.get("onoff") != 0 + self._brightness = self._state.get("level", 255) + self._color_temp = self._state.get("temperature_mireds") + self._available = True + + xy_color = self._state.get("color_xy") + + if xy_color: + self._hs_color = color_util.color_xy_to_hs(*xy_color) + else: + self._hs_color = None async def async_update(self): """Synchronize state with bridge.""" @@ -265,7 +287,6 @@ class WemoDimmer(LightEntity): except asyncio.TimeoutError: _LOGGER.warning("Lost connection to %s", self.name) self._available = False - self.wemo.reconnect_with_device() async def _async_locked_update(self, force_update): """Try updating within an async lock.""" @@ -282,6 +303,16 @@ class WemoDimmer(LightEntity): """Return the name of the dimmer if any.""" return self._name + @property + def device_info(self): + """Return the device info.""" + return { + "name": self._name, + "identifiers": {(WEMO_DOMAIN, self._serialnumber)}, + "model": self._model_name, + "manufacturer": "Belkin", + } + @property def supported_features(self): """Flag supported features.""" @@ -308,14 +339,13 @@ class WemoDimmer(LightEntity): if not self._available: _LOGGER.info("Reconnected to %s", self.name) self._available = True - except AttributeError as err: + except (AttributeError, ActionException) as err: _LOGGER.warning("Could not update status for %s (%s)", self.name, err) self._available = False + self.wemo.reconnect_with_device() def turn_on(self, **kwargs): """Turn the dimmer on.""" - self.wemo.on() - # Wemo dimmer switches use a range of [0, 100] to control # brightness. Level 255 might mean to set it to previous value if ATTR_BRIGHTNESS in kwargs: @@ -323,11 +353,21 @@ class WemoDimmer(LightEntity): brightness = int((brightness / 255) * 100) else: brightness = 255 - self.wemo.set_brightness(brightness) + + try: + self.wemo.on() + self.wemo.set_brightness(brightness) + except ActionException as err: + _LOGGER.warning("Error while turning on device %s (%s)", self.name, err) + self._available = False def turn_off(self, **kwargs): """Turn the dimmer off.""" - self.wemo.off() + try: + self.wemo.off() + except ActionException as err: + _LOGGER.warning("Error while turning on device %s (%s)", self.name, err) + self._available = False @property def available(self): diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index 0ad4574ecbc..96efb140cee 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -3,7 +3,7 @@ "name": "Belkin WeMo", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wemo", - "requirements": ["pywemo==0.4.34"], + "requirements": ["pywemo==0.4.43"], "ssdp": [ { "manufacturer": "Belkin International Inc." diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index 203c495e974..836ddf0730f 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -4,6 +4,7 @@ from datetime import datetime, timedelta import logging import async_timeout +from pywemo.ouimeaux_device.api.service import ActionException from homeassistant.components.switch import SwitchEntity from homeassistant.const import STATE_OFF, STATE_ON, STATE_STANDBY, STATE_UNKNOWN @@ -93,9 +94,9 @@ class WemoSwitch(SwitchEntity): def device_info(self): """Return the device info.""" return { - "name": self.wemo.name, - "identifiers": {(WEMO_DOMAIN, self.wemo.serialnumber)}, - "model": self.wemo.model_name, + "name": self._name, + "identifiers": {(WEMO_DOMAIN, self._serialnumber)}, + "model": self._model_name, "manufacturer": "Belkin", } @@ -189,11 +190,19 @@ class WemoSwitch(SwitchEntity): def turn_on(self, **kwargs): """Turn the switch on.""" - self.wemo.on() + try: + self.wemo.on() + except ActionException as err: + _LOGGER.warning("Error while turning on device %s (%s)", self.name, err) + self._available = False def turn_off(self, **kwargs): """Turn the switch off.""" - self.wemo.off() + try: + self.wemo.off() + except ActionException as err: + _LOGGER.warning("Error while turning off device %s (%s)", self.name, err) + self._available = False async def async_added_to_hass(self): """Wemo switch added to Home Assistant.""" @@ -245,6 +254,7 @@ class WemoSwitch(SwitchEntity): if not self._available: _LOGGER.info("Reconnected to %s", self.name) self._available = True - except AttributeError as err: + except (AttributeError, ActionException) as err: _LOGGER.warning("Could not update status for %s (%s)", self.name, err) self._available = False + self.wemo.reconnect_with_device() diff --git a/homeassistant/components/wemo/translations/bg.json b/homeassistant/components/wemo/translations/bg.json index 19f2121f42a..d532f7088f1 100644 --- a/homeassistant/components/wemo/translations/bg.json +++ b/homeassistant/components/wemo/translations/bg.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Wemo?", - "title": "Wemo" + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Wemo?" } } } diff --git a/homeassistant/components/wemo/translations/ca.json b/homeassistant/components/wemo/translations/ca.json index 36ce6153796..007252042cc 100644 --- a/homeassistant/components/wemo/translations/ca.json +++ b/homeassistant/components/wemo/translations/ca.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Vols configurar Wemo?", - "title": "Wemo" + "description": "Vols configurar Wemo?" } } } diff --git a/homeassistant/components/wemo/translations/da.json b/homeassistant/components/wemo/translations/da.json index d2192d85f4b..4094128de25 100644 --- a/homeassistant/components/wemo/translations/da.json +++ b/homeassistant/components/wemo/translations/da.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Vil du konfigurere Wemo?", - "title": "Wemo" + "description": "Vil du konfigurere Wemo?" } } } diff --git a/homeassistant/components/wemo/translations/de.json b/homeassistant/components/wemo/translations/de.json index a0d4465796d..f20ad5598ab 100644 --- a/homeassistant/components/wemo/translations/de.json +++ b/homeassistant/components/wemo/translations/de.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "M\u00f6chtest du Wemo einrichten?", - "title": "Wemo" + "description": "M\u00f6chtest du Wemo einrichten?" } } } diff --git a/homeassistant/components/wemo/translations/en.json b/homeassistant/components/wemo/translations/en.json index ef86613ea79..32ef4cc4cf5 100644 --- a/homeassistant/components/wemo/translations/en.json +++ b/homeassistant/components/wemo/translations/en.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Do you want to set up Wemo?", - "title": "Wemo" + "description": "Do you want to set up Wemo?" } } } diff --git a/homeassistant/components/wemo/translations/es-419.json b/homeassistant/components/wemo/translations/es-419.json index 3010cfed63b..56bcfd9249e 100644 --- a/homeassistant/components/wemo/translations/es-419.json +++ b/homeassistant/components/wemo/translations/es-419.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u00bfDesea configurar Wemo?", - "title": "Wemo" + "description": "\u00bfDesea configurar Wemo?" } } } diff --git a/homeassistant/components/wemo/translations/es.json b/homeassistant/components/wemo/translations/es.json index c8c45bb1083..1c7088a5280 100644 --- a/homeassistant/components/wemo/translations/es.json +++ b/homeassistant/components/wemo/translations/es.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u00bfQuieres configurar Wemo?", - "title": "Wemo" + "description": "\u00bfQuieres configurar Wemo?" } } } diff --git a/homeassistant/components/wemo/translations/fi.json b/homeassistant/components/wemo/translations/fi.json new file mode 100644 index 00000000000..afce1415ba7 --- /dev/null +++ b/homeassistant/components/wemo/translations/fi.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "confirm": { + "title": "Wemo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wemo/translations/fr.json b/homeassistant/components/wemo/translations/fr.json index 47b7d0e6b74..ccf2ac6ef21 100644 --- a/homeassistant/components/wemo/translations/fr.json +++ b/homeassistant/components/wemo/translations/fr.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Voulez-vous configurer Wemo?", - "title": "Wemo" + "description": "Voulez-vous configurer Wemo?" } } } diff --git a/homeassistant/components/wemo/translations/it.json b/homeassistant/components/wemo/translations/it.json index a14a47a459b..46204dc6057 100644 --- a/homeassistant/components/wemo/translations/it.json +++ b/homeassistant/components/wemo/translations/it.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Vuoi configurare Wemo?", - "title": "Wemo" + "description": "Vuoi configurare Wemo?" } } } diff --git a/homeassistant/components/wemo/translations/ko.json b/homeassistant/components/wemo/translations/ko.json index 41de2e3aaeb..a262f7ebd3e 100644 --- a/homeassistant/components/wemo/translations/ko.json +++ b/homeassistant/components/wemo/translations/ko.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Wemo \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "Wemo" + "description": "Wemo \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" } } } diff --git a/homeassistant/components/wemo/translations/lb.json b/homeassistant/components/wemo/translations/lb.json index 8602a1adcbf..9ec38088d1b 100644 --- a/homeassistant/components/wemo/translations/lb.json +++ b/homeassistant/components/wemo/translations/lb.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Soll Wemo konfigur\u00e9iert ginn?", - "title": "Wemo" + "description": "Soll Wemo konfigur\u00e9iert ginn?" } } } diff --git a/homeassistant/components/wemo/translations/nl.json b/homeassistant/components/wemo/translations/nl.json index dc40fd0d66b..6146072623a 100644 --- a/homeassistant/components/wemo/translations/nl.json +++ b/homeassistant/components/wemo/translations/nl.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Wilt u Wemo instellen?", - "title": "Wemo" + "description": "Wilt u Wemo instellen?" } } } diff --git a/homeassistant/components/wemo/translations/no.json b/homeassistant/components/wemo/translations/no.json index 13476f63ec2..59ff68d5790 100644 --- a/homeassistant/components/wemo/translations/no.json +++ b/homeassistant/components/wemo/translations/no.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u00d8nsker du \u00e5 sette opp Wemo?", - "title": "" + "description": "\u00d8nsker du \u00e5 sette opp Wemo?" } } } diff --git a/homeassistant/components/wemo/translations/pl.json b/homeassistant/components/wemo/translations/pl.json index c1c7d9746d5..456dca40d4f 100644 --- a/homeassistant/components/wemo/translations/pl.json +++ b/homeassistant/components/wemo/translations/pl.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Czy chcesz skonfigurowa\u0107 Wemo?", - "title": "Wemo" + "description": "Czy chcesz skonfigurowa\u0107 Wemo?" } } } diff --git a/homeassistant/components/wemo/translations/pt-BR.json b/homeassistant/components/wemo/translations/pt-BR.json index a4fb8960af9..c14cb64bf4e 100644 --- a/homeassistant/components/wemo/translations/pt-BR.json +++ b/homeassistant/components/wemo/translations/pt-BR.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Voc\u00ea quer configurar o Wemo?", - "title": "Wemo" + "description": "Voc\u00ea quer configurar o Wemo?" } } } diff --git a/homeassistant/components/wemo/translations/ru.json b/homeassistant/components/wemo/translations/ru.json index 5f5c4380586..7123ac1b601 100644 --- a/homeassistant/components/wemo/translations/ru.json +++ b/homeassistant/components/wemo/translations/ru.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Wemo?", - "title": "Wemo" + "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 Wemo?" } } } diff --git a/homeassistant/components/wemo/translations/sl.json b/homeassistant/components/wemo/translations/sl.json index 8f754b309be..e499e7eb787 100644 --- a/homeassistant/components/wemo/translations/sl.json +++ b/homeassistant/components/wemo/translations/sl.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Ali \u017eelite nastaviti Wemo?", - "title": "Wemo" + "description": "Ali \u017eelite nastaviti Wemo?" } } } diff --git a/homeassistant/components/wemo/translations/sv.json b/homeassistant/components/wemo/translations/sv.json index 8476f17077c..be479aa1333 100644 --- a/homeassistant/components/wemo/translations/sv.json +++ b/homeassistant/components/wemo/translations/sv.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "Vill du konfigurera Wemo?", - "title": "Wemo" + "description": "Vill du konfigurera Wemo?" } } } diff --git a/homeassistant/components/wemo/translations/zh-Hant.json b/homeassistant/components/wemo/translations/zh-Hant.json index b3845e0df57..29167a13480 100644 --- a/homeassistant/components/wemo/translations/zh-Hant.json +++ b/homeassistant/components/wemo/translations/zh-Hant.json @@ -6,8 +6,7 @@ }, "step": { "confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a Wemo\uff1f", - "title": "Wemo" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Wemo\uff1f" } } } diff --git a/homeassistant/components/wiffi/__init__.py b/homeassistant/components/wiffi/__init__.py index 05d0d381e18..000b961bda9 100644 --- a/homeassistant/components/wiffi/__init__.py +++ b/homeassistant/components/wiffi/__init__.py @@ -7,7 +7,7 @@ import logging from wiffi import WiffiTcpServer from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PORT +from homeassistant.const import CONF_PORT, CONF_TIMEOUT from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry @@ -22,6 +22,7 @@ from homeassistant.util.dt import utcnow from .const import ( CHECK_ENTITIES_SIGNAL, CREATE_ENTITY_SIGNAL, + DEFAULT_TIMEOUT, DOMAIN, UPDATE_ENTITY_SIGNAL, ) @@ -39,6 +40,9 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): """Set up wiffi from a config entry, config_entry contains data from config entry database.""" + if not config_entry.update_listeners: + config_entry.add_update_listener(async_update_options) + # create api object api = WiffiIntegrationApi(hass) api.async_setup(config_entry) @@ -63,6 +67,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): return True +async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry): + """Update options.""" + await hass.config_entries.async_reload(config_entry.entry_id) + + async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): """Unload a config entry.""" api: "WiffiIntegrationApi" = hass.data[DOMAIN][config_entry.entry_id] @@ -146,7 +155,7 @@ class WiffiIntegrationApi: class WiffiEntity(Entity): """Common functionality for all wiffi entities.""" - def __init__(self, device, metric): + def __init__(self, device, metric, options): """Initialize the base elements of a wiffi entity.""" self._id = generate_unique_id(device, metric) self._device_info = { @@ -162,6 +171,7 @@ class WiffiEntity(Entity): self._name = metric.description self._expiration_date = None self._value = None + self._timeout = options.get(CONF_TIMEOUT, DEFAULT_TIMEOUT) async def async_added_to_hass(self): """Entity has been added to hass.""" @@ -208,7 +218,7 @@ class WiffiEntity(Entity): Will be called by derived classes after a value update has been received. """ - self._expiration_date = utcnow() + timedelta(minutes=3) + self._expiration_date = utcnow() + timedelta(minutes=self._timeout) @callback def _update_value_callback(self, device, metric): diff --git a/homeassistant/components/wiffi/binary_sensor.py b/homeassistant/components/wiffi/binary_sensor.py index 009fc2b4a67..f6063b3c202 100644 --- a/homeassistant/components/wiffi/binary_sensor.py +++ b/homeassistant/components/wiffi/binary_sensor.py @@ -21,7 +21,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [] if metric.is_bool: - entities.append(BoolEntity(device, metric)) + entities.append(BoolEntity(device, metric, config_entry.options)) async_add_entities(entities) @@ -31,9 +31,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class BoolEntity(WiffiEntity, BinarySensorEntity): """Entity for wiffi metrics which have a boolean value.""" - def __init__(self, device, metric): + def __init__(self, device, metric, options): """Initialize the entity.""" - super().__init__(device, metric) + super().__init__(device, metric, options) self._value = metric.value self.reset_expiration_date() diff --git a/homeassistant/components/wiffi/config_flow.py b/homeassistant/components/wiffi/config_flow.py index 82dbbb040ef..f30ee8792df 100644 --- a/homeassistant/components/wiffi/config_flow.py +++ b/homeassistant/components/wiffi/config_flow.py @@ -8,10 +8,14 @@ import voluptuous as vol from wiffi import WiffiTcpServer from homeassistant import config_entries -from homeassistant.const import CONF_PORT +from homeassistant.const import CONF_PORT, CONF_TIMEOUT from homeassistant.core import callback -from .const import DEFAULT_PORT, DOMAIN # pylint: disable=unused-import +from .const import ( # pylint: disable=unused-import + DEFAULT_PORT, + DEFAULT_TIMEOUT, + DOMAIN, +) class WiffiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -20,6 +24,12 @@ class WiffiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Create Wiffi server setup option flow.""" + return OptionsFlowHandler(config_entry) + async def async_step_user(self, user_input=None): """Handle the start of the config flow. @@ -55,3 +65,30 @@ class WiffiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", data_schema=vol.Schema(data_schema), errors=errors or {} ) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Wiffi server setup option flow.""" + + def __init__(self, config_entry): + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_TIMEOUT, + default=self.config_entry.options.get( + CONF_TIMEOUT, DEFAULT_TIMEOUT + ), + ): int, + } + ), + ) diff --git a/homeassistant/components/wiffi/const.py b/homeassistant/components/wiffi/const.py index 6b71c89002f..584ab9899b6 100644 --- a/homeassistant/components/wiffi/const.py +++ b/homeassistant/components/wiffi/const.py @@ -6,6 +6,9 @@ DOMAIN = "wiffi" # Default port for TCP server DEFAULT_PORT = 8189 +# Default timeout in minutes +DEFAULT_TIMEOUT = 3 + # Signal name to send create/update to platform (sensor/binary_sensor) CREATE_ENTITY_SIGNAL = "wiffi_create_entity_signal" UPDATE_ENTITY_SIGNAL = "wiffi_update_entity_signal" diff --git a/homeassistant/components/wiffi/sensor.py b/homeassistant/components/wiffi/sensor.py index cc6befaf067..f207e3be3ac 100644 --- a/homeassistant/components/wiffi/sensor.py +++ b/homeassistant/components/wiffi/sensor.py @@ -49,9 +49,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [] if metric.is_number: - entities.append(NumberEntity(device, metric)) + entities.append(NumberEntity(device, metric, config_entry.options)) elif metric.is_string: - entities.append(StringEntity(device, metric)) + entities.append(StringEntity(device, metric, config_entry.options)) async_add_entities(entities) @@ -61,9 +61,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class NumberEntity(WiffiEntity): """Entity for wiffi metrics which have a number value.""" - def __init__(self, device, metric): + def __init__(self, device, metric, options): """Initialize the entity.""" - super().__init__(device, metric) + super().__init__(device, metric, options) self._device_class = UOM_TO_DEVICE_CLASS_MAP.get(metric.unit_of_measurement) self._unit_of_measurement = UOM_MAP.get( metric.unit_of_measurement, metric.unit_of_measurement @@ -103,9 +103,9 @@ class NumberEntity(WiffiEntity): class StringEntity(WiffiEntity): """Entity for wiffi metrics which have a string value.""" - def __init__(self, device, metric): + def __init__(self, device, metric, options): """Initialize the entity.""" - super().__init__(device, metric) + super().__init__(device, metric, options) self._value = metric.value self.reset_expiration_date() diff --git a/homeassistant/components/wiffi/strings.json b/homeassistant/components/wiffi/strings.json index 36f836366a5..e219b2ecae7 100644 --- a/homeassistant/components/wiffi/strings.json +++ b/homeassistant/components/wiffi/strings.json @@ -12,5 +12,14 @@ "addr_in_use": "Server port already in use.", "start_server_failed": "Start server failed." } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Timeout (minutes)" + } + } + } } } diff --git a/homeassistant/components/wiffi/translations/ca.json b/homeassistant/components/wiffi/translations/ca.json new file mode 100644 index 00000000000..33fc5015ecd --- /dev/null +++ b/homeassistant/components/wiffi/translations/ca.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "addr_in_use": "El port del servidor que ja est\u00e0 en us.", + "start_server_failed": "Ha fallat l'inici del servidor." + }, + "step": { + "user": { + "data": { + "port": "Port del servidor" + }, + "title": "Configuraci\u00f3 del servidor TCP per a dispositius WIFFI" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Temps m\u00e0xim d'espera (minuts)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiffi/translations/de.json b/homeassistant/components/wiffi/translations/de.json new file mode 100644 index 00000000000..79bf8168a14 --- /dev/null +++ b/homeassistant/components/wiffi/translations/de.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "addr_in_use": "Server Port wird bereits genutzt", + "start_server_failed": "Server starten fehlgeschlagen" + }, + "step": { + "user": { + "data": { + "port": "Server Port" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Zeit\u00fcberschreitung (Minuten)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiffi/translations/en.json b/homeassistant/components/wiffi/translations/en.json index bcaf0820bd5..0ac1868714d 100644 --- a/homeassistant/components/wiffi/translations/en.json +++ b/homeassistant/components/wiffi/translations/en.json @@ -12,5 +12,14 @@ "title": "Setup TCP server for WIFFI devices" } } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Timeout (minutes)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/wiffi/translations/es.json b/homeassistant/components/wiffi/translations/es.json new file mode 100644 index 00000000000..6ab1f5c5407 --- /dev/null +++ b/homeassistant/components/wiffi/translations/es.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "addr_in_use": "El puerto del servidor ya est\u00e1 en uso.", + "start_server_failed": "No se pudo iniciar el servidor." + }, + "step": { + "user": { + "data": { + "port": "Puerto del servidor" + }, + "title": "Configurar servidor TCP para dispositivos WIFFI" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Tiempo de espera (minutos)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiffi/translations/it.json b/homeassistant/components/wiffi/translations/it.json new file mode 100644 index 00000000000..0884f31cbd7 --- /dev/null +++ b/homeassistant/components/wiffi/translations/it.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "addr_in_use": "Porta del server gi\u00e0 in uso.", + "start_server_failed": "Avvio del server non riuscito." + }, + "step": { + "user": { + "data": { + "port": "Porta del server" + }, + "title": "Configurare il server TCP per i dispositivi WIFFI" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Timeout (minuti)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiffi/translations/ko.json b/homeassistant/components/wiffi/translations/ko.json new file mode 100644 index 00000000000..c332d3e5f26 --- /dev/null +++ b/homeassistant/components/wiffi/translations/ko.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "addr_in_use": "\uc11c\ubc84 \ud3ec\ud2b8\uac00 \uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4.", + "start_server_failed": "\uc11c\ubc84 \uc2e4\ud589\uc774 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4." + }, + "step": { + "user": { + "data": { + "port": "\uc11c\ubc84 \ud3ec\ud2b8" + }, + "title": "WIFFI \uae30\uae30\uc6a9 TCP \uc11c\ubc84 \uc124\uc815\ud558\uae30" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "\uc81c\ud55c \uc2dc\uac04 (\ubd84)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiffi/translations/lb.json b/homeassistant/components/wiffi/translations/lb.json new file mode 100644 index 00000000000..f9080d52584 --- /dev/null +++ b/homeassistant/components/wiffi/translations/lb.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "addr_in_use": "Server Port g\u00ebtt scho benotzt.", + "start_server_failed": "Feeler beim starte vum Server" + }, + "step": { + "user": { + "data": { + "port": "Server Port" + }, + "title": "TCP Server fir WIFFI Apparater ariichten" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Z\u00e4itiwwerscheidung (minutten)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiffi/translations/nl.json b/homeassistant/components/wiffi/translations/nl.json new file mode 100644 index 00000000000..af14d1942a7 --- /dev/null +++ b/homeassistant/components/wiffi/translations/nl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "addr_in_use": "Serverpoort al in gebruik.", + "start_server_failed": "Start server is mislukt." + }, + "step": { + "user": { + "data": { + "port": "Server poort" + }, + "title": "TCP-server instellen voor WIFFI-apparaten" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Time-out (minuten)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiffi/translations/no.json b/homeassistant/components/wiffi/translations/no.json index 8e966a19c00..9f745e0e4a8 100644 --- a/homeassistant/components/wiffi/translations/no.json +++ b/homeassistant/components/wiffi/translations/no.json @@ -12,5 +12,14 @@ "title": "Sett opp TCP-server for WIFFI-enheter" } } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Timeout (minutter)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/wiffi/translations/pl.json b/homeassistant/components/wiffi/translations/pl.json new file mode 100644 index 00000000000..810bc4fb1ec --- /dev/null +++ b/homeassistant/components/wiffi/translations/pl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "addr_in_use": "Port serwera jest ju\u017c w u\u017cyciu.", + "start_server_failed": "Uruchomienie serwera nie powiod\u0142o si\u0119." + }, + "step": { + "user": { + "data": { + "port": "Port serwera" + }, + "title": "Konfiguracja serwera TCP dla urz\u0105dze\u0144 WIFFI" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Limit czasu (minuty)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiffi/translations/pt-BR.json b/homeassistant/components/wiffi/translations/pt-BR.json new file mode 100644 index 00000000000..cbe6c6f78e7 --- /dev/null +++ b/homeassistant/components/wiffi/translations/pt-BR.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "addr_in_use": "Porta do servidor j\u00e1 em uso.", + "start_server_failed": "Falha ao iniciar o servidor." + }, + "step": { + "user": { + "data": { + "port": "Porta do servidor" + }, + "title": "Configurar servidor TCP para dispositivos WIFFI" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiffi/translations/ru.json b/homeassistant/components/wiffi/translations/ru.json index e54389a0f34..50d0ec06718 100644 --- a/homeassistant/components/wiffi/translations/ru.json +++ b/homeassistant/components/wiffi/translations/ru.json @@ -12,5 +12,14 @@ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 TCP-\u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0434\u043b\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 WIFFI" } } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 (\u0432 \u043c\u0438\u043d\u0443\u0442\u0430\u0445)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/wiffi/translations/zh-Hant.json b/homeassistant/components/wiffi/translations/zh-Hant.json index 0135fe86488..77b1488025c 100644 --- a/homeassistant/components/wiffi/translations/zh-Hant.json +++ b/homeassistant/components/wiffi/translations/zh-Hant.json @@ -12,5 +12,14 @@ "title": "\u8a2d\u5b9a WIFFI \u8a2d\u5099 TCP \u4f3a\u670d\u5668" } } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "\u903e\u6642\uff08\u5206\uff09" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index 1763a34fd87..26666bf4b15 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -14,6 +14,8 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_NAME, + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, CONF_EMAIL, CONF_PASSWORD, EVENT_HOMEASSISTANT_START, @@ -38,8 +40,6 @@ DOMAIN = "wink" SUBSCRIPTION_HANDLER = None -CONF_CLIENT_ID = "client_id" -CONF_CLIENT_SECRET = "client_secret" CONF_USER_AGENT = "user_agent" CONF_OAUTH = "oauth" CONF_LOCAL_CONTROL = "local_control" @@ -47,8 +47,6 @@ CONF_MISSING_OAUTH_MSG = "Missing oauth2 credentials." ATTR_ACCESS_TOKEN = "access_token" ATTR_REFRESH_TOKEN = "refresh_token" -ATTR_CLIENT_ID = "client_id" -ATTR_CLIENT_SECRET = "client_secret" ATTR_PAIRING_MODE = "pairing_mode" ATTR_KIDDE_RADIO_CODE = "kidde_radio_code" ATTR_HUB_NAME = "hub_name" @@ -58,7 +56,10 @@ WINK_AUTH_START = "/auth/wink" WINK_CONFIG_FILE = ".wink.conf" USER_AGENT = f"Manufacturer/Home-Assistant{__version__} python/3 Wink/3" -DEFAULT_CONFIG = {"client_id": "CLIENT_ID_HERE", "client_secret": "CLIENT_SECRET_HERE"} +DEFAULT_CONFIG = { + CONF_CLIENT_ID: "CLIENT_ID_HERE", + CONF_CLIENT_SECRET: "CLIENT_SECRET_HERE", +} SERVICE_ADD_NEW_DEVICES = "pull_newly_added_devices_from_wink" SERVICE_REFRESH_STATES = "refresh_state_from_wink" @@ -219,12 +220,12 @@ def _request_app_setup(hass, config): setup(hass, config) return - client_id = callback_data.get("client_id").strip() - client_secret = callback_data.get("client_secret").strip() + client_id = callback_data.get(CONF_CLIENT_ID).strip() + client_secret = callback_data.get(CONF_CLIENT_SECRET).strip() if None not in (client_id, client_secret): save_json( _config_path, - {ATTR_CLIENT_ID: client_id, ATTR_CLIENT_SECRET: client_secret}, + {CONF_CLIENT_ID: client_id, CONF_CLIENT_SECRET: client_secret}, ) setup(hass, config) return @@ -249,8 +250,8 @@ def _request_app_setup(hass, config): submit_caption="submit", description_image="/static/images/config_wink.png", fields=[ - {"id": "client_id", "name": "Client ID", "type": "string"}, - {"id": "client_secret", "name": "Client secret", "type": "string"}, + {"id": CONF_CLIENT_ID, "name": "Client ID", "type": "string"}, + {"id": CONF_CLIENT_SECRET, "name": "Client secret", "type": "string"}, ], ) @@ -293,8 +294,8 @@ def setup(hass, config): } if config.get(DOMAIN) is not None: - client_id = config[DOMAIN].get(ATTR_CLIENT_ID) - client_secret = config[DOMAIN].get(ATTR_CLIENT_SECRET) + client_id = config[DOMAIN].get(CONF_CLIENT_ID) + client_secret = config[DOMAIN].get(CONF_CLIENT_SECRET) email = config[DOMAIN].get(CONF_EMAIL) password = config[DOMAIN].get(CONF_PASSWORD) local_control = config[DOMAIN].get(CONF_LOCAL_CONTROL) @@ -309,8 +310,8 @@ def setup(hass, config): _LOGGER.info("Using legacy OAuth authentication") if not local_control: pywink.disable_local_control() - hass.data[DOMAIN]["oauth"]["client_id"] = client_id - hass.data[DOMAIN]["oauth"]["client_secret"] = client_secret + hass.data[DOMAIN]["oauth"][CONF_CLIENT_ID] = client_id + hass.data[DOMAIN]["oauth"][CONF_CLIENT_SECRET] = client_secret hass.data[DOMAIN]["oauth"]["email"] = email hass.data[DOMAIN]["oauth"]["password"] = password pywink.legacy_set_wink_credentials(email, password, client_id, client_secret) @@ -341,8 +342,8 @@ def setup(hass, config): # This will be called after authorizing Home-Assistant if None not in (access_token, refresh_token): pywink.set_wink_credentials( - config_file.get(ATTR_CLIENT_ID), - config_file.get(ATTR_CLIENT_SECRET), + config_file.get(CONF_CLIENT_ID), + config_file.get(CONF_CLIENT_SECRET), access_token=access_token, refresh_token=refresh_token, ) @@ -353,7 +354,7 @@ def setup(hass, config): redirect_uri = f"{get_url(hass)}{WINK_AUTH_CALLBACK_PATH}" wink_auth_start_url = pywink.get_authorization_url( - config_file.get(ATTR_CLIENT_ID), redirect_uri + config_file.get(CONF_CLIENT_ID), redirect_uri ) hass.http.register_redirect(WINK_AUTH_START, wink_auth_start_url) hass.http.register_view( @@ -698,14 +699,14 @@ class WinkAuthCallbackView(HomeAssistantView): if data.get("code") is not None: response = self.request_token( - data.get("code"), self.config_file["client_secret"] + data.get("code"), self.config_file[CONF_CLIENT_SECRET] ) config_contents = { ATTR_ACCESS_TOKEN: response["access_token"], ATTR_REFRESH_TOKEN: response["refresh_token"], - ATTR_CLIENT_ID: self.config_file["client_id"], - ATTR_CLIENT_SECRET: self.config_file["client_secret"], + CONF_CLIENT_ID: self.config_file[CONF_CLIENT_ID], + CONF_CLIENT_SECRET: self.config_file[CONF_CLIENT_SECRET], } save_json(hass.config.path(WINK_CONFIG_FILE), config_contents) diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 92c3f2ae155..93a6250ce03 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -7,23 +7,26 @@ import voluptuous as vol from withings_api import WithingsAuth from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from . import config_flow, const -from .common import _LOGGER, NotAuthenticatedError, get_data_manager - -DOMAIN = const.DOMAIN +from . import config_flow +from .common import ( + _LOGGER, + NotAuthenticatedError, + WithingsLocalOAuth2Implementation, + get_data_manager, +) +from .const import CONF_PROFILES, CONFIG, CREDENTIALS, DOMAIN CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( { - vol.Required(const.CLIENT_ID): vol.All(cv.string, vol.Length(min=1)), - vol.Required(const.CLIENT_SECRET): vol.All( - cv.string, vol.Length(min=1) - ), - vol.Required(const.PROFILES): vol.All( + vol.Required(CONF_CLIENT_ID): vol.All(cv.string, vol.Length(min=1)), + vol.Required(CONF_CLIENT_SECRET): vol.All(cv.string, vol.Length(min=1)), + vol.Required(CONF_PROFILES): vol.All( cv.ensure_list, vol.Unique(), vol.Length(min=1), @@ -42,15 +45,15 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: if not conf: return True - hass.data[DOMAIN] = {const.CONFIG: conf} + hass.data[DOMAIN] = {CONFIG: conf} config_flow.WithingsFlowHandler.async_register_implementation( hass, - config_entry_oauth2_flow.LocalOAuth2Implementation( + WithingsLocalOAuth2Implementation( hass, - const.DOMAIN, - conf[const.CLIENT_ID], - conf[const.CLIENT_SECRET], + DOMAIN, + conf[CONF_CLIENT_ID], + conf[CONF_CLIENT_SECRET], f"{WithingsAuth.URL}/oauth2_user/authorize2", f"{WithingsAuth.URL}/oauth2/token", ), @@ -65,12 +68,12 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool if "auth_implementation" not in entry.data: _LOGGER.debug("Upgrading existing config entry") data = entry.data - creds = data.get(const.CREDENTIALS, {}) + creds = data.get(CREDENTIALS, {}) hass.config_entries.async_update_entry( entry, data={ - "auth_implementation": const.DOMAIN, - "implementation": const.DOMAIN, + "auth_implementation": DOMAIN, + "implementation": DOMAIN, "profile": data.get("profile"), "token": { "access_token": creds.get("access_token"), diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index ac7bc149cd9..1539b973cb8 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -20,9 +20,12 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, PlatformNotReady from homeassistant.helpers.config_entry_oauth2_flow import ( + AUTH_CALLBACK_PATH, AbstractOAuth2Implementation, + LocalOAuth2Implementation, OAuth2Session, ) +from homeassistant.helpers.network import get_url from homeassistant.util import dt, slugify from . import const @@ -335,3 +338,13 @@ def get_data_manager( ) return dm_dict[entry_id] + + +class WithingsLocalOAuth2Implementation(LocalOAuth2Implementation): + """Oauth2 implementation that only uses the external url.""" + + @property + def redirect_uri(self) -> str: + """Return the redirect uri.""" + url = get_url(self.hass, allow_internal=False, prefer_cloud=True) + return f"{url}{AUTH_CALLBACK_PATH}" diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py index cd1e4e4485d..e18a4b0337a 100644 --- a/homeassistant/components/withings/config_flow.py +++ b/homeassistant/components/withings/config_flow.py @@ -51,7 +51,7 @@ class WithingsFlowHandler(config_entry_oauth2_flow.AbstractOAuth2FlowHandler): self._current_data = None return await self.async_step_finish(new_data) - profiles = self.hass.data[const.DOMAIN][const.CONFIG][const.PROFILES] + profiles = self.hass.data[const.DOMAIN][const.CONFIG][const.CONF_PROFILES] return self.async_show_form( step_id="profile", data_schema=vol.Schema({vol.Required(const.PROFILE): vol.In(profiles)}), diff --git a/homeassistant/components/withings/const.py b/homeassistant/components/withings/const.py index 172a17d2914..f2a29cfa3ca 100644 --- a/homeassistant/components/withings/const.py +++ b/homeassistant/components/withings/const.py @@ -1,19 +1,19 @@ """Constants used by the Withings component.""" import homeassistant.const as const +DOMAIN = "withings" + +CONF_PROFILES = "profiles" + DATA_MANAGER = "data_manager" BASE_URL = "base_url" -CLIENT_ID = "client_id" -CLIENT_SECRET = "client_secret" CODE = "code" CONFIG = "config" CREDENTIALS = "credentials" -DOMAIN = "withings" LOG_NAMESPACE = "homeassistant.components.withings" MEASURES = "measures" PROFILE = "profile" -PROFILES = "profiles" AUTH_CALLBACK_PATH = "/api/withings/authorize" AUTH_CALLBACK_NAME = "withings:authorize" diff --git a/homeassistant/components/withings/translations/ca.json b/homeassistant/components/withings/translations/ca.json index e98426446fc..40896dd7931 100644 --- a/homeassistant/components/withings/translations/ca.json +++ b/homeassistant/components/withings/translations/ca.json @@ -5,7 +5,7 @@ "missing_configuration": "La integraci\u00f3 Withings no est\u00e0 configurada. Mira'n la documentaci\u00f3." }, "create_entry": { - "default": "Autenticaci\u00f3 exitosa amb Withings per al perfil seleccionat." + "default": "Autenticaci\u00f3 exitosa amb Withings." }, "step": { "pick_implementation": { diff --git a/homeassistant/components/withings/translations/pl.json b/homeassistant/components/withings/translations/pl.json index 1b59e61a768..9896ba3ad5c 100644 --- a/homeassistant/components/withings/translations/pl.json +++ b/homeassistant/components/withings/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "authorize_url_timeout": "[%key_id:common::config_flow::abort::oauth2_authorize_url_timeout%]", + "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", "missing_configuration": "Integracja z Withings nie jest skonfigurowana. Post\u0119puj zgodnie z dokumentacj\u0105." }, "create_entry": { @@ -9,7 +9,7 @@ }, "step": { "pick_implementation": { - "title": "[%key_id:common::config_flow::title::oauth2_pick_implementation%]" + "title": "Wybierz metod\u0119 uwierzytelniania" }, "profile": { "data": { diff --git a/homeassistant/components/withings/translations/pt-BR.json b/homeassistant/components/withings/translations/pt-BR.json new file mode 100644 index 00000000000..f87b8b64576 --- /dev/null +++ b/homeassistant/components/withings/translations/pt-BR.json @@ -0,0 +1,7 @@ +{ + "config": { + "create_entry": { + "default": "Autenticado com sucesso no Withings." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index 6b86be6265b..6a22bf6852f 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -1,4 +1,5 @@ """Support for LED lights.""" +from functools import partial import logging from typing import Any, Callable, Dict, List, Optional, Tuple, Union @@ -20,8 +21,12 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_registry import ( + async_get_registry as async_get_entity_registry, +) from homeassistant.helpers.typing import HomeAssistantType import homeassistant.util.color as color_util @@ -70,28 +75,86 @@ async def async_setup_entry( "async_effect", ) - lights = [ - WLEDLight(entry.entry_id, coordinator, light.segment_id) - for light in coordinator.data.state.segments - ] + update_segments = partial( + async_update_segments, entry, coordinator, {}, async_add_entities + ) - async_add_entities(lights, True) + coordinator.async_add_listener(update_segments) + update_segments() -class WLEDLight(LightEntity, WLEDDeviceEntity): - """Defines a WLED light.""" +class WLEDMasterLight(LightEntity, WLEDDeviceEntity): + """Defines a WLED master light.""" + + def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator): + """Initialize WLED master light.""" + super().__init__( + entry_id=entry_id, + coordinator=coordinator, + name=f"{coordinator.data.info.name} Master", + icon="mdi:led-strip-variant", + ) + + @property + def unique_id(self) -> str: + """Return the unique ID for this sensor.""" + return f"{self.coordinator.data.info.mac_address}" + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION + + @property + def brightness(self) -> Optional[int]: + """Return the brightness of this light between 1..255.""" + return self.coordinator.data.state.brightness + + @property + def is_on(self) -> bool: + """Return the state of the light.""" + return bool(self.coordinator.data.state.on) + + @wled_exception_handler + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the light.""" + data = {ATTR_ON: False} + + if ATTR_TRANSITION in kwargs: + # WLED uses 100ms per unit, so 10 = 1 second. + data[ATTR_TRANSITION] = round(kwargs[ATTR_TRANSITION] * 10) + + await self.coordinator.wled.master(**data) + + @wled_exception_handler + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the light.""" + data = {ATTR_ON: True} + + if ATTR_TRANSITION in kwargs: + # WLED uses 100ms per unit, so 10 = 1 second. + data[ATTR_TRANSITION] = round(kwargs[ATTR_TRANSITION] * 10) + + if ATTR_BRIGHTNESS in kwargs: + data[ATTR_BRIGHTNESS] = kwargs[ATTR_BRIGHTNESS] + + await self.coordinator.wled.master(**data) + + +class WLEDSegmentLight(LightEntity, WLEDDeviceEntity): + """Defines a WLED light based on a segment.""" def __init__( self, entry_id: str, coordinator: WLEDDataUpdateCoordinator, segment: int ): - """Initialize WLED light.""" + """Initialize WLED segment light.""" self._rgbw = coordinator.data.info.leds.rgbw self._segment = segment - # Only apply the segment ID if it is not the first segment - name = coordinator.data.info.name - if segment != 0: - name += f" {segment}" + # If this is the one and only segment, use a simpler name + name = f"{coordinator.data.info.name} Segment {self._segment}" + if len(coordinator.data.state.segments) == 1: + name = coordinator.data.info.name super().__init__( entry_id=entry_id, @@ -105,6 +168,16 @@ class WLEDLight(LightEntity, WLEDDeviceEntity): """Return the unique ID for this sensor.""" return f"{self.coordinator.data.info.mac_address}_{self._segment}" + @property + def available(self) -> bool: + """Return True if entity is available.""" + try: + self.coordinator.data.state.segments[self._segment] + except IndexError: + return False + + return super().available + @property def device_state_attributes(self) -> Optional[Dict[str, Any]]: """Return the state attributes of the entity.""" @@ -140,7 +213,16 @@ class WLEDLight(LightEntity, WLEDDeviceEntity): @property def brightness(self) -> Optional[int]: """Return the brightness of this light between 1..255.""" - return self.coordinator.data.state.brightness + state = self.coordinator.data.state + + # If this is the one and only segment, calculate brightness based + # on the master and segment brightness + if len(state.segments) == 1: + return int( + (state.segments[self._segment].brightness * state.brightness) / 255 + ) + + return state.segments[self._segment].brightness @property def white_value(self) -> Optional[int]: @@ -172,18 +254,30 @@ class WLEDLight(LightEntity, WLEDDeviceEntity): @property def is_on(self) -> bool: """Return the state of the light.""" - return bool(self.coordinator.data.state.on) + state = self.coordinator.data.state + + # If there is a single segment, take master into account + if len(state.segments) == 1 and not state.on: + return False + + return bool(state.segments[self._segment].on) @wled_exception_handler async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the light.""" - data = {ATTR_ON: False, ATTR_SEGMENT_ID: self._segment} + data = {ATTR_ON: False} if ATTR_TRANSITION in kwargs: # WLED uses 100ms per unit, so 10 = 1 second. data[ATTR_TRANSITION] = round(kwargs[ATTR_TRANSITION] * 10) - await self.coordinator.wled.light(**data) + # If there is a single segment, control via the master + if len(self.coordinator.data.state.segments) == 1: + await self.coordinator.wled.master(**data) + return + + data[ATTR_SEGMENT_ID] = self._segment + await self.coordinator.wled.segment(**data) @wled_exception_handler async def async_turn_on(self, **kwargs: Any) -> None: @@ -233,7 +327,23 @@ class WLEDLight(LightEntity, WLEDDeviceEntity): else: data[ATTR_COLOR_PRIMARY] += (self.white_value,) - await self.coordinator.wled.light(**data) + # When only 1 segment is present, switch along the master, and use + # the master for power/brightness control. + if len(self.coordinator.data.state.segments) == 1: + master_data = {ATTR_ON: True} + if ATTR_BRIGHTNESS in data: + master_data[ATTR_BRIGHTNESS] = data[ATTR_BRIGHTNESS] + data[ATTR_BRIGHTNESS] = 255 + + if ATTR_TRANSITION in data: + master_data[ATTR_TRANSITION] = data[ATTR_TRANSITION] + del data[ATTR_TRANSITION] + + await self.coordinator.wled.segment(**data) + await self.coordinator.wled.master(**master_data) + return + + await self.coordinator.wled.segment(**data) @wled_exception_handler async def async_effect( @@ -258,4 +368,59 @@ class WLEDLight(LightEntity, WLEDDeviceEntity): if speed is not None: data[ATTR_SPEED] = speed - await self.coordinator.wled.light(**data) + await self.coordinator.wled.segment(**data) + + +@callback +def async_update_segments( + entry: ConfigEntry, + coordinator: WLEDDataUpdateCoordinator, + current: Dict[int, WLEDSegmentLight], + async_add_entities, +) -> None: + """Update segments.""" + segment_ids = {light.segment_id for light in coordinator.data.state.segments} + current_ids = set(current) + + # Discard master (if present) + current_ids.discard(-1) + + # Process new segments, add them to Home Assistant + new_entities = [] + for segment_id in segment_ids - current_ids: + current[segment_id] = WLEDSegmentLight(entry.entry_id, coordinator, segment_id) + new_entities.append(current[segment_id]) + + # More than 1 segment now? Add master controls + if len(current_ids) < 2 and len(segment_ids) > 1: + current[-1] = WLEDMasterLight(entry.entry_id, coordinator) + new_entities.append(current[-1]) + + if new_entities: + async_add_entities(new_entities) + + # Process deleted segments, remove them from Home Assistant + for segment_id in current_ids - segment_ids: + coordinator.hass.async_create_task( + async_remove_entity(segment_id, coordinator, current) + ) + + # Remove master if there is only 1 segment left + if len(current_ids) > 1 and len(segment_ids) < 2: + coordinator.hass.async_create_task( + async_remove_entity(-1, coordinator, current) + ) + + +async def async_remove_entity( + index: int, + coordinator: WLEDDataUpdateCoordinator, + current: Dict[int, WLEDSegmentLight], +) -> None: + """Remove WLED segment light from Home Assistant.""" + entity = current[index] + await entity.async_remove() + registry = await async_get_entity_registry(coordinator.hass) + if entity.entity_id in registry.entities: + registry.async_remove(entity.entity_id) + del current[index] diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json index 0e5bb990bae..1653ecf1365 100644 --- a/homeassistant/components/wled/manifest.json +++ b/homeassistant/components/wled/manifest.json @@ -3,7 +3,7 @@ "name": "WLED", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wled", - "requirements": ["wled==0.3.0"], + "requirements": ["wled==0.4.2"], "zeroconf": ["_wled._tcp.local."], "codeowners": ["@frenck"], "quality_scale": "platinum" diff --git a/homeassistant/components/wled/translations/af.json b/homeassistant/components/wled/translations/af.json index a9107341e37..77eafbdb9bd 100644 --- a/homeassistant/components/wled/translations/af.json +++ b/homeassistant/components/wled/translations/af.json @@ -10,8 +10,7 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, "zeroconf_confirm": { "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", diff --git a/homeassistant/components/wled/translations/ar.json b/homeassistant/components/wled/translations/ar.json index a9107341e37..77eafbdb9bd 100644 --- a/homeassistant/components/wled/translations/ar.json +++ b/homeassistant/components/wled/translations/ar.json @@ -10,8 +10,7 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, "zeroconf_confirm": { "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", diff --git a/homeassistant/components/wled/translations/bg.json b/homeassistant/components/wled/translations/bg.json index 8e091a7eace..beb1bc0d6e6 100644 --- a/homeassistant/components/wled/translations/bg.json +++ b/homeassistant/components/wled/translations/bg.json @@ -1,24 +1,10 @@ { "config": { - "abort": { - "already_configured": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "connection_error": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" - }, - "error": { - "connection_error": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" - }, - "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { "data": { "host": "\u0410\u0434\u0440\u0435\u0441" - }, - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" - }, - "zeroconf_confirm": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + } } } } diff --git a/homeassistant/components/wled/translations/bs.json b/homeassistant/components/wled/translations/bs.json index a9107341e37..77eafbdb9bd 100644 --- a/homeassistant/components/wled/translations/bs.json +++ b/homeassistant/components/wled/translations/bs.json @@ -10,8 +10,7 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, "zeroconf_confirm": { "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", diff --git a/homeassistant/components/wled/translations/ca.json b/homeassistant/components/wled/translations/ca.json index ac47147207e..72a8fc519b4 100644 --- a/homeassistant/components/wled/translations/ca.json +++ b/homeassistant/components/wled/translations/ca.json @@ -11,10 +11,9 @@ "step": { "user": { "data": { - "host": "Amfitri\u00f3 o adre\u00e7a IP" + "host": "Amfitri\u00f3" }, - "description": "Configura el teu WLED per integrar-lo amb Home Assistant.", - "title": "Enlla\u00e7 amb WLED" + "description": "Configura el teu WLED per integrar-lo amb Home Assistant." }, "zeroconf_confirm": { "description": "Vols afegir el WLED `{name}` a Home Assistant?", diff --git a/homeassistant/components/wled/translations/cs.json b/homeassistant/components/wled/translations/cs.json index a9107341e37..77eafbdb9bd 100644 --- a/homeassistant/components/wled/translations/cs.json +++ b/homeassistant/components/wled/translations/cs.json @@ -10,8 +10,7 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, "zeroconf_confirm": { "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", diff --git a/homeassistant/components/wled/translations/cy.json b/homeassistant/components/wled/translations/cy.json index a9107341e37..77eafbdb9bd 100644 --- a/homeassistant/components/wled/translations/cy.json +++ b/homeassistant/components/wled/translations/cy.json @@ -10,8 +10,7 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, "zeroconf_confirm": { "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", diff --git a/homeassistant/components/wled/translations/da.json b/homeassistant/components/wled/translations/da.json index 739a9befa82..4cf5ce622fa 100644 --- a/homeassistant/components/wled/translations/da.json +++ b/homeassistant/components/wled/translations/da.json @@ -1,24 +1,10 @@ { "config": { - "abort": { - "already_configured": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "connection_error": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" - }, - "error": { - "connection_error": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" - }, - "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { "data": { "host": "V\u00e6rt eller IP-adresse" - }, - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" - }, - "zeroconf_confirm": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + } } } } diff --git a/homeassistant/components/wled/translations/de.json b/homeassistant/components/wled/translations/de.json index 697fa1acf52..bfae4dd47da 100644 --- a/homeassistant/components/wled/translations/de.json +++ b/homeassistant/components/wled/translations/de.json @@ -13,8 +13,7 @@ "data": { "host": "Hostname oder IP-Adresse" }, - "description": "Richten Sie Ihren WLED f\u00fcr die Integration mit Home Assistant ein.", - "title": "Verkn\u00fcpfen Sie Ihre WLED" + "description": "Richten Sie Ihren WLED f\u00fcr die Integration mit Home Assistant ein." }, "zeroconf_confirm": { "description": "M\u00f6chten Sie die WLED mit dem Namen `{name}` zu Home Assistant hinzuf\u00fcgen?", diff --git a/homeassistant/components/wled/translations/el.json b/homeassistant/components/wled/translations/el.json index a9107341e37..77eafbdb9bd 100644 --- a/homeassistant/components/wled/translations/el.json +++ b/homeassistant/components/wled/translations/el.json @@ -10,8 +10,7 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, "zeroconf_confirm": { "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", diff --git a/homeassistant/components/wled/translations/en.json b/homeassistant/components/wled/translations/en.json index 362798be5bd..2576270808d 100644 --- a/homeassistant/components/wled/translations/en.json +++ b/homeassistant/components/wled/translations/en.json @@ -13,8 +13,7 @@ "data": { "host": "Host" }, - "description": "Set up your WLED to integrate with Home Assistant.", - "title": "Link your WLED" + "description": "Set up your WLED to integrate with Home Assistant." }, "zeroconf_confirm": { "description": "Do you want to add the WLED named `{name}` to Home Assistant?", diff --git a/homeassistant/components/wled/translations/eo.json b/homeassistant/components/wled/translations/eo.json index a9107341e37..77eafbdb9bd 100644 --- a/homeassistant/components/wled/translations/eo.json +++ b/homeassistant/components/wled/translations/eo.json @@ -10,8 +10,7 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, "zeroconf_confirm": { "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", diff --git a/homeassistant/components/wled/translations/es-419.json b/homeassistant/components/wled/translations/es-419.json index b5973638373..04a20091f50 100644 --- a/homeassistant/components/wled/translations/es-419.json +++ b/homeassistant/components/wled/translations/es-419.json @@ -1,24 +1,10 @@ { "config": { - "abort": { - "already_configured": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "connection_error": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" - }, - "error": { - "connection_error": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" - }, - "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { "data": { "host": "Host o direcci\u00f3n IP" - }, - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" - }, - "zeroconf_confirm": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + } } } } diff --git a/homeassistant/components/wled/translations/es.json b/homeassistant/components/wled/translations/es.json index 6b2c46f25e0..aab9752d8b6 100644 --- a/homeassistant/components/wled/translations/es.json +++ b/homeassistant/components/wled/translations/es.json @@ -13,8 +13,7 @@ "data": { "host": "Host o direcci\u00f3n IP" }, - "description": "Configura el WLED para integrarlo con Home Assistant.", - "title": "Vincula tu WLED" + "description": "Configura el WLED para integrarlo con Home Assistant." }, "zeroconf_confirm": { "description": "\u00bfQuieres a\u00f1adir el WLED `{name}` a Home Assistant?", diff --git a/homeassistant/components/wled/translations/et.json b/homeassistant/components/wled/translations/et.json index a9107341e37..77eafbdb9bd 100644 --- a/homeassistant/components/wled/translations/et.json +++ b/homeassistant/components/wled/translations/et.json @@ -10,8 +10,7 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, "zeroconf_confirm": { "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", diff --git a/homeassistant/components/wled/translations/eu.json b/homeassistant/components/wled/translations/eu.json index a9107341e37..77eafbdb9bd 100644 --- a/homeassistant/components/wled/translations/eu.json +++ b/homeassistant/components/wled/translations/eu.json @@ -10,8 +10,7 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, "zeroconf_confirm": { "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", diff --git a/homeassistant/components/wled/translations/fa.json b/homeassistant/components/wled/translations/fa.json index a9107341e37..77eafbdb9bd 100644 --- a/homeassistant/components/wled/translations/fa.json +++ b/homeassistant/components/wled/translations/fa.json @@ -10,8 +10,7 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, "zeroconf_confirm": { "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", diff --git a/homeassistant/components/wled/translations/fi.json b/homeassistant/components/wled/translations/fi.json index a9107341e37..77eafbdb9bd 100644 --- a/homeassistant/components/wled/translations/fi.json +++ b/homeassistant/components/wled/translations/fi.json @@ -10,8 +10,7 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, "zeroconf_confirm": { "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", diff --git a/homeassistant/components/wled/translations/fr.json b/homeassistant/components/wled/translations/fr.json index ded950bdf69..12e539cf755 100644 --- a/homeassistant/components/wled/translations/fr.json +++ b/homeassistant/components/wled/translations/fr.json @@ -1,24 +1,10 @@ { "config": { - "abort": { - "already_configured": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "connection_error": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" - }, - "error": { - "connection_error": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" - }, - "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { "data": { "host": "H\u00f4te ou adresse IP" - }, - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" - }, - "zeroconf_confirm": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + } } } } diff --git a/homeassistant/components/wled/translations/gsw.json b/homeassistant/components/wled/translations/gsw.json index a9107341e37..77eafbdb9bd 100644 --- a/homeassistant/components/wled/translations/gsw.json +++ b/homeassistant/components/wled/translations/gsw.json @@ -10,8 +10,7 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, "zeroconf_confirm": { "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", diff --git a/homeassistant/components/wled/translations/he.json b/homeassistant/components/wled/translations/he.json index a9107341e37..77eafbdb9bd 100644 --- a/homeassistant/components/wled/translations/he.json +++ b/homeassistant/components/wled/translations/he.json @@ -10,8 +10,7 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, "zeroconf_confirm": { "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", diff --git a/homeassistant/components/wled/translations/hi.json b/homeassistant/components/wled/translations/hi.json index a9107341e37..77eafbdb9bd 100644 --- a/homeassistant/components/wled/translations/hi.json +++ b/homeassistant/components/wled/translations/hi.json @@ -10,8 +10,7 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, "zeroconf_confirm": { "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", diff --git a/homeassistant/components/wled/translations/hr.json b/homeassistant/components/wled/translations/hr.json index a9107341e37..77eafbdb9bd 100644 --- a/homeassistant/components/wled/translations/hr.json +++ b/homeassistant/components/wled/translations/hr.json @@ -10,8 +10,7 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, "zeroconf_confirm": { "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", diff --git a/homeassistant/components/wled/translations/hu.json b/homeassistant/components/wled/translations/hu.json index 5ffd902214e..f86a02da7c9 100644 --- a/homeassistant/components/wled/translations/hu.json +++ b/homeassistant/components/wled/translations/hu.json @@ -11,10 +11,9 @@ "step": { "user": { "data": { - "host": "Hosztn\u00e9v vagy IP c\u00edm" + "host": "Hoszt" }, - "description": "\u00c1ll\u00edtsd be a WLED-et a Home Assistant-ba val\u00f3 integr\u00e1l\u00e1shoz.", - "title": "Csatlakoztasd a WLED-et" + "description": "\u00c1ll\u00edtsd be a WLED-et a Home Assistant-ba val\u00f3 integr\u00e1l\u00e1shoz." }, "zeroconf_confirm": { "description": "Szeretn\u00e9d hozz\u00e1adni a(z) `{name}` WLED-et a Home Assistant-hoz?", diff --git a/homeassistant/components/wled/translations/id.json b/homeassistant/components/wled/translations/id.json index a9107341e37..77eafbdb9bd 100644 --- a/homeassistant/components/wled/translations/id.json +++ b/homeassistant/components/wled/translations/id.json @@ -10,8 +10,7 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, "zeroconf_confirm": { "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", diff --git a/homeassistant/components/wled/translations/is.json b/homeassistant/components/wled/translations/is.json index a9107341e37..77eafbdb9bd 100644 --- a/homeassistant/components/wled/translations/is.json +++ b/homeassistant/components/wled/translations/is.json @@ -10,8 +10,7 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, "zeroconf_confirm": { "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", diff --git a/homeassistant/components/wled/translations/it.json b/homeassistant/components/wled/translations/it.json index 60a896c34d1..897c04539f0 100644 --- a/homeassistant/components/wled/translations/it.json +++ b/homeassistant/components/wled/translations/it.json @@ -11,10 +11,9 @@ "step": { "user": { "data": { - "host": "Host o indirizzo IP" + "host": "Host" }, - "description": "Configura WLED per l'integrazione con Home Assistant.", - "title": "Collega il tuo WLED" + "description": "Configura WLED per l'integrazione con Home Assistant." }, "zeroconf_confirm": { "description": "Vuoi aggiungere il WLED chiamato `{name}` a Home Assistant?", diff --git a/homeassistant/components/wled/translations/ja.json b/homeassistant/components/wled/translations/ja.json index a9107341e37..77eafbdb9bd 100644 --- a/homeassistant/components/wled/translations/ja.json +++ b/homeassistant/components/wled/translations/ja.json @@ -10,8 +10,7 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, "zeroconf_confirm": { "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", diff --git a/homeassistant/components/wled/translations/ko.json b/homeassistant/components/wled/translations/ko.json index 12d964f25c6..684f0879b55 100644 --- a/homeassistant/components/wled/translations/ko.json +++ b/homeassistant/components/wled/translations/ko.json @@ -13,8 +13,7 @@ "data": { "host": "\ud638\uc2a4\ud2b8" }, - "description": "Home Assistant \uc5d0 WLED \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4.", - "title": "WLED \uc5f0\uacb0\ud558\uae30" + "description": "Home Assistant \uc5d0 WLED \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4." }, "zeroconf_confirm": { "description": "Home Assistant \uc5d0 WLED `{name}` \uc744(\ub97c) \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", diff --git a/homeassistant/components/wled/translations/lb.json b/homeassistant/components/wled/translations/lb.json index 7657e1a12cc..8a5069a0e1b 100644 --- a/homeassistant/components/wled/translations/lb.json +++ b/homeassistant/components/wled/translations/lb.json @@ -13,8 +13,7 @@ "data": { "host": "Numm oder IP Adresse" }, - "description": "D\u00e4in WLED als Integratioun mam Home Assistant ariichten.", - "title": "D\u00e4in WLED verbannen" + "description": "D\u00e4in WLED als Integratioun mam Home Assistant ariichten." }, "zeroconf_confirm": { "description": "Soll de WLED mam Numm `{name}` am Home Assistant dob\u00e4i gesaat ginn?", diff --git a/homeassistant/components/wled/translations/lt.json b/homeassistant/components/wled/translations/lt.json index a9107341e37..77eafbdb9bd 100644 --- a/homeassistant/components/wled/translations/lt.json +++ b/homeassistant/components/wled/translations/lt.json @@ -10,8 +10,7 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, "zeroconf_confirm": { "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", diff --git a/homeassistant/components/wled/translations/lv.json b/homeassistant/components/wled/translations/lv.json index a9107341e37..77eafbdb9bd 100644 --- a/homeassistant/components/wled/translations/lv.json +++ b/homeassistant/components/wled/translations/lv.json @@ -10,8 +10,7 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, "zeroconf_confirm": { "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", diff --git a/homeassistant/components/wled/translations/nl.json b/homeassistant/components/wled/translations/nl.json index 5b78cbd791b..794f68256a7 100644 --- a/homeassistant/components/wled/translations/nl.json +++ b/homeassistant/components/wled/translations/nl.json @@ -1,24 +1,10 @@ { "config": { - "abort": { - "already_configured": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "connection_error": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" - }, - "error": { - "connection_error": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" - }, - "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { "data": { "host": "Hostnaam of IP-adres" - }, - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" - }, - "zeroconf_confirm": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + } } } } diff --git a/homeassistant/components/wled/translations/nn.json b/homeassistant/components/wled/translations/nn.json index a9107341e37..77eafbdb9bd 100644 --- a/homeassistant/components/wled/translations/nn.json +++ b/homeassistant/components/wled/translations/nn.json @@ -10,8 +10,7 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, "zeroconf_confirm": { "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", diff --git a/homeassistant/components/wled/translations/no.json b/homeassistant/components/wled/translations/no.json index fd3b5f94cbe..9f9c2b40e6e 100644 --- a/homeassistant/components/wled/translations/no.json +++ b/homeassistant/components/wled/translations/no.json @@ -13,8 +13,7 @@ "data": { "host": "Vert eller IP-adresse" }, - "description": "Sett opp WLED til \u00e5 integreres med Home Assistant.", - "title": "Linken din WLED" + "description": "Sett opp WLED til \u00e5 integreres med Home Assistant." }, "zeroconf_confirm": { "description": "Vil du legge til WLED med navnet ' {name} ' i Home Assistant?", diff --git a/homeassistant/components/wled/translations/pl.json b/homeassistant/components/wled/translations/pl.json index 6db76e82a0a..6f68055d385 100644 --- a/homeassistant/components/wled/translations/pl.json +++ b/homeassistant/components/wled/translations/pl.json @@ -1,23 +1,22 @@ { "config": { "abort": { - "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]", - "connection_error": "[%key_id:common::config_flow::error::cannot_connect%]" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." }, "error": { - "connection_error": "[%key_id:common::config_flow::error::cannot_connect%]" + "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." }, "flow_title": "WLED: {name}", "step": { "user": { "data": { - "host": "[%key_id:common::config_flow::data::host%]" + "host": "Nazwa hosta lub adres IP" }, - "description": "Konfiguracja WLED w celu integracji z Home Assistant'em.", - "title": "Po\u0142\u0105cz z WLED" + "description": "Konfiguracja WLED w celu integracji z Home Assistantem." }, "zeroconf_confirm": { - "description": "Czy chcesz doda\u0107 WLED o nazwie `{name}` do Home Assistant'a?", + "description": "Czy chcesz doda\u0107 WLED o nazwie `{name}` do Home Assistanta?", "title": "Wykryto urz\u0105dzenie WLED" } } diff --git a/homeassistant/components/wled/translations/pt-BR.json b/homeassistant/components/wled/translations/pt-BR.json index a9107341e37..77eafbdb9bd 100644 --- a/homeassistant/components/wled/translations/pt-BR.json +++ b/homeassistant/components/wled/translations/pt-BR.json @@ -10,8 +10,7 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, "zeroconf_confirm": { "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", diff --git a/homeassistant/components/wled/translations/pt.json b/homeassistant/components/wled/translations/pt.json index 356c842839e..a6e5cd46cbb 100644 --- a/homeassistant/components/wled/translations/pt.json +++ b/homeassistant/components/wled/translations/pt.json @@ -1,24 +1,10 @@ { "config": { - "abort": { - "already_configured": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "connection_error": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" - }, - "error": { - "connection_error": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" - }, - "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { "data": { "host": "Nome servidor ou endere\u00e7o IP" - }, - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" - }, - "zeroconf_confirm": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + } } } } diff --git a/homeassistant/components/wled/translations/ro.json b/homeassistant/components/wled/translations/ro.json index a9107341e37..77eafbdb9bd 100644 --- a/homeassistant/components/wled/translations/ro.json +++ b/homeassistant/components/wled/translations/ro.json @@ -10,8 +10,7 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, "zeroconf_confirm": { "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", diff --git a/homeassistant/components/wled/translations/ru.json b/homeassistant/components/wled/translations/ru.json index 867b2ca2ab6..5d50b75b94f 100644 --- a/homeassistant/components/wled/translations/ru.json +++ b/homeassistant/components/wled/translations/ru.json @@ -13,8 +13,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 WLED.", - "title": "WLED" + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 WLED." }, "zeroconf_confirm": { "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c WLED `{name}`?", diff --git a/homeassistant/components/wled/translations/sk.json b/homeassistant/components/wled/translations/sk.json index a9107341e37..77eafbdb9bd 100644 --- a/homeassistant/components/wled/translations/sk.json +++ b/homeassistant/components/wled/translations/sk.json @@ -10,8 +10,7 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, "zeroconf_confirm": { "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", diff --git a/homeassistant/components/wled/translations/sl.json b/homeassistant/components/wled/translations/sl.json index e0f4b187e54..086e8d111ab 100644 --- a/homeassistant/components/wled/translations/sl.json +++ b/homeassistant/components/wled/translations/sl.json @@ -13,8 +13,7 @@ "data": { "host": "Gostitelj ali IP naslov" }, - "description": "Nastavite svoj WLED za integracijo s Home Assistant.", - "title": "Pove\u017eite svoj WLED" + "description": "Nastavite svoj WLED za integracijo s Home Assistant." }, "zeroconf_confirm": { "description": "Ali \u017eelite dodati WLED z imenom `{name}` v Home Assistant?", diff --git a/homeassistant/components/wled/translations/sr-Latn.json b/homeassistant/components/wled/translations/sr-Latn.json index a9107341e37..77eafbdb9bd 100644 --- a/homeassistant/components/wled/translations/sr-Latn.json +++ b/homeassistant/components/wled/translations/sr-Latn.json @@ -10,8 +10,7 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, "zeroconf_confirm": { "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", diff --git a/homeassistant/components/wled/translations/sr.json b/homeassistant/components/wled/translations/sr.json index a9107341e37..77eafbdb9bd 100644 --- a/homeassistant/components/wled/translations/sr.json +++ b/homeassistant/components/wled/translations/sr.json @@ -10,8 +10,7 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, "zeroconf_confirm": { "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", diff --git a/homeassistant/components/wled/translations/sv.json b/homeassistant/components/wled/translations/sv.json index a348c0d5572..3c802a87007 100644 --- a/homeassistant/components/wled/translations/sv.json +++ b/homeassistant/components/wled/translations/sv.json @@ -1,24 +1,10 @@ { "config": { - "abort": { - "already_configured": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "connection_error": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" - }, - "error": { - "connection_error": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" - }, - "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { "data": { "host": "V\u00e4rd eller IP-adress" - }, - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" - }, - "zeroconf_confirm": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + } } } } diff --git a/homeassistant/components/wled/translations/ta.json b/homeassistant/components/wled/translations/ta.json index a9107341e37..77eafbdb9bd 100644 --- a/homeassistant/components/wled/translations/ta.json +++ b/homeassistant/components/wled/translations/ta.json @@ -10,8 +10,7 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, "zeroconf_confirm": { "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", diff --git a/homeassistant/components/wled/translations/te.json b/homeassistant/components/wled/translations/te.json index a9107341e37..77eafbdb9bd 100644 --- a/homeassistant/components/wled/translations/te.json +++ b/homeassistant/components/wled/translations/te.json @@ -10,8 +10,7 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, "zeroconf_confirm": { "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", diff --git a/homeassistant/components/wled/translations/th.json b/homeassistant/components/wled/translations/th.json index a9107341e37..77eafbdb9bd 100644 --- a/homeassistant/components/wled/translations/th.json +++ b/homeassistant/components/wled/translations/th.json @@ -10,8 +10,7 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, "zeroconf_confirm": { "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", diff --git a/homeassistant/components/wled/translations/tr.json b/homeassistant/components/wled/translations/tr.json index a9107341e37..77eafbdb9bd 100644 --- a/homeassistant/components/wled/translations/tr.json +++ b/homeassistant/components/wled/translations/tr.json @@ -10,8 +10,7 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, "zeroconf_confirm": { "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", diff --git a/homeassistant/components/wled/translations/uk.json b/homeassistant/components/wled/translations/uk.json index a9107341e37..77eafbdb9bd 100644 --- a/homeassistant/components/wled/translations/uk.json +++ b/homeassistant/components/wled/translations/uk.json @@ -10,8 +10,7 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, "zeroconf_confirm": { "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", diff --git a/homeassistant/components/wled/translations/ur.json b/homeassistant/components/wled/translations/ur.json index a9107341e37..77eafbdb9bd 100644 --- a/homeassistant/components/wled/translations/ur.json +++ b/homeassistant/components/wled/translations/ur.json @@ -10,8 +10,7 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, "zeroconf_confirm": { "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", diff --git a/homeassistant/components/wled/translations/vi.json b/homeassistant/components/wled/translations/vi.json index a9107341e37..77eafbdb9bd 100644 --- a/homeassistant/components/wled/translations/vi.json +++ b/homeassistant/components/wled/translations/vi.json @@ -10,8 +10,7 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, "zeroconf_confirm": { "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", diff --git a/homeassistant/components/wled/translations/zh-Hans.json b/homeassistant/components/wled/translations/zh-Hans.json index a9107341e37..77eafbdb9bd 100644 --- a/homeassistant/components/wled/translations/zh-Hans.json +++ b/homeassistant/components/wled/translations/zh-Hans.json @@ -10,8 +10,7 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { - "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", - "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" + "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, "zeroconf_confirm": { "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", diff --git a/homeassistant/components/wled/translations/zh-Hant.json b/homeassistant/components/wled/translations/zh-Hant.json index c712a3682ad..87490dc4595 100644 --- a/homeassistant/components/wled/translations/zh-Hant.json +++ b/homeassistant/components/wled/translations/zh-Hant.json @@ -13,8 +13,7 @@ "data": { "host": "\u4e3b\u6a5f\u7aef" }, - "description": "\u8a2d\u5b9a WLED \u4ee5\u6574\u5408\u81f3 Home Assistant\u3002", - "title": "\u9023\u7d50 WLED" + "description": "\u8a2d\u5b9a WLED \u4ee5\u6574\u5408\u81f3 Home Assistant\u3002" }, "zeroconf_confirm": { "description": "\u662f\u5426\u8981\u65b0\u589e WLED \u540d\u7a31\u300c{name}\u300d\u8a2d\u5099\u81f3 Home Assistant\uff1f", diff --git a/homeassistant/components/wunderlist/__init__.py b/homeassistant/components/wunderlist/__init__.py deleted file mode 100644 index 954088e4b21..00000000000 --- a/homeassistant/components/wunderlist/__init__.py +++ /dev/null @@ -1,91 +0,0 @@ -"""Support to interact with Wunderlist.""" -import logging - -import voluptuous as vol -from wunderpy2 import WunderApi - -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "wunderlist" -CONF_CLIENT_ID = "client_id" -CONF_LIST_NAME = "list_name" -CONF_STARRED = "starred" - - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_ACCESS_TOKEN): cv.string, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -SERVICE_CREATE_TASK = "create_task" - -SERVICE_SCHEMA_CREATE_TASK = vol.Schema( - { - vol.Required(CONF_LIST_NAME): cv.string, - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_STARRED, default=False): cv.boolean, - } -) - - -def setup(hass, config): - """Set up the Wunderlist component.""" - conf = config[DOMAIN] - client_id = conf.get(CONF_CLIENT_ID) - access_token = conf.get(CONF_ACCESS_TOKEN) - data = Wunderlist(access_token, client_id) - if not data.check_credentials(): - _LOGGER.error("Invalid credentials") - return False - - hass.services.register( - DOMAIN, "create_task", data.create_task, schema=SERVICE_SCHEMA_CREATE_TASK - ) - return True - - -class Wunderlist: - """Representation of an interface to Wunderlist.""" - - def __init__(self, access_token, client_id): - """Create new instance of Wunderlist component.""" - api = WunderApi() - self._client = api.get_client(access_token, client_id) - - _LOGGER.debug("Instance created") - - def check_credentials(self): - """Check if the provided credentials are valid.""" - try: - self._client.get_lists() - return True - except ValueError: - return False - - def create_task(self, call): - """Create a new task on a list of Wunderlist.""" - list_name = call.data[CONF_LIST_NAME] - task_title = call.data[CONF_NAME] - starred = call.data[CONF_STARRED] - list_id = self._list_by_name(list_name) - self._client.create_task(list_id, task_title, starred=starred) - return True - - def _list_by_name(self, name): - """Return a list ID by name.""" - lists = self._client.get_lists() - tmp = [lst for lst in lists if lst["title"] == name] - if tmp: - return tmp[0]["id"] - return None diff --git a/homeassistant/components/wunderlist/manifest.json b/homeassistant/components/wunderlist/manifest.json deleted file mode 100644 index 414a5eb7d33..00000000000 --- a/homeassistant/components/wunderlist/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "domain": "wunderlist", - "name": "Wunderlist", - "documentation": "https://www.home-assistant.io/integrations/wunderlist", - "requirements": ["wunderpy2==0.1.6"], - "codeowners": [] -} diff --git a/homeassistant/components/wunderlist/services.yaml b/homeassistant/components/wunderlist/services.yaml deleted file mode 100644 index 1b824e43843..00000000000 --- a/homeassistant/components/wunderlist/services.yaml +++ /dev/null @@ -1,15 +0,0 @@ -# Describes the format for available Wunderlist - -create_task: - description: > - Create a new task in Wunderlist. - fields: - list_name: - description: name of the new list where the task will be created - example: "Shopping list" - name: - description: name of the new task - example: "Buy 5 bottles of beer" - starred: - description: Create the task as starred [Optional] - example: true diff --git a/homeassistant/components/wwlln/__init__.py b/homeassistant/components/wwlln/__init__.py deleted file mode 100644 index d83e19bd391..00000000000 --- a/homeassistant/components/wwlln/__init__.py +++ /dev/null @@ -1,102 +0,0 @@ -"""Support for World Wide Lightning Location Network.""" -import logging - -from aiowwlln import Client -import voluptuous as vol - -from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS -from homeassistant.helpers import aiohttp_client, config_validation as cv - -from .const import CONF_WINDOW, DATA_CLIENT, DEFAULT_RADIUS, DEFAULT_WINDOW, DOMAIN - -_LOGGER = logging.getLogger(__name__) - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_LATITUDE): cv.latitude, - vol.Optional(CONF_LONGITUDE): cv.longitude, - vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): cv.positive_int, - vol.Optional(CONF_WINDOW, default=DEFAULT_WINDOW): vol.All( - cv.time_period, - cv.positive_timedelta, - lambda value: value.total_seconds(), - vol.Range(min=DEFAULT_WINDOW.total_seconds()), - ), - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass, config): - """Set up the WWLLN component.""" - if DOMAIN not in config: - return True - - conf = config[DOMAIN] - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=conf - ) - ) - - return True - - -async def async_setup_entry(hass, config_entry): - """Set up the WWLLN as config entry.""" - if not config_entry.unique_id: - hass.config_entries.async_update_entry( - config_entry, - unique_id=( - f"{config_entry.data[CONF_LATITUDE]}, " - f"{config_entry.data[CONF_LONGITUDE]}" - ), - ) - - hass.data[DOMAIN] = {} - hass.data[DOMAIN][DATA_CLIENT] = {} - - websession = aiohttp_client.async_get_clientsession(hass) - - hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = Client(websession) - - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, "geo_location") - ) - - return True - - -async def async_unload_entry(hass, config_entry): - """Unload an WWLLN config entry.""" - hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id) - - await hass.config_entries.async_forward_entry_unload(config_entry, "geo_location") - - return True - - -async def async_migrate_entry(hass, config_entry): - """Migrate the config entry upon new versions.""" - version = config_entry.version - data = config_entry.data - - default_total_seconds = DEFAULT_WINDOW.total_seconds() - - _LOGGER.debug("Migrating from version %s", version) - - # 1 -> 2: Expanding the default window to 1 hour (if needed): - if version == 1: - if data[CONF_WINDOW] < default_total_seconds: - data[CONF_WINDOW] = default_total_seconds - version = config_entry.version = 2 - hass.config_entries.async_update_entry(config_entry, data=data) - _LOGGER.info("Migration to version %s successful", version) - - return True diff --git a/homeassistant/components/wwlln/config_flow.py b/homeassistant/components/wwlln/config_flow.py deleted file mode 100644 index 4ec7c2a9a0c..00000000000 --- a/homeassistant/components/wwlln/config_flow.py +++ /dev/null @@ -1,70 +0,0 @@ -"""Config flow to configure the WWLLN integration.""" -import voluptuous as vol - -from homeassistant import config_entries -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS -from homeassistant.helpers import config_validation as cv - -from .const import ( # pylint: disable=unused-import - CONF_WINDOW, - DEFAULT_RADIUS, - DEFAULT_WINDOW, - DOMAIN, -) - - -class WWLLNFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a WWLLN config flow.""" - - VERSION = 2 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL - - @property - def data_schema(self): - """Return the data schema for the user form.""" - return vol.Schema( - { - vol.Optional( - CONF_LATITUDE, default=self.hass.config.latitude - ): cv.latitude, - vol.Optional( - CONF_LONGITUDE, default=self.hass.config.longitude - ): cv.longitude, - vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): cv.positive_int, - } - ) - - async def _show_form(self, errors=None): - """Show the form to the user.""" - return self.async_show_form( - step_id="user", data_schema=self.data_schema, errors=errors or {} - ) - - async def async_step_import(self, import_config): - """Import a config entry from configuration.yaml.""" - return await self.async_step_user(import_config) - - async def async_step_user(self, user_input=None): - """Handle the start of the config flow.""" - if not user_input: - return await self._show_form() - - latitude = user_input.get(CONF_LATITUDE, self.hass.config.latitude) - longitude = user_input.get(CONF_LONGITUDE, self.hass.config.longitude) - - identifier = f"{latitude}, {longitude}" - - await self.async_set_unique_id(identifier) - self._abort_if_unique_id_configured() - - return self.async_create_entry( - title=identifier, - data={ - CONF_LATITUDE: latitude, - CONF_LONGITUDE: longitude, - CONF_RADIUS: user_input.get(CONF_RADIUS, DEFAULT_RADIUS), - CONF_WINDOW: user_input.get( - CONF_WINDOW, DEFAULT_WINDOW.total_seconds() - ), - }, - ) diff --git a/homeassistant/components/wwlln/const.py b/homeassistant/components/wwlln/const.py deleted file mode 100644 index 141baf38cda..00000000000 --- a/homeassistant/components/wwlln/const.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Define constants for the WWLLN integration.""" -from datetime import timedelta - -DOMAIN = "wwlln" - -CONF_WINDOW = "window" - -DATA_CLIENT = "client" - -DEFAULT_RADIUS = 25 -DEFAULT_WINDOW = timedelta(hours=1) diff --git a/homeassistant/components/wwlln/geo_location.py b/homeassistant/components/wwlln/geo_location.py deleted file mode 100644 index ed4d4fcd6b8..00000000000 --- a/homeassistant/components/wwlln/geo_location.py +++ /dev/null @@ -1,228 +0,0 @@ -"""Support for WWLLN geo location events.""" -from datetime import timedelta -import logging - -from aiowwlln.errors import WWLLNError - -from homeassistant.components.geo_location import GeolocationEvent -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_RADIUS, - CONF_UNIT_SYSTEM_IMPERIAL, - LENGTH_KILOMETERS, - LENGTH_MILES, -) -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) -from homeassistant.helpers.event import async_track_time_interval -from homeassistant.util.dt import utc_from_timestamp - -from .const import CONF_WINDOW, DATA_CLIENT, DOMAIN - -_LOGGER = logging.getLogger(__name__) - -ATTR_EXTERNAL_ID = "external_id" -ATTR_PUBLICATION_DATE = "publication_date" - -DEFAULT_ATTRIBUTION = "Data provided by the WWLLN" -DEFAULT_EVENT_NAME = "Lightning Strike: {0}" -DEFAULT_ICON = "mdi:flash" -DEFAULT_UPDATE_INTERVAL = timedelta(minutes=10) - -SIGNAL_DELETE_ENTITY = "wwlln_delete_entity_{0}" - - -async def async_setup_entry(hass, entry, async_add_entities): - """Set up WWLLN based on a config entry.""" - client = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] - manager = WWLLNEventManager( - hass, - async_add_entities, - client, - entry.data[CONF_LATITUDE], - entry.data[CONF_LONGITUDE], - entry.data[CONF_RADIUS], - entry.data[CONF_WINDOW], - ) - await manager.async_init() - - -class WWLLNEventManager: - """Define a class to handle WWLLN events.""" - - def __init__( - self, - hass, - async_add_entities, - client, - latitude, - longitude, - radius, - window_seconds, - ): - """Initialize.""" - self._async_add_entities = async_add_entities - self._client = client - self._hass = hass - self._latitude = latitude - self._longitude = longitude - self._managed_strike_ids = set() - self._radius = radius - self._strikes = {} - self._window = timedelta(seconds=window_seconds) - - if hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: - self._unit = LENGTH_MILES - else: - self._unit = LENGTH_KILOMETERS - - @callback - def _create_events(self, ids_to_create): - """Create new geo location events.""" - _LOGGER.debug("Going to create %s", ids_to_create) - events = [] - for strike_id in ids_to_create: - strike = self._strikes[strike_id] - event = WWLLNEvent( - strike["distance"], - strike["lat"], - strike["long"], - self._unit, - strike_id, - strike["unixTime"], - ) - events.append(event) - - self._async_add_entities(events) - - @callback - def _remove_events(self, ids_to_remove): - """Remove old geo location events.""" - _LOGGER.debug("Going to remove %s", ids_to_remove) - for strike_id in ids_to_remove: - async_dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(strike_id)) - - async def async_init(self): - """Schedule regular updates based on configured time interval.""" - - async def update(event_time): - """Update.""" - await self.async_update() - - await self.async_update() - async_track_time_interval(self._hass, update, DEFAULT_UPDATE_INTERVAL) - - async def async_update(self): - """Refresh data.""" - _LOGGER.debug("Refreshing WWLLN data") - - try: - self._strikes = await self._client.within_radius( - self._latitude, - self._longitude, - self._radius, - unit=self._hass.config.units.name, - window=self._window, - ) - except WWLLNError as err: - _LOGGER.error("Error while updating WWLLN data: %s", err) - return - - new_strike_ids = set(self._strikes) - # Remove all managed entities that are not in the latest update anymore. - ids_to_remove = self._managed_strike_ids.difference(new_strike_ids) - self._remove_events(ids_to_remove) - - # Create new entities for all strikes that are not managed entities yet. - ids_to_create = new_strike_ids.difference(self._managed_strike_ids) - self._create_events(ids_to_create) - - # Store all external IDs of all managed strikes. - self._managed_strike_ids = new_strike_ids - - -class WWLLNEvent(GeolocationEvent): - """Define a lightning strike event.""" - - def __init__( - self, distance, latitude, longitude, unit, strike_id, publication_date - ): - """Initialize entity with data provided.""" - self._distance = distance - self._latitude = latitude - self._longitude = longitude - self._publication_date = publication_date - self._remove_signal_delete = None - self._strike_id = strike_id - self._unit_of_measurement = unit - - @property - def device_state_attributes(self): - """Return the device state attributes.""" - attributes = {} - for key, value in ( - (ATTR_EXTERNAL_ID, self._strike_id), - (ATTR_ATTRIBUTION, DEFAULT_ATTRIBUTION), - (ATTR_PUBLICATION_DATE, utc_from_timestamp(self._publication_date)), - ): - attributes[key] = value - return attributes - - @property - def distance(self): - """Return distance value of this external event.""" - return self._distance - - @property - def icon(self): - """Return the icon to use in the front-end.""" - return DEFAULT_ICON - - @property - def latitude(self): - """Return latitude value of this external event.""" - return self._latitude - - @property - def longitude(self): - """Return longitude value of this external event.""" - return self._longitude - - @property - def name(self): - """Return the name of the event.""" - return DEFAULT_EVENT_NAME.format(self._strike_id) - - @property - def source(self) -> str: - """Return source value of this external event.""" - return DOMAIN - - @property - def should_poll(self): - """Disable polling.""" - return False - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self._unit_of_measurement - - @callback - def _delete_callback(self): - """Remove this entity.""" - self._remove_signal_delete() - self.hass.async_create_task(self.async_remove()) - - 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._strike_id), - self._delete_callback, - ) diff --git a/homeassistant/components/wwlln/manifest.json b/homeassistant/components/wwlln/manifest.json deleted file mode 100644 index 19406ac4b7a..00000000000 --- a/homeassistant/components/wwlln/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "wwlln", - "name": "World Wide Lightning Location Network (WWLLN)", - "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/wwlln", - "requirements": ["aiowwlln==2.0.2"], - "codeowners": ["@bachya"] -} diff --git a/homeassistant/components/wwlln/strings.json b/homeassistant/components/wwlln/strings.json deleted file mode 100644 index c3c9193df33..00000000000 --- a/homeassistant/components/wwlln/strings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "config": { - "step": { - "user": { - "title": "Fill in your location information.", - "data": { - "latitude": "Latitude", - "longitude": "Longitude", - "radius": "Radius (using your base unit system)" - } - } - }, - "abort": { "already_configured": "This location is already registered." } - } -} diff --git a/homeassistant/components/wwlln/translations/bg.json b/homeassistant/components/wwlln/translations/bg.json deleted file mode 100644 index cd39935f171..00000000000 --- a/homeassistant/components/wwlln/translations/bg.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "latitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0448\u0438\u0440\u0438\u043d\u0430", - "longitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0434\u044a\u043b\u0436\u0438\u043d\u0430", - "radius": "\u0420\u0430\u0434\u0438\u0443\u0441 (\u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0439\u043a\u0438 \u0438\u0437\u0431\u0440\u0430\u043d\u0430\u0442\u0430 \u043e\u0442 \u0412\u0430\u0441 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u043e\u0442 \u043c\u0435\u0440\u043d\u0438 \u0435\u0434\u0438\u043d\u0438\u0446\u0438)" - }, - "title": "\u041f\u043e\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f\u0442\u0430 \u0437\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0441\u0438." - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/wwlln/translations/ca.json b/homeassistant/components/wwlln/translations/ca.json deleted file mode 100644 index b6e915aa35e..00000000000 --- a/homeassistant/components/wwlln/translations/ca.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Aquesta ubicaci\u00f3 ja est\u00e0 registrada." - }, - "step": { - "user": { - "data": { - "latitude": "Latitud", - "longitude": "Longitud", - "radius": "Radi (utilitzant el sistema d'unitats establert)" - }, - "title": "Introdueix la teva informaci\u00f3 d'ubicaci\u00f3." - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/wwlln/translations/cy.json b/homeassistant/components/wwlln/translations/cy.json deleted file mode 100644 index f9c36f4e72a..00000000000 --- a/homeassistant/components/wwlln/translations/cy.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "latitude": "Lledred", - "longitude": "Hydred", - "radius": "Radiws (gan ddefnyddio'ch system uned sylfaenol)" - }, - "title": "Cwblhewch gwybodaeth eich lleoliad" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/wwlln/translations/da.json b/homeassistant/components/wwlln/translations/da.json deleted file mode 100644 index b87d9af14f3..00000000000 --- a/homeassistant/components/wwlln/translations/da.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "latitude": "Breddegrad", - "longitude": "L\u00e6ngdegrad", - "radius": "Radius (ved hj\u00e6lp af dit basisenhedssystem)" - }, - "title": "Udfyld dine lokalitetsoplysninger." - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/wwlln/translations/de.json b/homeassistant/components/wwlln/translations/de.json deleted file mode 100644 index 2f59ea2d38c..00000000000 --- a/homeassistant/components/wwlln/translations/de.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Dieser Standort ist bereits registriert." - }, - "step": { - "user": { - "data": { - "latitude": "Breitengrad", - "longitude": "L\u00e4ngengrad", - "radius": "Radius (mit Ma\u00dfeinheit)" - }, - "title": "Gib deine Standortinformationen ein." - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/wwlln/translations/en.json b/homeassistant/components/wwlln/translations/en.json deleted file mode 100644 index 936c64c2a77..00000000000 --- a/homeassistant/components/wwlln/translations/en.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "This location is already registered." - }, - "step": { - "user": { - "data": { - "latitude": "Latitude", - "longitude": "Longitude", - "radius": "Radius (using your base unit system)" - }, - "title": "Fill in your location information." - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/wwlln/translations/es-419.json b/homeassistant/components/wwlln/translations/es-419.json deleted file mode 100644 index 11ae8c64359..00000000000 --- a/homeassistant/components/wwlln/translations/es-419.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Esta ubicaci\u00f3n ya est\u00e1 registrada." - }, - "step": { - "user": { - "data": { - "latitude": "Latitud", - "longitude": "Longitud", - "radius": "Radio (usando su sistema de unidad base)" - }, - "title": "Complete su informaci\u00f3n de ubicaci\u00f3n." - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/wwlln/translations/es.json b/homeassistant/components/wwlln/translations/es.json deleted file mode 100644 index 16b3b461ad1..00000000000 --- a/homeassistant/components/wwlln/translations/es.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Esta ubicaci\u00f3n ya est\u00e1 registrada." - }, - "step": { - "user": { - "data": { - "latitude": "Latitud", - "longitude": "Longitud", - "radius": "Radio (usando la unidad base del sistema)" - }, - "title": "Completa la informaci\u00f3n de tu ubicaci\u00f3n." - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/wwlln/translations/fr.json b/homeassistant/components/wwlln/translations/fr.json deleted file mode 100644 index d4fad7f0160..00000000000 --- a/homeassistant/components/wwlln/translations/fr.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Cet emplacement est d\u00e9j\u00e0 enregistr\u00e9." - }, - "step": { - "user": { - "data": { - "latitude": "Latitude", - "longitude": "Longitude", - "radius": "Rayon (en utilisant votre syst\u00e8me d'unit\u00e9 de base)" - }, - "title": "Veuillez saisir vos informations d'emplacement." - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/wwlln/translations/hr.json b/homeassistant/components/wwlln/translations/hr.json deleted file mode 100644 index 43af25b9047..00000000000 --- a/homeassistant/components/wwlln/translations/hr.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "latitude": "Zemljopisna \u0161irina", - "longitude": "Zemljopisna du\u017eina", - "radius": "Radius (koriste\u0107i sustav osnovne jedinice)" - }, - "title": "Ispunite podatke o lokaciji." - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/wwlln/translations/it.json b/homeassistant/components/wwlln/translations/it.json deleted file mode 100644 index 4193ccab890..00000000000 --- a/homeassistant/components/wwlln/translations/it.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Questa posizione \u00e8 gi\u00e0 registrata." - }, - "step": { - "user": { - "data": { - "latitude": "Latitudine", - "longitude": "Longitudine", - "radius": "Raggio (utilizzando il tuo sistema di unit\u00e0 di misura di base)" - }, - "title": "Inserisci le informazioni sulla tua posizione." - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/wwlln/translations/ko.json b/homeassistant/components/wwlln/translations/ko.json deleted file mode 100644 index 5ddf4f05184..00000000000 --- a/homeassistant/components/wwlln/translations/ko.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\uc774 \uc704\uce58\ub294 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" - }, - "step": { - "user": { - "data": { - "latitude": "\uc704\ub3c4", - "longitude": "\uacbd\ub3c4", - "radius": "\ubc18\uacbd (\uae30\ubcf8 \ub2e8\uc704 \uc2dc\uc2a4\ud15c \uc0ac\uc6a9)" - }, - "title": "\uc704\uce58 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694." - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/wwlln/translations/lb.json b/homeassistant/components/wwlln/translations/lb.json deleted file mode 100644 index dcdc2becf06..00000000000 --- a/homeassistant/components/wwlln/translations/lb.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "D\u00ebse Standuert ass scho registr\u00e9iert" - }, - "step": { - "user": { - "data": { - "latitude": "Breedegrad", - "longitude": "L\u00e4ngegrad", - "radius": "Radius (mat \u00e4ren Basis Unit\u00e9ite System)" - }, - "title": "F\u00ebllt \u00e4r Informatiounen aus." - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/wwlln/translations/nl.json b/homeassistant/components/wwlln/translations/nl.json deleted file mode 100644 index 34388295976..00000000000 --- a/homeassistant/components/wwlln/translations/nl.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Deze locatie is al geregistreerd." - }, - "step": { - "user": { - "data": { - "latitude": "Breedtegraad", - "longitude": "Lengtegraad", - "radius": "Radius (met behulp van uw basisstation systeem)" - }, - "title": "Vul uw locatiegegevens in." - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/wwlln/translations/no.json b/homeassistant/components/wwlln/translations/no.json deleted file mode 100644 index 4ce9b99b738..00000000000 --- a/homeassistant/components/wwlln/translations/no.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Denne plasseringen er allerede registrert." - }, - "step": { - "user": { - "data": { - "latitude": "Breddegrad", - "longitude": "Lengdegrad", - "radius": "Radius (ved hjelp av ditt basenhetssystem)" - }, - "title": "Fyll ut posisjonsinformasjonen din." - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/wwlln/translations/pl.json b/homeassistant/components/wwlln/translations/pl.json deleted file mode 100644 index 04071c31cb1..00000000000 --- a/homeassistant/components/wwlln/translations/pl.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Ta lokalizacja jest ju\u017c zarejestrowana." - }, - "step": { - "user": { - "data": { - "latitude": "Szeroko\u015b\u0107 geograficzna", - "longitude": "D\u0142ugo\u015b\u0107 geograficzna", - "radius": "Promie\u0144 (przy u\u017cyciu systemu jednostki bazowej)" - }, - "title": "Wprowad\u017a informacje o lokalizacji." - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/wwlln/translations/pt-BR.json b/homeassistant/components/wwlln/translations/pt-BR.json deleted file mode 100644 index 9119d281dd5..00000000000 --- a/homeassistant/components/wwlln/translations/pt-BR.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "latitude": "Latitude", - "longitude": "Longitude", - "radius": "Raio (usando seu sistema de unidade base)" - }, - "title": "Preencha suas informa\u00e7\u00f5es de localiza\u00e7\u00e3o." - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/wwlln/translations/ru.json b/homeassistant/components/wwlln/translations/ru.json deleted file mode 100644 index 007704bf406..00000000000 --- a/homeassistant/components/wwlln/translations/ru.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." - }, - "step": { - "user": { - "data": { - "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", - "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", - "radius": "\u0420\u0430\u0434\u0438\u0443\u0441 (\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0412\u0430\u0448\u0443 \u0431\u0430\u0437\u043e\u0432\u0443\u044e \u0441\u0438\u0441\u0442\u0435\u043c\u0443 \u0435\u0434\u0438\u043d\u0438\u0446)" - }, - "title": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/wwlln/translations/sl.json b/homeassistant/components/wwlln/translations/sl.json deleted file mode 100644 index 55712fd8590..00000000000 --- a/homeassistant/components/wwlln/translations/sl.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Ta lokacija je \u017ee registrirana." - }, - "step": { - "user": { - "data": { - "latitude": "Zemljepisna \u0161irina", - "longitude": "Zemljepisna dol\u017eina", - "radius": "Obmo\u010dje (z uporabo va\u0161ih osnovnih enot)" - }, - "title": "Izpolnite podatke o va\u0161i lokaciji." - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/wwlln/translations/sv.json b/homeassistant/components/wwlln/translations/sv.json deleted file mode 100644 index 22a91d7dfcc..00000000000 --- a/homeassistant/components/wwlln/translations/sv.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "latitude": "Latitud", - "longitude": "Longitud", - "radius": "Radie (i basinst\u00e4llningarnas enheter)" - }, - "title": "Fyll i platsinformation." - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/wwlln/translations/zh-Hans.json b/homeassistant/components/wwlln/translations/zh-Hans.json deleted file mode 100644 index 5346a777cd6..00000000000 --- a/homeassistant/components/wwlln/translations/zh-Hans.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "latitude": "\u7eac\u5ea6", - "longitude": "\u7ecf\u5ea6", - "radius": "\u534a\u5f84\uff08\u4f7f\u7528\u57fa\u672c\u5355\u4f4d\u7cfb\u7edf\uff09" - }, - "title": "\u586b\u5199\u60a8\u7684\u4f4d\u7f6e\u4fe1\u606f\u3002" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/wwlln/translations/zh-Hant.json b/homeassistant/components/wwlln/translations/zh-Hant.json deleted file mode 100644 index 7b4bfaa1c08..00000000000 --- a/homeassistant/components/wwlln/translations/zh-Hant.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u6b64\u4f4d\u7f6e\u5df2\u8a3b\u518a\u3002" - }, - "step": { - "user": { - "data": { - "latitude": "\u7def\u5ea6", - "longitude": "\u7d93\u5ea6", - "radius": "\u534a\u5f91\uff08\u4f7f\u7528\u57fa\u672c\u55ae\u4f4d\u7cfb\u7d71\uff09" - }, - "title": "\u586b\u5beb\u5ea7\u6a19\u8cc7\u8a0a\u3002" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zigbee/__init__.py b/homeassistant/components/xbee/__init__.py similarity index 88% rename from homeassistant/components/zigbee/__init__.py rename to homeassistant/components/xbee/__init__.py index 2fa8291538e..31e2d6dc495 100644 --- a/homeassistant/components/zigbee/__init__.py +++ b/homeassistant/components/xbee/__init__.py @@ -1,4 +1,4 @@ -"""Support for Zigbee devices.""" +"""Support for XBee Zigbee devices.""" from binascii import hexlify, unhexlify import logging @@ -23,9 +23,9 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -DOMAIN = "zigbee" +DOMAIN = "xbee" -SIGNAL_ZIGBEE_FRAME_RECEIVED = "zigbee_frame_received" +SIGNAL_XBEE_FRAME_RECEIVED = "xbee_frame_received" CONF_BAUD = "baud" @@ -58,28 +58,28 @@ PLATFORM_SCHEMA = vol.Schema( def setup(hass, config): - """Set up the connection to the Zigbee device.""" + """Set up the connection to the XBee Zigbee device.""" usb_device = config[DOMAIN].get(CONF_DEVICE, DEFAULT_DEVICE) baud = int(config[DOMAIN].get(CONF_BAUD, DEFAULT_BAUD)) try: ser = Serial(usb_device, baud) except SerialException as exc: - _LOGGER.exception("Unable to open serial port for Zigbee: %s", exc) + _LOGGER.exception("Unable to open serial port for XBee: %s", exc) return False zigbee_device = ZigBee(ser) def close_serial_port(*args): - """Close the serial port we're using to communicate with the Zigbee.""" + """Close the serial port we're using to communicate with the XBee.""" zigbee_device.zb.serial.close() def _frame_received(frame): - """Run when a Zigbee frame is received. + """Run when a XBee Zigbee frame is received. Pickles the frame, then encodes it into base64 since it contains non JSON serializable binary. """ - dispatcher_send(hass, SIGNAL_ZIGBEE_FRAME_RECEIVED, frame) + dispatcher_send(hass, SIGNAL_XBEE_FRAME_RECEIVED, frame) hass.data[DOMAIN] = zigbee_device hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, close_serial_port) @@ -92,12 +92,10 @@ def frame_is_relevant(entity, frame): """Test whether the frame is relevant to the entity.""" if frame.get("source_addr_long") != entity.config.address: return False - if "samples" not in frame: - return False - return True + return "samples" in frame -class ZigBeeConfig: +class XBeeConfig: """Handle the fetching of configuration from the config file.""" def __init__(self, config): @@ -115,7 +113,7 @@ class ZigBeeConfig: """Return the address of the device. If an address has been provided, unhexlify it, otherwise return None - as we're talking to our local Zigbee device. + as we're talking to our local XBee device. """ address = self._config.get("address") if address is not None: @@ -128,7 +126,7 @@ class ZigBeeConfig: return self._should_poll -class ZigBeePinConfig(ZigBeeConfig): +class XBeePinConfig(XBeeConfig): """Handle the fetching of configuration from the configuration file.""" @property @@ -137,11 +135,11 @@ class ZigBeePinConfig(ZigBeeConfig): return self._config["pin"] -class ZigBeeDigitalInConfig(ZigBeePinConfig): - """A subclass of ZigBeePinConfig.""" +class XBeeDigitalInConfig(XBeePinConfig): + """A subclass of XBeePinConfig.""" def __init__(self, config): - """Initialise the Zigbee Digital input config.""" + """Initialise the XBee Zigbee Digital input config.""" super().__init__(config) self._bool2state, self._state2bool = self.boolean_maps @@ -177,15 +175,15 @@ class ZigBeeDigitalInConfig(ZigBeePinConfig): return self._state2bool -class ZigBeeDigitalOutConfig(ZigBeePinConfig): - """A subclass of ZigBeePinConfig. +class XBeeDigitalOutConfig(XBeePinConfig): + """A subclass of XBeePinConfig. Set _should_poll to default as False instead of True. The value will still be overridden by the presence of a 'poll' config entry. """ def __init__(self, config): - """Initialize the Zigbee Digital out.""" + """Initialize the XBee Zigbee Digital out.""" super().__init__(config) self._bool2state, self._state2bool = self.boolean_maps self._should_poll = config.get("poll", False) @@ -227,8 +225,8 @@ class ZigBeeDigitalOutConfig(ZigBeePinConfig): return self._state2bool -class ZigBeeAnalogInConfig(ZigBeePinConfig): - """Representation of a Zigbee GPIO pin set to analog in.""" +class XBeeAnalogInConfig(XBeePinConfig): + """Representation of a XBee Zigbee GPIO pin set to analog in.""" @property def max_voltage(self): @@ -236,7 +234,7 @@ class ZigBeeAnalogInConfig(ZigBeePinConfig): return float(self._config.get("max_volts", DEFAULT_ADC_MAX_VOLTS)) -class ZigBeeDigitalIn(Entity): +class XBeeDigitalIn(Entity): """Representation of a GPIO pin configured as a digital input.""" def __init__(self, config, device): @@ -268,7 +266,7 @@ class ZigBeeDigitalIn(Entity): ] self.schedule_update_ha_state() - async_dispatcher_connect(self.hass, SIGNAL_ZIGBEE_FRAME_RECEIVED, handle_frame) + async_dispatcher_connect(self.hass, SIGNAL_XBEE_FRAME_RECEIVED, handle_frame) @property def name(self): @@ -316,11 +314,11 @@ class ZigBeeDigitalIn(Entity): self._state = self._config.state2bool[sample[pin_name]] -class ZigBeeDigitalOut(ZigBeeDigitalIn): +class XBeeDigitalOut(XBeeDigitalIn): """Representation of a GPIO pin configured as a digital input.""" def _set_state(self, state): - """Initialize the Zigbee digital out device.""" + """Initialize the XBee Zigbee digital out device.""" try: self._device.set_gpio_pin( self._config.pin, self._config.bool2state[state], self._config.address @@ -333,7 +331,7 @@ class ZigBeeDigitalOut(ZigBeeDigitalIn): ) return except ZigBeeException as exc: - _LOGGER.exception("Unable to set digital pin on Zigbee device: %s", exc) + _LOGGER.exception("Unable to set digital pin on XBee device: %s", exc) return self._state = state if not self.should_poll: @@ -348,7 +346,7 @@ class ZigBeeDigitalOut(ZigBeeDigitalIn): self._set_state(False) def update(self): - """Ask the Zigbee device what its output is set to.""" + """Ask the XBee device what its output is set to.""" try: pin_state = self._device.get_gpio_pin( self._config.pin, self._config.address @@ -362,17 +360,17 @@ class ZigBeeDigitalOut(ZigBeeDigitalIn): return except ZigBeeException as exc: _LOGGER.exception( - "Unable to get output pin status from Zigbee device: %s", exc + "Unable to get output pin status from XBee device: %s", exc ) return self._state = self._config.state2bool[pin_state] -class ZigBeeAnalogIn(Entity): +class XBeeAnalogIn(Entity): """Representation of a GPIO pin configured as an analog input.""" def __init__(self, config, device): - """Initialize the ZigBee analog in device.""" + """Initialize the XBee analog in device.""" self._config = config self._device = device self._value = None @@ -398,7 +396,7 @@ class ZigBeeAnalogIn(Entity): ) self.schedule_update_ha_state() - async_dispatcher_connect(self.hass, SIGNAL_ZIGBEE_FRAME_RECEIVED, handle_frame) + async_dispatcher_connect(self.hass, SIGNAL_XBEE_FRAME_RECEIVED, handle_frame) @property def name(self): diff --git a/homeassistant/components/zigbee/binary_sensor.py b/homeassistant/components/xbee/binary_sensor.py similarity index 54% rename from homeassistant/components/zigbee/binary_sensor.py rename to homeassistant/components/xbee/binary_sensor.py index fe35b54a88f..47c7515ddc7 100644 --- a/homeassistant/components/zigbee/binary_sensor.py +++ b/homeassistant/components/xbee/binary_sensor.py @@ -3,7 +3,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import BinarySensorEntity -from . import DOMAIN, PLATFORM_SCHEMA, ZigBeeDigitalIn, ZigBeeDigitalInConfig +from . import DOMAIN, PLATFORM_SCHEMA, XBeeDigitalIn, XBeeDigitalInConfig CONF_ON_STATE = "on_state" @@ -14,12 +14,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Optional(CONF_ON_STATE): vol.In(ST def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Zigbee binary sensor platform.""" + """Set up the XBee Zigbee binary sensor platform.""" zigbee_device = hass.data[DOMAIN] - add_entities( - [ZigBeeBinarySensor(ZigBeeDigitalInConfig(config), zigbee_device)], True - ) + add_entities([XBeeBinarySensor(XBeeDigitalInConfig(config), zigbee_device)], True) -class ZigBeeBinarySensor(ZigBeeDigitalIn, BinarySensorEntity): - """Use ZigBeeDigitalIn as binary sensor.""" +class XBeeBinarySensor(XBeeDigitalIn, BinarySensorEntity): + """Use XBeeDigitalIn as binary sensor.""" diff --git a/homeassistant/components/zigbee/light.py b/homeassistant/components/xbee/light.py similarity index 61% rename from homeassistant/components/zigbee/light.py rename to homeassistant/components/xbee/light.py index 10bb87aa426..76ed8120166 100644 --- a/homeassistant/components/zigbee/light.py +++ b/homeassistant/components/xbee/light.py @@ -1,9 +1,9 @@ -"""Support for Zigbee lights.""" +"""Support for XBee Zigbee lights.""" import voluptuous as vol from homeassistant.components.light import LightEntity -from . import DOMAIN, PLATFORM_SCHEMA, ZigBeeDigitalOut, ZigBeeDigitalOutConfig +from . import DOMAIN, PLATFORM_SCHEMA, XBeeDigitalOut, XBeeDigitalOutConfig CONF_ON_STATE = "on_state" @@ -18,8 +18,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Create and add an entity based on the configuration.""" zigbee_device = hass.data[DOMAIN] - add_entities([ZigBeeLight(ZigBeeDigitalOutConfig(config), zigbee_device)]) + add_entities([XBeeLight(XBeeDigitalOutConfig(config), zigbee_device)]) -class ZigBeeLight(ZigBeeDigitalOut, LightEntity): - """Use ZigBeeDigitalOut as light.""" +class XBeeLight(XBeeDigitalOut, LightEntity): + """Use XBeeDigitalOut as light.""" diff --git a/homeassistant/components/xbee/manifest.json b/homeassistant/components/xbee/manifest.json new file mode 100644 index 00000000000..9d70751e230 --- /dev/null +++ b/homeassistant/components/xbee/manifest.json @@ -0,0 +1,7 @@ +{ + "domain": "xbee", + "name": "XBee", + "documentation": "https://www.home-assistant.io/integrations/xbee", + "requirements": ["xbee-helper==0.0.7"], + "codeowners": [] +} diff --git a/homeassistant/components/zigbee/sensor.py b/homeassistant/components/xbee/sensor.py similarity index 84% rename from homeassistant/components/zigbee/sensor.py rename to homeassistant/components/xbee/sensor.py index 0c709a6d1a5..4a392691032 100644 --- a/homeassistant/components/zigbee/sensor.py +++ b/homeassistant/components/xbee/sensor.py @@ -1,4 +1,4 @@ -"""Support for Zigbee sensors.""" +"""Support for XBee Zigbee sensors.""" from binascii import hexlify import logging @@ -8,13 +8,7 @@ from xbee_helper.exceptions import ZigBeeException, ZigBeeTxFailure from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity import Entity -from . import ( - DOMAIN, - PLATFORM_SCHEMA, - ZigBeeAnalogIn, - ZigBeeAnalogInConfig, - ZigBeeConfig, -) +from . import DOMAIN, PLATFORM_SCHEMA, XBeeAnalogIn, XBeeAnalogInConfig, XBeeConfig _LOGGER = logging.getLogger(__name__) @@ -33,7 +27,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Zigbee platform. + """Set up the XBee Zigbee platform. Uses the 'type' config value to work out which type of Zigbee sensor we're dealing with and instantiates the relevant classes to handle it. @@ -44,13 +38,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): try: sensor_class, config_class = TYPE_CLASSES[typ] except KeyError: - _LOGGER.exception("Unknown Zigbee sensor type: %s", typ) + _LOGGER.exception("Unknown XBee Zigbee sensor type: %s", typ) return add_entities([sensor_class(config_class(config), zigbee_device)], True) -class ZigBeeTemperatureSensor(Entity): +class XBeeTemperatureSensor(Entity): """Representation of XBee Pro temperature sensor.""" def __init__(self, config, device): @@ -90,6 +84,6 @@ class ZigBeeTemperatureSensor(Entity): # This must be below the classes to which it refers. TYPE_CLASSES = { - "temperature": (ZigBeeTemperatureSensor, ZigBeeConfig), - "analog": (ZigBeeAnalogIn, ZigBeeAnalogInConfig), + "temperature": (XBeeTemperatureSensor, XBeeConfig), + "analog": (XBeeAnalogIn, XBeeAnalogInConfig), } diff --git a/homeassistant/components/zigbee/switch.py b/homeassistant/components/xbee/switch.py similarity index 50% rename from homeassistant/components/zigbee/switch.py rename to homeassistant/components/xbee/switch.py index f5b73f5d328..cdb0d2677c5 100644 --- a/homeassistant/components/zigbee/switch.py +++ b/homeassistant/components/xbee/switch.py @@ -1,9 +1,9 @@ -"""Support for Zigbee switches.""" +"""Support for XBee Zigbee switches.""" import voluptuous as vol from homeassistant.components.switch import SwitchEntity -from . import DOMAIN, PLATFORM_SCHEMA, ZigBeeDigitalOut, ZigBeeDigitalOutConfig +from . import DOMAIN, PLATFORM_SCHEMA, XBeeDigitalOut, XBeeDigitalOutConfig CONF_ON_STATE = "on_state" @@ -15,10 +15,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Optional(CONF_ON_STATE): vol.In(ST def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Zigbee switch platform.""" + """Set up the XBee Zigbee switch platform.""" zigbee_device = hass.data[DOMAIN] - add_entities([ZigBeeSwitch(ZigBeeDigitalOutConfig(config), zigbee_device)]) + add_entities([XBeeSwitch(XBeeDigitalOutConfig(config), zigbee_device)]) -class ZigBeeSwitch(ZigBeeDigitalOut, SwitchEntity): - """Representation of a Zigbee Digital Out device.""" +class XBeeSwitch(XBeeDigitalOut, SwitchEntity): + """Representation of a XBee Zigbee Digital Out device.""" diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 0dd03e42e7d..9524406a1f9 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -38,7 +38,11 @@ async def async_setup_gateway_entry( host = entry.data[CONF_HOST] token = entry.data[CONF_TOKEN] name = entry.title - gateway_id = entry.data["gateway_id"] + gateway_id = entry.unique_id + + # For backwards compat + if entry.unique_id.endswith("-gateway"): + hass.config_entries.async_update_entry(entry, unique_id=entry.data["mac"]) # Connect to gateway gateway = ConnectXiaomiGateway(hass) diff --git a/homeassistant/components/xiaomi_miio/alarm_control_panel.py b/homeassistant/components/xiaomi_miio/alarm_control_panel.py index dccd94dc963..7df9dc54e5a 100644 --- a/homeassistant/components/xiaomi_miio/alarm_control_panel.py +++ b/homeassistant/components/xiaomi_miio/alarm_control_panel.py @@ -33,10 +33,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): f"{config_entry.title} Alarm", config_entry.data["model"], config_entry.data["mac"], - config_entry.data["gateway_id"], + config_entry.unique_id, ) entities.append(entity) - async_add_entities(entities) + async_add_entities(entities, update_before_add=True) class XiaomiGatewayAlarm(AlarmControlPanelEntity): diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py index 092f5d85d30..e35aa0c8b10 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -5,6 +5,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN +from homeassistant.helpers.device_registry import format_mac # pylint: disable=unused-import from .const import DOMAIN @@ -15,14 +16,13 @@ _LOGGER = logging.getLogger(__name__) CONF_FLOW_TYPE = "config_flow_device" CONF_GATEWAY = "gateway" DEFAULT_GATEWAY_NAME = "Xiaomi Gateway" +ZEROCONF_GATEWAY = "lumi-gateway" -GATEWAY_CONFIG = vol.Schema( - { - vol.Required(CONF_HOST): str, - vol.Required(CONF_TOKEN): vol.All(str, vol.Length(min=32, max=32)), - vol.Optional(CONF_NAME, default=DEFAULT_GATEWAY_NAME): str, - } -) +GATEWAY_SETTINGS = { + vol.Required(CONF_TOKEN): vol.All(str, vol.Length(min=32, max=32)), + vol.Optional(CONF_NAME, default=DEFAULT_GATEWAY_NAME): str, +} +GATEWAY_CONFIG = vol.Schema({vol.Required(CONF_HOST): str}).extend(GATEWAY_SETTINGS) CONFIG_SCHEMA = vol.Schema({vol.Optional(CONF_GATEWAY, default=False): bool}) @@ -33,6 +33,10 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + def __init__(self): + """Initialize.""" + self.host = None + async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" errors = {} @@ -47,36 +51,67 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=CONFIG_SCHEMA, errors=errors ) + async def async_step_zeroconf(self, discovery_info): + """Handle zeroconf discovery.""" + name = discovery_info.get("name") + self.host = discovery_info.get("host") + mac_address = discovery_info.get("properties", {}).get("mac") + + if not name or not self.host or not mac_address: + return self.async_abort(reason="not_xiaomi_miio") + + # Check which device is discovered. + if name.startswith(ZEROCONF_GATEWAY): + unique_id = format_mac(mac_address) + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured({CONF_HOST: self.host}) + + return await self.async_step_gateway() + + # Discovered device is not yet supported + _LOGGER.debug( + "Not yet supported Xiaomi Miio device '%s' discovered with host %s", + name, + self.host, + ) + return self.async_abort(reason="not_xiaomi_miio") + async def async_step_gateway(self, user_input=None): """Handle a flow initialized by the user to configure a gateway.""" errors = {} if user_input is not None: - host = user_input[CONF_HOST] token = user_input[CONF_TOKEN] + if user_input.get(CONF_HOST): + self.host = user_input[CONF_HOST] # Try to connect to a Xiaomi Gateway. connect_gateway_class = ConnectXiaomiGateway(self.hass) - await connect_gateway_class.async_connect_gateway(host, token) + await connect_gateway_class.async_connect_gateway(self.host, token) gateway_info = connect_gateway_class.gateway_info if gateway_info is not None: - unique_id = f"{gateway_info.model}-{gateway_info.mac_address}-gateway" + mac = format_mac(gateway_info.mac_address) + unique_id = mac await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured() return self.async_create_entry( title=user_input[CONF_NAME], data={ CONF_FLOW_TYPE: CONF_GATEWAY, - CONF_HOST: host, + CONF_HOST: self.host, CONF_TOKEN: token, - "gateway_id": unique_id, "model": gateway_info.model, - "mac": gateway_info.mac_address, + "mac": mac, }, ) errors["base"] = "connect_error" + if self.host: + schema = vol.Schema(GATEWAY_SETTINGS) + else: + schema = GATEWAY_CONFIG + return self.async_show_form( - step_id="gateway", data_schema=GATEWAY_CONFIG, errors=errors + step_id="gateway", data_schema=schema, errors=errors ) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index aaeaf19c5f9..77f398aa3ad 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -34,6 +34,8 @@ SERVICE_EYECARE_MODE_OFF = "light_eyecare_mode_off" # Remote Services SERVICE_LEARN = "remote_learn_command" +SERVICE_SET_LED_ON = "remote_set_led_on" +SERVICE_SET_LED_OFF = "remote_set_led_off" # Switch Services SERVICE_SET_WIFI_LED_ON = "switch_set_wifi_led_on" diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index 468389b4626..e1ead8d966c 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", "requirements": ["construct==2.9.45", "python-miio==0.5.0.1"], - "codeowners": ["@rytilahti", "@syssi"] + "codeowners": ["@rytilahti", "@syssi"], + "zeroconf": ["_miio._udp.local."] } diff --git a/homeassistant/components/xiaomi_miio/remote.py b/homeassistant/components/xiaomi_miio/remote.py index fb188368127..6fbf4b8a0f6 100644 --- a/homeassistant/components/xiaomi_miio/remote.py +++ b/homeassistant/components/xiaomi_miio/remote.py @@ -15,7 +15,6 @@ from homeassistant.components.remote import ( RemoteEntity, ) from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_COMMAND, CONF_HOST, CONF_NAME, @@ -23,10 +22,10 @@ from homeassistant.const import ( CONF_TOKEN, ) from homeassistant.exceptions import PlatformNotReady -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.util.dt import utcnow -from .const import DOMAIN, SERVICE_LEARN +from .const import SERVICE_LEARN, SERVICE_SET_LED_OFF, SERVICE_SET_LED_ON _LOGGER = logging.getLogger(__name__) @@ -38,14 +37,6 @@ CONF_COMMANDS = "commands" DEFAULT_TIMEOUT = 10 DEFAULT_SLOT = 1 -LEARN_COMMAND_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): vol.All(str), - vol.Optional(CONF_TIMEOUT, default=10): vol.All(int, vol.Range(min=0)), - vol.Optional(CONF_SLOT, default=1): vol.All(int, vol.Range(min=1, max=1000000)), - } -) - COMMAND_SCHEMA = vol.Schema( {vol.Required(CONF_COMMAND): vol.All(cv.ensure_list, [cv.string])} ) @@ -112,22 +103,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([xiaomi_miio_remote]) - async def async_service_handler(service): + async def async_service_led_off_handler(entity, service): + """Handle set_led_off command.""" + await hass.async_add_executor_job(entity.device.set_indicator_led, False) + + async def async_service_led_on_handler(entity, service): + """Handle set_led_on command.""" + await hass.async_add_executor_job(entity.device.set_indicator_led, True) + + async def async_service_learn_handler(entity, service): """Handle a learn command.""" - if service.service != SERVICE_LEARN: - _LOGGER.error("We should not handle service: %s", service.service) - return - - entity_id = service.data.get(ATTR_ENTITY_ID) - entity = None - for remote in hass.data[DATA_KEY].values(): - if remote.entity_id == entity_id: - entity = remote - - if not entity: - _LOGGER.error("entity_id: '%s' not found", entity_id) - return - device = entity.device slot = service.data.get(CONF_SLOT, entity.slot) @@ -160,8 +145,23 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= "Timeout. No infrared command captured", title="Xiaomi Miio Remote" ) - hass.services.async_register( - DOMAIN, SERVICE_LEARN, async_service_handler, schema=LEARN_COMMAND_SCHEMA + platform = entity_platform.current_platform.get() + + platform.async_register_entity_service( + SERVICE_LEARN, + { + vol.Optional(CONF_TIMEOUT, default=10): vol.All(int, vol.Range(min=0)), + vol.Optional(CONF_SLOT, default=1): vol.All( + int, vol.Range(min=1, max=1000000) + ), + }, + async_service_learn_handler, + ) + platform.async_register_entity_service( + SERVICE_SET_LED_ON, {}, async_service_led_on_handler, + ) + platform.async_register_entity_service( + SERVICE_SET_LED_OFF, {}, async_service_led_off_handler, ) diff --git a/homeassistant/components/xiaomi_miio/services.yaml b/homeassistant/components/xiaomi_miio/services.yaml index a5308b08d6d..a92e46f11a1 100644 --- a/homeassistant/components/xiaomi_miio/services.yaml +++ b/homeassistant/components/xiaomi_miio/services.yaml @@ -224,6 +224,20 @@ remote_learn_command: description: "Define the timeout in seconds, before which the command must be learned." example: "30" +remote_set_led_on: + description: 'Turn on blue LED.' + fields: + entity_id: + description: "Name of the entity to turn LED on." + example: "remote.xiaomi_miio" + +remote_set_led_off: + description: 'Turn off blue LED.' + fields: + entity_id: + description: "Name of the entity to turn LED off." + example: "remote.xiaomi_miio" + switch_set_wifi_led_on: description: Turn the wifi led on. fields: diff --git a/homeassistant/components/xiaomi_miio/strings.json b/homeassistant/components/xiaomi_miio/strings.json index 150cebe084a..0024ffdfe9b 100644 --- a/homeassistant/components/xiaomi_miio/strings.json +++ b/homeassistant/components/xiaomi_miio/strings.json @@ -23,7 +23,8 @@ "no_device_selected": "No device selected, please select one device." }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_in_progress": "Config flow for this Xiaomi Miio device is already in progress." } } } diff --git a/homeassistant/components/xiaomi_miio/translations/ca.json b/homeassistant/components/xiaomi_miio/translations/ca.json index 1c6c21e02f0..81dc08731c3 100644 --- a/homeassistant/components/xiaomi_miio/translations/ca.json +++ b/homeassistant/components/xiaomi_miio/translations/ca.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "[%key::common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key::common::config_flow::abort::already_configured_device%]", + "already_in_progress": "El flux de dades de configuraci\u00f3 pel dispositiu Xiaomi Miio ja est\u00e0 en curs." }, "error": { "connect_error": "[%key::common::config_flow::error::cannot_connect%]", diff --git a/homeassistant/components/xiaomi_miio/translations/de.json b/homeassistant/components/xiaomi_miio/translations/de.json index d60099d7538..1ce000e4674 100644 --- a/homeassistant/components/xiaomi_miio/translations/de.json +++ b/homeassistant/components/xiaomi_miio/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf f\u00fcr dieses Xiaomi Miio-Ger\u00e4t wird bereits ausgef\u00fchrt." }, "error": { "connect_error": "Verbindung fehlgeschlagen", diff --git a/homeassistant/components/xiaomi_miio/translations/en.json b/homeassistant/components/xiaomi_miio/translations/en.json index f67df5b7826..49c5c683993 100644 --- a/homeassistant/components/xiaomi_miio/translations/en.json +++ b/homeassistant/components/xiaomi_miio/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "already_in_progress": "Config flow for this Xiaomi Miio device is already in progress." }, "error": { "connect_error": "Failed to connect", diff --git a/homeassistant/components/xiaomi_miio/translations/es.json b/homeassistant/components/xiaomi_miio/translations/es.json index 5760a12ed04..41428edd02f 100644 --- a/homeassistant/components/xiaomi_miio/translations/es.json +++ b/homeassistant/components/xiaomi_miio/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado" + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n para este dispositivo Xiaomi Miio ya est\u00e1 en progreso." }, "error": { "connect_error": "No se ha podido conectar", diff --git a/homeassistant/components/xiaomi_miio/translations/fr.json b/homeassistant/components/xiaomi_miio/translations/fr.json index bf00d30bc6e..c494456fc68 100644 --- a/homeassistant/components/xiaomi_miio/translations/fr.json +++ b/homeassistant/components/xiaomi_miio/translations/fr.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_in_progress": "Le flux de configuration pour cet appareil Xiaomi Miio est d\u00e9j\u00e0 en cours." + }, "error": { "connect_error": "Impossible de se connecter, veuillez r\u00e9essayer", "no_device_selected": "Aucun appareil s\u00e9lectionn\u00e9, veuillez s\u00e9lectionner un appareil." diff --git a/homeassistant/components/xiaomi_miio/translations/hu.json b/homeassistant/components/xiaomi_miio/translations/hu.json index 9cdda6533b5..1cb7833427e 100644 --- a/homeassistant/components/xiaomi_miio/translations/hu.json +++ b/homeassistant/components/xiaomi_miio/translations/hu.json @@ -1,13 +1,16 @@ { "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, "error": { - "connect_error": "Nem siker\u00fclt csatlakozni, pr\u00f3b\u00e1lkozzon \u00fajra.", + "connect_error": "Sikertelen csatlakoz\u00e1s", "no_device_selected": "Nincs kiv\u00e1lasztva eszk\u00f6z, k\u00e9rj\u00fck, v\u00e1lasszon egyet." }, "step": { "gateway": { "data": { - "host": "IP-c\u00edm" + "host": "IP c\u00edm" }, "description": "Sz\u00fcks\u00e9ge lesz az API Tokenre, tov\u00e1bbi inforaciok: https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token" }, diff --git a/homeassistant/components/xiaomi_miio/translations/it.json b/homeassistant/components/xiaomi_miio/translations/it.json index 911e16a3dc5..dae9a8dbbc1 100644 --- a/homeassistant/components/xiaomi_miio/translations/it.json +++ b/homeassistant/components/xiaomi_miio/translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione per questo dispositivo Xiaomi Miio \u00e8 gi\u00e0 in corso." }, "error": { "connect_error": "Impossibile connettersi", diff --git a/homeassistant/components/xiaomi_miio/translations/ko.json b/homeassistant/components/xiaomi_miio/translations/ko.json index dbf3e091c5e..3a1e6574915 100644 --- a/homeassistant/components/xiaomi_miio/translations/ko.json +++ b/homeassistant/components/xiaomi_miio/translations/ko.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "Xiaomi Miio \uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4." }, "error": { "connect_error": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", @@ -14,7 +15,7 @@ "name": "\uac8c\uc774\ud2b8\uc6e8\uc774 \uc774\ub984", "token": "API \ud1a0\ud070" }, - "description": "API \ud1a0\ud070\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \uc548\ub0b4\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.", + "description": "API \ud1a0\ud070\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.", "title": "Xiaomi \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud558\uae30" }, "user": { diff --git a/homeassistant/components/xiaomi_miio/translations/lb.json b/homeassistant/components/xiaomi_miio/translations/lb.json index 05c8e2354b8..9be68bfd057 100644 --- a/homeassistant/components/xiaomi_miio/translations/lb.json +++ b/homeassistant/components/xiaomi_miio/translations/lb.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Apparat ass scho konfigur\u00e9iert" + "already_configured": "Apparat ass scho konfigur\u00e9iert", + "already_in_progress": "Konfiguratioun's Oflaf fir d\u00ebse Xiaomi Miio Apparat ass schonn am gaangen." }, "error": { "connect_error": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol.", diff --git a/homeassistant/components/xiaomi_miio/translations/nl.json b/homeassistant/components/xiaomi_miio/translations/nl.json index cb4aa077ba0..5daf94a5e0d 100644 --- a/homeassistant/components/xiaomi_miio/translations/nl.json +++ b/homeassistant/components/xiaomi_miio/translations/nl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_in_progress": "De configuratiestroom voor dit Xiaomi Miio-apparaat is al bezig." + }, "error": { "connect_error": "Verbinding mislukt, probeer het opnieuw", "no_device_selected": "Geen apparaat geselecteerd, selecteer 1 apparaat alstublieft" diff --git a/homeassistant/components/xiaomi_miio/translations/no.json b/homeassistant/components/xiaomi_miio/translations/no.json index 5a92830cdb7..a83cf030cc3 100644 --- a/homeassistant/components/xiaomi_miio/translations/no.json +++ b/homeassistant/components/xiaomi_miio/translations/no.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Enheten er allerede konfigurert" + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyt for denne Xiaomi Miio-enheten p\u00e5g\u00e5r allerede." }, "error": { "no_device_selected": "Ingen enhet valgt, vennligst velg en enhet." diff --git a/homeassistant/components/xiaomi_miio/translations/pl.json b/homeassistant/components/xiaomi_miio/translations/pl.json index da144095ad4..bd6dd1923d5 100644 --- a/homeassistant/components/xiaomi_miio/translations/pl.json +++ b/homeassistant/components/xiaomi_miio/translations/pl.json @@ -1,10 +1,11 @@ { "config": { "abort": { - "already_configured": "[%key_id:common::config_flow::abort::already_configured_device%]" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "already_in_progress": "Konfiguracja tego urz\u0105dzenia Xiaomi Miio jest ju\u017c w toku." }, "error": { - "connect_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", + "connect_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia.", "no_device_selected": "Nie wybrano \u017cadnego urz\u0105dzenia, wybierz jedno urz\u0105dzenie." }, "step": { @@ -12,7 +13,7 @@ "data": { "host": "Adres IP", "name": "Nazwa bramki", - "token": "Klucz API" + "token": "Token API" }, "description": "B\u0119dziesz potrzebowa\u0107 tokenu API, odwied\u017a https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token, aby uzyska\u0107 instrukcje.", "title": "Po\u0142\u0105cz si\u0119 z bramk\u0105 Xiaomi" diff --git a/homeassistant/components/xiaomi_miio/translations/pt-BR.json b/homeassistant/components/xiaomi_miio/translations/pt-BR.json new file mode 100644 index 00000000000..2f7f84af27e --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/pt-BR.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_in_progress": "O fluxo de configura\u00e7\u00e3o para este dispositivo Xiaomi Miio j\u00e1 est\u00e1 em andamento." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/ru.json b/homeassistant/components/xiaomi_miio/translations/ru.json index fef7362537f..edd4365f20b 100644 --- a/homeassistant/components/xiaomi_miio/translations/ru.json +++ b/homeassistant/components/xiaomi_miio/translations/ru.json @@ -1,7 +1,8 @@ { "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_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." }, "error": { "connect_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", diff --git a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json index 05806b15470..83f8143b120 100644 --- a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u5c0f\u7c73 Miio \u8a2d\u5099\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002" }, "error": { "connect_error": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index fd144e1edc7..f37c22a38aa 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -291,7 +291,7 @@ class MiroboVacuum(StateVacuumEntity): @property def fan_speed_list(self): """Get the list of available fan speed steps of the vacuum cleaner.""" - return list(self._fan_speeds) + return list(self._fan_speeds) if self._fan_speeds else [] @property def device_state_attributes(self): diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 1e0fe841cac..c36c7be00fa 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from typing import Optional import voluptuous as vol from yeelight import Bulb, BulbException @@ -201,8 +202,7 @@ class YeelightDevice: self._config = config self._ipaddr = ipaddr self._name = config.get(CONF_NAME) - self._model = config.get(CONF_MODEL) - self._bulb_device = Bulb(self.ipaddr, model=self._model) + self._bulb_device = Bulb(self.ipaddr, model=config.get(CONF_MODEL)) self._device_type = None self._available = False self._initialized = False @@ -234,8 +234,8 @@ class YeelightDevice: @property def model(self): - """Return configured device model.""" - return self._model + """Return configured/autodetected device model.""" + return self._bulb_device.model @property def is_nightlight_supported(self) -> bool: @@ -287,6 +287,11 @@ class YeelightDevice: return self._device_type + @property + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return self.bulb.capabilities.get("id") + def turn_on(self, duration=DEFAULT_TRANSITION, light_type=None, power_mode=None): """Turn on device.""" try: @@ -324,7 +329,20 @@ class YeelightDevice: return self._available + def _get_capabilities(self): + """Request device capabilities.""" + try: + self.bulb.get_capabilities() + except BulbException as ex: + _LOGGER.error( + "Unable to get device capabilities %s, %s: %s", + self.ipaddr, + self.name, + ex, + ) + def _initialize_device(self): + self._get_capabilities() self._initialized = True dispatcher_send(self._hass, DEVICE_INITIALIZED, self.ipaddr) @@ -335,8 +353,4 @@ class YeelightDevice: def setup(self): """Fetch initial device properties.""" - initial_update = self._update_properties() - - # We can build correct class anyway. - if not initial_update and self.model: - self._initialize_device() + self._update_properties() diff --git a/homeassistant/components/yeelight/binary_sensor.py b/homeassistant/components/yeelight/binary_sensor.py index 78a20ab0104..1696ca9bcb2 100644 --- a/homeassistant/components/yeelight/binary_sensor.py +++ b/homeassistant/components/yeelight/binary_sensor.py @@ -1,5 +1,6 @@ """Sensor platform support for yeelight.""" import logging +from typing import Optional from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -38,6 +39,21 @@ class YeelightNightlightModeSensor(BinarySensorEntity): ) ) + @property + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + unique = self._device.unique_id + + if unique: + return unique + "-nightlight_sensor" + + return None + + @property + def available(self) -> bool: + """Return if bulb is available.""" + return self._device.available + @property def should_poll(self): """No polling needed.""" diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 29f943906d6..244ccd5745d 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -1,5 +1,6 @@ """Light platform support for yeelight.""" import logging +from typing import Optional import voluptuous as vol import yeelight @@ -470,6 +471,12 @@ class YeelightGenericLight(LightEntity): """No polling needed.""" return False + @property + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + + return self.device.unique_id + @property def available(self) -> bool: """Return if bulb is available.""" @@ -902,6 +909,16 @@ class YeelightWithNightLight( class YeelightNightLightMode(YeelightGenericLight): """Representation of a Yeelight when in nightlight mode.""" + @property + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + unique = super().unique_id + + if unique: + return unique + "-nightlight" + + return None + @property def name(self) -> str: """Return the name of the device if any.""" @@ -985,6 +1002,14 @@ class YeelightAmbientLight(YeelightColorLightWithoutNightlightSwitch): self._light_type = LightType.Ambient + @property + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + unique = super().unique_id + + if unique: + return unique + "-ambilight" + @property def name(self) -> str: """Return the name of the device if any.""" diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index ad3022d5d5a..32ccf1c117e 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,7 +2,7 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": ["yeelight==0.5.1"], + "requirements": ["yeelight==0.5.2"], "after_dependencies": ["discovery"], "codeowners": ["@rytilahti", "@zewelor"] } diff --git a/homeassistant/components/yeelightsunflower/light.py b/homeassistant/components/yeelightsunflower/light.py index 2e17b92c90a..00ea467c0d1 100644 --- a/homeassistant/components/yeelightsunflower/light.py +++ b/homeassistant/components/yeelightsunflower/light.py @@ -25,7 +25,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string}) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Yeelight Sunflower Light platform.""" - host = config.get(CONF_HOST) hub = yeelightsunflower.Hub(host) @@ -45,13 +44,19 @@ class SunflowerBulb(LightEntity): self._available = light.available self._brightness = light.brightness self._is_on = light.is_on - self._hs_color = light.rgb_color + self._rgb_color = light.rgb_color + self._unique_id = light.zid @property def name(self): """Return the display name of this light.""" return f"sunflower_{self._light.zid}" + @property + def unique_id(self): + """Return the unique ID of this light.""" + return self._unique_id + @property def available(self): """Return True if entity is available.""" @@ -70,7 +75,7 @@ class SunflowerBulb(LightEntity): @property def hs_color(self): """Return the color property.""" - return self._hs_color + return color_util.color_RGB_to_hs(*self._rgb_color) @property def supported_features(self): @@ -104,4 +109,4 @@ class SunflowerBulb(LightEntity): self._available = self._light.available self._brightness = self._light.brightness self._is_on = self._light.is_on - self._hs_color = color_util.color_RGB_to_hs(*self._light.rgb_color) + self._rgb_color = self._light.rgb_color diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 2c6ae4f4225..9f0d203d2b3 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -1,4 +1,5 @@ """Support for exposing Home Assistant via Zeroconf.""" +import asyncio import ipaddress import logging import socket @@ -13,6 +14,7 @@ from zeroconf import ( ServiceInfo, ServiceStateChange, Zeroconf, + log as zeroconf_log, ) from homeassistant import util @@ -113,12 +115,20 @@ class HaZeroconf(Zeroconf): def setup(hass, config): """Set up Zeroconf and make Home Assistant discoverable.""" + # Zeroconf sets its log level to WARNING, reset it to allow filtering by the logger component. + zeroconf_log.setLevel(logging.NOTSET) zeroconf = hass.data[DOMAIN] = _get_instance( hass, config.get(DOMAIN, {}).get(CONF_DEFAULT_INTERFACE) ) - zeroconf_name = f"{hass.config.location_name}.{ZEROCONF_TYPE}" + + # Get instance UUID + uuid = asyncio.run_coroutine_threadsafe( + hass.helpers.instance_id.async_get(), hass.loop + ).result() params = { + "location_name": hass.config.location_name, + "uuid": uuid, "version": __version__, "external_url": "", "internal_url": "", @@ -128,6 +138,7 @@ def setup(hass, config): "requires_api_password": True, } + # Get instance URL's try: params["external_url"] = get_url(hass, allow_internal=False) except NoURLAvailableError: @@ -150,8 +161,8 @@ def setup(hass, config): info = ServiceInfo( ZEROCONF_TYPE, - zeroconf_name, - None, + name=f"{hass.config.location_name}.{ZEROCONF_TYPE}", + server=f"{uuid}.local.", addresses=[host_ip_pton], port=hass.http.server_port, properties=params, diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index e28594d5598..c5e0efe1fe3 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.26.3"], + "requirements": ["zeroconf==0.27.1"], "dependencies": ["api"], "codeowners": ["@robbiet480", "@Kane610"], "quality_scale": "internal" diff --git a/homeassistant/components/zerproc/translations/ca.json b/homeassistant/components/zerproc/translations/ca.json index dc21c371e60..ddc9a9298cb 100644 --- a/homeassistant/components/zerproc/translations/ca.json +++ b/homeassistant/components/zerproc/translations/ca.json @@ -9,5 +9,6 @@ "description": "Vols comen\u00e7ar la configuraci\u00f3?" } } - } + }, + "title": "Zerproc" } \ No newline at end of file diff --git a/homeassistant/components/zerproc/translations/de.json b/homeassistant/components/zerproc/translations/de.json new file mode 100644 index 00000000000..fdbf8971238 --- /dev/null +++ b/homeassistant/components/zerproc/translations/de.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" + }, + "step": { + "confirm": { + "description": "M\u00f6chtest du die Installation starten?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zerproc/translations/es.json b/homeassistant/components/zerproc/translations/es.json new file mode 100644 index 00000000000..192afd87e65 --- /dev/null +++ b/homeassistant/components/zerproc/translations/es.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "No se encontraron dispositivos en la red", + "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n." + }, + "step": { + "confirm": { + "description": "\u00bfQuieres comenzar a configurar?" + } + } + }, + "title": "Zerproc" +} \ No newline at end of file diff --git a/homeassistant/components/zerproc/translations/hu.json b/homeassistant/components/zerproc/translations/hu.json new file mode 100644 index 00000000000..6c61530acbe --- /dev/null +++ b/homeassistant/components/zerproc/translations/hu.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "step": { + "confirm": { + "description": "El szeretn\u00e9d kezdeni a be\u00e1ll\u00edt\u00e1st?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zerproc/translations/it.json b/homeassistant/components/zerproc/translations/it.json new file mode 100644 index 00000000000..91f3a3a984b --- /dev/null +++ b/homeassistant/components/zerproc/translations/it.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nessun dispositivo trovato sulla rete", + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "step": { + "confirm": { + "description": "Vuoi iniziare la configurazione?" + } + } + }, + "title": "Zerproc" +} \ No newline at end of file diff --git a/homeassistant/components/zerproc/translations/lb.json b/homeassistant/components/zerproc/translations/lb.json new file mode 100644 index 00000000000..e1d2e73d527 --- /dev/null +++ b/homeassistant/components/zerproc/translations/lb.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keng Apparater am Netzwierk fonnt" + } + }, + "title": "Zerproc" +} \ No newline at end of file diff --git a/homeassistant/components/zerproc/translations/nl.json b/homeassistant/components/zerproc/translations/nl.json new file mode 100644 index 00000000000..cdfd3890fb8 --- /dev/null +++ b/homeassistant/components/zerproc/translations/nl.json @@ -0,0 +1,3 @@ +{ + "title": "Zerproc" +} \ No newline at end of file diff --git a/homeassistant/components/zerproc/translations/pl.json b/homeassistant/components/zerproc/translations/pl.json new file mode 100644 index 00000000000..429b6f37e65 --- /dev/null +++ b/homeassistant/components/zerproc/translations/pl.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci.", + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "step": { + "confirm": { + "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" + } + } + }, + "title": "Zerproc" +} \ No newline at end of file diff --git a/homeassistant/components/zerproc/translations/pt-BR.json b/homeassistant/components/zerproc/translations/pt-BR.json new file mode 100644 index 00000000000..f00723f833b --- /dev/null +++ b/homeassistant/components/zerproc/translations/pt-BR.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "single_instance_allowed": "J\u00e1 configurado. Somente uma \u00fanica configura\u00e7\u00e3o poss\u00edvel." + }, + "step": { + "confirm": { + "description": "Deseja iniciar a configura\u00e7\u00e3o?" + } + } + }, + "title": "Zerproc" +} \ No newline at end of file diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py new file mode 100644 index 00000000000..3a0ff6455d2 --- /dev/null +++ b/homeassistant/components/zha/climate.py @@ -0,0 +1,585 @@ +""" +Climate on Zigbee Home Automation networks. + +For more details on this platform, please refer to the documentation +at https://home-assistant.io/components/zha.climate/ +""" +from datetime import datetime, timedelta +import enum +import functools +import logging +from random import randint +from typing import List, Optional, Tuple + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + ATTR_HVAC_MODE, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + CURRENT_HVAC_COOL, + CURRENT_HVAC_FAN, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, + DOMAIN, + FAN_AUTO, + FAN_ON, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + PRESET_AWAY, + PRESET_NONE, + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, +) +from homeassistant.const import ATTR_TEMPERATURE, PRECISION_HALVES, TEMP_CELSIUS +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.event import async_track_time_interval +import homeassistant.util.dt as dt_util + +from .core import discovery +from .core.const import ( + CHANNEL_FAN, + CHANNEL_THERMOSTAT, + DATA_ZHA, + DATA_ZHA_DISPATCHERS, + SIGNAL_ADD_ENTITIES, + SIGNAL_ATTR_UPDATED, +) +from .core.registries import ZHA_ENTITIES +from .entity import ZhaEntity + +DEPENDENCIES = ["zha"] + +ATTR_SYS_MODE = "system_mode" +ATTR_RUNNING_MODE = "running_mode" +ATTR_SETPT_CHANGE_SRC = "setpoint_change_source" +ATTR_SETPT_CHANGE_AMT = "setpoint_change_amount" +ATTR_OCCUPANCY = "occupancy" +ATTR_PI_COOLING_DEMAND = "pi_cooling_demand" +ATTR_PI_HEATING_DEMAND = "pi_heating_demand" +ATTR_OCCP_COOL_SETPT = "occupied_cooling_setpoint" +ATTR_OCCP_HEAT_SETPT = "occupied_heating_setpoint" +ATTR_UNOCCP_HEAT_SETPT = "unoccupied_heating_setpoint" +ATTR_UNOCCP_COOL_SETPT = "unoccupied_cooling_setpoint" + + +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) +RUNNING_MODE = {0x00: HVAC_MODE_OFF, 0x03: HVAC_MODE_COOL, 0x04: HVAC_MODE_HEAT} + + +class ThermostatFanMode(enum.IntEnum): + """Fan channel enum for thermostat Fans.""" + + OFF = 0x00 + ON = 0x04 + AUTO = 0x05 + + +class RunningState(enum.IntFlag): + """ZCL Running state enum.""" + + HEAT = 0x0001 + COOL = 0x0002 + FAN = 0x0004 + HEAT_STAGE_2 = 0x0008 + COOL_STAGE_2 = 0x0010 + FAN_STAGE_2 = 0x0020 + FAN_STAGE_3 = 0x0040 + + +SEQ_OF_OPERATION = { + 0x00: (HVAC_MODE_OFF, HVAC_MODE_COOL), # cooling only + 0x01: (HVAC_MODE_OFF, HVAC_MODE_COOL), # cooling with reheat + 0x02: (HVAC_MODE_OFF, HVAC_MODE_HEAT), # heating only + 0x03: (HVAC_MODE_OFF, HVAC_MODE_HEAT), # heating with reheat + # cooling and heating 4-pipes + 0x04: (HVAC_MODE_OFF, HVAC_MODE_HEAT_COOL, HVAC_MODE_COOL, HVAC_MODE_HEAT), + # cooling and heating 4-pipes + 0x05: (HVAC_MODE_OFF, HVAC_MODE_HEAT_COOL, HVAC_MODE_COOL, HVAC_MODE_HEAT), + 0x06: (HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF), # centralite specific + 0x07: (HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF), # centralite specific +} + + +class SystemMode(enum.IntEnum): + """ZCL System Mode attribute enum.""" + + OFF = 0x00 + HEAT_COOL = 0x01 + COOL = 0x03 + HEAT = 0x04 + AUX_HEAT = 0x05 + PRE_COOL = 0x06 + FAN_ONLY = 0x07 + DRY = 0x08 + SLEEP = 0x09 + + +HVAC_MODE_2_SYSTEM = { + HVAC_MODE_OFF: SystemMode.OFF, + HVAC_MODE_HEAT_COOL: SystemMode.HEAT_COOL, + HVAC_MODE_COOL: SystemMode.COOL, + HVAC_MODE_HEAT: SystemMode.HEAT, + HVAC_MODE_FAN_ONLY: SystemMode.FAN_ONLY, + HVAC_MODE_DRY: SystemMode.DRY, +} + +SYSTEM_MODE_2_HVAC = { + SystemMode.OFF: HVAC_MODE_OFF, + SystemMode.HEAT_COOL: HVAC_MODE_HEAT_COOL, + SystemMode.COOL: HVAC_MODE_COOL, + SystemMode.HEAT: HVAC_MODE_HEAT, + SystemMode.AUX_HEAT: HVAC_MODE_HEAT, + SystemMode.PRE_COOL: HVAC_MODE_COOL, # this is 'precooling'. is it the same? + SystemMode.FAN_ONLY: HVAC_MODE_FAN_ONLY, + SystemMode.DRY: HVAC_MODE_DRY, + SystemMode.SLEEP: HVAC_MODE_OFF, +} + +ZCL_TEMP = 100 + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Zigbee Home Automation sensor from config entry.""" + entities_to_create = hass.data[DATA_ZHA][DOMAIN] + unsub = async_dispatcher_connect( + hass, + SIGNAL_ADD_ENTITIES, + functools.partial( + discovery.async_add_entities, async_add_entities, entities_to_create + ), + ) + hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) + + +@STRICT_MATCH(channel_names=CHANNEL_THERMOSTAT, aux_channels=CHANNEL_FAN) +class Thermostat(ZhaEntity, ClimateEntity): + """Representation of a ZHA Thermostat device.""" + + DEFAULT_MAX_TEMP = 35 + DEFAULT_MIN_TEMP = 7 + + _domain = DOMAIN + value_attribute = 0x0000 + + def __init__(self, unique_id, zha_device, channels, **kwargs): + """Initialize ZHA Thermostat instance.""" + super().__init__(unique_id, zha_device, channels, **kwargs) + self._thrm = self.cluster_channels.get(CHANNEL_THERMOSTAT) + self._preset = PRESET_NONE + self._presets = [] + self._supported_flags = SUPPORT_TARGET_TEMPERATURE + self._fan = self.cluster_channels.get(CHANNEL_FAN) + + @property + def current_temperature(self): + """Return the current temperature.""" + if self._thrm.local_temp is None: + return None + return self._thrm.local_temp / ZCL_TEMP + + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + data = {} + if self.hvac_mode: + mode = SYSTEM_MODE_2_HVAC.get(self._thrm.system_mode, "unknown") + data[ATTR_SYS_MODE] = f"[{self._thrm.system_mode}]/{mode}" + if self._thrm.occupancy is not None: + data[ATTR_OCCUPANCY] = self._thrm.occupancy + if self._thrm.occupied_cooling_setpoint is not None: + data[ATTR_OCCP_COOL_SETPT] = self._thrm.occupied_cooling_setpoint + if self._thrm.occupied_heating_setpoint is not None: + data[ATTR_OCCP_HEAT_SETPT] = self._thrm.occupied_heating_setpoint + + unoccupied_cooling_setpoint = self._thrm.unoccupied_cooling_setpoint + if unoccupied_cooling_setpoint is not None: + data[ATTR_UNOCCP_HEAT_SETPT] = unoccupied_cooling_setpoint + + unoccupied_heating_setpoint = self._thrm.unoccupied_heating_setpoint + if unoccupied_heating_setpoint is not None: + data[ATTR_UNOCCP_COOL_SETPT] = unoccupied_heating_setpoint + return data + + @property + def fan_mode(self) -> Optional[str]: + """Return current FAN mode.""" + if self._thrm.running_state is None: + return FAN_AUTO + + if self._thrm.running_state & ( + RunningState.FAN | RunningState.FAN_STAGE_2 | RunningState.FAN_STAGE_3 + ): + return FAN_ON + return FAN_AUTO + + @property + def fan_modes(self) -> Optional[List[str]]: + """Return supported FAN modes.""" + if not self._fan: + return None + return [FAN_AUTO, FAN_ON] + + @property + def hvac_action(self) -> Optional[str]: + """Return the current HVAC action.""" + if ( + self._thrm.pi_heating_demand is None + and self._thrm.pi_cooling_demand is None + ): + return self._rm_rs_action + return self._pi_demand_action + + @property + def _rm_rs_action(self) -> Optional[str]: + """Return the current HVAC action based on running mode and running state.""" + + running_mode = self._thrm.running_mode + if running_mode == SystemMode.HEAT: + return CURRENT_HVAC_HEAT + if running_mode == SystemMode.COOL: + return CURRENT_HVAC_COOL + + running_state = self._thrm.running_state + if running_state and running_state & ( + RunningState.FAN | RunningState.FAN_STAGE_2 | RunningState.FAN_STAGE_3 + ): + return CURRENT_HVAC_FAN + if self.hvac_mode != HVAC_MODE_OFF and running_mode == SystemMode.OFF: + return CURRENT_HVAC_IDLE + return CURRENT_HVAC_OFF + + @property + def _pi_demand_action(self) -> Optional[str]: + """Return the current HVAC action based on pi_demands.""" + + heating_demand = self._thrm.pi_heating_demand + if heating_demand is not None and heating_demand > 0: + return CURRENT_HVAC_HEAT + cooling_demand = self._thrm.pi_cooling_demand + if cooling_demand is not None and cooling_demand > 0: + return CURRENT_HVAC_COOL + + if self.hvac_mode != HVAC_MODE_OFF: + return CURRENT_HVAC_IDLE + return CURRENT_HVAC_OFF + + @property + def hvac_mode(self) -> Optional[str]: + """Return HVAC operation mode.""" + return SYSTEM_MODE_2_HVAC.get(self._thrm.system_mode) + + @property + def hvac_modes(self) -> Tuple[str, ...]: + """Return the list of available HVAC operation modes.""" + return SEQ_OF_OPERATION.get(self._thrm.ctrl_seqe_of_oper, (HVAC_MODE_OFF,)) + + @property + def precision(self): + """Return the precision of the system.""" + return PRECISION_HALVES + + @property + def preset_mode(self) -> Optional[str]: + """Return current preset mode.""" + return self._preset + + @property + def preset_modes(self) -> Optional[List[str]]: + """Return supported preset modes.""" + return self._presets + + @property + def supported_features(self): + """Return the list of supported features.""" + features = self._supported_flags + if HVAC_MODE_HEAT_COOL in self.hvac_modes: + features |= SUPPORT_TARGET_TEMPERATURE_RANGE + if self._fan is not None: + self._supported_flags |= SUPPORT_FAN_MODE + return features + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + temp = None + if self.hvac_mode == HVAC_MODE_COOL: + if self.preset_mode == PRESET_AWAY: + temp = self._thrm.unoccupied_cooling_setpoint + else: + temp = self._thrm.occupied_cooling_setpoint + elif self.hvac_mode == HVAC_MODE_HEAT: + if self.preset_mode == PRESET_AWAY: + temp = self._thrm.unoccupied_heating_setpoint + else: + temp = self._thrm.occupied_heating_setpoint + if temp is None: + return temp + return round(temp / ZCL_TEMP, 1) + + @property + def target_temperature_high(self): + """Return the upper bound temperature we try to reach.""" + if self.hvac_mode != HVAC_MODE_HEAT_COOL: + return None + if self.preset_mode == PRESET_AWAY: + temp = self._thrm.unoccupied_cooling_setpoint + else: + temp = self._thrm.occupied_cooling_setpoint + + if temp is None: + return temp + + return round(temp / ZCL_TEMP, 1) + + @property + def target_temperature_low(self): + """Return the lower bound temperature we try to reach.""" + if self.hvac_mode != HVAC_MODE_HEAT_COOL: + return None + if self.preset_mode == PRESET_AWAY: + temp = self._thrm.unoccupied_heating_setpoint + else: + temp = self._thrm.occupied_heating_setpoint + + if temp is None: + return temp + return round(temp / ZCL_TEMP, 1) + + @property + def temperature_unit(self): + """Return the unit of measurement used by the platform.""" + return TEMP_CELSIUS + + @property + def max_temp(self) -> float: + """Return the maximum temperature.""" + temps = [] + if HVAC_MODE_HEAT in self.hvac_modes: + temps.append(self._thrm.max_heat_setpoint_limit) + if HVAC_MODE_COOL in self.hvac_modes: + temps.append(self._thrm.max_cool_setpoint_limit) + + if not temps: + return self.DEFAULT_MAX_TEMP + return round(max(temps) / ZCL_TEMP, 1) + + @property + def min_temp(self) -> float: + """Return the minimum temperature.""" + temps = [] + if HVAC_MODE_HEAT in self.hvac_modes: + temps.append(self._thrm.min_heat_setpoint_limit) + if HVAC_MODE_COOL in self.hvac_modes: + temps.append(self._thrm.min_cool_setpoint_limit) + + if not temps: + return self.DEFAULT_MIN_TEMP + return round(min(temps) / ZCL_TEMP, 1) + + async def async_added_to_hass(self): + """Run when about to be added to hass.""" + await super().async_added_to_hass() + await self.async_accept_signal( + self._thrm, SIGNAL_ATTR_UPDATED, self.async_attribute_updated + ) + + async def async_attribute_updated(self, record): + """Handle attribute update from device.""" + if ( + record.attr_name in (ATTR_OCCP_COOL_SETPT, ATTR_OCCP_HEAT_SETPT) + and self.preset_mode == PRESET_AWAY + ): + # occupancy attribute is an unreportable attribute, but if we get + # an attribute update for an "occupied" setpoint, there's a chance + # occupancy has changed + occupancy = await self._thrm.get_occupancy() + if occupancy is True: + self._preset = PRESET_NONE + + self.debug("Attribute '%s' = %s update", record.attr_name, record.value) + self.async_write_ha_state() + + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set fan mode.""" + if fan_mode not in self.fan_modes: + self.warning("Unsupported '%s' fan mode", fan_mode) + return + + if fan_mode == FAN_ON: + mode = ThermostatFanMode.ON + else: + mode = ThermostatFanMode.AUTO + + await self._fan.async_set_speed(mode) + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set new target operation mode.""" + if hvac_mode not in self.hvac_modes: + self.warning( + "can't set '%s' mode. Supported modes are: %s", + hvac_mode, + self.hvac_modes, + ) + return + + if await self._thrm.async_set_operation_mode(HVAC_MODE_2_SYSTEM[hvac_mode]): + self.async_write_ha_state() + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + if preset_mode not in self.preset_modes: + self.debug("preset mode '%s' is not supported", preset_mode) + return + + if self.preset_mode not in (preset_mode, PRESET_NONE): + if not await self.async_preset_handler(self.preset_mode, enable=False): + self.debug("Couldn't turn off '%s' preset", self.preset_mode) + return + + if preset_mode != PRESET_NONE: + if not await self.async_preset_handler(preset_mode, enable=True): + self.debug("Couldn't turn on '%s' preset", preset_mode) + return + self._preset = preset_mode + self.async_write_ha_state() + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) + high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) + temp = kwargs.get(ATTR_TEMPERATURE) + hvac_mode = kwargs.get(ATTR_HVAC_MODE) + + if hvac_mode is not None: + await self.async_set_hvac_mode(hvac_mode) + + thrm = self._thrm + if self.hvac_mode == HVAC_MODE_HEAT_COOL: + success = True + if low_temp is not None: + low_temp = int(low_temp * ZCL_TEMP) + success = success and await thrm.async_set_heating_setpoint( + low_temp, self.preset_mode == PRESET_AWAY + ) + self.debug("Setting heating %s setpoint: %s", low_temp, success) + if high_temp is not None: + high_temp = int(high_temp * ZCL_TEMP) + success = success and await thrm.async_set_cooling_setpoint( + high_temp, self.preset_mode == PRESET_AWAY + ) + self.debug("Setting cooling %s setpoint: %s", low_temp, success) + elif temp is not None: + temp = int(temp * ZCL_TEMP) + if self.hvac_mode == HVAC_MODE_COOL: + success = await thrm.async_set_cooling_setpoint( + temp, self.preset_mode == PRESET_AWAY + ) + elif self.hvac_mode == HVAC_MODE_HEAT: + success = await thrm.async_set_heating_setpoint( + temp, self.preset_mode == PRESET_AWAY + ) + else: + self.debug("Not setting temperature for '%s' mode", self.hvac_mode) + return + else: + self.debug("incorrect %s setting for '%s' mode", kwargs, self.hvac_mode) + return + + if success: + self.async_write_ha_state() + + async def async_preset_handler(self, preset: str, enable: bool = False) -> bool: + """Set the preset mode via handler.""" + + handler = getattr(self, f"async_preset_handler_{preset}") + return await handler(enable) + + +@STRICT_MATCH( + channel_names={CHANNEL_THERMOSTAT, "sinope_manufacturer_specific"}, + manufacturers="Sinope Technologies", +) +class SinopeTechnologiesThermostat(Thermostat): + """Sinope Technologies Thermostat.""" + + manufacturer = 0x119C + update_time_interval = timedelta(minutes=randint(45, 75)) + + def __init__(self, unique_id, zha_device, channels, **kwargs): + """Initialize ZHA Thermostat instance.""" + super().__init__(unique_id, zha_device, channels, **kwargs) + self._presets = [PRESET_AWAY, PRESET_NONE] + self._supported_flags |= SUPPORT_PRESET_MODE + self._manufacturer_ch = self.cluster_channels["sinope_manufacturer_specific"] + + @callback + def _async_update_time(self, timestamp=None) -> None: + """Update thermostat's time display.""" + + secs_2k = ( + dt_util.now().replace(tzinfo=None) - datetime(2000, 1, 1, 0, 0, 0, 0) + ).total_seconds() + + self.debug("Updating time: %s", secs_2k) + self._manufacturer_ch.cluster.create_catching_task( + self._manufacturer_ch.cluster.write_attributes( + {"secs_since_2k": secs_2k}, manufacturer=self.manufacturer + ) + ) + + async def async_added_to_hass(self): + """Run when about to be added to Hass.""" + await super().async_added_to_hass() + async_track_time_interval( + self.hass, self._async_update_time, self.update_time_interval + ) + self._async_update_time() + + async def async_preset_handler_away(self, is_away: bool = False) -> bool: + """Set occupancy.""" + mfg_code = self._zha_device.manufacturer_code + res = await self._thrm.write_attributes( + {"set_occupancy": 0 if is_away else 1}, manufacturer=mfg_code + ) + + self.debug("set occupancy to %s. Status: %s", 0 if is_away else 1, res) + return res + + +@STRICT_MATCH( + channel_names=CHANNEL_THERMOSTAT, + aux_channels=CHANNEL_FAN, + manufacturers="Zen Within", +) +class ZenWithinThermostat(Thermostat): + """Zen Within Thermostat implementation.""" + + @property + def _rm_rs_action(self) -> Optional[str]: + """Return the current HVAC action based on running mode and running state.""" + + running_state = self._thrm.running_state + if running_state is None: + return None + if running_state & (RunningState.HEAT | RunningState.HEAT_STAGE_2): + return CURRENT_HVAC_HEAT + if running_state & (RunningState.COOL | RunningState.COOL_STAGE_2): + return CURRENT_HVAC_COOL + if running_state & ( + RunningState.FAN | RunningState.FAN_STAGE_2 | RunningState.FAN_STAGE_3 + ): + return CURRENT_HVAC_FAN + + if self.hvac_mode != HVAC_MODE_OFF: + return CURRENT_HVAC_IDLE + return CURRENT_HVAC_OFF diff --git a/homeassistant/components/zha/core/channels/closures.py b/homeassistant/components/zha/core/channels/closures.py index 826c99fbd3b..5641deffb60 100644 --- a/homeassistant/components/zha/core/channels/closures.py +++ b/homeassistant/components/zha/core/channels/closures.py @@ -7,7 +7,7 @@ from homeassistant.core import callback from .. import registries from ..const import REPORT_CONFIG_IMMEDIATE, SIGNAL_ATTR_UPDATED -from .base import ZigbeeChannel +from .base import ClientChannel, ZigbeeChannel _LOGGER = logging.getLogger(__name__) @@ -50,6 +50,11 @@ class Shade(ZigbeeChannel): """Shade channel.""" +@registries.CLIENT_CHANNELS_REGISTRY.register(closures.WindowCovering.cluster_id) +class WindowCoveringClient(ClientChannel): + """Window client channel.""" + + @registries.ZIGBEE_CHANNEL_REGISTRY.register(closures.WindowCovering.cluster_id) class WindowCovering(ZigbeeChannel): """Window channel.""" diff --git a/homeassistant/components/zha/core/channels/homeautomation.py b/homeassistant/components/zha/core/channels/homeautomation.py index b295a567b3d..2601cf47573 100644 --- a/homeassistant/components/zha/core/channels/homeautomation.py +++ b/homeassistant/components/zha/core/channels/homeautomation.py @@ -75,7 +75,6 @@ class ElectricalMeasurementChannel(ZigbeeChannel): async def async_initialize(self, from_cache): """Initialize channel.""" - await self.get_attribute_value("active_power", from_cache=from_cache) await self.fetch_config(from_cache) await super().async_initialize(from_cache) @@ -90,9 +89,11 @@ class ElectricalMeasurementChannel(ZigbeeChannel): ], from_cache=from_cache, ) - self._divisor = results.get("ac_power_divisor", results.get("power_divisor", 1)) + self._divisor = results.get( + "ac_power_divisor", results.get("power_divisor", self._divisor) + ) self._multiplier = results.get( - "ac_power_multiplier", results.get("power_multiplier", 1) + "ac_power_multiplier", results.get("power_multiplier", self._multiplier) ) @property diff --git a/homeassistant/components/zha/core/channels/hvac.py b/homeassistant/components/zha/core/channels/hvac.py index 3c58ff946b9..56345916cd3 100644 --- a/homeassistant/components/zha/core/channels/hvac.py +++ b/homeassistant/components/zha/core/channels/hvac.py @@ -1,17 +1,37 @@ -"""HVAC channels module for Zigbee Home Automation.""" +""" +HVAC channels module for Zigbee Home Automation. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/integrations/zha/ +""" +import asyncio +from collections import namedtuple import logging +from typing import Any, Dict, List, Optional, Tuple, Union from zigpy.exceptions import ZigbeeException import zigpy.zcl.clusters.hvac as hvac +from zigpy.zcl.foundation import Status from homeassistant.core import callback -from .. import registries -from ..const import REPORT_CONFIG_OP, SIGNAL_ATTR_UPDATED +from .. import registries, typing as zha_typing +from ..const import ( + REPORT_CONFIG_MAX_INT, + REPORT_CONFIG_MIN_INT, + REPORT_CONFIG_OP, + SIGNAL_ATTR_UPDATED, +) +from ..helpers import retryable_req from .base import ZigbeeChannel _LOGGER = logging.getLogger(__name__) +AttributeUpdateRecord = namedtuple("AttributeUpdateRecord", "attr_id, attr_name, value") +REPORT_CONFIG_CLIMATE = (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 25) +REPORT_CONFIG_CLIMATE_DEMAND = (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 5) +REPORT_CONFIG_CLIMATE_DISCRETE = (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 1) + @registries.ZIGBEE_CHANNEL_REGISTRY.register(hvac.Dehumidification.cluster_id) class Dehumidification(ZigbeeChannel): @@ -26,6 +46,18 @@ class FanChannel(ZigbeeChannel): REPORT_CONFIG = ({"attr": "fan_mode", "config": REPORT_CONFIG_OP},) + def __init__( + self, cluster: zha_typing.ZigpyClusterType, ch_pool: zha_typing.ChannelPoolType + ): + """Init Thermostat channel instance.""" + super().__init__(cluster, ch_pool) + self._fan_mode = None + + @property + def fan_mode(self) -> Optional[int]: + """Return current fan mode.""" + return self._fan_mode + async def async_set_speed(self, value) -> None: """Set the speed of the fan.""" @@ -35,41 +67,390 @@ class FanChannel(ZigbeeChannel): self.error("Could not set speed: %s", ex) return - async def async_update(self): + async def async_update(self) -> None: """Retrieve latest state.""" result = await self.get_attribute_value("fan_mode", from_cache=True) if result is not None: + self._fan_mode = result self.async_send_signal( f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", 0, "fan_mode", result ) @callback - def attribute_updated(self, attrid, value): + def attribute_updated(self, attrid: int, value: Any) -> None: """Handle attribute update from fan cluster.""" attr_name = self.cluster.attributes.get(attrid, [attrid])[0] self.debug( "Attribute report '%s'[%s] = %s", self.cluster.name, attr_name, value ) if attrid == self._value_attribute: + self._fan_mode = value self.async_send_signal( f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", attrid, attr_name, value ) - async def async_initialize(self, from_cache): - """Initialize channel.""" - await self.get_attribute_value(self._value_attribute, from_cache=from_cache) - await super().async_initialize(from_cache) - @registries.ZIGBEE_CHANNEL_REGISTRY.register(hvac.Pump.cluster_id) class Pump(ZigbeeChannel): """Pump channel.""" +@registries.CLIMATE_CLUSTERS.register(hvac.Thermostat.cluster_id) @registries.ZIGBEE_CHANNEL_REGISTRY.register(hvac.Thermostat.cluster_id) -class Thermostat(ZigbeeChannel): +class ThermostatChannel(ZigbeeChannel): """Thermostat channel.""" + def __init__( + self, cluster: zha_typing.ZigpyClusterType, ch_pool: zha_typing.ChannelPoolType + ) -> None: + """Init Thermostat channel instance.""" + super().__init__(cluster, ch_pool) + self._init_attrs = { + "abs_min_heat_setpoint_limit": True, + "abs_max_heat_setpoint_limit": True, + "abs_min_cool_setpoint_limit": True, + "abs_max_cool_setpoint_limit": True, + "ctrl_seqe_of_oper": False, + "local_temp": False, + "max_cool_setpoint_limit": True, + "max_heat_setpoint_limit": True, + "min_cool_setpoint_limit": True, + "min_heat_setpoint_limit": True, + "occupancy": False, + "occupied_cooling_setpoint": False, + "occupied_heating_setpoint": False, + "pi_cooling_demand": False, + "pi_heating_demand": False, + "running_mode": False, + "running_state": False, + "system_mode": False, + "unoccupied_heating_setpoint": False, + "unoccupied_cooling_setpoint": False, + } + self._abs_max_cool_setpoint_limit = 3200 # 32C + self._abs_min_cool_setpoint_limit = 1600 # 16C + self._ctrl_seqe_of_oper = 0xFF + self._abs_max_heat_setpoint_limit = 3000 # 30C + self._abs_min_heat_setpoint_limit = 700 # 7C + self._running_mode = None + self._max_cool_setpoint_limit = None + self._max_heat_setpoint_limit = None + self._min_cool_setpoint_limit = None + self._min_heat_setpoint_limit = None + self._local_temp = None + self._occupancy = None + self._occupied_cooling_setpoint = None + self._occupied_heating_setpoint = None + self._pi_cooling_demand = None + self._pi_heating_demand = None + self._running_state = None + self._system_mode = None + self._unoccupied_cooling_setpoint = None + self._unoccupied_heating_setpoint = None + self._report_config = [ + {"attr": "local_temp", "config": REPORT_CONFIG_CLIMATE}, + {"attr": "occupied_cooling_setpoint", "config": REPORT_CONFIG_CLIMATE}, + {"attr": "occupied_heating_setpoint", "config": REPORT_CONFIG_CLIMATE}, + {"attr": "unoccupied_cooling_setpoint", "config": REPORT_CONFIG_CLIMATE}, + {"attr": "unoccupied_heating_setpoint", "config": REPORT_CONFIG_CLIMATE}, + {"attr": "running_mode", "config": REPORT_CONFIG_CLIMATE}, + {"attr": "running_state", "config": REPORT_CONFIG_CLIMATE_DEMAND}, + {"attr": "system_mode", "config": REPORT_CONFIG_CLIMATE}, + {"attr": "occupancy", "config": REPORT_CONFIG_CLIMATE_DISCRETE}, + {"attr": "pi_cooling_demand", "config": REPORT_CONFIG_CLIMATE_DEMAND}, + {"attr": "pi_heating_demand", "config": REPORT_CONFIG_CLIMATE_DEMAND}, + ] + + @property + def abs_max_cool_setpoint_limit(self) -> int: + """Absolute maximum cooling setpoint.""" + return self._abs_max_cool_setpoint_limit + + @property + def abs_min_cool_setpoint_limit(self) -> int: + """Absolute minimum cooling setpoint.""" + return self._abs_min_cool_setpoint_limit + + @property + def abs_max_heat_setpoint_limit(self) -> int: + """Absolute maximum heating setpoint.""" + return self._abs_max_heat_setpoint_limit + + @property + def abs_min_heat_setpoint_limit(self) -> int: + """Absolute minimum heating setpoint.""" + return self._abs_min_heat_setpoint_limit + + @property + def ctrl_seqe_of_oper(self) -> int: + """Control Sequence of operations attribute.""" + return self._ctrl_seqe_of_oper + + @property + def max_cool_setpoint_limit(self) -> int: + """Maximum cooling setpoint.""" + if self._max_cool_setpoint_limit is None: + return self.abs_max_cool_setpoint_limit + return self._max_cool_setpoint_limit + + @property + def min_cool_setpoint_limit(self) -> int: + """Minimum cooling setpoint.""" + if self._min_cool_setpoint_limit is None: + return self.abs_min_cool_setpoint_limit + return self._min_cool_setpoint_limit + + @property + def max_heat_setpoint_limit(self) -> int: + """Maximum heating setpoint.""" + if self._max_heat_setpoint_limit is None: + return self.abs_max_heat_setpoint_limit + return self._max_heat_setpoint_limit + + @property + def min_heat_setpoint_limit(self) -> int: + """Minimum heating setpoint.""" + if self._min_heat_setpoint_limit is None: + return self.abs_min_heat_setpoint_limit + return self._min_heat_setpoint_limit + + @property + def local_temp(self) -> Optional[int]: + """Thermostat temperature.""" + return self._local_temp + + @property + def occupancy(self) -> Optional[int]: + """Is occupancy detected.""" + return self._occupancy + + @property + def occupied_cooling_setpoint(self) -> Optional[int]: + """Temperature when room is occupied.""" + return self._occupied_cooling_setpoint + + @property + def occupied_heating_setpoint(self) -> Optional[int]: + """Temperature when room is occupied.""" + return self._occupied_heating_setpoint + + @property + def pi_cooling_demand(self) -> int: + """Cooling demand.""" + return self._pi_cooling_demand + + @property + def pi_heating_demand(self) -> int: + """Heating demand.""" + return self._pi_heating_demand + + @property + def running_mode(self) -> Optional[int]: + """Thermostat running mode.""" + return self._running_mode + + @property + def running_state(self) -> Optional[int]: + """Thermostat running state, state of heat, cool, fan relays.""" + return self._running_state + + @property + def system_mode(self) -> Optional[int]: + """System mode.""" + return self._system_mode + + @property + def unoccupied_cooling_setpoint(self) -> Optional[int]: + """Temperature when room is not occupied.""" + return self._unoccupied_cooling_setpoint + + @property + def unoccupied_heating_setpoint(self) -> Optional[int]: + """Temperature when room is not occupied.""" + return self._unoccupied_heating_setpoint + + @callback + def attribute_updated(self, attrid, value): + """Handle attribute update cluster.""" + attr_name = self.cluster.attributes.get(attrid, [attrid])[0] + self.debug( + "Attribute report '%s'[%s] = %s", self.cluster.name, attr_name, value + ) + setattr(self, f"_{attr_name}", value) + self.async_send_signal( + f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", + AttributeUpdateRecord(attrid, attr_name, value), + ) + + async def _chunk_attr_read(self, attrs, cached=False): + chunk, attrs = attrs[:4], attrs[4:] + while chunk: + res, fail = await self.cluster.read_attributes(chunk, allow_cache=cached) + self.debug("read attributes: Success: %s. Failed: %s", res, fail) + for attr in chunk: + self._init_attrs.pop(attr, None) + if attr in fail: + continue + if isinstance(attr, str): + setattr(self, f"_{attr}", res[attr]) + self.async_send_signal( + f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", + AttributeUpdateRecord(None, attr, res[attr]), + ) + + chunk, attrs = attrs[:4], attrs[4:] + + async def configure_reporting(self): + """Configure attribute reporting for a cluster. + + This also swallows DeliveryError exceptions that are thrown when + devices are unreachable. + """ + kwargs = {} + if self.cluster.cluster_id >= 0xFC00 and self._ch_pool.manufacturer_code: + kwargs["manufacturer"] = self._ch_pool.manufacturer_code + + chunk, rest = self._report_config[:4], self._report_config[4:] + while chunk: + attrs = {record["attr"]: record["config"] for record in chunk} + try: + res = await self.cluster.configure_reporting_multiple(attrs, **kwargs) + self._configure_reporting_status(attrs, res[0]) + except (ZigbeeException, asyncio.TimeoutError) as ex: + self.debug( + "failed to set reporting on '%s' cluster for: %s", + self.cluster.ep_attribute, + str(ex), + ) + break + chunk, rest = rest[:4], rest[4:] + + def _configure_reporting_status( + self, attrs: Dict[Union[int, str], Tuple], res: Union[List, Tuple] + ) -> None: + """Parse configure reporting result.""" + if not isinstance(res, list): + # assume default response + self.debug( + "attr reporting for '%s' on '%s': %s", attrs, self.name, res, + ) + return + if res[0].status == Status.SUCCESS and len(res) == 1: + self.debug( + "Successfully configured reporting for '%s' on '%s' cluster: %s", + attrs, + self.name, + res, + ) + return + + failed = [ + self.cluster.attributes.get(r.attrid, [r.attrid])[0] + for r in res + if r.status != Status.SUCCESS + ] + attrs = {self.cluster.attributes.get(r, [r])[0] for r in attrs} + self.debug( + "Successfully configured reporting for '%s' on '%s' cluster", + attrs - set(failed), + self.name, + ) + self.debug( + "Failed to configure reporting for '%s' on '%s' cluster: %s", + failed, + self.name, + res, + ) + + @retryable_req(delays=(1, 1, 3)) + async def async_initialize(self, from_cache): + """Initialize channel.""" + + cached = [a for a, cached in self._init_attrs.items() if cached] + uncached = [a for a, cached in self._init_attrs.items() if not cached] + + await self._chunk_attr_read(cached, cached=True) + await self._chunk_attr_read(uncached, cached=False) + await super().async_initialize(from_cache) + + async def async_set_operation_mode(self, mode) -> bool: + """Set Operation mode.""" + if not await self.write_attributes({"system_mode": mode}): + self.debug("couldn't set '%s' operation mode", mode) + return False + + self._system_mode = mode + self.debug("set system to %s", mode) + return True + + async def async_set_heating_setpoint( + self, temperature: int, is_away: bool = False + ) -> bool: + """Set heating setpoint.""" + if is_away: + data = {"unoccupied_heating_setpoint": temperature} + else: + data = {"occupied_heating_setpoint": temperature} + if not await self.write_attributes(data): + self.debug("couldn't set heating setpoint") + return False + + if is_away: + self._unoccupied_heating_setpoint = temperature + else: + self._occupied_heating_setpoint = temperature + self.debug("set heating setpoint to %s", temperature) + return True + + async def async_set_cooling_setpoint( + self, temperature: int, is_away: bool = False + ) -> bool: + """Set cooling setpoint.""" + if is_away: + data = {"unoccupied_cooling_setpoint": temperature} + else: + data = {"occupied_cooling_setpoint": temperature} + if not await self.write_attributes(data): + self.debug("couldn't set cooling setpoint") + return False + if is_away: + self._unoccupied_cooling_setpoint = temperature + else: + self._occupied_cooling_setpoint = temperature + self.debug("set cooling setpoint to %s", temperature) + return True + + async def get_occupancy(self) -> Optional[bool]: + """Get unreportable occupancy attribute.""" + try: + res, fail = await self.cluster.read_attributes(["occupancy"]) + self.debug("read 'occupancy' attr, success: %s, fail: %s", res, fail) + if "occupancy" not in res: + return None + self._occupancy = res["occupancy"] + return bool(self.occupancy) + except ZigbeeException as ex: + self.debug("Couldn't read 'occupancy' attribute: %s", ex) + + async def write_attributes(self, data, **kwargs): + """Write attributes helper.""" + try: + res = await self.cluster.write_attributes(data, **kwargs) + except ZigbeeException as exc: + self.debug("couldn't write %s: %s", data, exc) + return False + + self.debug("wrote %s attrs, Status: %s", data, res) + return self.check_result(res) + + @staticmethod + def check_result(res: list) -> bool: + """Normalize the result.""" + if not isinstance(res, list): + return False + + return all([record.status == Status.SUCCESS for record in res[0]]) + @registries.ZIGBEE_CHANNEL_REGISTRY.register(hvac.UserInterface.cluster_id) class UserInterface(ZigbeeChannel): diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 08252ab1c97..8a99a8a1b11 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -11,6 +11,7 @@ import zigpy_xbee.zigbee.application import zigpy_zigate.zigbee.application from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR +from homeassistant.components.climate import DOMAIN as CLIMATE from homeassistant.components.cover import DOMAIN as COVER from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER from homeassistant.components.fan import DOMAIN as FAN @@ -85,8 +86,10 @@ CHANNEL_OCCUPANCY = "occupancy" CHANNEL_ON_OFF = "on_off" CHANNEL_POWER_CONFIGURATION = "power" CHANNEL_PRESSURE = "pressure" +CHANNEL_SHADE = "shade" CHANNEL_SMARTENERGY_METERING = "smartenergy_metering" CHANNEL_TEMPERATURE = "temperature" +CHANNEL_THERMOSTAT = "thermostat" CHANNEL_ZDO = "zdo" CHANNEL_ZONE = ZONE = "ias_zone" @@ -96,7 +99,17 @@ CLUSTER_COMMANDS_SERVER = "server_commands" CLUSTER_TYPE_IN = "in" CLUSTER_TYPE_OUT = "out" -COMPONENTS = (BINARY_SENSOR, COVER, DEVICE_TRACKER, FAN, LIGHT, LOCK, SENSOR, SWITCH) +COMPONENTS = ( + BINARY_SENSOR, + CLIMATE, + COVER, + DEVICE_TRACKER, + FAN, + LIGHT, + LOCK, + SENSOR, + SWITCH, +) CONF_BAUDRATE = "baudrate" CONF_DATABASE = "database_path" diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index f72ac2161ec..25f320b0bf1 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -16,6 +16,7 @@ from homeassistant.helpers.typing import HomeAssistantType from . import const as zha_const, registries as zha_regs, typing as zha_typing from .. import ( # noqa: F401 pylint: disable=unused-import, binary_sensor, + climate, cover, device_tracker, fan, diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index bb8a202e789..7813c7133ad 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -1,8 +1,19 @@ -"""Helpers for Zigbee Home Automation.""" +""" +Helpers for Zigbee Home Automation. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/integrations/zha/ +""" + +import asyncio import collections +import functools +import itertools import logging +from random import uniform from typing import Any, Callable, Iterator, List, Optional +import zigpy.exceptions import zigpy.types from homeassistant.core import State, callback @@ -147,3 +158,50 @@ class LogMixin: def error(self, msg, *args): """Error level log.""" return self.log(logging.ERROR, msg, *args) + + +def retryable_req( + delays=(1, 5, 10, 15, 30, 60, 120, 180, 360, 600, 900, 1800), raise_=False +): + """Make a method with ZCL requests retryable. + + This adds delays keyword argument to function. + len(delays) is number of tries. + raise_ if the final attempt should raise the exception. + """ + + def decorator(func): + @functools.wraps(func) + async def wrapper(channel, *args, **kwargs): + + exceptions = (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) + try_count, errors = 1, [] + for delay in itertools.chain(delays, [None]): + try: + return await func(channel, *args, **kwargs) + except exceptions as ex: + errors.append(ex) + if delay: + delay = uniform(delay * 0.75, delay * 1.25) + channel.debug( + ( + "%s: retryable request #%d failed: %s. " + "Retrying in %ss" + ), + func.__name__, + try_count, + ex, + round(delay, 1), + ) + try_count += 1 + await asyncio.sleep(delay) + else: + channel.warning( + "%s: all attempts have failed: %s", func.__name__, errors + ) + if raise_: + raise + + return wrapper + + return decorator diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 6ddf48de5aa..c9b3435482b 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -8,6 +8,7 @@ import zigpy.profiles.zll import zigpy.zcl as zcl from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR +from homeassistant.components.climate import DOMAIN as CLIMATE from homeassistant.components.cover import DOMAIN as COVER from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER from homeassistant.components.fan import DOMAIN as FAN @@ -84,21 +85,24 @@ BINARY_SENSOR_CLUSTERS.add(SMARTTHINGS_ACCELERATION_CLUSTER) BINDABLE_CLUSTERS = SetRegistry() CHANNEL_ONLY_CLUSTERS = SetRegistry() +CLIMATE_CLUSTERS = SetRegistry() CUSTOM_CLUSTER_MAPPINGS = {} DEVICE_CLASS = { zigpy.profiles.zha.PROFILE_ID: { SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE: DEVICE_TRACKER, + zigpy.profiles.zha.DeviceType.THERMOSTAT: CLIMATE, zigpy.profiles.zha.DeviceType.COLOR_DIMMABLE_LIGHT: LIGHT, zigpy.profiles.zha.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT, zigpy.profiles.zha.DeviceType.DIMMABLE_BALLAST: LIGHT, zigpy.profiles.zha.DeviceType.DIMMABLE_LIGHT: LIGHT, zigpy.profiles.zha.DeviceType.DIMMABLE_PLUG_IN_UNIT: LIGHT, zigpy.profiles.zha.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT, - zigpy.profiles.zha.DeviceType.LEVEL_CONTROLLABLE_OUTPUT: LIGHT, + zigpy.profiles.zha.DeviceType.LEVEL_CONTROLLABLE_OUTPUT: COVER, zigpy.profiles.zha.DeviceType.ON_OFF_BALLAST: SWITCH, zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT: LIGHT, zigpy.profiles.zha.DeviceType.ON_OFF_PLUG_IN_UNIT: SWITCH, + zigpy.profiles.zha.DeviceType.SHADE: COVER, zigpy.profiles.zha.DeviceType.SMART_PLUG: SWITCH, }, zigpy.profiles.zll.PROFILE_ID: { @@ -120,6 +124,7 @@ CLIENT_CHANNELS_REGISTRY = DictRegistry() COMPONENT_CLUSTERS = { BINARY_SENSOR: BINARY_SENSOR_CLUSTERS, + CLIMATE: CLIMATE_CLUSTERS, DEVICE_TRACKER: DEVICE_TRACKER_CLUSTERS, LIGHT: LIGHT_CLUSTERS, SWITCH: SWITCH_CLUSTERS, diff --git a/homeassistant/components/zha/cover.py b/homeassistant/components/zha/cover.py index 0feaf14b3c5..235368080f0 100644 --- a/homeassistant/components/zha/cover.py +++ b/homeassistant/components/zha/cover.py @@ -1,11 +1,19 @@ """Support for ZHA covers.""" -from datetime import timedelta +import asyncio import functools import logging +from typing import List, Optional from zigpy.zcl.foundation import Status -from homeassistant.components.cover import ATTR_POSITION, DOMAIN, CoverEntity +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + ATTR_POSITION, + DEVICE_CLASS_DAMPER, + DEVICE_CLASS_SHADE, + DOMAIN, + CoverEntity, +) from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -13,17 +21,21 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .core import discovery from .core.const import ( CHANNEL_COVER, + CHANNEL_LEVEL, + CHANNEL_ON_OFF, + CHANNEL_SHADE, DATA_ZHA, DATA_ZHA_DISPATCHERS, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, + SIGNAL_SET_LEVEL, ) from .core.registries import ZHA_ENTITIES +from .core.typing import ChannelType, ZhaDeviceType from .entity import ZhaEntity _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(minutes=60) STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) @@ -158,3 +170,141 @@ class ZhaCover(ZhaEntity, CoverEntity): else: self._current_position = None self._state = None + + +@STRICT_MATCH(channel_names={CHANNEL_LEVEL, CHANNEL_ON_OFF, CHANNEL_SHADE}) +class Shade(ZhaEntity, CoverEntity): + """ZHA Shade.""" + + def __init__( + self, + unique_id: str, + zha_device: ZhaDeviceType, + channels: List[ChannelType], + **kwargs, + ): + """Initialize the ZHA light.""" + super().__init__(unique_id, zha_device, channels, **kwargs) + self._on_off_channel = self.cluster_channels[CHANNEL_ON_OFF] + self._level_channel = self.cluster_channels[CHANNEL_LEVEL] + self._position = None + self._is_open = None + + @property + def current_cover_position(self): + """Return current position of cover. + + None is unknown, 0 is closed, 100 is fully open. + """ + return self._position + + @property + def device_class(self) -> Optional[str]: + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_SHADE + + @property + def is_closed(self) -> Optional[bool]: + """Return True if shade is closed.""" + if self._is_open is None: + return None + return not self._is_open + + async def async_added_to_hass(self): + """Run when about to be added to hass.""" + await super().async_added_to_hass() + await self.async_accept_signal( + self._on_off_channel, SIGNAL_ATTR_UPDATED, self.async_set_open_closed + ) + await self.async_accept_signal( + self._level_channel, SIGNAL_SET_LEVEL, self.async_set_level + ) + + @callback + def async_restore_last_state(self, last_state): + """Restore previous state.""" + self._is_open = last_state.state == STATE_OPEN + if ATTR_CURRENT_POSITION in last_state.attributes: + self._position = last_state.attributes[ATTR_CURRENT_POSITION] + + @callback + def async_set_open_closed(self, attr_id: int, attr_name: str, value: bool) -> None: + """Set open/closed state.""" + self._is_open = bool(value) + self.async_write_ha_state() + + @callback + def async_set_level(self, value: int) -> None: + """Set the reported position.""" + value = max(0, min(255, value)) + self._position = int(value * 100 / 255) + self.async_write_ha_state() + + async def async_open_cover(self, **kwargs): + """Open the window cover.""" + res = await self._on_off_channel.on() + if not isinstance(res, list) or res[1] != Status.SUCCESS: + self.debug("couldn't open cover: %s", res) + return + + self._is_open = True + self.async_write_ha_state() + + async def async_close_cover(self, **kwargs): + """Close the window cover.""" + res = await self._on_off_channel.off() + if not isinstance(res, list) or res[1] != Status.SUCCESS: + self.debug("couldn't open cover: %s", res) + return + + self._is_open = False + self.async_write_ha_state() + + async def async_set_cover_position(self, **kwargs): + """Move the roller shutter to a specific position.""" + new_pos = kwargs[ATTR_POSITION] + res = await self._level_channel.move_to_level_with_on_off( + new_pos * 255 / 100, 1 + ) + + if not isinstance(res, list) or res[1] != Status.SUCCESS: + self.debug("couldn't set cover's position: %s", res) + return + + self._position = new_pos + self.async_write_ha_state() + + async def async_stop_cover(self, **kwargs) -> None: + """Stop the cover.""" + res = await self._level_channel.stop() + if not isinstance(res, list) or res[1] != Status.SUCCESS: + self.debug("couldn't stop cover: %s", res) + return + + +@STRICT_MATCH( + channel_names={CHANNEL_LEVEL, CHANNEL_ON_OFF}, manufacturers="Keen Home Inc" +) +class KeenVent(Shade): + """Keen vent cover.""" + + @property + def device_class(self) -> Optional[str]: + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_DAMPER + + async def async_open_cover(self, **kwargs): + """Open the cover.""" + position = self._position or 100 + tasks = [ + self._level_channel.move_to_level_with_on_off(position * 255 / 100, 1), + self._on_off_channel.on(), + ] + results = await asyncio.gather(*tasks, return_exceptions=True) + if any([isinstance(result, Exception) for result in results]): + self.debug("couldn't open cover") + return + + self._is_open = True + self._position = position + self.async_write_ha_state() diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index fe213f7920b..8629fc50075 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -28,7 +28,7 @@ from .core.const import ( SIGNAL_REMOVE_GROUP, ) from .core.helpers import LogMixin -from .core.typing import CALLABLE_T, ChannelsType, ChannelType, ZhaDeviceType +from .core.typing import CALLABLE_T, ChannelType, ZhaDeviceType _LOGGER = logging.getLogger(__name__) @@ -150,7 +150,7 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity): self, unique_id: str, zha_device: ZhaDeviceType, - channels: ChannelsType, + channels: List[ChannelType], **kwargs, ): """Init ZHA entity.""" diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index ef96d6efef1..63a87932ba9 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,7 +7,7 @@ "bellows==0.16.2", "pyserial==3.4", "zha-quirks==0.0.39", - "zigpy-cc==0.4.2", + "zigpy-cc==0.4.4", "zigpy-deconz==0.9.2", "zigpy==0.20.4", "zigpy-xbee==0.12.1", diff --git a/homeassistant/components/zha/translations/bg.json b/homeassistant/components/zha/translations/bg.json index a84ef9b4abd..c0bd15fd462 100644 --- a/homeassistant/components/zha/translations/bg.json +++ b/homeassistant/components/zha/translations/bg.json @@ -8,9 +8,6 @@ }, "step": { "user": { - "data": { - "radio_type": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e" - }, "title": "ZHA" } } diff --git a/homeassistant/components/zha/translations/ca.json b/homeassistant/components/zha/translations/ca.json index d4e8b7997b0..8b2e9a9aed0 100644 --- a/homeassistant/components/zha/translations/ca.json +++ b/homeassistant/components/zha/translations/ca.json @@ -25,9 +25,7 @@ }, "user": { "data": { - "path": "Ruta del port s\u00e8rie al dispositiu", - "radio_type": "Tipus de r\u00e0dio", - "usb_path": "[%key::common::config_flow::data::usb_path%]" + "path": "Ruta del port s\u00e8rie al dispositiu" }, "description": "Selecciona el port s\u00e8rie per a la r\u00e0dio Zigbee", "title": "ZHA" diff --git a/homeassistant/components/zha/translations/da.json b/homeassistant/components/zha/translations/da.json index 2ebf02c5455..6a6e34c2581 100644 --- a/homeassistant/components/zha/translations/da.json +++ b/homeassistant/components/zha/translations/da.json @@ -25,9 +25,7 @@ }, "user": { "data": { - "path": "Stien til seriel enhed", - "radio_type": "Radio-type", - "usb_path": "USB-enheds sti" + "path": "Stien til seriel enhed" }, "description": "V\u00e6lg seriel port til Zigbee-radio", "title": "ZHA" diff --git a/homeassistant/components/zha/translations/de.json b/homeassistant/components/zha/translations/de.json index d25894e338d..592450fcfbc 100644 --- a/homeassistant/components/zha/translations/de.json +++ b/homeassistant/components/zha/translations/de.json @@ -25,9 +25,7 @@ }, "user": { "data": { - "path": "Serieller Ger\u00e4tepfad", - "radio_type": "Radio-Type", - "usb_path": "USB-Ger\u00e4te-Pfad" + "path": "Serieller Ger\u00e4tepfad" }, "description": "W\u00e4hlen Sie die serielle Schnittstelle f\u00fcr den ZigBee-Funk", "title": "ZHA" diff --git a/homeassistant/components/zha/translations/en.json b/homeassistant/components/zha/translations/en.json index 760492552a7..6a1eb4bac8e 100644 --- a/homeassistant/components/zha/translations/en.json +++ b/homeassistant/components/zha/translations/en.json @@ -25,9 +25,7 @@ }, "user": { "data": { - "path": "Serial Device Path", - "radio_type": "Radio Type", - "usb_path": "USB Device Path" + "path": "Serial Device Path" }, "description": "Select serial port for Zigbee radio", "title": "ZHA" diff --git a/homeassistant/components/zha/translations/es-419.json b/homeassistant/components/zha/translations/es-419.json index 36dadce7087..f2aaf80c78a 100644 --- a/homeassistant/components/zha/translations/es-419.json +++ b/homeassistant/components/zha/translations/es-419.json @@ -8,9 +8,6 @@ }, "step": { "user": { - "data": { - "radio_type": "Tipo de radio" - }, "title": "ZHA" } } diff --git a/homeassistant/components/zha/translations/es.json b/homeassistant/components/zha/translations/es.json index 97bbc5ce033..fd06722a445 100644 --- a/homeassistant/components/zha/translations/es.json +++ b/homeassistant/components/zha/translations/es.json @@ -25,9 +25,7 @@ }, "user": { "data": { - "path": "Ruta del Dispositivo Serie", - "radio_type": "Tipo de radio", - "usb_path": "Ruta del Dispositivo USB" + "path": "Ruta del Dispositivo Serie" }, "description": "Selecciona puerto serie para radio Zigbee", "title": "ZHA" diff --git a/homeassistant/components/zha/translations/fi.json b/homeassistant/components/zha/translations/fi.json index 5e008b35ddf..e22705142d4 100644 --- a/homeassistant/components/zha/translations/fi.json +++ b/homeassistant/components/zha/translations/fi.json @@ -21,9 +21,6 @@ "title": "Asetukset" }, "user": { - "data": { - "radio_type": "Radiotyyppi" - }, "title": "ZHA" } } diff --git a/homeassistant/components/zha/translations/fr.json b/homeassistant/components/zha/translations/fr.json index 035ca744dda..11e58f7be31 100644 --- a/homeassistant/components/zha/translations/fr.json +++ b/homeassistant/components/zha/translations/fr.json @@ -16,14 +16,18 @@ }, "port_config": { "data": { - "baudrate": "vitesse du port" + "baudrate": "vitesse du port", + "flow_control": "contr\u00f4le du flux de donn\u00e9es", + "path": "Chemin du p\u00e9riph\u00e9rique s\u00e9rie" }, + "description": "Saisir les param\u00e8tres sp\u00e9cifiques au port", "title": "R\u00e9glages" }, "user": { "data": { - "radio_type": "Type de radio" + "path": "Chemin du p\u00e9riph\u00e9rique s\u00e9rie" }, + "description": "S\u00e9lectionnez le port s\u00e9rie de la radio Zigbee", "title": "ZHA" } } @@ -65,6 +69,7 @@ "device_shaken": "Appareil secou\u00e9", "device_slid": "Appareil gliss\u00e9 \"{subtype}\"", "device_tilted": "Dispositif inclin\u00e9", + "remote_button_alt_triple_press": "\"{subtype}\" bouton triple-cliqu\u00e9 (mode alternatif)", "remote_button_double_press": "Double clic sur le bouton \" {subtype} \"", "remote_button_long_press": "Bouton \"{subtype}\" appuy\u00e9 continuellement", "remote_button_long_release": "Bouton \" {subtype} \" rel\u00e2ch\u00e9 apr\u00e8s un appui long", diff --git a/homeassistant/components/zha/translations/hu.json b/homeassistant/components/zha/translations/hu.json index 4f9e9796925..935663ed9e4 100644 --- a/homeassistant/components/zha/translations/hu.json +++ b/homeassistant/components/zha/translations/hu.json @@ -14,9 +14,6 @@ "title": "Be\u00e1ll\u00edt\u00e1sok" }, "user": { - "data": { - "radio_type": "R\u00e1di\u00f3 t\u00edpusa" - }, "title": "ZHA" } } diff --git a/homeassistant/components/zha/translations/it.json b/homeassistant/components/zha/translations/it.json index be5da92433c..7b2531c1290 100644 --- a/homeassistant/components/zha/translations/it.json +++ b/homeassistant/components/zha/translations/it.json @@ -25,9 +25,7 @@ }, "user": { "data": { - "path": "Percorso del dispositivo seriale", - "radio_type": "Tipo di Radio", - "usb_path": "Percorso del dispositivo USB" + "path": "Percorso del dispositivo seriale" }, "description": "Selezionare la porta seriale per la radio Zigbee", "title": "ZHA" diff --git a/homeassistant/components/zha/translations/ko.json b/homeassistant/components/zha/translations/ko.json index f1190f590d4..93582cc9202 100644 --- a/homeassistant/components/zha/translations/ko.json +++ b/homeassistant/components/zha/translations/ko.json @@ -25,9 +25,7 @@ }, "user": { "data": { - "path": "\uc2dc\ub9ac\uc5bc \uc7a5\uce58 \uacbd\ub85c", - "radio_type": "\ubb34\uc120 \uc720\ud615", - "usb_path": "USB \uc7a5\uce58 \uacbd\ub85c" + "path": "\uc2dc\ub9ac\uc5bc \uc7a5\uce58 \uacbd\ub85c" }, "description": "Zigbee \ubb34\uc120 \uc7a5\uce58\uc758 \uc2dc\ub9ac\uc5bc \ud3ec\ud2b8\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694", "title": "ZHA" diff --git a/homeassistant/components/zha/translations/lb.json b/homeassistant/components/zha/translations/lb.json index 5f72f64b099..62498a7dfe2 100644 --- a/homeassistant/components/zha/translations/lb.json +++ b/homeassistant/components/zha/translations/lb.json @@ -25,9 +25,7 @@ }, "user": { "data": { - "path": "Pad zum seriellen Apparat", - "radio_type": "Typ vun Radio", - "usb_path": "Pad zum USB Apparat" + "path": "Pad zum seriellen Apparat" }, "description": "Serielle Port fir Zigbee Radio auswielen", "title": "ZHA" diff --git a/homeassistant/components/zha/translations/nl.json b/homeassistant/components/zha/translations/nl.json index e06c186cbef..ca8d18dae12 100644 --- a/homeassistant/components/zha/translations/nl.json +++ b/homeassistant/components/zha/translations/nl.json @@ -7,10 +7,26 @@ "cannot_connect": "Kan geen verbinding maken met ZHA apparaat." }, "step": { + "pick_radio": { + "data": { + "radio_type": "Radio type" + }, + "description": "Kies een type Zigbee-radio", + "title": "Radio type" + }, + "port_config": { + "data": { + "baudrate": "poort snelheid", + "path": "Serieel apparaatpad" + }, + "description": "Voer poortspecifieke instellingen in", + "title": "Instellingen" + }, "user": { "data": { - "radio_type": "Radio Type" + "path": "Serieel apparaatpad" }, + "description": "Selecteer seri\u00eble poort voor Zigbee-radio", "title": "ZHA" } } diff --git a/homeassistant/components/zha/translations/no.json b/homeassistant/components/zha/translations/no.json index fe5714d5359..9699a7219ad 100644 --- a/homeassistant/components/zha/translations/no.json +++ b/homeassistant/components/zha/translations/no.json @@ -25,9 +25,7 @@ }, "user": { "data": { - "path": "Seriell enhetsbane", - "radio_type": "Radio type", - "usb_path": "USB enhetsbane" + "path": "Seriell enhetsbane" }, "description": "Velg seriell port for Zigbee radio", "title": "" diff --git a/homeassistant/components/zha/translations/pl.json b/homeassistant/components/zha/translations/pl.json index 438cd44d5b5..b9780d8427f 100644 --- a/homeassistant/components/zha/translations/pl.json +++ b/homeassistant/components/zha/translations/pl.json @@ -25,9 +25,7 @@ }, "user": { "data": { - "path": "\u015acie\u017cka urz\u0105dzenia szeregowego", - "radio_type": "Typ radia", - "usb_path": "[%key_id:common::config_flow::data::usb_path%]" + "path": "\u015acie\u017cka urz\u0105dzenia szeregowego" }, "description": "Wyb\u00f3r portu szeregowego dla radia Zigbee", "title": "ZHA" diff --git a/homeassistant/components/zha/translations/pt-BR.json b/homeassistant/components/zha/translations/pt-BR.json index 8a9b2b21677..4b943032d28 100644 --- a/homeassistant/components/zha/translations/pt-BR.json +++ b/homeassistant/components/zha/translations/pt-BR.json @@ -7,10 +7,20 @@ "cannot_connect": "N\u00e3o \u00e9 poss\u00edvel conectar-se ao dispositivo ZHA." }, "step": { - "user": { + "pick_radio": { "data": { "radio_type": "Tipo de r\u00e1dio" }, + "description": "Escolha o tipo de seu r\u00e1dio Zigbee", + "title": "Tipo de r\u00e1dio" + }, + "port_config": { + "data": { + "baudrate": "velocidade da porta" + }, + "title": "Configura\u00e7\u00f5es" + }, + "user": { "title": "ZHA" } } diff --git a/homeassistant/components/zha/translations/pt.json b/homeassistant/components/zha/translations/pt.json index 1b9073b21f0..233791e63f1 100644 --- a/homeassistant/components/zha/translations/pt.json +++ b/homeassistant/components/zha/translations/pt.json @@ -8,9 +8,6 @@ }, "step": { "user": { - "data": { - "radio_type": "Tipo de r\u00e1dio" - }, "title": "ZHA" } } diff --git a/homeassistant/components/zha/translations/ru.json b/homeassistant/components/zha/translations/ru.json index 557b17ebe92..d535e9dd588 100644 --- a/homeassistant/components/zha/translations/ru.json +++ b/homeassistant/components/zha/translations/ru.json @@ -25,9 +25,7 @@ }, "user": { "data": { - "path": "\u041f\u0443\u0442\u044c \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443", - "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" + "path": "\u041f\u0443\u0442\u044c \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" }, "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u043e\u0440\u0430 \u0441\u0435\u0442\u0438 Zigbee", "title": "Zigbee Home Automation" diff --git a/homeassistant/components/zha/translations/sl.json b/homeassistant/components/zha/translations/sl.json index bfaf0757ea4..54d87c087bb 100644 --- a/homeassistant/components/zha/translations/sl.json +++ b/homeassistant/components/zha/translations/sl.json @@ -8,10 +8,6 @@ }, "step": { "user": { - "data": { - "radio_type": "Vrsta radia", - "usb_path": "USB Pot" - }, "title": "ZHA" } } diff --git a/homeassistant/components/zha/translations/sv.json b/homeassistant/components/zha/translations/sv.json index 00358fb1e23..7bbfe8dcfb1 100644 --- a/homeassistant/components/zha/translations/sv.json +++ b/homeassistant/components/zha/translations/sv.json @@ -25,8 +25,7 @@ }, "user": { "data": { - "path": "Seriell enhetsv\u00e4g", - "radio_type": "Typ av radio" + "path": "Seriell enhetsv\u00e4g" }, "title": "ZHA" } diff --git a/homeassistant/components/zha/translations/zh-Hans.json b/homeassistant/components/zha/translations/zh-Hans.json index d5c25573b7b..7a841edbb3b 100644 --- a/homeassistant/components/zha/translations/zh-Hans.json +++ b/homeassistant/components/zha/translations/zh-Hans.json @@ -8,9 +8,6 @@ }, "step": { "user": { - "data": { - "radio_type": "\u65e0\u7ebf\u7535\u7c7b\u578b" - }, "title": "ZHA" } } diff --git a/homeassistant/components/zha/translations/zh-Hant.json b/homeassistant/components/zha/translations/zh-Hant.json index ecfd8ff9e51..cbe4d8fbedd 100644 --- a/homeassistant/components/zha/translations/zh-Hant.json +++ b/homeassistant/components/zha/translations/zh-Hant.json @@ -25,9 +25,7 @@ }, "user": { "data": { - "path": "\u5e8f\u5217\u8a2d\u5099\u8def\u5f91", - "radio_type": "\u7121\u7dda\u96fb\u985e\u578b", - "usb_path": "USB \u8a2d\u5099\u8def\u5f91" + "path": "\u5e8f\u5217\u8a2d\u5099\u8def\u5f91" }, "description": "\u9078\u64c7 Zigbee \u7121\u7dda\u96fb\u5e8f\u5217\u57e0", "title": "ZHA" diff --git a/homeassistant/components/zigbee/manifest.json b/homeassistant/components/zigbee/manifest.json deleted file mode 100644 index 6940aaef7dc..00000000000 --- a/homeassistant/components/zigbee/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "domain": "zigbee", - "name": "Zigbee", - "documentation": "https://www.home-assistant.io/integrations/zigbee", - "requirements": ["xbee-helper==0.0.7"], - "codeowners": [] -} diff --git a/homeassistant/components/zwave/translations/ca.json b/homeassistant/components/zwave/translations/ca.json index 27f543a44c7..dedf7f60e4a 100644 --- a/homeassistant/components/zwave/translations/ca.json +++ b/homeassistant/components/zwave/translations/ca.json @@ -11,7 +11,7 @@ "user": { "data": { "network_key": "Clau de xarxa (deixa-ho en blanc per generar-la autom\u00e0ticament)", - "usb_path": "Ruta del port USB" + "usb_path": "Ruta del port USB del dispositiu" }, "description": "Consulta https://www.home-assistant.io/docs/z-wave/installation/ per obtenir informaci\u00f3 sobre les variables de configuraci\u00f3", "title": "Configuraci\u00f3 de Z-Wave" diff --git a/homeassistant/components/zwave/translations/hu.json b/homeassistant/components/zwave/translations/hu.json index 72026949c78..b443a30bada 100644 --- a/homeassistant/components/zwave/translations/hu.json +++ b/homeassistant/components/zwave/translations/hu.json @@ -11,7 +11,7 @@ "user": { "data": { "network_key": "H\u00e1l\u00f3zati kulcs (hagyja \u00fcresen az automatikus gener\u00e1l\u00e1shoz)", - "usb_path": "USB el\u00e9r\u00e9si \u00fat" + "usb_path": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat" }, "description": "A konfigur\u00e1ci\u00f3s v\u00e1ltoz\u00f3kr\u00f3l az inform\u00e1ci\u00f3kat l\u00e1sd a https://www.home-assistant.io/docs/z-wave/installation/ oldalon.", "title": "Z-Wave be\u00e1ll\u00edt\u00e1sa" diff --git a/homeassistant/components/zwave/translations/it.json b/homeassistant/components/zwave/translations/it.json index 8b8ffb732fc..2b1b248e92c 100644 --- a/homeassistant/components/zwave/translations/it.json +++ b/homeassistant/components/zwave/translations/it.json @@ -11,7 +11,7 @@ "user": { "data": { "network_key": "Chiave di rete (lascia vuoto per generare automaticamente)", - "usb_path": "Percorso USB" + "usb_path": "Percorso del dispositivo USB" }, "description": "Vai su https://www.home-assistant.io/docs/z-wave/installation/ per le informazioni sulle variabili di configurazione", "title": "Configura Z-Wave" diff --git a/homeassistant/components/zwave/translations/pl.json b/homeassistant/components/zwave/translations/pl.json index dd7ae5aead9..f871ab928b6 100644 --- a/homeassistant/components/zwave/translations/pl.json +++ b/homeassistant/components/zwave/translations/pl.json @@ -5,13 +5,13 @@ "one_instance_only": "Komponent obs\u0142uguje tylko jedn\u0105 instancj\u0119 Z-Wave" }, "error": { - "option_error": "Walidacja Z-Wave nie powiod\u0142a si\u0119. Czy \u015bcie\u017cka do kontrolera Z-Wave USB jest prawid\u0142owa?" + "option_error": "Walidacja Z-Wave si\u0119 nie powiod\u0142a. Czy \u015bcie\u017cka do kontrolera Z-Wave USB jest prawid\u0142owa?" }, "step": { "user": { "data": { "network_key": "Klucz sieciowy (pozostaw pusty, by generowa\u0107 automatycznie)", - "usb_path": "\u015acie\u017cka do kontrolera Z-Wave USB" + "usb_path": "\u015acie\u017cka urz\u0105dzenia USB" }, "description": "Przejd\u017a na https://www.home-assistant.io/docs/z-wave/installation/, aby uzyska\u0107 informacje na temat zmiennych konfiguracyjnych", "title": "Konfiguracja Z-Wave" diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 6dc259d6515..8dc88aa4da9 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -268,7 +268,15 @@ class ConfigEntry: return True if integration is None: - integration = await loader.async_get_integration(hass, self.domain) + try: + integration = await loader.async_get_integration(hass, self.domain) + except loader.IntegrationNotFound: + # The integration was likely a custom_component + # that was uninstalled, or an integration + # that has been renamed without removing the config + # entry. + self.state = ENTRY_STATE_NOT_LOADED + return True component = integration.get_component() @@ -316,7 +324,15 @@ class ConfigEntry: if self.source == SOURCE_IGNORE: return - integration = await loader.async_get_integration(hass, self.domain) + try: + integration = await loader.async_get_integration(hass, self.domain) + except loader.IntegrationNotFound: + # The integration was likely a custom_component + # that was uninstalled, or an integration + # that has been renamed without removing the config + # entry. + return + component = integration.get_component() if not hasattr(component, "async_remove_entry"): return diff --git a/homeassistant/const.py b/homeassistant/const.py index 7517217f7d2..67ad32d392b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 110 -PATCH_VERSION = "7" +MINOR_VERSION = 111 +PATCH_VERSION = "0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) @@ -180,7 +180,6 @@ CONF_XY = "xy" CONF_ZONE = "zone" # #### EVENTS #### -EVENT_AUTOMATION_TRIGGERED = "automation_triggered" EVENT_CALL_SERVICE = "call_service" EVENT_COMPONENT_LOADED = "component_loaded" EVENT_CORE_CONFIG_UPDATE = "core_config_updated" @@ -357,6 +356,10 @@ VOLT = "V" ENERGY_WATT_HOUR = f"{POWER_WATT}h" ENERGY_KILO_WATT_HOUR = f"k{ENERGY_WATT_HOUR}" +# Electrical units +ELECTRICAL_CURRENT_AMPERE = "A" +ELECTRICAL_VOLT_AMPERE = f"{VOLT}{ELECTRICAL_CURRENT_AMPERE}" + # Degree units DEGREE = "°" diff --git a/homeassistant/core.py b/homeassistant/core.py index 34df648a4df..eb7457daecb 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -209,6 +209,11 @@ class HomeAssistant: """Return if Home Assistant is running.""" return self.state in (CoreState.starting, CoreState.running) + @property + def is_stopping(self) -> bool: + """Return if Home Assistant is stopping.""" + return self.state in (CoreState.stopping, CoreState.final_write) + def start(self) -> int: """Start Home Assistant. @@ -260,6 +265,7 @@ class HomeAssistant: setattr(self.loop, "_thread_ident", threading.get_ident()) self.bus.async_fire(EVENT_HOMEASSISTANT_START) + self.bus.async_fire(EVENT_CORE_CONFIG_UPDATE) try: # Only block for EVENT_HOMEASSISTANT_START listener @@ -1391,6 +1397,7 @@ class Config: "version": __version__, "config_source": self.config_source, "safe_mode": self.safe_mode, + "state": self.hass.state.value, "external_url": self.external_url, "internal_url": self.internal_url, } diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 14ad0783380..33059ee0d68 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -7,6 +7,7 @@ To update, run python3 -m script.hassfest FLOWS = [ "abode", + "acmeda", "adguard", "agent_dvr", "airly", @@ -32,6 +33,7 @@ FLOWS = [ "dialogflow", "directv", "doorbird", + "dunehd", "dynalite", "ecobee", "elgato", @@ -51,8 +53,10 @@ FLOWS = [ "geonetnz_volcano", "gios", "glances", + "gogogate2", "gpslogger", "griddy", + "guardian", "hangouts", "harmony", "heos", @@ -111,6 +115,7 @@ FLOWS = [ "pi_hole", "plaato", "plex", + "plugwise", "point", "powerwall", "ps4", @@ -131,6 +136,7 @@ FLOWS = [ "solarlog", "soma", "somfy", + "sonarr", "songpal", "sonos", "spotify", @@ -161,7 +167,6 @@ FLOWS = [ "wiffi", "withings", "wled", - "wwlln", "xiaomi_miio", "zerproc", "zha", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 880bfedf400..ead2c0fa42d 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -6,6 +6,9 @@ To update, run python3 -m script.hassfest # fmt: off ZEROCONF = { + "_api._udp.local.": [ + "guardian" + ], "_axis-video._tcp.local.": [ "axis", "doorbird" @@ -13,6 +16,9 @@ ZEROCONF = { "_daap._tcp.local.": [ "forked_daapd" ], + "_dkapi._tcp.local.": [ + "daikin" + ], "_elg._tcp.local.": [ "elgato" ], @@ -31,6 +37,9 @@ ZEROCONF = { "_ipps._tcp.local.": [ "ipp" ], + "_miio._udp.local.": [ + "xiaomi_miio" + ], "_printer._tcp.local.": [ "brother" ], diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 32121958b03..c24adc76597 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -657,30 +657,30 @@ def deprecated( if replacement_key and invalidation_version: warning = ( - "The '{key}' option (with value '{value}') is" - " deprecated, please replace it with '{replacement_key}'." + "The '{key}' option is deprecated," + " please replace it with '{replacement_key}'." " This option will become invalid in version" " {invalidation_version}" ) elif replacement_key: warning = ( - "The '{key}' option (with value '{value}') is" - " deprecated, please replace it with '{replacement_key}'" + "The '{key}' option is deprecated," + " please replace it with '{replacement_key}'" ) elif invalidation_version: warning = ( - "The '{key}' option (with value '{value}') is" - " deprecated, please remove it from your configuration." + "The '{key}' option is deprecated," + " please remove it from your configuration." " This option will become invalid in version" " {invalidation_version}" ) else: warning = ( - "The '{key}' option (with value '{value}') is" - " deprecated, please remove it from your configuration" + "The '{key}' option is deprecated," + " please remove it from your configuration" ) - def check_for_invalid_version(value: Optional[Any]) -> None: + def check_for_invalid_version() -> None: """Raise error if current version has reached invalidation.""" if not invalidation_version: return @@ -689,7 +689,6 @@ def deprecated( raise vol.Invalid( warning.format( key=key, - value=value, replacement_key=replacement_key, invalidation_version=invalidation_version, ) @@ -698,19 +697,20 @@ def deprecated( def validator(config: Dict) -> Dict: """Check if key is in config and log warning.""" if key in config: - value = config[key] - check_for_invalid_version(value) + check_for_invalid_version() KeywordStyleAdapter(logging.getLogger(module_name)).warning( warning, key=key, - value=value, replacement_key=replacement_key, invalidation_version=invalidation_version, ) + + value = config[key] if replacement_key: config.pop(key) else: value = default + keys = [key] if replacement_key: keys.append(replacement_key) diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index fe6420750c6..1cf1fa4545c 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -1,5 +1,8 @@ """Helpers for the data entry flow.""" +from typing import Any, Dict + +from aiohttp import web import voluptuous as vol from homeassistant import config_entries, data_entry_flow @@ -8,18 +11,16 @@ from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.const import HTTP_NOT_FOUND import homeassistant.helpers.config_validation as cv -# mypy: allow-untyped-calls, allow-untyped-defs - class _BaseFlowManagerView(HomeAssistantView): """Foundation for flow manager views.""" - def __init__(self, flow_mgr): + def __init__(self, flow_mgr: data_entry_flow.FlowManager) -> None: """Initialize the flow manager index view.""" self._flow_mgr = flow_mgr # pylint: disable=no-self-use - def _prepare_result_json(self, result): + def _prepare_result_json(self, result: Dict[str, Any]) -> Dict[str, Any]: """Convert result to JSON.""" if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: data = result.copy() @@ -57,7 +58,7 @@ class FlowManagerIndexView(_BaseFlowManagerView): extra=vol.ALLOW_EXTRA, ) ) - async def post(self, request, data): + async def post(self, request: web.Request, data: Dict[str, Any]) -> web.Response: """Handle a POST request.""" if isinstance(data["handler"], list): handler = tuple(data["handler"]) @@ -66,7 +67,7 @@ class FlowManagerIndexView(_BaseFlowManagerView): try: result = await self._flow_mgr.async_init( - handler, + handler, # type: ignore context={ "source": config_entries.SOURCE_USER, "show_advanced_options": data["show_advanced_options"], @@ -85,7 +86,7 @@ class FlowManagerIndexView(_BaseFlowManagerView): class FlowManagerResourceView(_BaseFlowManagerView): """View to interact with the flow manager.""" - async def get(self, request, flow_id): + async def get(self, request: web.Request, flow_id: str) -> web.Response: """Get the current state of a data_entry_flow.""" try: result = await self._flow_mgr.async_configure(flow_id) @@ -97,7 +98,9 @@ class FlowManagerResourceView(_BaseFlowManagerView): return self.json(result) @RequestDataValidator(vol.Schema(dict), allow_empty=True) - async def post(self, request, flow_id, data): + async def post( + self, request: web.Request, flow_id: str, data: Dict[str, Any] + ) -> web.Response: """Handle a POST request.""" try: result = await self._flow_mgr.async_configure(flow_id, data) @@ -110,7 +113,7 @@ class FlowManagerResourceView(_BaseFlowManagerView): return self.json(result) - async def delete(self, request, flow_id): + async def delete(self, request: web.Request, flow_id: str) -> web.Response: """Cancel a flow in progress.""" try: self._flow_mgr.async_abort(flow_id) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 2e91d0d6622..aeee9e802c9 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -33,6 +33,26 @@ CONNECTION_UPNP = "upnp" CONNECTION_ZIGBEE = "zigbee" +@attr.s(slots=True, frozen=True) +class DeletedDeviceEntry: + """Deleted Device Registry Entry.""" + + config_entries: Set[str] = attr.ib() + connections: Set[Tuple[str, str]] = attr.ib() + identifiers: Set[Tuple[str, str]] = attr.ib() + id: str = attr.ib() + + def to_device_entry(self): + """Create DeviceEntry from DeletedDeviceEntry.""" + return DeviceEntry( + config_entries=self.config_entries, + connections=self.connections, + identifiers=self.identifiers, + id=self.id, + is_new=True, + ) + + @attr.s(slots=True, frozen=True) class DeviceEntry: """Device Registry Entry.""" @@ -81,6 +101,7 @@ class DeviceRegistry: """Class to hold a registry of devices.""" devices: Dict[str, DeviceEntry] + deleted_devices: Dict[str, DeletedDeviceEntry] def __init__(self, hass: HomeAssistantType) -> None: """Initialize the device registry.""" @@ -104,6 +125,18 @@ class DeviceRegistry: return device return None + @callback + def _async_get_deleted_device( + self, identifiers: set, connections: set + ) -> Optional[DeletedDeviceEntry]: + """Check if device has previously been registered.""" + for device in self.deleted_devices.values(): + if any(iden in device.identifiers for iden in identifiers) or any( + conn in device.connections for conn in connections + ): + return device + return None + @callback def async_get_or_create( self, @@ -136,7 +169,12 @@ class DeviceRegistry: device = self.async_get_device(identifiers, connections) if device is None: - device = DeviceEntry(is_new=True) + deleted_device = self._async_get_deleted_device(identifiers, connections) + if deleted_device is None: + device = DeviceEntry(is_new=True) + else: + self.deleted_devices.pop(deleted_device.id) + device = deleted_device.to_device_entry() self.devices[device.id] = device if via_device is not None: @@ -283,7 +321,13 @@ class DeviceRegistry: @callback def async_remove_device(self, device_id: str) -> None: """Remove a device from the device registry.""" - del self.devices[device_id] + device = self.devices.pop(device_id) + self.deleted_devices[device_id] = DeletedDeviceEntry( + config_entries=device.config_entries, + connections=device.connections, + identifiers=device.identifiers, + id=device.id, + ) self.hass.bus.async_fire( EVENT_DEVICE_REGISTRY_UPDATED, {"action": "remove", "device_id": device_id} ) @@ -296,6 +340,7 @@ class DeviceRegistry: data = await self._store.async_load() devices = OrderedDict() + deleted_devices = OrderedDict() if data is not None: for device in data["devices"]: @@ -319,8 +364,17 @@ class DeviceRegistry: area_id=device.get("area_id"), name_by_user=device.get("name_by_user"), ) + # Introduced in 0.111 + for device in data.get("deleted_devices", []): + deleted_devices[device["id"]] = DeletedDeviceEntry( + config_entries=set(device["config_entries"]), + connections={tuple(conn) for conn in device["connections"]}, + identifiers={tuple(iden) for iden in device["identifiers"]}, + id=device["id"], + ) self.devices = devices + self.deleted_devices = deleted_devices @callback def async_schedule_save(self) -> None: @@ -349,6 +403,15 @@ class DeviceRegistry: } for entry in self.devices.values() ] + data["deleted_devices"] = [ + { + "config_entries": list(entry.config_entries), + "connections": list(entry.connections), + "identifiers": list(entry.identifiers), + "id": entry.id, + } + for entry in self.deleted_devices.values() + ] return data @@ -357,6 +420,19 @@ class DeviceRegistry: """Clear config entry from registry entries.""" for device in list(self.devices.values()): self._async_update_device(device.id, remove_config_entry_id=config_entry_id) + for deleted_device in list(self.deleted_devices.values()): + config_entries = deleted_device.config_entries + if config_entry_id not in config_entries: + continue + if config_entries == {config_entry_id}: + # Permanently remove the device from the device registry. + del self.deleted_devices[deleted_device.id] + else: + config_entries = config_entries - {config_entry_id} + self.deleted_devices[deleted_device.id] = attr.evolve( + deleted_device, config_entries=config_entries + ) + self.async_schedule_save() @callback def async_clear_area_id(self, area_id: str) -> None: diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index fb7762fb9ae..e651d2e8cc7 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -123,15 +123,11 @@ class EntityComponent: self.config = config # Look in config for Domain, Domain 2, Domain 3 etc and load them - tasks = [] for p_type, p_config in config_per_platform(config, self.domain): - tasks.append(self.async_setup_platform(p_type, p_config)) - - if tasks: - await asyncio.gather(*tasks) + self.hass.async_create_task(self.async_setup_platform(p_type, p_config)) # Generic discovery listener for loading platform dynamically - # Refer to: homeassistant.components.discovery.load_platform() + # Refer to: homeassistant.helpers.discovery.async_load_platform() async def component_platform_discovered( platform: str, info: Optional[Dict[str, Any]] ) -> None: diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 30b07c98252..5eb5b213732 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -4,7 +4,7 @@ from contextvars import ContextVar from datetime import datetime, timedelta from logging import Logger from types import ModuleType -from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, cast +from typing import TYPE_CHECKING, Dict, Iterable, List, Optional from homeassistant.const import DEVICE_DEFAULT_NAME from homeassistant.core import CALLBACK_TYPE, callback, split_entity_id, valid_entity_id @@ -240,12 +240,9 @@ class EntityPlatform: ) -> None: """Schedule adding entities for a single platform async.""" self._tasks.append( - cast( - asyncio.Future, - self.hass.async_add_job( - self.async_add_entities( # type: ignore - new_entities, update_before_add=update_before_add - ), + self.hass.async_create_task( + self.async_add_entities( + new_entities, update_before_add=update_before_add ), ) ) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index c724b9e890d..107cd9e2106 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -393,58 +393,85 @@ class _ScriptRun(_ScriptRunBase): except KeyError: delay = None done = asyncio.Event() + tasks = [ + self._hass.async_create_task(flag.wait()) for flag in (self._stop, done) + ] try: async with timeout(delay): - _, pending = await asyncio.wait( - {self._stop.wait(), done.wait()}, - return_when=asyncio.FIRST_COMPLETED, - ) - for pending_task in pending: - pending_task.cancel() + await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) except asyncio.TimeoutError: if not self._action.get(CONF_CONTINUE_ON_TIMEOUT, True): self._log(_TIMEOUT_MSG) raise _StopScript finally: + for task in tasks: + task.cancel() unsub() async def _async_call_service_step(self): """Call the service specified in the action.""" domain, service, service_data = self._prep_call_service_step() + running_script = ( + domain == "automation" + and service == "trigger" + or domain == "python_script" + or domain == "script" + and service != SERVICE_TURN_OFF + ) # If this might start a script then disable the call timeout. # Otherwise use the normal service call limit. - if domain == "script" and service != SERVICE_TURN_OFF: + if running_script: limit = None else: limit = SERVICE_CALL_LIMIT - coro = self._hass.services.async_call( - domain, - service, - service_data, - blocking=True, - context=self._context, - limit=limit, + service_task = self._hass.async_create_task( + self._hass.services.async_call( + domain, + service, + service_data, + blocking=True, + context=self._context, + limit=limit, + ) ) - if limit is not None: # There is a call limit, so just wait for it to finish. - await coro + await service_task return - # No call limit (i.e., potentially starting one or more fully blocking scripts) - # so watch for a stop request. - done, pending = await asyncio.wait( - {self._stop.wait(), coro}, return_when=asyncio.FIRST_COMPLETED, - ) - # Note that cancelling the service call, if it has not yet returned, will also - # stop any non-background script runs that it may have started. - for pending_task in pending: - pending_task.cancel() - # Propagate any exceptions that might have happened. - for done_task in done: - done_task.result() + async def async_cancel_service_task(): + # Stop service task and wait for it to finish. + service_task.cancel() + try: + await service_task + except Exception: # pylint: disable=broad-except + pass + + # No call limit so watch for a stop request. + stop_task = self._hass.async_create_task(self._stop.wait()) + try: + await asyncio.wait( + {service_task, stop_task}, return_when=asyncio.FIRST_COMPLETED + ) + # If our task is cancelled, then cancel service task, too. Note that if service + # task is cancelled otherwise the CancelledError exception will not be raised to + # here due to the call to asyncio.wait(). Rather we'll check for that below. + except asyncio.CancelledError: + await async_cancel_service_task() + raise + finally: + stop_task.cancel() + + if service_task.cancelled(): + raise asyncio.CancelledError + if service_task.done(): + # Propagate any exceptions that occurred. + service_task.result() + elif running_script: + # Stopped before service completed, so cancel service. + await async_cancel_service_task() class _QueuedScriptRun(_ScriptRun): @@ -459,12 +486,18 @@ class _QueuedScriptRun(_ScriptRun): lock_task = self._hass.async_create_task( self._script._queue_lck.acquire() # pylint: disable=protected-access ) - done, pending = await asyncio.wait( - {self._stop.wait(), lock_task}, return_when=asyncio.FIRST_COMPLETED - ) - for pending_task in pending: - pending_task.cancel() - self.lock_acquired = lock_task in done + stop_task = self._hass.async_create_task(self._stop.wait()) + try: + await asyncio.wait( + {lock_task, stop_task}, return_when=asyncio.FIRST_COMPLETED + ) + except asyncio.CancelledError: + lock_task.cancel() + self._finish() + raise + finally: + stop_task.cancel() + self.lock_acquired = lock_task.done() and not lock_task.cancelled() # If we've been told to stop, then just finish up. Otherwise, we've acquired the # lock so we can go ahead and start the run. diff --git a/homeassistant/helpers/system_info.py b/homeassistant/helpers/system_info.py index 293a6e7bcef..a857858de1b 100644 --- a/homeassistant/helpers/system_info.py +++ b/homeassistant/helpers/system_info.py @@ -36,11 +36,9 @@ async def async_get_system_info(hass: HomeAssistantType) -> Dict: # Determine installation type on current data if info_object["docker"]: - info_object["installation_type"] = "Home Assistant Core on Docker" + info_object["installation_type"] = "Home Assistant Container" elif is_virtual_env(): - info_object[ - "installation_type" - ] = "Home Assistant Core in a Python Virtual Environment" + info_object["installation_type"] = "Home Assistant Core" # Enrich with Supervisor information if hass.components.hassio.is_hassio(): @@ -50,6 +48,7 @@ async def async_get_system_info(hass: HomeAssistantType) -> Dict: info_object["supervisor"] = info.get("supervisor") info_object["host_os"] = host.get("operating_system") info_object["chassis"] = host.get("chassis") + info_object["docker_version"] = info.get("docker") if info.get("hassos") is not None: info_object["installation_type"] = "Home Assistant" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0a0fc81ae5b..1f971ecda57 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,27 +11,30 @@ ciso8601==2.1.3 cryptography==2.9.2 defusedxml==0.6.0 distro==1.5.0 -hass-nabucasa==0.34.3 -home-assistant-frontend==20200519.5 +hass-nabucasa==0.34.6 +home-assistant-frontend==20200603.2 importlib-metadata==1.6.0 jinja2>=2.11.1 -netdisco==2.6.0 +netdisco==2.7.0 pip>=8.0.3 python-slugify==4.0.0 pytz>=2020.1 pyyaml==5.3.1 requests==2.23.0 ruamel.yaml==0.15.100 -sqlalchemy==1.3.16 +sqlalchemy==1.3.17 voluptuous-serialize==2.3.0 voluptuous==0.11.7 -zeroconf==0.26.3 +zeroconf==0.27.1 pycryptodome>=3.6.6 # Constrain urllib3 to ensure we deal with CVE-2019-11236 & CVE-2019-11324 urllib3>=1.24.3 +# Constrain httplib2 to protect against CVE-2020-11078 +httplib2>=0.18.0 + # Not needed for our supported Python versions enum34==1000000000.0.0 diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 70321d364b8..67d9200df61 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -10,15 +10,20 @@ from homeassistant.config import async_notify_setup_error from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.typing import ConfigType +from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) ATTR_COMPONENT = "component" +DATA_SETUP_STARTED = "setup_started" DATA_SETUP = "setup_tasks" DATA_DEPS_REQS = "deps_reqs_processed" SLOW_SETUP_WARNING = 10 +# Since a pip install can run, we wait +# 30 minutes to timeout +SLOW_SETUP_MAX_WAIT = 1800 def setup_component(hass: core.HomeAssistant, domain: str, config: ConfigType) -> bool: @@ -152,6 +157,7 @@ async def _async_setup_component( start = timer() _LOGGER.info("Setting up %s", domain) + hass.data.setdefault(DATA_SETUP_STARTED, {})[domain] = dt_util.utcnow() if hasattr(component, "PLATFORM_SCHEMA"): # Entity components have their own warning @@ -167,19 +173,34 @@ async def _async_setup_component( try: if hasattr(component, "async_setup"): - result = await component.async_setup( # type: ignore + task = component.async_setup( # type: ignore hass, processed_config ) elif hasattr(component, "setup"): - result = await hass.async_add_executor_job( - component.setup, hass, processed_config # type: ignore + # This should not be replaced with hass.async_add_executor_job because + # we don't want to track this task in case it blocks startup. + task = hass.loop.run_in_executor( + None, component.setup, hass, processed_config # type: ignore ) else: log_error("No setup function defined.") + hass.data[DATA_SETUP_STARTED].pop(domain) return False + + result = await asyncio.wait_for(task, SLOW_SETUP_MAX_WAIT) + except asyncio.TimeoutError: + _LOGGER.error( + "Setup of %s is taking longer than %s seconds." + " Startup will proceed without waiting any longer.", + domain, + SLOW_SETUP_MAX_WAIT, + ) + hass.data[DATA_SETUP_STARTED].pop(domain) + return False except Exception: # pylint: disable=broad-except _LOGGER.exception("Error during setup of component %s", domain) async_notify_setup_error(hass, domain, integration.documentation) + hass.data[DATA_SETUP_STARTED].pop(domain) return False finally: end = timer() @@ -189,12 +210,14 @@ async def _async_setup_component( if result is False: log_error("Integration failed to initialize.") + hass.data[DATA_SETUP_STARTED].pop(domain) return False if result is not True: log_error( f"Integration {domain!r} did not return boolean if setup was " "successful. Disabling component." ) + hass.data[DATA_SETUP_STARTED].pop(domain) return False # Flush out async_setup calling create_task. Fragile but covered by test. @@ -209,6 +232,7 @@ async def _async_setup_component( ) hass.config.components.add(domain) + hass.data[DATA_SETUP_STARTED].pop(domain) # Cleanup if domain in hass.data[DATA_SETUP]: diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index ae59e9bf4f9..ed710f573f4 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -1,12 +1,15 @@ """Logging utilities.""" import asyncio -from asyncio.events import AbstractEventLoop from functools import partial, wraps import inspect import logging -import threading +import logging.handlers +import queue import traceback -from typing import Any, Callable, Coroutine, Optional +from typing import Any, Callable, Coroutine + +from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE +from homeassistant.core import HomeAssistant, callback class HideSensitiveDataFilter(logging.Filter): @@ -24,104 +27,67 @@ class HideSensitiveDataFilter(logging.Filter): return True -class AsyncHandler: - """Logging handler wrapper to add an async layer.""" +class HomeAssistantQueueHandler(logging.handlers.QueueHandler): + """Process the log in another thread.""" - def __init__(self, loop: AbstractEventLoop, handler: logging.Handler) -> None: - """Initialize async logging handler wrapper.""" - self.handler = handler - self.loop = loop - self._queue: asyncio.Queue = asyncio.Queue(loop=loop) - self._thread = threading.Thread(target=self._process) - - # Delegate from handler - # pylint: disable=invalid-name - self.setLevel = handler.setLevel - self.setFormatter = handler.setFormatter - self.addFilter = handler.addFilter - self.removeFilter = handler.removeFilter - self.filter = handler.filter - self.flush = handler.flush - self.handle = handler.handle - self.handleError = handler.handleError - self.format = handler.format - - self._thread.start() - - def close(self) -> None: - """Wrap close to handler.""" - self.emit(None) - - async def async_close(self, blocking: bool = False) -> None: - """Close the handler. - - When blocking=True, will wait till closed. - """ - await self._queue.put(None) - - if blocking: - while self._thread.is_alive(): - await asyncio.sleep(0) - - def emit(self, record: Optional[logging.LogRecord]) -> None: - """Process a record.""" - ident = self.loop.__dict__.get("_thread_ident") - - # inside eventloop - if ident is not None and ident == threading.get_ident(): - self._queue.put_nowait(record) - # from a thread/executor - else: - self.loop.call_soon_threadsafe(self._queue.put_nowait, record) - - def __repr__(self) -> str: - """Return the string names.""" - return str(self.handler) - - def _process(self) -> None: - """Process log in a thread.""" + def emit(self, record: logging.LogRecord) -> None: + """Emit a log record.""" try: - while True: - record = asyncio.run_coroutine_threadsafe( - self._queue.get(), self.loop - ).result() + self.enqueue(record) + except asyncio.CancelledError: # pylint: disable=try-except-raise + raise + except Exception: # pylint: disable=broad-except + self.handleError(record) - if record is None: - self.handler.close() - return + def handle(self, record: logging.LogRecord) -> Any: + """ + Conditionally emit the specified logging record. - self.handler.emit(record) - except asyncio.CancelledError: - self.handler.close() + Depending on which filters have been added to the handler, push the new + records onto the backing Queue. - def createLock(self) -> None: # pylint: disable=invalid-name - """Ignore lock stuff.""" + The default python logger Handler acquires a lock + in the parent class which we do not need as + SimpleQueue is already thread safe. - def acquire(self) -> None: - """Ignore lock stuff.""" + See https://bugs.python.org/issue24645 + """ + return_value = self.filter(record) + if return_value: + self.emit(record) + return return_value - def release(self) -> None: - """Ignore lock stuff.""" - @property - def level(self) -> int: - """Wrap property level to handler.""" - return self.handler.level +@callback +def async_activate_log_queue_handler(hass: HomeAssistant) -> None: + """ + Migrate the existing log handlers to use the queue. - @property - def formatter(self) -> Optional[logging.Formatter]: - """Wrap property formatter to handler.""" - return self.handler.formatter + This allows us to avoid blocking I/O and formatting messages + in the event loop as log messages are written in another thread. + """ + simple_queue = queue.SimpleQueue() # type: ignore + queue_handler = HomeAssistantQueueHandler(simple_queue) + logging.root.addHandler(queue_handler) - @property - def name(self) -> str: - """Wrap property set_name to handler.""" - return self.handler.get_name() # type: ignore + migrated_handlers = [] + for handler in logging.root.handlers[:]: + if handler is queue_handler: + continue + logging.root.removeHandler(handler) + migrated_handlers.append(handler) - @name.setter - def name(self, name: str) -> None: - """Wrap property get_name to handler.""" - self.handler.set_name(name) # type: ignore + listener = logging.handlers.QueueListener(simple_queue, *migrated_handlers) + + listener.start() + + @callback + def _async_stop_queue_handler(_: Any) -> None: + """Cleanup handler.""" + logging.root.removeHandler(queue_handler) + listener.stop() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, _async_stop_queue_handler) def log_exception(format_err: Callable[..., Any], *args: Any) -> None: diff --git a/requirements_all.txt b/requirements_all.txt index 15d8f257edd..728e70634cf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -35,7 +35,7 @@ Adafruit-SHT31==1.0.2 # Adafruit_BBIO==1.1.1 # homeassistant.components.homekit -HAP-python==2.8.4 +HAP-python==2.9.1 # homeassistant.components.mastodon Mastodon.py==1.5.1 @@ -43,6 +43,9 @@ Mastodon.py==1.5.1 # homeassistant.components.orangepi_gpio OPi.GPIO==0.4.0 +# homeassistant.components.plugwise +Plugwise_Smile==0.2.13 + # homeassistant.components.essent PyEssent==0.13 @@ -155,9 +158,6 @@ aioambient==1.1.1 # homeassistant.components.asuswrt aioasuswrt==1.2.5 -# homeassistant.components.automatic -aioautomatic==0.6.5 - # homeassistant.components.aws aiobotocore==0.11.1 @@ -174,6 +174,9 @@ aiofreepybox==0.0.8 # homeassistant.components.yi aioftp==0.12.0 +# homeassistant.components.guardian +aioguardian==0.2.3 + # homeassistant.components.harmony aioharmony==0.1.13 @@ -194,7 +197,7 @@ aioimaplib==0.7.15 aiokafka==0.5.1 # homeassistant.components.kef -aiokef==0.2.9 +aiokef==0.2.10 # homeassistant.components.lifx aiolifx==0.6.7 @@ -208,6 +211,9 @@ aionotify==0.2.0 # homeassistant.components.notion aionotion==1.1.0 +# homeassistant.components.acmeda +aiopulse==0.4.0 + # homeassistant.components.hunterdouglas_powerview aiopvapi==1.6.14 @@ -223,9 +229,6 @@ aioswitcher==1.2.0 # homeassistant.components.unifi aiounifi==22 -# homeassistant.components.wwlln -aiowwlln==2.0.2 - # homeassistant.components.airly airly==0.0.2 @@ -269,7 +272,7 @@ aprslib==0.6.46 aqualogic==1.0 # homeassistant.components.arcam_fmj -arcam-fmj==0.4.4 +arcam-fmj==0.4.6 # homeassistant.components.arris_tg2492lg arris-tg2492lg==1.0.0 @@ -291,7 +294,7 @@ atenpdu==0.3.0 aurorapy==0.2.6 # homeassistant.components.stream -av==8.0.1 +av==8.0.2 # homeassistant.components.avea avea==1.4 @@ -303,10 +306,10 @@ avea==1.4 avri-api==0.1.7 # homeassistant.components.axis -axis==25 +axis==29 # homeassistant.components.azure_event_hub -azure-eventhub==1.3.1 +azure-eventhub==5.1.0 # homeassistant.components.azure_service_bus azure-servicebus==0.50.1 @@ -371,7 +374,7 @@ bomradarloop==0.1.4 boto3==1.9.252 # homeassistant.components.braviatv -bravia-tv==1.0.4 +bravia-tv==1.0.5 # homeassistant.components.broadlink broadlink==0.14.0 @@ -403,6 +406,9 @@ buienradar==1.0.4 # homeassistant.components.caldav caldav==0.6.1 +# homeassistant.components.circuit +circuit-webhook==1.0.1 + # homeassistant.components.cisco_mobility_express ciscomobilityexpress==0.3.3 @@ -436,7 +442,7 @@ connect-box==0.2.5 construct==2.9.45 # homeassistant.components.coronavirus -coronavirus==1.1.0 +coronavirus==1.1.1 # homeassistant.scripts.credstash # credstash==1.15.0 @@ -520,7 +526,7 @@ elgato==0.2.0 eliqonline==1.2.2 # homeassistant.components.elkm1 -elkm1-lib==0.7.17 +elkm1-lib==0.7.18 # homeassistant.components.emulated_roku emulated_roku==0.2.1 @@ -532,7 +538,7 @@ enocean==0.50 enturclient==0.2.1 # homeassistant.components.environment_canada -env_canada==0.0.35 +env_canada==0.0.38 # homeassistant.components.envirophat # envirophat==0.0.6 @@ -604,10 +610,7 @@ fritzconnection==1.2.0 gTTS-token==1.1.3 # homeassistant.components.garmin_connect -garminconnect==0.1.10 - -# homeassistant.components.gearbest -gearbest_parser==1.0.7 +garminconnect==0.1.13 # homeassistant.components.geizhals geizhals==0.0.9 @@ -649,6 +652,9 @@ glances_api==0.2.0 # homeassistant.components.gntp gntp==1.0.3 +# homeassistant.components.gogogate2 +gogogate2-api==1.0.3 + # homeassistant.components.google google-api-python-client==1.6.4 @@ -691,9 +697,6 @@ ha-ffmpeg==2.0 # homeassistant.components.philips_js ha-philipsjs==0.0.8 -# homeassistant.components.plugwise -haanna==0.15.0 - # homeassistant.components.habitica habitipy==0.2.0 @@ -701,7 +704,7 @@ habitipy==0.2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.34.3 +hass-nabucasa==0.34.6 # homeassistant.components.mqtt hbmqtt==0.9.5 @@ -731,7 +734,7 @@ hole==0.5.1 holidays==0.10.2 # homeassistant.components.frontend -home-assistant-frontend==20200519.5 +home-assistant-frontend==20200603.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 @@ -764,7 +767,7 @@ hydrawiser==0.1.1 iammeter==0.1.7 # homeassistant.components.iaqualink -iaqualink==0.3.3 +iaqualink==0.3.4 # homeassistant.components.watson_tts ibm-watson==4.0.1 @@ -784,9 +787,6 @@ incomfort-client==0.4.0 # homeassistant.components.influxdb influxdb==5.2.3 -# homeassistant.components.insteon -insteonplm==0.16.8 - # homeassistant.components.iperf3 iperf3==0.1.11 @@ -807,7 +807,7 @@ jsonrpc-websocket==0.6 kaiterra-async-client==0.0.2 # homeassistant.components.keba -keba-kecontact==1.0.0 +keba-kecontact==1.1.0 # homeassistant.scripts.keyring keyring==21.2.0 @@ -946,7 +946,7 @@ netdata==0.1.2 # homeassistant.components.discovery # homeassistant.components.ssdp -netdisco==2.6.0 +netdisco==2.7.0 # homeassistant.components.neurio_energy neurio==0.3.1 @@ -1009,7 +1009,7 @@ openerz-api==0.1.0 openevsewifi==0.4 # homeassistant.components.openhome -openhomedevice==0.6.3 +openhomedevice==0.7.2 # homeassistant.components.opensensemap opensensemap-api==0.1.5 @@ -1040,7 +1040,7 @@ panasonic_viera==0.3.5 pcal9535a==0.7 # homeassistant.components.dunehd -pdunehd==1.3 +pdunehd==1.3.1 # homeassistant.components.pencom pencompy==0.0.3 @@ -1078,13 +1078,13 @@ pillow==7.1.2 pizzapi==0.0.3 # homeassistant.components.plex -plexapi==3.6.0 +plexapi==4.0.0 # homeassistant.components.plex plexauth==0.0.5 # homeassistant.components.plex -plexwebsocket==0.0.8 +plexwebsocket==0.0.10 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 @@ -1115,7 +1115,7 @@ prometheus_client==0.7.1 protobuf==3.6.1 # homeassistant.components.proxmoxve -proxmoxer==1.0.4 +proxmoxer==1.1.0 # homeassistant.components.systemmonitor psutil==5.7.0 @@ -1212,7 +1212,7 @@ pyalmond==0.0.2 pyarlo==0.2.3 # homeassistant.components.atag -pyatag==0.3.1.2 +pyatag==0.3.3.4 # homeassistant.components.netatmo pyatmo==3.3.1 @@ -1245,7 +1245,7 @@ pycfdns==0.0.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==5.3.0 +pychromecast==6.0.0 # homeassistant.components.cmus pycmus==0.1.1 @@ -1263,13 +1263,13 @@ pycsspeechtts==1.0.3 # pycups==1.9.73 # homeassistant.components.daikin -pydaikin==2.0.4 +pydaikin==2.1.1 # homeassistant.components.danfoss_air pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==70 +pydeconz==71 # homeassistant.components.delijn pydelijn==0.6.0 @@ -1332,7 +1332,7 @@ pyflunearyou==1.0.7 pyfnip==0.2 # homeassistant.components.forked_daapd -pyforked-daapd==0.1.9 +pyforked-daapd==0.1.10 # homeassistant.components.fritzbox pyfritzhome==0.4.2 @@ -1347,9 +1347,6 @@ pyfttt==0.3 # homeassistant.components.skybeacon pygatt[GATTTOOL]==4.0.5 -# homeassistant.components.gogogate2 -pygogogate2==0.1.1 - # homeassistant.components.gtfs pygtfs==0.1.5 @@ -1366,7 +1363,7 @@ pyhik==0.2.7 pyhiveapi==0.2.20.1 # homeassistant.components.homematic -pyhomematic==0.1.66 +pyhomematic==0.1.67 # homeassistant.components.homeworks pyhomeworks==0.0.6 @@ -1377,6 +1374,9 @@ pyialarm==0.3 # homeassistant.components.icloud pyicloud==0.9.7 +# homeassistant.components.insteon +pyinsteon==1.0.3 + # homeassistant.components.intesishome pyintesishome==1.7.4 @@ -1447,7 +1447,7 @@ pymediaroom==0.6.4 pymelcloud==2.5.2 # homeassistant.components.somfy -pymfy==0.7.1 +pymfy==0.9.0 # homeassistant.components.xiaomi_tv pymitv==1.4.3 @@ -1486,7 +1486,7 @@ pynetgear==0.6.1 pynetio==0.1.9.1 # homeassistant.components.nuki -pynuki==1.3.3 +pynuki==1.3.7 # homeassistant.components.nut pynut2==2.1.2 @@ -1539,7 +1539,7 @@ pypca==0.0.7 pypck==0.6.4 # homeassistant.components.pjlink -pypjlink2==1.2.0 +pypjlink2==1.2.1 # homeassistant.components.point pypoint==1.1.2 @@ -1615,13 +1615,13 @@ pysnmp==4.4.12 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.30 +pysonos==0.0.31 # homeassistant.components.spc pyspcwebgw==0.4.0 # homeassistant.components.squeezebox -pysqueezebox==0.1.4 +pysqueezebox==0.2.1 # homeassistant.components.stiebel_eltron pystiebeleltron==0.0.1.dev2 @@ -1799,7 +1799,7 @@ pyversasense==0.0.6 pyvesync==1.1.0 # homeassistant.components.vizio -pyvizio==0.1.47 +pyvizio==0.1.48 # homeassistant.components.velux pyvlx==0.2.16 @@ -1808,7 +1808,7 @@ pyvlx==0.2.16 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.4.34 +pywemo==0.4.43 # homeassistant.components.xeoma pyxeoma==1.4.1 @@ -1989,6 +1989,9 @@ somecomfort==0.5.2 # homeassistant.components.somfy_mylink somfy-mylink-synergy==1.0.6 +# homeassistant.components.sonarr +sonarr==0.2.2 + # homeassistant.components.marytts speak2mary==1.4.0 @@ -2006,7 +2009,7 @@ spotipy==2.12.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.16 +sqlalchemy==1.3.17 # homeassistant.components.starline starline==0.1.3 @@ -2075,10 +2078,10 @@ temperusb==1.5.3 # tensorflow==1.13.2 # homeassistant.components.powerwall -tesla-powerwall==0.2.8 +tesla-powerwall==0.2.10 # homeassistant.components.tesla -teslajsonpy==0.8.0 +teslajsonpy==0.8.1 # homeassistant.components.thermoworks_smoke thermoworks_smoke==0.1.8 @@ -2099,7 +2102,7 @@ todoist-python==8.0.0 toonapilib==3.2.4 # homeassistant.components.totalconnect -total_connect_client==0.54.1 +total_connect_client==0.55 # homeassistant.components.tplink_lte tp-connected==0.0.4 @@ -2192,12 +2195,9 @@ wirelesstagpy==0.4.0 withings-api==2.1.3 # homeassistant.components.wled -wled==0.3.0 +wled==0.4.2 -# homeassistant.components.wunderlist -wunderpy2==0.1.6 - -# homeassistant.components.zigbee +# homeassistant.components.xbee xbee-helper==0.0.7 # homeassistant.components.xbox_live @@ -2227,19 +2227,19 @@ ya_ma==0.3.8 yalesmartalarmclient==0.1.6 # homeassistant.components.yeelight -yeelight==0.5.1 +yeelight==0.5.2 # homeassistant.components.yeelightsunflower yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2020.05.08 +youtube_dl==2020.05.29 # homeassistant.components.zengge zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.26.3 +zeroconf==0.27.1 # homeassistant.components.zha zha-quirks==0.0.39 @@ -2251,7 +2251,7 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-cc==0.4.2 +zigpy-cc==0.4.4 # homeassistant.components.zha zigpy-deconz==0.9.2 diff --git a/requirements_test.txt b/requirements_test.txt index 54d6f6b036d..60d085752ed 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -4,7 +4,7 @@ -r requirements_test_pre_commit.txt asynctest==0.13.0 -codecov==2.0.22 +codecov==2.1.0 coverage==5.1 mock-open==1.4.0 mypy==0.770 @@ -16,6 +16,6 @@ pytest-aiohttp==0.3.0 pytest-cov==2.8.1 pytest-sugar==0.9.3 pytest-timeout==1.3.4 -pytest==5.4.2 +pytest==5.4.3 requests_mock==1.8.0 responses==0.10.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2ed01366cde..c576dd99695 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -4,7 +4,10 @@ -r requirements_test.txt # homeassistant.components.homekit -HAP-python==2.8.4 +HAP-python==2.9.1 + +# homeassistant.components.plugwise +Plugwise_Smile==0.2.13 # homeassistant.components.flick_electric PyFlick==0.0.2 @@ -78,6 +81,9 @@ aioesphomeapi==2.6.1 # homeassistant.components.freebox aiofreepybox==0.0.8 +# homeassistant.components.guardian +aioguardian==0.2.3 + # homeassistant.components.harmony aioharmony==0.1.13 @@ -94,6 +100,9 @@ aiohue==2.1.0 # homeassistant.components.notion aionotion==1.1.0 +# homeassistant.components.acmeda +aiopulse==0.4.0 + # homeassistant.components.hunterdouglas_powerview aiopvapi==1.6.14 @@ -109,9 +118,6 @@ aioswitcher==1.2.0 # homeassistant.components.unifi aiounifi==22 -# homeassistant.components.wwlln -aiowwlln==2.0.2 - # homeassistant.components.airly airly==0.0.2 @@ -131,17 +137,17 @@ apprise==0.8.5 aprslib==0.6.46 # homeassistant.components.arcam_fmj -arcam-fmj==0.4.4 +arcam-fmj==0.4.6 # homeassistant.components.dlna_dmr # homeassistant.components.upnp async-upnp-client==0.14.13 # homeassistant.components.stream -av==8.0.1 +av==8.0.2 # homeassistant.components.axis -axis==25 +axis==29 # homeassistant.components.homekit base36==0.1.1 @@ -156,7 +162,7 @@ blebox_uniapi==1.3.2 bomradarloop==0.1.4 # homeassistant.components.braviatv -bravia-tv==1.0.4 +bravia-tv==1.0.5 # homeassistant.components.broadlink broadlink==0.14.0 @@ -185,7 +191,7 @@ colorlog==4.1.0 construct==2.9.45 # homeassistant.components.coronavirus -coronavirus==1.1.0 +coronavirus==1.1.1 # homeassistant.scripts.credstash # credstash==1.15.0 @@ -227,7 +233,7 @@ eebrightbox==0.0.4 elgato==0.2.0 # homeassistant.components.elkm1 -elkm1-lib==0.7.17 +elkm1-lib==0.7.18 # homeassistant.components.emulated_roku emulated_roku==0.2.1 @@ -248,7 +254,7 @@ foobot_async==0.3.1 gTTS-token==1.1.3 # homeassistant.components.garmin_connect -garminconnect==0.1.10 +garminconnect==0.1.13 # homeassistant.components.geo_json_events # homeassistant.components.usgs_earthquakes_feed @@ -278,6 +284,9 @@ gios==0.1.1 # homeassistant.components.glances glances_api==0.2.0 +# homeassistant.components.gogogate2 +gogogate2-api==1.0.3 + # homeassistant.components.google google-api-python-client==1.6.4 @@ -294,7 +303,7 @@ ha-ffmpeg==2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.34.3 +hass-nabucasa==0.34.6 # homeassistant.components.mqtt hbmqtt==0.9.5 @@ -312,7 +321,7 @@ hole==0.5.1 holidays==0.10.2 # homeassistant.components.frontend -home-assistant-frontend==20200519.5 +home-assistant-frontend==20200603.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 @@ -331,7 +340,7 @@ httplib2==0.10.3 huawei-lte-api==1.4.12 # homeassistant.components.iaqualink -iaqualink==0.3.3 +iaqualink==0.3.4 # homeassistant.components.influxdb influxdb==5.2.3 @@ -390,7 +399,7 @@ nessclient==0.9.15 # homeassistant.components.discovery # homeassistant.components.ssdp -netdisco==2.6.0 +netdisco==2.7.0 # homeassistant.components.nexia nexia==0.9.3 @@ -426,6 +435,9 @@ paho-mqtt==1.5.0 # homeassistant.components.panasonic_viera panasonic_viera==0.3.5 +# homeassistant.components.dunehd +pdunehd==1.3.1 + # homeassistant.components.aruba # homeassistant.components.cisco_ios # homeassistant.components.pandora @@ -444,13 +456,13 @@ pilight==0.1.1 pillow==7.1.2 # homeassistant.components.plex -plexapi==3.6.0 +plexapi==4.0.0 # homeassistant.components.plex plexauth==0.0.5 # homeassistant.components.plex -plexwebsocket==0.0.8 +plexwebsocket==0.0.10 # homeassistant.components.mhz19 # homeassistant.components.serial_pm @@ -515,7 +527,7 @@ pyalmond==0.0.2 pyarlo==0.2.3 # homeassistant.components.atag -pyatag==0.3.1.2 +pyatag==0.3.3.4 # homeassistant.components.netatmo pyatmo==3.3.1 @@ -527,16 +539,16 @@ pyblackbird==0.5 pybotvac==0.0.17 # homeassistant.components.cast -pychromecast==5.3.0 +pychromecast==6.0.0 # homeassistant.components.coolmaster pycoolmasternet==0.0.4 # homeassistant.components.daikin -pydaikin==2.0.4 +pydaikin==2.1.1 # homeassistant.components.deconz -pydeconz==70 +pydeconz==71 # homeassistant.components.zwave pydispatcher==2.0.5 @@ -554,7 +566,7 @@ pyflume==0.4.0 pyflunearyou==1.0.7 # homeassistant.components.forked_daapd -pyforked-daapd==0.1.9 +pyforked-daapd==0.1.10 # homeassistant.components.fritzbox pyfritzhome==0.4.2 @@ -573,7 +585,7 @@ pyhaversion==3.3.0 pyheos==0.6.0 # homeassistant.components.homematic -pyhomematic==0.1.66 +pyhomematic==0.1.67 # homeassistant.components.icloud pyicloud==0.9.7 @@ -615,7 +627,7 @@ pymailgunner==1.4 pymelcloud==2.5.2 # homeassistant.components.somfy -pymfy==0.7.1 +pymfy==0.9.0 # homeassistant.components.mochad pymochad==0.2.0 @@ -681,7 +693,7 @@ pysmartthings==0.7.1 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.30 +pysonos==0.0.31 # homeassistant.components.spc pyspcwebgw==0.4.0 @@ -738,7 +750,7 @@ pyvera==0.3.7 pyvesync==1.1.0 # homeassistant.components.vizio -pyvizio==0.1.47 +pyvizio==0.1.48 # homeassistant.components.html5 pywebpush==1.9.2 @@ -800,6 +812,9 @@ solaredge==0.0.2 # homeassistant.components.honeywell somecomfort==0.5.2 +# homeassistant.components.sonarr +sonarr==0.2.2 + # homeassistant.components.marytts speak2mary==1.4.0 @@ -808,7 +823,7 @@ spotipy==2.12.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.16 +sqlalchemy==1.3.17 # homeassistant.components.starline starline==0.1.3 @@ -829,16 +844,16 @@ sunwatcher==0.2.1 tellduslive==0.10.11 # homeassistant.components.powerwall -tesla-powerwall==0.2.8 +tesla-powerwall==0.2.10 # homeassistant.components.tesla -teslajsonpy==0.8.0 +teslajsonpy==0.8.1 # homeassistant.components.toon toonapilib==3.2.4 # homeassistant.components.totalconnect -total_connect_client==0.54.1 +total_connect_client==0.55 # homeassistant.components.transmission transmissionrpc==0.11 @@ -886,7 +901,7 @@ wiffi==1.0.0 withings-api==2.1.3 # homeassistant.components.wled -wled==0.3.0 +wled==0.4.2 # homeassistant.components.bluesound # homeassistant.components.rest @@ -900,13 +915,13 @@ xmltodict==0.12.0 ya_ma==0.3.8 # homeassistant.components.zeroconf -zeroconf==0.26.3 +zeroconf==0.27.1 # homeassistant.components.zha zha-quirks==0.0.39 # homeassistant.components.zha -zigpy-cc==0.4.2 +zigpy-cc==0.4.4 # homeassistant.components.zha zigpy-deconz==0.9.2 diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 798377780ff..e0882a786ea 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -4,7 +4,7 @@ bandit==1.6.2 black==19.10b0 codespell==1.16.0 flake8-docstrings==1.5.0 -flake8==3.7.9 +flake8==3.8.1 isort==4.3.21 pydocstyle==5.0.2 pyupgrade==2.3.0 diff --git a/rootfs/etc/services.d/home-assistant/run b/rootfs/etc/services.d/home-assistant/run index 750d00a91ec..11af113e4b9 100644 --- a/rootfs/etc/services.d/home-assistant/run +++ b/rootfs/etc/services.d/home-assistant/run @@ -2,9 +2,11 @@ # ============================================================================== # Start Home Assistant service # ============================================================================== + cd /config || bashio::exit.nok "Can't find config folder!" -# Enable Jemalloc for Home Assistant Core -export LD_PRELOAD="/usr/local/lib/libjemalloc.so.2" - +# Enable Jemalloc for Home Assistant Core, unless disabled +if [[ -z "${DISABLE_JEMALLOC+x}" ]]; then + export LD_PRELOAD="/usr/local/lib/libjemalloc.so.2" +fi exec python3 -m homeassistant --config /config diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index fdd0c564efb..c3b7b05dc30 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -61,6 +61,9 @@ pycryptodome>=3.6.6 # Constrain urllib3 to ensure we deal with CVE-2019-11236 & CVE-2019-11324 urllib3>=1.24.3 +# Constrain httplib2 to protect against CVE-2020-11078 +httplib2>=0.18.0 + # Not needed for our supported Python versions enum34==1000000000.0.0 diff --git a/script/translations/const.py b/script/translations/const.py index ddb753ef1ca..d282c9c2915 100644 --- a/script/translations/const.py +++ b/script/translations/const.py @@ -3,6 +3,6 @@ import pathlib CORE_PROJECT_ID = "130246255a974bd3b5e8a1.51616605" FRONTEND_PROJECT_ID = "3420425759f6d6d241f598.13594006" -DOCKER_IMAGE = "b8329d20280263cad04f65b843e54b9e8e6909a348a678eac959550b5ef5c75f" +CLI_2_DOCKER_IMAGE = "v2.3.0" INTEGRATIONS_DIR = pathlib.Path("homeassistant/components") FRONTEND_DIR = pathlib.Path("../frontend") diff --git a/script/translations/download.py b/script/translations/download.py index 364f309b644..8f17e057080 100755 --- a/script/translations/download.py +++ b/script/translations/download.py @@ -7,7 +7,7 @@ import re import subprocess from typing import Dict, List, Union -from .const import CORE_PROJECT_ID, DOCKER_IMAGE +from .const import CLI_2_DOCKER_IMAGE, CORE_PROJECT_ID from .error import ExitApp from .util import get_lokalise_token @@ -25,18 +25,23 @@ def run_download_docker(): "-v", f"{DOWNLOAD_DIR}:/opt/dest/locale", "--rm", - f"lokalise/lokalise-cli@sha256:{DOCKER_IMAGE}", + f"lokalise/lokalise-cli-2:{CLI_2_DOCKER_IMAGE}", # Lokalise command - "lokalise", + "lokalise2", "--token", get_lokalise_token(), - "export", + "--project-id", CORE_PROJECT_ID, - "--export_empty", + "file", + "download", + CORE_PROJECT_ID, + "--original-filenames=false", + "--replace-breaks=false", + "--export-empty-as", "skip", - "--type", + "--format", "json", - "--unzip_to", + "--unzip-to", "/opt/dest", ] ) diff --git a/script/translations/upload.py b/script/translations/upload.py index 844c706d064..02d964a94c9 100755 --- a/script/translations/upload.py +++ b/script/translations/upload.py @@ -6,7 +6,7 @@ import pathlib import re import subprocess -from .const import CORE_PROJECT_ID, DOCKER_IMAGE, INTEGRATIONS_DIR +from .const import CLI_2_DOCKER_IMAGE, CORE_PROJECT_ID, INTEGRATIONS_DIR from .error import ExitApp from .util import get_current_branch, get_lokalise_token @@ -26,21 +26,21 @@ def run_upload_docker(): "-v", f"{LOCAL_FILE}:{CONTAINER_FILE}", "--rm", - f"lokalise/lokalise-cli@sha256:{DOCKER_IMAGE}", + f"lokalise/lokalise-cli-2:{CLI_2_DOCKER_IMAGE}", # Lokalise command - "lokalise", + "lokalise2", "--token", get_lokalise_token(), - "import", + "--project-id", CORE_PROJECT_ID, + "file", + "upload", "--file", CONTAINER_FILE, - "--lang_iso", + "--lang-iso", LANG_ISO, - "--convert_placeholders", - "0", - "--replace", - "1", + "--convert-placeholders=false", + "--replace-modified", ], ) print() diff --git a/tests/common.py b/tests/common.py index d6ae25adb5e..2136de3584f 100644 --- a/tests/common.py +++ b/tests/common.py @@ -381,10 +381,11 @@ def mock_area_registry(hass, mock_entries=None): return registry -def mock_device_registry(hass, mock_entries=None): +def mock_device_registry(hass, mock_entries=None, mock_deleted_entries=None): """Mock the Device Registry.""" registry = device_registry.DeviceRegistry(hass) registry.devices = mock_entries or OrderedDict() + registry.deleted_devices = mock_deleted_entries or OrderedDict() hass.data[device_registry.DATA_REGISTRY] = registry return registry diff --git a/tests/components/acmeda/__init__.py b/tests/components/acmeda/__init__.py new file mode 100644 index 00000000000..126c834d1ee --- /dev/null +++ b/tests/components/acmeda/__init__.py @@ -0,0 +1 @@ +"""Tests for the Rollease Acmeda Automate integration.""" diff --git a/tests/components/acmeda/test_config_flow.py b/tests/components/acmeda/test_config_flow.py new file mode 100644 index 00000000000..1663bc3d443 --- /dev/null +++ b/tests/components/acmeda/test_config_flow.py @@ -0,0 +1,143 @@ +"""Define tests for the Acmeda config flow.""" +import aiopulse +from asynctest.mock import patch +import pytest + +from homeassistant import data_entry_flow +from homeassistant.components.acmeda.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_HOST + +from tests.common import MockConfigEntry + +DUMMY_HOST1 = "127.0.0.1" +DUMMY_HOST2 = "127.0.0.2" + +CONFIG = { + CONF_HOST: DUMMY_HOST1, +} + + +@pytest.fixture +def mock_hub_discover(): + """Mock the hub discover method.""" + with patch("aiopulse.Hub.discover") as mock_discover: + yield mock_discover + + +@pytest.fixture +def mock_hub_run(): + """Mock the hub run method.""" + with patch("aiopulse.Hub.run") as mock_run: + yield mock_run + + +async def async_generator(items): + """Async yields items provided in a list.""" + for item in items: + yield item + + +async def test_show_form_no_hubs(hass, mock_hub_discover): + """Test that flow aborts if no hubs are discovered.""" + mock_hub_discover.return_value = async_generator([]) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "all_configured" + + # Check we performed the discovery + assert len(mock_hub_discover.mock_calls) == 1 + + +async def test_show_form_one_hub(hass, mock_hub_discover, mock_hub_run): + """Test that a config is created when one hub discovered.""" + + dummy_hub_1 = aiopulse.Hub(DUMMY_HOST1) + dummy_hub_1.id = "ABC123" + + mock_hub_discover.return_value = async_generator([dummy_hub_1]) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == dummy_hub_1.id + assert result["result"].data == { + "host": DUMMY_HOST1, + } + + # Check we performed the discovery + assert len(mock_hub_discover.mock_calls) == 1 + + +async def test_show_form_two_hubs(hass, mock_hub_discover): + """Test that the form is served when more than one hub discovered.""" + + dummy_hub_1 = aiopulse.Hub(DUMMY_HOST1) + dummy_hub_1.id = "ABC123" + + dummy_hub_2 = aiopulse.Hub(DUMMY_HOST1) + dummy_hub_2.id = "DEF456" + + mock_hub_discover.return_value = async_generator([dummy_hub_1, dummy_hub_2]) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + # Check we performed the discovery + assert len(mock_hub_discover.mock_calls) == 1 + + +async def test_create_second_entry(hass, mock_hub_run, mock_hub_discover): + """Test that a config is created when a second hub is discovered.""" + + dummy_hub_1 = aiopulse.Hub(DUMMY_HOST1) + dummy_hub_1.id = "ABC123" + + dummy_hub_2 = aiopulse.Hub(DUMMY_HOST2) + dummy_hub_2.id = "DEF456" + + mock_hub_discover.return_value = async_generator([dummy_hub_1, dummy_hub_2]) + + MockConfigEntry(domain=DOMAIN, unique_id=dummy_hub_1.id, data=CONFIG).add_to_hass( + hass + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == dummy_hub_2.id + assert result["result"].data == { + "host": DUMMY_HOST2, + } + + +async def test_already_configured(hass, mock_hub_discover): + """Test that flow aborts when all hubs are configured.""" + + dummy_hub_1 = aiopulse.Hub(DUMMY_HOST1) + dummy_hub_1.id = "ABC123" + + mock_hub_discover.return_value = async_generator([dummy_hub_1]) + + MockConfigEntry(domain=DOMAIN, unique_id=dummy_hub_1.id, data=CONFIG).add_to_hass( + hass + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] == "abort" + assert result["reason"] == "all_configured" diff --git a/tests/components/air_quality/test_air_quality.py b/tests/components/air_quality/test_air_quality.py index f457ebb3d2f..e0692931e1c 100644 --- a/tests/components/air_quality/test_air_quality.py +++ b/tests/components/air_quality/test_air_quality.py @@ -17,6 +17,7 @@ async def test_state(hass): config = {"air_quality": {"platform": "demo"}} assert await async_setup_component(hass, "air_quality", config) + await hass.async_block_till_done() state = hass.states.get("air_quality.demo_air_quality_home") assert state is not None @@ -29,6 +30,7 @@ async def test_attributes(hass): config = {"air_quality": {"platform": "demo"}} assert await async_setup_component(hass, "air_quality", config) + await hass.async_block_till_done() state = hass.states.get("air_quality.demo_air_quality_office") assert state is not None diff --git a/tests/components/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py index fcb2360b6c6..6741731e0e5 100644 --- a/tests/components/airvisual/test_config_flow.py +++ b/tests/components/airvisual/test_config_flow.py @@ -111,8 +111,11 @@ async def test_migration(hass): assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - with patch("pyairvisual.api.API.nearest_city"): + with patch("pyairvisual.api.API.nearest_city"), patch.object( + hass.config_entries, "async_forward_entry_setup" + ): assert await async_setup_component(hass, DOMAIN, {DOMAIN: conf}) + await hass.async_block_till_done() config_entries = hass.config_entries.async_entries(DOMAIN) diff --git a/tests/components/alexa/test_auth.py b/tests/components/alexa/test_auth.py index 9d14fffe7e4..ae28052efc9 100644 --- a/tests/components/alexa/test_auth.py +++ b/tests/components/alexa/test_auth.py @@ -1,5 +1,6 @@ """Test Alexa auth endpoints.""" from homeassistant.components.alexa.auth import Auth +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from . import TEST_TOKEN_URL @@ -53,13 +54,13 @@ async def test_auth_get_access_token_expired(hass, aioclient_mock): assert auth_call_json["grant_type"] == "authorization_code" assert auth_call_json["code"] == accept_grant_code - assert auth_call_json["client_id"] == client_id - assert auth_call_json["client_secret"] == client_secret + assert auth_call_json[CONF_CLIENT_ID] == client_id + assert auth_call_json[CONF_CLIENT_SECRET] == client_secret assert token_call_json["grant_type"] == "refresh_token" assert token_call_json["refresh_token"] == refresh_token - assert token_call_json["client_id"] == client_id - assert token_call_json["client_secret"] == client_secret + assert token_call_json[CONF_CLIENT_ID] == client_id + assert token_call_json[CONF_CLIENT_SECRET] == client_secret async def test_auth_get_access_token_not_expired(hass, aioclient_mock): @@ -86,5 +87,5 @@ async def test_auth_get_access_token_not_expired(hass, aioclient_mock): assert auth_call_json["grant_type"] == "authorization_code" assert auth_call_json["code"] == accept_grant_code - assert auth_call_json["client_id"] == client_id - assert auth_call_json["client_secret"] == client_secret + assert auth_call_json[CONF_CLIENT_ID] == client_id + assert auth_call_json[CONF_CLIENT_SECRET] == client_secret diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 591d200ef90..f2777ab00f8 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -55,19 +55,19 @@ def events(hass): @pytest.fixture -def mock_camera(hass): +async def mock_camera(hass): """Initialize a demo camera platform.""" - assert hass.loop.run_until_complete( - async_setup_component(hass, "camera", {camera.DOMAIN: {"platform": "demo"}}) + assert await async_setup_component( + hass, "camera", {camera.DOMAIN: {"platform": "demo"}} ) + await hass.async_block_till_done() @pytest.fixture -def mock_stream(hass): +async def mock_stream(hass): """Initialize a demo camera platform with streaming.""" - assert hass.loop.run_until_complete( - async_setup_component(hass, "stream", {"stream": {}}) - ) + assert await async_setup_component(hass, "stream", {"stream": {}}) + await hass.async_block_till_done() def test_create_api_message_defaults(hass): @@ -3297,8 +3297,8 @@ async def test_media_player_eq_modes(hass): assert call.data["sound_mode"] == mode.lower() -async def test_media_player_sound_mode_list_none(hass): - """Test EqualizerController bands directive not supported.""" +async def test_media_player_sound_mode_list_unsupported(hass): + """Test EqualizerController with unsupported sound modes.""" device = ( "media_player.test", "on", @@ -3306,13 +3306,18 @@ async def test_media_player_sound_mode_list_none(hass): "friendly_name": "Test media player", "supported_features": SUPPORT_SELECT_SOUND_MODE, "sound_mode": "unknown", - "sound_mode_list": None, + "sound_mode_list": ["unsupported", "non-existing"], }, ) appliance = await discovery_test(device, hass) assert appliance["endpointId"] == "media_player#test" assert appliance["friendlyName"] == "Test media player" + # Test equalizer controller is not there + assert_endpoint_capabilities( + appliance, "Alexa", "Alexa.PowerController", "Alexa.EndpointHealth", + ) + async def test_media_player_eq_bands_not_supported(hass): """Test EqualizerController bands directive not supported.""" diff --git a/tests/components/almond/test_config_flow.py b/tests/components/almond/test_config_flow.py index d1403c017aa..959846bd017 100644 --- a/tests/components/almond/test_config_flow.py +++ b/tests/components/almond/test_config_flow.py @@ -4,6 +4,7 @@ import asyncio from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.almond import config_flow from homeassistant.components.almond.const import DOMAIN +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow from tests.async_mock import patch @@ -17,9 +18,7 @@ async def test_import(hass): """Test that we can import a config entry.""" with patch("pyalmond.WebAlmondAPI.async_list_apps"): assert await setup.async_setup_component( - hass, - "almond", - {"almond": {"type": "local", "host": "http://localhost:3000"}}, + hass, DOMAIN, {DOMAIN: {"type": "local", "host": "http://localhost:3000"}}, ) await hass.async_block_till_done() @@ -35,9 +34,7 @@ async def test_import_cannot_connect(hass): "pyalmond.WebAlmondAPI.async_list_apps", side_effect=asyncio.TimeoutError ): assert await setup.async_setup_component( - hass, - "almond", - {"almond": {"type": "local", "host": "http://localhost:3000"}}, + hass, DOMAIN, {DOMAIN: {"type": "local", "host": "http://localhost:3000"}}, ) await hass.async_block_till_done() @@ -94,19 +91,19 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock): """Check full flow.""" assert await setup.async_setup_component( hass, - "almond", + DOMAIN, { - "almond": { + DOMAIN: { "type": "oauth2", - "client_id": CLIENT_ID_VALUE, - "client_secret": CLIENT_SECRET_VALUE, + CONF_CLIENT_ID: CLIENT_ID_VALUE, + CONF_CLIENT_SECRET: CLIENT_SECRET_VALUE, }, "http": {"base_url": "https://example.com"}, }, ) result = await hass.config_entries.flow.async_init( - "almond", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) diff --git a/tests/components/ambiclimate/test_config_flow.py b/tests/components/ambiclimate/test_config_flow.py index 6dee15c27f9..2ff7942f8dd 100644 --- a/tests/components/ambiclimate/test_config_flow.py +++ b/tests/components/ambiclimate/test_config_flow.py @@ -3,6 +3,7 @@ import ambiclimate from homeassistant import data_entry_flow from homeassistant.components.ambiclimate import config_flow +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.setup import async_setup_component from homeassistant.util import aiohttp @@ -71,8 +72,8 @@ async def test_full_flow_implementation(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "Ambiclimate" assert result["data"]["callback_url"] == "https://hass.com/api/ambiclimate" - assert result["data"]["client_secret"] == "secret" - assert result["data"]["client_id"] == "id" + assert result["data"][CONF_CLIENT_SECRET] == "secret" + assert result["data"][CONF_CLIENT_ID] == "id" with patch("ambiclimate.AmbiclimateOAuth.get_access_token", return_value=None): result = await flow.async_step_code("123ABC") diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index fa4f6ffbed6..85e4a75acd0 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -116,6 +116,7 @@ async def _test_reconnect(hass, caplog, config): patch_key ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) @@ -187,6 +188,7 @@ async def _test_adb_shell_returns_none(hass, config): patch_key ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) assert state is not None @@ -295,6 +297,7 @@ async def test_setup_with_adbkey(hass): patch_key ], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER, patchers.PATCH_ISFILE, patchers.PATCH_ACCESS: assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) assert state is not None @@ -315,6 +318,7 @@ async def _test_sources(hass, config0): patch_key ], patchers.patch_shell("")[patch_key]: assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) assert state is not None @@ -395,6 +399,7 @@ async def _test_exclude_sources(hass, config0, expected_sources): patch_key ], patchers.patch_shell("")[patch_key]: assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) assert state is not None @@ -463,6 +468,7 @@ async def _test_select_source(hass, config0, source, expected_arg, method_patch) patch_key ], patchers.patch_shell("")[patch_key]: assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) assert state is not None @@ -666,6 +672,7 @@ async def _test_setup_fail(hass, config): patch_key ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) assert state is None @@ -698,6 +705,7 @@ async def test_setup_two_devices(hass): patch_key ], patchers.patch_shell("")[patch_key]: assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() for entity_id in ["media_player.android_tv", "media_player.fire_tv"]: await hass.helpers.entity_component.async_update_entity(entity_id) @@ -714,6 +722,7 @@ async def test_setup_same_device_twice(hass): patch_key ], patchers.patch_shell("")[patch_key]: assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER) + await hass.async_block_till_done() state = hass.states.get(entity_id) assert state is not None @@ -723,6 +732,7 @@ async def test_setup_same_device_twice(hass): patch_key ], patchers.patch_shell("")[patch_key]: assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER) + await hass.async_block_till_done() async def test_adb_command(hass): @@ -735,6 +745,7 @@ async def test_adb_command(hass): patch_key ], patchers.patch_shell("")[patch_key]: assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER) + await hass.async_block_till_done() with patch( "androidtv.basetv.BaseTV.adb_shell", return_value=response @@ -762,6 +773,7 @@ async def test_adb_command_unicode_decode_error(hass): patch_key ], patchers.patch_shell("")[patch_key]: assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER) + await hass.async_block_till_done() with patch( "androidtv.basetv.BaseTV.adb_shell", @@ -791,6 +803,7 @@ async def test_adb_command_key(hass): patch_key ], patchers.patch_shell("")[patch_key]: assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER) + await hass.async_block_till_done() with patch( "androidtv.basetv.BaseTV.adb_shell", return_value=response @@ -819,6 +832,7 @@ async def test_adb_command_get_properties(hass): patch_key ], patchers.patch_shell("")[patch_key]: assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER) + await hass.async_block_till_done() with patch( "androidtv.androidtv.AndroidTV.get_properties_dict", return_value=response @@ -844,6 +858,7 @@ async def test_update_lock_not_acquired(hass): patch_key ], patchers.patch_shell("")[patch_key]: assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER) + await hass.async_block_till_done() with patchers.patch_shell("")[patch_key]: await hass.helpers.entity_component.async_update_entity(entity_id) @@ -877,6 +892,7 @@ async def test_download(hass): patch_key ], patchers.patch_shell("")[patch_key]: assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER) + await hass.async_block_till_done() # Failed download because path is not whitelisted with patch("androidtv.basetv.BaseTV.adb_pull") as patch_pull: @@ -919,6 +935,7 @@ async def test_upload(hass): patch_key ], patchers.patch_shell("")[patch_key]: assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER) + await hass.async_block_till_done() # Failed upload because path is not whitelisted with patch("androidtv.basetv.BaseTV.adb_push") as patch_push: @@ -959,6 +976,7 @@ async def test_androidtv_volume_set(hass): patch_key ], patchers.patch_shell("")[patch_key]: assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER) + await hass.async_block_till_done() with patch( "androidtv.basetv.BaseTV.set_volume_level", return_value=0.5 @@ -984,6 +1002,7 @@ async def test_get_image(hass, hass_ws_client): patch_key ], patchers.patch_shell("")[patch_key]: assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER) + await hass.async_block_till_done() with patchers.patch_shell("11")[patch_key]: await hass.helpers.entity_component.async_update_entity(entity_id) diff --git a/tests/components/arcam_fmj/conftest.py b/tests/components/arcam_fmj/conftest.py index e515b71468b..386cdf9a2b0 100644 --- a/tests/components/arcam_fmj/conftest.py +++ b/tests/components/arcam_fmj/conftest.py @@ -7,8 +7,9 @@ from homeassistant.components.arcam_fmj import DEVICE_SCHEMA from homeassistant.components.arcam_fmj.const import DOMAIN from homeassistant.components.arcam_fmj.media_player import ArcamFmj from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.setup import async_setup_component -from tests.async_mock import Mock +from tests.async_mock import Mock, patch MOCK_HOST = "127.0.0.1" MOCK_PORT = 1234 @@ -17,7 +18,8 @@ MOCK_TURN_ON = { "data": {"entity_id": "switch.test"}, } MOCK_NAME = "dummy" -MOCK_ENTITY_ID = "media_player.arcam_fmj_1" +MOCK_UUID = "1234" +MOCK_ENTITY_ID = "media_player.arcam_fmj_127_0_0_1_1234_1" MOCK_CONFIG = DEVICE_SCHEMA({CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}) @@ -36,8 +38,8 @@ def client_fixture(): return client -@pytest.fixture(name="state") -def state_fixture(client): +@pytest.fixture(name="state_1") +def state_1_fixture(client): """Get a mocked state.""" state = Mock(State) state.client = client @@ -50,11 +52,49 @@ def state_fixture(client): return state +@pytest.fixture(name="state_2") +def state_2_fixture(client): + """Get a mocked state.""" + state = Mock(State) + state.client = client + state.zn = 2 + state.get_power.return_value = True + state.get_volume.return_value = 0.0 + state.get_source_list.return_value = [] + state.get_incoming_audio_format.return_value = (0, 0) + state.get_mute.return_value = None + return state + + +@pytest.fixture(name="state") +def state_fixture(state_1): + """Get a mocked state.""" + return state_1 + + @pytest.fixture(name="player") def player_fixture(hass, state): """Get standard player.""" - player = ArcamFmj(state, MOCK_NAME, None) + player = ArcamFmj(state, MOCK_UUID, MOCK_NAME, None) player.entity_id = MOCK_ENTITY_ID player.hass = hass player.async_write_ha_state = Mock() return player + + +@pytest.fixture(name="player_setup") +async def player_setup_fixture(hass, config, state_1, state_2, client): + """Get standard player.""" + + def state_mock(cli, zone): + if zone == 1: + return state_1 + if zone == 2: + return state_2 + + with patch("homeassistant.components.arcam_fmj.Client", return_value=client), patch( + "homeassistant.components.arcam_fmj.media_player.State", side_effect=state_mock + ), patch("homeassistant.components.arcam_fmj._run_client", return_value=None): + assert await async_setup_component(hass, "arcam_fmj", config) + await hass.async_block_till_done() + yield MOCK_ENTITY_ID diff --git a/tests/components/arcam_fmj/test_device_trigger.py b/tests/components/arcam_fmj/test_device_trigger.py new file mode 100644 index 00000000000..7d0aca8628e --- /dev/null +++ b/tests/components/arcam_fmj/test_device_trigger.py @@ -0,0 +1,91 @@ +"""The tests for Arcam FMJ Receiver control device triggers.""" +import pytest + +from homeassistant.components.arcam_fmj.const import DOMAIN +import homeassistant.components.automation as automation +from homeassistant.setup import async_setup_component + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_get_device_automations, + async_mock_service, + mock_device_registry, + mock_registry, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock service.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers(hass, device_reg, entity_reg): + """Test we get the expected triggers from a arcam_fmj.""" + config_entry = MockConfigEntry(domain=DOMAIN, data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, "host", 1234)}, + ) + entity_reg.async_get_or_create( + "media_player", DOMAIN, "5678", device_id=device_entry.id + ) + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "turn_on", + "device_id": device_entry.id, + "entity_id": "media_player.arcam_fmj_5678", + }, + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert_lists_same(triggers, expected_triggers) + + +async def test_if_fires_on_turn_on_request(hass, calls, player_setup, state): + """Test for turn_on and turn_off triggers firing.""" + state.get_power.return_value = None + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": player_setup, + "type": "turn_on", + }, + "action": { + "service": "test.automation", + "data_template": {"some": "{{ trigger.entity_id }}"}, + }, + } + ] + }, + ) + + await hass.services.async_call( + "media_player", "turn_on", {"entity_id": player_setup}, blocking=True, + ) + + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == player_setup diff --git a/tests/components/arcam_fmj/test_media_player.py b/tests/components/arcam_fmj/test_media_player.py index 5a73e770129..3d88f337e93 100644 --- a/tests/components/arcam_fmj/test_media_player.py +++ b/tests/components/arcam_fmj/test_media_player.py @@ -7,7 +7,7 @@ import pytest from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC from homeassistant.core import HomeAssistant -from .conftest import MOCK_ENTITY_ID, MOCK_HOST, MOCK_NAME, MOCK_PORT +from .conftest import MOCK_ENTITY_ID, MOCK_HOST, MOCK_NAME, MOCK_PORT, MOCK_UUID from tests.async_mock import ANY, MagicMock, Mock, PropertyMock, patch @@ -25,7 +25,7 @@ async def update(player, force_refresh=False): async def test_properties(player, state): """Test standard properties.""" - assert player.unique_id is None + assert player.unique_id == f"{MOCK_UUID}-1" assert player.device_info == { "identifiers": {("arcam_fmj", MOCK_HOST, MOCK_PORT)}, "model": "FMJ", @@ -54,30 +54,8 @@ async def test_powered_on(player, state): assert data.state == "on" -async def test_supported_features_no_service(player, state): +async def test_supported_features(player, state): """Test support when turn on service exist.""" - state.get_power.return_value = None - data = await update(player) - assert data.attributes["supported_features"] == 68876 - - state.get_power.return_value = False - data = await update(player) - assert data.attributes["supported_features"] == 69004 - - -async def test_supported_features_service(hass, state): - """Test support when turn on service exist.""" - from homeassistant.components.arcam_fmj.media_player import ArcamFmj - - player = ArcamFmj(state, "dummy", MOCK_TURN_ON) - player.hass = hass - player.entity_id = MOCK_ENTITY_ID - - state.get_power.return_value = None - data = await update(player) - assert data.attributes["supported_features"] == 69004 - - state.get_power.return_value = False data = await update(player) assert data.attributes["supported_features"] == 69004 @@ -97,7 +75,7 @@ async def test_turn_on_with_service(hass, state): """Test support when turn on service exist.""" from homeassistant.components.arcam_fmj.media_player import ArcamFmj - player = ArcamFmj(state, "dummy", MOCK_TURN_ON) + player = ArcamFmj(state, MOCK_UUID, "dummy", MOCK_TURN_ON) player.hass = Mock(HomeAssistant) player.entity_id = MOCK_ENTITY_ID with patch( diff --git a/tests/components/atag/__init__.py b/tests/components/atag/__init__.py index b975a8de929..3f2b6468491 100644 --- a/tests/components/atag/__init__.py +++ b/tests/components/atag/__init__.py @@ -1 +1,85 @@ -"""Tests for the Atag component.""" +"""Tests for the Atag integration.""" + +from homeassistant.components.atag import DOMAIN +from homeassistant.const import CONF_EMAIL, CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.test_util.aiohttp import AiohttpClientMocker + +USER_INPUT = { + CONF_HOST: "127.0.0.1", + CONF_EMAIL: "atag@domain.com", + CONF_PORT: 10000, +} +UID = "xxxx-xxxx-xxxx_xx-xx-xxx-xxx" +PAIR_REPLY = {"pair_reply": {"status": {"device_id": UID}, "acc_status": 2}} +UPDATE_REPLY = {"update_reply": {"status": {"device_id": UID}, "acc_status": 2}} +RECEIVE_REPLY = { + "retrieve_reply": { + "status": {"device_id": UID}, + "report": { + "burning_hours": 1000, + "room_temp": 20, + "outside_temp": 15, + "dhw_water_temp": 30, + "ch_water_temp": 40, + "ch_water_pres": 1.8, + "ch_return_temp": 35, + "boiler_status": 0, + "tout_avg": 12, + "details": {"rel_mod_level": 0}, + }, + "control": { + "ch_control_mode": 0, + "ch_mode": 1, + "ch_mode_duration": 0, + "ch_mode_temp": 12, + "dhw_temp_setp": 40, + "dhw_mode": 1, + "dhw_mode_temp": 150, + "weather_status": 8, + }, + "configuration": { + "download_url": "http://firmware.atag-one.com:80/R58", + "temp_unit": 0, + "dhw_max_set": 65, + "dhw_min_set": 40, + }, + "acc_status": 2, + } +} + + +async def init_integration( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + rgbw: bool = False, + skip_setup: bool = False, +) -> MockConfigEntry: + """Set up the Atag integration in Home Assistant.""" + + aioclient_mock.get( + "http://127.0.0.1:10000/retrieve", + json=RECEIVE_REPLY, + headers={"Content-Type": "application/json"}, + ) + aioclient_mock.post( + "http://127.0.0.1:10000/update", + json=UPDATE_REPLY, + headers={"Content-Type": "application/json"}, + ) + aioclient_mock.post( + "http://127.0.0.1:10000/pair", + json=PAIR_REPLY, + headers={"Content-Type": "application/json"}, + ) + + entry = MockConfigEntry(domain=DOMAIN, data=USER_INPUT) + entry.add_to_hass(hass) + + if not skip_setup: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/atag/test_climate.py b/tests/components/atag/test_climate.py new file mode 100644 index 00000000000..0ccb67d0eb5 --- /dev/null +++ b/tests/components/atag/test_climate.py @@ -0,0 +1,108 @@ +"""Tests for the Atag climate platform.""" + +from homeassistant.components.atag import CLIMATE, DOMAIN +from homeassistant.components.climate import ( + ATTR_HVAC_ACTION, + ATTR_HVAC_MODE, + ATTR_PRESET_MODE, + HVAC_MODE_HEAT, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_PRESET_MODE, + SERVICE_SET_TEMPERATURE, +) +from homeassistant.components.climate.const import CURRENT_HVAC_HEAT, PRESET_AWAY +from homeassistant.components.homeassistant import ( + DOMAIN as HA_DOMAIN, + SERVICE_UPDATE_ENTITY, +) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_UNKNOWN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.async_mock import PropertyMock, patch +from tests.components.atag import UID, init_integration +from tests.test_util.aiohttp import AiohttpClientMocker + +CLIMATE_ID = f"{CLIMATE}.{DOMAIN}" + + +async def test_climate( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the creation and values of Atag climate device.""" + with patch("pyatag.entities.Climate.status"): + entry = await init_integration(hass, aioclient_mock) + registry = await hass.helpers.entity_registry.async_get_registry() + + assert registry.async_is_registered(CLIMATE_ID) + entry = registry.async_get(CLIMATE_ID) + assert entry.unique_id == f"{UID}-{CLIMATE}" + assert ( + hass.states.get(CLIMATE_ID).attributes[ATTR_HVAC_ACTION] + == CURRENT_HVAC_HEAT + ) + + +async def test_setting_climate( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test setting the climate device.""" + await init_integration(hass, aioclient_mock) + with patch("pyatag.entities.Climate.set_temp") as mock_set_temp: + await hass.services.async_call( + CLIMATE, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: CLIMATE_ID, ATTR_TEMPERATURE: 15}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_temp.assert_called_once_with(15) + + with patch("pyatag.entities.Climate.set_preset_mode") as mock_set_preset: + await hass.services.async_call( + CLIMATE, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: CLIMATE_ID, ATTR_PRESET_MODE: PRESET_AWAY}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_preset.assert_called_once_with(PRESET_AWAY) + + with patch("pyatag.entities.Climate.set_hvac_mode") as mock_set_hvac: + await hass.services.async_call( + CLIMATE, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: CLIMATE_ID, ATTR_HVAC_MODE: HVAC_MODE_HEAT}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_hvac.assert_called_once_with(HVAC_MODE_HEAT) + + +async def test_incorrect_modes( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, +) -> None: + """Test incorrect values are handled correctly.""" + with patch( + "pyatag.entities.Climate.hvac_mode", + new_callable=PropertyMock(return_value="bug"), + ): + await init_integration(hass, aioclient_mock) + assert hass.states.get(CLIMATE_ID).state == STATE_UNKNOWN + + +async def test_update_service( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the updater service is called.""" + await init_integration(hass, aioclient_mock) + await async_setup_component(hass, HA_DOMAIN, {}) + with patch("pyatag.AtagOne.update") as updater: + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: CLIMATE_ID}, + blocking=True, + ) + await hass.async_block_till_done() + updater.assert_called_once() diff --git a/tests/components/atag/test_config_flow.py b/tests/components/atag/test_config_flow.py index 65583340524..dc675e24eba 100644 --- a/tests/components/atag/test_config_flow.py +++ b/tests/components/atag/test_config_flow.py @@ -1,20 +1,19 @@ """Tests for the Atag config flow.""" -from pyatag import AtagException +from pyatag import errors from homeassistant import config_entries, data_entry_flow from homeassistant.components.atag import DOMAIN -from homeassistant.const import CONF_DEVICE, CONF_EMAIL, CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant from tests.async_mock import PropertyMock, patch -from tests.common import MockConfigEntry - -FIXTURE_USER_INPUT = { - CONF_HOST: "127.0.0.1", - CONF_EMAIL: "test@domain.com", - CONF_PORT: 10000, -} -FIXTURE_COMPLETE_ENTRY = FIXTURE_USER_INPUT.copy() -FIXTURE_COMPLETE_ENTRY[CONF_DEVICE] = "device_identifier" +from tests.components.atag import ( + PAIR_REPLY, + RECEIVE_REPLY, + UID, + USER_INPUT, + init_integration, +) +from tests.test_util.aiohttp import AiohttpClientMocker async def test_show_form(hass): @@ -27,29 +26,31 @@ async def test_show_form(hass): assert result["step_id"] == "user" -async def test_one_config_allowed(hass): +async def test_adding_second_device( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test that only one Atag configuration is allowed.""" - MockConfigEntry(domain="atag", data=FIXTURE_USER_INPUT).add_to_hass(hass) - + await init_integration(hass, aioclient_mock) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=USER_INPUT ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" + with patch( + "pyatag.AtagOne.id", new_callable=PropertyMock(return_value="secondary_device"), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=USER_INPUT + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY async def test_connection_error(hass): """Test we show user form on Atag connection error.""" - - with patch( - "homeassistant.components.atag.config_flow.AtagOne.authorize", - side_effect=AtagException(), - ): + with patch("pyatag.AtagOne.authorize", side_effect=errors.AtagException()): result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_USER}, - data=FIXTURE_USER_INPUT, + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=USER_INPUT, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -57,19 +58,30 @@ async def test_connection_error(hass): assert result["errors"] == {"base": "connection_error"} -async def test_full_flow_implementation(hass): - """Test registering an integration and finishing flow works.""" - with patch("homeassistant.components.atag.AtagOne.authorize",), patch( - "homeassistant.components.atag.AtagOne.update", - ), patch( - "homeassistant.components.atag.AtagOne.id", - new_callable=PropertyMock(return_value="device_identifier"), - ): +async def test_unauthorized(hass): + """Test we show correct form when Unauthorized error is raised.""" + with patch("pyatag.AtagOne.authorize", side_effect=errors.Unauthorized()): result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_USER}, - data=FIXTURE_USER_INPUT, + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == FIXTURE_COMPLETE_ENTRY[CONF_DEVICE] - assert result["data"] == FIXTURE_COMPLETE_ENTRY + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "unauthorized"} + + +async def test_full_flow_implementation( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test registering an integration and finishing flow works.""" + aioclient_mock.get( + "http://127.0.0.1:10000/retrieve", json=RECEIVE_REPLY, + ) + aioclient_mock.post( + "http://127.0.0.1:10000/pair", json=PAIR_REPLY, + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=USER_INPUT, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == UID + assert result["result"].unique_id == UID diff --git a/tests/components/atag/test_init.py b/tests/components/atag/test_init.py new file mode 100644 index 00000000000..0fca4b37c46 --- /dev/null +++ b/tests/components/atag/test_init.py @@ -0,0 +1,39 @@ +"""Tests for the ATAG integration.""" +import aiohttp + +from homeassistant.components.atag import DOMAIN +from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY +from homeassistant.core import HomeAssistant + +from tests.async_mock import patch +from tests.components.atag import init_integration +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_config_entry_not_ready( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test configuration entry not ready on library error.""" + aioclient_mock.get("http://127.0.0.1:10000/retrieve", exc=aiohttp.ClientError) + entry = await init_integration(hass, aioclient_mock) + assert entry.state == ENTRY_STATE_SETUP_RETRY + + +async def test_config_entry_empty_reply( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test configuration entry not ready when library returns False.""" + with patch("pyatag.AtagOne.update", return_value=False): + entry = await init_integration(hass, aioclient_mock) + assert entry.state == ENTRY_STATE_SETUP_RETRY + + +async def test_unload_config_entry( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the ATAG configuration entry unloading.""" + entry = await init_integration(hass, aioclient_mock) + assert hass.data[DOMAIN] + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert not hass.data.get(DOMAIN) diff --git a/tests/components/atag/test_sensors.py b/tests/components/atag/test_sensors.py new file mode 100644 index 00000000000..e7bf4df44e9 --- /dev/null +++ b/tests/components/atag/test_sensors.py @@ -0,0 +1,21 @@ +"""Tests for the Atag sensor platform.""" + +from homeassistant.components.atag.sensor import SENSORS +from homeassistant.core import HomeAssistant + +from tests.components.atag import UID, init_integration +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_sensors( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the creation of ATAG sensors.""" + entry = await init_integration(hass, aioclient_mock) + registry = await hass.helpers.entity_registry.async_get_registry() + + for item in SENSORS: + sensor_id = "_".join(f"sensor.{item}".lower().split()) + assert registry.async_is_registered(sensor_id) + entry = registry.async_get(sensor_id) + assert entry.unique_id in [f"{UID}-{v}" for v in SENSORS.values()] diff --git a/tests/components/atag/test_water_heater.py b/tests/components/atag/test_water_heater.py new file mode 100644 index 00000000000..0d717db70bc --- /dev/null +++ b/tests/components/atag/test_water_heater.py @@ -0,0 +1,41 @@ +"""Tests for the Atag water heater platform.""" + +from homeassistant.components.atag import DOMAIN, WATER_HEATER +from homeassistant.components.water_heater import SERVICE_SET_TEMPERATURE +from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE +from homeassistant.core import HomeAssistant + +from tests.async_mock import patch +from tests.components.atag import UID, init_integration +from tests.test_util.aiohttp import AiohttpClientMocker + +WATER_HEATER_ID = f"{WATER_HEATER}.{DOMAIN}" + + +async def test_water_heater( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the creation of Atag water heater.""" + with patch("pyatag.entities.DHW.status"): + entry = await init_integration(hass, aioclient_mock) + registry = await hass.helpers.entity_registry.async_get_registry() + + assert registry.async_is_registered(WATER_HEATER_ID) + entry = registry.async_get(WATER_HEATER_ID) + assert entry.unique_id == f"{UID}-{WATER_HEATER}" + + +async def test_setting_target_temperature( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test setting the water heater device.""" + await init_integration(hass, aioclient_mock) + with patch("pyatag.entities.DHW.set_temp") as mock_set_temp: + await hass.services.async_call( + WATER_HEATER, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: WATER_HEATER_ID, ATTR_TEMPERATURE: 50}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_temp.assert_called_once_with(50) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 7a082ba1931..d17e55691bc 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -3,17 +3,21 @@ from datetime import timedelta import pytest +from homeassistant.components import logbook import homeassistant.components.automation as automation -from homeassistant.components.automation import DOMAIN +from homeassistant.components.automation import ( + DOMAIN, + EVENT_AUTOMATION_RELOADED, + EVENT_AUTOMATION_TRIGGERED, +) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_NAME, - EVENT_AUTOMATION_TRIGGERED, EVENT_HOMEASSISTANT_STARTED, STATE_OFF, STATE_ON, ) -from homeassistant.core import Context, CoreState, State +from homeassistant.core import Context, CoreState, Event, State from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -483,6 +487,11 @@ async def test_reload_config_service(hass, calls, hass_admin_user, hass_read_onl assert len(calls) == 1 assert calls[0].data.get("event") == "test_event" + test_reload_event = [] + hass.bus.async_listen( + EVENT_AUTOMATION_RELOADED, lambda event: test_reload_event.append(event) + ) + with patch( "homeassistant.config.load_yaml_config_file", autospec=True, @@ -505,6 +514,8 @@ async def test_reload_config_service(hass, calls, hass_admin_user, hass_read_onl # De-flake ?! await hass.async_block_till_done() + assert len(test_reload_event) == 1 + assert hass.states.get("automation.hello") is None assert hass.states.get("automation.bye") is not None listeners = hass.bus.async_listeners() @@ -1023,3 +1034,34 @@ async def test_extraction_functions(hass): "device-in-both", "device-in-last", } + + +async def test_logbook_humanify_automation_triggered_event(hass): + """Test humanifying Automation Trigger event.""" + await async_setup_component(hass, automation.DOMAIN, {}) + + event1, event2 = list( + logbook.humanify( + hass, + [ + Event( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_ENTITY_ID: "automation.hello", ATTR_NAME: "Hello Automation"}, + ), + Event( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_ENTITY_ID: "automation.bye", ATTR_NAME: "Bye Automation"}, + ), + ], + ) + ) + + assert event1["name"] == "Hello Automation" + assert event1["domain"] == "automation" + assert event1["message"] == "has been triggered" + assert event1["entity_id"] == "automation.hello" + + assert event2["name"] == "Bye Automation" + assert event2["domain"] == "automation" + assert event2["message"] == "has been triggered" + assert event2["entity_id"] == "automation.bye" diff --git a/tests/components/axis/test_binary_sensor.py b/tests/components/axis/test_binary_sensor.py index d70d55e0d1e..1ffe37ef857 100644 --- a/tests/components/axis/test_binary_sensor.py +++ b/tests/components/axis/test_binary_sensor.py @@ -1,7 +1,10 @@ """Axis binary sensor platform tests.""" -from homeassistant.components import axis -import homeassistant.components.binary_sensor as binary_sensor +from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOTION, + DOMAIN as BINARY_SENSOR_DOMAIN, +) from homeassistant.setup import async_setup_component from .test_device import NAME, setup_axis_integration @@ -28,19 +31,21 @@ async def test_platform_manually_configured(hass): """Test that nothing happens when platform is manually configured.""" assert ( await async_setup_component( - hass, binary_sensor.DOMAIN, {"binary_sensor": {"platform": axis.DOMAIN}} + hass, + BINARY_SENSOR_DOMAIN, + {BINARY_SENSOR_DOMAIN: {"platform": AXIS_DOMAIN}}, ) is True ) - assert axis.DOMAIN not in hass.data + assert AXIS_DOMAIN not in hass.data async def test_no_binary_sensors(hass): """Test that no sensors in Axis results in no sensor entities.""" await setup_axis_integration(hass) - assert not hass.states.async_entity_ids("binary_sensor") + assert not hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN) async def test_binary_sensors(hass): @@ -48,15 +53,17 @@ async def test_binary_sensors(hass): device = await setup_axis_integration(hass) for event in EVENTS: - device.api.stream.event.manage_event(event) + device.api.event.process_event(event) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids("binary_sensor")) == 2 + assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 2 pir = hass.states.get(f"binary_sensor.{NAME}_pir_0") assert pir.state == "off" assert pir.name == f"{NAME} PIR 0" + assert pir.attributes["device_class"] == DEVICE_CLASS_MOTION vmd4 = hass.states.get(f"binary_sensor.{NAME}_vmd4_camera1profile1") assert vmd4.state == "on" assert vmd4.name == f"{NAME} VMD4 Camera1Profile1" + assert vmd4.attributes["device_class"] == DEVICE_CLASS_MOTION diff --git a/tests/components/axis/test_camera.py b/tests/components/axis/test_camera.py index 5cbc5e993ca..6db8de0a0a8 100644 --- a/tests/components/axis/test_camera.py +++ b/tests/components/axis/test_camera.py @@ -1,30 +1,76 @@ """Axis camera platform tests.""" -from homeassistant.components import axis -import homeassistant.components.camera as camera +from homeassistant.components import camera +from homeassistant.components.axis.const import ( + CONF_CAMERA, + CONF_STREAM_PROFILE, + DOMAIN as AXIS_DOMAIN, +) +from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN from homeassistant.setup import async_setup_component -from .test_device import NAME, setup_axis_integration +from .test_device import ENTRY_OPTIONS, NAME, setup_axis_integration + +from tests.async_mock import patch async def test_platform_manually_configured(hass): """Test that nothing happens when platform is manually configured.""" assert ( await async_setup_component( - hass, camera.DOMAIN, {"camera": {"platform": axis.DOMAIN}} + hass, CAMERA_DOMAIN, {"camera": {"platform": AXIS_DOMAIN}} ) is True ) - assert axis.DOMAIN not in hass.data + assert AXIS_DOMAIN not in hass.data async def test_camera(hass): """Test that Axis camera platform is loaded properly.""" await setup_axis_integration(hass) - assert len(hass.states.async_entity_ids("camera")) == 1 + assert len(hass.states.async_entity_ids(CAMERA_DOMAIN)) == 1 cam = hass.states.get(f"camera.{NAME}") assert cam.state == "idle" assert cam.name == NAME + + camera_entity = camera._get_camera_from_entity_id(hass, f"camera.{NAME}") + assert camera_entity.image_source == "http://1.2.3.4:80/axis-cgi/jpg/image.cgi" + assert camera_entity.mjpeg_source == "http://1.2.3.4:80/axis-cgi/mjpg/video.cgi" + assert ( + await camera_entity.stream_source() + == "rtsp://root:pass@1.2.3.4/axis-media/media.amp?videocodec=h264" + ) + + +async def test_camera_with_stream_profile(hass): + """Test that Axis camera entity is using the correct path with stream profike.""" + with patch.dict(ENTRY_OPTIONS, {CONF_STREAM_PROFILE: "profile_1"}): + await setup_axis_integration(hass) + + assert len(hass.states.async_entity_ids(CAMERA_DOMAIN)) == 1 + + cam = hass.states.get(f"camera.{NAME}") + assert cam.state == "idle" + assert cam.name == NAME + + camera_entity = camera._get_camera_from_entity_id(hass, f"camera.{NAME}") + assert camera_entity.image_source == "http://1.2.3.4:80/axis-cgi/jpg/image.cgi" + assert ( + camera_entity.mjpeg_source + == "http://1.2.3.4:80/axis-cgi/mjpg/video.cgi?&streamprofile=profile_1" + ) + assert ( + await camera_entity.stream_source() + == "rtsp://root:pass@1.2.3.4/axis-media/media.amp?videocodec=h264&streamprofile=profile_1" + ) + + +async def test_camera_disabled(hass): + """Test that Axis camera platform is loaded properly but does not create camera entity.""" + with patch.dict(ENTRY_OPTIONS, {CONF_CAMERA: False}): + await setup_axis_integration(hass) + + assert len(hass.states.async_entity_ids(CAMERA_DOMAIN)) == 0 diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index d3972332c89..941961f623a 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -1,65 +1,59 @@ """Test Axis config flow.""" -from homeassistant.components import axis +from homeassistant import data_entry_flow from homeassistant.components.axis import config_flow +from homeassistant.components.axis.const import ( + CONF_CAMERA, + CONF_EVENTS, + CONF_MODEL, + CONF_STREAM_PROFILE, + DEFAULT_STREAM_PROFILE, + DOMAIN as AXIS_DOMAIN, +) +from homeassistant.const import ( + CONF_HOST, + CONF_MAC, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, +) -from .test_device import MAC, MODEL, NAME, setup_axis_integration +from .test_device import MAC, MODEL, NAME, setup_axis_integration, vapix_session_request -from tests.async_mock import Mock, patch +from tests.async_mock import patch from tests.common import MockConfigEntry -def setup_mock_axis_device(mock_device): - """Prepare mock axis device.""" - - def mock_constructor(loop, host, username, password, port, web_proto): - """Fake the controller constructor.""" - mock_device.loop = loop - mock_device.host = host - mock_device.username = username - mock_device.password = password - mock_device.port = port - return mock_device - - mock_device.side_effect = mock_constructor - mock_device.vapix.params.system_serialnumber = MAC - mock_device.vapix.params.prodnbr = "prodnbr" - mock_device.vapix.params.prodtype = "prodtype" - mock_device.vapix.params.firmware_version = "firmware_version" - - async def test_flow_manual_configuration(hass): """Test that config flow works.""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} + AXIS_DOMAIN, context={"source": "user"} ) assert result["type"] == "form" assert result["step_id"] == "user" - with patch("axis.AxisDevice") as mock_device: - - setup_mock_axis_device(mock_device) - + with patch("axis.vapix.session_request", new=vapix_session_request): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ - config_flow.CONF_HOST: "1.2.3.4", - config_flow.CONF_USERNAME: "user", - config_flow.CONF_PASSWORD: "pass", - config_flow.CONF_PORT: 80, + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "user", + CONF_PASSWORD: "pass", + CONF_PORT: 80, }, ) assert result["type"] == "create_entry" - assert result["title"] == f"prodnbr - {MAC}" + assert result["title"] == f"M1065-LW - {MAC}" assert result["data"] == { - config_flow.CONF_HOST: "1.2.3.4", - config_flow.CONF_USERNAME: "user", - config_flow.CONF_PASSWORD: "pass", - config_flow.CONF_PORT: 80, - config_flow.CONF_MAC: MAC, - config_flow.CONF_MODEL: "prodnbr", - config_flow.CONF_NAME: "prodnbr 0", + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "user", + CONF_PASSWORD: "pass", + CONF_PORT: 80, + CONF_MAC: MAC, + CONF_MODEL: "M1065-LW", + CONF_NAME: "M1065-LW 0", } @@ -68,32 +62,26 @@ async def test_manual_configuration_update_configuration(hass): device = await setup_axis_integration(hass) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} + AXIS_DOMAIN, context={"source": "user"} ) assert result["type"] == "form" assert result["step_id"] == "user" - mock_device = Mock() - mock_device.vapix.params.system_serialnumber = MAC - - with patch( - "homeassistant.components.axis.config_flow.get_device", - return_value=mock_device, - ): + with patch("axis.vapix.session_request", new=vapix_session_request): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ - config_flow.CONF_HOST: "2.3.4.5", - config_flow.CONF_USERNAME: "user", - config_flow.CONF_PASSWORD: "pass", - config_flow.CONF_PORT: 80, + CONF_HOST: "2.3.4.5", + CONF_USERNAME: "user", + CONF_PASSWORD: "pass", + CONF_PORT: 80, }, ) assert result["type"] == "abort" assert result["reason"] == "already_configured" - assert device.config_entry.data[config_flow.CONF_HOST] == "2.3.4.5" + assert device.host == "2.3.4.5" async def test_flow_fails_already_configured(hass): @@ -101,26 +89,20 @@ async def test_flow_fails_already_configured(hass): await setup_axis_integration(hass) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} + AXIS_DOMAIN, context={"source": "user"} ) assert result["type"] == "form" assert result["step_id"] == "user" - mock_device = Mock() - mock_device.vapix.params.system_serialnumber = MAC - - with patch( - "homeassistant.components.axis.config_flow.get_device", - return_value=mock_device, - ): + with patch("axis.vapix.session_request", new=vapix_session_request): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ - config_flow.CONF_HOST: "1.2.3.4", - config_flow.CONF_USERNAME: "user", - config_flow.CONF_PASSWORD: "pass", - config_flow.CONF_PORT: 80, + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "user", + CONF_PASSWORD: "pass", + CONF_PORT: 80, }, ) @@ -131,7 +113,7 @@ async def test_flow_fails_already_configured(hass): async def test_flow_fails_faulty_credentials(hass): """Test that config flow fails on faulty credentials.""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} + AXIS_DOMAIN, context={"source": "user"} ) assert result["type"] == "form" @@ -144,10 +126,10 @@ async def test_flow_fails_faulty_credentials(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ - config_flow.CONF_HOST: "1.2.3.4", - config_flow.CONF_USERNAME: "user", - config_flow.CONF_PASSWORD: "pass", - config_flow.CONF_PORT: 80, + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "user", + CONF_PASSWORD: "pass", + CONF_PORT: 80, }, ) @@ -157,7 +139,7 @@ async def test_flow_fails_faulty_credentials(hass): async def test_flow_fails_device_unavailable(hass): """Test that config flow fails on device unavailable.""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} + AXIS_DOMAIN, context={"source": "user"} ) assert result["type"] == "form" @@ -170,10 +152,10 @@ async def test_flow_fails_device_unavailable(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ - config_flow.CONF_HOST: "1.2.3.4", - config_flow.CONF_USERNAME: "user", - config_flow.CONF_PASSWORD: "pass", - config_flow.CONF_PORT: 80, + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "user", + CONF_PASSWORD: "pass", + CONF_PORT: 80, }, ) @@ -183,96 +165,87 @@ async def test_flow_fails_device_unavailable(hass): async def test_flow_create_entry_multiple_existing_entries_of_same_model(hass): """Test that create entry can generate a name with other entries.""" entry = MockConfigEntry( - domain=axis.DOMAIN, - data={config_flow.CONF_NAME: "prodnbr 0", config_flow.CONF_MODEL: "prodnbr"}, + domain=AXIS_DOMAIN, data={CONF_NAME: "M1065-LW 0", CONF_MODEL: "M1065-LW"}, ) entry.add_to_hass(hass) entry2 = MockConfigEntry( - domain=axis.DOMAIN, - data={config_flow.CONF_NAME: "prodnbr 1", config_flow.CONF_MODEL: "prodnbr"}, + domain=AXIS_DOMAIN, data={CONF_NAME: "M1065-LW 1", CONF_MODEL: "M1065-LW"}, ) entry2.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} + AXIS_DOMAIN, context={"source": "user"} ) assert result["type"] == "form" assert result["step_id"] == "user" - with patch("axis.AxisDevice") as mock_device: - - setup_mock_axis_device(mock_device) - + with patch("axis.vapix.session_request", new=vapix_session_request): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ - config_flow.CONF_HOST: "1.2.3.4", - config_flow.CONF_USERNAME: "user", - config_flow.CONF_PASSWORD: "pass", - config_flow.CONF_PORT: 80, + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "user", + CONF_PASSWORD: "pass", + CONF_PORT: 80, }, ) assert result["type"] == "create_entry" - assert result["title"] == f"prodnbr - {MAC}" + assert result["title"] == f"M1065-LW - {MAC}" assert result["data"] == { - config_flow.CONF_HOST: "1.2.3.4", - config_flow.CONF_USERNAME: "user", - config_flow.CONF_PASSWORD: "pass", - config_flow.CONF_PORT: 80, - config_flow.CONF_MAC: MAC, - config_flow.CONF_MODEL: "prodnbr", - config_flow.CONF_NAME: "prodnbr 2", + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "user", + CONF_PASSWORD: "pass", + CONF_PORT: 80, + CONF_MAC: MAC, + CONF_MODEL: "M1065-LW", + CONF_NAME: "M1065-LW 2", } - assert result["data"][config_flow.CONF_NAME] == "prodnbr 2" + assert result["data"][CONF_NAME] == "M1065-LW 2" async def test_zeroconf_flow(hass): """Test that zeroconf discovery for new devices work.""" - with patch.object(axis, "get_device", return_value=Mock()): - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - data={ - config_flow.CONF_HOST: "1.2.3.4", - config_flow.CONF_PORT: 80, - "hostname": "name", - "properties": {"macaddress": MAC}, - }, - context={"source": "zeroconf"}, - ) + result = await hass.config_entries.flow.async_init( + AXIS_DOMAIN, + data={ + CONF_HOST: "1.2.3.4", + CONF_PORT: 80, + "hostname": "name", + "properties": {"macaddress": MAC}, + }, + context={"source": "zeroconf"}, + ) assert result["type"] == "form" assert result["step_id"] == "user" - with patch("axis.AxisDevice") as mock_device: - - setup_mock_axis_device(mock_device) - + with patch("axis.vapix.session_request", new=vapix_session_request): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ - config_flow.CONF_HOST: "1.2.3.4", - config_flow.CONF_USERNAME: "user", - config_flow.CONF_PASSWORD: "pass", - config_flow.CONF_PORT: 80, + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "user", + CONF_PASSWORD: "pass", + CONF_PORT: 80, }, ) assert result["type"] == "create_entry" - assert result["title"] == f"prodnbr - {MAC}" + assert result["title"] == f"M1065-LW - {MAC}" assert result["data"] == { - config_flow.CONF_HOST: "1.2.3.4", - config_flow.CONF_USERNAME: "user", - config_flow.CONF_PASSWORD: "pass", - config_flow.CONF_PORT: 80, - config_flow.CONF_MAC: MAC, - config_flow.CONF_MODEL: "prodnbr", - config_flow.CONF_NAME: "prodnbr 0", + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "user", + CONF_PASSWORD: "pass", + CONF_PORT: 80, + CONF_MAC: MAC, + CONF_MODEL: "M1065-LW", + CONF_NAME: "M1065-LW 0", } - assert result["data"][config_flow.CONF_NAME] == "prodnbr 0" + assert result["data"][CONF_NAME] == "M1065-LW 0" async def test_zeroconf_flow_already_configured(hass): @@ -281,10 +254,10 @@ async def test_zeroconf_flow_already_configured(hass): assert device.host == "1.2.3.4" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, + AXIS_DOMAIN, data={ - config_flow.CONF_HOST: "1.2.3.4", - config_flow.CONF_PORT: 80, + CONF_HOST: "1.2.3.4", + CONF_PORT: 80, "hostname": "name", "properties": {"macaddress": MAC}, }, @@ -301,20 +274,20 @@ async def test_zeroconf_flow_updated_configuration(hass): device = await setup_axis_integration(hass) assert device.host == "1.2.3.4" assert device.config_entry.data == { - config_flow.CONF_HOST: "1.2.3.4", - config_flow.CONF_PORT: 80, - config_flow.CONF_USERNAME: "username", - config_flow.CONF_PASSWORD: "password", - config_flow.CONF_MAC: MAC, - config_flow.CONF_MODEL: MODEL, - config_flow.CONF_NAME: NAME, + CONF_HOST: "1.2.3.4", + CONF_PORT: 80, + CONF_USERNAME: "root", + CONF_PASSWORD: "pass", + CONF_MAC: MAC, + CONF_MODEL: MODEL, + CONF_NAME: NAME, } result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, + AXIS_DOMAIN, data={ - config_flow.CONF_HOST: "2.3.4.5", - config_flow.CONF_PORT: 8080, + CONF_HOST: "2.3.4.5", + CONF_PORT: 8080, "hostname": "name", "properties": {"macaddress": MAC}, }, @@ -324,24 +297,21 @@ async def test_zeroconf_flow_updated_configuration(hass): assert result["type"] == "abort" assert result["reason"] == "already_configured" assert device.config_entry.data == { - config_flow.CONF_HOST: "2.3.4.5", - config_flow.CONF_PORT: 8080, - config_flow.CONF_USERNAME: "username", - config_flow.CONF_PASSWORD: "password", - config_flow.CONF_MAC: MAC, - config_flow.CONF_MODEL: MODEL, - config_flow.CONF_NAME: NAME, + CONF_HOST: "2.3.4.5", + CONF_PORT: 8080, + CONF_USERNAME: "root", + CONF_PASSWORD: "pass", + CONF_MAC: MAC, + CONF_MODEL: MODEL, + CONF_NAME: NAME, } async def test_zeroconf_flow_ignore_non_axis_device(hass): """Test that zeroconf doesn't setup devices with link local addresses.""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - data={ - config_flow.CONF_HOST: "169.254.3.4", - "properties": {"macaddress": "01234567890"}, - }, + AXIS_DOMAIN, + data={CONF_HOST: "169.254.3.4", "properties": {"macaddress": "01234567890"}}, context={"source": "zeroconf"}, ) @@ -352,10 +322,38 @@ async def test_zeroconf_flow_ignore_non_axis_device(hass): async def test_zeroconf_flow_ignore_link_local_address(hass): """Test that zeroconf doesn't setup devices with link local addresses.""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - data={config_flow.CONF_HOST: "169.254.3.4", "properties": {"macaddress": MAC}}, + AXIS_DOMAIN, + data={CONF_HOST: "169.254.3.4", "properties": {"macaddress": MAC}}, context={"source": "zeroconf"}, ) assert result["type"] == "abort" assert result["reason"] == "link_local_address" + + +async def test_option_flow(hass): + """Test config flow options.""" + device = await setup_axis_integration(hass) + assert device.option_stream_profile == DEFAULT_STREAM_PROFILE + + result = await hass.config_entries.options.async_init(device.config_entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "configure_stream" + assert set(result["data_schema"].schema[CONF_STREAM_PROFILE].container) == { + DEFAULT_STREAM_PROFILE, + "profile_1", + "profile_2", + } + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_STREAM_PROFILE: "profile_1"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + CONF_CAMERA: True, + CONF_EVENTS: True, + CONF_STREAM_PROFILE: "profile_1", + } + assert device.option_stream_profile == "profile_1" diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index 74b0ab3b992..e4b0a960979 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -1,32 +1,128 @@ """Test Axis device.""" from copy import deepcopy +import json +from unittest import mock import axis as axislib +from axis.api_discovery import URL as API_DISCOVERY_URL +from axis.basic_device_info import URL as BASIC_DEVICE_INFO_URL +from axis.event_stream import OPERATION_INITIALIZED +from axis.mqtt import URL_CLIENT as MQTT_CLIENT_URL +from axis.param_cgi import ( + BRAND as BRAND_URL, + INPUT as INPUT_URL, + IOPORT as IOPORT_URL, + OUTPUT as OUTPUT_URL, + PROPERTIES as PROPERTIES_URL, + STREAM_PROFILES as STREAM_PROFILES_URL, +) +from axis.port_management import URL as PORT_MANAGEMENT_URL import pytest from homeassistant import config_entries from homeassistant.components import axis +from homeassistant.components.axis.const import ( + CONF_CAMERA, + CONF_EVENTS, + CONF_MODEL, + DOMAIN as AXIS_DOMAIN, +) +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.const import ( + CONF_HOST, + CONF_MAC, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, +) from tests.async_mock import Mock, patch -from tests.common import MockConfigEntry +from tests.common import ( + MockConfigEntry, + async_fire_mqtt_message, + async_mock_mqtt_component, +) MAC = "00408C12345" MODEL = "model" NAME = "name" -ENTRY_OPTIONS = {axis.CONF_CAMERA: True, axis.CONF_EVENTS: True} +ENTRY_OPTIONS = {CONF_CAMERA: True, CONF_EVENTS: True} ENTRY_CONFIG = { - axis.CONF_HOST: "1.2.3.4", - axis.CONF_USERNAME: "username", - axis.CONF_PASSWORD: "password", - axis.CONF_PORT: 80, - axis.CONF_MAC: MAC, - axis.device.CONF_MODEL: MODEL, - axis.device.CONF_NAME: NAME, + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "root", + CONF_PASSWORD: "pass", + CONF_PORT: 80, + CONF_MAC: MAC, + CONF_MODEL: MODEL, + CONF_NAME: NAME, } -DEFAULT_BRAND = """root.Brand.Brand=AXIS +API_DISCOVERY_RESPONSE = { + "method": "getApiList", + "apiVersion": "1.0", + "data": { + "apiList": [ + {"id": "api-discovery", "version": "1.0", "name": "API Discovery Service"}, + {"id": "param-cgi", "version": "1.0", "name": "Legacy Parameter Handling"}, + ] + }, +} + +API_DISCOVERY_BASIC_DEVICE_INFO = { + "id": "basic-device-info", + "version": "1.1", + "name": "Basic Device Information", +} +API_DISCOVERY_MQTT = {"id": "mqtt-client", "version": "1.0", "name": "MQTT Client API"} +API_DISCOVERY_PORT_MANAGEMENT = { + "id": "io-port-management", + "version": "1.0", + "name": "IO Port Management", +} + + +BASIC_DEVICE_INFO_RESPONSE = { + "apiVersion": "1.1", + "data": { + "propertyList": { + "ProdNbr": "M1065-LW", + "ProdType": "Network Camera", + "SerialNumber": "00408C12345", + "Version": "9.80.1", + } + }, +} + +MQTT_CLIENT_RESPONSE = { + "apiVersion": "1.0", + "context": "some context", + "method": "getClientStatus", + "data": {"status": {"state": "active", "connectionStatus": "Connected"}}, +} + +PORT_MANAGEMENT_RESPONSE = { + "apiVersion": "1.0", + "method": "getPorts", + "data": { + "numberOfPorts": 1, + "items": [ + { + "port": "0", + "configurable": False, + "usage": "", + "name": "PIR sensor", + "direction": "input", + "state": "open", + "normalState": "open", + } + ], + }, +} + +BRAND_RESPONSE = """root.Brand.Brand=AXIS root.Brand.ProdFullName=AXIS M1065-LW Network Camera root.Brand.ProdNbr=M1065-LW root.Brand.ProdShortName=AXIS M1065-LW @@ -35,7 +131,7 @@ root.Brand.ProdVariant= root.Brand.WebURL=http://www.axis.com """ -DEFAULT_PORTS = """root.Input.NbrOfInputs=1 +PORTS_RESPONSE = """root.Input.NbrOfInputs=1 root.IOPort.I0.Configurable=no root.IOPort.I0.Direction=input root.IOPort.I0.Input.Name=PIR sensor @@ -43,7 +139,7 @@ root.IOPort.I0.Input.Trig=closed root.Output.NbrOfOutputs=0 """ -DEFAULT_PROPERTIES = """root.Properties.API.HTTP.Version=3 +PROPERTIES_RESPONSE = """root.Properties.API.HTTP.Version=3 root.Properties.API.Metadata.Metadata=yes root.Properties.API.Metadata.Version=1.0 root.Properties.Firmware.BuildDate=Feb 15 2019 09:42 @@ -56,18 +152,40 @@ root.Properties.Image.Rotation=0,180 root.Properties.System.SerialNumber=00408C12345 """ +STREAM_PROFILES_RESPONSE = """root.StreamProfile.MaxGroups=26 +root.StreamProfile.S0.Description=profile_1_description +root.StreamProfile.S0.Name=profile_1 +root.StreamProfile.S0.Parameters=videocodec=h264 +root.StreamProfile.S1.Description=profile_2_description +root.StreamProfile.S1.Name=profile_2 +root.StreamProfile.S1.Parameters=videocodec=h265 +""" -async def setup_axis_integration( - hass, - config=ENTRY_CONFIG, - options=ENTRY_OPTIONS, - brand=DEFAULT_BRAND, - ports=DEFAULT_PORTS, - properties=DEFAULT_PROPERTIES, -): + +def vapix_session_request(session, url, **kwargs): + """Return data based on url.""" + if API_DISCOVERY_URL in url: + return json.dumps(API_DISCOVERY_RESPONSE) + if BASIC_DEVICE_INFO_URL in url: + return json.dumps(BASIC_DEVICE_INFO_RESPONSE) + if MQTT_CLIENT_URL in url: + return json.dumps(MQTT_CLIENT_RESPONSE) + if PORT_MANAGEMENT_URL in url: + return json.dumps(PORT_MANAGEMENT_RESPONSE) + if BRAND_URL in url: + return BRAND_RESPONSE + if IOPORT_URL in url or INPUT_URL in url or OUTPUT_URL in url: + return PORTS_RESPONSE + if PROPERTIES_URL in url: + return PROPERTIES_RESPONSE + if STREAM_PROFILES_URL in url: + return STREAM_PROFILES_RESPONSE + + +async def setup_axis_integration(hass, config=ENTRY_CONFIG, options=ENTRY_OPTIONS): """Create the Axis device.""" config_entry = MockConfigEntry( - domain=axis.DOMAIN, + domain=AXIS_DOMAIN, data=deepcopy(config), connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, options=deepcopy(options), @@ -76,26 +194,13 @@ async def setup_axis_integration( ) config_entry.add_to_hass(hass) - def mock_update_brand(self): - self.process_raw(brand) - - def mock_update_ports(self): - self.process_raw(ports) - - def mock_update_properties(self): - self.process_raw(properties) - - with patch("axis.param_cgi.Brand.update_brand", new=mock_update_brand), patch( - "axis.param_cgi.Ports.update_ports", new=mock_update_ports - ), patch( - "axis.param_cgi.Properties.update_properties", new=mock_update_properties - ), patch( - "axis.AxisDevice.start", return_value=True + with patch("axis.vapix.session_request", new=vapix_session_request), patch( + "axis.rtsp.RTSPClient.start", return_value=True, ): await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + await hass.async_block_till_done() - return hass.data[axis.DOMAIN].get(config[axis.CONF_MAC]) + return hass.data[AXIS_DOMAIN].get(config_entry.unique_id) async def test_device_setup(hass): @@ -106,17 +211,61 @@ async def test_device_setup(hass): ) as forward_entry_setup: device = await setup_axis_integration(hass) - entry = device.config_entry + assert device.api.vapix.firmware_version == "9.10.1" + assert device.api.vapix.product_number == "M1065-LW" + assert device.api.vapix.product_type == "Network Camera" + assert device.api.vapix.serial_number == "00408C12345" + + entry = device.config_entry assert len(forward_entry_setup.mock_calls) == 3 - assert forward_entry_setup.mock_calls[0][1] == (entry, "camera") - assert forward_entry_setup.mock_calls[1][1] == (entry, "binary_sensor") + assert forward_entry_setup.mock_calls[0][1] == (entry, "binary_sensor") + assert forward_entry_setup.mock_calls[1][1] == (entry, "camera") assert forward_entry_setup.mock_calls[2][1] == (entry, "switch") - assert device.host == ENTRY_CONFIG[axis.CONF_HOST] - assert device.model == ENTRY_CONFIG[axis.device.CONF_MODEL] - assert device.name == ENTRY_CONFIG[axis.device.CONF_NAME] - assert device.serial == ENTRY_CONFIG[axis.CONF_MAC] + assert device.host == ENTRY_CONFIG[CONF_HOST] + assert device.model == ENTRY_CONFIG[CONF_MODEL] + assert device.name == ENTRY_CONFIG[CONF_NAME] + assert device.serial == ENTRY_CONFIG[CONF_MAC] + + +async def test_device_info(hass): + """Verify other path of device information works.""" + api_discovery = deepcopy(API_DISCOVERY_RESPONSE) + api_discovery["data"]["apiList"].append(API_DISCOVERY_BASIC_DEVICE_INFO) + + with patch.dict(API_DISCOVERY_RESPONSE, api_discovery): + device = await setup_axis_integration(hass) + + assert device.api.vapix.firmware_version == "9.80.1" + assert device.api.vapix.product_number == "M1065-LW" + assert device.api.vapix.product_type == "Network Camera" + assert device.api.vapix.serial_number == "00408C12345" + + +async def test_device_support_mqtt(hass): + """Successful setup.""" + api_discovery = deepcopy(API_DISCOVERY_RESPONSE) + api_discovery["data"]["apiList"].append(API_DISCOVERY_MQTT) + + mock_mqtt = await async_mock_mqtt_component(hass) + + with patch.dict(API_DISCOVERY_RESPONSE, api_discovery): + await setup_axis_integration(hass) + + mock_mqtt.async_subscribe.assert_called_with(f"{MAC}/#", mock.ANY, 0, "utf-8") + + topic = f"{MAC}/event/tns:onvif/Device/tns:axis/Sensor/PIR/$source/sensor/0" + message = b'{"timestamp": 1590258472044, "topic": "onvif:Device/axis:Sensor/PIR", "message": {"source": {"sensor": "0"}, "key": {}, "data": {"state": "1"}}}' + + assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 0 + async_fire_mqtt_message(hass, topic, message) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 1 + + pir = hass.states.get(f"binary_sensor.{NAME}_pir_0") + assert pir.state == "on" + assert pir.name == f"{NAME} PIR 0" async def test_update_address(hass): @@ -125,7 +274,7 @@ async def test_update_address(hass): assert device.api.config.host == "1.2.3.4" await hass.config_entries.flow.async_init( - axis.DOMAIN, + AXIS_DOMAIN, data={ "host": "2.3.4.5", "port": 80, @@ -157,14 +306,14 @@ async def test_device_not_accessible(hass): """Failed setup schedules a retry of setup.""" with patch.object(axis.device, "get_device", side_effect=axis.errors.CannotConnect): await setup_axis_integration(hass) - assert hass.data[axis.DOMAIN] == {} + assert hass.data[AXIS_DOMAIN] == {} async def test_device_unknown_error(hass): """Unknown errors are handled.""" with patch.object(axis.device, "get_device", side_effect=Exception): await setup_axis_integration(hass) - assert hass.data[axis.DOMAIN] == {} + assert hass.data[AXIS_DOMAIN] == {} async def test_new_event_sends_signal(hass): @@ -175,7 +324,7 @@ async def test_new_event_sends_signal(hass): axis_device = axis.device.AxisNetworkDevice(hass, entry) with patch.object(axis.device, "async_dispatcher_send") as mock_dispatch_send: - axis_device.async_event_callback(action="add", event_id="event") + axis_device.async_event_callback(action=OPERATION_INITIALIZED, event_id="event") await hass.async_block_till_done() assert len(mock_dispatch_send.mock_calls) == 1 @@ -193,13 +342,13 @@ async def test_shutdown(): axis_device.shutdown(None) - assert len(axis_device.api.stop.mock_calls) == 1 + assert len(axis_device.api.stream.stop.mock_calls) == 1 async def test_get_device_fails(hass): """Device unauthorized yields authentication required error.""" with patch( - "axis.param_cgi.Params.update_brand", side_effect=axislib.Unauthorized + "axis.vapix.session_request", side_effect=axislib.Unauthorized ), pytest.raises(axis.errors.AuthenticationRequired): await axis.device.get_device(hass, host="", port="", username="", password="") @@ -207,7 +356,7 @@ async def test_get_device_fails(hass): async def test_get_device_device_unavailable(hass): """Device unavailable yields cannot connect error.""" with patch( - "axis.param_cgi.Params.update_brand", side_effect=axislib.RequestError + "axis.vapix.session_request", side_effect=axislib.RequestError ), pytest.raises(axis.errors.CannotConnect): await axis.device.get_device(hass, host="", port="", username="", password="") @@ -215,6 +364,6 @@ async def test_get_device_device_unavailable(hass): async def test_get_device_unknown_error(hass): """Device yield unknown error.""" with patch( - "axis.param_cgi.Params.update_brand", side_effect=axislib.AxisException + "axis.vapix.session_request", side_effect=axislib.AxisException ), pytest.raises(axis.errors.AuthenticationRequired): await axis.device.get_device(hass, host="", port="", username="", password="") diff --git a/tests/components/axis/test_init.py b/tests/components/axis/test_init.py index b8baf18a67d..3feee94267a 100644 --- a/tests/components/axis/test_init.py +++ b/tests/components/axis/test_init.py @@ -1,5 +1,15 @@ """Test Axis component setup process.""" from homeassistant.components import axis +from homeassistant.components.axis.const import CONF_MODEL, DOMAIN as AXIS_DOMAIN +from homeassistant.const import ( + CONF_DEVICE, + CONF_HOST, + CONF_MAC, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, +) from homeassistant.setup import async_setup_component from .test_device import MAC, setup_axis_integration @@ -10,21 +20,21 @@ from tests.common import MockConfigEntry async def test_setup_no_config(hass): """Test setup without configuration.""" - assert await async_setup_component(hass, axis.DOMAIN, {}) - assert axis.DOMAIN not in hass.data + assert await async_setup_component(hass, AXIS_DOMAIN, {}) + assert AXIS_DOMAIN not in hass.data async def test_setup_entry(hass): """Test successful setup of entry.""" await setup_axis_integration(hass) - assert len(hass.data[axis.DOMAIN]) == 1 - assert MAC in hass.data[axis.DOMAIN] + assert len(hass.data[AXIS_DOMAIN]) == 1 + assert MAC in hass.data[AXIS_DOMAIN] async def test_setup_entry_fails(hass): """Test successful setup of entry.""" config_entry = MockConfigEntry( - domain=axis.DOMAIN, data={axis.CONF_MAC: "0123"}, version=2 + domain=AXIS_DOMAIN, data={CONF_MAC: "0123"}, version=2 ) config_entry.add_to_hass(hass) @@ -36,43 +46,32 @@ async def test_setup_entry_fails(hass): assert not await hass.config_entries.async_setup(config_entry.entry_id) - assert not hass.data[axis.DOMAIN] + assert not hass.data[AXIS_DOMAIN] async def test_unload_entry(hass): """Test successful unload of entry.""" device = await setup_axis_integration(hass) - assert hass.data[axis.DOMAIN] + assert hass.data[AXIS_DOMAIN] assert await hass.config_entries.async_unload(device.config_entry.entry_id) - assert not hass.data[axis.DOMAIN] - - -async def test_populate_options(hass): - """Test successful populate options.""" - device = await setup_axis_integration(hass, options=None) - - assert device.config_entry.options == { - axis.CONF_CAMERA: True, - axis.CONF_EVENTS: True, - axis.CONF_TRIGGER_TIME: axis.DEFAULT_TRIGGER_TIME, - } + assert not hass.data[AXIS_DOMAIN] async def test_migrate_entry(hass): """Test successful migration of entry data.""" legacy_config = { - axis.CONF_DEVICE: { - axis.CONF_HOST: "1.2.3.4", - axis.CONF_USERNAME: "username", - axis.CONF_PASSWORD: "password", - axis.CONF_PORT: 80, + CONF_DEVICE: { + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + CONF_PORT: 80, }, - axis.CONF_MAC: "mac", - axis.device.CONF_MODEL: "model", - axis.device.CONF_NAME: "name", + CONF_MAC: "mac", + CONF_MODEL: "model", + CONF_NAME: "name", } - entry = MockConfigEntry(domain=axis.DOMAIN, data=legacy_config) + entry = MockConfigEntry(domain=AXIS_DOMAIN, data=legacy_config) assert entry.data == legacy_config assert entry.version == 1 @@ -80,18 +79,18 @@ async def test_migrate_entry(hass): await entry.async_migrate(hass) assert entry.data == { - axis.CONF_DEVICE: { - axis.CONF_HOST: "1.2.3.4", - axis.CONF_USERNAME: "username", - axis.CONF_PASSWORD: "password", - axis.CONF_PORT: 80, + CONF_DEVICE: { + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + CONF_PORT: 80, }, - axis.CONF_HOST: "1.2.3.4", - axis.CONF_USERNAME: "username", - axis.CONF_PASSWORD: "password", - axis.CONF_PORT: 80, - axis.CONF_MAC: "mac", - axis.device.CONF_MODEL: "model", - axis.device.CONF_NAME: "name", + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + CONF_PORT: 80, + CONF_MAC: "mac", + CONF_MODEL: "model", + CONF_NAME: "name", } assert entry.version == 2 diff --git a/tests/components/axis/test_switch.py b/tests/components/axis/test_switch.py index d8d69265f3a..f00c17784d2 100644 --- a/tests/components/axis/test_switch.py +++ b/tests/components/axis/test_switch.py @@ -1,12 +1,19 @@ """Axis switch platform tests.""" -from homeassistant.components import axis -import homeassistant.components.switch as switch +from copy import deepcopy + +from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.setup import async_setup_component -from .test_device import NAME, setup_axis_integration +from .test_device import ( + API_DISCOVERY_PORT_MANAGEMENT, + API_DISCOVERY_RESPONSE, + NAME, + setup_axis_integration, +) -from tests.async_mock import Mock, call as mock_call +from tests.async_mock import Mock, patch EVENTS = [ { @@ -31,21 +38,21 @@ EVENTS = [ async def test_platform_manually_configured(hass): """Test that nothing happens when platform is manually configured.""" assert await async_setup_component( - hass, switch.DOMAIN, {"switch": {"platform": axis.DOMAIN}} + hass, SWITCH_DOMAIN, {SWITCH_DOMAIN: {"platform": AXIS_DOMAIN}} ) - assert axis.DOMAIN not in hass.data + assert AXIS_DOMAIN not in hass.data async def test_no_switches(hass): """Test that no output events in Axis results in no switch entities.""" await setup_axis_integration(hass) - assert not hass.states.async_entity_ids("switch") + assert not hass.states.async_entity_ids(SWITCH_DOMAIN) -async def test_switches(hass): - """Test that switches are loaded properly.""" +async def test_switches_with_port_cgi(hass): + """Test that switches are loaded properly using port.cgi.""" device = await setup_axis_integration(hass) device.api.vapix.ports = {"0": Mock(), "1": Mock()} @@ -53,10 +60,10 @@ async def test_switches(hass): device.api.vapix.ports["1"].name = "" for event in EVENTS: - device.api.stream.event.manage_event(event) + device.api.event.process_event(event) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids("switch")) == 2 + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 relay_0 = hass.states.get(f"switch.{NAME}_doorbell") assert relay_0.state == "off" @@ -66,17 +73,61 @@ async def test_switches(hass): assert relay_1.state == "on" assert relay_1.name == f"{NAME} Relay 1" - device.api.vapix.ports["0"].action = Mock() + await hass.services.async_call( + SWITCH_DOMAIN, + "turn_on", + {"entity_id": f"switch.{NAME}_doorbell"}, + blocking=True, + ) + device.api.vapix.ports["0"].close.assert_called_once() await hass.services.async_call( - "switch", "turn_on", {"entity_id": f"switch.{NAME}_doorbell"}, blocking=True + SWITCH_DOMAIN, + "turn_off", + {"entity_id": f"switch.{NAME}_doorbell"}, + blocking=True, ) + device.api.vapix.ports["0"].open.assert_called_once() + + +async def test_switches_with_port_management(hass): + """Test that switches are loaded properly using port management.""" + api_discovery = deepcopy(API_DISCOVERY_RESPONSE) + api_discovery["data"]["apiList"].append(API_DISCOVERY_PORT_MANAGEMENT) + + with patch.dict(API_DISCOVERY_RESPONSE, api_discovery): + device = await setup_axis_integration(hass) + + device.api.vapix.ports = {"0": Mock(), "1": Mock()} + device.api.vapix.ports["0"].name = "Doorbell" + device.api.vapix.ports["1"].name = "" + + for event in EVENTS: + device.api.event.process_event(event) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 + + relay_0 = hass.states.get(f"switch.{NAME}_doorbell") + assert relay_0.state == "off" + assert relay_0.name == f"{NAME} Doorbell" + + relay_1 = hass.states.get(f"switch.{NAME}_relay_1") + assert relay_1.state == "on" + assert relay_1.name == f"{NAME} Relay 1" await hass.services.async_call( - "switch", "turn_off", {"entity_id": f"switch.{NAME}_doorbell"}, blocking=True + SWITCH_DOMAIN, + "turn_on", + {"entity_id": f"switch.{NAME}_doorbell"}, + blocking=True, ) + device.api.vapix.ports["0"].close.assert_called_once() - assert device.api.vapix.ports["0"].action.call_args_list == [ - mock_call("/"), - mock_call("\\"), - ] + await hass.services.async_call( + SWITCH_DOMAIN, + "turn_off", + {"entity_id": f"switch.{NAME}_doorbell"}, + blocking=True, + ) + device.api.vapix.ports["0"].open.assert_called_once() diff --git a/tests/components/bayesian/test_binary_sensor.py b/tests/components/bayesian/test_binary_sensor.py index 6b13ef58521..7bbb9eeda27 100644 --- a/tests/components/bayesian/test_binary_sensor.py +++ b/tests/components/bayesian/test_binary_sensor.py @@ -45,6 +45,7 @@ class TestBayesianBinarySensor(unittest.TestCase): self.hass.block_till_done() assert setup_component(self.hass, "binary_sensor", config) + self.hass.block_till_done() state = self.hass.states.get("binary_sensor.test_binary") assert state.attributes.get("observations")[0]["prob_given_true"] == 0.8 @@ -75,6 +76,7 @@ class TestBayesianBinarySensor(unittest.TestCase): self.hass.block_till_done() assert setup_component(self.hass, "binary_sensor", config) + self.hass.block_till_done() state = self.hass.states.get("binary_sensor.test_binary") assert state.attributes.get("observations") == [] @@ -107,6 +109,7 @@ class TestBayesianBinarySensor(unittest.TestCase): } assert setup_component(self.hass, "binary_sensor", config) + self.hass.block_till_done() self.hass.states.set("sensor.test_monitored", 4) self.hass.block_till_done() @@ -173,6 +176,7 @@ class TestBayesianBinarySensor(unittest.TestCase): } assert setup_component(self.hass, "binary_sensor", config) + self.hass.block_till_done() self.hass.states.set("sensor.test_monitored", "on") @@ -227,6 +231,7 @@ class TestBayesianBinarySensor(unittest.TestCase): } assert setup_component(self.hass, "binary_sensor", config) + self.hass.block_till_done() self.hass.states.set("sensor.test_monitored", "on") @@ -281,6 +286,7 @@ class TestBayesianBinarySensor(unittest.TestCase): } assert setup_component(self.hass, "binary_sensor", config) + self.hass.block_till_done() self.hass.states.set("sensor.test_monitored", "on") self.hass.block_till_done() @@ -318,6 +324,7 @@ class TestBayesianBinarySensor(unittest.TestCase): } assert setup_component(self.hass, "binary_sensor", config) + self.hass.block_till_done() self.hass.states.set("sensor.test_monitored", "off") @@ -401,6 +408,7 @@ class TestBayesianBinarySensor(unittest.TestCase): } assert setup_component(self.hass, "binary_sensor", config) + self.hass.block_till_done() self.hass.states.set("sensor.test_monitored", "on") self.hass.block_till_done() @@ -452,6 +460,7 @@ class TestBayesianBinarySensor(unittest.TestCase): } assert setup_component(self.hass, "binary_sensor", config) + self.hass.block_till_done() self.hass.states.set("sensor.test_monitored", "on") self.hass.block_till_done() diff --git a/tests/components/binary_sensor/test_device_condition.py b/tests/components/binary_sensor/test_device_condition.py index 968b54b7892..fef12c88f49 100644 --- a/tests/components/binary_sensor/test_device_condition.py +++ b/tests/components/binary_sensor/test_device_condition.py @@ -104,6 +104,7 @@ async def test_if_state(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() sensor1 = platform.ENTITIES["battery"] @@ -180,6 +181,7 @@ async def test_if_fires_on_for_condition(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() sensor1 = platform.ENTITIES["battery"] diff --git a/tests/components/binary_sensor/test_device_trigger.py b/tests/components/binary_sensor/test_device_trigger.py index 6234d464f52..92fbce27bc1 100644 --- a/tests/components/binary_sensor/test_device_trigger.py +++ b/tests/components/binary_sensor/test_device_trigger.py @@ -103,6 +103,7 @@ async def test_if_fires_on_state_change(hass, calls): platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() sensor1 = platform.ENTITIES["battery"] @@ -187,6 +188,7 @@ async def test_if_fires_on_state_change_with_for(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() sensor1 = platform.ENTITIES["battery"] diff --git a/tests/components/blebox/conftest.py b/tests/components/blebox/conftest.py index 69798d01fb8..ece02d50b39 100644 --- a/tests/components/blebox/conftest.py +++ b/tests/components/blebox/conftest.py @@ -71,17 +71,24 @@ def config_fixture(): @pytest.fixture(name="feature") -def feature(request): +def feature_fixture(request): """Return an entity wrapper from given fixture name.""" return request.getfixturevalue(request.param) -async def async_setup_entity(hass, config, entity_id): - """Return a configured entity with the given entity_id.""" +async def async_setup_entities(hass, config, entity_ids): + """Return configured entries with the given entity ids.""" + config_entry = mock_config() config_entry.add_to_hass(hass) assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() entity_registry = await hass.helpers.entity_registry.async_get_registry() - return entity_registry.async_get(entity_id) + return [entity_registry.async_get(entity_id) for entity_id in entity_ids] + + +async def async_setup_entity(hass, config, entity_id): + """Return a configured entry with the given entity_id.""" + + return (await async_setup_entities(hass, config, [entity_id]))[0] diff --git a/tests/components/blebox/test_air_quality.py b/tests/components/blebox/test_air_quality.py new file mode 100644 index 00000000000..3467c94411c --- /dev/null +++ b/tests/components/blebox/test_air_quality.py @@ -0,0 +1,94 @@ +"""Blebox air_quality tests.""" + +import logging + +import blebox_uniapi +import pytest + +from homeassistant.components.air_quality import ATTR_PM_0_1, ATTR_PM_2_5, ATTR_PM_10 +from homeassistant.const import ATTR_ICON, STATE_UNKNOWN + +from .conftest import async_setup_entity, mock_feature + +from tests.async_mock import AsyncMock, PropertyMock + + +@pytest.fixture(name="airsensor") +def airsensor_fixture(): + """Return a default air quality fixture.""" + feature = mock_feature( + "air_qualities", + blebox_uniapi.air_quality.AirQuality, + unique_id="BleBox-airSensor-1afe34db9437-0.air", + full_name="airSensor-0.air", + device_class=None, + pm1=None, + pm2_5=None, + pm10=None, + ) + product = feature.product + type(product).name = PropertyMock(return_value="My air sensor") + type(product).model = PropertyMock(return_value="airSensor") + return (feature, "air_quality.airsensor_0_air") + + +async def test_init(airsensor, hass, config): + """Test airSensor default state.""" + + _, entity_id = airsensor + entry = await async_setup_entity(hass, config, entity_id) + assert entry.unique_id == "BleBox-airSensor-1afe34db9437-0.air" + + state = hass.states.get(entity_id) + assert state.name == "airSensor-0.air" + + assert ATTR_PM_0_1 not in state.attributes + assert ATTR_PM_2_5 not in state.attributes + assert ATTR_PM_10 not in state.attributes + + assert state.attributes[ATTR_ICON] == "mdi:blur" + + assert state.state == STATE_UNKNOWN + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(entry.device_id) + + assert device.name == "My air sensor" + assert device.identifiers == {("blebox", "abcd0123ef5678")} + assert device.manufacturer == "BleBox" + assert device.model == "airSensor" + assert device.sw_version == "1.23" + + +async def test_update(airsensor, hass, config): + """Test air quality sensor state after update.""" + + feature_mock, entity_id = airsensor + + def initial_update(): + feature_mock.pm1 = 49 + feature_mock.pm2_5 = 222 + feature_mock.pm10 = 333 + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + await async_setup_entity(hass, config, entity_id) + + state = hass.states.get(entity_id) + + assert state.attributes[ATTR_PM_0_1] == 49 + assert state.attributes[ATTR_PM_2_5] == 222 + assert state.attributes[ATTR_PM_10] == 333 + + assert state.state == "222" + + +async def test_update_failure(airsensor, hass, config, caplog): + """Test that update failures are logged.""" + + caplog.set_level(logging.ERROR) + + feature_mock, entity_id = airsensor + feature_mock.async_update = AsyncMock(side_effect=blebox_uniapi.error.ClientError) + await async_setup_entity(hass, config, entity_id) + + assert f"Updating '{feature_mock.full_name}' failed: " in caplog.text diff --git a/tests/components/blebox/test_climate.py b/tests/components/blebox/test_climate.py new file mode 100644 index 00000000000..c36c93a7f98 --- /dev/null +++ b/tests/components/blebox/test_climate.py @@ -0,0 +1,260 @@ +"""BleBox climate entities tests.""" + +import logging + +import blebox_uniapi +import pytest + +from homeassistant.components.climate.const import ( + ATTR_CURRENT_TEMPERATURE, + ATTR_HVAC_ACTION, + ATTR_HVAC_MODE, + ATTR_HVAC_MODES, + ATTR_MAX_TEMP, + ATTR_MIN_TEMP, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_SUPPORTED_FEATURES, + ATTR_TEMPERATURE, + STATE_UNKNOWN, +) + +from .conftest import async_setup_entity, mock_feature + +from tests.async_mock import AsyncMock, PropertyMock + + +@pytest.fixture(name="saunabox") +def saunabox_fixture(): + """Return a default climate entity mock.""" + feature = mock_feature( + "climates", + blebox_uniapi.climate.Climate, + unique_id="BleBox-saunaBox-1afe34db9437-thermostat", + full_name="saunaBox-thermostat", + device_class=None, + is_on=None, + desired=None, + current=None, + min_temp=-54.3, + max_temp=124.3, + ) + product = feature.product + type(product).name = PropertyMock(return_value="My sauna") + type(product).model = PropertyMock(return_value="saunaBox") + return (feature, "climate.saunabox_thermostat") + + +async def test_init(saunabox, hass, config): + """Test default state.""" + + _, entity_id = saunabox + entry = await async_setup_entity(hass, config, entity_id) + assert entry.unique_id == "BleBox-saunaBox-1afe34db9437-thermostat" + + state = hass.states.get(entity_id) + assert state.name == "saunaBox-thermostat" + + supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] + assert supported_features & SUPPORT_TARGET_TEMPERATURE + + assert state.attributes[ATTR_HVAC_MODES] == [HVAC_MODE_OFF, HVAC_MODE_HEAT] + + assert ATTR_DEVICE_CLASS not in state.attributes + assert ATTR_HVAC_MODE not in state.attributes + assert ATTR_HVAC_ACTION not in state.attributes + + assert state.attributes[ATTR_MIN_TEMP] == -54.3 + assert state.attributes[ATTR_MAX_TEMP] == 124.3 + assert state.attributes[ATTR_TEMPERATURE] is None + assert state.attributes[ATTR_CURRENT_TEMPERATURE] is None + + assert state.state == STATE_UNKNOWN + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(entry.device_id) + + assert device.name == "My sauna" + assert device.identifiers == {("blebox", "abcd0123ef5678")} + assert device.manufacturer == "BleBox" + assert device.model == "saunaBox" + assert device.sw_version == "1.23" + + +async def test_update(saunabox, hass, config): + """Test updating.""" + + feature_mock, entity_id = saunabox + + def initial_update(): + feature_mock.is_on = False + feature_mock.desired = 64.3 + feature_mock.current = 40.9 + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + await async_setup_entity(hass, config, entity_id) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + assert state.attributes[ATTR_TEMPERATURE] == 64.3 + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 40.9 + assert state.state == HVAC_MODE_OFF + + +async def test_on_when_below_desired(saunabox, hass, config): + """Test when temperature is below desired.""" + + feature_mock, entity_id = saunabox + + def initial_update(): + feature_mock.is_on = False + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + await async_setup_entity(hass, config, entity_id) + feature_mock.async_update = AsyncMock() + + def turn_on(): + feature_mock.is_on = True + feature_mock.is_heating = True + feature_mock.desired = 64.8 + feature_mock.current = 25.7 + + feature_mock.async_on = AsyncMock(side_effect=turn_on) + await hass.services.async_call( + "climate", + SERVICE_SET_HVAC_MODE, + {"entity_id": entity_id, ATTR_HVAC_MODE: HVAC_MODE_HEAT}, + blocking=True, + ) + feature_mock.async_off.assert_not_called() + state = hass.states.get(entity_id) + + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT + assert state.attributes[ATTR_TEMPERATURE] == 64.8 + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 25.7 + assert state.state == HVAC_MODE_HEAT + + +async def test_on_when_above_desired(saunabox, hass, config): + """Test when temperature is below desired.""" + + feature_mock, entity_id = saunabox + + def initial_update(): + feature_mock.is_on = False + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + await async_setup_entity(hass, config, entity_id) + feature_mock.async_update = AsyncMock() + + def turn_on(): + feature_mock.is_on = True + feature_mock.is_heating = False + feature_mock.desired = 23.4 + feature_mock.current = 28.7 + + feature_mock.async_on = AsyncMock(side_effect=turn_on) + + await hass.services.async_call( + "climate", + SERVICE_SET_HVAC_MODE, + {"entity_id": entity_id, ATTR_HVAC_MODE: HVAC_MODE_HEAT}, + blocking=True, + ) + feature_mock.async_off.assert_not_called() + state = hass.states.get(entity_id) + + assert state.attributes[ATTR_TEMPERATURE] == 23.4 + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 28.7 + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + assert state.state == HVAC_MODE_HEAT + + +async def test_off(saunabox, hass, config): + """Test turning off.""" + + feature_mock, entity_id = saunabox + + def initial_update(): + feature_mock.is_on = True + feature_mock.is_heating = False + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + await async_setup_entity(hass, config, entity_id) + feature_mock.async_update = AsyncMock() + + def turn_off(): + feature_mock.is_on = False + feature_mock.is_heating = False + feature_mock.desired = 29.8 + feature_mock.current = 22.7 + + feature_mock.async_off = AsyncMock(side_effect=turn_off) + await hass.services.async_call( + "climate", + SERVICE_SET_HVAC_MODE, + {"entity_id": entity_id, ATTR_HVAC_MODE: HVAC_MODE_OFF}, + blocking=True, + ) + feature_mock.async_on.assert_not_called() + state = hass.states.get(entity_id) + + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + assert state.attributes[ATTR_TEMPERATURE] == 29.8 + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.7 + assert state.state == HVAC_MODE_OFF + + +async def test_set_thermo(saunabox, hass, config): + """Test setting thermostat.""" + + feature_mock, entity_id = saunabox + + def update(): + feature_mock.is_on = False + feature_mock.is_heating = False + + feature_mock.async_update = AsyncMock(side_effect=update) + await async_setup_entity(hass, config, entity_id) + feature_mock.async_update = AsyncMock() + + def set_temp(temp): + feature_mock.is_on = True + feature_mock.is_heating = True + feature_mock.desired = 29.2 + feature_mock.current = 29.1 + + feature_mock.async_set_temperature = AsyncMock(side_effect=set_temp) + await hass.services.async_call( + "climate", + SERVICE_SET_TEMPERATURE, + {"entity_id": entity_id, ATTR_TEMPERATURE: 43.21}, + blocking=True, + ) + state = hass.states.get(entity_id) + + assert state.attributes[ATTR_TEMPERATURE] == 29.2 + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 29.1 + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT + assert state.state == HVAC_MODE_HEAT + + +async def test_update_failure(saunabox, hass, config, caplog): + """Test that update failures are logged.""" + + caplog.set_level(logging.ERROR) + + feature_mock, entity_id = saunabox + feature_mock.async_update = AsyncMock(side_effect=blebox_uniapi.error.ClientError) + await async_setup_entity(hass, config, entity_id) + + assert f"Updating '{feature_mock.full_name}' failed: " in caplog.text diff --git a/tests/components/blebox/test_config_flow.py b/tests/components/blebox/test_config_flow.py index fe13dfae15e..a7200b05b28 100644 --- a/tests/components/blebox/test_config_flow.py +++ b/tests/components/blebox/test_config_flow.py @@ -36,14 +36,14 @@ def create_valid_feature_mock(path="homeassistant.components.blebox.Products"): return feature -@pytest.fixture -def valid_feature_mock(): +@pytest.fixture(name="valid_feature_mock") +def valid_feature_mock_fixture(): """Return a valid, complete BleBox feature mock.""" return create_valid_feature_mock() -@pytest.fixture -def flow_feature_mock(): +@pytest.fixture(name="flow_feature_mock") +def flow_feature_mock_fixture(): """Return a mocked user flow feature.""" return create_valid_feature_mock( "homeassistant.components.blebox.config_flow.Products" @@ -74,8 +74,8 @@ async def test_flow_works(hass, valid_feature_mock, flow_feature_mock): } -@pytest.fixture -def product_class_mock(): +@pytest.fixture(name="product_class_mock") +def product_class_mock_fixture(): """Return a mocked feature.""" path = "homeassistant.components.blebox.config_flow.Products" patcher = patch(path, DEFAULT, blebox_uniapi.products.Products, True, True) diff --git a/tests/components/blebox/test_light.py b/tests/components/blebox/test_light.py new file mode 100644 index 00000000000..fb0d1302e33 --- /dev/null +++ b/tests/components/blebox/test_light.py @@ -0,0 +1,597 @@ +"""BleBox light entities tests.""" + +import logging + +import blebox_uniapi +import pytest + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_HS_COLOR, + ATTR_WHITE_VALUE, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_WHITE_VALUE, +) +from homeassistant.const import ( + ATTR_SUPPORTED_FEATURES, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.util import color + +from .conftest import async_setup_entity, mock_feature + +from tests.async_mock import AsyncMock, PropertyMock + +ALL_LIGHT_FIXTURES = ["dimmer", "wlightbox_s", "wlightbox"] + + +@pytest.fixture(name="dimmer") +def dimmer_fixture(): + """Return a default light entity mock.""" + + feature = mock_feature( + "lights", + blebox_uniapi.light.Light, + unique_id="BleBox-dimmerBox-1afe34e750b8-brightness", + full_name="dimmerBox-brightness", + device_class=None, + brightness=65, + is_on=True, + supports_color=False, + supports_white=False, + ) + product = feature.product + type(product).name = PropertyMock(return_value="My dimmer") + type(product).model = PropertyMock(return_value="dimmerBox") + return (feature, "light.dimmerbox_brightness") + + +async def test_dimmer_init(dimmer, hass, config): + """Test cover default state.""" + + _, entity_id = dimmer + entry = await async_setup_entity(hass, config, entity_id) + assert entry.unique_id == "BleBox-dimmerBox-1afe34e750b8-brightness" + + state = hass.states.get(entity_id) + assert state.name == "dimmerBox-brightness" + + supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] + assert supported_features & SUPPORT_BRIGHTNESS + + assert state.attributes[ATTR_BRIGHTNESS] == 65 + assert state.state == STATE_ON + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(entry.device_id) + + assert device.name == "My dimmer" + assert device.identifiers == {("blebox", "abcd0123ef5678")} + assert device.manufacturer == "BleBox" + assert device.model == "dimmerBox" + assert device.sw_version == "1.23" + + +async def test_dimmer_update(dimmer, hass, config): + """Test light updating.""" + + feature_mock, entity_id = dimmer + + def initial_update(): + feature_mock.brightness = 53 + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + await async_setup_entity(hass, config, entity_id) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_BRIGHTNESS] == 53 + assert state.state == STATE_ON + + +async def test_dimmer_on(dimmer, hass, config): + """Test light on.""" + + feature_mock, entity_id = dimmer + + def initial_update(): + feature_mock.is_on = False + feature_mock.brightness = 0 # off + feature_mock.sensible_on_value = 254 + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + await async_setup_entity(hass, config, entity_id) + feature_mock.async_update = AsyncMock() + + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + def turn_on(brightness): + assert brightness == 254 + feature_mock.brightness = 254 # on + feature_mock.is_on = True # on + + feature_mock.async_on = AsyncMock(side_effect=turn_on) + await hass.services.async_call( + "light", SERVICE_TURN_ON, {"entity_id": entity_id}, blocking=True, + ) + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 254 + + +async def test_dimmer_on_with_brightness(dimmer, hass, config): + """Test light on with a brightness value.""" + + feature_mock, entity_id = dimmer + + def initial_update(): + feature_mock.is_on = False + feature_mock.brightness = 0 # off + feature_mock.sensible_on_value = 254 + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + await async_setup_entity(hass, config, entity_id) + feature_mock.async_update = AsyncMock() + + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + def turn_on(brightness): + assert brightness == 202 + feature_mock.brightness = 202 # on + feature_mock.is_on = True # on + + feature_mock.async_on = AsyncMock(side_effect=turn_on) + + def apply(value, brightness): + assert value == 254 + return brightness + + feature_mock.apply_brightness = apply + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + {"entity_id": entity_id, ATTR_BRIGHTNESS: 202}, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_BRIGHTNESS] == 202 + assert state.state == STATE_ON + + +async def test_dimmer_off(dimmer, hass, config): + """Test light off.""" + + feature_mock, entity_id = dimmer + + def initial_update(): + feature_mock.is_on = True + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + await async_setup_entity(hass, config, entity_id) + feature_mock.async_update = AsyncMock() + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + def turn_off(): + feature_mock.is_on = False + feature_mock.brightness = 0 # off + + feature_mock.async_off = AsyncMock(side_effect=turn_off) + await hass.services.async_call( + "light", SERVICE_TURN_OFF, {"entity_id": entity_id}, blocking=True, + ) + + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + assert ATTR_BRIGHTNESS not in state.attributes + + +@pytest.fixture(name="wlightbox_s") +def wlightboxs_fixture(): + """Return a default light entity mock.""" + + feature = mock_feature( + "lights", + blebox_uniapi.light.Light, + unique_id="BleBox-wLightBoxS-1afe34e750b8-color", + full_name="wLightBoxS-color", + device_class=None, + brightness=None, + is_on=None, + supports_color=False, + supports_white=False, + ) + product = feature.product + type(product).name = PropertyMock(return_value="My wLightBoxS") + type(product).model = PropertyMock(return_value="wLightBoxS") + return (feature, "light.wlightboxs_color") + + +async def test_wlightbox_s_init(wlightbox_s, hass, config): + """Test cover default state.""" + + _, entity_id = wlightbox_s + entry = await async_setup_entity(hass, config, entity_id) + assert entry.unique_id == "BleBox-wLightBoxS-1afe34e750b8-color" + + state = hass.states.get(entity_id) + assert state.name == "wLightBoxS-color" + + supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] + assert supported_features & SUPPORT_BRIGHTNESS + + assert ATTR_BRIGHTNESS not in state.attributes + assert state.state == STATE_OFF + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(entry.device_id) + + assert device.name == "My wLightBoxS" + assert device.identifiers == {("blebox", "abcd0123ef5678")} + assert device.manufacturer == "BleBox" + assert device.model == "wLightBoxS" + assert device.sw_version == "1.23" + + +async def test_wlightbox_s_update(wlightbox_s, hass, config): + """Test light updating.""" + + feature_mock, entity_id = wlightbox_s + + def initial_update(): + feature_mock.brightness = 0xAB + feature_mock.is_on = True + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + + await async_setup_entity(hass, config, entity_id) + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 0xAB + + +async def test_wlightbox_s_on(wlightbox_s, hass, config): + """Test light on.""" + + feature_mock, entity_id = wlightbox_s + + def initial_update(): + feature_mock.is_on = False + feature_mock.sensible_on_value = 254 + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + await async_setup_entity(hass, config, entity_id) + feature_mock.async_update = AsyncMock() + + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + def turn_on(brightness): + assert brightness == 254 + feature_mock.brightness = 254 # on + feature_mock.is_on = True # on + + feature_mock.async_on = AsyncMock(side_effect=turn_on) + await hass.services.async_call( + "light", SERVICE_TURN_ON, {"entity_id": entity_id}, blocking=True, + ) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_BRIGHTNESS] == 254 + assert state.state == STATE_ON + + +@pytest.fixture(name="wlightbox") +def wlightbox_fixture(): + """Return a default light entity mock.""" + + feature = mock_feature( + "lights", + blebox_uniapi.light.Light, + unique_id="BleBox-wLightBox-1afe34e750b8-color", + full_name="wLightBox-color", + device_class=None, + is_on=None, + supports_color=True, + supports_white=True, + white_value=None, + rgbw_hex=None, + ) + product = feature.product + type(product).name = PropertyMock(return_value="My wLightBox") + type(product).model = PropertyMock(return_value="wLightBox") + return (feature, "light.wlightbox_color") + + +async def test_wlightbox_init(wlightbox, hass, config): + """Test cover default state.""" + + _, entity_id = wlightbox + entry = await async_setup_entity(hass, config, entity_id) + assert entry.unique_id == "BleBox-wLightBox-1afe34e750b8-color" + + state = hass.states.get(entity_id) + assert state.name == "wLightBox-color" + + supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] + assert supported_features & SUPPORT_WHITE_VALUE + assert supported_features & SUPPORT_COLOR + + assert ATTR_WHITE_VALUE not in state.attributes + assert ATTR_HS_COLOR not in state.attributes + assert ATTR_BRIGHTNESS not in state.attributes + assert state.state == STATE_OFF + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(entry.device_id) + + assert device.name == "My wLightBox" + assert device.identifiers == {("blebox", "abcd0123ef5678")} + assert device.manufacturer == "BleBox" + assert device.model == "wLightBox" + assert device.sw_version == "1.23" + + +async def test_wlightbox_update(wlightbox, hass, config): + """Test light updating.""" + + feature_mock, entity_id = wlightbox + + def initial_update(): + feature_mock.is_on = True + feature_mock.rgbw_hex = "fa00203A" + feature_mock.white_value = 0x3A + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + await async_setup_entity(hass, config, entity_id) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_HS_COLOR] == (352.32, 100.0) + assert state.attributes[ATTR_WHITE_VALUE] == 0x3A + assert state.state == STATE_ON + + +async def test_wlightbox_on_via_just_whiteness(wlightbox, hass, config): + """Test light on.""" + + feature_mock, entity_id = wlightbox + + def initial_update(): + feature_mock.is_on = False + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + await async_setup_entity(hass, config, entity_id) + feature_mock.async_update = AsyncMock() + + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + def turn_on(value): + feature_mock.is_on = True + assert value == "f1e2d3c7" + feature_mock.white_value = 0xC7 # on + feature_mock.rgbw_hex = "f1e2d3c7" + + feature_mock.async_on = AsyncMock(side_effect=turn_on) + + def apply_white(value, white): + assert value == "f1e2d305" + assert white == 0xC7 + return "f1e2d3c7" + + feature_mock.apply_white = apply_white + + feature_mock.sensible_on_value = "f1e2d305" + + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + {"entity_id": entity_id, ATTR_WHITE_VALUE: 0xC7}, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes[ATTR_WHITE_VALUE] == 0xC7 + + assert state.attributes[ATTR_HS_COLOR] == color.color_RGB_to_hs(0xF1, 0xE2, 0xD3) + + +async def test_wlightbox_on_via_reset_whiteness(wlightbox, hass, config): + """Test light on.""" + + feature_mock, entity_id = wlightbox + + def initial_update(): + feature_mock.is_on = False + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + await async_setup_entity(hass, config, entity_id) + feature_mock.async_update = AsyncMock() + + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + def turn_on(value): + feature_mock.is_on = True + feature_mock.white_value = 0x0 + assert value == "f1e2d300" + feature_mock.rgbw_hex = "f1e2d300" + + feature_mock.async_on = AsyncMock(side_effect=turn_on) + + def apply_white(value, white): + assert value == "f1e2d305" + assert white == 0x0 + return "f1e2d300" + + feature_mock.apply_white = apply_white + + feature_mock.sensible_on_value = "f1e2d305" + + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + {"entity_id": entity_id, ATTR_WHITE_VALUE: 0x0}, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes[ATTR_WHITE_VALUE] == 0x0 + assert state.attributes[ATTR_HS_COLOR] == color.color_RGB_to_hs(0xF1, 0xE2, 0xD3) + + +async def test_wlightbox_on_via_just_hsl_color(wlightbox, hass, config): + """Test light on.""" + + feature_mock, entity_id = wlightbox + + def initial_update(): + feature_mock.is_on = False + feature_mock.rgbw_hex = "00000000" + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + await async_setup_entity(hass, config, entity_id) + feature_mock.async_update = AsyncMock() + + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + hs_color = color.color_RGB_to_hs(0xFF, 0xA1, 0xB2) + + def turn_on(value): + feature_mock.is_on = True + assert value == "ffa1b2e4" + feature_mock.white_value = 0xE4 + feature_mock.rgbw_hex = value + + feature_mock.async_on = AsyncMock(side_effect=turn_on) + + def apply_color(value, color_value): + assert value == "c1a2e3e4" + assert color_value == "ffa0b1" + return "ffa1b2e4" + + feature_mock.apply_color = apply_color + feature_mock.sensible_on_value = "c1a2e3e4" + + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + {"entity_id": entity_id, ATTR_HS_COLOR: hs_color}, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_HS_COLOR] == hs_color + assert state.attributes[ATTR_WHITE_VALUE] == 0xE4 + assert state.state == STATE_ON + + +async def test_wlightbox_on_to_last_color(wlightbox, hass, config): + """Test light on.""" + + feature_mock, entity_id = wlightbox + + def initial_update(): + feature_mock.is_on = False + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + await async_setup_entity(hass, config, entity_id) + feature_mock.async_update = AsyncMock() + + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + def turn_on(value): + feature_mock.is_on = True + assert value == "f1e2d3e4" + feature_mock.white_value = 0xE4 + feature_mock.rgbw_hex = value + + feature_mock.async_on = AsyncMock(side_effect=turn_on) + feature_mock.sensible_on_value = "f1e2d3e4" + + await hass.services.async_call( + "light", SERVICE_TURN_ON, {"entity_id": entity_id}, blocking=True, + ) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_WHITE_VALUE] == 0xE4 + assert state.attributes[ATTR_HS_COLOR] == color.color_RGB_to_hs(0xF1, 0xE2, 0xD3) + assert state.state == STATE_ON + + +async def test_wlightbox_off(wlightbox, hass, config): + """Test light off.""" + + feature_mock, entity_id = wlightbox + + def initial_update(): + feature_mock.is_on = True + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + await async_setup_entity(hass, config, entity_id) + feature_mock.async_update = AsyncMock() + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + def turn_off(): + feature_mock.is_on = False + feature_mock.white_value = 0x0 + feature_mock.rgbw_hex = "00000000" + + feature_mock.async_off = AsyncMock(side_effect=turn_off) + + await hass.services.async_call( + "light", SERVICE_TURN_OFF, {"entity_id": entity_id}, blocking=True, + ) + + state = hass.states.get(entity_id) + assert ATTR_WHITE_VALUE not in state.attributes + assert ATTR_HS_COLOR not in state.attributes + assert state.state == STATE_OFF + + +@pytest.mark.parametrize("feature", ALL_LIGHT_FIXTURES, indirect=["feature"]) +async def test_update_failure(feature, hass, config, caplog): + """Test that update failures are logged.""" + + caplog.set_level(logging.ERROR) + + feature_mock, entity_id = feature + feature_mock.async_update = AsyncMock(side_effect=blebox_uniapi.error.ClientError) + await async_setup_entity(hass, config, entity_id) + + assert f"Updating '{feature_mock.full_name}' failed: " in caplog.text + + +@pytest.mark.parametrize("feature", ALL_LIGHT_FIXTURES, indirect=["feature"]) +async def test_turn_on_failure(feature, hass, config, caplog): + """Test that turn_on failures are logged.""" + + caplog.set_level(logging.ERROR) + + feature_mock, entity_id = feature + feature_mock.async_on = AsyncMock(side_effect=blebox_uniapi.error.BadOnValueError) + await async_setup_entity(hass, config, entity_id) + + feature_mock.sensible_on_value = 123 + await hass.services.async_call( + "light", SERVICE_TURN_ON, {"entity_id": entity_id}, blocking=True, + ) + + assert ( + f"Turning on '{feature_mock.full_name}' failed: Bad value 123 ()" in caplog.text + ) diff --git a/tests/components/blebox/test_switch.py b/tests/components/blebox/test_switch.py new file mode 100644 index 00000000000..c41273757f2 --- /dev/null +++ b/tests/components/blebox/test_switch.py @@ -0,0 +1,386 @@ +"""Blebox switch tests.""" + +import logging + +import blebox_uniapi +import pytest + +from homeassistant.components.switch import DEVICE_CLASS_SWITCH +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) + +from .conftest import ( + async_setup_entities, + async_setup_entity, + mock_feature, + mock_only_feature, + setup_product_mock, +) + +from tests.async_mock import AsyncMock, PropertyMock + + +@pytest.fixture(name="switchbox") +def switchbox_fixture(): + """Return a default switchBox switch entity mock.""" + feature = mock_feature( + "switches", + blebox_uniapi.switch.Switch, + unique_id="BleBox-switchBox-1afe34e750b8-0.relay", + full_name="switchBox-0.relay", + device_class="relay", + is_on=False, + ) + feature.async_update = AsyncMock() + product = feature.product + type(product).name = PropertyMock(return_value="My switch box") + type(product).model = PropertyMock(return_value="switchBox") + return (feature, "switch.switchbox_0_relay") + + +async def test_switchbox_init(switchbox, hass, config): + """Test switch default state.""" + + feature_mock, entity_id = switchbox + + feature_mock.async_update = AsyncMock() + entry = await async_setup_entity(hass, config, entity_id) + assert entry.unique_id == "BleBox-switchBox-1afe34e750b8-0.relay" + + state = hass.states.get(entity_id) + assert state.name == "switchBox-0.relay" + + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_SWITCH + + assert state.state == STATE_OFF + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(entry.device_id) + + assert device.name == "My switch box" + assert device.identifiers == {("blebox", "abcd0123ef5678")} + assert device.manufacturer == "BleBox" + assert device.model == "switchBox" + assert device.sw_version == "1.23" + + +async def test_switchbox_update_when_off(switchbox, hass, config): + """Test switch updating when off.""" + + feature_mock, entity_id = switchbox + + def initial_update(): + feature_mock.is_on = False + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + await async_setup_entity(hass, config, entity_id) + + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + +async def test_switchbox_update_when_on(switchbox, hass, config): + """Test switch updating when on.""" + + feature_mock, entity_id = switchbox + + def initial_update(): + feature_mock.is_on = True + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + await async_setup_entity(hass, config, entity_id) + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + +async def test_switchbox_on(switchbox, hass, config): + """Test turning switch on.""" + + feature_mock, entity_id = switchbox + + def initial_update(): + feature_mock.is_on = False + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + await async_setup_entity(hass, config, entity_id) + feature_mock.async_update = AsyncMock() + + def turn_on(): + feature_mock.is_on = True + + feature_mock.async_turn_on = AsyncMock(side_effect=turn_on) + + await hass.services.async_call( + "switch", SERVICE_TURN_ON, {"entity_id": entity_id}, blocking=True, + ) + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + +async def test_switchbox_off(switchbox, hass, config): + """Test turning switch off.""" + + feature_mock, entity_id = switchbox + + def initial_update(): + feature_mock.is_on = True + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + await async_setup_entity(hass, config, entity_id) + feature_mock.async_update = AsyncMock() + + def turn_off(): + feature_mock.is_on = False + + feature_mock.async_turn_off = AsyncMock(side_effect=turn_off) + + await hass.services.async_call( + "switch", SERVICE_TURN_OFF, {"entity_id": entity_id}, blocking=True, + ) + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + +def relay_mock(relay_id=0): + """Return a default switchBoxD switch entity mock.""" + + return mock_only_feature( + blebox_uniapi.switch.Switch, + unique_id=f"BleBox-switchBoxD-1afe34e750b8-{relay_id}.relay", + full_name=f"switchBoxD-{relay_id}.relay", + device_class="relay", + is_on=None, + ) + + +@pytest.fixture(name="switchbox_d") +def switchbox_d_fixture(): + """Set up two mocked Switch features representing a switchBoxD.""" + + relay1 = relay_mock(0) + relay2 = relay_mock(1) + features = [relay1, relay2] + + product = setup_product_mock("switches", features) + + type(product).name = PropertyMock(return_value="My relays") + type(product).model = PropertyMock(return_value="switchBoxD") + type(product).brand = PropertyMock(return_value="BleBox") + type(product).firmware_version = PropertyMock(return_value="1.23") + type(product).unique_id = PropertyMock(return_value="abcd0123ef5678") + + type(relay1).product = product + type(relay2).product = product + + return (features, ["switch.switchboxd_0_relay", "switch.switchboxd_1_relay"]) + + +async def test_switchbox_d_init(switchbox_d, hass, config): + """Test switch default state.""" + + feature_mocks, entity_ids = switchbox_d + + feature_mocks[0].async_update = AsyncMock() + feature_mocks[1].async_update = AsyncMock() + entries = await async_setup_entities(hass, config, entity_ids) + + entry = entries[0] + assert entry.unique_id == "BleBox-switchBoxD-1afe34e750b8-0.relay" + + state = hass.states.get(entity_ids[0]) + assert state.name == "switchBoxD-0.relay" + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_SWITCH + assert state.state == STATE_OFF # NOTE: should instead be STATE_UNKNOWN? + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(entry.device_id) + + assert device.name == "My relays" + assert device.identifiers == {("blebox", "abcd0123ef5678")} + assert device.manufacturer == "BleBox" + assert device.model == "switchBoxD" + assert device.sw_version == "1.23" + + entry = entries[1] + assert entry.unique_id == "BleBox-switchBoxD-1afe34e750b8-1.relay" + + state = hass.states.get(entity_ids[1]) + assert state.name == "switchBoxD-1.relay" + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_SWITCH + assert state.state == STATE_OFF # NOTE: should instead be STATE_UNKNOWN? + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(entry.device_id) + + assert device.name == "My relays" + assert device.identifiers == {("blebox", "abcd0123ef5678")} + assert device.manufacturer == "BleBox" + assert device.model == "switchBoxD" + assert device.sw_version == "1.23" + + +async def test_switchbox_d_update_when_off(switchbox_d, hass, config): + """Test switch updating when off.""" + + feature_mocks, entity_ids = switchbox_d + + def initial_update0(): + feature_mocks[0].is_on = False + feature_mocks[1].is_on = False + + feature_mocks[0].async_update = AsyncMock(side_effect=initial_update0) + feature_mocks[1].async_update = AsyncMock() + await async_setup_entities(hass, config, entity_ids) + + assert hass.states.get(entity_ids[0]).state == STATE_OFF + assert hass.states.get(entity_ids[1]).state == STATE_OFF + + +async def test_switchbox_d_update_when_second_off(switchbox_d, hass, config): + """Test switch updating when off.""" + + feature_mocks, entity_ids = switchbox_d + + def initial_update0(): + feature_mocks[0].is_on = True + feature_mocks[1].is_on = False + + feature_mocks[0].async_update = AsyncMock(side_effect=initial_update0) + feature_mocks[1].async_update = AsyncMock() + await async_setup_entities(hass, config, entity_ids) + + assert hass.states.get(entity_ids[0]).state == STATE_ON + assert hass.states.get(entity_ids[1]).state == STATE_OFF + + +async def test_switchbox_d_turn_first_on(switchbox_d, hass, config): + """Test turning switch on.""" + + feature_mocks, entity_ids = switchbox_d + + def initial_update0(): + feature_mocks[0].is_on = False + feature_mocks[1].is_on = False + + feature_mocks[0].async_update = AsyncMock(side_effect=initial_update0) + feature_mocks[1].async_update = AsyncMock() + await async_setup_entities(hass, config, entity_ids) + feature_mocks[0].async_update = AsyncMock() + + def turn_on0(): + feature_mocks[0].is_on = True + + feature_mocks[0].async_turn_on = AsyncMock(side_effect=turn_on0) + await hass.services.async_call( + "switch", SERVICE_TURN_ON, {"entity_id": entity_ids[0]}, blocking=True, + ) + + assert hass.states.get(entity_ids[0]).state == STATE_ON + assert hass.states.get(entity_ids[1]).state == STATE_OFF + + +async def test_switchbox_d_second_on(switchbox_d, hass, config): + """Test turning switch on.""" + + feature_mocks, entity_ids = switchbox_d + + def initial_update0(): + feature_mocks[0].is_on = False + feature_mocks[1].is_on = False + + feature_mocks[0].async_update = AsyncMock(side_effect=initial_update0) + feature_mocks[1].async_update = AsyncMock() + await async_setup_entities(hass, config, entity_ids) + feature_mocks[0].async_update = AsyncMock() + + def turn_on1(): + feature_mocks[1].is_on = True + + feature_mocks[1].async_turn_on = AsyncMock(side_effect=turn_on1) + await hass.services.async_call( + "switch", SERVICE_TURN_ON, {"entity_id": entity_ids[1]}, blocking=True, + ) + + assert hass.states.get(entity_ids[0]).state == STATE_OFF + assert hass.states.get(entity_ids[1]).state == STATE_ON + + +async def test_switchbox_d_first_off(switchbox_d, hass, config): + """Test turning switch on.""" + + feature_mocks, entity_ids = switchbox_d + + def initial_update_any(): + feature_mocks[0].is_on = True + feature_mocks[1].is_on = True + + feature_mocks[0].async_update = AsyncMock(side_effect=initial_update_any) + feature_mocks[1].async_update = AsyncMock() + await async_setup_entities(hass, config, entity_ids) + feature_mocks[0].async_update = AsyncMock() + + def turn_off0(): + feature_mocks[0].is_on = False + + feature_mocks[0].async_turn_off = AsyncMock(side_effect=turn_off0) + await hass.services.async_call( + "switch", SERVICE_TURN_OFF, {"entity_id": entity_ids[0]}, blocking=True, + ) + + assert hass.states.get(entity_ids[0]).state == STATE_OFF + assert hass.states.get(entity_ids[1]).state == STATE_ON + + +async def test_switchbox_d_second_off(switchbox_d, hass, config): + """Test turning switch on.""" + + feature_mocks, entity_ids = switchbox_d + + def initial_update_any(): + feature_mocks[0].is_on = True + feature_mocks[1].is_on = True + + feature_mocks[0].async_update = AsyncMock(side_effect=initial_update_any) + feature_mocks[1].async_update = AsyncMock() + await async_setup_entities(hass, config, entity_ids) + feature_mocks[0].async_update = AsyncMock() + + def turn_off1(): + feature_mocks[1].is_on = False + + feature_mocks[1].async_turn_off = AsyncMock(side_effect=turn_off1) + await hass.services.async_call( + "switch", SERVICE_TURN_OFF, {"entity_id": entity_ids[1]}, blocking=True, + ) + assert hass.states.get(entity_ids[0]).state == STATE_ON + assert hass.states.get(entity_ids[1]).state == STATE_OFF + + +ALL_SWITCH_FIXTURES = ["switchbox", "switchbox_d"] + + +@pytest.mark.parametrize("feature", ALL_SWITCH_FIXTURES, indirect=["feature"]) +async def test_update_failure(feature, hass, config, caplog): + """Test that update failures are logged.""" + + caplog.set_level(logging.ERROR) + + feature_mock, entity_id = feature + + if isinstance(feature_mock, list): + feature_mock[0].async_update = AsyncMock() + feature_mock[1].async_update = AsyncMock() + feature_mock = feature_mock[0] + entity_id = entity_id[0] + + feature_mock.async_update = AsyncMock(side_effect=blebox_uniapi.error.ClientError) + await async_setup_entity(hass, config, entity_id) + + assert f"Updating '{feature_mock.full_name}' failed: " in caplog.text diff --git a/tests/components/bom/test_sensor.py b/tests/components/bom/test_sensor.py index 6e85dbca1cd..8c647bbf6cf 100644 --- a/tests/components/bom/test_sensor.py +++ b/tests/components/bom/test_sensor.py @@ -70,6 +70,7 @@ class TestBOMWeatherSensor(unittest.TestCase): """Test the setup with custom settings.""" with assert_setup_component(1, sensor.DOMAIN): assert setup_component(self.hass, sensor.DOMAIN, {"sensor": VALID_CONFIG}) + self.hass.block_till_done() fake_entities = [ "bom_fake_feels_like_c", @@ -85,6 +86,7 @@ class TestBOMWeatherSensor(unittest.TestCase): def test_sensor_values(self, mock_get): """Test retrieval of sensor values.""" assert setup_component(self.hass, sensor.DOMAIN, {"sensor": VALID_CONFIG}) + self.hass.block_till_done() weather = self.hass.states.get("sensor.bom_fake_weather").state assert "Fine" == weather diff --git a/tests/components/braviatv/test_config_flow.py b/tests/components/braviatv/test_config_flow.py index 87fa26c242a..c6f76105cfc 100644 --- a/tests/components/braviatv/test_config_flow.py +++ b/tests/components/braviatv/test_config_flow.py @@ -1,4 +1,6 @@ """Define tests for the Bravia TV config flow.""" +from bravia_tv.braviarc import NoIPControl + from homeassistant import data_entry_flow from homeassistant.components.braviatv.const import CONF_IGNORED_SOURCES, DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER @@ -28,14 +30,8 @@ BRAVIA_SOURCE_LIST = { "AV/Component": "extInput:component?port=1", } -IMPORT_CONFIG_HOSTNAME = { - CONF_HOST: "bravia-host", - CONF_PIN: "1234", -} -IMPORT_CONFIG_IP = { - CONF_HOST: "10.10.10.12", - CONF_PIN: "1234", -} +IMPORT_CONFIG_HOSTNAME = {CONF_HOST: "bravia-host", CONF_PIN: "1234"} +IMPORT_CONFIG_IP = {CONF_HOST: "10.10.10.12", CONF_PIN: "1234"} async def test_show_form(hass): @@ -58,7 +54,7 @@ async def test_import(hass): "homeassistant.components.braviatv.async_setup_entry", return_value=True ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=IMPORT_CONFIG_HOSTNAME, + DOMAIN, context={"source": SOURCE_IMPORT}, data=IMPORT_CONFIG_HOSTNAME ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -77,7 +73,7 @@ async def test_import_cannot_connect(hass): "bravia_tv.BraviaRC.is_connected", return_value=False ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=IMPORT_CONFIG_HOSTNAME, + DOMAIN, context={"source": SOURCE_IMPORT}, data=IMPORT_CONFIG_HOSTNAME ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -90,13 +86,24 @@ async def test_import_model_unsupported(hass): "bravia_tv.BraviaRC.is_connected", return_value=True ), patch("bravia_tv.BraviaRC.get_system_info", return_value={}): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=IMPORT_CONFIG_IP, + DOMAIN, context={"source": SOURCE_IMPORT}, data=IMPORT_CONFIG_IP ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "unsupported_model" +async def test_import_no_ip_control(hass): + """Test that errors are shown when IP Control is disabled on the TV during import.""" + with patch("bravia_tv.BraviaRC.connect", side_effect=NoIPControl("No IP Control")): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=IMPORT_CONFIG_IP + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "no_ip_control" + + async def test_import_duplicate_error(hass): """Test that errors are shown when duplicates are added during import.""" config_entry = MockConfigEntry( @@ -116,7 +123,7 @@ async def test_import_duplicate_error(hass): ), patch("bravia_tv.BraviaRC.get_system_info", return_value=BRAVIA_SYSTEM_INFO): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=IMPORT_CONFIG_HOSTNAME, + DOMAIN, context={"source": SOURCE_IMPORT}, data=IMPORT_CONFIG_HOSTNAME ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -136,7 +143,7 @@ async def test_authorize_cannot_connect(hass): """Test that errors are shown when cannot connect to host at the authorize step.""" with patch("bravia_tv.BraviaRC.connect", return_value=True): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"}, + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PIN: "1234"} @@ -151,7 +158,7 @@ async def test_authorize_model_unsupported(hass): "bravia_tv.BraviaRC.is_connected", return_value=True ), patch("bravia_tv.BraviaRC.get_system_info", return_value={}): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "10.10.10.12"}, + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "10.10.10.12"} ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PIN: "1234"} @@ -160,6 +167,17 @@ async def test_authorize_model_unsupported(hass): assert result["errors"] == {"base": "unsupported_model"} +async def test_authorize_no_ip_control(hass): + """Test that errors are shown when IP Control is disabled on the TV.""" + with patch("bravia_tv.BraviaRC.connect", side_effect=NoIPControl("No IP Control")): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "no_ip_control" + + async def test_duplicate_error(hass): """Test that errors are shown when duplicates are added.""" config_entry = MockConfigEntry( @@ -179,7 +197,7 @@ async def test_duplicate_error(hass): ), patch("bravia_tv.BraviaRC.get_system_info", return_value=BRAVIA_SYSTEM_INFO): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"}, + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PIN: "1234"} diff --git a/tests/components/buienradar/test_camera.py b/tests/components/buienradar/test_camera.py index 96c5d2ec67c..9935cc19cb8 100644 --- a/tests/components/buienradar/test_camera.py +++ b/tests/components/buienradar/test_camera.py @@ -23,6 +23,7 @@ async def test_fetching_url_and_caching(aioclient_mock, hass, hass_client): await async_setup_component( hass, "camera", {"camera": {"name": "config_test", "platform": "buienradar"}} ) + await hass.async_block_till_done() client = await hass_client() @@ -55,6 +56,7 @@ async def test_expire_delta(aioclient_mock, hass, hass_client): } }, ) + await hass.async_block_till_done() client = await hass_client() @@ -78,6 +80,7 @@ async def test_only_one_fetch_at_a_time(aioclient_mock, hass, hass_client): await async_setup_component( hass, "camera", {"camera": {"name": "config_test", "platform": "buienradar"}} ) + await hass.async_block_till_done() client = await hass_client() @@ -101,6 +104,7 @@ async def test_dimension(aioclient_mock, hass, hass_client): "camera", {"camera": {"name": "config_test", "platform": "buienradar", "dimension": 700}}, ) + await hass.async_block_till_done() client = await hass_client() @@ -124,6 +128,7 @@ async def test_belgium_country(aioclient_mock, hass, hass_client): } }, ) + await hass.async_block_till_done() client = await hass_client() @@ -139,6 +144,7 @@ async def test_failure_response_not_cached(aioclient_mock, hass, hass_client): await async_setup_component( hass, "camera", {"camera": {"name": "config_test", "platform": "buienradar"}} ) + await hass.async_block_till_done() client = await hass_client() @@ -172,6 +178,7 @@ async def test_last_modified_updates(aioclient_mock, hass, hass_client): } }, ) + await hass.async_block_till_done() client = await hass_client() @@ -200,6 +207,7 @@ async def test_retries_after_error(aioclient_mock, hass, hass_client): await async_setup_component( hass, "camera", {"camera": {"name": "config_test", "platform": "buienradar"}} ) + await hass.async_block_till_done() client = await hass_client() diff --git a/tests/components/buienradar/test_sensor.py b/tests/components/buienradar/test_sensor.py index 0c1cbd2a158..801f5706a08 100644 --- a/tests/components/buienradar/test_sensor.py +++ b/tests/components/buienradar/test_sensor.py @@ -19,6 +19,7 @@ BASE_CONFIG = { async def test_smoke_test_setup_component(hass): """Smoke test for successfully set-up with default config.""" assert await async_setup_component(hass, sensor.DOMAIN, BASE_CONFIG) + await hass.async_block_till_done() for cond in CONDITIONS: state = hass.states.get(f"sensor.volkel_{cond}") diff --git a/tests/components/buienradar/test_weather.py b/tests/components/buienradar/test_weather.py index 081616a1406..db0a6ce3984 100644 --- a/tests/components/buienradar/test_weather.py +++ b/tests/components/buienradar/test_weather.py @@ -19,6 +19,7 @@ BASE_CONFIG = { async def test_smoke_test_setup_component(hass): """Smoke test for successfully set-up with default config.""" assert await async_setup_component(hass, weather.DOMAIN, BASE_CONFIG) + await hass.async_block_till_done() state = hass.states.get("weather.volkel") assert state.state == "unknown" diff --git a/tests/components/calendar/test_init.py b/tests/components/calendar/test_init.py index ede5479e407..1f87c38c6bf 100644 --- a/tests/components/calendar/test_init.py +++ b/tests/components/calendar/test_init.py @@ -8,6 +8,7 @@ import homeassistant.util.dt as dt_util async def test_events_http_api(hass, hass_client): """Test the calendar demo view.""" await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}}) + await hass.async_block_till_done() client = await hass_client() response = await client.get("/api/calendars/calendar.calendar_2") assert response.status == 400 @@ -27,6 +28,7 @@ async def test_events_http_api(hass, hass_client): async def test_calendars_http_api(hass, hass_client): """Test the calendar demo view.""" await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}}) + await hass.async_block_till_done() client = await hass_client() response = await client.get("/api/calendars") assert response.status == 200 diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 36ee9b8aabf..8a79cb39ec9 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -19,11 +19,12 @@ from tests.components.camera import common @pytest.fixture(name="mock_camera") -def mock_camera_fixture(hass): +async def mock_camera_fixture(hass): """Initialize a demo camera platform.""" - assert hass.loop.run_until_complete( - async_setup_component(hass, "camera", {camera.DOMAIN: {"platform": "demo"}}) + assert await async_setup_component( + hass, "camera", {camera.DOMAIN: {"platform": "demo"}} ) + await hass.async_block_till_done() with patch( "homeassistant.components.demo.camera.Path.read_bytes", return_value=b"Test", @@ -51,6 +52,7 @@ async def image_mock_url_fixture(hass): await async_setup_component( hass, camera.DOMAIN, {camera.DOMAIN: {"platform": "demo"}} ) + await hass.async_block_till_done() async def test_get_image_from_camera(hass, image_mock_url): @@ -67,6 +69,19 @@ async def test_get_image_from_camera(hass, image_mock_url): assert image.content == b"Test" +async def test_get_stream_source_from_camera(hass, mock_camera): + """Fetch stream source from camera entity.""" + + with patch( + "homeassistant.components.camera.Camera.stream_source", + return_value="rtsp://127.0.0.1/stream", + ) as mock_camera_stream_source: + stream_source = await camera.async_get_stream_source(hass, "camera.demo_camera") + + assert mock_camera_stream_source.called + assert stream_source == "rtsp://127.0.0.1/stream" + + async def test_get_image_without_exists_camera(hass, image_mock_url): """Try to get image without exists camera.""" with patch( @@ -298,7 +313,10 @@ async def test_preload_stream(hass, mock_stream): "homeassistant.components.demo.camera.DemoCamera.stream_source", return_value="http://example.com", ): - await async_setup_component(hass, "camera", {DOMAIN: {"platform": "demo"}}) + assert await async_setup_component( + hass, "camera", {DOMAIN: {"platform": "demo"}} + ) + await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() assert mock_request_stream.called diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 2adb8b63052..90b3896396c 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -80,8 +80,11 @@ async def async_setup_cast_internal_discovery(hass, config=None, discovery_info= browser = MagicMock(zc={}) with patch( + "homeassistant.components.cast.discovery.pychromecast.CastListener", + return_value=listener, + ) as cast_listener, patch( "homeassistant.components.cast.discovery.pychromecast.start_discovery", - return_value=(listener, browser), + return_value=browser, ) as start_discovery: add_entities = await async_setup_cast(hass, config, discovery_info) await hass.async_block_till_done() @@ -89,7 +92,7 @@ async def async_setup_cast_internal_discovery(hass, config=None, discovery_info= assert start_discovery.call_count == 1 - discovery_callback = start_discovery.call_args[0][0] + discovery_callback = cast_listener.call_args[0][0] def discover_chromecast(service_name: str, info: ChromecastInfo) -> None: """Discover a chromecast device.""" @@ -117,9 +120,12 @@ async def async_setup_media_player_cast(hass: HomeAssistantType, info: Chromecas "homeassistant.components.cast.discovery.pychromecast.get_chromecast_from_service", return_value=chromecast, ) as get_chromecast, patch( + "homeassistant.components.cast.discovery.pychromecast.CastListener", + return_value=listener, + ) as cast_listener, patch( "homeassistant.components.cast.discovery.pychromecast.start_discovery", - return_value=(listener, browser), - ) as start_discovery: + return_value=browser, + ): await async_setup_component( hass, "media_player", @@ -128,7 +134,7 @@ async def async_setup_media_player_cast(hass: HomeAssistantType, info: Chromecas await hass.async_block_till_done() - discovery_callback = start_discovery.call_args[0][0] + discovery_callback = cast_listener.call_args[0][0] def discover_chromecast(service_name: str, info: ChromecastInfo) -> None: """Discover a chromecast device.""" @@ -154,7 +160,7 @@ async def test_start_discovery_called_once(hass): """Test pychromecast.start_discovery called exactly once.""" with patch( "homeassistant.components.cast.discovery.pychromecast.start_discovery", - return_value=(None, Mock()), + return_value=Mock(), ) as start_discovery: await async_setup_cast(hass) @@ -170,7 +176,7 @@ async def test_stop_discovery_called_on_stop(hass): with patch( "homeassistant.components.cast.discovery.pychromecast.start_discovery", - return_value=(None, browser), + return_value=browser, ) as start_discovery: # start_discovery should be called with empty config await async_setup_cast(hass, {}) @@ -188,7 +194,7 @@ async def test_stop_discovery_called_on_stop(hass): with patch( "homeassistant.components.cast.discovery.pychromecast.start_discovery", - return_value=(None, browser), + return_value=browser, ) as start_discovery: # start_discovery should be called again on re-startup await async_setup_cast(hass) diff --git a/tests/components/climate/test_init.py b/tests/components/climate/test_init.py index e42bf8c7e3c..89c12b4c517 100644 --- a/tests/components/climate/test_init.py +++ b/tests/components/climate/test_init.py @@ -65,6 +65,12 @@ class MockClimateEntity(ClimateEntity): """ return [HVAC_MODE_OFF, HVAC_MODE_HEAT] + def turn_on(self) -> None: + """Turn on.""" + + def turn_off(self) -> None: + """Turn off.""" + async def test_sync_turn_on(hass): """Test if async turn_on calls sync turn_on.""" @@ -92,9 +98,13 @@ def test_deprecated_base_class(caplog): """Test deprecated base class.""" class CustomClimate(ClimateDevice): + """Custom climate entity class.""" + + @property def hvac_mode(self): pass + @property def hvac_modes(self): pass diff --git a/tests/components/coinmarketcap/test_sensor.py b/tests/components/coinmarketcap/test_sensor.py index 8997bc4a5d6..f53a92a3d86 100644 --- a/tests/components/coinmarketcap/test_sensor.py +++ b/tests/components/coinmarketcap/test_sensor.py @@ -36,6 +36,7 @@ class TestCoinMarketCapSensor(unittest.TestCase): """Test the setup with custom settings.""" with assert_setup_component(1, sensor.DOMAIN): assert setup_component(self.hass, sensor.DOMAIN, {"sensor": VALID_CONFIG}) + self.hass.block_till_done() state = self.hass.states.get("sensor.ethereum") assert state is not None diff --git a/tests/components/command_line/test_cover.py b/tests/components/command_line/test_cover.py index fc359d49b26..c4f45ed9704 100644 --- a/tests/components/command_line/test_cover.py +++ b/tests/components/command_line/test_cover.py @@ -68,6 +68,7 @@ async def test_state_value(hass): ) is True ) + await hass.async_block_till_done() assert "unknown" == hass.states.get("cover.test").state diff --git a/tests/components/command_line/test_switch.py b/tests/components/command_line/test_switch.py index acdb5b23b10..ab5d0044f73 100644 --- a/tests/components/command_line/test_switch.py +++ b/tests/components/command_line/test_switch.py @@ -43,6 +43,7 @@ class TestCommandSwitch(unittest.TestCase): } }, ) + self.hass.block_till_done() state = self.hass.states.get("switch.test") assert STATE_OFF == state.state @@ -79,6 +80,7 @@ class TestCommandSwitch(unittest.TestCase): } }, ) + self.hass.block_till_done() state = self.hass.states.get("switch.test") assert STATE_OFF == state.state @@ -117,6 +119,7 @@ class TestCommandSwitch(unittest.TestCase): } }, ) + self.hass.block_till_done() state = self.hass.states.get("switch.test") assert STATE_OFF == state.state @@ -152,7 +155,7 @@ class TestCommandSwitch(unittest.TestCase): } }, ) - + self.hass.block_till_done() state = self.hass.states.get("switch.test") assert STATE_OFF == state.state diff --git a/tests/components/cover/test_device_condition.py b/tests/components/cover/test_device_condition.py index b355053ad36..511c7ced898 100644 --- a/tests/components/cover/test_device_condition.py +++ b/tests/components/cover/test_device_condition.py @@ -454,6 +454,7 @@ async def test_if_position(hass, calls): platform.init() ent = platform.ENTITIES[1] assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() assert await async_setup_component( hass, @@ -557,6 +558,7 @@ async def test_if_tilt_position(hass, calls): platform.init() ent = platform.ENTITIES[2] assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() assert await async_setup_component( hass, diff --git a/tests/components/cover/test_device_trigger.py b/tests/components/cover/test_device_trigger.py index 50738e2c549..1996cf9d6df 100644 --- a/tests/components/cover/test_device_trigger.py +++ b/tests/components/cover/test_device_trigger.py @@ -464,6 +464,7 @@ async def test_if_fires_on_position(hass, calls): platform.init() ent = platform.ENTITIES[1] assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() assert await async_setup_component( hass, @@ -586,6 +587,7 @@ async def test_if_fires_on_tilt_position(hass, calls): platform.init() ent = platform.ENTITIES[1] assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() assert await async_setup_component( hass, diff --git a/tests/components/daikin/test_config_flow.py b/tests/components/daikin/test_config_flow.py index 25fc8ba26f2..67affc3d501 100644 --- a/tests/components/daikin/test_config_flow.py +++ b/tests/components/daikin/test_config_flow.py @@ -6,8 +6,13 @@ from aiohttp import ClientError from aiohttp.web_exceptions import HTTPForbidden import pytest -from homeassistant.components.daikin.const import KEY_IP, KEY_MAC -from homeassistant.config_entries import SOURCE_DISCOVERY, SOURCE_IMPORT, SOURCE_USER +from homeassistant.components.daikin.const import KEY_HOSTNAME, KEY_IP, KEY_MAC +from homeassistant.config_entries import ( + SOURCE_DISCOVERY, + SOURCE_IMPORT, + SOURCE_USER, + SOURCE_ZEROCONF, +) from homeassistant.const import CONF_HOST from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, @@ -104,9 +109,17 @@ async def test_device_abort(hass, mock_daikin, s_effect, reason): @pytest.mark.parametrize( - "source, data, unique_id", [(SOURCE_DISCOVERY, {KEY_IP: HOST, KEY_MAC: MAC}, MAC)], + "source, data, unique_id", + [ + (SOURCE_DISCOVERY, {KEY_IP: HOST, KEY_MAC: MAC}, MAC), + ( + SOURCE_ZEROCONF, + {CONF_HOST: HOST, KEY_HOSTNAME: "DaikinUNIQE.local"}, + "DaikinUNIQE.local", + ), + ], ) -async def test_discovery(hass, mock_daikin, source, data, unique_id): +async def test_discovery_zeroconf(hass, mock_daikin, source, data, unique_id): """Test discovery/zeroconf step.""" result = await hass.config_entries.flow.async_init( "daikin", context={"source": source}, data=data, diff --git a/tests/components/darksky/test_sensor.py b/tests/components/darksky/test_sensor.py index 8c4038f91c6..9a48e4f1cce 100644 --- a/tests/components/darksky/test_sensor.py +++ b/tests/components/darksky/test_sensor.py @@ -117,6 +117,7 @@ class TestDarkSkySetup(unittest.TestCase): def test_setup_with_config(self): """Test the platform setup with configuration.""" setup_component(self.hass, "sensor", VALID_CONFIG_MINIMAL) + self.hass.block_till_done() state = self.hass.states.get("sensor.dark_sky_summary") assert state is not None @@ -124,6 +125,7 @@ class TestDarkSkySetup(unittest.TestCase): def test_setup_with_invalid_config(self): """Test the platform setup with invalid configuration.""" setup_component(self.hass, "sensor", INVALID_CONFIG_MINIMAL) + self.hass.block_till_done() state = self.hass.states.get("sensor.dark_sky_summary") assert state is None @@ -135,6 +137,7 @@ class TestDarkSkySetup(unittest.TestCase): def test_setup_with_language_config(self): """Test the platform setup with language configuration.""" setup_component(self.hass, "sensor", VALID_CONFIG_LANG_DE) + self.hass.block_till_done() state = self.hass.states.get("sensor.dark_sky_summary") assert state is not None @@ -142,6 +145,7 @@ class TestDarkSkySetup(unittest.TestCase): def test_setup_with_invalid_language_config(self): """Test the platform setup with language configuration.""" setup_component(self.hass, "sensor", INVALID_CONFIG_LANG) + self.hass.block_till_done() state = self.hass.states.get("sensor.dark_sky_summary") assert state is None @@ -169,6 +173,7 @@ class TestDarkSkySetup(unittest.TestCase): def test_setup_with_alerts_config(self): """Test the platform setup with alert configuration.""" setup_component(self.hass, "sensor", VALID_CONFIG_ALERTS) + self.hass.block_till_done() state = self.hass.states.get("sensor.dark_sky_alerts") assert state.state == "0" @@ -184,6 +189,7 @@ class TestDarkSkySetup(unittest.TestCase): mock_req.get(re.compile(uri), text=load_fixture("darksky.json")) assert setup_component(self.hass, "sensor", VALID_CONFIG_MINIMAL) + self.hass.block_till_done() assert mock_get_forecast.called assert mock_get_forecast.call_count == 1 diff --git a/tests/components/darksky/test_weather.py b/tests/components/darksky/test_weather.py index f871d424db6..9f43534d7cd 100644 --- a/tests/components/darksky/test_weather.py +++ b/tests/components/darksky/test_weather.py @@ -43,6 +43,7 @@ class TestDarkSky(unittest.TestCase): weather.DOMAIN, {"weather": {"name": "test", "platform": "darksky", "api_key": "foo"}}, ) + self.hass.block_till_done() assert mock_get_forecast.called assert mock_get_forecast.call_count == 1 @@ -59,6 +60,7 @@ class TestDarkSky(unittest.TestCase): weather.DOMAIN, {"weather": {"name": "test", "platform": "darksky", "api_key": "foo"}}, ) + self.hass.block_till_done() state = self.hass.states.get("weather.test") assert state.state == "unavailable" diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 864ba91fbc1..30f7251e067 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -3,6 +3,10 @@ from copy import deepcopy from homeassistant.components import deconz import homeassistant.components.binary_sensor as binary_sensor +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOTION, + DEVICE_CLASS_VIBRATION, +) from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration @@ -79,6 +83,7 @@ async def test_binary_sensors(hass): presence_sensor = hass.states.get("binary_sensor.presence_sensor") assert presence_sensor.state == "off" + assert presence_sensor.attributes["device_class"] == DEVICE_CLASS_MOTION temperature_sensor = hass.states.get("binary_sensor.temperature_sensor") assert temperature_sensor is None @@ -88,6 +93,7 @@ async def test_binary_sensors(hass): vibration_sensor = hass.states.get("binary_sensor.vibration_sensor") assert vibration_sensor.state == "on" + assert vibration_sensor.attributes["device_class"] == DEVICE_CLASS_VIBRATION state_changed_event = { "t": "event", diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index 4f45e36c5b1..095ae7e4bc5 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -41,6 +41,14 @@ COVERS = { "modelid": "Not zigbee spec", "uniqueid": "00:00:00:00:00:00:00:03-00", }, + "5": { + "id": "Window covering controller id", + "name": "Window covering controller", + "type": "Window covering controller", + "state": {"bri": 254, "on": True, "reachable": True}, + "modelid": "Motor controller", + "uniqueid": "00:00:00:00:00:00:00:04-00", + }, } @@ -71,7 +79,8 @@ async def test_cover(hass): assert "cover.window_covering_device" in gateway.deconz_ids assert "cover.unsupported_cover" not in gateway.deconz_ids assert "cover.deconz_old_brightness_cover" in gateway.deconz_ids - assert len(hass.states.async_all()) == 4 + assert "cover.window_covering_controller" in gateway.deconz_ids + assert len(hass.states.async_all()) == 5 level_controllable_cover = hass.states.get("cover.level_controllable_cover") assert level_controllable_cover.state == "open" diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 229c085916e..d070bd5b420 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -46,6 +46,8 @@ LIGHTS = { "uniqueid": "00:00:00:00:00:00:00:00-00", }, "2": { + "ctmax": 454, + "ctmin": 155, "id": "Tunable white light id", "name": "Tunable white light", "state": {"on": True, "colormode": "ct", "ct": 2500, "reachable": True}, @@ -111,6 +113,8 @@ async def test_lights_and_groups(hass): tunable_white_light = hass.states.get("light.tunable_white_light") assert tunable_white_light.state == "on" assert tunable_white_light.attributes["color_temp"] == 2500 + assert tunable_white_light.attributes["max_mireds"] == 454 + assert tunable_white_light.attributes["min_mireds"] == 155 assert tunable_white_light.attributes["supported_features"] == 2 on_off_light = hass.states.get("light.on_off_light") @@ -185,6 +189,26 @@ async def test_lights_and_groups(hass): json={"xy": (0.411, 0.351), "alert": "lselect", "effect": "none"}, ) + with patch.object(rgb_light_device, "_request", return_value=True) as set_callback: + await hass.services.async_call( + light.DOMAIN, + light.SERVICE_TURN_OFF, + {"entity_id": "light.rgb_light", "transition": 5, "flash": "short"}, + blocking=True, + ) + await hass.async_block_till_done() + assert not set_callback.called + + state_changed_event = { + "t": "event", + "e": "changed", + "r": "lights", + "id": "1", + "state": {"on": True}, + } + gateway.api.event_handler(state_changed_event) + await hass.async_block_till_done() + with patch.object(rgb_light_device, "_request", return_value=True) as set_callback: await hass.services.async_call( light.DOMAIN, diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index cda3138557d..9d87c7b91cb 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -3,6 +3,11 @@ from copy import deepcopy from homeassistant.components import deconz import homeassistant.components.sensor as sensor +from homeassistant.const import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_POWER, +) from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration @@ -112,6 +117,7 @@ async def test_sensors(hass): light_level_sensor = hass.states.get("sensor.light_level_sensor") assert light_level_sensor.state == "999.8" + assert light_level_sensor.attributes["device_class"] == DEVICE_CLASS_ILLUMINANCE presence_sensor = hass.states.get("sensor.presence_sensor") assert presence_sensor is None @@ -127,15 +133,18 @@ async def test_sensors(hass): switch_2_battery_level = hass.states.get("sensor.switch_2_battery_level") assert switch_2_battery_level.state == "100" + assert switch_2_battery_level.attributes["device_class"] == DEVICE_CLASS_BATTERY daylight_sensor = hass.states.get("sensor.daylight_sensor") assert daylight_sensor is None power_sensor = hass.states.get("sensor.power_sensor") assert power_sensor.state == "6" + assert power_sensor.attributes["device_class"] == DEVICE_CLASS_POWER consumption_sensor = hass.states.get("sensor.consumption_sensor") assert consumption_sensor.state == "0.002" + assert "device_class" not in consumption_sensor.attributes state_changed_event = { "t": "event", diff --git a/tests/components/demo/test_camera.py b/tests/components/demo/test_camera.py index 49b8e017f1a..e62d6db0464 100644 --- a/tests/components/demo/test_camera.py +++ b/tests/components/demo/test_camera.py @@ -22,13 +22,12 @@ ENTITY_CAMERA = "camera.demo_camera" @pytest.fixture(autouse=True) -def demo_camera(hass): +async def demo_camera(hass): """Initialize a demo camera platform.""" - hass.loop.run_until_complete( - async_setup_component( - hass, CAMERA_DOMAIN, {CAMERA_DOMAIN: {"platform": DOMAIN}} - ) + assert await async_setup_component( + hass, CAMERA_DOMAIN, {CAMERA_DOMAIN: {"platform": DOMAIN}} ) + await hass.async_block_till_done() async def test_init_state_is_streaming(hass): diff --git a/tests/components/demo/test_climate.py b/tests/components/demo/test_climate.py index efdab74304a..91e14247834 100644 --- a/tests/components/demo/test_climate.py +++ b/tests/components/demo/test_climate.py @@ -43,6 +43,7 @@ async def setup_demo_climate(hass): """Initialize setup demo climate.""" hass.config.units = METRIC_SYSTEM assert await async_setup_component(hass, DOMAIN, {"climate": {"platform": "demo"}}) + await hass.async_block_till_done() def test_setup_params(hass): diff --git a/tests/components/demo/test_cover.py b/tests/components/demo/test_cover.py index e2478f64f3a..8d561f328a5 100644 --- a/tests/components/demo/test_cover.py +++ b/tests/components/demo/test_cover.py @@ -42,6 +42,7 @@ async def setup_comp(hass): """Set up demo cover component.""" with assert_setup_component(1, DOMAIN): await async_setup_component(hass, DOMAIN, CONFIG) + await hass.async_block_till_done() async def test_supported_features(hass, setup_comp): diff --git a/tests/components/demo/test_fan.py b/tests/components/demo/test_fan.py index 71ec5c385dc..d9c9ac77dc8 100644 --- a/tests/components/demo/test_fan.py +++ b/tests/components/demo/test_fan.py @@ -16,11 +16,10 @@ def get_entity(hass): @pytest.fixture(autouse=True) -def setup_comp(hass): +async def setup_comp(hass): """Initialize components.""" - hass.loop.run_until_complete( - async_setup_component(hass, fan.DOMAIN, {"fan": {"platform": "demo"}}) - ) + assert await async_setup_component(hass, fan.DOMAIN, {"fan": {"platform": "demo"}}) + await hass.async_block_till_done() async def test_turn_on(hass): diff --git a/tests/components/demo/test_light.py b/tests/components/demo/test_light.py index fd17d7bdb0e..4e7f58811d9 100644 --- a/tests/components/demo/test_light.py +++ b/tests/components/demo/test_light.py @@ -24,11 +24,12 @@ ENTITY_LIGHT = "light.bed_light" @pytest.fixture(autouse=True) -def setup_comp(hass): +async def setup_comp(hass): """Set up demo component.""" - hass.loop.run_until_complete( - async_setup_component(hass, LIGHT_DOMAIN, {LIGHT_DOMAIN: {"platform": DOMAIN}}) + assert await async_setup_component( + hass, LIGHT_DOMAIN, {LIGHT_DOMAIN: {"platform": DOMAIN}} ) + await hass.async_block_till_done() async def test_state_attributes(hass): diff --git a/tests/components/demo/test_lock.py b/tests/components/demo/test_lock.py index 68d5a884cf0..bf8c0ddb63d 100644 --- a/tests/components/demo/test_lock.py +++ b/tests/components/demo/test_lock.py @@ -21,11 +21,12 @@ OPENABLE_LOCK = "lock.openable_lock" @pytest.fixture(autouse=True) -def setup_comp(hass): +async def setup_comp(hass): """Set up demo component.""" - hass.loop.run_until_complete( - async_setup_component(hass, LOCK_DOMAIN, {LOCK_DOMAIN: {"platform": DOMAIN}}) + assert await async_setup_component( + hass, LOCK_DOMAIN, {LOCK_DOMAIN: {"platform": DOMAIN}} ) + await hass.async_block_till_done() async def test_locking(hass): diff --git a/tests/components/demo/test_media_player.py b/tests/components/demo/test_media_player.py index e663600f84f..1ab8195c4db 100644 --- a/tests/components/demo/test_media_player.py +++ b/tests/components/demo/test_media_player.py @@ -29,6 +29,7 @@ async def test_source_select(hass): assert await async_setup_component( hass, mp.DOMAIN, {"media_player": {"platform": "demo"}} ) + await hass.async_block_till_done() state = hass.states.get(entity_id) assert state.attributes.get("source") == "dvd" @@ -47,6 +48,7 @@ async def test_clear_playlist(hass): assert await async_setup_component( hass, mp.DOMAIN, {"media_player": {"platform": "demo"}} ) + await hass.async_block_till_done() assert hass.states.is_state(TEST_ENTITY_ID, "playing") await common.async_clear_playlist(hass, TEST_ENTITY_ID) @@ -58,6 +60,7 @@ async def test_volume_services(hass): assert await async_setup_component( hass, mp.DOMAIN, {"media_player": {"platform": "demo"}} ) + await hass.async_block_till_done() state = hass.states.get(TEST_ENTITY_ID) assert state.attributes.get("volume_level") == 1.0 @@ -95,6 +98,7 @@ async def test_turning_off_and_on(hass): assert await async_setup_component( hass, mp.DOMAIN, {"media_player": {"platform": "demo"}} ) + await hass.async_block_till_done() assert hass.states.is_state(TEST_ENTITY_ID, "playing") await common.async_turn_off(hass, TEST_ENTITY_ID) @@ -114,6 +118,7 @@ async def test_playing_pausing(hass): assert await async_setup_component( hass, mp.DOMAIN, {"media_player": {"platform": "demo"}} ) + await hass.async_block_till_done() assert hass.states.is_state(TEST_ENTITY_ID, "playing") await common.async_media_pause(hass, TEST_ENTITY_ID) @@ -134,6 +139,7 @@ async def test_prev_next_track(hass): assert await async_setup_component( hass, mp.DOMAIN, {"media_player": {"platform": "demo"}} ) + await hass.async_block_till_done() state = hass.states.get(TEST_ENTITY_ID) assert state.attributes.get("media_track") == 1 @@ -152,6 +158,7 @@ async def test_prev_next_track(hass): assert await async_setup_component( hass, mp.DOMAIN, {"media_player": {"platform": "demo"}} ) + await hass.async_block_till_done() ent_id = "media_player.lounge_room" state = hass.states.get(ent_id) assert state.attributes.get("media_episode") == 1 @@ -170,6 +177,7 @@ async def test_play_media(hass): assert await async_setup_component( hass, mp.DOMAIN, {"media_player": {"platform": "demo"}} ) + await hass.async_block_till_done() ent_id = "media_player.living_room" state = hass.states.get(ent_id) assert mp.SUPPORT_PLAY_MEDIA & state.attributes.get("supported_features") > 0 @@ -192,6 +200,7 @@ async def test_seek(hass, mock_media_seek): assert await async_setup_component( hass, mp.DOMAIN, {"media_player": {"platform": "demo"}} ) + await hass.async_block_till_done() ent_id = "media_player.living_room" state = hass.states.get(ent_id) assert state.attributes["supported_features"] & mp.SUPPORT_SEEK @@ -209,6 +218,7 @@ async def test_media_image_proxy(hass, hass_client): assert await async_setup_component( hass, mp.DOMAIN, {"media_player": {"platform": "demo"}} ) + await hass.async_block_till_done() fake_picture_data = "test.test" diff --git a/tests/components/demo/test_remote.py b/tests/components/demo/test_remote.py index b83eaca4c9c..7ea31fbeb69 100644 --- a/tests/components/demo/test_remote.py +++ b/tests/components/demo/test_remote.py @@ -22,6 +22,7 @@ class TestDemoRemote(unittest.TestCase): assert setup_component( self.hass, remote.DOMAIN, {"remote": {"platform": "demo"}} ) + self.hass.block_till_done() # pylint: disable=invalid-name def tearDown(self): diff --git a/tests/components/demo/test_stt.py b/tests/components/demo/test_stt.py index 3fe4e223961..f749d6288a7 100644 --- a/tests/components/demo/test_stt.py +++ b/tests/components/demo/test_stt.py @@ -6,11 +6,10 @@ from homeassistant.setup import async_setup_component @pytest.fixture(autouse=True) -def setup_comp(hass): +async def setup_comp(hass): """Set up demo component.""" - hass.loop.run_until_complete( - async_setup_component(hass, stt.DOMAIN, {"stt": {"platform": "demo"}}) - ) + assert await async_setup_component(hass, stt.DOMAIN, {"stt": {"platform": "demo"}}) + await hass.async_block_till_done() async def test_demo_settings(hass_client): diff --git a/tests/components/demo/test_vacuum.py b/tests/components/demo/test_vacuum.py index 5c9ac4205fe..d07d7b3d4b2 100644 --- a/tests/components/demo/test_vacuum.py +++ b/tests/components/demo/test_vacuum.py @@ -51,6 +51,7 @@ ENTITY_VACUUM_STATE = f"{DOMAIN}.{DEMO_VACUUM_STATE}".lower() async def setup_demo_vacuum(hass): """Initialize setup demo vacuum.""" assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "demo"}}) + await hass.async_block_till_done() async def test_supported_features(hass): diff --git a/tests/components/demo/test_water_heater.py b/tests/components/demo/test_water_heater.py index b8474e978c4..a6b4d999ddb 100644 --- a/tests/components/demo/test_water_heater.py +++ b/tests/components/demo/test_water_heater.py @@ -13,14 +13,13 @@ ENTITY_WATER_HEATER_CELSIUS = "water_heater.demo_water_heater_celsius" @pytest.fixture(autouse=True) -def setup_comp(hass): +async def setup_comp(hass): """Set up demo component.""" hass.config.units = IMPERIAL_SYSTEM - hass.loop.run_until_complete( - async_setup_component( - hass, water_heater.DOMAIN, {"water_heater": {"platform": "demo"}} - ) + assert await async_setup_component( + hass, water_heater.DOMAIN, {"water_heater": {"platform": "demo"}} ) + await hass.async_block_till_done() async def test_setup_params(hass): diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 48426e2640e..d6a9fda00bc 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -621,7 +621,7 @@ async def test_automation_with_sub_condition(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - + await hass.async_block_till_done() ent1, ent2, ent3 = platform.ENTITIES assert await async_setup_component( diff --git a/tests/components/device_sun_light_trigger/test_init.py b/tests/components/device_sun_light_trigger/test_init.py index 23e400adac4..2663018fc09 100644 --- a/tests/components/device_sun_light_trigger/test_init.py +++ b/tests/components/device_sun_light_trigger/test_init.py @@ -14,11 +14,13 @@ from homeassistant.components.device_tracker.const import DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, CONF_PLATFORM, + EVENT_HOMEASSISTANT_START, STATE_HOME, STATE_NOT_HOME, STATE_OFF, STATE_ON, ) +from homeassistant.core import CoreState from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -214,3 +216,19 @@ async def test_lights_turn_on_when_coming_home_after_sun_set_person(hass, scanne assert hass.states.get(device_1).state == "home" assert hass.states.get(device_2).state == "home" assert hass.states.get("person.me").state == "home" + + +async def test_initialize_start(hass): + """Test we initialize when HA starts.""" + hass.state = CoreState.not_running + assert await async_setup_component( + hass, device_sun_light_trigger.DOMAIN, {device_sun_light_trigger.DOMAIN: {}}, + ) + + with patch( + "homeassistant.components.device_sun_light_trigger.activate_automation" + ) as mock_activate: + hass.bus.fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + assert len(mock_activate.mock_calls) == 1 diff --git a/tests/components/devolo_home_control/test_config_flow.py b/tests/components/devolo_home_control/test_config_flow.py index aacf33b69c1..01b4603257a 100644 --- a/tests/components/devolo_home_control/test_config_flow.py +++ b/tests/components/devolo_home_control/test_config_flow.py @@ -30,12 +30,7 @@ async def test_form(hass): ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { - "username": "test-username", - "password": "test-password", - "home_control_url": "https://homecontrol.mydevolo.com", - "mydevolo_url": "https://www.mydevolo.com", - }, + {"username": "test-username", "password": "test-password"}, ) assert result2["type"] == "create_entry" @@ -67,12 +62,7 @@ async def test_form_invalid_credentials(hass): ): result = await hass.config_entries.flow.async_configure( result["flow_id"], - { - "username": "test-username", - "password": "test-password", - "home_control_url": "https://homecontrol.mydevolo.com", - "mydevolo_url": "https://www.mydevolo.com", - }, + {"username": "test-username", "password": "test-password"}, ) assert result["errors"] == {"base": "invalid_credentials"} @@ -91,12 +81,51 @@ async def test_form_already_configured(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, - data={ - "username": "test-username", - "password": "test-password", - "home_control_url": "https://homecontrol.mydevolo.com", - "mydevolo_url": "https://www.mydevolo.com", - }, + data={"username": "test-username", "password": "test-password"}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" + + +async def test_form_advanced_options(hass): + """Test if we get the advanced options if user has enabled it.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user", "show_advanced_options": True} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.devolo_home_control.async_setup", return_value=True, + ) as mock_setup, patch( + "homeassistant.components.devolo_home_control.async_setup_entry", + return_value=True, + ) as mock_setup_entry, patch( + "homeassistant.components.devolo_home_control.config_flow.Mydevolo.credentials_valid", + return_value=True, + ), patch( + "homeassistant.components.devolo_home_control.config_flow.Mydevolo.get_gateway_ids", + return_value=["123456"], + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + "home_control_url": "https://test_url.test", + "mydevolo_url": "https://test_mydevolo_url.test", + }, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "devolo Home Control" + assert result2["data"] == { + "username": "test-username", + "password": "test-password", + "home_control_url": "https://test_url.test", + "mydevolo_url": "https://test_mydevolo_url.test", + } + + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 895a95bef7b..6e6cc8f55c6 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -75,6 +75,7 @@ async def test_default_setup(hass, mock_connection_factory): with assert_setup_component(1): await async_setup_component(hass, "sensor", {"sensor": config}) + await hass.async_block_till_done() telegram_callback = connection_factory.call_args_list[0][0][2] @@ -171,6 +172,7 @@ async def test_v4_meter(hass, mock_connection_factory): with assert_setup_component(1): await async_setup_component(hass, "sensor", {"sensor": config}) + await hass.async_block_till_done() telegram_callback = connection_factory.call_args_list[0][0][2] @@ -215,6 +217,7 @@ async def test_v5_meter(hass, mock_connection_factory): with assert_setup_component(1): await async_setup_component(hass, "sensor", {"sensor": config}) + await hass.async_block_till_done() telegram_callback = connection_factory.call_args_list[0][0][2] @@ -259,6 +262,7 @@ async def test_belgian_meter(hass, mock_connection_factory): with assert_setup_component(1): await async_setup_component(hass, "sensor", {"sensor": config}) + await hass.async_block_till_done() telegram_callback = connection_factory.call_args_list[0][0][2] @@ -292,6 +296,7 @@ async def test_belgian_meter_low(hass, mock_connection_factory): with assert_setup_component(1): await async_setup_component(hass, "sensor", {"sensor": config}) + await hass.async_block_till_done() telegram_callback = connection_factory.call_args_list[0][0][2] @@ -315,6 +320,7 @@ async def test_tcp(hass, mock_connection_factory): with assert_setup_component(1): await async_setup_component(hass, "sensor", {"sensor": config}) + await hass.async_block_till_done() assert connection_factory.call_args_list[0][0][0] == "localhost" assert connection_factory.call_args_list[0][0][1] == "1234" diff --git a/tests/components/dte_energy_bridge/test_sensor.py b/tests/components/dte_energy_bridge/test_sensor.py index d95bc31f066..34f0a0a28c3 100644 --- a/tests/components/dte_energy_bridge/test_sensor.py +++ b/tests/components/dte_energy_bridge/test_sensor.py @@ -38,6 +38,7 @@ class TestDteEnergyBridgeSetup(unittest.TestCase): assert setup_component( self.hass, "sensor", {"sensor": DTE_ENERGY_BRIDGE_CONFIG} ) + self.hass.block_till_done() assert "0.411" == self.hass.states.get("sensor.current_energy_usage").state @requests_mock.Mocker() @@ -50,6 +51,7 @@ class TestDteEnergyBridgeSetup(unittest.TestCase): assert setup_component( self.hass, "sensor", {"sensor": DTE_ENERGY_BRIDGE_CONFIG} ) + self.hass.block_till_done() assert "0.411" == self.hass.states.get("sensor.current_energy_usage").state @requests_mock.Mocker() @@ -62,4 +64,5 @@ class TestDteEnergyBridgeSetup(unittest.TestCase): assert setup_component( self.hass, "sensor", {"sensor": DTE_ENERGY_BRIDGE_CONFIG} ) + self.hass.block_till_done() assert "unknown" == self.hass.states.get("sensor.current_energy_usage").state diff --git a/tests/components/dunehd/__init__.py b/tests/components/dunehd/__init__.py new file mode 100644 index 00000000000..5474e62bb8a --- /dev/null +++ b/tests/components/dunehd/__init__.py @@ -0,0 +1 @@ +"""Tests for Dune HD.""" diff --git a/tests/components/dunehd/test_config_flow.py b/tests/components/dunehd/test_config_flow.py new file mode 100644 index 00000000000..2336d2e4a3f --- /dev/null +++ b/tests/components/dunehd/test_config_flow.py @@ -0,0 +1,98 @@ +"""Define tests for the Dune HD config flow.""" +from homeassistant import data_entry_flow +from homeassistant.components.dunehd.const import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import CONF_HOST + +from tests.async_mock import patch +from tests.common import MockConfigEntry + +CONFIG_HOSTNAME = {CONF_HOST: "dunehd-host"} +CONFIG_IP = {CONF_HOST: "10.10.10.12"} + +DUNEHD_STATE = {"protocol_version": "4", "player_state": "navigator"} + + +async def test_import(hass): + """Test that the import works.""" + with patch("pdunehd.DuneHDPlayer.update_state", return_value=DUNEHD_STATE): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=CONFIG_HOSTNAME + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "dunehd-host" + assert result["data"] == {CONF_HOST: "dunehd-host"} + + +async def test_import_cannot_connect(hass): + """Test that errors are shown when cannot connect to the host during import.""" + with patch("pdunehd.DuneHDPlayer.update_state", return_value={}): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=CONFIG_HOSTNAME + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + + +async def test_import_duplicate_error(hass): + """Test that errors are shown when duplicates are added during import.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "dunehd-host"}, title="dunehd-host", + ) + config_entry.add_to_hass(hass) + + with patch("pdunehd.DuneHDPlayer.update_state", return_value=DUNEHD_STATE): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=CONFIG_HOSTNAME + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_user_invalid_host(hass): + """Test that errors are shown when the host is invalid.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "invalid/host"} + ) + + assert result["errors"] == {CONF_HOST: "invalid_host"} + + +async def test_user_cannot_connect(hass): + """Test that errors are shown when cannot connect to the host.""" + with patch("pdunehd.DuneHDPlayer.update_state", return_value={}): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG_IP + ) + + assert result["errors"] == {CONF_HOST: "cannot_connect"} + + +async def test_duplicate_error(hass): + """Test that errors are shown when duplicates are added.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data=CONFIG_HOSTNAME, title="dunehd-host", + ) + config_entry.add_to_hass(hass) + + with patch("pdunehd.DuneHDPlayer.update_state", return_value=DUNEHD_STATE): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG_HOSTNAME + ) + + assert result["errors"] == {CONF_HOST: "already_configured"} + + +async def test_create_entry(hass): + """Test that the user step works.""" + with patch("pdunehd.DuneHDPlayer.update_state", return_value=DUNEHD_STATE): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG_HOSTNAME + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "dunehd-host" + assert result["data"] == {CONF_HOST: "dunehd-host"} diff --git a/tests/components/efergy/test_sensor.py b/tests/components/efergy/test_sensor.py index 166325ee242..252669233e5 100644 --- a/tests/components/efergy/test_sensor.py +++ b/tests/components/efergy/test_sensor.py @@ -85,6 +85,7 @@ class TestEfergySensor(unittest.TestCase): """Test for successfully setting up the Efergy platform.""" mock_responses(mock) assert setup_component(self.hass, "sensor", {"sensor": ONE_SENSOR_CONFIG}) + self.hass.block_till_done() assert "38.21" == self.hass.states.get("sensor.energy_consumed").state assert "1580" == self.hass.states.get("sensor.energy_usage").state @@ -97,6 +98,7 @@ class TestEfergySensor(unittest.TestCase): """Test for multiple sensors in one household.""" mock_responses(mock) assert setup_component(self.hass, "sensor", {"sensor": MULTI_SENSOR_CONFIG}) + self.hass.block_till_done() assert "218" == self.hass.states.get("sensor.efergy_728386").state assert "1808" == self.hass.states.get("sensor.efergy_0").state diff --git a/tests/components/facebox/test_image_processing.py b/tests/components/facebox/test_image_processing.py index b40211d5a91..00ce7af0d01 100644 --- a/tests/components/facebox/test_image_processing.py +++ b/tests/components/facebox/test_image_processing.py @@ -156,6 +156,7 @@ def test_valid_file_path(): async def test_setup_platform(hass, mock_healthybox): """Set up platform with one entity.""" await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) + await hass.async_block_till_done() assert hass.states.get(VALID_ENTITY_ID) @@ -166,12 +167,14 @@ async def test_setup_platform_with_auth(hass, mock_healthybox): valid_config_auth[ip.DOMAIN][CONF_PASSWORD] = MOCK_PASSWORD await async_setup_component(hass, ip.DOMAIN, valid_config_auth) + await hass.async_block_till_done() assert hass.states.get(VALID_ENTITY_ID) async def test_process_image(hass, mock_healthybox, mock_image): """Test successful processing of an image.""" await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) + await hass.async_block_till_done() assert hass.states.get(VALID_ENTITY_ID) face_events = [] @@ -215,6 +218,7 @@ async def test_process_image(hass, mock_healthybox, mock_image): async def test_process_image_errors(hass, mock_healthybox, mock_image, caplog): """Test process_image errors.""" await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) + await hass.async_block_till_done() assert hass.states.get(VALID_ENTITY_ID) # Test connection error. @@ -246,6 +250,7 @@ async def test_teach_service( ): """Test teaching of facebox.""" await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) + await hass.async_block_till_done() assert hass.states.get(VALID_ENTITY_ID) # Patch out 'is_allowed_path' as the mock files aren't allowed @@ -319,6 +324,7 @@ async def test_setup_platform_with_name(hass, mock_healthybox): valid_config_named[ip.DOMAIN][ip.CONF_SOURCE][ip.CONF_NAME] = MOCK_NAME await async_setup_component(hass, ip.DOMAIN, valid_config_named) + await hass.async_block_till_done() assert hass.states.get(named_entity_id) state = hass.states.get(named_entity_id) assert state.attributes.get(CONF_FRIENDLY_NAME) == MOCK_NAME diff --git a/tests/components/fido/test_sensor.py b/tests/components/fido/test_sensor.py index b57232d25ad..c96e6d0ab58 100644 --- a/tests/components/fido/test_sensor.py +++ b/tests/components/fido/test_sensor.py @@ -76,6 +76,7 @@ async def test_fido_sensor(loop, hass): } with assert_setup_component(1): await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() state = hass.states.get("sensor.fido_1112223344_balance") assert state.state == "160.12" assert state.attributes.get("number") == "1112223344" diff --git a/tests/components/filesize/test_sensor.py b/tests/components/filesize/test_sensor.py index b2552f58c6a..11709296375 100644 --- a/tests/components/filesize/test_sensor.py +++ b/tests/components/filesize/test_sensor.py @@ -42,6 +42,7 @@ class TestFileSensor(unittest.TestCase): create_file(TEST_FILE) config = {"sensor": {"platform": "filesize", CONF_FILE_PATHS: [TEST_FILE]}} assert setup_component(self.hass, "sensor", config) + self.hass.block_till_done() assert len(self.hass.states.entity_ids()) == 1 state = self.hass.states.get("sensor.mock_file_test_filesize_txt") assert state.state == "0.0" diff --git a/tests/components/filter/test_sensor.py b/tests/components/filter/test_sensor.py index 238cb366f73..f6eae30c653 100644 --- a/tests/components/filter/test_sensor.py +++ b/tests/components/filter/test_sensor.py @@ -59,6 +59,7 @@ class TestFilterSensor(unittest.TestCase): } with assert_setup_component(0): assert setup_component(self.hass, "sensor", config) + self.hass.block_till_done() def test_chain(self): """Test if filter chaining works.""" @@ -77,6 +78,7 @@ class TestFilterSensor(unittest.TestCase): with assert_setup_component(1, "sensor"): assert setup_component(self.hass, "sensor", config) + self.hass.block_till_done() for value in self.values: self.hass.states.set(config["sensor"]["entity_id"], value.state) @@ -128,6 +130,7 @@ class TestFilterSensor(unittest.TestCase): ): with assert_setup_component(1, "sensor"): assert setup_component(self.hass, "sensor", config) + self.hass.block_till_done() for value in self.values: self.hass.states.set(config["sensor"]["entity_id"], value.state) @@ -176,6 +179,7 @@ class TestFilterSensor(unittest.TestCase): ): with assert_setup_component(1, "sensor"): assert setup_component(self.hass, "sensor", config) + self.hass.block_till_done() self.hass.block_till_done() state = self.hass.states.get("sensor.test") diff --git a/tests/components/flume/test_config_flow.py b/tests/components/flume/test_config_flow.py index a408a652f32..6f2c8d2ed04 100644 --- a/tests/components/flume/test_config_flow.py +++ b/tests/components/flume/test_config_flow.py @@ -3,6 +3,12 @@ import requests.exceptions from homeassistant import config_entries, setup from homeassistant.components.flume.const import DOMAIN +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_PASSWORD, + CONF_USERNAME, +) from tests.async_mock import MagicMock, patch @@ -37,20 +43,20 @@ async def test_form(hass): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "username": "test-username", - "password": "test-password", - "client_id": "client_id", - "client_secret": "client_secret", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_CLIENT_ID: "client_id", + CONF_CLIENT_SECRET: "client_secret", }, ) assert result2["type"] == "create_entry" assert result2["title"] == "test-username" assert result2["data"] == { - "username": "test-username", - "password": "test-password", - "client_id": "client_id", - "client_secret": "client_secret", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_CLIENT_ID: "client_id", + CONF_CLIENT_SECRET: "client_secret", } await hass.async_block_till_done() assert len(mock_setup.mock_calls) == 1 @@ -76,20 +82,20 @@ async def test_form_import(hass): DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={ - "username": "test-username", - "password": "test-password", - "client_id": "client_id", - "client_secret": "client_secret", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_CLIENT_ID: "client_id", + CONF_CLIENT_SECRET: "client_secret", }, ) assert result["type"] == "create_entry" assert result["title"] == "test-username" assert result["data"] == { - "username": "test-username", - "password": "test-password", - "client_id": "client_id", - "client_secret": "client_secret", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_CLIENT_ID: "client_id", + CONF_CLIENT_SECRET: "client_secret", } await hass.async_block_till_done() assert len(mock_setup.mock_calls) == 1 @@ -111,10 +117,10 @@ async def test_form_invalid_auth(hass): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "username": "test-username", - "password": "test-password", - "client_id": "client_id", - "client_secret": "client_secret", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_CLIENT_ID: "client_id", + CONF_CLIENT_SECRET: "client_secret", }, ) @@ -136,10 +142,10 @@ async def test_form_cannot_connect(hass): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "username": "test-username", - "password": "test-password", - "client_id": "client_id", - "client_secret": "client_secret", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_CLIENT_ID: "client_id", + CONF_CLIENT_SECRET: "client_secret", }, ) diff --git a/tests/components/flux/test_switch.py b/tests/components/flux/test_switch.py index ed16e94a283..0eada7da667 100644 --- a/tests/components/flux/test_switch.py +++ b/tests/components/flux/test_switch.py @@ -36,7 +36,7 @@ async def test_valid_config(hass): } }, ) - + await hass.async_block_till_done() state = hass.states.get("switch.flux") assert state assert state.state == "off" @@ -57,6 +57,7 @@ async def test_restore_state_last_on(hass): } }, ) + await hass.async_block_till_done() state = hass.states.get("switch.flux") assert state @@ -78,6 +79,7 @@ async def test_restore_state_last_off(hass): } }, ) + await hass.async_block_till_done() state = hass.states.get("switch.flux") assert state @@ -129,6 +131,7 @@ async def test_flux_when_switch_is_off(hass): assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) + await hass.async_block_till_done() ent1 = platform.ENTITIES[0] @@ -178,6 +181,7 @@ async def test_flux_before_sunrise(hass): assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) + await hass.async_block_till_done() ent1 = platform.ENTITIES[0] @@ -231,6 +235,7 @@ async def test_flux_before_sunrise_known_location(hass): assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) + await hass.async_block_till_done() ent1 = platform.ENTITIES[0] @@ -284,6 +289,7 @@ async def test_flux_after_sunrise_before_sunset(hass): assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) + await hass.async_block_till_done() ent1 = platform.ENTITIES[0] @@ -337,6 +343,7 @@ async def test_flux_after_sunset_before_stop(hass): assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) + await hass.async_block_till_done() ent1 = platform.ENTITIES[0] @@ -391,6 +398,7 @@ async def test_flux_after_stop_before_sunrise(hass): assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) + await hass.async_block_till_done() ent1 = platform.ENTITIES[0] @@ -444,6 +452,7 @@ async def test_flux_with_custom_start_stop_times(hass): assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) + await hass.async_block_till_done() ent1 = platform.ENTITIES[0] @@ -501,6 +510,7 @@ async def test_flux_before_sunrise_stop_next_day(hass): assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) + await hass.async_block_till_done() ent1 = platform.ENTITIES[0] @@ -559,6 +569,7 @@ async def test_flux_after_sunrise_before_sunset_stop_next_day(hass): assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) + await hass.async_block_till_done() ent1 = platform.ENTITIES[0] @@ -617,6 +628,7 @@ async def test_flux_after_sunset_before_midnight_stop_next_day(hass, x): assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) + await hass.async_block_till_done() ent1 = platform.ENTITIES[0] @@ -674,6 +686,7 @@ async def test_flux_after_sunset_after_midnight_stop_next_day(hass): assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) + await hass.async_block_till_done() ent1 = platform.ENTITIES[0] @@ -731,6 +744,7 @@ async def test_flux_after_stop_before_sunrise_stop_next_day(hass): assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) + await hass.async_block_till_done() ent1 = platform.ENTITIES[0] @@ -785,6 +799,7 @@ async def test_flux_with_custom_colortemps(hass): assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) + await hass.async_block_till_done() ent1 = platform.ENTITIES[0] @@ -841,6 +856,7 @@ async def test_flux_with_custom_brightness(hass): assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) + await hass.async_block_till_done() ent1 = platform.ENTITIES[0] @@ -895,6 +911,7 @@ async def test_flux_with_multiple_lights(hass): assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) + await hass.async_block_till_done() ent1, ent2, ent3 = platform.ENTITIES @@ -972,6 +989,7 @@ async def test_flux_with_mired(hass): assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) + await hass.async_block_till_done() ent1 = platform.ENTITIES[0] @@ -1023,6 +1041,7 @@ async def test_flux_with_rgb(hass): assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) + await hass.async_block_till_done() ent1 = platform.ENTITIES[0] diff --git a/tests/components/folder/test_sensor.py b/tests/components/folder/test_sensor.py index 1c465c6a864..9578eadc915 100644 --- a/tests/components/folder/test_sensor.py +++ b/tests/components/folder/test_sensor.py @@ -48,6 +48,7 @@ class TestFolderSensor(unittest.TestCase): create_file(TEST_FILE) config = {"sensor": {"platform": "folder", CONF_FOLDER_PATHS: TEST_DIR}} assert setup_component(self.hass, "sensor", config) + self.hass.block_till_done() assert len(self.hass.states.entity_ids()) == 1 state = self.hass.states.get("sensor.test_folder") assert state.state == "0.0" diff --git a/tests/components/foobot/test_sensor.py b/tests/components/foobot/test_sensor.py index 0a145c88479..d2f55fbda16 100644 --- a/tests/components/foobot/test_sensor.py +++ b/tests/components/foobot/test_sensor.py @@ -39,6 +39,7 @@ async def test_default_setup(hass, aioclient_mock): re.compile("api.foobot.io/v2/device/.*"), text=load_fixture("foobot_data.json") ) assert await async_setup_component(hass, sensor.DOMAIN, {"sensor": VALID_CONFIG}) + await hass.async_block_till_done() metrics = { "co2": ["1232.0", CONCENTRATION_PARTS_PER_MILLION], diff --git a/tests/components/forked_daapd/test_media_player.py b/tests/components/forked_daapd/test_media_player.py index 43a39146199..4f5cb57d66c 100644 --- a/tests/components/forked_daapd/test_media_player.py +++ b/tests/components/forked_daapd/test_media_player.py @@ -364,9 +364,9 @@ def test_master_state(hass, mock_api_object): assert state.attributes[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC assert state.attributes[ATTR_MEDIA_DURATION] == 0.05 assert state.attributes[ATTR_MEDIA_POSITION] == 0.005 - assert state.attributes[ATTR_MEDIA_TITLE] == "Short TTS file" + assert state.attributes[ATTR_MEDIA_TITLE] == "No album" # reversed for url assert state.attributes[ATTR_MEDIA_ARTIST] == "Google" - assert state.attributes[ATTR_MEDIA_ALBUM_NAME] == "No album" + assert state.attributes[ATTR_MEDIA_ALBUM_NAME] == "Short TTS file" # reversed assert state.attributes[ATTR_MEDIA_ALBUM_ARTIST] == "The xx" assert state.attributes[ATTR_MEDIA_TRACK] == 1 assert not state.attributes[ATTR_MEDIA_SHUFFLE] @@ -466,7 +466,7 @@ async def test_last_outputs_master(hass, mock_api_object): assert mock_api_object.set_enabled_outputs.call_count == 2 -async def test_bunch_of_stuff_master(hass, mock_api_object, get_request_return_values): +async def test_bunch_of_stuff_master(hass, get_request_return_values, mock_api_object): """Run bunch of stuff.""" await _service_call(hass, TEST_MASTER_ENTITY_NAME, SERVICE_TURN_ON) await _service_call(hass, TEST_MASTER_ENTITY_NAME, SERVICE_TURN_OFF) @@ -710,6 +710,9 @@ async def test_librespot_java_stuff( await hass.async_block_till_done() state = hass.states.get(TEST_MASTER_ENTITY_NAME) assert state.attributes[ATTR_INPUT_SOURCE] == "librespot-java (pipe)" + # test title and album not reversed when data_kind not url + assert state.attributes[ATTR_MEDIA_TITLE] == "librespot-java" + assert state.attributes[ATTR_MEDIA_ALBUM_NAME] == "some album" async def test_librespot_java_play_media(hass, pipe_control_api_object): diff --git a/tests/components/garmin_connect/__init__py b/tests/components/garmin_connect/__init__.py similarity index 100% rename from tests/components/garmin_connect/__init__py rename to tests/components/garmin_connect/__init__.py diff --git a/tests/components/gdacs/test_geo_location.py b/tests/components/gdacs/test_geo_location.py index 2162340154f..ccffd77a658 100644 --- a/tests/components/gdacs/test_geo_location.py +++ b/tests/components/gdacs/test_geo_location.py @@ -91,6 +91,7 @@ async def test_setup(hass): ) as mock_feed_update: mock_feed_update.return_value = "OK", [mock_entry_1, mock_entry_2, mock_entry_3] assert await async_setup_component(hass, gdacs.DOMAIN, CONFIG) + await hass.async_block_till_done() # Artificially trigger update and collect events. hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -213,6 +214,7 @@ async def test_setup_imperial(hass): ): mock_feed_update.return_value = "OK", [mock_entry_1] assert await async_setup_component(hass, gdacs.DOMAIN, CONFIG) + await hass.async_block_till_done() # Artificially trigger update and collect events. hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index e519e0f9416..a983efa115c 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -23,6 +23,7 @@ async def test_fetching_url(aioclient_mock, hass, hass_client): } }, ) + await hass.async_block_till_done() client = await hass_client() @@ -55,6 +56,7 @@ async def test_fetching_without_verify_ssl(aioclient_mock, hass, hass_client): } }, ) + await hass.async_block_till_done() client = await hass_client() @@ -81,6 +83,7 @@ async def test_fetching_url_with_verify_ssl(aioclient_mock, hass, hass_client): } }, ) + await hass.async_block_till_done() client = await hass_client() @@ -108,6 +111,7 @@ async def test_limit_refetch(aioclient_mock, hass, hass_client): } }, ) + await hass.async_block_till_done() client = await hass_client() @@ -171,6 +175,7 @@ async def test_camera_content_type(aioclient_mock, hass, hass_client): await async_setup_component( hass, "camera", {"camera": [cam_config_svg, cam_config_normal]} ) + await hass.async_block_till_done() client = await hass_client() diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index f0b29539ed5..5e144c3684a 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -76,12 +76,11 @@ async def test_valid_conf(hass): @pytest.fixture -def setup_comp_1(hass): +async def setup_comp_1(hass): """Initialize components.""" hass.config.units = METRIC_SYSTEM - assert hass.loop.run_until_complete( - async_setup_component(hass, "homeassistant", {}) - ) + assert await async_setup_component(hass, "homeassistant", {}) + await hass.async_block_till_done() async def test_heater_input_boolean(hass, setup_comp_1): @@ -104,6 +103,7 @@ async def test_heater_input_boolean(hass, setup_comp_1): } }, ) + await hass.async_block_till_done() assert STATE_OFF == hass.states.get(heater_switch).state @@ -122,6 +122,7 @@ async def test_heater_switch(hass, setup_comp_1): assert await async_setup_component( hass, switch.DOMAIN, {"switch": {"platform": "test"}} ) + await hass.async_block_till_done() heater_switch = switch_1.entity_id assert await async_setup_component( @@ -154,27 +155,26 @@ def _setup_sensor(hass, temp): @pytest.fixture -def setup_comp_2(hass): +async def setup_comp_2(hass): """Initialize components.""" hass.config.units = METRIC_SYSTEM - assert hass.loop.run_until_complete( - async_setup_component( - hass, - DOMAIN, - { - "climate": { - "platform": "generic_thermostat", - "name": "test", - "cold_tolerance": 2, - "hot_tolerance": 4, - "heater": ENT_SWITCH, - "target_sensor": ENT_SENSOR, - "away_temp": 16, - "initial_hvac_mode": HVAC_MODE_HEAT, - } - }, - ) + assert await async_setup_component( + hass, + DOMAIN, + { + "climate": { + "platform": "generic_thermostat", + "name": "test", + "cold_tolerance": 2, + "hot_tolerance": 4, + "heater": ENT_SWITCH, + "target_sensor": ENT_SENSOR, + "away_temp": 16, + "initial_hvac_mode": HVAC_MODE_HEAT, + } + }, ) + await hass.async_block_till_done() async def test_setup_defaults_to_unknown(hass): @@ -195,6 +195,7 @@ async def test_setup_defaults_to_unknown(hass): } }, ) + await hass.async_block_till_done() assert HVAC_MODE_OFF == hass.states.get(ENTITY).state @@ -288,6 +289,7 @@ async def test_sensor_unknown(hass): } }, ) + await hass.async_block_till_done() state = hass.states.get("climate.unknown") assert state.attributes.get("current_temperature") is None @@ -307,6 +309,7 @@ async def test_sensor_unavailable(hass): } }, ) + await hass.async_block_till_done() state = hass.states.get("climate.unavailable") assert state.attributes.get("current_temperature") is None @@ -438,28 +441,27 @@ def _setup_switch(hass, is_on): @pytest.fixture -def setup_comp_3(hass): +async def setup_comp_3(hass): """Initialize components.""" hass.config.temperature_unit = TEMP_CELSIUS - assert hass.loop.run_until_complete( - async_setup_component( - hass, - DOMAIN, - { - "climate": { - "platform": "generic_thermostat", - "name": "test", - "cold_tolerance": 2, - "hot_tolerance": 4, - "away_temp": 30, - "heater": ENT_SWITCH, - "target_sensor": ENT_SENSOR, - "ac_mode": True, - "initial_hvac_mode": HVAC_MODE_COOL, - } - }, - ) + assert await async_setup_component( + hass, + DOMAIN, + { + "climate": { + "platform": "generic_thermostat", + "name": "test", + "cold_tolerance": 2, + "hot_tolerance": 4, + "away_temp": 30, + "heater": ENT_SWITCH, + "target_sensor": ENT_SENSOR, + "ac_mode": True, + "initial_hvac_mode": HVAC_MODE_COOL, + } + }, ) + await hass.async_block_till_done() async def test_set_target_temp_ac_off(hass, setup_comp_3): @@ -584,28 +586,27 @@ async def test_no_state_change_when_operation_mode_off_2(hass, setup_comp_3): @pytest.fixture -def setup_comp_4(hass): +async def setup_comp_4(hass): """Initialize components.""" hass.config.temperature_unit = TEMP_CELSIUS - assert hass.loop.run_until_complete( - async_setup_component( - hass, - DOMAIN, - { - "climate": { - "platform": "generic_thermostat", - "name": "test", - "cold_tolerance": 0.3, - "hot_tolerance": 0.3, - "heater": ENT_SWITCH, - "target_sensor": ENT_SENSOR, - "ac_mode": True, - "min_cycle_duration": datetime.timedelta(minutes=10), - "initial_hvac_mode": HVAC_MODE_COOL, - } - }, - ) + assert await async_setup_component( + hass, + DOMAIN, + { + "climate": { + "platform": "generic_thermostat", + "name": "test", + "cold_tolerance": 0.3, + "hot_tolerance": 0.3, + "heater": ENT_SWITCH, + "target_sensor": ENT_SENSOR, + "ac_mode": True, + "min_cycle_duration": datetime.timedelta(minutes=10), + "initial_hvac_mode": HVAC_MODE_COOL, + } + }, ) + await hass.async_block_till_done() async def test_temp_change_ac_trigger_on_not_long_enough(hass, setup_comp_4): @@ -695,28 +696,27 @@ async def test_mode_change_ac_trigger_on_not_long_enough(hass, setup_comp_4): @pytest.fixture -def setup_comp_5(hass): +async def setup_comp_5(hass): """Initialize components.""" hass.config.temperature_unit = TEMP_CELSIUS - assert hass.loop.run_until_complete( - async_setup_component( - hass, - DOMAIN, - { - "climate": { - "platform": "generic_thermostat", - "name": "test", - "cold_tolerance": 0.3, - "hot_tolerance": 0.3, - "heater": ENT_SWITCH, - "target_sensor": ENT_SENSOR, - "ac_mode": True, - "min_cycle_duration": datetime.timedelta(minutes=10), - "initial_hvac_mode": HVAC_MODE_COOL, - } - }, - ) + assert await async_setup_component( + hass, + DOMAIN, + { + "climate": { + "platform": "generic_thermostat", + "name": "test", + "cold_tolerance": 0.3, + "hot_tolerance": 0.3, + "heater": ENT_SWITCH, + "target_sensor": ENT_SENSOR, + "ac_mode": True, + "min_cycle_duration": datetime.timedelta(minutes=10), + "initial_hvac_mode": HVAC_MODE_COOL, + } + }, ) + await hass.async_block_till_done() async def test_temp_change_ac_trigger_on_not_long_enough_2(hass, setup_comp_5): @@ -806,27 +806,26 @@ async def test_mode_change_ac_trigger_on_not_long_enough_2(hass, setup_comp_5): @pytest.fixture -def setup_comp_6(hass): +async def setup_comp_6(hass): """Initialize components.""" hass.config.temperature_unit = TEMP_CELSIUS - assert hass.loop.run_until_complete( - async_setup_component( - hass, - DOMAIN, - { - "climate": { - "platform": "generic_thermostat", - "name": "test", - "cold_tolerance": 0.3, - "hot_tolerance": 0.3, - "heater": ENT_SWITCH, - "target_sensor": ENT_SENSOR, - "min_cycle_duration": datetime.timedelta(minutes=10), - "initial_hvac_mode": HVAC_MODE_HEAT, - } - }, - ) + assert await async_setup_component( + hass, + DOMAIN, + { + "climate": { + "platform": "generic_thermostat", + "name": "test", + "cold_tolerance": 0.3, + "hot_tolerance": 0.3, + "heater": ENT_SWITCH, + "target_sensor": ENT_SENSOR, + "min_cycle_duration": datetime.timedelta(minutes=10), + "initial_hvac_mode": HVAC_MODE_HEAT, + } + }, ) + await hass.async_block_till_done() async def test_temp_change_heater_trigger_off_not_long_enough(hass, setup_comp_6): @@ -916,31 +915,31 @@ async def test_mode_change_heater_trigger_on_not_long_enough(hass, setup_comp_6) @pytest.fixture -def setup_comp_7(hass): +async def setup_comp_7(hass): """Initialize components.""" hass.config.temperature_unit = TEMP_CELSIUS - assert hass.loop.run_until_complete( - async_setup_component( - hass, - DOMAIN, - { - "climate": { - "platform": "generic_thermostat", - "name": "test", - "cold_tolerance": 0.3, - "hot_tolerance": 0.3, - "heater": ENT_SWITCH, - "target_temp": 25, - "target_sensor": ENT_SENSOR, - "ac_mode": True, - "min_cycle_duration": datetime.timedelta(minutes=15), - "keep_alive": datetime.timedelta(minutes=10), - "initial_hvac_mode": HVAC_MODE_COOL, - } - }, - ) + assert await async_setup_component( + hass, + DOMAIN, + { + "climate": { + "platform": "generic_thermostat", + "name": "test", + "cold_tolerance": 0.3, + "hot_tolerance": 0.3, + "heater": ENT_SWITCH, + "target_temp": 25, + "target_sensor": ENT_SENSOR, + "ac_mode": True, + "min_cycle_duration": datetime.timedelta(minutes=15), + "keep_alive": datetime.timedelta(minutes=10), + "initial_hvac_mode": HVAC_MODE_COOL, + } + }, ) + await hass.async_block_till_done() + async def test_temp_change_ac_trigger_on_long_enough_3(hass, setup_comp_7): """Test if turn on signal is sent at keep-alive intervals.""" @@ -994,29 +993,28 @@ def _send_time_changed(hass, now): @pytest.fixture -def setup_comp_8(hass): +async def setup_comp_8(hass): """Initialize components.""" hass.config.temperature_unit = TEMP_CELSIUS - assert hass.loop.run_until_complete( - async_setup_component( - hass, - DOMAIN, - { - "climate": { - "platform": "generic_thermostat", - "name": "test", - "cold_tolerance": 0.3, - "hot_tolerance": 0.3, - "target_temp": 25, - "heater": ENT_SWITCH, - "target_sensor": ENT_SENSOR, - "min_cycle_duration": datetime.timedelta(minutes=15), - "keep_alive": datetime.timedelta(minutes=10), - "initial_hvac_mode": HVAC_MODE_HEAT, - } - }, - ) + assert await async_setup_component( + hass, + DOMAIN, + { + "climate": { + "platform": "generic_thermostat", + "name": "test", + "cold_tolerance": 0.3, + "hot_tolerance": 0.3, + "target_temp": 25, + "heater": ENT_SWITCH, + "target_sensor": ENT_SENSOR, + "min_cycle_duration": datetime.timedelta(minutes=15), + "keep_alive": datetime.timedelta(minutes=10), + "initial_hvac_mode": HVAC_MODE_HEAT, + } + }, ) + await hass.async_block_till_done() async def test_temp_change_heater_trigger_on_long_enough_2(hass, setup_comp_8): @@ -1066,29 +1064,28 @@ async def test_temp_change_heater_trigger_off_long_enough_2(hass, setup_comp_8): @pytest.fixture -def setup_comp_9(hass): +async def setup_comp_9(hass): """Initialize components.""" hass.config.temperature_unit = TEMP_FAHRENHEIT - assert hass.loop.run_until_complete( - async_setup_component( - hass, - DOMAIN, - { - "climate": { - "platform": "generic_thermostat", - "name": "test", - "cold_tolerance": 0.3, - "hot_tolerance": 0.3, - "target_temp": 25, - "heater": ENT_SWITCH, - "target_sensor": ENT_SENSOR, - "min_cycle_duration": datetime.timedelta(minutes=15), - "keep_alive": datetime.timedelta(minutes=10), - "precision": 0.1, - } - }, - ) + assert await async_setup_component( + hass, + DOMAIN, + { + "climate": { + "platform": "generic_thermostat", + "name": "test", + "cold_tolerance": 0.3, + "hot_tolerance": 0.3, + "target_temp": 25, + "heater": ENT_SWITCH, + "target_sensor": ENT_SENSOR, + "min_cycle_duration": datetime.timedelta(minutes=15), + "keep_alive": datetime.timedelta(minutes=10), + "precision": 0.1, + } + }, ) + await hass.async_block_till_done() async def test_precision(hass, setup_comp_9): @@ -1116,6 +1113,7 @@ async def test_custom_setup_params(hass): }, ) assert result + await hass.async_block_till_done() state = hass.states.get(ENTITY) assert state.attributes.get("min_temp") == MIN_TEMP assert state.attributes.get("max_temp") == MAX_TEMP @@ -1150,7 +1148,7 @@ async def test_restore_state(hass): } }, ) - + await hass.async_block_till_done() state = hass.states.get("climate.test_thermostat") assert state.attributes[ATTR_TEMPERATURE] == 20 assert state.attributes[ATTR_PRESET_MODE] == PRESET_AWAY @@ -1188,7 +1186,7 @@ async def test_no_restore_state(hass): } }, ) - + await hass.async_block_till_done() state = hass.states.get("climate.test_thermostat") assert state.attributes[ATTR_TEMPERATURE] == 22 assert state.state == HVAC_MODE_OFF diff --git a/tests/components/geo_json_events/test_geo_location.py b/tests/components/geo_json_events/test_geo_location.py index d79fa3cb18d..f73eaa11e79 100644 --- a/tests/components/geo_json_events/test_geo_location.py +++ b/tests/components/geo_json_events/test_geo_location.py @@ -73,6 +73,7 @@ async def test_setup(hass): ) with assert_setup_component(1, geo_location.DOMAIN): assert await async_setup_component(hass, geo_location.DOMAIN, CONFIG) + await hass.async_block_till_done() # Artificially trigger update. hass.bus.async_fire(EVENT_HOMEASSISTANT_START) # Collect events. @@ -162,6 +163,7 @@ async def test_setup_with_custom_location(hass): assert await async_setup_component( hass, geo_location.DOMAIN, CONFIG_WITH_CUSTOM_LOCATION ) + await hass.async_block_till_done() # Artificially trigger update. hass.bus.async_fire(EVENT_HOMEASSISTANT_START) @@ -198,6 +200,7 @@ async def test_setup_race_condition(hass): ) as mock_feed: with assert_setup_component(1, geo_location.DOMAIN): assert await async_setup_component(hass, geo_location.DOMAIN, CONFIG) + await hass.async_block_till_done() mock_feed.return_value.update.return_value = "OK", [mock_entry_1] diff --git a/tests/components/geonetnz_quakes/test_geo_location.py b/tests/components/geonetnz_quakes/test_geo_location.py index 89644445ef1..2622cd100b3 100644 --- a/tests/components/geonetnz_quakes/test_geo_location.py +++ b/tests/components/geonetnz_quakes/test_geo_location.py @@ -67,6 +67,7 @@ async def test_setup(hass): ) as mock_feed_update: mock_feed_update.return_value = "OK", [mock_entry_1, mock_entry_2, mock_entry_3] assert await async_setup_component(hass, geonetnz_quakes.DOMAIN, CONFIG) + await hass.async_block_till_done() # Artificially trigger update and collect events. hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -172,6 +173,7 @@ async def test_setup_imperial(hass): ): mock_feed_update.return_value = "OK", [mock_entry_1] assert await async_setup_component(hass, geonetnz_quakes.DOMAIN, CONFIG) + await hass.async_block_till_done() # Artificially trigger update and collect events. hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() diff --git a/tests/components/gogogate2/__init__.py b/tests/components/gogogate2/__init__.py new file mode 100644 index 00000000000..bc867ab646b --- /dev/null +++ b/tests/components/gogogate2/__init__.py @@ -0,0 +1 @@ +"""Tests for the GogoGate2 component.""" diff --git a/tests/components/gogogate2/common.py b/tests/components/gogogate2/common.py new file mode 100644 index 00000000000..d344a31cf4b --- /dev/null +++ b/tests/components/gogogate2/common.py @@ -0,0 +1,162 @@ +"""Common test code.""" +from typing import List, NamedTuple, Optional +from unittest.mock import MagicMock, Mock + +from gogogate2_api import GogoGate2Api, InfoResponse +from gogogate2_api.common import Door, DoorMode, DoorStatus, Network, Outputs, Wifi + +from homeassistant.components import persistent_notification +from homeassistant.components.cover import DOMAIN as COVER_DOMAIN +from homeassistant.components.gogogate2 import async_unload_entry +from homeassistant.components.gogogate2.common import ( + GogoGateDataUpdateCoordinator, + get_data_update_coordinator, +) +import homeassistant.components.gogogate2.const as const +from homeassistant.components.homeassistant import DOMAIN as HA_DOMAIN +from homeassistant.config import async_process_ha_core_config +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_METRIC +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.setup import async_setup_component + +INFO_RESPONSE = InfoResponse( + user="user1", + gogogatename="gogogatename1", + model="", + apiversion="", + remoteaccessenabled=False, + remoteaccess="abcdefg.my-gogogate.com", + firmwareversion="", + apicode="API_CODE", + door1=Door( + door_id=1, + permission=True, + name="Door1", + mode=DoorMode.GARAGE, + status=DoorStatus.OPENED, + sensor=True, + sensorid=None, + camera=False, + events=2, + temperature=None, + ), + door2=Door( + door_id=2, + permission=True, + name=None, + mode=DoorMode.GARAGE, + status=DoorStatus.OPENED, + sensor=True, + sensorid=None, + camera=False, + events=2, + temperature=None, + ), + door3=Door( + door_id=3, + permission=True, + name="Door3", + mode=DoorMode.GARAGE, + status=DoorStatus.OPENED, + sensor=True, + sensorid=None, + camera=False, + events=2, + temperature=None, + ), + outputs=Outputs(output1=True, output2=False, output3=True), + network=Network(ip=""), + wifi=Wifi(SSID="", linkquality="", signal=""), +) + + +class ComponentData(NamedTuple): + """Test data for a mocked component.""" + + api: GogoGate2Api + data_update_coordinator: GogoGateDataUpdateCoordinator + + +class ComponentFactory: + """Manages the setup and unloading of the withing component and profiles.""" + + def __init__(self, hass: HomeAssistant, gogogate_api_mock: Mock) -> None: + """Initialize the object.""" + self._hass = hass + self._gogogate_api_mock = gogogate_api_mock + + @property + def api_class_mock(self): + """Get the api class mock.""" + return self._gogogate_api_mock + + async def configure_component( + self, cover_config: Optional[List[dict]] = None + ) -> None: + """Configure the component.""" + hass_config = { + "homeassistant": {CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC}, + "cover": cover_config or [], + } + + await async_process_ha_core_config(self._hass, hass_config.get("homeassistant")) + assert await async_setup_component(self._hass, HA_DOMAIN, {}) + assert await async_setup_component( + self._hass, persistent_notification.DOMAIN, {} + ) + assert await async_setup_component(self._hass, COVER_DOMAIN, hass_config) + assert await async_setup_component(self._hass, const.DOMAIN, hass_config) + await self._hass.async_block_till_done() + + async def run_config_flow( + self, config_data: dict, api_mock: Optional[GogoGate2Api] = None + ) -> ComponentData: + """Run a config flow.""" + if api_mock is None: + api_mock: GogoGate2Api = MagicMock(spec=GogoGate2Api) + api_mock.info.return_value = INFO_RESPONSE + + self._gogogate_api_mock.reset_mocks() + self._gogogate_api_mock.return_value = api_mock + + result = await self._hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": SOURCE_USER} + ) + assert result + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + + result = await self._hass.config_entries.flow.async_configure( + result["flow_id"], user_input=config_data, + ) + assert result + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == config_data + + await self._hass.async_block_till_done() + + config_entry = next( + iter( + entry + for entry in self._hass.config_entries.async_entries(const.DOMAIN) + if entry.unique_id == "abcdefg" + ) + ) + + return ComponentData( + api=api_mock, + data_update_coordinator=get_data_update_coordinator( + self._hass, config_entry + ), + ) + + async def unload(self) -> None: + """Unload all config entries.""" + config_entries = self._hass.config_entries.async_entries(const.DOMAIN) + for config_entry in config_entries: + await async_unload_entry(self._hass, config_entry) + + await self._hass.async_block_till_done() + assert not self._hass.states.async_entity_ids("gogogate") diff --git a/tests/components/gogogate2/conftest.py b/tests/components/gogogate2/conftest.py new file mode 100644 index 00000000000..31e85c5f14e --- /dev/null +++ b/tests/components/gogogate2/conftest.py @@ -0,0 +1,18 @@ +"""Fixtures for tests.""" + +import pytest + +from homeassistant.core import HomeAssistant + +from .common import ComponentFactory + +from tests.async_mock import patch + + +@pytest.fixture() +def component_factory(hass: HomeAssistant): + """Return a factory for initializing the gogogate2 api.""" + with patch( + "homeassistant.components.gogogate2.common.GogoGate2Api" + ) as gogogate2_api_mock: + yield ComponentFactory(hass, gogogate2_api_mock) diff --git a/tests/components/gogogate2/test_config_flow.py b/tests/components/gogogate2/test_config_flow.py new file mode 100644 index 00000000000..e921df406d2 --- /dev/null +++ b/tests/components/gogogate2/test_config_flow.py @@ -0,0 +1,64 @@ +"""Tests for the GogoGate2 component.""" +from gogogate2_api import GogoGate2Api +from gogogate2_api.common import ApiError +from gogogate2_api.const import ApiErrorCode + +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_FORM + +from .common import ComponentFactory + +from tests.async_mock import MagicMock, patch + + +async def test_auth_fail( + hass: HomeAssistant, component_factory: ComponentFactory +) -> None: + """Test authorization failures.""" + api_mock: GogoGate2Api = MagicMock(spec=GogoGate2Api) + + with patch( + "homeassistant.components.gogogate2.async_setup", return_value=True + ), patch( + "homeassistant.components.gogogate2.async_setup_entry", return_value=True, + ): + await component_factory.configure_component() + component_factory.api_class_mock.return_value = api_mock + + api_mock.reset_mock() + api_mock.info.side_effect = ApiError(ApiErrorCode.CREDENTIALS_INCORRECT, "blah") + result = await hass.config_entries.flow.async_init( + "gogogate2", context={"source": SOURCE_USER} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_IP_ADDRESS: "127.0.0.2", + CONF_USERNAME: "user0", + CONF_PASSWORD: "password0", + }, + ) + assert result + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == { + "base": "invalid_auth", + } + + api_mock.reset_mock() + api_mock.info.side_effect = Exception("Generic connection error.") + result = await hass.config_entries.flow.async_init( + "gogogate2", context={"source": SOURCE_USER} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_IP_ADDRESS: "127.0.0.2", + CONF_USERNAME: "user0", + CONF_PASSWORD: "password0", + }, + ) + assert result + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/gogogate2/test_cover.py b/tests/components/gogogate2/test_cover.py new file mode 100644 index 00000000000..8cffec47e65 --- /dev/null +++ b/tests/components/gogogate2/test_cover.py @@ -0,0 +1,531 @@ +"""Tests for the GogoGate2 component.""" +from datetime import datetime, timedelta +from unittest.mock import MagicMock, patch + +from gogogate2_api import GogoGate2Api +from gogogate2_api.common import ( + ActivateResponse, + ApiError, + Door, + DoorMode, + DoorStatus, + InfoResponse, + Network, + Outputs, + Wifi, +) + +from homeassistant.components.cover import DOMAIN as COVER_DOMAIN +from homeassistant.components.homeassistant import DOMAIN as HA_DOMAIN +from homeassistant.const import ( + CONF_IP_ADDRESS, + CONF_NAME, + CONF_PASSWORD, + CONF_PLATFORM, + CONF_USERNAME, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant + +from .common import ComponentFactory + + +async def test_import_fail( + hass: HomeAssistant, component_factory: ComponentFactory +) -> None: + """Test the failure to import.""" + api = MagicMock(spec=GogoGate2Api) + api.info.side_effect = ApiError(22, "Error") + + component_factory.api_class_mock.return_value = api + + await component_factory.configure_component( + cover_config=[ + { + CONF_PLATFORM: "gogogate2", + CONF_NAME: "cover0", + CONF_IP_ADDRESS: "127.0.1.0", + CONF_USERNAME: "user0", + CONF_PASSWORD: "password0", + } + ] + ) + + entity_ids = hass.states.async_entity_ids(COVER_DOMAIN) + assert not entity_ids + + +async def test_import(hass: HomeAssistant, component_factory: ComponentFactory) -> None: + """Test importing of file based config.""" + api0 = MagicMock(spec=GogoGate2Api) + api0.info.return_value = InfoResponse( + user="user1", + gogogatename="gogogatename0", + model="", + apiversion="", + remoteaccessenabled=False, + remoteaccess="abc123.blah.blah", + firmwareversion="", + apicode="", + door1=Door( + door_id=1, + permission=True, + name="Door1", + mode=DoorMode.GARAGE, + status=DoorStatus.OPENED, + sensor=True, + sensorid=None, + camera=False, + events=2, + temperature=None, + ), + door2=Door( + door_id=2, + permission=True, + name=None, + mode=DoorMode.GARAGE, + status=DoorStatus.UNDEFINED, + sensor=True, + sensorid=None, + camera=False, + events=0, + temperature=None, + ), + door3=Door( + door_id=3, + permission=True, + name=None, + mode=DoorMode.GARAGE, + status=DoorStatus.UNDEFINED, + sensor=True, + sensorid=None, + camera=False, + events=0, + temperature=None, + ), + outputs=Outputs(output1=True, output2=False, output3=True), + network=Network(ip=""), + wifi=Wifi(SSID="", linkquality="", signal=""), + ) + + api1 = MagicMock(spec=GogoGate2Api) + api1.info.return_value = InfoResponse( + user="user1", + gogogatename="gogogatename0", + model="", + apiversion="", + remoteaccessenabled=False, + remoteaccess="321bca.blah.blah", + firmwareversion="", + apicode="", + door1=Door( + door_id=1, + permission=True, + name="Door1", + mode=DoorMode.GARAGE, + status=DoorStatus.CLOSED, + sensor=True, + sensorid=None, + camera=False, + events=2, + temperature=None, + ), + door2=Door( + door_id=2, + permission=True, + name=None, + mode=DoorMode.GARAGE, + status=DoorStatus.UNDEFINED, + sensor=True, + sensorid=None, + camera=False, + events=0, + temperature=None, + ), + door3=Door( + door_id=3, + permission=True, + name=None, + mode=DoorMode.GARAGE, + status=DoorStatus.UNDEFINED, + sensor=True, + sensorid=None, + camera=False, + events=0, + temperature=None, + ), + outputs=Outputs(output1=True, output2=False, output3=True), + network=Network(ip=""), + wifi=Wifi(SSID="", linkquality="", signal=""), + ) + + def new_api(ip_address: str, username: str, password: str) -> GogoGate2Api: + if ip_address == "127.0.1.0": + return api0 + if ip_address == "127.0.1.1": + return api1 + raise Exception(f"Untested ip address {ip_address}") + + component_factory.api_class_mock.side_effect = new_api + + await component_factory.configure_component( + cover_config=[ + { + CONF_PLATFORM: "gogogate2", + CONF_NAME: "cover0", + CONF_IP_ADDRESS: "127.0.1.0", + CONF_USERNAME: "user0", + CONF_PASSWORD: "password0", + }, + { + CONF_PLATFORM: "gogogate2", + CONF_NAME: "cover1", + CONF_IP_ADDRESS: "127.0.1.1", + CONF_USERNAME: "user1", + CONF_PASSWORD: "password1", + }, + ] + ) + entity_ids = hass.states.async_entity_ids(COVER_DOMAIN) + assert entity_ids is not None + assert len(entity_ids) == 2 + assert "cover.door1" in entity_ids + assert "cover.door1_2" in entity_ids + + await component_factory.unload() + + +async def test_cover_update( + hass: HomeAssistant, component_factory: ComponentFactory +) -> None: + """Test cover.""" + await component_factory.configure_component() + component_data = await component_factory.run_config_flow( + config_data={ + CONF_IP_ADDRESS: "127.0.0.2", + CONF_USERNAME: "user0", + CONF_PASSWORD: "password0", + } + ) + + assert hass.states.async_entity_ids(COVER_DOMAIN) + + state = hass.states.get("cover.door1") + assert state + assert state.state == STATE_OPEN + assert state.attributes["friendly_name"] == "Door1" + assert state.attributes["supported_features"] == 3 + assert state.attributes["device_class"] == "garage" + + component_data.data_update_coordinator.api.info.return_value = InfoResponse( + user="user1", + gogogatename="gogogatename0", + model="", + apiversion="", + remoteaccessenabled=False, + remoteaccess="abc123.blah.blah", + firmwareversion="", + apicode="", + door1=Door( + door_id=1, + permission=True, + name="Door1", + mode=DoorMode.GARAGE, + status=DoorStatus.OPENED, + sensor=True, + sensorid=None, + camera=False, + events=2, + temperature=None, + ), + door2=Door( + door_id=2, + permission=True, + name=None, + mode=DoorMode.GARAGE, + status=DoorStatus.UNDEFINED, + sensor=True, + sensorid=None, + camera=False, + events=0, + temperature=None, + ), + door3=Door( + door_id=3, + permission=True, + name=None, + mode=DoorMode.GARAGE, + status=DoorStatus.UNDEFINED, + sensor=True, + sensorid=None, + camera=False, + events=0, + temperature=None, + ), + outputs=Outputs(output1=True, output2=False, output3=True), + network=Network(ip=""), + wifi=Wifi(SSID="", linkquality="", signal=""), + ) + await component_data.data_update_coordinator.async_refresh() + await hass.async_block_till_done() + state = hass.states.get("cover.door1") + assert state + assert state.state == STATE_OPEN + + component_data.data_update_coordinator.api.info.return_value = InfoResponse( + user="user1", + gogogatename="gogogatename0", + model="", + apiversion="", + remoteaccessenabled=False, + remoteaccess="abc123.blah.blah", + firmwareversion="", + apicode="", + door1=Door( + door_id=1, + permission=True, + name="Door1", + mode=DoorMode.GARAGE, + status=DoorStatus.CLOSED, + sensor=True, + sensorid=None, + camera=False, + events=2, + temperature=None, + ), + door2=Door( + door_id=2, + permission=True, + name=None, + mode=DoorMode.GARAGE, + status=DoorStatus.UNDEFINED, + sensor=True, + sensorid=None, + camera=False, + events=0, + temperature=None, + ), + door3=Door( + door_id=3, + permission=True, + name=None, + mode=DoorMode.GARAGE, + status=DoorStatus.UNDEFINED, + sensor=True, + sensorid=None, + camera=False, + events=0, + temperature=None, + ), + outputs=Outputs(output1=True, output2=False, output3=True), + network=Network(ip=""), + wifi=Wifi(SSID="", linkquality="", signal=""), + ) + await component_data.data_update_coordinator.async_refresh() + await hass.async_block_till_done() + state = hass.states.get("cover.door1") + assert state + assert state.state == STATE_CLOSED + + +async def test_open_close( + hass: HomeAssistant, component_factory: ComponentFactory +) -> None: + """Test open and close.""" + closed_door_response = InfoResponse( + user="user1", + gogogatename="gogogatename0", + model="", + apiversion="", + remoteaccessenabled=False, + remoteaccess="abc123.blah.blah", + firmwareversion="", + apicode="", + door1=Door( + door_id=1, + permission=True, + name="Door1", + mode=DoorMode.GARAGE, + status=DoorStatus.CLOSED, + sensor=True, + sensorid=None, + camera=False, + events=2, + temperature=None, + ), + door2=Door( + door_id=2, + permission=True, + name=None, + mode=DoorMode.GARAGE, + status=DoorStatus.UNDEFINED, + sensor=True, + sensorid=None, + camera=False, + events=0, + temperature=None, + ), + door3=Door( + door_id=3, + permission=True, + name=None, + mode=DoorMode.GARAGE, + status=DoorStatus.UNDEFINED, + sensor=True, + sensorid=None, + camera=False, + events=0, + temperature=None, + ), + outputs=Outputs(output1=True, output2=False, output3=True), + network=Network(ip=""), + wifi=Wifi(SSID="", linkquality="", signal=""), + ) + + await component_factory.configure_component() + assert hass.states.get("cover.door1") is None + + component_data = await component_factory.run_config_flow( + config_data={ + CONF_IP_ADDRESS: "127.0.0.2", + CONF_USERNAME: "user0", + CONF_PASSWORD: "password0", + } + ) + + component_data.api.activate.return_value = ActivateResponse(result=True) + + assert hass.states.get("cover.door1").state == STATE_OPEN + await hass.services.async_call( + COVER_DOMAIN, "close_cover", service_data={"entity_id": "cover.door1"}, + ) + await hass.async_block_till_done() + component_data.api.close_door.assert_called_with(1) + await hass.services.async_call( + HA_DOMAIN, "update_entity", service_data={"entity_id": "cover.door1"}, + ) + await hass.async_block_till_done() + assert hass.states.get("cover.door1").state == STATE_CLOSING + + component_data.data_update_coordinator.api.info.return_value = closed_door_response + await component_data.data_update_coordinator.async_refresh() + await hass.async_block_till_done() + assert hass.states.get("cover.door1").state == STATE_CLOSED + + # Assert mid state changed when new status is received. + await hass.services.async_call( + COVER_DOMAIN, "open_cover", service_data={"entity_id": "cover.door1"}, + ) + await hass.async_block_till_done() + component_data.api.open_door.assert_called_with(1) + await hass.services.async_call( + HA_DOMAIN, "update_entity", service_data={"entity_id": "cover.door1"}, + ) + await hass.async_block_till_done() + assert hass.states.get("cover.door1").state == STATE_OPENING + + # Assert the mid state does not change when the same status is returned. + component_data.data_update_coordinator.api.info.return_value = closed_door_response + await component_data.data_update_coordinator.async_refresh() + component_data.data_update_coordinator.api.info.return_value = closed_door_response + await component_data.data_update_coordinator.async_refresh() + await hass.services.async_call( + HA_DOMAIN, "update_entity", service_data={"entity_id": "cover.door1"}, + ) + await hass.async_block_till_done() + assert hass.states.get("cover.door1").state == STATE_OPENING + + # Assert the mid state times out. + with patch("homeassistant.components.gogogate2.cover.datetime") as datetime_mock: + datetime_mock.now.return_value = datetime.now() + timedelta(seconds=60.1) + component_data.data_update_coordinator.api.info.return_value = ( + closed_door_response + ) + await component_data.data_update_coordinator.async_refresh() + await hass.services.async_call( + HA_DOMAIN, "update_entity", service_data={"entity_id": "cover.door1"}, + ) + await hass.async_block_till_done() + assert hass.states.get("cover.door1").state == STATE_CLOSED + + +async def test_availability( + hass: HomeAssistant, component_factory: ComponentFactory +) -> None: + """Test open and close.""" + closed_door_response = InfoResponse( + user="user1", + gogogatename="gogogatename0", + model="", + apiversion="", + remoteaccessenabled=False, + remoteaccess="abc123.blah.blah", + firmwareversion="", + apicode="", + door1=Door( + door_id=1, + permission=True, + name="Door1", + mode=DoorMode.GARAGE, + status=DoorStatus.CLOSED, + sensor=True, + sensorid=None, + camera=False, + events=2, + temperature=None, + ), + door2=Door( + door_id=2, + permission=True, + name=None, + mode=DoorMode.GARAGE, + status=DoorStatus.UNDEFINED, + sensor=True, + sensorid=None, + camera=False, + events=0, + temperature=None, + ), + door3=Door( + door_id=3, + permission=True, + name=None, + mode=DoorMode.GARAGE, + status=DoorStatus.UNDEFINED, + sensor=True, + sensorid=None, + camera=False, + events=0, + temperature=None, + ), + outputs=Outputs(output1=True, output2=False, output3=True), + network=Network(ip=""), + wifi=Wifi(SSID="", linkquality="", signal=""), + ) + + await component_factory.configure_component() + assert hass.states.get("cover.door1") is None + + component_data = await component_factory.run_config_flow( + config_data={ + CONF_IP_ADDRESS: "127.0.0.2", + CONF_USERNAME: "user0", + CONF_PASSWORD: "password0", + } + ) + assert hass.states.get("cover.door1").state == STATE_OPEN + + component_data.api.info.side_effect = Exception("Error") + await component_data.data_update_coordinator.async_refresh() + await hass.async_block_till_done() + assert hass.states.get("cover.door1").state == STATE_UNAVAILABLE + + component_data.api.info.side_effect = None + component_data.api.info.return_value = closed_door_response + await component_data.data_update_coordinator.async_refresh() + await hass.async_block_till_done() + assert hass.states.get("cover.door1").state == STATE_CLOSED diff --git a/tests/components/gogogate2/test_init.py b/tests/components/gogogate2/test_init.py new file mode 100644 index 00000000000..8788590407f --- /dev/null +++ b/tests/components/gogogate2/test_init.py @@ -0,0 +1,28 @@ +"""Tests for the GogoGate2 component.""" +import pytest + +from homeassistant.components.gogogate2 import async_setup_entry +from homeassistant.components.gogogate2.common import GogoGateDataUpdateCoordinator +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from tests.async_mock import MagicMock, patch +from tests.common import MockConfigEntry + + +async def test_auth_fail(hass: HomeAssistant) -> None: + """Test authorization failures.""" + + coordinator_mock: GogoGateDataUpdateCoordinator = MagicMock( + spec=GogoGateDataUpdateCoordinator + ) + coordinator_mock.last_update_success = False + + config_entry = MockConfigEntry() + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.gogogate2.get_data_update_coordinator", + return_value=coordinator_mock, + ), pytest.raises(ConfigEntryNotReady): + await async_setup_entry(hass, config_entry) diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index 6f7ce74ce62..e3412a01f5e 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -2,6 +2,7 @@ import pytest import homeassistant.components.google as google +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.setup import async_setup_component from tests.async_mock import patch @@ -24,7 +25,7 @@ def mock_google_setup(hass): async def test_setup_component(hass, google_setup): """Test setup component.""" - config = {"google": {"client_id": "id", "client_secret": "secret"}} + config = {"google": {CONF_CLIENT_ID: "id", CONF_CLIENT_SECRET: "secret"}} assert await async_setup_component(hass, "google", config) @@ -51,8 +52,8 @@ async def test_found_calendar(hass, google_setup, mock_next_event, test_calendar """Test when a calendar is found.""" config = { "google": { - "client_id": "id", - "client_secret": "secret", + CONF_CLIENT_ID: "id", + CONF_CLIENT_SECRET: "secret", "track_new_calendar": True, } } diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index 8f6b0908f83..e8b5cd87be0 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -167,8 +167,10 @@ DEMO_DEVICES = [ "action.devices.traits.OnOff", "action.devices.traits.Volume", "action.devices.traits.Modes", + "action.devices.traits.TransportControl", + "action.devices.traits.MediaState", ], - "type": "action.devices.types.SWITCH", + "type": "action.devices.types.SETTOP", "willReportState": False, }, { @@ -178,15 +180,22 @@ DEMO_DEVICES = [ "action.devices.traits.OnOff", "action.devices.traits.Volume", "action.devices.traits.Modes", + "action.devices.traits.TransportControl", + "action.devices.traits.MediaState", ], - "type": "action.devices.types.SWITCH", + "type": "action.devices.types.SETTOP", "willReportState": False, }, { "id": "media_player.lounge_room", "name": {"name": "Lounge room"}, - "traits": ["action.devices.traits.OnOff", "action.devices.traits.Modes"], - "type": "action.devices.types.SWITCH", + "traits": [ + "action.devices.traits.OnOff", + "action.devices.traits.Modes", + "action.devices.traits.TransportControl", + "action.devices.traits.MediaState", + ], + "type": "action.devices.types.SETTOP", "willReportState": False, }, { @@ -196,8 +205,10 @@ DEMO_DEVICES = [ "action.devices.traits.OnOff", "action.devices.traits.Volume", "action.devices.traits.Modes", + "action.devices.traits.TransportControl", + "action.devices.traits.MediaState", ], - "type": "action.devices.types.SWITCH", + "type": "action.devices.types.SETTOP", "willReportState": False, }, { diff --git a/tests/components/google_assistant/test_helpers.py b/tests/components/google_assistant/test_helpers.py index 05afd29a5bd..c58f44c06c8 100644 --- a/tests/components/google_assistant/test_helpers.py +++ b/tests/components/google_assistant/test_helpers.py @@ -44,15 +44,17 @@ async def test_google_entity_sync_serialize_with_local_sdk(hass): config.async_enable_local_sdk() - serialized = await entity.sync_serialize(None) - assert serialized["otherDeviceIds"] == [{"deviceId": "light.ceiling_lights"}] - assert serialized["customData"] == { - "httpPort": 1234, - "httpSSL": True, - "proxyDeviceId": None, - "webhookId": "mock-webhook-id", - "baseUrl": "https://hostname:1234", - } + with patch("homeassistant.helpers.instance_id.async_get", return_value="abcdef"): + serialized = await entity.sync_serialize(None) + assert serialized["otherDeviceIds"] == [{"deviceId": "light.ceiling_lights"}] + assert serialized["customData"] == { + "httpPort": 1234, + "httpSSL": True, + "proxyDeviceId": None, + "webhookId": "mock-webhook-id", + "baseUrl": "https://hostname:1234", + "uuid": "abcdef", + } for device_type in NOT_EXPOSE_LOCAL: with patch( diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index e619356717f..e9795a9320f 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -289,6 +289,7 @@ async def test_query_message(hass): async def test_execute(hass): """Test an execute command.""" await async_setup_component(hass, "light", {"light": {"platform": "demo"}}) + await hass.async_block_till_done() await hass.services.async_call( "light", "turn_off", {"entity_id": "light.ceiling_lights"}, blocking=True @@ -743,7 +744,7 @@ async def test_device_class_cover(hass, device_class, google_type): @pytest.mark.parametrize( "device_class,google_type", [ - ("non_existing_class", "action.devices.types.SWITCH"), + ("non_existing_class", "action.devices.types.SETTOP"), ("tv", "action.devices.types.TV"), ], ) @@ -768,10 +769,16 @@ async def test_device_media_player(hass, device_class, google_type): "agentUserId": "test-agent", "devices": [ { - "attributes": {}, + "attributes": { + "supportActivityState": True, + "supportPlaybackState": True, + }, "id": sensor.entity_id, "name": {"name": sensor.name}, - "traits": ["action.devices.traits.OnOff"], + "traits": [ + "action.devices.traits.OnOff", + "action.devices.traits.MediaState", + ], "type": google_type, "willReportState": False, } diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 801f4c1b5ba..3dca89b8193 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1,4 +1,5 @@ """Tests for the Google Assistant traits.""" +from datetime import datetime, timedelta import logging import pytest @@ -11,6 +12,7 @@ from homeassistant.components import ( fan, group, input_boolean, + input_select, light, lock, media_player, @@ -34,8 +36,13 @@ from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, + STATE_IDLE, STATE_OFF, STATE_ON, + STATE_PAUSED, + STATE_PLAYING, + STATE_STANDBY, + STATE_UNAVAILABLE, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, @@ -1267,8 +1274,8 @@ async def test_fan_speed(hass): assert calls[0].data == {"entity_id": "fan.living_room_fan", "speed": "medium"} -async def test_modes(hass): - """Test Mode trait.""" +async def test_modes_media_player(hass): + """Test Media Player Mode trait.""" assert helpers.get_google_type(media_player.DOMAIN, None) is not None assert trait.ModesTrait.supported( media_player.DOMAIN, media_player.SUPPORT_SELECT_SOURCE, None @@ -1351,6 +1358,72 @@ async def test_modes(hass): assert calls[0].data == {"entity_id": "media_player.living_room", "source": "media"} +async def test_modes_input_select(hass): + """Test Input Select Mode trait.""" + assert helpers.get_google_type(input_select.DOMAIN, None) is not None + assert trait.ModesTrait.supported(input_select.DOMAIN, None, None) + + trt = trait.ModesTrait( + hass, + State( + "input_select.bla", + "abc", + attributes={input_select.ATTR_OPTIONS: ["abc", "123", "xyz"]}, + ), + BASIC_CONFIG, + ) + + attribs = trt.sync_attributes() + assert attribs == { + "availableModes": [ + { + "name": "option", + "name_values": [ + { + "name_synonym": ["option", "setting", "mode", "value"], + "lang": "en", + } + ], + "settings": [ + { + "setting_name": "abc", + "setting_values": [{"setting_synonym": ["abc"], "lang": "en"}], + }, + { + "setting_name": "123", + "setting_values": [{"setting_synonym": ["123"], "lang": "en"}], + }, + { + "setting_name": "xyz", + "setting_values": [{"setting_synonym": ["xyz"], "lang": "en"}], + }, + ], + "ordered": False, + } + ] + } + + assert trt.query_attributes() == { + "currentModeSettings": {"option": "abc"}, + "on": True, + "online": True, + } + + assert trt.can_execute( + trait.COMMAND_MODES, params={"updateModeSettings": {"option": "xyz"}}, + ) + + calls = async_mock_service( + hass, input_select.DOMAIN, input_select.SERVICE_SELECT_OPTION + ) + await trt.execute( + trait.COMMAND_MODES, BASIC_DATA, {"updateModeSettings": {"option": "xyz"}}, {}, + ) + + assert len(calls) == 1 + assert calls[0].data == {"entity_id": "input_select.bla", "option": "xyz"} + + async def test_sound_modes(hass): """Test Mode trait.""" assert helpers.get_google_type(media_player.DOMAIN, None) is not None @@ -1783,3 +1856,167 @@ async def test_humidity_setting_sensor_data(hass, state, ambient): with pytest.raises(helpers.SmartHomeError) as err: await trt.execute(trait.COMMAND_ONOFF, BASIC_DATA, {"on": False}, {}) assert err.value.code == const.ERR_NOT_SUPPORTED + + +async def test_transport_control(hass): + """Test the TransportControlTrait.""" + assert helpers.get_google_type(media_player.DOMAIN, None) is not None + + for feature in trait.MEDIA_COMMAND_SUPPORT_MAPPING.values(): + assert trait.TransportControlTrait.supported(media_player.DOMAIN, feature, None) + + now = datetime(2020, 1, 1) + + trt = trait.TransportControlTrait( + hass, + State( + "media_player.bla", + media_player.STATE_PLAYING, + { + media_player.ATTR_MEDIA_POSITION: 100, + media_player.ATTR_MEDIA_DURATION: 200, + media_player.ATTR_MEDIA_POSITION_UPDATED_AT: now + - timedelta(seconds=10), + media_player.ATTR_MEDIA_VOLUME_LEVEL: 0.5, + ATTR_SUPPORTED_FEATURES: media_player.SUPPORT_PLAY + | media_player.SUPPORT_STOP, + }, + ), + BASIC_CONFIG, + ) + + assert trt.sync_attributes() == { + "transportControlSupportedCommands": ["RESUME", "STOP"] + } + assert trt.query_attributes() == {} + + # COMMAND_MEDIA_SEEK_RELATIVE + calls = async_mock_service( + hass, media_player.DOMAIN, media_player.SERVICE_MEDIA_SEEK + ) + + # Patch to avoid time ticking over during the command failing the test + with patch("homeassistant.util.dt.utcnow", return_value=now): + await trt.execute( + trait.COMMAND_MEDIA_SEEK_RELATIVE, + BASIC_DATA, + {"relativePositionMs": 10000}, + {}, + ) + assert len(calls) == 1 + assert calls[0].data == { + ATTR_ENTITY_ID: "media_player.bla", + # 100s (current position) + 10s (from command) + 10s (from updated_at) + media_player.ATTR_MEDIA_SEEK_POSITION: 120, + } + + # COMMAND_MEDIA_SEEK_TO_POSITION + calls = async_mock_service( + hass, media_player.DOMAIN, media_player.SERVICE_MEDIA_SEEK + ) + await trt.execute( + trait.COMMAND_MEDIA_SEEK_TO_POSITION, BASIC_DATA, {"absPositionMs": 50000}, {} + ) + assert len(calls) == 1 + assert calls[0].data == { + ATTR_ENTITY_ID: "media_player.bla", + media_player.ATTR_MEDIA_SEEK_POSITION: 50, + } + + # COMMAND_MEDIA_NEXT + calls = async_mock_service( + hass, media_player.DOMAIN, media_player.SERVICE_MEDIA_NEXT_TRACK + ) + await trt.execute(trait.COMMAND_MEDIA_NEXT, BASIC_DATA, {}, {}) + assert len(calls) == 1 + assert calls[0].data == {ATTR_ENTITY_ID: "media_player.bla"} + + # COMMAND_MEDIA_PAUSE + calls = async_mock_service( + hass, media_player.DOMAIN, media_player.SERVICE_MEDIA_PAUSE + ) + await trt.execute(trait.COMMAND_MEDIA_PAUSE, BASIC_DATA, {}, {}) + assert len(calls) == 1 + assert calls[0].data == {ATTR_ENTITY_ID: "media_player.bla"} + + # COMMAND_MEDIA_PREVIOUS + calls = async_mock_service( + hass, media_player.DOMAIN, media_player.SERVICE_MEDIA_PREVIOUS_TRACK + ) + await trt.execute(trait.COMMAND_MEDIA_PREVIOUS, BASIC_DATA, {}, {}) + assert len(calls) == 1 + assert calls[0].data == {ATTR_ENTITY_ID: "media_player.bla"} + + # COMMAND_MEDIA_RESUME + calls = async_mock_service( + hass, media_player.DOMAIN, media_player.SERVICE_MEDIA_PLAY + ) + await trt.execute(trait.COMMAND_MEDIA_RESUME, BASIC_DATA, {}, {}) + assert len(calls) == 1 + assert calls[0].data == {ATTR_ENTITY_ID: "media_player.bla"} + + # COMMAND_MEDIA_SHUFFLE + calls = async_mock_service( + hass, media_player.DOMAIN, media_player.SERVICE_SHUFFLE_SET + ) + await trt.execute(trait.COMMAND_MEDIA_SHUFFLE, BASIC_DATA, {}, {}) + assert len(calls) == 1 + assert calls[0].data == { + ATTR_ENTITY_ID: "media_player.bla", + media_player.ATTR_MEDIA_SHUFFLE: True, + } + + # COMMAND_MEDIA_STOP + calls = async_mock_service( + hass, media_player.DOMAIN, media_player.SERVICE_MEDIA_STOP + ) + await trt.execute(trait.COMMAND_MEDIA_STOP, BASIC_DATA, {}, {}) + assert len(calls) == 1 + assert calls[0].data == {ATTR_ENTITY_ID: "media_player.bla"} + + +@pytest.mark.parametrize( + "state", + ( + STATE_OFF, + STATE_IDLE, + STATE_PLAYING, + STATE_ON, + STATE_PAUSED, + STATE_STANDBY, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + ), +) +async def test_media_state(hass, state): + """Test the MediaStateTrait.""" + assert helpers.get_google_type(media_player.DOMAIN, None) is not None + + assert trait.TransportControlTrait.supported( + media_player.DOMAIN, media_player.SUPPORT_PLAY, None + ) + + trt = trait.MediaStateTrait( + hass, + State( + "media_player.bla", + state, + { + media_player.ATTR_MEDIA_POSITION: 100, + media_player.ATTR_MEDIA_DURATION: 200, + media_player.ATTR_MEDIA_VOLUME_LEVEL: 0.5, + ATTR_SUPPORTED_FEATURES: media_player.SUPPORT_PLAY + | media_player.SUPPORT_STOP, + }, + ), + BASIC_CONFIG, + ) + + assert trt.sync_attributes() == { + "supportActivityState": True, + "supportPlaybackState": True, + } + assert trt.query_attributes() == { + "activityState": trt.activity_lookup.get(state), + "playbackState": trt.playback_lookup.get(state), + } diff --git a/tests/components/group/test_cover.py b/tests/components/group/test_cover.py index 42345536120..1adad3e3d85 100644 --- a/tests/components/group/test_cover.py +++ b/tests/components/group/test_cover.py @@ -28,7 +28,9 @@ from homeassistant.const import ( SERVICE_TOGGLE, SERVICE_TOGGLE_COVER_TILT, STATE_CLOSED, + STATE_CLOSING, STATE_OPEN, + STATE_OPENING, ) from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -41,7 +43,7 @@ DEMO_COVER_POS = "cover.hall_window" DEMO_COVER_TILT = "cover.living_room_window" DEMO_TILT = "cover.tilt_demo" -CONFIG = { +CONFIG_ALL = { DOMAIN: [ {"platform": "demo"}, { @@ -51,26 +53,36 @@ CONFIG = { ] } +CONFIG_POS = { + DOMAIN: [ + {"platform": "demo"}, + { + "platform": "group", + CONF_ENTITIES: [DEMO_COVER_POS, DEMO_COVER_TILT, DEMO_TILT], + }, + ] +} + +CONFIG_ATTRIBUTES = { + DOMAIN: { + "platform": "group", + CONF_ENTITIES: [DEMO_COVER, DEMO_COVER_POS, DEMO_COVER_TILT, DEMO_TILT], + } +} + @pytest.fixture -async def setup_comp(hass): +async def setup_comp(hass, config_count): """Set up group cover component.""" - with assert_setup_component(2, DOMAIN): - await async_setup_component(hass, DOMAIN, CONFIG) - - -async def test_attributes(hass): - """Test handling of state attributes.""" - config = { - DOMAIN: { - "platform": "group", - CONF_ENTITIES: [DEMO_COVER, DEMO_COVER_POS, DEMO_COVER_TILT, DEMO_TILT], - } - } - - with assert_setup_component(1, DOMAIN): + config, count = config_count + with assert_setup_component(count, DOMAIN): await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + +@pytest.mark.parametrize("config_count", [(CONFIG_ATTRIBUTES, 1)]) +async def test_attributes(hass, setup_comp): + """Test handling of state attributes.""" state = hass.states.get(COVER_GROUP) assert state.state == STATE_CLOSED assert state.attributes[ATTR_FRIENDLY_NAME] == DEFAULT_NAME @@ -191,11 +203,13 @@ async def test_attributes(hass): assert state.attributes[ATTR_ASSUMED_STATE] is True +@pytest.mark.parametrize("config_count", [(CONFIG_ALL, 2)]) async def test_open_covers(hass, setup_comp): """Test open cover function.""" await hass.services.async_call( DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: COVER_GROUP}, blocking=True ) + for _ in range(10): future = dt_util.utcnow() + timedelta(seconds=1) async_fire_time_changed(hass, future) @@ -210,11 +224,13 @@ async def test_open_covers(hass, setup_comp): assert hass.states.get(DEMO_COVER_TILT).attributes[ATTR_CURRENT_POSITION] == 100 +@pytest.mark.parametrize("config_count", [(CONFIG_ALL, 2)]) async def test_close_covers(hass, setup_comp): """Test close cover function.""" await hass.services.async_call( DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: COVER_GROUP}, blocking=True ) + for _ in range(10): future = dt_util.utcnow() + timedelta(seconds=1) async_fire_time_changed(hass, future) @@ -229,6 +245,7 @@ async def test_close_covers(hass, setup_comp): assert hass.states.get(DEMO_COVER_TILT).attributes[ATTR_CURRENT_POSITION] == 0 +@pytest.mark.parametrize("config_count", [(CONFIG_ALL, 2)]) async def test_toggle_covers(hass, setup_comp): """Test toggle cover function.""" # Start covers in open state @@ -278,6 +295,7 @@ async def test_toggle_covers(hass, setup_comp): assert hass.states.get(DEMO_COVER_TILT).attributes[ATTR_CURRENT_POSITION] == 100 +@pytest.mark.parametrize("config_count", [(CONFIG_ALL, 2)]) async def test_stop_covers(hass, setup_comp): """Test stop cover function.""" await hass.services.async_call( @@ -303,6 +321,7 @@ async def test_stop_covers(hass, setup_comp): assert hass.states.get(DEMO_COVER_TILT).attributes[ATTR_CURRENT_POSITION] == 80 +@pytest.mark.parametrize("config_count", [(CONFIG_ALL, 2)]) async def test_set_cover_position(hass, setup_comp): """Test set cover position function.""" await hass.services.async_call( @@ -325,6 +344,7 @@ async def test_set_cover_position(hass, setup_comp): assert hass.states.get(DEMO_COVER_TILT).attributes[ATTR_CURRENT_POSITION] == 50 +@pytest.mark.parametrize("config_count", [(CONFIG_ALL, 2)]) async def test_open_tilts(hass, setup_comp): """Test open tilt function.""" await hass.services.async_call( @@ -344,6 +364,7 @@ async def test_open_tilts(hass, setup_comp): ) +@pytest.mark.parametrize("config_count", [(CONFIG_ALL, 2)]) async def test_close_tilts(hass, setup_comp): """Test close tilt function.""" await hass.services.async_call( @@ -361,6 +382,7 @@ async def test_close_tilts(hass, setup_comp): assert hass.states.get(DEMO_COVER_TILT).attributes[ATTR_CURRENT_TILT_POSITION] == 0 +@pytest.mark.parametrize("config_count", [(CONFIG_ALL, 2)]) async def test_toggle_tilts(hass, setup_comp): """Test toggle tilt function.""" # Start tilted open @@ -413,6 +435,7 @@ async def test_toggle_tilts(hass, setup_comp): ) +@pytest.mark.parametrize("config_count", [(CONFIG_ALL, 2)]) async def test_stop_tilts(hass, setup_comp): """Test stop tilts function.""" await hass.services.async_call( @@ -436,6 +459,7 @@ async def test_stop_tilts(hass, setup_comp): assert hass.states.get(DEMO_COVER_TILT).attributes[ATTR_CURRENT_TILT_POSITION] == 60 +@pytest.mark.parametrize("config_count", [(CONFIG_ALL, 2)]) async def test_set_tilt_positions(hass, setup_comp): """Test set tilt position function.""" await hass.services.async_call( @@ -454,3 +478,40 @@ async def test_set_tilt_positions(hass, setup_comp): assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 80 assert hass.states.get(DEMO_COVER_TILT).attributes[ATTR_CURRENT_TILT_POSITION] == 80 + + +@pytest.mark.parametrize("config_count", [(CONFIG_POS, 2)]) +async def test_is_opening_closing(hass, setup_comp): + """Test is_opening property.""" + await hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: COVER_GROUP}, blocking=True + ) + + assert hass.states.get(DEMO_COVER_POS).state == STATE_OPENING + assert hass.states.get(DEMO_COVER_TILT).state == STATE_OPENING + assert hass.states.get(COVER_GROUP).state == STATE_OPENING + + for _ in range(10): + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + await hass.services.async_call( + DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: COVER_GROUP}, blocking=True + ) + + assert hass.states.get(DEMO_COVER_POS).state == STATE_CLOSING + assert hass.states.get(DEMO_COVER_TILT).state == STATE_CLOSING + assert hass.states.get(COVER_GROUP).state == STATE_CLOSING + + hass.states.async_set(DEMO_COVER_POS, STATE_OPENING, {ATTR_SUPPORTED_FEATURES: 11}) + await hass.async_block_till_done() + + assert hass.states.get(DEMO_COVER_POS).state == STATE_OPENING + assert hass.states.get(COVER_GROUP).state == STATE_OPENING + + hass.states.async_set(DEMO_COVER_POS, STATE_CLOSING, {ATTR_SUPPORTED_FEATURES: 11}) + await hass.async_block_till_done() + + assert hass.states.get(DEMO_COVER_POS).state == STATE_CLOSING + assert hass.states.get(COVER_GROUP).state == STATE_CLOSING diff --git a/tests/components/guardian/__init__.py b/tests/components/guardian/__init__.py new file mode 100644 index 00000000000..8bbb10defb4 --- /dev/null +++ b/tests/components/guardian/__init__.py @@ -0,0 +1 @@ +"""Tests for the Elexa Guardian integration.""" diff --git a/tests/components/guardian/conftest.py b/tests/components/guardian/conftest.py new file mode 100644 index 00000000000..40df9c3cdb1 --- /dev/null +++ b/tests/components/guardian/conftest.py @@ -0,0 +1,17 @@ +"""Define fixtures for Elexa Guardian tests.""" +from asynctest import patch +import pytest + + +@pytest.fixture() +def ping_client(): + """Define a patched client that returns a successful ping response.""" + with patch( + "homeassistant.components.guardian.async_setup_entry", return_value=True + ), patch("aioguardian.client.Client.connect"), patch( + "aioguardian.commands.device.Device.ping", + return_value={"command": 0, "status": "ok", "data": {"uid": "ABCDEF123456"}}, + ), patch( + "aioguardian.client.Client.disconnect" + ): + yield diff --git a/tests/components/guardian/test_config_flow.py b/tests/components/guardian/test_config_flow.py new file mode 100644 index 00000000000..8e44b9f1417 --- /dev/null +++ b/tests/components/guardian/test_config_flow.py @@ -0,0 +1,140 @@ +"""Define tests for the Elexa Guardian config flow.""" +from aioguardian.errors import GuardianError +from asynctest import patch + +from homeassistant import data_entry_flow +from homeassistant.components.guardian import CONF_UID, DOMAIN +from homeassistant.components.guardian.config_flow import ( + async_get_pin_from_discovery_hostname, + async_get_pin_from_uid, +) +from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT + +from tests.common import MockConfigEntry + + +async def test_duplicate_error(hass, ping_client): + """Test that errors are shown when duplicate entries are added.""" + conf = {CONF_IP_ADDRESS: "192.168.1.100", CONF_PORT: 7777} + + MockConfigEntry(domain=DOMAIN, unique_id="guardian_3456", data=conf).add_to_hass( + hass + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=conf + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_connect_error(hass): + """Test that the config entry errors out if the device cannot connect.""" + conf = {CONF_IP_ADDRESS: "192.168.1.100", CONF_PORT: 7777} + + with patch( + "aioguardian.client.Client.connect", side_effect=GuardianError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=conf + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_IP_ADDRESS: "cannot_connect"} + + +async def test_get_pin_from_discovery_hostname(): + """Test getting a device PIN from the zeroconf-discovered hostname.""" + pin = async_get_pin_from_discovery_hostname("GVC1-3456.local.") + assert pin == "3456" + + +async def test_get_pin_from_uid(): + """Test getting a device PIN from its UID.""" + pin = async_get_pin_from_uid("ABCDEF123456") + assert pin == "3456" + + +async def test_step_user(hass, ping_client): + """Test the user step.""" + conf = {CONF_IP_ADDRESS: "192.168.1.100", CONF_PORT: 7777} + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=conf + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "ABCDEF123456" + assert result["data"] == { + CONF_IP_ADDRESS: "192.168.1.100", + CONF_PORT: 7777, + CONF_UID: "ABCDEF123456", + } + + +async def test_step_zeroconf(hass, ping_client): + """Test the zeroconf step.""" + zeroconf_data = { + "host": "192.168.1.100", + "port": 7777, + "hostname": "GVC1-ABCD.local.", + "type": "_api._udp.local.", + "name": "Guardian Valve Controller API._api._udp.local.", + "properties": {"_raw": {}}, + } + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf_data + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "zeroconf_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "ABCDEF123456" + assert result["data"] == { + CONF_IP_ADDRESS: "192.168.1.100", + CONF_PORT: 7777, + CONF_UID: "ABCDEF123456", + } + + +async def test_step_zeroconf_already_in_progress(hass): + """Test the zeroconf step aborting because it's already in progress.""" + zeroconf_data = { + "host": "192.168.1.100", + "port": 7777, + "hostname": "GVC1-ABCD.local.", + "type": "_api._udp.local.", + "name": "Guardian Valve Controller API._api._udp.local.", + "properties": {"_raw": {}}, + } + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf_data + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "zeroconf_confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf_data + ) + assert result["type"] == "abort" + assert result["reason"] == "already_in_progress" + + +async def test_step_zeroconf_no_discovery_info(hass): + """Test the zeroconf step aborting because no discovery info came along.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "connection_error" diff --git a/tests/components/hassio/test_discovery.py b/tests/components/hassio/test_discovery.py index 845c60c2f85..9d148745f18 100644 --- a/tests/components/hassio/test_discovery.py +++ b/tests/components/hassio/test_discovery.py @@ -60,6 +60,9 @@ async def test_hassio_discovery_startup(hass, aioclient_mock, hassio_client): async def test_hassio_discovery_startup_done(hass, aioclient_mock, hassio_client): """Test startup and discovery with hass discovery.""" + aioclient_mock.post( + "http://127.0.0.1/supervisor/options", json={"result": "ok", "data": {}}, + ) aioclient_mock.get( "http://127.0.0.1/discovery", json={ @@ -101,7 +104,7 @@ async def test_hassio_discovery_startup_done(hass, aioclient_mock, hassio_client await async_setup_component(hass, "hassio", {}) await hass.async_block_till_done() - assert aioclient_mock.call_count == 2 + assert aioclient_mock.call_count == 3 assert mock_mqtt.called mock_mqtt.assert_called_with( { diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index c3110b6599c..d0043747835 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -215,7 +215,7 @@ async def test_warn_when_cannot_connect(hass, caplog): assert result assert hass.components.hassio.is_hassio() - assert "Not connected with Hass.io / system to busy!" in caplog.text + assert "Not connected with Hass.io / system too busy!" in caplog.text async def test_service_register(hassio_env, hass): diff --git a/tests/components/hddtemp/test_sensor.py b/tests/components/hddtemp/test_sensor.py index 4583079829a..84315afb476 100644 --- a/tests/components/hddtemp/test_sensor.py +++ b/tests/components/hddtemp/test_sensor.py @@ -96,6 +96,7 @@ class TestHDDTempSensor(unittest.TestCase): def test_hddtemp_min_config(self): """Test minimal hddtemp configuration.""" assert setup_component(self.hass, "sensor", VALID_CONFIG_MINIMAL) + self.hass.block_till_done() entity = self.hass.states.all()[0].entity_id state = self.hass.states.get(entity) @@ -118,6 +119,7 @@ class TestHDDTempSensor(unittest.TestCase): def test_hddtemp_rename_config(self): """Test hddtemp configuration with different name.""" assert setup_component(self.hass, "sensor", VALID_CONFIG_NAME) + self.hass.block_till_done() entity = self.hass.states.all()[0].entity_id state = self.hass.states.get(entity) @@ -130,6 +132,7 @@ class TestHDDTempSensor(unittest.TestCase): def test_hddtemp_one_disk(self): """Test hddtemp one disk configuration.""" assert setup_component(self.hass, "sensor", VALID_CONFIG_ONE_DISK) + self.hass.block_till_done() state = self.hass.states.get("sensor.hd_temperature_dev_sdd1") @@ -151,6 +154,7 @@ class TestHDDTempSensor(unittest.TestCase): def test_hddtemp_wrong_disk(self): """Test hddtemp wrong disk configuration.""" assert setup_component(self.hass, "sensor", VALID_CONFIG_WRONG_DISK) + self.hass.block_till_done() assert len(self.hass.states.all()) == 1 state = self.hass.states.get("sensor.hd_temperature_dev_sdx1") @@ -160,6 +164,7 @@ class TestHDDTempSensor(unittest.TestCase): def test_hddtemp_multiple_disks(self): """Test hddtemp multiple disk configuration.""" assert setup_component(self.hass, "sensor", VALID_CONFIG_MULTIPLE_DISKS) + self.hass.block_till_done() for sensor in [ "sensor.hd_temperature_dev_sda1", @@ -187,10 +192,12 @@ class TestHDDTempSensor(unittest.TestCase): def test_hddtemp_host_refused(self): """Test hddtemp if host unreachable.""" assert setup_component(self.hass, "sensor", VALID_CONFIG_HOST) + self.hass.block_till_done() assert len(self.hass.states.all()) == 0 @patch("telnetlib.Telnet", new=TelnetMock) def test_hddtemp_host_unreachable(self): """Test hddtemp if host unreachable.""" assert setup_component(self.hass, "sensor", VALID_CONFIG_HOST_UNREACHABLE) + self.hass.block_till_done() assert len(self.hass.states.all()) == 0 diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index fd2922e94fe..c6240749aa3 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -175,6 +175,7 @@ async def test_car(hass, requests_mock_car_disabled_response): } } assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -236,6 +237,7 @@ async def test_traffic_mode_enabled(hass, requests_mock_credentials_check): } } assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -262,6 +264,7 @@ async def test_imperial(hass, requests_mock_car_disabled_response): } } assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -293,6 +296,7 @@ async def test_route_mode_shortest(hass, requests_mock_credentials_check): } } assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -324,6 +328,7 @@ async def test_route_mode_fastest(hass, requests_mock_credentials_check): } } assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -347,6 +352,7 @@ async def test_truck(hass, requests_mock_truck_response): } } assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -378,6 +384,7 @@ async def test_public_transport(hass, requests_mock_credentials_check): } } assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -428,6 +435,7 @@ async def test_public_transport_time_table(hass, requests_mock_credentials_check } } assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -478,6 +486,7 @@ async def test_pedestrian(hass, requests_mock_credentials_check): } assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -529,6 +538,7 @@ async def test_bicycle(hass, requests_mock_credentials_check): } } assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -592,6 +602,7 @@ async def test_location_zone(hass, requests_mock_truck_response): } assert await async_setup_component(hass, "zone", zone_config) assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -631,6 +642,7 @@ async def test_location_sensor(hass, requests_mock_truck_response): } } assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -679,6 +691,7 @@ async def test_location_person(hass, requests_mock_truck_response): } } assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -727,6 +740,7 @@ async def test_location_device_tracker(hass, requests_mock_truck_response): } } assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -761,6 +775,7 @@ async def test_location_device_tracker_added_after_update( } } assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -829,6 +844,7 @@ async def test_location_device_tracker_in_zone( } } assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -862,6 +878,7 @@ async def test_route_not_found(hass, requests_mock_credentials_check, caplog): } } assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -885,7 +902,8 @@ async def test_pattern_origin(hass, caplog): } } assert await async_setup_component(hass, DOMAIN, config) - assert len(caplog.records) == 1 + await hass.async_block_till_done() + assert len(caplog.records) == 2 assert "invalid latitude" in caplog.text @@ -904,7 +922,8 @@ async def test_pattern_destination(hass, caplog): } } assert await async_setup_component(hass, DOMAIN, config) - assert len(caplog.records) == 1 + await hass.async_block_till_done() + assert len(caplog.records) == 2 assert "invalid latitude" in caplog.text @@ -935,6 +954,7 @@ async def test_invalid_credentials(hass, requests_mock, caplog): } } assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() assert len(caplog.records) == 1 assert "Invalid credentials" in caplog.text @@ -964,6 +984,7 @@ async def test_attribution(hass, requests_mock_credentials_check): } } assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -992,6 +1013,7 @@ async def test_pattern_entity_state(hass, requests_mock_truck_response, caplog): } } assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -1018,6 +1040,7 @@ async def test_pattern_entity_state_with_space(hass, requests_mock_truck_respons } } assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() async def test_delayed_update(hass, requests_mock_truck_response, caplog): @@ -1044,6 +1067,7 @@ async def test_delayed_update(hass, requests_mock_truck_response, caplog): } } assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() assert await async_setup_component(hass, "sensor", sensor_config) hass.states.async_set( "sensor.origin", ",".join([TRUCK_ORIGIN_LATITUDE, TRUCK_ORIGIN_LONGITUDE]) @@ -1084,6 +1108,7 @@ async def test_arrival(hass, requests_mock_credentials_check): } } assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -1121,6 +1146,7 @@ async def test_departure(hass, requests_mock_credentials_check): } } assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -1147,7 +1173,8 @@ async def test_arrival_only_allowed_for_timetable(hass, caplog): } } assert await async_setup_component(hass, DOMAIN, config) - assert len(caplog.records) == 1 + await hass.async_block_till_done() + assert len(caplog.records) == 2 assert "[arrival] is an invalid option" in caplog.text @@ -1171,5 +1198,6 @@ async def test_exclusive_arrival_and_departure(hass, caplog): } } assert await async_setup_component(hass, DOMAIN, config) - assert len(caplog.records) == 1 + await hass.async_block_till_done() + assert len(caplog.records) == 2 assert "two or more values in the same group of exclusion" in caplog.text diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 29e43c8428e..ba0e0b9f1c0 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -1,10 +1,13 @@ """The tests the History component.""" # pylint: disable=protected-access,invalid-name from datetime import timedelta +import json import unittest from homeassistant.components import history, recorder +from homeassistant.components.recorder.models import process_timestamp import homeassistant.core as ha +from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component, setup_component import homeassistant.util.dt as dt_util @@ -103,6 +106,11 @@ class TestComponentHistory(unittest.TestCase): # Test get_state here because we have a DB setup assert states[0] == history.get_state(self.hass, future, states[0].entity_id) + time_before_recorder_ran = now - timedelta(days=1000) + assert history.get_states(self.hass, time_before_recorder_ran) == [] + + assert history.get_state(self.hass, time_before_recorder_ran, "demo.id") is None + def test_state_changes_during_period(self): """Test state change during period.""" self.init_recorder() @@ -192,6 +200,41 @@ class TestComponentHistory(unittest.TestCase): ) assert states == hist + def test_get_significant_states_minimal_response(self): + """Test that only significant states are returned. + + When minimal responses is set only the first and + last states return a complete state. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + zero, four, states = self.record_states() + hist = history.get_significant_states( + self.hass, zero, four, filters=history.Filters(), minimal_response=True + ) + + # The second media_player.test state is reduced + # down to last_changed and state when minimal_response + # is set. We use JSONEncoder to make sure that are + # pre-encoded last_changed is always the same as what + # will happen with encoding a native state + input_state = states["media_player.test"][1] + orig_last_changed = json.dumps( + process_timestamp(input_state.last_changed.replace(microsecond=0)), + cls=JSONEncoder, + ).replace('"', "") + if orig_last_changed.endswith("+00:00"): + orig_last_changed = f"{orig_last_changed[:-6]}{recorder.models.DB_TIMEZONE}" + orig_state = input_state.state + states["media_player.test"][1] = { + "last_changed": orig_last_changed, + "state": orig_state, + } + + assert states == hist + def test_get_significant_states_with_initial(self): """Test that only significant states are returned. @@ -247,6 +290,7 @@ class TestComponentHistory(unittest.TestCase): """Test that only significant states are returned for one entity.""" zero, four, states = self.record_states() del states["media_player.test2"] + del states["media_player.test3"] del states["thermostat.test"] del states["thermostat.test2"] del states["script.can_cancel_this_one"] @@ -260,6 +304,7 @@ class TestComponentHistory(unittest.TestCase): """Test that only significant states are returned for one entity.""" zero, four, states = self.record_states() del states["media_player.test2"] + del states["media_player.test3"] del states["thermostat.test2"] del states["script.can_cancel_this_one"] @@ -281,6 +326,7 @@ class TestComponentHistory(unittest.TestCase): zero, four, states = self.record_states() del states["media_player.test"] del states["media_player.test2"] + del states["media_player.test3"] config = history.CONFIG_SCHEMA( { @@ -341,6 +387,7 @@ class TestComponentHistory(unittest.TestCase): """ zero, four, states = self.record_states() del states["media_player.test2"] + del states["media_player.test3"] del states["thermostat.test"] del states["thermostat.test2"] del states["script.can_cancel_this_one"] @@ -367,6 +414,7 @@ class TestComponentHistory(unittest.TestCase): zero, four, states = self.record_states() del states["media_player.test"] del states["media_player.test2"] + del states["media_player.test3"] config = history.CONFIG_SCHEMA( { @@ -387,6 +435,7 @@ class TestComponentHistory(unittest.TestCase): """ zero, four, states = self.record_states() del states["media_player.test2"] + del states["media_player.test3"] del states["thermostat.test"] del states["thermostat.test2"] del states["script.can_cancel_this_one"] @@ -409,6 +458,7 @@ class TestComponentHistory(unittest.TestCase): """ zero, four, states = self.record_states() del states["media_player.test2"] + del states["media_player.test3"] del states["script.can_cancel_this_one"] config = history.CONFIG_SCHEMA( @@ -433,6 +483,7 @@ class TestComponentHistory(unittest.TestCase): zero, four, states = self.record_states() del states["media_player.test"] del states["media_player.test2"] + del states["media_player.test3"] del states["thermostat.test"] del states["thermostat.test2"] del states["script.can_cancel_this_one"] @@ -457,6 +508,7 @@ class TestComponentHistory(unittest.TestCase): zero, four, states = self.record_states() del states["media_player.test"] del states["media_player.test2"] + del states["media_player.test3"] del states["thermostat.test"] del states["thermostat.test2"] del states["script.can_cancel_this_one"] @@ -602,6 +654,7 @@ class TestComponentHistory(unittest.TestCase): self.init_recorder() mp = "media_player.test" mp2 = "media_player.test2" + mp3 = "media_player.test3" therm = "thermostat.test" therm2 = "thermostat.test2" zone = "zone.home" @@ -620,7 +673,7 @@ class TestComponentHistory(unittest.TestCase): three = two + timedelta(seconds=1) four = three + timedelta(seconds=1) - states = {therm: [], therm2: [], mp: [], mp2: [], script_c: []} + states = {therm: [], therm2: [], mp: [], mp2: [], mp3: [], script_c: []} with patch( "homeassistant.components.recorder.dt_util.utcnow", return_value=one ): @@ -633,6 +686,9 @@ class TestComponentHistory(unittest.TestCase): states[mp2].append( set_state(mp2, "YouTube", attributes={"media_title": str(sentinel.mt2)}) ) + states[mp3].append( + set_state(mp3, "idle", attributes={"media_title": str(sentinel.mt1)}) + ) states[therm].append( set_state(therm, 20, attributes={"current_temperature": 19.5}) ) @@ -642,6 +698,12 @@ class TestComponentHistory(unittest.TestCase): ): # This state will be skipped only different in time set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt3)}) + # This state will be skipped as it hidden + set_state( + mp3, + "Apple TV", + attributes={"media_title": str(sentinel.mt2), "hidden": True}, + ) # This state will be skipped because domain blacklisted set_state(zone, "zoning") set_state(script_nc, "off") @@ -661,6 +723,9 @@ class TestComponentHistory(unittest.TestCase): states[mp].append( set_state(mp, "Netflix", attributes={"media_title": str(sentinel.mt4)}) ) + states[mp3].append( + set_state(mp3, "Netflix", attributes={"media_title": str(sentinel.mt3)}) + ) # Attributes changed even though state is the same states[therm].append( set_state(therm, 21, attributes={"current_temperature": 20}) @@ -681,6 +746,30 @@ async def test_fetch_period_api(hass, hass_client): assert response.status == 200 +async def test_fetch_period_api_with_use_include_order(hass, hass_client): + """Test the fetch period view for history with include order.""" + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component( + hass, "history", {history.DOMAIN: {history.CONF_ORDER: True}} + ) + await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + client = await hass_client() + response = await client.get(f"/api/history/period/{dt_util.utcnow().isoformat()}") + assert response.status == 200 + + +async def test_fetch_period_api_with_minimal_response(hass, hass_client): + """Test the fetch period view for history with minimal_response.""" + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "history", {}) + await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + client = await hass_client() + response = await client.get( + f"/api/history/period/{dt_util.utcnow().isoformat()}?minimal_response" + ) + assert response.status == 200 + + async def test_fetch_period_api_with_include_order(hass, hass_client): """Test the fetch period view for history.""" await hass.async_add_executor_job(init_recorder_component, hass) diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index d9f489d20b4..900af4988e2 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -45,6 +45,7 @@ class TestHistoryStatsSensor(unittest.TestCase): } assert setup_component(self.hass, "sensor", config) + self.hass.block_till_done() state = self.hass.states.get("sensor.test") assert state.state == STATE_UNKNOWN diff --git a/tests/components/home_connect/test_config_flow.py b/tests/components/home_connect/test_config_flow.py index be6c21fe0a7..d6d936fe16e 100644 --- a/tests/components/home_connect/test_config_flow.py +++ b/tests/components/home_connect/test_config_flow.py @@ -5,6 +5,7 @@ from homeassistant.components.home_connect.const import ( OAUTH2_AUTHORIZE, OAUTH2_TOKEN, ) +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow CLIENT_ID = "1234" @@ -17,7 +18,10 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock): hass, "home_connect", { - "home_connect": {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET}, + "home_connect": { + CONF_CLIENT_ID: CLIENT_ID, + CONF_CLIENT_SECRET: CLIENT_SECRET, + }, "http": {"base_url": "https://example.com"}, }, ) diff --git a/tests/components/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py index 6234d425d8d..a1f502a3475 100644 --- a/tests/components/homeassistant/test_scene.py +++ b/tests/components/homeassistant/test_scene.py @@ -3,6 +3,7 @@ import pytest import voluptuous as vol from homeassistant.components.homeassistant import scene as ha_scene +from homeassistant.components.homeassistant.scene import EVENT_SCENE_RELOADED from homeassistant.setup import async_setup_component from tests.async_mock import patch @@ -13,6 +14,11 @@ async def test_reload_config_service(hass): """Test the reload config service.""" assert await async_setup_component(hass, "scene", {}) + test_reloaded_event = [] + hass.bus.async_listen( + EVENT_SCENE_RELOADED, lambda event: test_reloaded_event.append(event) + ) + with patch( "homeassistant.config.load_yaml_config_file", autospec=True, @@ -22,6 +28,7 @@ async def test_reload_config_service(hass): await hass.async_block_till_done() assert hass.states.get("scene.hallo") is not None + assert len(test_reloaded_event) == 1 with patch( "homeassistant.config.load_yaml_config_file", @@ -31,6 +38,7 @@ async def test_reload_config_service(hass): await hass.services.async_call("scene", "reload", blocking=True) await hass.async_block_till_done() + assert len(test_reloaded_event) == 2 assert hass.states.get("scene.hallo") is None assert hass.states.get("scene.bye") is not None @@ -39,6 +47,7 @@ async def test_apply_service(hass): """Test the apply service.""" assert await async_setup_component(hass, "scene", {}) assert await async_setup_component(hass, "light", {"light": {"platform": "demo"}}) + await hass.async_block_till_done() assert await hass.services.async_call( "scene", "apply", {"entities": {"light.bed_light": "off"}}, blocking=True @@ -83,6 +92,7 @@ async def test_create_service(hass, caplog): "scene", {"scene": {"name": "hallo_2", "entities": {"light.kitchen": "on"}}}, ) + await hass.async_block_till_done() assert hass.states.get("scene.hallo") is None assert hass.states.get("scene.hallo_2") is not None @@ -155,6 +165,7 @@ async def test_create_service(hass, caplog): async def test_snapshot_service(hass, caplog): """Test the snapshot option.""" assert await async_setup_component(hass, "scene", {"scene": {}}) + await hass.async_block_till_done() hass.states.async_set("light.my_light", "on", {"hs_color": (345, 75)}) assert hass.states.get("scene.hallo") is None @@ -212,6 +223,7 @@ async def test_snapshot_service(hass, caplog): async def test_ensure_no_intersection(hass): """Test that entities and snapshot_entities do not overlap.""" assert await async_setup_component(hass, "scene", {"scene": {}}) + await hass.async_block_till_done() with pytest.raises(vol.MultipleInvalid) as ex: assert await hass.services.async_call( @@ -245,6 +257,7 @@ async def test_scenes_with_entity(hass): ] }, ) + await hass.async_block_till_done() assert sorted(ha_scene.scenes_with_entity(hass, "light.kitchen")) == [ "scene.scene_1", @@ -268,6 +281,7 @@ async def test_entities_in_scene(hass): ] }, ) + await hass.async_block_till_done() for scene_id, entities in ( ("scene.scene_1", ["light.kitchen"]), @@ -297,6 +311,7 @@ async def test_config(hass): ] }, ) + await hass.async_block_till_done() icon = hass.states.get("scene.scene_icon") assert icon is not None diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 092c68a5480..abc6d9b5528 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -42,6 +42,7 @@ from homeassistant.const import ( EVENT_TIME_CHANGED, STATE_OFF, STATE_ON, + STATE_UNAVAILABLE, __version__, ) import homeassistant.util.dt as dt_util @@ -88,7 +89,7 @@ async def test_home_accessory(hass, hk_driver): entity_id2 = "light.accessory" hass.states.async_set(entity_id, None) - hass.states.async_set(entity_id2, None) + hass.states.async_set(entity_id2, STATE_UNAVAILABLE) await hass.async_block_till_done() @@ -98,6 +99,7 @@ async def test_home_accessory(hass, hk_driver): assert acc.hass == hass assert acc.display_name == "Home Accessory" assert acc.aid == 2 + assert acc.available is True assert acc.category == 1 # Category.OTHER assert len(acc.services) == 1 serv = acc.services[0] # SERV_ACCESSORY_INFO @@ -127,6 +129,7 @@ async def test_home_accessory(hass, hk_driver): ATTR_INTERGRATION: "luxe", }, ) + assert acc3.available is False serv = acc3.services[0] # SERV_ACCESSORY_INFO assert serv.get_characteristic(CHAR_NAME).value == "Home Accessory" assert serv.get_characteristic(CHAR_MANUFACTURER).value == "Lux Brands" diff --git a/tests/components/homekit/test_aidmanager.py b/tests/components/homekit/test_aidmanager.py index ff55ba9afa4..b7c12e86443 100644 --- a/tests/components/homekit/test_aidmanager.py +++ b/tests/components/homekit/test_aidmanager.py @@ -1,7 +1,7 @@ """Tests for the HomeKit AID manager.""" import os -from zlib import adler32 +from fnvhash import fnv1a_32 import pytest from homeassistant.components.homekit.aidmanager import ( @@ -59,19 +59,19 @@ async def test_aid_generation(hass, device_reg, entity_reg): for _ in range(0, 2): assert ( aid_storage.get_or_allocate_aid_for_entity_id(light_ent.entity_id) - == 1692141785 + == 1953095294 ) assert ( aid_storage.get_or_allocate_aid_for_entity_id(light_ent2.entity_id) - == 2732133210 + == 1975378727 ) assert ( aid_storage.get_or_allocate_aid_for_entity_id(remote_ent.entity_id) - == 1867188557 + == 3508011530 ) assert ( aid_storage.get_or_allocate_aid_for_entity_id("remote.has_no_unique_id") - == 1872038229 + == 1751603975 ) aid_storage.delete_aid(get_system_unique_id(light_ent)) @@ -82,23 +82,23 @@ async def test_aid_generation(hass, device_reg, entity_reg): for _ in range(0, 2): assert ( aid_storage.get_or_allocate_aid_for_entity_id(light_ent.entity_id) - == 1692141785 + == 1953095294 ) assert ( aid_storage.get_or_allocate_aid_for_entity_id(light_ent2.entity_id) - == 2732133210 + == 1975378727 ) assert ( aid_storage.get_or_allocate_aid_for_entity_id(remote_ent.entity_id) - == 1867188557 + == 3508011530 ) assert ( aid_storage.get_or_allocate_aid_for_entity_id("remote.has_no_unique_id") - == 1872038229 + == 1751603975 ) -async def test_aid_adler32_collision(hass, device_reg, entity_reg): +async def test_no_aid_collision(hass, device_reg, entity_reg): """Test generating aids.""" config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -130,16 +130,22 @@ async def test_aid_generation_no_unique_ids_handles_collision( ): """Test colliding aids is stable.""" config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) aid_storage = AccessoryAidStorage(hass, config_entry) await aid_storage.async_initialize() + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + seen_aids = set() collisions = [] for light_id in range(0, 220): entity_id = f"light.light{light_id}" hass.states.async_set(entity_id, "on") - expected_aid = adler32(entity_id.encode("utf-8")) + expected_aid = fnv1a_32(entity_id.encode("utf-8")) aid = aid_storage.get_or_allocate_aid_for_entity_id(entity_id) if aid != expected_aid: collisions.append(entity_id) @@ -147,143 +153,131 @@ async def test_aid_generation_no_unique_ids_handles_collision( assert aid not in seen_aids seen_aids.add(aid) - assert collisions == [ - "light.light201", - "light.light202", - "light.light203", - "light.light204", - "light.light205", - "light.light206", - "light.light207", - "light.light208", - "light.light209", - "light.light211", - "light.light212", - "light.light213", - "light.light214", - "light.light215", - "light.light216", - "light.light217", - "light.light218", - "light.light219", - ] + light_ent = entity_reg.async_get_or_create( + "light", "device", "unique_id", device_id=device_entry.id + ) + hass.states.async_set(light_ent.entity_id, "on") + aid_storage.get_or_allocate_aid_for_entity_id(light_ent.entity_id) + + assert not collisions assert aid_storage.allocations == { - "light.light0": 514851983, - "light.light1": 514917520, - "light.light10": 594609344, - "light.light100": 677446896, - "light.light101": 677512433, - "light.light102": 677577970, - "light.light103": 677643507, - "light.light104": 677709044, - "light.light105": 677774581, - "light.light106": 677840118, - "light.light107": 677905655, - "light.light108": 677971192, - "light.light109": 678036729, - "light.light11": 594674881, - "light.light110": 677577969, - "light.light111": 677643506, - "light.light112": 677709043, - "light.light113": 677774580, - "light.light114": 677840117, - "light.light115": 677905654, - "light.light116": 677971191, - "light.light117": 678036728, - "light.light118": 678102265, - "light.light119": 678167802, - "light.light12": 594740418, - "light.light120": 677709042, - "light.light121": 677774579, - "light.light122": 677840116, - "light.light123": 677905653, - "light.light124": 677971190, - "light.light125": 678036727, - "light.light126": 678102264, - "light.light127": 678167801, - "light.light128": 678233338, - "light.light129": 678298875, - "light.light13": 594805955, - "light.light130": 677840115, - "light.light131": 677905652, - "light.light132": 677971189, - "light.light133": 678036726, - "light.light134": 678102263, - "light.light135": 678167800, - "light.light136": 678233337, - "light.light137": 678298874, - "light.light138": 678364411, - "light.light139": 678429948, - "light.light14": 594871492, - "light.light140": 677971188, - "light.light141": 678036725, - "light.light142": 678102262, - "light.light143": 678167799, - "light.light144": 678233336, - "light.light145": 678298873, - "light.light146": 678364410, - "light.light147": 678429947, - "light.light148": 678495484, - "light.light149": 678561021, - "light.light15": 594937029, - "light.light150": 678102261, - "light.light151": 678167798, - "light.light152": 678233335, - "light.light153": 678298872, - "light.light154": 678364409, - "light.light155": 678429946, - "light.light156": 678495483, - "light.light157": 678561020, - "light.light158": 678626557, - "light.light159": 678692094, - "light.light16": 595002566, - "light.light160": 678233334, - "light.light161": 678298871, - "light.light162": 678364408, - "light.light163": 678429945, - "light.light164": 678495482, - "light.light165": 678561019, - "light.light166": 678626556, - "light.light167": 678692093, - "light.light168": 678757630, - "light.light169": 678823167, - "light.light17": 595068103, - "light.light170": 678364407, - "light.light171": 678429944, - "light.light172": 678495481, - "light.light173": 678561018, - "light.light174": 678626555, - "light.light175": 678692092, - "light.light176": 678757629, - "light.light177": 678823166, - "light.light178": 678888703, - "light.light179": 678954240, - "light.light18": 595133640, - "light.light180": 678495480, - "light.light181": 678561017, - "light.light182": 678626554, - "light.light183": 678692091, - "light.light184": 678757628, - "light.light185": 678823165, - "light.light186": 678888702, - "light.light187": 678954239, - "light.light188": 679019776, - "light.light189": 679085313, - "light.light19": 595199177, - "light.light190": 678626553, - "light.light191": 678692090, - "light.light192": 678757627, - "light.light193": 678823164, - "light.light194": 678888701, - "light.light195": 678954238, - "light.light196": 679019775, - "light.light197": 679085312, - "light.light198": 679150849, - "light.light199": 679216386, - "light.light2": 514983057, - "light.light20": 594740417, - "light.light200": 677643505, + "device.light.unique_id": 1953095294, + "light.light0": 301577847, + "light.light1": 284800228, + "light.light10": 2367138236, + "light.light100": 2822760292, + "light.light101": 2839537911, + "light.light102": 2856315530, + "light.light103": 2873093149, + "light.light104": 2755649816, + "light.light105": 2772427435, + "light.light106": 2789205054, + "light.light107": 2805982673, + "light.light108": 2688539340, + "light.light109": 2705316959, + "light.light11": 2383915855, + "light.light110": 776141037, + "light.light111": 759363418, + "light.light112": 742585799, + "light.light113": 725808180, + "light.light114": 709030561, + "light.light115": 692252942, + "light.light116": 675475323, + "light.light117": 658697704, + "light.light118": 641920085, + "light.light119": 625142466, + "light.light12": 2400693474, + "light.light120": 340070038, + "light.light121": 356847657, + "light.light122": 306514800, + "light.light123": 323292419, + "light.light124": 407180514, + "light.light125": 423958133, + "light.light126": 373625276, + "light.light127": 390402895, + "light.light128": 474290990, + "light.light129": 491068609, + "light.light13": 2417471093, + "light.light130": 440882847, + "light.light131": 424105228, + "light.light132": 474438085, + "light.light133": 457660466, + "light.light134": 373772371, + "light.light135": 356994752, + "light.light136": 407327609, + "light.light137": 390549990, + "light.light138": 575103799, + "light.light139": 558326180, + "light.light14": 2300027760, + "light.light140": 271973824, + "light.light141": 288751443, + "light.light142": 305529062, + "light.light143": 322306681, + "light.light144": 339084300, + "light.light145": 355861919, + "light.light146": 372639538, + "light.light147": 389417157, + "light.light148": 406194776, + "light.light149": 422972395, + "light.light15": 2316805379, + "light.light150": 2520321865, + "light.light151": 2503544246, + "light.light152": 2486766627, + "light.light153": 2469989008, + "light.light154": 2587432341, + "light.light155": 2570654722, + "light.light156": 2553877103, + "light.light157": 2537099484, + "light.light158": 2654542817, + "light.light159": 2637765198, + "light.light16": 2333582998, + "light.light160": 2621134674, + "light.light161": 2637912293, + "light.light162": 2587579436, + "light.light163": 2604357055, + "light.light164": 2554024198, + "light.light165": 2570801817, + "light.light166": 2520468960, + "light.light167": 2537246579, + "light.light168": 2755355626, + "light.light169": 2772133245, + "light.light17": 2350360617, + "light.light170": 2721947483, + "light.light171": 2705169864, + "light.light172": 2755502721, + "light.light173": 2738725102, + "light.light174": 2789057959, + "light.light175": 2772280340, + "light.light176": 2822613197, + "light.light177": 2805835578, + "light.light178": 2587726531, + "light.light179": 2570948912, + "light.light18": 2501359188, + "light.light180": 408166252, + "light.light181": 424943871, + "light.light182": 441721490, + "light.light183": 458499109, + "light.light184": 341055776, + "light.light185": 357833395, + "light.light186": 374611014, + "light.light187": 391388633, + "light.light188": 542387204, + "light.light189": 559164823, + "light.light19": 2518136807, + "light.light190": 508979061, + "light.light191": 492201442, + "light.light192": 475423823, + "light.light193": 458646204, + "light.light194": 441868585, + "light.light195": 425090966, + "light.light196": 408313347, + "light.light197": 391535728, + "light.light198": 643200013, + "light.light199": 626422394, + "light.light2": 335133085, + "light.light20": 522144599, + "light.light200": 1698935589, "light.light201": 1682157970, "light.light202": 1665380351, "light.light203": 1648602732, @@ -293,8 +287,8 @@ async def test_aid_generation_no_unique_ids_handles_collision( "light.light207": 1581492256, "light.light208": 1833156541, "light.light209": 1816378922, - "light.light21": 594805954, - "light.light210": 677774578, + "light.light21": 505366980, + "light.light210": 1598122780, "light.light211": 1614900399, "light.light212": 1631678018, "light.light213": 1648455637, @@ -304,215 +298,217 @@ async def test_aid_generation_no_unique_ids_handles_collision( "light.light217": 1581345161, "light.light218": 1732343732, "light.light219": 1749121351, - "light.light22": 594871491, - "light.light23": 594937028, - "light.light24": 595002565, - "light.light25": 595068102, - "light.light26": 595133639, - "light.light27": 595199176, - "light.light28": 595264713, - "light.light29": 595330250, - "light.light3": 515048594, - "light.light30": 594871490, - "light.light31": 594937027, - "light.light32": 595002564, - "light.light33": 595068101, - "light.light34": 595133638, - "light.light35": 595199175, - "light.light36": 595264712, - "light.light37": 595330249, - "light.light38": 595395786, - "light.light39": 595461323, - "light.light4": 515114131, - "light.light40": 595002563, - "light.light41": 595068100, - "light.light42": 595133637, - "light.light43": 595199174, - "light.light44": 595264711, - "light.light45": 595330248, - "light.light46": 595395785, - "light.light47": 595461322, - "light.light48": 595526859, - "light.light49": 595592396, - "light.light5": 515179668, - "light.light50": 595133636, - "light.light51": 595199173, - "light.light52": 595264710, - "light.light53": 595330247, - "light.light54": 595395784, - "light.light55": 595461321, - "light.light56": 595526858, - "light.light57": 595592395, - "light.light58": 595657932, - "light.light59": 595723469, - "light.light6": 515245205, - "light.light60": 595264709, - "light.light61": 595330246, - "light.light62": 595395783, - "light.light63": 595461320, - "light.light64": 595526857, - "light.light65": 595592394, - "light.light66": 595657931, - "light.light67": 595723468, - "light.light68": 595789005, - "light.light69": 595854542, - "light.light7": 515310742, - "light.light70": 595395782, - "light.light71": 595461319, - "light.light72": 595526856, - "light.light73": 595592393, - "light.light74": 595657930, - "light.light75": 595723467, - "light.light76": 595789004, - "light.light77": 595854541, - "light.light78": 595920078, - "light.light79": 595985615, - "light.light8": 515376279, - "light.light80": 595526855, - "light.light81": 595592392, - "light.light82": 595657929, - "light.light83": 595723466, - "light.light84": 595789003, - "light.light85": 595854540, - "light.light86": 595920077, - "light.light87": 595985614, - "light.light88": 596051151, - "light.light89": 596116688, - "light.light9": 515441816, - "light.light90": 595657928, - "light.light91": 595723465, - "light.light92": 595789002, - "light.light93": 595854539, - "light.light94": 595920076, - "light.light95": 595985613, - "light.light96": 596051150, - "light.light97": 596116687, - "light.light98": 596182224, - "light.light99": 596247761, + "light.light22": 555699837, + "light.light23": 538922218, + "light.light24": 455034123, + "light.light25": 438256504, + "light.light26": 488589361, + "light.light27": 471811742, + "light.light28": 387923647, + "light.light29": 371146028, + "light.light3": 318355466, + "light.light30": 421331790, + "light.light31": 438109409, + "light.light32": 387776552, + "light.light33": 404554171, + "light.light34": 488442266, + "light.light35": 505219885, + "light.light36": 454887028, + "light.light37": 471664647, + "light.light38": 287110838, + "light.light39": 303888457, + "light.light4": 234467371, + "light.light40": 454048385, + "light.light41": 437270766, + "light.light42": 420493147, + "light.light43": 403715528, + "light.light44": 521158861, + "light.light45": 504381242, + "light.light46": 487603623, + "light.light47": 470826004, + "light.light48": 319827433, + "light.light49": 303049814, + "light.light5": 217689752, + "light.light50": 353235576, + "light.light51": 370013195, + "light.light52": 386790814, + "light.light53": 403568433, + "light.light54": 420346052, + "light.light55": 437123671, + "light.light56": 453901290, + "light.light57": 470678909, + "light.light58": 219014624, + "light.light59": 235792243, + "light.light6": 268022609, + "light.light60": 2266325427, + "light.light61": 2249547808, + "light.light62": 2299880665, + "light.light63": 2283103046, + "light.light64": 2333435903, + "light.light65": 2316658284, + "light.light66": 2366991141, + "light.light67": 2350213522, + "light.light68": 2400546379, + "light.light69": 2383768760, + "light.light7": 251244990, + "light.light70": 554861194, + "light.light71": 571638813, + "light.light72": 521305956, + "light.light73": 538083575, + "light.light74": 487750718, + "light.light75": 504528337, + "light.light76": 454195480, + "light.light77": 470973099, + "light.light78": 420640242, + "light.light79": 437417861, + "light.light8": 167356895, + "light.light80": 2735113021, + "light.light81": 2718335402, + "light.light82": 2701557783, + "light.light83": 2684780164, + "light.light84": 2668002545, + "light.light85": 2651224926, + "light.light86": 2634447307, + "light.light87": 2617669688, + "light.light88": 2600892069, + "light.light89": 2584114450, + "light.light9": 150579276, + "light.light90": 2634300212, + "light.light91": 2651077831, + "light.light92": 2667855450, + "light.light93": 2684633069, + "light.light94": 2567189736, + "light.light95": 2583967355, + "light.light96": 2600744974, + "light.light97": 2617522593, + "light.light98": 2500079260, + "light.light99": 2516856879, } await aid_storage.async_save() await hass.async_block_till_done() - aid_storage = AccessoryAidStorage(hass, config_entry) + with patch("fnvhash.fnv1a_32", side_effect=Exception): + aid_storage = AccessoryAidStorage(hass, config_entry) await aid_storage.async_initialize() assert aid_storage.allocations == { - "light.light0": 514851983, - "light.light1": 514917520, - "light.light10": 594609344, - "light.light100": 677446896, - "light.light101": 677512433, - "light.light102": 677577970, - "light.light103": 677643507, - "light.light104": 677709044, - "light.light105": 677774581, - "light.light106": 677840118, - "light.light107": 677905655, - "light.light108": 677971192, - "light.light109": 678036729, - "light.light11": 594674881, - "light.light110": 677577969, - "light.light111": 677643506, - "light.light112": 677709043, - "light.light113": 677774580, - "light.light114": 677840117, - "light.light115": 677905654, - "light.light116": 677971191, - "light.light117": 678036728, - "light.light118": 678102265, - "light.light119": 678167802, - "light.light12": 594740418, - "light.light120": 677709042, - "light.light121": 677774579, - "light.light122": 677840116, - "light.light123": 677905653, - "light.light124": 677971190, - "light.light125": 678036727, - "light.light126": 678102264, - "light.light127": 678167801, - "light.light128": 678233338, - "light.light129": 678298875, - "light.light13": 594805955, - "light.light130": 677840115, - "light.light131": 677905652, - "light.light132": 677971189, - "light.light133": 678036726, - "light.light134": 678102263, - "light.light135": 678167800, - "light.light136": 678233337, - "light.light137": 678298874, - "light.light138": 678364411, - "light.light139": 678429948, - "light.light14": 594871492, - "light.light140": 677971188, - "light.light141": 678036725, - "light.light142": 678102262, - "light.light143": 678167799, - "light.light144": 678233336, - "light.light145": 678298873, - "light.light146": 678364410, - "light.light147": 678429947, - "light.light148": 678495484, - "light.light149": 678561021, - "light.light15": 594937029, - "light.light150": 678102261, - "light.light151": 678167798, - "light.light152": 678233335, - "light.light153": 678298872, - "light.light154": 678364409, - "light.light155": 678429946, - "light.light156": 678495483, - "light.light157": 678561020, - "light.light158": 678626557, - "light.light159": 678692094, - "light.light16": 595002566, - "light.light160": 678233334, - "light.light161": 678298871, - "light.light162": 678364408, - "light.light163": 678429945, - "light.light164": 678495482, - "light.light165": 678561019, - "light.light166": 678626556, - "light.light167": 678692093, - "light.light168": 678757630, - "light.light169": 678823167, - "light.light17": 595068103, - "light.light170": 678364407, - "light.light171": 678429944, - "light.light172": 678495481, - "light.light173": 678561018, - "light.light174": 678626555, - "light.light175": 678692092, - "light.light176": 678757629, - "light.light177": 678823166, - "light.light178": 678888703, - "light.light179": 678954240, - "light.light18": 595133640, - "light.light180": 678495480, - "light.light181": 678561017, - "light.light182": 678626554, - "light.light183": 678692091, - "light.light184": 678757628, - "light.light185": 678823165, - "light.light186": 678888702, - "light.light187": 678954239, - "light.light188": 679019776, - "light.light189": 679085313, - "light.light19": 595199177, - "light.light190": 678626553, - "light.light191": 678692090, - "light.light192": 678757627, - "light.light193": 678823164, - "light.light194": 678888701, - "light.light195": 678954238, - "light.light196": 679019775, - "light.light197": 679085312, - "light.light198": 679150849, - "light.light199": 679216386, - "light.light2": 514983057, - "light.light20": 594740417, - "light.light200": 677643505, + "device.light.unique_id": 1953095294, + "light.light0": 301577847, + "light.light1": 284800228, + "light.light10": 2367138236, + "light.light100": 2822760292, + "light.light101": 2839537911, + "light.light102": 2856315530, + "light.light103": 2873093149, + "light.light104": 2755649816, + "light.light105": 2772427435, + "light.light106": 2789205054, + "light.light107": 2805982673, + "light.light108": 2688539340, + "light.light109": 2705316959, + "light.light11": 2383915855, + "light.light110": 776141037, + "light.light111": 759363418, + "light.light112": 742585799, + "light.light113": 725808180, + "light.light114": 709030561, + "light.light115": 692252942, + "light.light116": 675475323, + "light.light117": 658697704, + "light.light118": 641920085, + "light.light119": 625142466, + "light.light12": 2400693474, + "light.light120": 340070038, + "light.light121": 356847657, + "light.light122": 306514800, + "light.light123": 323292419, + "light.light124": 407180514, + "light.light125": 423958133, + "light.light126": 373625276, + "light.light127": 390402895, + "light.light128": 474290990, + "light.light129": 491068609, + "light.light13": 2417471093, + "light.light130": 440882847, + "light.light131": 424105228, + "light.light132": 474438085, + "light.light133": 457660466, + "light.light134": 373772371, + "light.light135": 356994752, + "light.light136": 407327609, + "light.light137": 390549990, + "light.light138": 575103799, + "light.light139": 558326180, + "light.light14": 2300027760, + "light.light140": 271973824, + "light.light141": 288751443, + "light.light142": 305529062, + "light.light143": 322306681, + "light.light144": 339084300, + "light.light145": 355861919, + "light.light146": 372639538, + "light.light147": 389417157, + "light.light148": 406194776, + "light.light149": 422972395, + "light.light15": 2316805379, + "light.light150": 2520321865, + "light.light151": 2503544246, + "light.light152": 2486766627, + "light.light153": 2469989008, + "light.light154": 2587432341, + "light.light155": 2570654722, + "light.light156": 2553877103, + "light.light157": 2537099484, + "light.light158": 2654542817, + "light.light159": 2637765198, + "light.light16": 2333582998, + "light.light160": 2621134674, + "light.light161": 2637912293, + "light.light162": 2587579436, + "light.light163": 2604357055, + "light.light164": 2554024198, + "light.light165": 2570801817, + "light.light166": 2520468960, + "light.light167": 2537246579, + "light.light168": 2755355626, + "light.light169": 2772133245, + "light.light17": 2350360617, + "light.light170": 2721947483, + "light.light171": 2705169864, + "light.light172": 2755502721, + "light.light173": 2738725102, + "light.light174": 2789057959, + "light.light175": 2772280340, + "light.light176": 2822613197, + "light.light177": 2805835578, + "light.light178": 2587726531, + "light.light179": 2570948912, + "light.light18": 2501359188, + "light.light180": 408166252, + "light.light181": 424943871, + "light.light182": 441721490, + "light.light183": 458499109, + "light.light184": 341055776, + "light.light185": 357833395, + "light.light186": 374611014, + "light.light187": 391388633, + "light.light188": 542387204, + "light.light189": 559164823, + "light.light19": 2518136807, + "light.light190": 508979061, + "light.light191": 492201442, + "light.light192": 475423823, + "light.light193": 458646204, + "light.light194": 441868585, + "light.light195": 425090966, + "light.light196": 408313347, + "light.light197": 391535728, + "light.light198": 643200013, + "light.light199": 626422394, + "light.light2": 335133085, + "light.light20": 522144599, + "light.light200": 1698935589, "light.light201": 1682157970, "light.light202": 1665380351, "light.light203": 1648602732, @@ -522,8 +518,8 @@ async def test_aid_generation_no_unique_ids_handles_collision( "light.light207": 1581492256, "light.light208": 1833156541, "light.light209": 1816378922, - "light.light21": 594805954, - "light.light210": 677774578, + "light.light21": 505366980, + "light.light210": 1598122780, "light.light211": 1614900399, "light.light212": 1631678018, "light.light213": 1648455637, @@ -533,91 +529,91 @@ async def test_aid_generation_no_unique_ids_handles_collision( "light.light217": 1581345161, "light.light218": 1732343732, "light.light219": 1749121351, - "light.light22": 594871491, - "light.light23": 594937028, - "light.light24": 595002565, - "light.light25": 595068102, - "light.light26": 595133639, - "light.light27": 595199176, - "light.light28": 595264713, - "light.light29": 595330250, - "light.light3": 515048594, - "light.light30": 594871490, - "light.light31": 594937027, - "light.light32": 595002564, - "light.light33": 595068101, - "light.light34": 595133638, - "light.light35": 595199175, - "light.light36": 595264712, - "light.light37": 595330249, - "light.light38": 595395786, - "light.light39": 595461323, - "light.light4": 515114131, - "light.light40": 595002563, - "light.light41": 595068100, - "light.light42": 595133637, - "light.light43": 595199174, - "light.light44": 595264711, - "light.light45": 595330248, - "light.light46": 595395785, - "light.light47": 595461322, - "light.light48": 595526859, - "light.light49": 595592396, - "light.light5": 515179668, - "light.light50": 595133636, - "light.light51": 595199173, - "light.light52": 595264710, - "light.light53": 595330247, - "light.light54": 595395784, - "light.light55": 595461321, - "light.light56": 595526858, - "light.light57": 595592395, - "light.light58": 595657932, - "light.light59": 595723469, - "light.light6": 515245205, - "light.light60": 595264709, - "light.light61": 595330246, - "light.light62": 595395783, - "light.light63": 595461320, - "light.light64": 595526857, - "light.light65": 595592394, - "light.light66": 595657931, - "light.light67": 595723468, - "light.light68": 595789005, - "light.light69": 595854542, - "light.light7": 515310742, - "light.light70": 595395782, - "light.light71": 595461319, - "light.light72": 595526856, - "light.light73": 595592393, - "light.light74": 595657930, - "light.light75": 595723467, - "light.light76": 595789004, - "light.light77": 595854541, - "light.light78": 595920078, - "light.light79": 595985615, - "light.light8": 515376279, - "light.light80": 595526855, - "light.light81": 595592392, - "light.light82": 595657929, - "light.light83": 595723466, - "light.light84": 595789003, - "light.light85": 595854540, - "light.light86": 595920077, - "light.light87": 595985614, - "light.light88": 596051151, - "light.light89": 596116688, - "light.light9": 515441816, - "light.light90": 595657928, - "light.light91": 595723465, - "light.light92": 595789002, - "light.light93": 595854539, - "light.light94": 595920076, - "light.light95": 595985613, - "light.light96": 596051150, - "light.light97": 596116687, - "light.light98": 596182224, - "light.light99": 596247761, + "light.light22": 555699837, + "light.light23": 538922218, + "light.light24": 455034123, + "light.light25": 438256504, + "light.light26": 488589361, + "light.light27": 471811742, + "light.light28": 387923647, + "light.light29": 371146028, + "light.light3": 318355466, + "light.light30": 421331790, + "light.light31": 438109409, + "light.light32": 387776552, + "light.light33": 404554171, + "light.light34": 488442266, + "light.light35": 505219885, + "light.light36": 454887028, + "light.light37": 471664647, + "light.light38": 287110838, + "light.light39": 303888457, + "light.light4": 234467371, + "light.light40": 454048385, + "light.light41": 437270766, + "light.light42": 420493147, + "light.light43": 403715528, + "light.light44": 521158861, + "light.light45": 504381242, + "light.light46": 487603623, + "light.light47": 470826004, + "light.light48": 319827433, + "light.light49": 303049814, + "light.light5": 217689752, + "light.light50": 353235576, + "light.light51": 370013195, + "light.light52": 386790814, + "light.light53": 403568433, + "light.light54": 420346052, + "light.light55": 437123671, + "light.light56": 453901290, + "light.light57": 470678909, + "light.light58": 219014624, + "light.light59": 235792243, + "light.light6": 268022609, + "light.light60": 2266325427, + "light.light61": 2249547808, + "light.light62": 2299880665, + "light.light63": 2283103046, + "light.light64": 2333435903, + "light.light65": 2316658284, + "light.light66": 2366991141, + "light.light67": 2350213522, + "light.light68": 2400546379, + "light.light69": 2383768760, + "light.light7": 251244990, + "light.light70": 554861194, + "light.light71": 571638813, + "light.light72": 521305956, + "light.light73": 538083575, + "light.light74": 487750718, + "light.light75": 504528337, + "light.light76": 454195480, + "light.light77": 470973099, + "light.light78": 420640242, + "light.light79": 437417861, + "light.light8": 167356895, + "light.light80": 2735113021, + "light.light81": 2718335402, + "light.light82": 2701557783, + "light.light83": 2684780164, + "light.light84": 2668002545, + "light.light85": 2651224926, + "light.light86": 2634447307, + "light.light87": 2617669688, + "light.light88": 2600892069, + "light.light89": 2584114450, + "light.light9": 150579276, + "light.light90": 2634300212, + "light.light91": 2651077831, + "light.light92": 2667855450, + "light.light93": 2684633069, + "light.light94": 2567189736, + "light.light95": 2583967355, + "light.light96": 2600744974, + "light.light97": 2617522593, + "light.light98": 2500079260, + "light.light99": 2516856879, } aidstore = get_aid_storage_filename_for_entry_id(config_entry.entry_id) diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index cb4d81408cb..3bda3ed7491 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -27,7 +27,6 @@ def _mock_config_entry_with_options_populated(): }, "auto_start": False, "safe_mode": False, - "zeroconf_default_interface": True, }, ) @@ -149,12 +148,7 @@ async def test_options_flow_advanced(hass): with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): result3 = await hass.config_entries.options.async_configure( - result2["flow_id"], - user_input={ - "auto_start": True, - "safe_mode": True, - "zeroconf_default_interface": False, - }, + result2["flow_id"], user_input={"auto_start": True, "safe_mode": True}, ) assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -167,7 +161,6 @@ async def test_options_flow_advanced(hass): "include_entities": [], }, "safe_mode": True, - "zeroconf_default_interface": False, } @@ -202,8 +195,7 @@ async def test_options_flow_basic(hass): with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): result3 = await hass.config_entries.options.async_configure( - result2["flow_id"], - user_input={"safe_mode": True, "zeroconf_default_interface": False}, + result2["flow_id"], user_input={"safe_mode": True}, ) assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -216,7 +208,6 @@ async def test_options_flow_basic(hass): "include_entities": [], }, "safe_mode": True, - "zeroconf_default_interface": False, } @@ -264,8 +255,7 @@ async def test_options_flow_with_cameras(hass): with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): result4 = await hass.config_entries.options.async_configure( - result3["flow_id"], - user_input={"safe_mode": True, "zeroconf_default_interface": False}, + result3["flow_id"], user_input={"safe_mode": True}, ) assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -279,7 +269,6 @@ async def test_options_flow_with_cameras(hass): }, "entity_config": {"camera.native_h264": {"video_codec": "copy"}}, "safe_mode": True, - "zeroconf_default_interface": False, } # Now run though again and verify we can turn off copy @@ -315,8 +304,7 @@ async def test_options_flow_with_cameras(hass): with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): result4 = await hass.config_entries.options.async_configure( - result3["flow_id"], - user_input={"safe_mode": True, "zeroconf_default_interface": False}, + result3["flow_id"], user_input={"safe_mode": True}, ) assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -330,7 +318,6 @@ async def test_options_flow_with_cameras(hass): }, "entity_config": {"camera.native_h264": {}}, "safe_mode": True, - "zeroconf_default_interface": False, } @@ -353,7 +340,6 @@ async def test_options_flow_blocked_when_from_yaml(hass): "exclude_entities": ["climate.front_gate"], }, "safe_mode": False, - "zeroconf_default_interface": True, }, source=SOURCE_IMPORT, ) diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index b016997b7c9..ca31b4501b9 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -2,11 +2,14 @@ import os from typing import Dict +from asynctest import MagicMock import pytest -from zeroconf import InterfaceChoice from homeassistant.components import zeroconf -from homeassistant.components.binary_sensor import DEVICE_CLASS_BATTERY_CHARGING +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_BATTERY_CHARGING, + DEVICE_CLASS_MOTION, +) from homeassistant.components.homekit import ( MAX_DEVICES, STATUS_READY, @@ -23,10 +26,10 @@ from homeassistant.components.homekit.const import ( CONF_AUTO_START, CONF_ENTRY_INDEX, CONF_SAFE_MODE, - CONF_ZEROCONF_DEFAULT_INTERFACE, DEFAULT_PORT, DEFAULT_SAFE_MODE, DOMAIN, + HOMEKIT, HOMEKIT_FILE, SERVICE_HOMEKIT_RESET_ACCESSORY, SERVICE_HOMEKIT_START, @@ -96,7 +99,6 @@ async def test_setup_min(hass): with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit: mock_homekit.return_value = homekit = Mock() type(homekit).async_start = AsyncMock() - type(homekit).async_setup_zeroconf = AsyncMock() assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -109,7 +111,6 @@ async def test_setup_min(hass): {}, DEFAULT_SAFE_MODE, None, - None, entry.entry_id, ) assert mock_homekit().setup.called is True @@ -127,18 +128,13 @@ async def test_setup_auto_start_disabled(hass): entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "Test Name", CONF_PORT: 11111, CONF_IP_ADDRESS: "172.0.0.0"}, - options={ - CONF_AUTO_START: False, - CONF_SAFE_MODE: DEFAULT_SAFE_MODE, - CONF_ZEROCONF_DEFAULT_INTERFACE: True, - }, + options={CONF_AUTO_START: False, CONF_SAFE_MODE: DEFAULT_SAFE_MODE}, ) entry.add_to_hass(hass) with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit: mock_homekit.return_value = homekit = Mock() type(homekit).async_start = AsyncMock() - type(homekit).async_setup_zeroconf = AsyncMock() assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -151,7 +147,6 @@ async def test_setup_auto_start_disabled(hass): {}, DEFAULT_SAFE_MODE, None, - InterfaceChoice.Default, entry.entry_id, ) assert mock_homekit().setup.called is True @@ -198,15 +193,15 @@ async def test_homekit_setup(hass, hk_driver): {}, DEFAULT_SAFE_MODE, advertise_ip=None, - interface_choice=None, entry_id=entry.entry_id, ) + zeroconf_mock = MagicMock() with patch( f"{PATH_HOMEKIT}.accessories.HomeDriver", return_value=hk_driver ) as mock_driver, patch("homeassistant.util.get_local_ip") as mock_ip: mock_ip.return_value = IP_ADDRESS - await hass.async_add_executor_job(homekit.setup) + await hass.async_add_executor_job(homekit.setup, zeroconf_mock) path = get_persist_fullpath_for_entry_id(hass, entry.entry_id) assert isinstance(homekit.bridge, HomeBridge) @@ -218,7 +213,7 @@ async def test_homekit_setup(hass, hk_driver): port=DEFAULT_PORT, persist_file=path, advertised_address=None, - interface_choice=None, + zeroconf_instance=zeroconf_mock, ) assert homekit.driver.safe_mode is False @@ -242,15 +237,15 @@ async def test_homekit_setup_ip_address(hass, hk_driver): {}, None, None, - interface_choice=None, entry_id=entry.entry_id, ) + mock_zeroconf = MagicMock() path = get_persist_fullpath_for_entry_id(hass, entry.entry_id) with patch( f"{PATH_HOMEKIT}.accessories.HomeDriver", return_value=hk_driver ) as mock_driver: - await hass.async_add_executor_job(homekit.setup) + await hass.async_add_executor_job(homekit.setup, mock_zeroconf) mock_driver.assert_called_with( hass, entry.entry_id, @@ -259,7 +254,7 @@ async def test_homekit_setup_ip_address(hass, hk_driver): port=DEFAULT_PORT, persist_file=path, advertised_address=None, - interface_choice=None, + zeroconf_instance=mock_zeroconf, ) @@ -279,15 +274,15 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver): {}, None, "192.168.1.100", - interface_choice=None, entry_id=entry.entry_id, ) + zeroconf_instance = MagicMock() path = get_persist_fullpath_for_entry_id(hass, entry.entry_id) with patch( f"{PATH_HOMEKIT}.accessories.HomeDriver", return_value=hk_driver ) as mock_driver: - await hass.async_add_executor_job(homekit.setup) + await hass.async_add_executor_job(homekit.setup, zeroconf_instance) mock_driver.assert_called_with( hass, entry.entry_id, @@ -296,44 +291,7 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver): port=DEFAULT_PORT, persist_file=path, advertised_address="192.168.1.100", - interface_choice=None, - ) - - -async def test_homekit_setup_interface_choice(hass, hk_driver): - """Test setup with interface choice of Default.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={CONF_NAME: "mock_name", CONF_PORT: 12345}, - source=SOURCE_IMPORT, - ) - homekit = HomeKit( - hass, - BRIDGE_NAME, - DEFAULT_PORT, - "0.0.0.0", - {}, - {}, - None, - None, - InterfaceChoice.Default, - entry_id=entry.entry_id, - ) - - path = get_persist_fullpath_for_entry_id(hass, entry.entry_id) - with patch( - f"{PATH_HOMEKIT}.accessories.HomeDriver", return_value=hk_driver - ) as mock_driver: - await hass.async_add_executor_job(homekit.setup) - mock_driver.assert_called_with( - hass, - entry.entry_id, - BRIDGE_NAME, - address="0.0.0.0", - port=DEFAULT_PORT, - persist_file=path, - advertised_address=None, - interface_choice=InterfaceChoice.Default, + zeroconf_instance=zeroconf_instance, ) @@ -353,12 +311,11 @@ async def test_homekit_setup_safe_mode(hass, hk_driver): {}, True, advertise_ip=None, - interface_choice=None, entry_id=entry.entry_id, ) with patch(f"{PATH_HOMEKIT}.accessories.HomeDriver", return_value=hk_driver): - await hass.async_add_executor_job(homekit.setup) + await hass.async_add_executor_job(homekit.setup, MagicMock()) assert homekit.driver.safe_mode is True @@ -375,7 +332,6 @@ async def test_homekit_add_accessory(hass): {}, DEFAULT_SAFE_MODE, advertise_ip=None, - interface_choice=None, entry_id=entry.entry_id, ) homekit.driver = "driver" @@ -387,15 +343,15 @@ async def test_homekit_add_accessory(hass): with patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc: mock_get_acc.side_effect = [None, "acc", None] homekit.add_bridge_accessory(State("light.demo", "on")) - mock_get_acc.assert_called_with(hass, "driver", ANY, 363398124, {}) + mock_get_acc.assert_called_with(hass, "driver", ANY, 1403373688, {}) assert not mock_bridge.add_accessory.called homekit.add_bridge_accessory(State("demo.test", "on")) - mock_get_acc.assert_called_with(hass, "driver", ANY, 294192020, {}) + mock_get_acc.assert_called_with(hass, "driver", ANY, 600325356, {}) assert mock_bridge.add_accessory.called homekit.add_bridge_accessory(State("demo.test_2", "on")) - mock_get_acc.assert_called_with(hass, "driver", ANY, 429982757, {}) + mock_get_acc.assert_called_with(hass, "driver", ANY, 1467253281, {}) mock_bridge.add_accessory.assert_called_with("acc") @@ -412,7 +368,6 @@ async def test_homekit_remove_accessory(hass): {}, DEFAULT_SAFE_MODE, advertise_ip=None, - interface_choice=None, entry_id=entry.entry_id, ) homekit.driver = "driver" @@ -438,7 +393,6 @@ async def test_homekit_entity_filter(hass): {}, DEFAULT_SAFE_MODE, advertise_ip=None, - interface_choice=None, entry_id=entry.entry_id, ) homekit.bridge = Mock() @@ -473,7 +427,6 @@ async def test_homekit_start(hass, hk_driver, device_reg, debounce_patcher): {}, DEFAULT_SAFE_MODE, advertise_ip=None, - interface_choice=None, entry_id=entry.entry_id, ) homekit.bridge = Mock() @@ -563,7 +516,6 @@ async def test_homekit_start_with_a_broken_accessory(hass, hk_driver, debounce_p {}, DEFAULT_SAFE_MODE, advertise_ip=None, - interface_choice=None, entry_id=entry.entry_id, ) @@ -609,7 +561,6 @@ async def test_homekit_stop(hass): {}, DEFAULT_SAFE_MODE, advertise_ip=None, - interface_choice=None, entry_id=entry.entry_id, ) homekit.driver = Mock() @@ -650,7 +601,6 @@ async def test_homekit_reset_accessories(hass): {entity_id: {}}, DEFAULT_SAFE_MODE, advertise_ip=None, - interface_choice=None, entry_id=entry.entry_id, ) homekit.bridge = Mock() @@ -699,7 +649,6 @@ async def test_homekit_too_many_accessories(hass, hk_driver): {}, DEFAULT_SAFE_MODE, advertise_ip=None, - interface_choice=None, entry_id=entry.entry_id, ) homekit.bridge = Mock() @@ -734,7 +683,6 @@ async def test_homekit_finds_linked_batteries( {"light.demo": {}}, DEFAULT_SAFE_MODE, advertise_ip=None, - interface_choice=None, entry_id=entry.entry_id, ) homekit.driver = hk_driver @@ -829,7 +777,6 @@ async def test_setup_imported(hass): with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit: mock_homekit.return_value = homekit = Mock() type(homekit).async_start = AsyncMock() - type(homekit).async_setup_zeroconf = AsyncMock() assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -842,7 +789,6 @@ async def test_setup_imported(hass): {}, DEFAULT_SAFE_MODE, None, - None, entry.entry_id, ) assert mock_homekit().setup.called is True @@ -884,7 +830,6 @@ async def test_yaml_updates_update_config_entry_for_name(hass): with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit: mock_homekit.return_value = homekit = Mock() type(homekit).async_start = AsyncMock() - type(homekit).async_setup_zeroconf = AsyncMock() assert await async_setup_component( hass, "homekit", {"homekit": {CONF_NAME: BRIDGE_NAME, CONF_PORT: 12345}} ) @@ -899,7 +844,6 @@ async def test_yaml_updates_update_config_entry_for_name(hass): {}, DEFAULT_SAFE_MODE, None, - None, entry.entry_id, ) assert mock_homekit().setup.called is True @@ -929,7 +873,7 @@ async def test_raise_config_entry_not_ready(hass): await hass.async_block_till_done() -async def test_homekit_uses_system_zeroconf(hass, hk_driver, mock_zeroconf): +async def test_homekit_uses_system_zeroconf(hass, mock_zeroconf): """Test HomeKit uses system zeroconf.""" entry = MockConfigEntry( domain=DOMAIN, @@ -938,13 +882,15 @@ async def test_homekit_uses_system_zeroconf(hass, hk_driver, mock_zeroconf): ) system_zc = await zeroconf.async_get_instance(hass) - with patch(f"{PATH_HOMEKIT}.accessories.HomeDriver", return_value=hk_driver), patch( - f"{PATH_HOMEKIT}.HomeKit.async_start" + with patch(f"{PATH_HOMEKIT}.HomeKit.add_bridge_accessory"), patch( + f"{PATH_HOMEKIT}.show_setup_message" + ), patch("pyhap.accessory_driver.AccessoryDriver.add_accessory"), patch( + "pyhap.accessory_driver.AccessoryDriver.start" ): entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert hk_driver.advertiser == system_zc + assert hass.data[DOMAIN][entry.entry_id][HOMEKIT].driver.advertiser == system_zc def _write_data(path: str, data: Dict) -> None: @@ -969,7 +915,6 @@ async def test_homekit_ignored_missing_devices( {"light.demo": {}}, DEFAULT_SAFE_MODE, advertise_ip=None, - interface_choice=None, entry_id=entry.entry_id, ) homekit.driver = hk_driver @@ -1032,3 +977,77 @@ async def test_homekit_ignored_missing_devices( "linked_battery_sensor": "sensor.powerwall_battery", }, ) + + +async def test_homekit_finds_linked_motion_sensors( + hass, hk_driver, debounce_patcher, device_reg, entity_reg +): + """Test HomeKit start method.""" + entry = await async_init_integration(hass) + + homekit = HomeKit( + hass, + None, + None, + None, + {}, + {"camera.camera_demo": {}}, + DEFAULT_SAFE_MODE, + advertise_ip=None, + entry_id=entry.entry_id, + ) + homekit.driver = hk_driver + homekit._filter = Mock(return_value=True) + homekit.bridge = HomeBridge(hass, hk_driver, "mock_bridge") + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + sw_version="0.16.0", + model="Camera Server", + manufacturer="Ubq", + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + binary_motion_sensor = entity_reg.async_get_or_create( + "binary_sensor", + "camera", + "motion_sensor", + device_id=device_entry.id, + device_class=DEVICE_CLASS_MOTION, + ) + camera = entity_reg.async_get_or_create( + "camera", "camera", "demo", device_id=device_entry.id + ) + + hass.states.async_set( + binary_motion_sensor.entity_id, + STATE_ON, + {ATTR_DEVICE_CLASS: DEVICE_CLASS_MOTION}, + ) + hass.states.async_set(camera.entity_id, STATE_ON) + + def _mock_get_accessory(*args, **kwargs): + return [None, "acc", None] + + with patch.object(homekit.bridge, "add_accessory"), patch( + f"{PATH_HOMEKIT}.show_setup_message" + ), patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc, patch( + "pyhap.accessory_driver.AccessoryDriver.start" + ): + await homekit.async_start() + await hass.async_block_till_done() + + mock_get_acc.assert_called_with( + hass, + hk_driver, + ANY, + ANY, + { + "manufacturer": "Ubq", + "model": "Camera Server", + "sw_version": "0.16.0", + "linked_motion_sensor": "binary_sensor.camera_motion_sensor", + }, + ) diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py index 0c002fa7213..78e27231d19 100644 --- a/tests/components/homekit/test_type_cameras.py +++ b/tests/components/homekit/test_type_cameras.py @@ -9,16 +9,21 @@ from homeassistant.components import camera, ffmpeg from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import ( AUDIO_CODEC_COPY, + CHAR_MOTION_DETECTED, CONF_AUDIO_CODEC, + CONF_LINKED_MOTION_SENSOR, CONF_STREAM_SOURCE, CONF_SUPPORT_AUDIO, CONF_VIDEO_CODEC, + DEVICE_CLASS_MOTION, + SERV_MOTION_SENSOR, VIDEO_CODEC_COPY, VIDEO_CODEC_H264_OMX, ) from homeassistant.components.homekit.img_util import TurboJPEGSingleton from homeassistant.components.homekit.type_cameras import Camera from homeassistant.components.homekit.type_switches import Switch +from homeassistant.const import ATTR_DEVICE_CLASS, STATE_OFF, STATE_ON from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component @@ -118,6 +123,7 @@ async def test_camera_stream_source_configured(hass, run_driver, events): await async_setup_component( hass, camera.DOMAIN, {camera.DOMAIN: {"platform": "demo"}} ) + await hass.async_block_till_done() entity_id = "camera.demo_camera" @@ -222,6 +228,7 @@ async def test_camera_stream_source_configured_with_failing_ffmpeg( await async_setup_component( hass, camera.DOMAIN, {camera.DOMAIN: {"platform": "demo"}} ) + await hass.async_block_till_done() entity_id = "camera.demo_camera" @@ -266,6 +273,7 @@ async def test_camera_stream_source_found(hass, run_driver, events): await async_setup_component( hass, camera.DOMAIN, {camera.DOMAIN: {"platform": "demo"}} ) + await hass.async_block_till_done() entity_id = "camera.demo_camera" @@ -308,6 +316,7 @@ async def test_camera_stream_source_fails(hass, run_driver, events): await async_setup_component( hass, camera.DOMAIN, {camera.DOMAIN: {"platform": "demo"}} ) + await hass.async_block_till_done() entity_id = "camera.demo_camera" @@ -363,6 +372,7 @@ async def test_camera_stream_source_configured_and_copy_codec(hass, run_driver, await async_setup_component( hass, camera.DOMAIN, {camera.DOMAIN: {"platform": "demo"}} ) + await hass.async_block_till_done() entity_id = "camera.demo_camera" @@ -431,6 +441,7 @@ async def test_camera_streaming_fails_after_starting_ffmpeg(hass, run_driver, ev await async_setup_component( hass, camera.DOMAIN, {camera.DOMAIN: {"platform": "demo"}} ) + await hass.async_block_till_done() entity_id = "camera.demo_camera" @@ -492,3 +503,93 @@ async def test_camera_streaming_fails_after_starting_ffmpeg(hass, run_driver, ev output=expected_output.format(**session_info), stdout_pipe=False, ) + + +async def test_camera_with_linked_motion_sensor(hass, run_driver, events): + """Test a camera with a linked motion sensor can update.""" + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + await async_setup_component( + hass, camera.DOMAIN, {camera.DOMAIN: {"platform": "demo"}} + ) + await hass.async_block_till_done() + motion_entity_id = "binary_sensor.motion" + + hass.states.async_set( + motion_entity_id, STATE_ON, {ATTR_DEVICE_CLASS: DEVICE_CLASS_MOTION} + ) + await hass.async_block_till_done() + entity_id = "camera.demo_camera" + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = Camera( + hass, + run_driver, + "Camera", + entity_id, + 2, + { + CONF_STREAM_SOURCE: "/dev/null", + CONF_SUPPORT_AUDIO: True, + CONF_VIDEO_CODEC: VIDEO_CODEC_H264_OMX, + CONF_AUDIO_CODEC: AUDIO_CODEC_COPY, + CONF_LINKED_MOTION_SENSOR: motion_entity_id, + }, + ) + bridge = HomeBridge("hass", run_driver, "Test Bridge") + bridge.add_accessory(acc) + + await acc.run_handler() + + assert acc.aid == 2 + assert acc.category == 17 # Camera + + service = acc.get_service(SERV_MOTION_SENSOR) + assert service + char = service.get_characteristic(CHAR_MOTION_DETECTED) + assert char + + assert char.value is True + + hass.states.async_set( + motion_entity_id, STATE_OFF, {ATTR_DEVICE_CLASS: DEVICE_CLASS_MOTION} + ) + await hass.async_block_till_done() + assert char.value is False + + char.set_value(True) + hass.states.async_set( + motion_entity_id, STATE_ON, {ATTR_DEVICE_CLASS: DEVICE_CLASS_MOTION} + ) + await hass.async_block_till_done() + assert char.value is True + + +async def test_camera_with_a_missing_linked_motion_sensor(hass, run_driver, events): + """Test a camera with a configured linked motion sensor that is missing.""" + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + await async_setup_component( + hass, camera.DOMAIN, {camera.DOMAIN: {"platform": "demo"}} + ) + await hass.async_block_till_done() + motion_entity_id = "binary_sensor.motion" + entity_id = "camera.demo_camera" + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = Camera( + hass, + run_driver, + "Camera", + entity_id, + 2, + {CONF_LINKED_MOTION_SENSOR: motion_entity_id}, + ) + bridge = HomeBridge("hass", run_driver, "Test Bridge") + bridge.add_accessory(acc) + + await acc.run_handler() + + assert acc.aid == 2 + assert acc.category == 17 # Camera + + assert not acc.get_service(SERV_MOTION_SENSOR) diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py index bd533417121..e4842b93125 100644 --- a/tests/components/homekit/test_type_media_players.py +++ b/tests/components/homekit/test_type_media_players.py @@ -1,12 +1,15 @@ """Test different accessory types: Media Players.""" from homeassistant.components.homekit.const import ( + ATTR_KEY_NAME, ATTR_VALUE, CONF_FEATURE_LIST, + EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, + KEY_ARROW_RIGHT, ) from homeassistant.components.homekit.type_media_players import ( MediaPlayer, @@ -342,6 +345,22 @@ async def test_media_player_television(hass, hk_driver, events, caplog): assert len(events) == 11 assert events[-1].data[ATTR_VALUE] is None + events = [] + + def listener(event): + events.append(event) + + hass.bus.async_listen(EVENT_HOMEKIT_TV_REMOTE_KEY_PRESSED, listener) + + await hass.async_add_executor_job(acc.char_remote_key.client_update_value, 20) + await hass.async_block_till_done() + + await hass.async_add_executor_job(acc.char_remote_key.client_update_value, 7) + await hass.async_block_till_done() + + assert len(events) == 1 + assert events[0].data[ATTR_KEY_NAME] == KEY_ARROW_RIGHT + async def test_media_player_television_basic(hass, hk_driver, events, caplog): """Test if basic television accessory and HA are updated accordingly.""" diff --git a/tests/components/http/test_data_validator.py b/tests/components/http/test_data_validator.py index b0a14a31bc5..c7b5ed42ccd 100644 --- a/tests/components/http/test_data_validator.py +++ b/tests/components/http/test_data_validator.py @@ -11,7 +11,7 @@ from tests.async_mock import Mock async def get_client(aiohttp_client, validator): """Generate a client that hits a view decorated with validator.""" app = web.Application() - app["hass"] = Mock(is_running=True) + app["hass"] = Mock(is_stopping=False) class TestView(HomeAssistantView): url = "/" diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 18ec9ccf471..2c95d03a9ef 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -1,7 +1,6 @@ """The tests for the Home Assistant HTTP component.""" from ipaddress import ip_network import logging -import unittest import pytest @@ -12,6 +11,32 @@ from homeassistant.util.ssl import server_context_intermediate, server_context_m from tests.async_mock import Mock, patch +@pytest.fixture +def mock_stack(): + """Mock extract stack.""" + with patch( + "homeassistant.components.http.extract_stack", + return_value=[ + Mock( + filename="/home/paulus/core/homeassistant/core.py", + lineno="23", + line="do_something()", + ), + Mock( + filename="/home/paulus/core/homeassistant/components/hue/light.py", + lineno="23", + line="self.light.is_on", + ), + Mock( + filename="/home/paulus/core/homeassistant/components/http/__init__.py", + lineno="157", + line="base_url", + ), + ], + ): + yield + + class TestView(http.HomeAssistantView): """Test the HTTP views.""" @@ -36,110 +61,73 @@ async def test_registering_view_while_running( hass.http.register_view(TestView) -class TestApiConfig(unittest.TestCase): - """Test API configuration methods.""" - - def test_api_base_url_with_domain(hass): - """Test setting API URL with domain.""" - api_config = http.ApiConfig("127.0.0.1", "example.com") - assert api_config.base_url == "http://example.com:8123" - - def test_api_base_url_with_ip(hass): - """Test setting API URL with IP.""" - api_config = http.ApiConfig("127.0.0.1", "1.1.1.1") - assert api_config.base_url == "http://1.1.1.1:8123" - - def test_api_base_url_with_ip_and_port(hass): - """Test setting API URL with IP and port.""" - api_config = http.ApiConfig("127.0.0.1", "1.1.1.1", 8124) - assert api_config.base_url == "http://1.1.1.1:8124" - - def test_api_base_url_with_protocol(hass): - """Test setting API URL with protocol.""" - api_config = http.ApiConfig("127.0.0.1", "https://example.com") - assert api_config.base_url == "https://example.com:8123" - - def test_api_base_url_with_protocol_and_port(hass): - """Test setting API URL with protocol and port.""" - api_config = http.ApiConfig("127.0.0.1", "https://example.com", 433) - assert api_config.base_url == "https://example.com:433" - - def test_api_base_url_with_ssl_enable(hass): - """Test setting API URL with use_ssl enabled.""" - api_config = http.ApiConfig("127.0.0.1", "example.com", use_ssl=True) - assert api_config.base_url == "https://example.com:8123" - - def test_api_base_url_with_ssl_enable_and_port(hass): - """Test setting API URL with use_ssl enabled and port.""" - api_config = http.ApiConfig("127.0.0.1", "1.1.1.1", use_ssl=True, port=8888) - assert api_config.base_url == "https://1.1.1.1:8888" - - def test_api_base_url_with_protocol_and_ssl_enable(hass): - """Test setting API URL with specific protocol and use_ssl enabled.""" - api_config = http.ApiConfig("127.0.0.1", "http://example.com", use_ssl=True) - assert api_config.base_url == "http://example.com:8123" - - def test_api_base_url_removes_trailing_slash(hass): - """Test a trialing slash is removed when setting the API URL.""" - api_config = http.ApiConfig("127.0.0.1", "http://example.com/") - assert api_config.base_url == "http://example.com:8123" - - def test_api_local_ip(hass): - """Test a trialing slash is removed when setting the API URL.""" - api_config = http.ApiConfig("127.0.0.1", "http://example.com/") - assert api_config.local_ip == "127.0.0.1" +def test_api_base_url_with_domain(mock_stack): + """Test setting API URL with domain.""" + api_config = http.ApiConfig("127.0.0.1", "example.com") + assert api_config.base_url == "http://example.com:8123" -async def test_api_base_url_with_domain(hass): - """Test setting API URL.""" - result = await async_setup_component( - hass, "http", {"http": {"base_url": "example.com"}} - ) - assert result - assert hass.config.api.base_url == "http://example.com" +def test_api_base_url_with_ip(mock_stack): + """Test setting API URL with IP.""" + api_config = http.ApiConfig("127.0.0.1", "1.1.1.1") + assert api_config.base_url == "http://1.1.1.1:8123" -async def test_api_base_url_with_ip(hass): - """Test setting api url.""" - result = await async_setup_component( - hass, "http", {"http": {"server_host": "1.1.1.1"}} - ) - assert result - assert hass.config.api.base_url == "http://1.1.1.1:8123" +def test_api_base_url_with_ip_and_port(mock_stack): + """Test setting API URL with IP and port.""" + api_config = http.ApiConfig("127.0.0.1", "1.1.1.1", 8124) + assert api_config.base_url == "http://1.1.1.1:8124" -async def test_api_base_url_with_ip_port(hass): - """Test setting api url.""" - result = await async_setup_component( - hass, "http", {"http": {"base_url": "1.1.1.1:8124"}} - ) - assert result - assert hass.config.api.base_url == "http://1.1.1.1:8124" +def test_api_base_url_with_protocol(mock_stack): + """Test setting API URL with protocol.""" + api_config = http.ApiConfig("127.0.0.1", "https://example.com") + assert api_config.base_url == "https://example.com:8123" -async def test_api_no_base_url(hass): +def test_api_base_url_with_protocol_and_port(mock_stack): + """Test setting API URL with protocol and port.""" + api_config = http.ApiConfig("127.0.0.1", "https://example.com", 433) + assert api_config.base_url == "https://example.com:433" + + +def test_api_base_url_with_ssl_enable(mock_stack): + """Test setting API URL with use_ssl enabled.""" + api_config = http.ApiConfig("127.0.0.1", "example.com", use_ssl=True) + assert api_config.base_url == "https://example.com:8123" + + +def test_api_base_url_with_ssl_enable_and_port(mock_stack): + """Test setting API URL with use_ssl enabled and port.""" + api_config = http.ApiConfig("127.0.0.1", "1.1.1.1", use_ssl=True, port=8888) + assert api_config.base_url == "https://1.1.1.1:8888" + + +def test_api_base_url_with_protocol_and_ssl_enable(mock_stack): + """Test setting API URL with specific protocol and use_ssl enabled.""" + api_config = http.ApiConfig("127.0.0.1", "http://example.com", use_ssl=True) + assert api_config.base_url == "http://example.com:8123" + + +def test_api_base_url_removes_trailing_slash(mock_stack): + """Test a trialing slash is removed when setting the API URL.""" + api_config = http.ApiConfig("127.0.0.1", "http://example.com/") + assert api_config.base_url == "http://example.com:8123" + + +def test_api_local_ip(mock_stack): + """Test a trialing slash is removed when setting the API URL.""" + api_config = http.ApiConfig("127.0.0.1", "http://example.com/") + assert api_config.local_ip == "127.0.0.1" + + +async def test_api_no_base_url(hass, mock_stack): """Test setting api url.""" result = await async_setup_component(hass, "http", {"http": {}}) assert result assert hass.config.api.base_url == "http://127.0.0.1:8123" -async def test_api_local_ip(hass): - """Test setting api url.""" - result = await async_setup_component(hass, "http", {"http": {}}) - assert result - assert hass.config.api.local_ip == "127.0.0.1" - - -async def test_api_base_url_removes_trailing_slash(hass): - """Test setting api url.""" - result = await async_setup_component( - hass, "http", {"http": {"base_url": "https://example.com/"}} - ) - assert result - assert hass.config.api.base_url == "https://example.com" - - async def test_not_log_password(hass, aiohttp_client, caplog, legacy_auth): """Test access with password doesn't get logged.""" assert await async_setup_component(hass, "api", {"http": {}}) diff --git a/tests/components/http/test_view.py b/tests/components/http/test_view.py index a6e4bdc12c8..045f0837983 100644 --- a/tests/components/http/test_view.py +++ b/tests/components/http/test_view.py @@ -19,7 +19,13 @@ from tests.async_mock import AsyncMock, Mock @pytest.fixture def mock_request(): """Mock a request.""" - return Mock(app={"hass": Mock(is_running=True)}, match_info={}) + return Mock(app={"hass": Mock(is_stopping=False)}, match_info={}) + + +@pytest.fixture +def mock_request_with_stopping(): + """Mock a request.""" + return Mock(app={"hass": Mock(is_stopping=True)}, match_info={}) async def test_invalid_json(caplog): @@ -55,3 +61,11 @@ async def test_handling_service_not_found(mock_request): Mock(requires_auth=False), AsyncMock(side_effect=ServiceNotFound("test", "test")), )(mock_request) + + +async def test_not_running(mock_request_with_stopping): + """Test we get a 503 when not running.""" + response = await request_handler_factory( + Mock(requires_auth=False), AsyncMock(side_effect=Unauthorized) + )(mock_request_with_stopping) + assert response.status == 503 diff --git a/tests/components/ign_sismologia/test_geo_location.py b/tests/components/ign_sismologia/test_geo_location.py index 8b9852ea234..5695f26c5aa 100644 --- a/tests/components/ign_sismologia/test_geo_location.py +++ b/tests/components/ign_sismologia/test_geo_location.py @@ -103,6 +103,7 @@ async def test_setup(hass): ) with assert_setup_component(1, geo_location.DOMAIN): assert await async_setup_component(hass, geo_location.DOMAIN, CONFIG) + await hass.async_block_till_done() # Artificially trigger update. hass.bus.async_fire(EVENT_HOMEASSISTANT_START) # Collect events. @@ -207,6 +208,7 @@ async def test_setup_with_custom_location(hass): assert await async_setup_component( hass, geo_location.DOMAIN, CONFIG_WITH_CUSTOM_LOCATION ) + await hass.async_block_till_done() # Artificially trigger update. hass.bus.async_fire(EVENT_HOMEASSISTANT_START) diff --git a/tests/components/image_processing/test_init.py b/tests/components/image_processing/test_init.py index cc708db75db..72edcd7160a 100644 --- a/tests/components/image_processing/test_init.py +++ b/tests/components/image_processing/test_init.py @@ -59,9 +59,10 @@ class TestImageProcessing: config = {ip.DOMAIN: {"platform": "test"}, "camera": {"platform": "demo"}} setup_component(self.hass, ip.DOMAIN, config) + self.hass.block_till_done() state = self.hass.states.get("camera.demo_camera") - self.url = f"{self.hass.config.api.base_url}{state.attributes.get(ATTR_ENTITY_PICTURE)}" + self.url = f"{self.hass.config.internal_url}{state.attributes.get(ATTR_ENTITY_PICTURE)}" def teardown_method(self): """Stop everything that was started.""" @@ -113,9 +114,10 @@ class TestImageProcessingAlpr: new_callable=PropertyMock(return_value=False), ): setup_component(self.hass, ip.DOMAIN, config) + self.hass.block_till_done() state = self.hass.states.get("camera.demo_camera") - self.url = f"{self.hass.config.api.base_url}{state.attributes.get(ATTR_ENTITY_PICTURE)}" + self.url = f"{self.hass.config.internal_url}{state.attributes.get(ATTR_ENTITY_PICTURE)}" self.alpr_events = [] @@ -218,9 +220,10 @@ class TestImageProcessingFace: new_callable=PropertyMock(return_value=False), ): setup_component(self.hass, ip.DOMAIN, config) + self.hass.block_till_done() state = self.hass.states.get("camera.demo_camera") - self.url = f"{self.hass.config.api.base_url}{state.attributes.get(ATTR_ENTITY_PICTURE)}" + self.url = f"{self.hass.config.internal_url}{state.attributes.get(ATTR_ENTITY_PICTURE)}" self.face_events = [] diff --git a/tests/components/ipma/test_weather.py b/tests/components/ipma/test_weather.py index e7542070d2c..c7a8bbda63f 100644 --- a/tests/components/ipma/test_weather.py +++ b/tests/components/ipma/test_weather.py @@ -135,7 +135,7 @@ async def test_setup_configuration(hass): weather.DOMAIN, {"weather": {"name": "HomeTown", "platform": "ipma", "mode": "hourly"}}, ) - await hass.async_block_till_done() + await hass.async_block_till_done() state = hass.states.get("weather.hometown") assert state.state == "rainy" @@ -182,7 +182,7 @@ async def test_daily_forecast(hass): weather.DOMAIN, {"weather": {"name": "HomeTown", "platform": "ipma", "mode": "daily"}}, ) - await hass.async_block_till_done() + await hass.async_block_till_done() state = hass.states.get("weather.hometown") assert state.state == "rainy" @@ -208,7 +208,7 @@ async def test_hourly_forecast(hass): weather.DOMAIN, {"weather": {"name": "HomeTown", "platform": "ipma", "mode": "hourly"}}, ) - await hass.async_block_till_done() + await hass.async_block_till_done() state = hass.states.get("weather.hometown") assert state.state == "rainy" diff --git a/tests/components/lastfm/test_sensor.py b/tests/components/lastfm/test_sensor.py index 7ae70a6f152..82c789415bf 100644 --- a/tests/components/lastfm/test_sensor.py +++ b/tests/components/lastfm/test_sensor.py @@ -54,6 +54,7 @@ async def test_update_not_playing(hass, lastfm_network): sensor.DOMAIN, {"sensor": {"platform": "lastfm", "api_key": "secret-key", "users": ["test"]}}, ) + await hass.async_block_till_done() entity_id = "sensor.test" @@ -74,6 +75,7 @@ async def test_update_playing(hass, lastfm_network): sensor.DOMAIN, {"sensor": {"platform": "lastfm", "api_key": "secret-key", "users": ["test"]}}, ) + await hass.async_block_till_done() entity_id = "sensor.test" diff --git a/tests/components/light/test_device_action.py b/tests/components/light/test_device_action.py index 116aff4ee78..1f3f0c22bd0 100644 --- a/tests/components/light/test_device_action.py +++ b/tests/components/light/test_device_action.py @@ -203,6 +203,7 @@ async def test_action(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() ent1, ent2, ent3 = platform.ENTITIES diff --git a/tests/components/light/test_device_condition.py b/tests/components/light/test_device_condition.py index 998ef7851c1..2a43a0abebe 100644 --- a/tests/components/light/test_device_condition.py +++ b/tests/components/light/test_device_condition.py @@ -96,6 +96,7 @@ async def test_if_state(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() ent1, ent2, ent3 = platform.ENTITIES @@ -173,6 +174,7 @@ async def test_if_fires_on_for_condition(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() ent1, ent2, ent3 = platform.ENTITIES diff --git a/tests/components/light/test_device_trigger.py b/tests/components/light/test_device_trigger.py index 969b4278aeb..dd4745ac513 100644 --- a/tests/components/light/test_device_trigger.py +++ b/tests/components/light/test_device_trigger.py @@ -96,6 +96,7 @@ async def test_if_fires_on_state_change(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() ent1, ent2, ent3 = platform.ENTITIES @@ -180,6 +181,7 @@ async def test_if_fires_on_state_change_with_for(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() ent1, ent2, ent3 = platform.ENTITIES diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index b1f9327ff50..2fa22cd81dd 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -123,6 +123,7 @@ class TestLight(unittest.TestCase): assert setup_component( self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) + self.hass.block_till_done() ent1, ent2, ent3 = platform.ENTITIES @@ -335,6 +336,7 @@ class TestLight(unittest.TestCase): assert setup_component( self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) + self.hass.block_till_done() ent1, _, _ = platform.ENTITIES @@ -376,12 +378,13 @@ class TestLight(unittest.TestCase): return real_open(path, *args, **kwargs) profile_data = "id,x,y,brightness\ngroup.all_lights.default,.4,.6,99\n" - with mock.patch("os.path.isfile", side_effect=_mock_isfile): - with mock.patch("builtins.open", side_effect=_mock_open): - with mock_storage(): - assert setup_component( - self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} - ) + with mock.patch("os.path.isfile", side_effect=_mock_isfile), mock.patch( + "builtins.open", side_effect=_mock_open + ), mock_storage(): + assert setup_component( + self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} + ) + self.hass.block_till_done() ent, _, _ = platform.ENTITIES common.turn_on(self.hass, ent.entity_id) @@ -413,12 +416,13 @@ class TestLight(unittest.TestCase): + "group.all_lights.default,.3,.5,200\n" + "light.ceiling_2.default,.6,.6,100\n" ) - with mock.patch("os.path.isfile", side_effect=_mock_isfile): - with mock.patch("builtins.open", side_effect=_mock_open): - with mock_storage(): - assert setup_component( - self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} - ) + with mock.patch("os.path.isfile", side_effect=_mock_isfile), mock.patch( + "builtins.open", side_effect=_mock_open + ), mock_storage(): + assert setup_component( + self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} + ) + self.hass.block_till_done() dev = next( filter(lambda x: x.entity_id == "light.ceiling_2", platform.ENTITIES) @@ -434,6 +438,7 @@ async def test_light_context(hass, hass_admin_user): platform = getattr(hass.components, "test.light") platform.init() assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) + await hass.async_block_till_done() state = hass.states.get("light.ceiling") assert state is not None @@ -457,6 +462,7 @@ async def test_light_turn_on_auth(hass, hass_admin_user): platform = getattr(hass.components, "test.light") platform.init() assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) + await hass.async_block_till_done() state = hass.states.get("light.ceiling") assert state is not None @@ -481,6 +487,7 @@ async def test_light_brightness_step(hass): entity.supported_features = light.SUPPORT_BRIGHTNESS entity.brightness = 100 assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) + await hass.async_block_till_done() state = hass.states.get(entity.entity_id) assert state is not None @@ -515,6 +522,7 @@ async def test_light_brightness_pct_conversion(hass): entity.supported_features = light.SUPPORT_BRIGHTNESS entity.brightness = 100 assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) + await hass.async_block_till_done() state = hass.states.get(entity.entity_id) assert state is not None diff --git a/tests/components/local_file/test_camera.py b/tests/components/local_file/test_camera.py index ea2b3c3252e..b1a9b04412d 100644 --- a/tests/components/local_file/test_camera.py +++ b/tests/components/local_file/test_camera.py @@ -25,6 +25,7 @@ async def test_loading_file(hass, hass_client): } }, ) + await hass.async_block_till_done() client = await hass_client() @@ -57,6 +58,7 @@ async def test_file_not_readable(hass, caplog): } }, ) + await hass.async_block_till_done() assert "Could not read" in caplog.text assert "config_test" in caplog.text @@ -91,6 +93,7 @@ async def test_camera_content_type(hass, hass_client): "camera", {"camera": [cam_config_jpg, cam_config_png, cam_config_svg, cam_config_noext]}, ) + await hass.async_block_till_done() client = await hass_client() @@ -143,6 +146,7 @@ async def test_update_file_path(hass): "file_path": "mock/path_2.jpg", } await async_setup_component(hass, "camera", {"camera": [camera_1, camera_2]}) + await hass.async_block_till_done() # Fetch state and check motion detection attribute state = hass.states.get("camera.local_file") diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index abe6f6ec515..e1341e64e92 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -14,7 +14,6 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_HIDDEN, ATTR_NAME, - EVENT_AUTOMATION_TRIGGERED, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_SCRIPT_STARTED, @@ -43,7 +42,8 @@ class TestComponentLogbook(unittest.TestCase): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() init_recorder_component(self.hass) # Force an in memory DB - assert setup_component(self.hass, logbook.DOMAIN, self.EMPTY_CONFIG) + with patch("homeassistant.components.http.start_http_server_and_save_config"): + assert setup_component(self.hass, logbook.DOMAIN, self.EMPTY_CONFIG) def tearDown(self): """Stop everything that was started.""" @@ -305,45 +305,6 @@ class TestComponentLogbook(unittest.TestCase): entries[1], pointB, "blu", domain="sensor", entity_id=entity_id2 ) - def test_exclude_automation_events(self): - """Test if automation entries can be excluded by entity_id.""" - name = "My Automation Rule" - domain = "automation" - entity_id = "automation.my_automation_rule" - entity_id2 = "automation.my_automation_rule_2" - entity_id2 = "sensor.blu" - - eventA = ha.Event( - logbook.EVENT_AUTOMATION_TRIGGERED, - {logbook.ATTR_NAME: name, logbook.ATTR_ENTITY_ID: entity_id}, - ) - eventB = ha.Event( - logbook.EVENT_AUTOMATION_TRIGGERED, - {logbook.ATTR_NAME: name, logbook.ATTR_ENTITY_ID: entity_id2}, - ) - - config = logbook.CONFIG_SCHEMA( - { - ha.DOMAIN: {}, - logbook.DOMAIN: { - logbook.CONF_EXCLUDE: {logbook.CONF_ENTITIES: [entity_id]} - }, - } - ) - entities_filter = logbook._generate_filter_from_config(config[logbook.DOMAIN]) - events = [ - e - for e in (ha.Event(EVENT_HOMEASSISTANT_STOP), eventA, eventB) - if logbook._keep_event(self.hass, e, entities_filter) - ] - entries = list(logbook.humanify(self.hass, events)) - - assert len(entries) == 2 - self.assert_entry( - entries[0], name="Home Assistant", message="stopped", domain=ha.DOMAIN - ) - self.assert_entry(entries[1], name=name, domain=domain, entity_id=entity_id2) - def test_exclude_script_events(self): """Test if script start can be excluded by entity_id.""" name = "My Script Rule" @@ -1335,35 +1296,6 @@ async def test_logbook_view_period_entity(hass, hass_client): assert json[0]["entity_id"] == entity_id_test -async def test_humanify_automation_triggered_event(hass): - """Test humanifying Automation Trigger event.""" - event1, event2 = list( - logbook.humanify( - hass, - [ - ha.Event( - EVENT_AUTOMATION_TRIGGERED, - {ATTR_ENTITY_ID: "automation.hello", ATTR_NAME: "Hello Automation"}, - ), - ha.Event( - EVENT_AUTOMATION_TRIGGERED, - {ATTR_ENTITY_ID: "automation.bye", ATTR_NAME: "Bye Automation"}, - ), - ], - ) - ) - - assert event1["name"] == "Hello Automation" - assert event1["domain"] == "automation" - assert event1["message"] == "has been triggered" - assert event1["entity_id"] == "automation.hello" - - assert event2["name"] == "Bye Automation" - assert event2["domain"] == "automation" - assert event2["message"] == "has been triggered" - assert event2["entity_id"] == "automation.bye" - - async def test_humanify_script_started_event(hass): """Test humanifying Script Run event.""" event1, event2 = list( diff --git a/tests/components/logger/test_init.py b/tests/components/logger/test_init.py index 00fa5aa3558..ccbc476c204 100644 --- a/tests/components/logger/test_init.py +++ b/tests/components/logger/test_init.py @@ -1,123 +1,120 @@ """The tests for the Logger component.""" from collections import namedtuple import logging -import unittest from homeassistant.components import logger -from homeassistant.setup import setup_component - -from tests.common import get_test_home_assistant +from homeassistant.setup import async_setup_component RECORD = namedtuple("record", ("name", "levelno")) NO_DEFAULT_CONFIG = {"logger": {}} NO_LOGS_CONFIG = {"logger": {"default": "info"}} -TEST_CONFIG = {"logger": {"default": "warning", "logs": {"test": "info"}}} +TEST_CONFIG = { + "logger": { + "default": "warning", + "logs": {"test": "info", "test.child": "debug", "test.child.child": "warning"}, + } +} -class TestUpdater(unittest.TestCase): - """Test logger component.""" +async def async_setup_logger(hass, config): + """Set up logger and save log filter.""" + await async_setup_component(hass, logger.DOMAIN, config) + return logging.root.handlers[-1].filters[0] - def setUp(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.log_filter = None - def tearDown(self): - """Stop everything that was started.""" - del logging.root.handlers[-1] - self.hass.stop() +async def test_logger_setup(hass): + """Use logger to create a logging filter.""" + await async_setup_logger(hass, TEST_CONFIG) - def setup_logger(self, config): - """Set up logger and save log filter.""" - setup_component(self.hass, logger.DOMAIN, config) - self.log_filter = logging.root.handlers[-1].filters[0] + assert len(logging.root.handlers) > 0 + handler = logging.root.handlers[-1] - def assert_logged(self, name, level): - """Assert that a certain record was logged.""" - assert self.log_filter.filter(RECORD(name, level)) + assert len(handler.filters) == 1 - def assert_not_logged(self, name, level): - """Assert that a certain record was not logged.""" - assert not self.log_filter.filter(RECORD(name, level)) - def test_logger_setup(self): - """Use logger to create a logging filter.""" - self.setup_logger(TEST_CONFIG) +async def test_logger_test_filters(hass): + """Test resulting filter operation.""" + log_filter = await async_setup_logger(hass, TEST_CONFIG) - assert len(logging.root.handlers) > 0 - handler = logging.root.handlers[-1] + # Blocked default record + assert not log_filter.filter(RECORD("asdf", logging.DEBUG)) - assert len(handler.filters) == 1 - log_filter = handler.filters[0].logfilter + # Allowed default record + assert log_filter.filter(RECORD("asdf", logging.WARNING)) - assert log_filter["default"] == logging.WARNING - assert log_filter["logs"]["test"] == logging.INFO + # Blocked named record + assert not log_filter.filter(RECORD("test", logging.DEBUG)) - def test_logger_test_filters(self): - """Test resulting filter operation.""" - self.setup_logger(TEST_CONFIG) + # Allowed named record + assert log_filter.filter(RECORD("test", logging.INFO)) - # Blocked default record - self.assert_not_logged("asdf", logging.DEBUG) + # Allowed named record child + assert log_filter.filter(RECORD("test.child", logging.INFO)) - # Allowed default record - self.assert_logged("asdf", logging.WARNING) + # Allowed named record child + assert log_filter.filter(RECORD("test.child", logging.DEBUG)) - # Blocked named record - self.assert_not_logged("test", logging.DEBUG) + # Blocked named record child of child + assert not log_filter.filter(RECORD("test.child.child", logging.DEBUG)) - # Allowed named record - self.assert_logged("test", logging.INFO) + # Allowed named record child of child + assert log_filter.filter(RECORD("test.child.child", logging.WARNING)) - def test_set_filter_empty_config(self): - """Test change log level from empty configuration.""" - self.setup_logger(NO_LOGS_CONFIG) - self.assert_not_logged("test", logging.DEBUG) +async def test_set_filter_empty_config(hass): + """Test change log level from empty configuration.""" + log_filter = await async_setup_logger(hass, NO_LOGS_CONFIG) - self.hass.services.call(logger.DOMAIN, "set_level", {"test": "debug"}) - self.hass.block_till_done() + assert not log_filter.filter(RECORD("test", logging.DEBUG)) - self.assert_logged("test", logging.DEBUG) + await hass.services.async_call(logger.DOMAIN, "set_level", {"test": "debug"}) + await hass.async_block_till_done() - def test_set_filter(self): - """Test change log level of existing filter.""" - self.setup_logger(TEST_CONFIG) + assert log_filter.filter(RECORD("test", logging.DEBUG)) - self.assert_not_logged("asdf", logging.DEBUG) - self.assert_logged("dummy", logging.WARNING) - self.hass.services.call( - logger.DOMAIN, "set_level", {"asdf": "debug", "dummy": "info"} - ) - self.hass.block_till_done() +async def test_set_filter(hass): + """Test change log level of existing filter.""" + log_filter = await async_setup_logger(hass, TEST_CONFIG) - self.assert_logged("asdf", logging.DEBUG) - self.assert_logged("dummy", logging.WARNING) + assert not log_filter.filter(RECORD("asdf", logging.DEBUG)) + assert log_filter.filter(RECORD("dummy", logging.WARNING)) - def test_set_default_filter_empty_config(self): - """Test change default log level from empty configuration.""" - self.setup_logger(NO_DEFAULT_CONFIG) + await hass.services.async_call( + logger.DOMAIN, "set_level", {"asdf": "debug", "dummy": "info"} + ) + await hass.async_block_till_done() - self.assert_logged("test", logging.DEBUG) + assert log_filter.filter(RECORD("asdf", logging.DEBUG)) + assert log_filter.filter(RECORD("dummy", logging.WARNING)) - self.hass.services.call( - logger.DOMAIN, "set_default_level", {"level": "warning"} - ) - self.hass.block_till_done() - self.assert_not_logged("test", logging.DEBUG) +async def test_set_default_filter_empty_config(hass): + """Test change default log level from empty configuration.""" + log_filter = await async_setup_logger(hass, NO_DEFAULT_CONFIG) - def test_set_default_filter(self): - """Test change default log level with existing default.""" - self.setup_logger(TEST_CONFIG) + assert log_filter.filter(RECORD("test", logging.DEBUG)) - self.assert_not_logged("asdf", logging.DEBUG) - self.assert_logged("dummy", logging.WARNING) + await hass.services.async_call( + logger.DOMAIN, "set_default_level", {"level": "warning"} + ) + await hass.async_block_till_done() - self.hass.services.call(logger.DOMAIN, "set_default_level", {"level": "debug"}) - self.hass.block_till_done() + assert not log_filter.filter(RECORD("test", logging.DEBUG)) - self.assert_logged("asdf", logging.DEBUG) - self.assert_logged("dummy", logging.WARNING) + +async def test_set_default_filter(hass): + """Test change default log level with existing default.""" + log_filter = await async_setup_logger(hass, TEST_CONFIG) + + assert not log_filter.filter(RECORD("asdf", logging.DEBUG)) + assert log_filter.filter(RECORD("dummy", logging.WARNING)) + + await hass.services.async_call( + logger.DOMAIN, "set_default_level", {"level": "debug"} + ) + await hass.async_block_till_done() + + assert log_filter.filter(RECORD("asdf", logging.DEBUG)) + assert log_filter.filter(RECORD("dummy", logging.WARNING)) diff --git a/tests/components/london_air/test_sensor.py b/tests/components/london_air/test_sensor.py index 83405095f2e..f596750ea7d 100644 --- a/tests/components/london_air/test_sensor.py +++ b/tests/components/london_air/test_sensor.py @@ -28,6 +28,7 @@ class TestLondonAirSensor(unittest.TestCase): """Test for operational tube_state sensor with proper attributes.""" mock_req.get(URL, text=load_fixture("london_air.json")) assert setup_component(self.hass, "sensor", {"sensor": self.config}) + self.hass.block_till_done() state = self.hass.states.get("sensor.merton") assert state.state == "Low" diff --git a/tests/components/manual/test_alarm_control_panel.py b/tests/components/manual/test_alarm_control_panel.py index d3e26597d84..c1f7fd5a7e0 100644 --- a/tests/components/manual/test_alarm_control_panel.py +++ b/tests/components/manual/test_alarm_control_panel.py @@ -47,6 +47,7 @@ async def test_arm_home_no_pending(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -73,6 +74,7 @@ async def test_arm_home_no_pending_when_code_not_req(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -98,6 +100,7 @@ async def test_arm_home_with_pending(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -137,6 +140,7 @@ async def test_arm_home_with_invalid_code(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -162,6 +166,7 @@ async def test_arm_away_no_pending(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -188,6 +193,7 @@ async def test_arm_away_no_pending_when_code_not_req(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -213,6 +219,7 @@ async def test_arm_home_with_template_code(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -239,6 +246,7 @@ async def test_arm_away_with_pending(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -278,6 +286,7 @@ async def test_arm_away_with_invalid_code(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -303,6 +312,7 @@ async def test_arm_night_no_pending(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -329,6 +339,7 @@ async def test_arm_night_no_pending_when_code_not_req(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -354,6 +365,7 @@ async def test_arm_night_with_pending(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -398,6 +410,7 @@ async def test_arm_night_with_invalid_code(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -422,6 +435,7 @@ async def test_trigger_no_pending(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -458,6 +472,7 @@ async def test_trigger_with_delay(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -500,6 +515,7 @@ async def test_trigger_zero_trigger_time(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -525,6 +541,7 @@ async def test_trigger_zero_trigger_time_with_pending(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -550,6 +567,7 @@ async def test_trigger_with_pending(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -602,6 +620,7 @@ async def test_trigger_with_unused_specific_delay(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -646,6 +665,7 @@ async def test_trigger_with_specific_delay(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -689,6 +709,7 @@ async def test_trigger_with_pending_and_delay(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -745,6 +766,7 @@ async def test_trigger_with_pending_and_specific_delay(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -798,6 +820,7 @@ async def test_armed_home_with_specific_pending(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -830,6 +853,7 @@ async def test_armed_away_with_specific_pending(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -862,6 +886,7 @@ async def test_armed_night_with_specific_pending(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -896,6 +921,7 @@ async def test_trigger_with_specific_pending(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -939,6 +965,7 @@ async def test_trigger_with_disarm_after_trigger(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -975,6 +1002,7 @@ async def test_trigger_with_zero_specific_trigger_time(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -1001,6 +1029,7 @@ async def test_trigger_with_unused_zero_specific_trigger_time(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -1036,6 +1065,7 @@ async def test_trigger_with_specific_trigger_time(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -1072,6 +1102,7 @@ async def test_trigger_with_no_disarm_after_trigger(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -1112,6 +1143,7 @@ async def test_back_to_back_trigger_with_no_disarm_after_trigger(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -1164,6 +1196,7 @@ async def test_disarm_while_pending_trigger(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -1203,6 +1236,7 @@ async def test_disarm_during_trigger_with_invalid_code(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -1242,6 +1276,7 @@ async def test_disarm_with_template_code(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -1278,6 +1313,7 @@ async def test_arm_custom_bypass_no_pending(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -1304,6 +1340,7 @@ async def test_arm_custom_bypass_no_pending_when_code_not_req(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -1329,6 +1366,7 @@ async def test_arm_custom_bypass_with_pending(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -1368,6 +1406,7 @@ async def test_arm_custom_bypass_with_invalid_code(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -1392,6 +1431,7 @@ async def test_armed_custom_bypass_with_specific_pending(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -1428,6 +1468,7 @@ async def test_arm_away_after_disabled_disarmed(hass): } }, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -1499,6 +1540,7 @@ async def test_restore_armed_state(hass): } }, ) + await hass.async_block_till_done() state = hass.states.get("alarm_control_panel.test") assert state @@ -1525,6 +1567,7 @@ async def test_restore_disarmed_state(hass): } }, ) + await hass.async_block_till_done() state = hass.states.get("alarm_control_panel.test") assert state diff --git a/tests/components/manual_mqtt/test_alarm_control_panel.py b/tests/components/manual_mqtt/test_alarm_control_panel.py index bff3818af56..a23382d9f78 100644 --- a/tests/components/manual_mqtt/test_alarm_control_panel.py +++ b/tests/components/manual_mqtt/test_alarm_control_panel.py @@ -86,6 +86,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -114,6 +115,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -141,6 +143,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -184,6 +187,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -211,6 +215,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -239,6 +244,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -266,6 +272,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -294,6 +301,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -337,6 +345,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -364,6 +373,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -392,6 +402,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -419,6 +430,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -468,6 +480,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -494,6 +507,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -535,6 +549,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -583,6 +598,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -610,6 +626,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -637,6 +654,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -693,6 +711,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -734,6 +753,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -762,6 +782,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -802,6 +823,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -842,6 +864,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -904,6 +927,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -949,6 +973,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -996,6 +1021,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -1046,6 +1072,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -1096,6 +1123,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -1162,6 +1190,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -1224,6 +1253,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -1261,6 +1291,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -1298,6 +1329,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -1337,6 +1369,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -1391,6 +1424,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -1465,6 +1499,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -1504,6 +1539,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -1544,6 +1580,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -1584,6 +1621,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -1624,6 +1662,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() entity_id = "alarm_control_panel.test" @@ -1656,6 +1695,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): } }, ) + self.hass.block_till_done() # Component should send disarmed alarm state on startup self.hass.block_till_done() diff --git a/tests/components/marytts/test_tts.py b/tests/components/marytts/test_tts.py index 637ed1900b8..da221a7effd 100644 --- a/tests/components/marytts/test_tts.py +++ b/tests/components/marytts/test_tts.py @@ -4,8 +4,6 @@ import os import shutil from urllib.parse import urlencode -from mock import Mock, patch - from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_ID, DOMAIN as DOMAIN_MP, @@ -16,6 +14,7 @@ from homeassistant.config import async_process_ha_core_config from homeassistant.const import HTTP_INTERNAL_SERVER_ERROR from homeassistant.setup import setup_component +from tests.async_mock import Mock, patch from tests.common import assert_setup_component, get_test_home_assistant, mock_service diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index 50924af5d76..d5eac466093 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -13,6 +13,7 @@ async def test_get_image(hass, hass_ws_client, caplog): await async_setup_component( hass, "media_player", {"media_player": {"platform": "demo"}} ) + await hass.async_block_till_done() client = await hass_ws_client(hass) @@ -45,6 +46,7 @@ async def test_get_image_http(hass, aiohttp_client): await async_setup_component( hass, "media_player", {"media_player": {"platform": "demo"}} ) + await hass.async_block_till_done() state = hass.states.get("media_player.bedroom") assert "entity_picture_local" not in state.attributes @@ -72,6 +74,7 @@ async def test_get_image_http_remote(hass, aiohttp_client): await async_setup_component( hass, "media_player", {"media_player": {"platform": "demo"}} ) + await hass.async_block_till_done() state = hass.states.get("media_player.bedroom") assert "entity_picture_local" in state.attributes diff --git a/tests/components/meteo_france/test_config_flow.py b/tests/components/meteo_france/test_config_flow.py index d4381073209..8a5c734a0ed 100644 --- a/tests/components/meteo_france/test_config_flow.py +++ b/tests/components/meteo_france/test_config_flow.py @@ -27,6 +27,17 @@ def mock_controller_client_1(): yield service_mock +@pytest.fixture(autouse=True) +def mock_setup(): + """Prevent setup.""" + with patch( + "homeassistant.components.meteo_france.async_setup", return_value=True, + ), patch( + "homeassistant.components.meteo_france.async_setup_entry", return_value=True, + ): + yield + + @pytest.fixture(name="client_2") def mock_controller_client_2(): """Mock a successful client.""" diff --git a/tests/components/mfi/test_sensor.py b/tests/components/mfi/test_sensor.py index ff9c7fa7182..05e1379cfb4 100644 --- a/tests/components/mfi/test_sensor.py +++ b/tests/components/mfi/test_sensor.py @@ -64,6 +64,7 @@ class TestMfiSensorSetup(unittest.TestCase): config = dict(self.GOOD_CONFIG) del config[self.THING]["port"] assert setup_component(self.hass, self.COMPONENT.DOMAIN, config) + self.hass.block_till_done() assert mock_client.call_count == 1 assert mock_client.call_args == mock.call( "foo", "user", "pass", port=6443, use_tls=True, verify=True @@ -75,6 +76,7 @@ class TestMfiSensorSetup(unittest.TestCase): config = dict(self.GOOD_CONFIG) config[self.THING]["port"] = 6123 assert setup_component(self.hass, self.COMPONENT.DOMAIN, config) + self.hass.block_till_done() assert mock_client.call_count == 1 assert mock_client.call_args == mock.call( "foo", "user", "pass", port=6123, use_tls=True, verify=True @@ -88,6 +90,7 @@ class TestMfiSensorSetup(unittest.TestCase): config[self.THING]["ssl"] = False config[self.THING]["verify_ssl"] = False assert setup_component(self.hass, self.COMPONENT.DOMAIN, config) + self.hass.block_till_done() assert mock_client.call_count == 1 assert mock_client.call_args == mock.call( "foo", "user", "pass", port=6080, use_tls=False, verify=False @@ -105,6 +108,7 @@ class TestMfiSensorSetup(unittest.TestCase): mock.MagicMock(ports=ports) ] assert setup_component(self.hass, sensor.DOMAIN, self.GOOD_CONFIG) + self.hass.block_till_done() for ident, port in ports.items(): if ident != "bad": mock_sensor.assert_any_call(port, self.hass) diff --git a/tests/components/mfi/test_switch.py b/tests/components/mfi/test_switch.py index 414e0b8b50b..45bb3530266 100644 --- a/tests/components/mfi/test_switch.py +++ b/tests/components/mfi/test_switch.py @@ -48,6 +48,7 @@ class TestMfiSwitchSetup(unittest.TestCase): mock.MagicMock(ports=ports) ] assert setup_component(self.hass, switch.DOMAIN, self.GOOD_CONFIG) + self.hass.block_till_done() for ident, port in ports.items(): if ident != "bad": mock_switch.assert_any_call(port) diff --git a/tests/components/microsoft_face_detect/test_image_processing.py b/tests/components/microsoft_face_detect/test_image_processing.py index 3f38c07cb43..d2e0eb19d57 100644 --- a/tests/components/microsoft_face_detect/test_image_processing.py +++ b/tests/components/microsoft_face_detect/test_image_processing.py @@ -45,6 +45,7 @@ class TestMicrosoftFaceDetectSetup: with assert_setup_component(1, ip.DOMAIN): setup_component(self.hass, ip.DOMAIN, config) + self.hass.block_till_done() assert self.hass.states.get("image_processing.microsoftface_demo_camera") @@ -65,6 +66,7 @@ class TestMicrosoftFaceDetectSetup: with assert_setup_component(1, ip.DOMAIN): setup_component(self.hass, ip.DOMAIN, config) + self.hass.block_till_done() assert self.hass.states.get("image_processing.test_local") @@ -113,9 +115,10 @@ class TestMicrosoftFaceDetect: ) setup_component(self.hass, ip.DOMAIN, self.config) + self.hass.block_till_done() state = self.hass.states.get("camera.demo_camera") - url = f"{self.hass.config.api.base_url}{state.attributes.get(ATTR_ENTITY_PICTURE)}" + url = f"{self.hass.config.internal_url}{state.attributes.get(ATTR_ENTITY_PICTURE)}" face_events = [] diff --git a/tests/components/microsoft_face_identify/test_image_processing.py b/tests/components/microsoft_face_identify/test_image_processing.py index 58a752c1887..856d308816c 100644 --- a/tests/components/microsoft_face_identify/test_image_processing.py +++ b/tests/components/microsoft_face_identify/test_image_processing.py @@ -45,6 +45,7 @@ class TestMicrosoftFaceIdentifySetup: with assert_setup_component(1, ip.DOMAIN): setup_component(self.hass, ip.DOMAIN, config) + self.hass.block_till_done() assert self.hass.states.get("image_processing.microsoftface_demo_camera") @@ -66,6 +67,7 @@ class TestMicrosoftFaceIdentifySetup: with assert_setup_component(1, ip.DOMAIN): setup_component(self.hass, ip.DOMAIN, config) + self.hass.block_till_done() assert self.hass.states.get("image_processing.test_local") @@ -114,9 +116,10 @@ class TestMicrosoftFaceIdentify: ) setup_component(self.hass, ip.DOMAIN, self.config) + self.hass.block_till_done() state = self.hass.states.get("camera.demo_camera") - url = f"{self.hass.config.api.base_url}{state.attributes.get(ATTR_ENTITY_PICTURE)}" + url = f"{self.hass.config.internal_url}{state.attributes.get(ATTR_ENTITY_PICTURE)}" face_events = [] diff --git a/tests/components/min_max/test_sensor.py b/tests/components/min_max/test_sensor.py index 10860d0fbe4..57ac39f8ee4 100644 --- a/tests/components/min_max/test_sensor.py +++ b/tests/components/min_max/test_sensor.py @@ -54,7 +54,9 @@ class TestMinMaxSensor(unittest.TestCase): state = self.hass.states.get("sensor.test_min") assert str(float(self.min)) == state.state + assert entity_ids[2] == state.attributes.get("min_entity_id") assert self.max == state.attributes.get("max_value") + assert entity_ids[1] == state.attributes.get("max_entity_id") assert self.mean == state.attributes.get("mean") def test_max_sensor(self): @@ -79,7 +81,9 @@ class TestMinMaxSensor(unittest.TestCase): state = self.hass.states.get("sensor.test_max") assert str(float(self.max)) == state.state + assert entity_ids[2] == state.attributes.get("min_entity_id") assert self.min == state.attributes.get("min_value") + assert entity_ids[1] == state.attributes.get("max_entity_id") assert self.mean == state.attributes.get("mean") def test_mean_sensor(self): @@ -105,7 +109,9 @@ class TestMinMaxSensor(unittest.TestCase): assert str(float(self.mean)) == state.state assert self.min == state.attributes.get("min_value") + assert entity_ids[2] == state.attributes.get("min_entity_id") assert self.max == state.attributes.get("max_value") + assert entity_ids[1] == state.attributes.get("max_entity_id") def test_mean_1_digit_sensor(self): """Test the mean with 1-digit precision sensor.""" @@ -131,7 +137,9 @@ class TestMinMaxSensor(unittest.TestCase): assert str(float(self.mean_1_digit)) == state.state assert self.min == state.attributes.get("min_value") + assert entity_ids[2] == state.attributes.get("min_entity_id") assert self.max == state.attributes.get("max_value") + assert entity_ids[1] == state.attributes.get("max_entity_id") def test_mean_4_digit_sensor(self): """Test the mean with 1-digit precision sensor.""" @@ -157,7 +165,9 @@ class TestMinMaxSensor(unittest.TestCase): assert str(float(self.mean_4_digits)) == state.state assert self.min == state.attributes.get("min_value") + assert entity_ids[2] == state.attributes.get("min_entity_id") assert self.max == state.attributes.get("max_value") + assert entity_ids[1] == state.attributes.get("max_entity_id") def test_not_enough_sensor_value(self): """Test that there is nothing done if not enough values available.""" @@ -179,24 +189,40 @@ class TestMinMaxSensor(unittest.TestCase): state = self.hass.states.get("sensor.test_max") assert STATE_UNKNOWN == state.state + assert state.attributes.get("min_entity_id") is None + assert state.attributes.get("min_value") is None + assert state.attributes.get("max_entity_id") is None + assert state.attributes.get("max_value") is None self.hass.states.set(entity_ids[1], self.values[1]) self.hass.block_till_done() state = self.hass.states.get("sensor.test_max") assert STATE_UNKNOWN != state.state + assert entity_ids[1] == state.attributes.get("min_entity_id") + assert self.values[1] == state.attributes.get("min_value") + assert entity_ids[1] == state.attributes.get("max_entity_id") + assert self.values[1] == state.attributes.get("max_value") self.hass.states.set(entity_ids[2], STATE_UNKNOWN) self.hass.block_till_done() state = self.hass.states.get("sensor.test_max") assert STATE_UNKNOWN != state.state + assert entity_ids[1] == state.attributes.get("min_entity_id") + assert self.values[1] == state.attributes.get("min_value") + assert entity_ids[1] == state.attributes.get("max_entity_id") + assert self.values[1] == state.attributes.get("max_value") self.hass.states.set(entity_ids[1], STATE_UNAVAILABLE) self.hass.block_till_done() state = self.hass.states.get("sensor.test_max") assert STATE_UNKNOWN == state.state + assert state.attributes.get("min_entity_id") is None + assert state.attributes.get("min_value") is None + assert state.attributes.get("max_entity_id") is None + assert state.attributes.get("max_value") is None def test_different_unit_of_measurement(self): """Test for different unit of measurement.""" @@ -264,6 +290,7 @@ class TestMinMaxSensor(unittest.TestCase): self.hass.block_till_done() state = self.hass.states.get("sensor.test_last") assert str(float(value)) == state.state + assert entity_id == state.attributes.get("last_entity_id") assert self.min == state.attributes.get("min_value") assert self.max == state.attributes.get("max_value") diff --git a/tests/components/mobile_app/test_device_tracker.py b/tests/components/mobile_app/test_device_tracker.py index ab97ac3b9ac..164b90a5290 100644 --- a/tests/components/mobile_app/test_device_tracker.py +++ b/tests/components/mobile_app/test_device_tracker.py @@ -97,6 +97,7 @@ async def test_restoring_location(hass, create_registrations, webhook_client): # mobile app doesn't support unloading, so we just reload device tracker await hass.config_entries.async_forward_entry_unload(config_entry, "device_tracker") await hass.config_entries.async_forward_entry_setup(config_entry, "device_tracker") + await hass.async_block_till_done() state_2 = hass.states.get("device_tracker.test_1_2") assert state_2 is not None diff --git a/tests/components/mobile_app/test_entity.py b/tests/components/mobile_app/test_entity.py index 5dc9717ce36..fd5baf50beb 100644 --- a/tests/components/mobile_app/test_entity.py +++ b/tests/components/mobile_app/test_entity.py @@ -95,8 +95,8 @@ async def test_sensor_must_register(hass, create_registrations, webhook_client): assert json["battery_state"]["error"]["code"] == "not_registered" -async def test_sensor_id_no_dupes(hass, create_registrations, webhook_client): - """Test that sensors must have a unique ID.""" +async def test_sensor_id_no_dupes(hass, create_registrations, webhook_client, caplog): + """Test that a duplicate unique ID in registration updates the sensor.""" webhook_id = create_registrations[1]["webhook_id"] webhook_url = f"/api/webhook/{webhook_id}" @@ -120,14 +120,41 @@ async def test_sensor_id_no_dupes(hass, create_registrations, webhook_client): reg_json = await reg_resp.json() assert reg_json == {"success": True} + await hass.async_block_till_done() + assert "Re-register existing sensor" not in caplog.text + + entity = hass.states.get("sensor.test_1_battery_state") + assert entity is not None + + assert entity.attributes["device_class"] == "battery" + assert entity.attributes["icon"] == "mdi:battery" + assert entity.attributes["unit_of_measurement"] == UNIT_PERCENTAGE + assert entity.attributes["foo"] == "bar" + assert entity.domain == "sensor" + assert entity.name == "Test 1 Battery State" + assert entity.state == "100" + + payload["data"]["state"] = 99 dupe_resp = await webhook_client.post(webhook_url, json=payload) - assert dupe_resp.status == 409 + assert dupe_resp.status == 201 + dupe_reg_json = await dupe_resp.json() + assert dupe_reg_json == {"success": True} + await hass.async_block_till_done() - dupe_json = await dupe_resp.json() - assert dupe_json["success"] is False - assert dupe_json["error"]["code"] == "duplicate_unique_id" + assert "Re-register existing sensor" in caplog.text + + entity = hass.states.get("sensor.test_1_battery_state") + assert entity is not None + + assert entity.attributes["device_class"] == "battery" + assert entity.attributes["icon"] == "mdi:battery" + assert entity.attributes["unit_of_measurement"] == UNIT_PERCENTAGE + assert entity.attributes["foo"] == "bar" + assert entity.domain == "sensor" + assert entity.name == "Test 1 Battery State" + assert entity.state == "99" async def test_register_sensor_no_state(hass, create_registrations, webhook_client): diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index 814e59e5571..885aa5fc235 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -69,6 +69,7 @@ async def run_test( now = dt_util.utcnow() with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): assert await async_setup_component(hass, entity_domain, config) + await hass.async_block_till_done() # Trigger update call with time_changed event now += timedelta(seconds=scan_interval + 1) diff --git a/tests/components/mold_indicator/test_sensor.py b/tests/components/mold_indicator/test_sensor.py index bad7430e9b2..5f3b223bf66 100644 --- a/tests/components/mold_indicator/test_sensor.py +++ b/tests/components/mold_indicator/test_sensor.py @@ -52,7 +52,7 @@ class TestSensorMoldIndicator(unittest.TestCase): } }, ) - + self.hass.block_till_done() moldind = self.hass.states.get("sensor.mold_indicator") assert moldind assert UNIT_PERCENTAGE == moldind.attributes.get("unit_of_measurement") @@ -82,6 +82,7 @@ class TestSensorMoldIndicator(unittest.TestCase): } }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() moldind = self.hass.states.get("sensor.mold_indicator") @@ -116,6 +117,7 @@ class TestSensorMoldIndicator(unittest.TestCase): }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() moldind = self.hass.states.get("sensor.mold_indicator") @@ -159,6 +161,7 @@ class TestSensorMoldIndicator(unittest.TestCase): } }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() moldind = self.hass.states.get("sensor.mold_indicator") @@ -196,6 +199,7 @@ class TestSensorMoldIndicator(unittest.TestCase): } }, ) + self.hass.block_till_done() self.hass.start() self.hass.states.set( @@ -268,6 +272,7 @@ class TestSensorMoldIndicator(unittest.TestCase): } }, ) + self.hass.block_till_done() self.hass.start() self.hass.states.set( diff --git a/tests/components/moon/test_sensor.py b/tests/components/moon/test_sensor.py index 89d5a8f798c..fe6e57dd9b6 100644 --- a/tests/components/moon/test_sensor.py +++ b/tests/components/moon/test_sensor.py @@ -29,6 +29,7 @@ class TestMoonSensor(unittest.TestCase): config = {"sensor": {"platform": "moon", "name": "moon_day1"}} assert setup_component(self.hass, "sensor", config) + self.hass.block_till_done() state = self.hass.states.get("sensor.moon_day1") assert state.state == "waxing_crescent" @@ -39,6 +40,7 @@ class TestMoonSensor(unittest.TestCase): config = {"sensor": {"platform": "moon", "name": "moon_day2"}} assert setup_component(self.hass, "sensor", config) + self.hass.block_till_done() state = self.hass.states.get("sensor.moon_day2") assert state.state == "waning_gibbous" diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index 6677122cf10..03e8133bde9 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -107,6 +107,7 @@ async def test_update_state_via_state_topic(hass, mqtt_mock): assert await async_setup_component( hass, alarm_control_panel.DOMAIN, DEFAULT_CONFIG, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -132,6 +133,7 @@ async def test_ignore_update_state_if_unknown_via_state_topic(hass, mqtt_mock): assert await async_setup_component( hass, alarm_control_panel.DOMAIN, DEFAULT_CONFIG, ) + await hass.async_block_till_done() entity_id = "alarm_control_panel.test" @@ -146,6 +148,7 @@ async def test_arm_home_publishes_mqtt(hass, mqtt_mock): assert await async_setup_component( hass, alarm_control_panel.DOMAIN, DEFAULT_CONFIG, ) + await hass.async_block_till_done() await common.async_alarm_arm_home(hass) mqtt_mock.async_publish.assert_called_once_with( @@ -175,6 +178,7 @@ async def test_arm_home_publishes_mqtt_when_code_not_req(hass, mqtt_mock): config = copy.deepcopy(DEFAULT_CONFIG_CODE) config[alarm_control_panel.DOMAIN]["code_arm_required"] = False assert await async_setup_component(hass, alarm_control_panel.DOMAIN, config,) + await hass.async_block_till_done() await common.async_alarm_arm_home(hass) mqtt_mock.async_publish.assert_called_once_with( @@ -187,6 +191,7 @@ async def test_arm_away_publishes_mqtt(hass, mqtt_mock): assert await async_setup_component( hass, alarm_control_panel.DOMAIN, DEFAULT_CONFIG, ) + await hass.async_block_till_done() await common.async_alarm_arm_away(hass) mqtt_mock.async_publish.assert_called_once_with( @@ -216,6 +221,7 @@ async def test_arm_away_publishes_mqtt_when_code_not_req(hass, mqtt_mock): config = copy.deepcopy(DEFAULT_CONFIG_CODE) config[alarm_control_panel.DOMAIN]["code_arm_required"] = False assert await async_setup_component(hass, alarm_control_panel.DOMAIN, config,) + await hass.async_block_till_done() await common.async_alarm_arm_away(hass) mqtt_mock.async_publish.assert_called_once_with( @@ -228,6 +234,7 @@ async def test_arm_night_publishes_mqtt(hass, mqtt_mock): assert await async_setup_component( hass, alarm_control_panel.DOMAIN, DEFAULT_CONFIG, ) + await hass.async_block_till_done() await common.async_alarm_arm_night(hass) mqtt_mock.async_publish.assert_called_once_with( @@ -257,6 +264,7 @@ async def test_arm_night_publishes_mqtt_when_code_not_req(hass, mqtt_mock): config = copy.deepcopy(DEFAULT_CONFIG_CODE) config[alarm_control_panel.DOMAIN]["code_arm_required"] = False assert await async_setup_component(hass, alarm_control_panel.DOMAIN, config,) + await hass.async_block_till_done() await common.async_alarm_arm_night(hass) mqtt_mock.async_publish.assert_called_once_with( @@ -278,6 +286,7 @@ async def test_arm_custom_bypass_publishes_mqtt(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() await common.async_alarm_arm_custom_bypass(hass) mqtt_mock.async_publish.assert_called_once_with( @@ -306,6 +315,7 @@ async def test_arm_custom_bypass_not_publishes_mqtt_with_invalid_code_when_req( } }, ) + await hass.async_block_till_done() call_count = mqtt_mock.async_publish.call_count await common.async_alarm_arm_custom_bypass(hass, "abcd") @@ -331,6 +341,7 @@ async def test_arm_custom_bypass_publishes_mqtt_when_code_not_req(hass, mqtt_moc } }, ) + await hass.async_block_till_done() await common.async_alarm_arm_custom_bypass(hass) mqtt_mock.async_publish.assert_called_once_with( @@ -343,6 +354,7 @@ async def test_disarm_publishes_mqtt(hass, mqtt_mock): assert await async_setup_component( hass, alarm_control_panel.DOMAIN, DEFAULT_CONFIG, ) + await hass.async_block_till_done() await common.async_alarm_disarm(hass) mqtt_mock.async_publish.assert_called_once_with("alarm/command", "DISARM", 0, False) @@ -359,6 +371,7 @@ async def test_disarm_publishes_mqtt_with_template(hass, mqtt_mock): '{"action":"{{ action }}",' '"code":"{{ code }}"}' ) assert await async_setup_component(hass, alarm_control_panel.DOMAIN, config,) + await hass.async_block_till_done() await common.async_alarm_disarm(hass, 1234) mqtt_mock.async_publish.assert_called_once_with( @@ -375,6 +388,7 @@ async def test_disarm_publishes_mqtt_when_code_not_req(hass, mqtt_mock): config[alarm_control_panel.DOMAIN]["code"] = "1234" config[alarm_control_panel.DOMAIN]["code_disarm_required"] = False assert await async_setup_component(hass, alarm_control_panel.DOMAIN, config,) + await hass.async_block_till_done() await common.async_alarm_disarm(hass) mqtt_mock.async_publish.assert_called_once_with("alarm/command", "DISARM", 0, False) @@ -414,6 +428,7 @@ async def test_update_state_via_state_topic_template(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("alarm_control_panel.test") assert state.state == STATE_UNKNOWN @@ -430,6 +445,7 @@ async def test_attributes_code_number(hass, mqtt_mock): config[alarm_control_panel.DOMAIN]["code"] = CODE_NUMBER assert await async_setup_component(hass, alarm_control_panel.DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get("alarm_control_panel.test") assert ( @@ -444,6 +460,7 @@ async def test_attributes_code_text(hass, mqtt_mock): config[alarm_control_panel.DOMAIN]["code"] = CODE_TEXT assert await async_setup_component(hass, alarm_control_panel.DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get("alarm_control_panel.test") assert ( diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 31acf187ad5..8c68fabf214 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -71,6 +71,7 @@ async def test_setting_sensor_value_expires_availability_topic(hass, mqtt_mock, } }, ) + await hass.async_block_till_done() state = hass.states.get("binary_sensor.test") assert state.state == STATE_UNAVAILABLE @@ -99,6 +100,7 @@ async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog): } }, ) + await hass.async_block_till_done() # State should be unavailable since expire_after is defined and > 0 state = hass.states.get("binary_sensor.test") @@ -172,6 +174,7 @@ async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("binary_sensor.test") @@ -201,6 +204,7 @@ async def test_invalid_sensor_value_via_mqtt_message(hass, mqtt_mock, caplog): } }, ) + await hass.async_block_till_done() state = hass.states.get("binary_sensor.test") @@ -240,6 +244,7 @@ async def test_setting_sensor_value_via_mqtt_message_and_template(hass, mqtt_moc } }, ) + await hass.async_block_till_done() state = hass.states.get("binary_sensor.test") assert state.state == STATE_OFF @@ -267,6 +272,7 @@ async def test_valid_device_class(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("binary_sensor.test") assert state.attributes.get("device_class") == "motion" @@ -286,6 +292,7 @@ async def test_invalid_device_class(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("binary_sensor.test") assert state is None @@ -327,6 +334,7 @@ async def test_force_update_disabled(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() events = [] @@ -362,6 +370,7 @@ async def test_force_update_enabled(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() events = [] @@ -398,6 +407,7 @@ async def test_off_delay(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() events = [] diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index 5747d876b57..11b846d4c38 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -49,6 +49,7 @@ async def test_run_camera_setup(hass, aiohttp_client): "camera", {"camera": {"platform": "mqtt", "topic": topic, "name": "Test Camera"}}, ) + await hass.async_block_till_done() url = hass.states.get("camera.test_camera").attributes["entity_picture"] diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 8c3bfebed20..30018c7c175 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -72,6 +72,7 @@ DEFAULT_CONFIG = { async def test_setup_params(hass, mqtt_mock): """Test the initial parameters.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("temperature") == 21 @@ -85,6 +86,7 @@ async def test_setup_params(hass, mqtt_mock): async def test_supported_features(hass, mqtt_mock): """Test the supported_features.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) support = ( @@ -102,6 +104,7 @@ async def test_supported_features(hass, mqtt_mock): async def test_get_hvac_modes(hass, mqtt_mock): """Test that the operation list returns the correct modes.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) modes = state.attributes.get("hvac_modes") @@ -121,6 +124,7 @@ async def test_set_operation_bad_attr_and_state(hass, mqtt_mock, caplog): Also check the state. """ assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) assert state.state == "off" @@ -136,6 +140,7 @@ async def test_set_operation_bad_attr_and_state(hass, mqtt_mock, caplog): async def test_set_operation(hass, mqtt_mock): """Test setting of new operation mode.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) assert state.state == "off" @@ -151,6 +156,7 @@ async def test_set_operation_pessimistic(hass, mqtt_mock): config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["mode_state_topic"] = "mode-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) assert state.state == "unknown" @@ -173,6 +179,7 @@ async def test_set_operation_with_power_command(hass, mqtt_mock): config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["power_command_topic"] = "power-command" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) assert state.state == "off" @@ -196,6 +203,7 @@ async def test_set_operation_with_power_command(hass, mqtt_mock): async def test_set_fan_mode_bad_attr(hass, mqtt_mock, caplog): """Test setting fan mode without required attribute.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("fan_mode") == "low" @@ -213,6 +221,7 @@ async def test_set_fan_mode_pessimistic(hass, mqtt_mock): config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["fan_mode_state_topic"] = "fan-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("fan_mode") is None @@ -233,6 +242,7 @@ async def test_set_fan_mode_pessimistic(hass, mqtt_mock): async def test_set_fan_mode(hass, mqtt_mock): """Test setting of new fan mode.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("fan_mode") == "low" @@ -245,6 +255,7 @@ async def test_set_fan_mode(hass, mqtt_mock): async def test_set_swing_mode_bad_attr(hass, mqtt_mock, caplog): """Test setting swing mode without required attribute.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("swing_mode") == "off" @@ -262,6 +273,7 @@ async def test_set_swing_pessimistic(hass, mqtt_mock): config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["swing_mode_state_topic"] = "swing-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("swing_mode") is None @@ -282,6 +294,7 @@ async def test_set_swing_pessimistic(hass, mqtt_mock): async def test_set_swing(hass, mqtt_mock): """Test setting of new swing mode.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("swing_mode") == "off" @@ -294,6 +307,7 @@ async def test_set_swing(hass, mqtt_mock): async def test_set_target_temperature(hass, mqtt_mock): """Test setting the target temperature.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("temperature") == 21 @@ -326,6 +340,7 @@ async def test_set_target_temperature_pessimistic(hass, mqtt_mock): config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["temperature_state_topic"] = "temperature-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("temperature") is None @@ -346,6 +361,7 @@ async def test_set_target_temperature_pessimistic(hass, mqtt_mock): async def test_set_target_temperature_low_high(hass, mqtt_mock): """Test setting the low/high target temperature.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + await hass.async_block_till_done() await common.async_set_temperature( hass, target_temp_low=20, target_temp_high=23, entity_id=ENTITY_CLIMATE @@ -363,6 +379,7 @@ async def test_set_target_temperature_low_highpessimistic(hass, mqtt_mock): config["climate"]["temperature_low_state_topic"] = "temperature-low-state" config["climate"]["temperature_high_state_topic"] = "temperature-high-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("target_temp_low") is None @@ -398,6 +415,7 @@ async def test_receive_mqtt_temperature(hass, mqtt_mock): config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["current_temperature_topic"] = "current_temperature" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() async_fire_mqtt_message(hass, "current_temperature", "47") state = hass.states.get(ENTITY_CLIMATE) @@ -409,6 +427,7 @@ async def test_set_away_mode_pessimistic(hass, mqtt_mock): config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["away_mode_state_topic"] = "away-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") is None @@ -437,6 +456,7 @@ async def test_set_away_mode(hass, mqtt_mock): config["climate"]["payload_off"] = "AUS" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") is None @@ -467,6 +487,7 @@ async def test_set_hvac_action(hass, mqtt_mock): config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["action_topic"] = "action" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("hvac_action") is None @@ -481,6 +502,7 @@ async def test_set_hold_pessimistic(hass, mqtt_mock): config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["hold_state_topic"] = "hold-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("hold_mode") is None @@ -501,6 +523,7 @@ async def test_set_hold_pessimistic(hass, mqtt_mock): async def test_set_hold(hass, mqtt_mock): """Test setting the hold mode.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") is None @@ -525,6 +548,7 @@ async def test_set_hold(hass, mqtt_mock): async def test_set_preset_mode_twice(hass, mqtt_mock): """Test setting of the same mode twice only publishes once.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") is None @@ -543,6 +567,7 @@ async def test_set_aux_pessimistic(hass, mqtt_mock): config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["aux_state_topic"] = "aux-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("aux_heat") == "off" @@ -567,6 +592,7 @@ async def test_set_aux_pessimistic(hass, mqtt_mock): async def test_set_aux(hass, mqtt_mock): """Test setting of the aux heating.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("aux_heat") == "off" @@ -612,6 +638,7 @@ async def test_set_target_temperature_low_high_with_templates(hass, mqtt_mock, c config["climate"]["temperature_high_state_template"] = "{{ value_json.temp_high }}" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) @@ -658,6 +685,7 @@ async def test_set_with_templates(hass, mqtt_mock, caplog): config["climate"]["current_temperature_topic"] = "current-temperature" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() # Operation Mode state = hass.states.get(ENTITY_CLIMATE) @@ -745,6 +773,7 @@ async def test_min_temp_custom(hass, mqtt_mock): config["climate"]["min_temp"] = 26 assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) min_temp = state.attributes.get("min_temp") @@ -759,6 +788,7 @@ async def test_max_temp_custom(hass, mqtt_mock): config["climate"]["max_temp"] = 60 assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) max_temp = state.attributes.get("max_temp") @@ -773,6 +803,7 @@ async def test_temp_step_custom(hass, mqtt_mock): config["climate"]["temp_step"] = 0.01 assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) temp_step = state.attributes.get("target_temp_step") @@ -788,6 +819,7 @@ async def test_temperature_unit(hass, mqtt_mock): config["climate"]["current_temperature_topic"] = "current_temperature" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() async_fire_mqtt_message(hass, "current_temperature", "77") @@ -945,6 +977,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): async def test_precision_default(hass, mqtt_mock): """Test that setting precision to tenths works as intended.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + await hass.async_block_till_done() await common.async_set_temperature( hass, temperature=23.67, entity_id=ENTITY_CLIMATE @@ -959,6 +992,7 @@ async def test_precision_halves(hass, mqtt_mock): config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["precision"] = 0.5 assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() await common.async_set_temperature( hass, temperature=23.67, entity_id=ENTITY_CLIMATE @@ -973,6 +1007,7 @@ async def test_precision_whole(hass, mqtt_mock): config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["precision"] = 1.0 assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() await common.async_set_temperature( hass, temperature=23.67, entity_id=ENTITY_CLIMATE diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index bfd478a712e..d0ddc1d4830 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -39,6 +39,7 @@ async def help_test_availability_without_topic(hass, mqtt_mock, domain, config): """Test availability without defined availability topic.""" assert "availability_topic" not in config[domain] assert await async_setup_component(hass, domain, config) + await hass.async_block_till_done() state = hass.states.get(f"{domain}.test") assert state.state != STATE_UNAVAILABLE @@ -61,6 +62,7 @@ async def help_test_default_availability_payload( config = copy.deepcopy(config) config[domain]["availability_topic"] = "availability-topic" assert await async_setup_component(hass, domain, config,) + await hass.async_block_till_done() state = hass.states.get(f"{domain}.test") assert state.state == STATE_UNAVAILABLE @@ -108,6 +110,7 @@ async def help_test_custom_availability_payload( config[domain]["payload_available"] = "good" config[domain]["payload_not_available"] = "nogood" assert await async_setup_component(hass, domain, config,) + await hass.async_block_till_done() state = hass.states.get(f"{domain}.test") assert state.state == STATE_UNAVAILABLE @@ -147,6 +150,7 @@ async def help_test_setting_attribute_via_mqtt_json_message( config = copy.deepcopy(config) config[domain]["json_attributes_topic"] = "attr-topic" assert await async_setup_component(hass, domain, config,) + await hass.async_block_till_done() async_fire_mqtt_message(hass, "attr-topic", '{ "val": "100" }') state = hass.states.get(f"{domain}.test") @@ -164,6 +168,7 @@ async def help_test_setting_attribute_with_template(hass, mqtt_mock, domain, con config[domain]["json_attributes_topic"] = "attr-topic" config[domain]["json_attributes_template"] = "{{ value_json['Timer1'] | tojson }}" assert await async_setup_component(hass, domain, config,) + await hass.async_block_till_done() async_fire_mqtt_message( hass, "attr-topic", json.dumps({"Timer1": {"Arm": 0, "Time": "22:18"}}) @@ -185,6 +190,7 @@ async def help_test_update_with_json_attrs_not_dict( config = copy.deepcopy(config) config[domain]["json_attributes_topic"] = "attr-topic" assert await async_setup_component(hass, domain, config,) + await hass.async_block_till_done() async_fire_mqtt_message(hass, "attr-topic", '[ "list", "of", "things"]') state = hass.states.get(f"{domain}.test") @@ -204,6 +210,7 @@ async def help_test_update_with_json_attrs_bad_JSON( config = copy.deepcopy(config) config[domain]["json_attributes_topic"] = "attr-topic" assert await async_setup_component(hass, domain, config,) + await hass.async_block_till_done() async_fire_mqtt_message(hass, "attr-topic", "This is not JSON") @@ -251,7 +258,8 @@ async def help_test_discovery_update_attr(hass, mqtt_mock, caplog, domain, confi async def help_test_unique_id(hass, domain, config): """Test unique id option only creates one entity per unique_id.""" await async_mock_mqtt_component(hass) - assert await async_setup_component(hass, domain, config,) + assert await async_setup_component(hass, domain, config) + await hass.async_block_till_done() assert len(hass.states.async_entity_ids(domain)) == 1 @@ -459,6 +467,7 @@ async def help_test_entity_id_update_subscriptions( registry = mock_registry(hass, {}) mock_mqtt = await async_mock_mqtt_component(hass) assert await async_setup_component(hass, domain, config,) + await hass.async_block_till_done() state = hass.states.get(f"{domain}.test") assert state is not None diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 201bb17c7a8..eb758ebf93a 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -76,6 +76,7 @@ async def test_state_via_state_topic(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -112,6 +113,7 @@ async def test_opening_and_closing_state_via_custom_state_payload(hass, mqtt_moc } }, ) + await hass.async_block_till_done() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -152,6 +154,7 @@ async def test_open_closed_state_from_position_optimistic(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -199,6 +202,7 @@ async def test_position_via_position_topic(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -236,6 +240,7 @@ async def test_state_via_template(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -267,6 +272,7 @@ async def test_position_via_template(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -301,6 +307,7 @@ async def test_optimistic_state_change(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -358,6 +365,7 @@ async def test_optimistic_state_change_with_position(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -419,6 +427,7 @@ async def test_send_open_cover_command(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -447,6 +456,7 @@ async def test_send_close_cover_command(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -475,6 +485,7 @@ async def test_send_stop__cover_command(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -507,6 +518,7 @@ async def test_current_cover_position(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state_attributes_dict = hass.states.get("cover.test").attributes assert not (ATTR_CURRENT_POSITION in state_attributes_dict) @@ -557,6 +569,7 @@ async def test_current_cover_position_inverted(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state_attributes_dict = hass.states.get("cover.test").attributes assert not (ATTR_CURRENT_POSITION in state_attributes_dict) @@ -613,6 +626,7 @@ async def test_optimistic_position(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("cover.test") assert state is None @@ -638,6 +652,7 @@ async def test_position_update(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state_attributes_dict = hass.states.get("cover.test").attributes assert not (ATTR_CURRENT_POSITION in state_attributes_dict) @@ -675,6 +690,7 @@ async def test_set_position_templated(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() await hass.services.async_call( cover.DOMAIN, @@ -706,6 +722,7 @@ async def test_set_position_untemplated(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() await hass.services.async_call( cover.DOMAIN, @@ -737,6 +754,7 @@ async def test_set_position_untemplated_custom_percentage_range(hass, mqtt_mock) } }, ) + await hass.async_block_till_done() await hass.services.async_call( cover.DOMAIN, @@ -766,6 +784,7 @@ async def test_no_command_topic(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() assert hass.states.get("cover.test").attributes["supported_features"] == 240 @@ -787,6 +806,7 @@ async def test_no_payload_stop(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() assert hass.states.get("cover.test").attributes["supported_features"] == 3 @@ -810,6 +830,7 @@ async def test_with_command_topic_and_tilt(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() assert hass.states.get("cover.test").attributes["supported_features"] == 251 @@ -834,6 +855,7 @@ async def test_tilt_defaults(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state_attributes_dict = hass.states.get("cover.test").attributes assert ATTR_CURRENT_TILT_POSITION in state_attributes_dict @@ -864,6 +886,7 @@ async def test_tilt_via_invocation_defaults(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() await hass.services.async_call( cover.DOMAIN, @@ -943,6 +966,7 @@ async def test_tilt_given_value(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() await hass.services.async_call( cover.DOMAIN, @@ -1023,6 +1047,7 @@ async def test_tilt_given_value_optimistic(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() await hass.services.async_call( cover.DOMAIN, @@ -1079,6 +1104,7 @@ async def test_tilt_given_value_altered_range(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() await hass.services.async_call( cover.DOMAIN, @@ -1145,6 +1171,7 @@ async def test_tilt_via_topic(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() async_fire_mqtt_message(hass, "tilt-status-topic", "0") @@ -1184,6 +1211,7 @@ async def test_tilt_via_topic_template(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() async_fire_mqtt_message(hass, "tilt-status-topic", "99") @@ -1222,6 +1250,7 @@ async def test_tilt_via_topic_altered_range(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() async_fire_mqtt_message(hass, "tilt-status-topic", "0") @@ -1270,6 +1299,7 @@ async def test_tilt_via_topic_template_altered_range(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() async_fire_mqtt_message(hass, "tilt-status-topic", "99") @@ -1313,6 +1343,7 @@ async def test_tilt_position(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() await hass.services.async_call( cover.DOMAIN, @@ -1348,6 +1379,7 @@ async def test_tilt_position_altered_range(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() await hass.services.async_call( cover.DOMAIN, @@ -1738,6 +1770,7 @@ async def test_valid_device_class(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("cover.test") assert state.attributes.get("device_class") == "garage" @@ -1757,6 +1790,7 @@ async def test_invalid_device_class(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("cover.test") assert state is None diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index d8b6ce00ee6..7f6eb79e85e 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -50,6 +50,7 @@ async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): assert await async_setup_component( hass, fan.DOMAIN, {fan.DOMAIN: {"platform": "mqtt", "name": "test"}} ) + await hass.async_block_till_done() assert hass.states.get("fan.test") is None @@ -79,6 +80,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("fan.test") assert state.state is STATE_OFF @@ -141,6 +143,7 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("fan.test") assert state.state is STATE_OFF @@ -207,6 +210,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("fan.test") assert state.state is STATE_OFF @@ -300,6 +304,7 @@ async def test_on_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("fan.test") assert state.state is STATE_OFF @@ -352,6 +357,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("fan.test") assert state.state is STATE_OFF @@ -450,6 +456,7 @@ async def test_attributes(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("fan.test") assert state.state is STATE_OFF @@ -537,6 +544,7 @@ async def test_custom_speed_list(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("fan.test") assert state.state is STATE_OFF @@ -577,6 +585,7 @@ async def test_supported_features(hass, mqtt_mock): ] }, ) + await hass.async_block_till_done() state = hass.states.get("fan.test1") assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 0 diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index b402c23e299..032a55edee4 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -76,6 +76,7 @@ async def test_default_supported_features(hass, mqtt_mock): assert await async_setup_component( hass, vacuum.DOMAIN, {vacuum.DOMAIN: DEFAULT_CONFIG} ) + await hass.async_block_till_done() entity = hass.states.get("vacuum.mqtttest") entity_features = entity.attributes.get(mqttvacuum.CONF_SUPPORTED_FEATURES, 0) assert sorted(services_to_strings(entity_features, SERVICE_TO_STRING)) == sorted( @@ -99,6 +100,7 @@ async def test_all_commands(hass, mqtt_mock): ) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + await hass.async_block_till_done() await common.async_turn_on(hass, "vacuum.mqtttest") mqtt_mock.async_publish.assert_called_once_with( @@ -178,6 +180,7 @@ async def test_commands_without_supported_features(hass, mqtt_mock): ) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + await hass.async_block_till_done() await common.async_turn_on(hass, "vacuum.mqtttest") mqtt_mock.async_publish.assert_not_called() @@ -225,6 +228,7 @@ async def test_attributes_without_supported_features(hass, mqtt_mock): ) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + await hass.async_block_till_done() message = """{ "battery_level": 54, @@ -250,6 +254,7 @@ async def test_status(hass, mqtt_mock): ) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + await hass.async_block_till_done() message = """{ "battery_level": 54, @@ -289,6 +294,7 @@ async def test_status_battery(hass, mqtt_mock): ) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + await hass.async_block_till_done() message = """{ "battery_level": 54 @@ -306,6 +312,7 @@ async def test_status_cleaning(hass, mqtt_mock): ) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + await hass.async_block_till_done() message = """{ "cleaning": true @@ -323,6 +330,7 @@ async def test_status_docked(hass, mqtt_mock): ) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + await hass.async_block_till_done() message = """{ "docked": true @@ -340,6 +348,7 @@ async def test_status_charging(hass, mqtt_mock): ) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + await hass.async_block_till_done() message = """{ "charging": true @@ -357,6 +366,7 @@ async def test_status_fan_speed(hass, mqtt_mock): ) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + await hass.async_block_till_done() message = """{ "fan_speed": "max" @@ -374,6 +384,7 @@ async def test_status_fan_speed_list(hass, mqtt_mock): ) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + await hass.async_block_till_done() state = hass.states.get("vacuum.mqtttest") assert state.attributes.get(ATTR_FAN_SPEED_LIST) == ["min", "medium", "high", "max"] @@ -391,6 +402,7 @@ async def test_status_no_fan_speed_list(hass, mqtt_mock): ) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + await hass.async_block_till_done() state = hass.states.get("vacuum.mqtttest") assert state.attributes.get(ATTR_FAN_SPEED_LIST) is None @@ -404,6 +416,7 @@ async def test_status_error(hass, mqtt_mock): ) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + await hass.async_block_till_done() message = """{ "error": "Error1" @@ -434,6 +447,7 @@ async def test_battery_template(hass, mqtt_mock): ) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + await hass.async_block_till_done() async_fire_mqtt_message(hass, "retroroomba/battery_level", "54") state = hass.states.get("vacuum.mqtttest") @@ -449,6 +463,7 @@ async def test_status_invalid_json(hass, mqtt_mock): ) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + await hass.async_block_till_done() async_fire_mqtt_message(hass, "vacuum/state", '{"asdfasas false}') state = hass.states.get("vacuum.mqtttest") @@ -462,6 +477,7 @@ async def test_missing_battery_template(hass, mqtt_mock): config.pop(mqttvacuum.CONF_BATTERY_LEVEL_TEMPLATE) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + await hass.async_block_till_done() state = hass.states.get("vacuum.mqtttest") assert state is None @@ -473,6 +489,7 @@ async def test_missing_charging_template(hass, mqtt_mock): config.pop(mqttvacuum.CONF_CHARGING_TEMPLATE) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + await hass.async_block_till_done() state = hass.states.get("vacuum.mqtttest") assert state is None @@ -484,6 +501,7 @@ async def test_missing_cleaning_template(hass, mqtt_mock): config.pop(mqttvacuum.CONF_CLEANING_TEMPLATE) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + await hass.async_block_till_done() state = hass.states.get("vacuum.mqtttest") assert state is None @@ -495,6 +513,7 @@ async def test_missing_docked_template(hass, mqtt_mock): config.pop(mqttvacuum.CONF_DOCKED_TEMPLATE) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + await hass.async_block_till_done() state = hass.states.get("vacuum.mqtttest") assert state is None @@ -506,6 +525,7 @@ async def test_missing_error_template(hass, mqtt_mock): config.pop(mqttvacuum.CONF_ERROR_TEMPLATE) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + await hass.async_block_till_done() state = hass.states.get("vacuum.mqtttest") assert state is None @@ -517,6 +537,7 @@ async def test_missing_fan_speed_template(hass, mqtt_mock): config.pop(mqttvacuum.CONF_FAN_SPEED_TEMPLATE) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + await hass.async_block_till_done() state = hass.states.get("vacuum.mqtttest") assert state is None diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 7cf034ec4e1..f832e235915 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -201,6 +201,7 @@ async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {"platform": "mqtt", "name": "test"}} ) + await hass.async_block_till_done() assert hass.states.get("light.test") is None @@ -218,6 +219,7 @@ async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics(hass, mqt } }, ) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -269,6 +271,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): } assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -365,6 +368,7 @@ async def test_invalid_state_via_topic(hass, mqtt_mock, caplog): } assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -456,6 +460,7 @@ async def test_brightness_controlling_scale(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -501,6 +506,7 @@ async def test_brightness_from_rgb_controlling_scale(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -540,6 +546,7 @@ async def test_white_value_controlling_scale(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -599,6 +606,7 @@ async def test_controlling_state_via_topic_with_templates(hass, mqtt_mock): } assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -676,6 +684,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): ): with assert_setup_component(1, light.DOMAIN): assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_ON @@ -751,6 +760,7 @@ async def test_sending_mqtt_rgb_command_with_template(hass, mqtt_mock): } assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -786,6 +796,7 @@ async def test_sending_mqtt_color_temp_command_with_template(hass, mqtt_mock): } assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -818,6 +829,7 @@ async def test_show_brightness_if_only_command_topic(hass, mqtt_mock): } assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -843,6 +855,7 @@ async def test_show_color_temp_only_if_command_topic(hass, mqtt_mock): } assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -868,6 +881,7 @@ async def test_show_effect_only_if_command_topic(hass, mqtt_mock): } assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -893,6 +907,7 @@ async def test_show_hs_if_only_command_topic(hass, mqtt_mock): } assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -918,6 +933,7 @@ async def test_show_white_value_if_only_command_topic(hass, mqtt_mock): } assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -943,6 +959,7 @@ async def test_show_xy_if_only_command_topic(hass, mqtt_mock): } assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -968,6 +985,7 @@ async def test_on_command_first(hass, mqtt_mock): } assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -1002,6 +1020,7 @@ async def test_on_command_last(hass, mqtt_mock): } assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -1038,6 +1057,7 @@ async def test_on_command_brightness(hass, mqtt_mock): } assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -1091,6 +1111,7 @@ async def test_on_command_brightness_scaled(hass, mqtt_mock): } assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -1153,6 +1174,7 @@ async def test_on_command_rgb(hass, mqtt_mock): } assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -1243,6 +1265,7 @@ async def test_on_command_rgb_template(hass, mqtt_mock): } assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -1279,6 +1302,7 @@ async def test_effect(hass, mqtt_mock): } assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -1498,6 +1522,7 @@ async def test_max_mireds(hass, mqtt_mock): } assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.attributes.get("min_mireds") == 153 diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index b98282b5288..bb9e2afb0e5 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -156,6 +156,7 @@ async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): light.DOMAIN, {light.DOMAIN: {"platform": "mqtt", "schema": "json", "name": "test"}}, ) + await hass.async_block_till_done() assert hass.states.get("light.test") is None @@ -174,6 +175,7 @@ async def test_no_color_brightness_color_temp_white_val_if_no_topics(hass, mqtt_ } }, ) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -222,6 +224,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -346,6 +349,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_ON @@ -457,6 +461,7 @@ async def test_sending_hs_color(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -519,6 +524,7 @@ async def test_sending_rgb_color_no_brightness(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -572,6 +578,7 @@ async def test_sending_rgb_color_with_brightness(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -641,6 +648,7 @@ async def test_sending_rgb_color_with_scaled_brightness(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -709,6 +717,7 @@ async def test_sending_xy_color(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -771,6 +780,7 @@ async def test_effect(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -830,6 +840,7 @@ async def test_flash_short_and_long(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -869,6 +880,7 @@ async def test_transition(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -916,6 +928,7 @@ async def test_brightness_scale(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -959,6 +972,7 @@ async def test_invalid_values(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -1235,6 +1249,7 @@ async def test_max_mireds(hass, mqtt_mock): } assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.attributes.get("min_mireds") == 153 diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index edb7900e0da..cb5aff40b4b 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -84,6 +84,7 @@ async def test_setup_fails(hass, mqtt_mock): light.DOMAIN, {light.DOMAIN: {"platform": "mqtt", "schema": "template", "name": "test"}}, ) + await hass.async_block_till_done() assert hass.states.get("light.test") is None with assert_setup_component(0, light.DOMAIN): @@ -99,6 +100,7 @@ async def test_setup_fails(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() assert hass.states.get("light.test") is None with assert_setup_component(0, light.DOMAIN): @@ -115,6 +117,7 @@ async def test_setup_fails(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() assert hass.states.get("light.test") is None with assert_setup_component(0, light.DOMAIN): @@ -131,6 +134,7 @@ async def test_setup_fails(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() assert hass.states.get("light.test") is None @@ -159,6 +163,7 @@ async def test_state_change_via_topic(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -214,6 +219,7 @@ async def test_state_brightness_color_effect_temp_white_change_via_topic( } }, ) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -313,6 +319,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_ON @@ -454,6 +461,7 @@ async def test_sending_mqtt_commands_non_optimistic_brightness_template( } }, ) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -567,6 +575,7 @@ async def test_effect(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -617,6 +626,7 @@ async def test_flash(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -664,6 +674,7 @@ async def test_transition(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -719,6 +730,7 @@ async def test_invalid_values(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.state == STATE_OFF @@ -994,6 +1006,7 @@ async def test_max_mireds(hass, mqtt_mock): } assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() state = hass.states.get("light.test") assert state.attributes.get("min_mireds") == 153 diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index 80ecbde3c4d..0e9a9af850f 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -58,6 +58,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED @@ -92,6 +93,7 @@ async def test_controlling_non_default_state_via_topic(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED @@ -127,6 +129,7 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED @@ -163,6 +166,7 @@ async def test_controlling_non_default_state_via_topic_and_json_message( } }, ) + await hass.async_block_till_done() state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED @@ -195,6 +199,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED @@ -240,6 +245,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 58c98f02484..d711b9e3bb8 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -64,6 +64,7 @@ async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() async_fire_mqtt_message(hass, "test-topic", "100") state = hass.states.get("sensor.test") @@ -88,6 +89,7 @@ async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog): } }, ) + await hass.async_block_till_done() state = hass.states.get("sensor.test") assert state.state == "unknown" @@ -155,6 +157,7 @@ async def test_setting_sensor_value_via_mqtt_json_message(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() async_fire_mqtt_message(hass, "test-topic", '{ "val": "100" }') state = hass.states.get("sensor.test") @@ -176,6 +179,7 @@ async def test_force_update_disabled(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() events = [] @@ -209,6 +213,7 @@ async def test_force_update_enabled(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() events = [] @@ -262,6 +267,7 @@ async def test_invalid_device_class(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() state = hass.states.get("sensor.test") assert state is None diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index 15429e6bc57..f77a1a11ca1 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -82,6 +82,7 @@ async def test_default_supported_features(hass, mqtt_mock): assert await async_setup_component( hass, vacuum.DOMAIN, {vacuum.DOMAIN: DEFAULT_CONFIG} ) + await hass.async_block_till_done() entity = hass.states.get("vacuum.mqtttest") entity_features = entity.attributes.get(mqttvacuum.CONF_SUPPORTED_FEATURES, 0) assert sorted(services_to_strings(entity_features, SERVICE_TO_STRING)) == sorted( @@ -97,6 +98,7 @@ async def test_all_commands(hass, mqtt_mock): ) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + await hass.async_block_till_done() await hass.services.async_call( DOMAIN, SERVICE_START, {"entity_id": ENTITY_MATCH_ALL}, blocking=True @@ -168,6 +170,7 @@ async def test_commands_without_supported_features(hass, mqtt_mock): ) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + await hass.async_block_till_done() await hass.services.async_call( DOMAIN, SERVICE_START, {"entity_id": ENTITY_MATCH_ALL}, blocking=True @@ -223,6 +226,7 @@ async def test_status(hass, mqtt_mock): ) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + await hass.async_block_till_done() message = """{ "battery_level": 54, @@ -260,6 +264,7 @@ async def test_no_fan_vacuum(hass, mqtt_mock): ) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + await hass.async_block_till_done() message = """{ "battery_level": 54, @@ -309,6 +314,7 @@ async def test_status_invalid_json(hass, mqtt_mock): ) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) + await hass.async_block_till_done() async_fire_mqtt_message(hass, "vacuum/state", '{"asdfasas false}') state = hass.states.get("vacuum.mqtttest") diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index b51812d2fa3..da66e2a7f60 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -59,6 +59,7 @@ async def test_controlling_state_via_topic(hass, mock_publish): } }, ) + await hass.async_block_till_done() state = hass.states.get("switch.test") assert state.state == STATE_OFF @@ -97,6 +98,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mock_publish): } }, ) + await hass.async_block_till_done() state = hass.states.get("switch.test") assert state.state == STATE_ON @@ -137,6 +139,7 @@ async def test_controlling_state_via_topic_and_json_message(hass, mock_publish): } }, ) + await hass.async_block_till_done() state = hass.states.get("switch.test") assert state.state == STATE_OFF @@ -213,6 +216,7 @@ async def test_custom_state_payload(hass, mock_publish): } }, ) + await hass.async_block_till_done() state = hass.states.get("switch.test") assert state.state == STATE_OFF diff --git a/tests/components/mqtt_room/test_sensor.py b/tests/components/mqtt_room/test_sensor.py index f11786951f6..20aa34342d3 100644 --- a/tests/components/mqtt_room/test_sensor.py +++ b/tests/components/mqtt_room/test_sensor.py @@ -68,6 +68,7 @@ async def test_room_update(hass): } }, ) + await hass.async_block_till_done() await send_message(hass, BEDROOM_TOPIC, FAR_MESSAGE) await assert_state(hass, BEDROOM) diff --git a/tests/components/netatmo/test_config_flow.py b/tests/components/netatmo/test_config_flow.py index 047dd4c0c40..b497df93bf5 100644 --- a/tests/components/netatmo/test_config_flow.py +++ b/tests/components/netatmo/test_config_flow.py @@ -6,6 +6,7 @@ from homeassistant.components.netatmo.const import ( OAUTH2_AUTHORIZE, OAUTH2_TOKEN, ) +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow from tests.async_mock import patch @@ -43,7 +44,7 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock): hass, "netatmo", { - "netatmo": {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET}, + "netatmo": {CONF_CLIENT_ID: CLIENT_ID, CONF_CLIENT_SECRET: CLIENT_SECRET}, "http": {"base_url": "https://example.com"}, }, ) diff --git a/tests/components/nextbus/test_sensor.py b/tests/components/nextbus/test_sensor.py index dd709618ec0..74ea6cce127 100644 --- a/tests/components/nextbus/test_sensor.py +++ b/tests/components/nextbus/test_sensor.py @@ -49,6 +49,7 @@ async def assert_setup_sensor(hass, config, count=1): """Set up the sensor and assert it's been created.""" with assert_setup_component(count): assert await async_setup_component(hass, sensor.DOMAIN, config) + await hass.async_block_till_done() @pytest.fixture diff --git a/tests/components/nsw_fuel_station/test_sensor.py b/tests/components/nsw_fuel_station/test_sensor.py index 2d348204dcc..861aa155f4f 100644 --- a/tests/components/nsw_fuel_station/test_sensor.py +++ b/tests/components/nsw_fuel_station/test_sensor.py @@ -92,6 +92,7 @@ class TestNSWFuelStation(unittest.TestCase): """Test the setup with custom settings.""" with assert_setup_component(1, sensor.DOMAIN): assert setup_component(self.hass, sensor.DOMAIN, {"sensor": VALID_CONFIG}) + self.hass.block_till_done() fake_entities = ["my_fake_station_p95", "my_fake_station_e10"] @@ -106,6 +107,7 @@ class TestNSWFuelStation(unittest.TestCase): def test_sensor_values(self): """Test retrieval of sensor values.""" assert setup_component(self.hass, sensor.DOMAIN, {"sensor": VALID_CONFIG}) + self.hass.block_till_done() assert "140.0" == self.hass.states.get("sensor.my_fake_station_e10").state assert "150.0" == self.hass.states.get("sensor.my_fake_station_p95").state diff --git a/tests/components/nsw_rural_fire_service_feed/test_geo_location.py b/tests/components/nsw_rural_fire_service_feed/test_geo_location.py index 584616967c4..b8923d854ee 100644 --- a/tests/components/nsw_rural_fire_service_feed/test_geo_location.py +++ b/tests/components/nsw_rural_fire_service_feed/test_geo_location.py @@ -126,6 +126,7 @@ async def test_setup(hass): ) with assert_setup_component(1, geo_location.DOMAIN): assert await async_setup_component(hass, geo_location.DOMAIN, CONFIG) + await hass.async_block_till_done() # Artificially trigger update. hass.bus.async_fire(EVENT_HOMEASSISTANT_START) # Collect events. @@ -242,6 +243,7 @@ async def test_setup_with_custom_location(hass): assert await async_setup_component( hass, geo_location.DOMAIN, CONFIG_WITH_CUSTOM_LOCATION ) + await hass.async_block_till_done() # Artificially trigger update. hass.bus.async_fire(EVENT_HOMEASSISTANT_START) diff --git a/tests/components/nws/test_weather.py b/tests/components/nws/test_weather.py index 667f40db137..1486015d80e 100644 --- a/tests/components/nws/test_weather.py +++ b/tests/components/nws/test_weather.py @@ -10,6 +10,7 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM +from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_time_changed from tests.components.nws.const import ( EXPECTED_FORECAST_IMPERIAL, @@ -154,39 +155,79 @@ async def test_entity_refresh(hass, mock_simple_nws): async def test_error_observation(hass, mock_simple_nws): """Test error during update observation.""" - instance = mock_simple_nws.return_value - instance.update_observation.side_effect = aiohttp.ClientError + utc_time = dt_util.utcnow() + with patch("homeassistant.components.nws.utcnow") as mock_utc, patch( + "homeassistant.components.nws.weather.utcnow" + ) as mock_utc_weather: - entry = MockConfigEntry(domain=nws.DOMAIN, data=NWS_CONFIG,) - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + def increment_time(time): + mock_utc.return_value += time + mock_utc_weather.return_value += time + async_fire_time_changed(hass, mock_utc.return_value) - instance.update_observation.assert_called_once() + mock_utc.return_value = utc_time + mock_utc_weather.return_value = utc_time + instance = mock_simple_nws.return_value + # first update fails + instance.update_observation.side_effect = aiohttp.ClientError - state = hass.states.get("weather.abc_daynight") - assert state - assert state.state == "unavailable" + entry = MockConfigEntry(domain=nws.DOMAIN, data=NWS_CONFIG,) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() - state = hass.states.get("weather.abc_hourly") - assert state - assert state.state == "unavailable" + instance.update_observation.assert_called_once() - instance.update_observation.side_effect = None + state = hass.states.get("weather.abc_daynight") + assert state + assert state.state == "unavailable" - future_time = dt_util.utcnow() + timedelta(minutes=15) - async_fire_time_changed(hass, future_time) - await hass.async_block_till_done() + state = hass.states.get("weather.abc_hourly") + assert state + assert state.state == "unavailable" - assert instance.update_observation.call_count == 2 + # second update happens faster and succeeds + instance.update_observation.side_effect = None + increment_time(timedelta(minutes=1)) + await hass.async_block_till_done() - state = hass.states.get("weather.abc_daynight") - assert state - assert state.state == "sunny" + assert instance.update_observation.call_count == 2 - state = hass.states.get("weather.abc_hourly") - assert state - assert state.state == "sunny" + state = hass.states.get("weather.abc_daynight") + assert state + assert state.state == "sunny" + + state = hass.states.get("weather.abc_hourly") + assert state + assert state.state == "sunny" + + # third udate fails, but data is cached + instance.update_observation.side_effect = aiohttp.ClientError + + increment_time(timedelta(minutes=10)) + await hass.async_block_till_done() + + assert instance.update_observation.call_count == 3 + + state = hass.states.get("weather.abc_daynight") + assert state + assert state.state == "sunny" + + state = hass.states.get("weather.abc_hourly") + assert state + assert state.state == "sunny" + + # after 20 minutes data caching expires, data is no longer shown + increment_time(timedelta(minutes=10)) + await hass.async_block_till_done() + + state = hass.states.get("weather.abc_daynight") + assert state + assert state.state == "unavailable" + + state = hass.states.get("weather.abc_hourly") + assert state + assert state.state == "unavailable" async def test_error_forecast(hass, mock_simple_nws): @@ -207,8 +248,7 @@ async def test_error_forecast(hass, mock_simple_nws): instance.update_forecast.side_effect = None - future_time = dt_util.utcnow() + timedelta(minutes=15) - async_fire_time_changed(hass, future_time) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=1)) await hass.async_block_till_done() assert instance.update_forecast.call_count == 2 @@ -236,8 +276,7 @@ async def test_error_forecast_hourly(hass, mock_simple_nws): instance.update_forecast_hourly.side_effect = None - future_time = dt_util.utcnow() + timedelta(minutes=15) - async_fire_time_changed(hass, future_time) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=1)) await hass.async_block_till_done() assert instance.update_forecast_hourly.call_count == 2 diff --git a/tests/components/openalpr_cloud/test_image_processing.py b/tests/components/openalpr_cloud/test_image_processing.py index c164f2f03a2..7f285d150b7 100644 --- a/tests/components/openalpr_cloud/test_image_processing.py +++ b/tests/components/openalpr_cloud/test_image_processing.py @@ -36,6 +36,7 @@ class TestOpenAlprCloudSetup: with assert_setup_component(1, ip.DOMAIN): setup_component(self.hass, ip.DOMAIN, config) + self.hass.block_till_done() assert self.hass.states.get("image_processing.openalpr_demo_camera") @@ -53,6 +54,7 @@ class TestOpenAlprCloudSetup: with assert_setup_component(1, ip.DOMAIN): setup_component(self.hass, ip.DOMAIN, config) + self.hass.block_till_done() assert self.hass.states.get("image_processing.test_local") @@ -108,6 +110,7 @@ class TestOpenAlprCloud: new_callable=PropertyMock(return_value=False), ): setup_component(self.hass, ip.DOMAIN, config) + self.hass.block_till_done() self.alpr_events = [] diff --git a/tests/components/openalpr_local/test_image_processing.py b/tests/components/openalpr_local/test_image_processing.py index 996d23184a2..d98c27490e8 100644 --- a/tests/components/openalpr_local/test_image_processing.py +++ b/tests/components/openalpr_local/test_image_processing.py @@ -46,6 +46,7 @@ class TestOpenAlprLocalSetup: with assert_setup_component(1, ip.DOMAIN): setup_component(self.hass, ip.DOMAIN, config) + self.hass.block_till_done() assert self.hass.states.get("image_processing.openalpr_demo_camera") @@ -62,6 +63,7 @@ class TestOpenAlprLocalSetup: with assert_setup_component(1, ip.DOMAIN): setup_component(self.hass, ip.DOMAIN, config) + self.hass.block_till_done() assert self.hass.states.get("image_processing.test_local") @@ -77,6 +79,7 @@ class TestOpenAlprLocalSetup: with assert_setup_component(0, ip.DOMAIN): setup_component(self.hass, ip.DOMAIN, config) + self.hass.block_till_done() class TestOpenAlprLocal: @@ -101,9 +104,10 @@ class TestOpenAlprLocal: new_callable=PropertyMock(return_value=False), ): setup_component(self.hass, ip.DOMAIN, config) + self.hass.block_till_done() state = self.hass.states.get("camera.demo_camera") - self.url = f"{self.hass.config.api.base_url}{state.attributes.get(ATTR_ENTITY_PICTURE)}" + self.url = f"{self.hass.config.internal_url}{state.attributes.get(ATTR_ENTITY_PICTURE)}" self.alpr_events = [] diff --git a/tests/components/openhardwaremonitor/test_sensor.py b/tests/components/openhardwaremonitor/test_sensor.py index 3fb93cb1375..db44216c535 100644 --- a/tests/components/openhardwaremonitor/test_sensor.py +++ b/tests/components/openhardwaremonitor/test_sensor.py @@ -35,6 +35,7 @@ class TestOpenHardwareMonitorSetup(unittest.TestCase): ) assert setup_component(self.hass, "sensor", self.config) + self.hass.block_till_done() entities = self.hass.states.async_entity_ids("sensor") assert len(entities) == 38 diff --git a/tests/components/openuv/test_config_flow.py b/tests/components/openuv/test_config_flow.py index 3aa67abdc4f..06eca531d2d 100644 --- a/tests/components/openuv/test_config_flow.py +++ b/tests/components/openuv/test_config_flow.py @@ -1,11 +1,10 @@ """Define tests for the OpenUV config flow.""" -from unittest.mock import patch - -from pyopenuv.errors import OpenUvError +from pyopenuv.errors import InvalidApiKeyError import pytest from homeassistant import data_entry_flow -from homeassistant.components.openuv import DOMAIN, config_flow +from homeassistant.components.openuv import DOMAIN +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( CONF_API_KEY, CONF_ELEVATION, @@ -13,21 +12,19 @@ from homeassistant.const import ( CONF_LONGITUDE, ) -from tests.common import MockConfigEntry, mock_coro +from tests.async_mock import patch +from tests.common import MockConfigEntry -@pytest.fixture -def uv_index_response(): - """Define a fixture for a successful /uv response.""" - return mock_coro() - - -@pytest.fixture -def mock_pyopenuv(uv_index_response): - """Mock the pyopenuv library.""" - with patch("homeassistant.components.openuv.config_flow.Client") as MockClient: - MockClient().uv_index.return_value = uv_index_response - yield MockClient +@pytest.fixture(autouse=True) +def mock_setup(): + """Prevent setup.""" + with patch( + "homeassistant.components.openuv.async_setup", return_value=True, + ), patch( + "homeassistant.components.openuv.async_setup_entry", return_value=True, + ): + yield async def test_duplicate_error(hass): @@ -39,16 +36,19 @@ async def test_duplicate_error(hass): CONF_LONGITUDE: -104.9812612, } - MockConfigEntry(domain=DOMAIN, data=conf).add_to_hass(hass) - flow = config_flow.OpenUvFlowHandler() - flow.hass = hass + MockConfigEntry( + domain=DOMAIN, unique_id="39.128712, -104.9812612", data=conf + ).add_to_hass(hass) - result = await flow.async_step_user(user_input=conf) - assert result["errors"] == {CONF_LATITUDE: "identifier_exists"} + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=conf + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" -@pytest.mark.parametrize("uv_index_response", [mock_coro(exception=OpenUvError)]) -async def test_invalid_api_key(hass, mock_pyopenuv): +async def test_invalid_api_key(hass): """Test that an invalid API key throws an error.""" conf = { CONF_API_KEY: "12345abcde", @@ -57,48 +57,17 @@ async def test_invalid_api_key(hass, mock_pyopenuv): CONF_LONGITUDE: -104.9812612, } - flow = config_flow.OpenUvFlowHandler() - flow.hass = hass - - result = await flow.async_step_user(user_input=conf) - assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} + with patch( + "pyopenuv.client.Client.uv_index", side_effect=InvalidApiKeyError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=conf + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} -async def test_show_form(hass): - """Test that the form is served with no input.""" - flow = config_flow.OpenUvFlowHandler() - flow.hass = hass - - result = await flow.async_step_user(user_input=None) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - - -async def test_step_import(hass, mock_pyopenuv): - """Test that the import step works.""" - conf = { - CONF_API_KEY: "12345abcde", - CONF_ELEVATION: 59.1234, - CONF_LATITUDE: 39.128712, - CONF_LONGITUDE: -104.9812612, - } - - flow = config_flow.OpenUvFlowHandler() - flow.hass = hass - - result = await flow.async_step_import(import_config=conf) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "39.128712, -104.9812612" - assert result["data"] == { - CONF_API_KEY: "12345abcde", - CONF_ELEVATION: 59.1234, - CONF_LATITUDE: 39.128712, - CONF_LONGITUDE: -104.9812612, - } - - -async def test_step_user(hass, mock_pyopenuv): +async def test_step_user(hass): """Test that the user step works.""" conf = { CONF_API_KEY: "12345abcde", @@ -107,15 +76,23 @@ async def test_step_user(hass, mock_pyopenuv): CONF_LONGITUDE: -104.9812612, } - flow = config_flow.OpenUvFlowHandler() - flow.hass = hass + with patch( + "homeassistant.components.airvisual.async_setup_entry", return_value=True + ), patch("pyopenuv.client.Client.uv_index"): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" - result = await flow.async_step_user(user_input=conf) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "39.128712, -104.9812612" - assert result["data"] == { - CONF_API_KEY: "12345abcde", - CONF_ELEVATION: 59.1234, - CONF_LATITUDE: 39.128712, - CONF_LONGITUDE: -104.9812612, - } + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=conf + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "39.128712, -104.9812612" + assert result["data"] == { + CONF_API_KEY: "12345abcde", + CONF_ELEVATION: 59.1234, + CONF_LATITUDE: 39.128712, + CONF_LONGITUDE: -104.9812612, + } diff --git a/tests/components/ozw/common.py b/tests/components/ozw/common.py index a71103fdf85..7a78d11a445 100644 --- a/tests/components/ozw/common.py +++ b/tests/components/ozw/common.py @@ -32,7 +32,10 @@ async def setup_ozw(hass, entry=None, fixture=None): if fixture is not None: for line in fixture.split("\n"): - topic, payload = line.strip().split(",", 1) + line = line.strip() + if not line: + continue + topic, payload = line.split(",", 1) receive_message(Mock(topic=topic, payload=payload)) await hass.async_block_till_done() diff --git a/tests/components/ozw/conftest.py b/tests/components/ozw/conftest.py index b984172d355..5f29435760c 100644 --- a/tests/components/ozw/conftest.py +++ b/tests/components/ozw/conftest.py @@ -15,12 +15,30 @@ def generic_data_fixture(): return load_fixture("ozw/generic_network_dump.csv") +@pytest.fixture(name="fan_data", scope="session") +def fan_data_fixture(): + """Load fan MQTT data and return it.""" + return load_fixture("ozw/fan_network_dump.csv") + + @pytest.fixture(name="light_data", scope="session") def light_data_fixture(): """Load light dimmer MQTT data and return it.""" return load_fixture("ozw/light_network_dump.csv") +@pytest.fixture(name="climate_data", scope="session") +def climate_data_fixture(): + """Load climate MQTT data and return it.""" + return load_fixture("ozw/climate_network_dump.csv") + + +@pytest.fixture(name="lock_data", scope="session") +def lock_data_fixture(): + """Load lock MQTT data and return it.""" + return load_fixture("ozw/lock_network_dump.csv") + + @pytest.fixture(name="sent_messages") def sent_messages_fixture(): """Fixture to capture sent messages.""" @@ -35,6 +53,17 @@ def sent_messages_fixture(): yield sent_messages +@pytest.fixture(name="fan_msg") +async def fan_msg_fixture(hass): + """Return a mock MQTT msg with a fan actuator message.""" + fan_json = json.loads( + await hass.async_add_executor_job(load_fixture, "ozw/fan.json") + ) + message = MQTTMessage(topic=fan_json["topic"], payload=fan_json["payload"]) + message.encode() + return message + + @pytest.fixture(name="light_msg") async def light_msg_fixture(hass): """Return a mock MQTT msg with a light actuator message.""" @@ -88,3 +117,25 @@ async def binary_sensor_alt_msg_fixture(hass): message = MQTTMessage(topic=sensor_json["topic"], payload=sensor_json["payload"]) message.encode() return message + + +@pytest.fixture(name="climate_msg") +async def climate_msg_fixture(hass): + """Return a mock MQTT msg with a climate mode change message.""" + sensor_json = json.loads( + await hass.async_add_executor_job(load_fixture, "ozw/climate.json") + ) + message = MQTTMessage(topic=sensor_json["topic"], payload=sensor_json["payload"]) + message.encode() + return message + + +@pytest.fixture(name="lock_msg") +async def lock_msg_fixture(hass): + """Return a mock MQTT msg with a lock actuator message.""" + lock_json = json.loads( + await hass.async_add_executor_job(load_fixture, "ozw/lock.json") + ) + message = MQTTMessage(topic=lock_json["topic"], payload=lock_json["payload"]) + message.encode() + return message diff --git a/tests/components/ozw/test_climate.py b/tests/components/ozw/test_climate.py new file mode 100644 index 00000000000..c0f4e362735 --- /dev/null +++ b/tests/components/ozw/test_climate.py @@ -0,0 +1,220 @@ +"""Test Z-Wave Multi-setpoint Climate entities.""" +from homeassistant.components.climate import ATTR_TEMPERATURE +from homeassistant.components.climate.const import ( + ATTR_CURRENT_TEMPERATURE, + ATTR_FAN_MODE, + ATTR_FAN_MODES, + ATTR_HVAC_ACTION, + ATTR_HVAC_MODES, + ATTR_PRESET_MODES, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + CURRENT_HVAC_IDLE, + HVAC_MODE_AUTO, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, +) + +from .common import setup_ozw + + +async def test_climate(hass, climate_data, sent_messages, climate_msg, caplog): + """Test setting up config entry.""" + receive_message = await setup_ozw(hass, fixture=climate_data) + + # Test multi-setpoint thermostat (node 7 in dump) + # mode is heat, this should be single setpoint + state = hass.states.get("climate.ct32_thermostat_mode") + assert state is not None + assert state.state == HVAC_MODE_HEAT + assert state.attributes[ATTR_HVAC_MODES] == [ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_AUTO, + ] + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 23.1 + assert state.attributes[ATTR_TEMPERATURE] == 21.1 + assert state.attributes.get(ATTR_TARGET_TEMP_LOW) is None + assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) is None + assert state.attributes[ATTR_FAN_MODE] == "Auto Low" + assert state.attributes[ATTR_FAN_MODES] == ["Auto Low", "On Low"] + + # Test set target temperature + await hass.services.async_call( + "climate", + "set_temperature", + {"entity_id": "climate.ct32_thermostat_mode", "temperature": 26.1}, + blocking=True, + ) + assert len(sent_messages) == 1 + msg = sent_messages[-1] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + # Celsius is converted to Fahrenheit here! + assert round(msg["payload"]["Value"], 2) == 78.98 + assert msg["payload"]["ValueIDKey"] == 281475099443218 + + # Test set mode + await hass.services.async_call( + "climate", + "set_hvac_mode", + {"entity_id": "climate.ct32_thermostat_mode", "hvac_mode": HVAC_MODE_AUTO}, + blocking=True, + ) + assert len(sent_messages) == 2 + msg = sent_messages[-1] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == {"Value": 3, "ValueIDKey": 122683412} + + # Test set missing mode + await hass.services.async_call( + "climate", + "set_hvac_mode", + {"entity_id": "climate.ct32_thermostat_mode", "hvac_mode": "fan_only"}, + blocking=True, + ) + assert len(sent_messages) == 2 + assert "Received an invalid hvac mode: fan_only" in caplog.text + + # Test set fan mode + await hass.services.async_call( + "climate", + "set_fan_mode", + {"entity_id": "climate.ct32_thermostat_mode", "fan_mode": "On Low"}, + blocking=True, + ) + assert len(sent_messages) == 3 + msg = sent_messages[-1] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == {"Value": 1, "ValueIDKey": 122748948} + + # Test set invalid fan mode + await hass.services.async_call( + "climate", + "set_fan_mode", + {"entity_id": "climate.ct32_thermostat_mode", "fan_mode": "invalid fan mode"}, + blocking=True, + ) + assert len(sent_messages) == 3 + assert "Received an invalid fan mode: invalid fan mode" in caplog.text + + # Test incoming mode change to auto, + # resulting in multiple setpoints + receive_message(climate_msg) + await hass.async_block_till_done() + state = hass.states.get("climate.ct32_thermostat_mode") + assert state is not None + assert state.state == HVAC_MODE_AUTO + assert state.attributes.get(ATTR_TEMPERATURE) is None + assert state.attributes[ATTR_TARGET_TEMP_LOW] == 21.1 + assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 25.6 + + # Test setting high/low temp on multiple setpoints + await hass.services.async_call( + "climate", + "set_temperature", + { + "entity_id": "climate.ct32_thermostat_mode", + "target_temp_low": 20, + "target_temp_high": 25, + }, + blocking=True, + ) + assert len(sent_messages) == 5 # 2 messages ! + msg = sent_messages[-2] # low setpoint + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert round(msg["payload"]["Value"], 2) == 68.0 + assert msg["payload"]["ValueIDKey"] == 281475099443218 + msg = sent_messages[-1] # high setpoint + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert round(msg["payload"]["Value"], 2) == 77.0 + assert msg["payload"]["ValueIDKey"] == 562950076153874 + + # Test basic/single-setpoint thermostat (node 16 in dump) + state = hass.states.get("climate.komforthaus_spirit_z_wave_plus_mode") + assert state is not None + assert state.state == HVAC_MODE_HEAT + assert state.attributes[ATTR_HVAC_MODES] == [ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + ] + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 17.3 + assert round(state.attributes[ATTR_TEMPERATURE], 0) == 19 + assert state.attributes.get(ATTR_TARGET_TEMP_LOW) is None + assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) is None + assert state.attributes[ATTR_PRESET_MODES] == [ + "none", + "Heat Eco", + "Full Power", + "Manufacturer Specific", + ] + + # Test set target temperature + await hass.services.async_call( + "climate", + "set_temperature", + { + "entity_id": "climate.komforthaus_spirit_z_wave_plus_mode", + "temperature": 28.0, + }, + blocking=True, + ) + assert len(sent_messages) == 6 + msg = sent_messages[-1] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == { + "Value": 28.0, + "ValueIDKey": 281475250438162, + } + + # Test set preset mode + await hass.services.async_call( + "climate", + "set_preset_mode", + { + "entity_id": "climate.komforthaus_spirit_z_wave_plus_mode", + "preset_mode": "Heat Eco", + }, + blocking=True, + ) + assert len(sent_messages) == 7 + msg = sent_messages[-1] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == { + "Value": 11, + "ValueIDKey": 273678356, + } + + # Test set preset mode None + # This preset should set and return to current hvac mode + await hass.services.async_call( + "climate", + "set_preset_mode", + { + "entity_id": "climate.komforthaus_spirit_z_wave_plus_mode", + "preset_mode": "none", + }, + blocking=True, + ) + assert len(sent_messages) == 8 + msg = sent_messages[-1] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == { + "Value": 1, + "ValueIDKey": 273678356, + } + + # Test set invalid preset mode + await hass.services.async_call( + "climate", + "set_preset_mode", + { + "entity_id": "climate.komforthaus_spirit_z_wave_plus_mode", + "preset_mode": "invalid preset mode", + }, + blocking=True, + ) + assert len(sent_messages) == 8 + assert "Received an invalid preset mode: invalid preset mode" in caplog.text diff --git a/tests/components/ozw/test_fan.py b/tests/components/ozw/test_fan.py new file mode 100644 index 00000000000..cca1143c44b --- /dev/null +++ b/tests/components/ozw/test_fan.py @@ -0,0 +1,135 @@ +"""Test Z-Wave Lights.""" +from homeassistant.components.ozw.fan import SPEED_TO_VALUE + +from .common import setup_ozw + + +async def test_fan(hass, fan_data, fan_msg, sent_messages, caplog): + """Test fan.""" + receive_message = await setup_ozw(hass, fixture=fan_data) + + # Test loaded + state = hass.states.get("fan.in_wall_smart_fan_control_level") + assert state is not None + assert state.state == "on" + + # Test turning off + await hass.services.async_call( + "fan", + "turn_off", + {"entity_id": "fan.in_wall_smart_fan_control_level"}, + blocking=True, + ) + + assert len(sent_messages) == 1 + msg = sent_messages[-1] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == {"Value": 0, "ValueIDKey": 172589073} + + # Feedback on state + fan_msg.decode() + fan_msg.payload["Value"] = 0 + fan_msg.encode() + receive_message(fan_msg) + await hass.async_block_till_done() + + state = hass.states.get("fan.in_wall_smart_fan_control_level") + assert state is not None + assert state.state == "off" + + # Test turning on + new_speed = "medium" + await hass.services.async_call( + "fan", + "turn_on", + {"entity_id": "fan.in_wall_smart_fan_control_level", "speed": new_speed}, + blocking=True, + ) + + assert len(sent_messages) == 2 + msg = sent_messages[-1] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == { + "Value": SPEED_TO_VALUE[new_speed], + "ValueIDKey": 172589073, + } + + # Feedback on state + fan_msg.decode() + fan_msg.payload["Value"] = SPEED_TO_VALUE[new_speed] + fan_msg.encode() + receive_message(fan_msg) + await hass.async_block_till_done() + + state = hass.states.get("fan.in_wall_smart_fan_control_level") + assert state is not None + assert state.state == "on" + assert state.attributes["speed"] == new_speed + + # Test turn on without speed + await hass.services.async_call( + "fan", + "turn_on", + {"entity_id": "fan.in_wall_smart_fan_control_level"}, + blocking=True, + ) + + assert len(sent_messages) == 3 + msg = sent_messages[-1] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == { + "Value": 255, + "ValueIDKey": 172589073, + } + + # Feedback on state + fan_msg.decode() + fan_msg.payload["Value"] = SPEED_TO_VALUE[new_speed] + fan_msg.encode() + receive_message(fan_msg) + await hass.async_block_till_done() + + state = hass.states.get("fan.in_wall_smart_fan_control_level") + assert state is not None + assert state.state == "on" + assert state.attributes["speed"] == new_speed + + # Test set speed to off + new_speed = "off" + await hass.services.async_call( + "fan", + "set_speed", + {"entity_id": "fan.in_wall_smart_fan_control_level", "speed": new_speed}, + blocking=True, + ) + + assert len(sent_messages) == 4 + msg = sent_messages[-1] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == { + "Value": SPEED_TO_VALUE[new_speed], + "ValueIDKey": 172589073, + } + + # Feedback on state + fan_msg.decode() + fan_msg.payload["Value"] = SPEED_TO_VALUE[new_speed] + fan_msg.encode() + receive_message(fan_msg) + await hass.async_block_till_done() + + state = hass.states.get("fan.in_wall_smart_fan_control_level") + assert state is not None + assert state.state == "off" + + # Test invalid speed + new_speed = "invalid" + await hass.services.async_call( + "fan", + "set_speed", + {"entity_id": "fan.in_wall_smart_fan_control_level", "speed": new_speed}, + blocking=True, + ) + + assert len(sent_messages) == 4 + assert "Invalid speed received: invalid" in caplog.text diff --git a/tests/components/ozw/test_lock.py b/tests/components/ozw/test_lock.py new file mode 100644 index 00000000000..d36c3d4bbbf --- /dev/null +++ b/tests/components/ozw/test_lock.py @@ -0,0 +1,41 @@ +"""Test Z-Wave Locks.""" +from .common import setup_ozw + + +async def test_lock(hass, lock_data, sent_messages, lock_msg): + """Test lock.""" + receive_message = await setup_ozw(hass, fixture=lock_data) + + # Test loaded + state = hass.states.get("lock.danalock_v3_btze_locked") + assert state is not None + assert state.state == "unlocked" + + # Test locking + await hass.services.async_call( + "lock", "lock", {"entity_id": "lock.danalock_v3_btze_locked"}, blocking=True + ) + assert len(sent_messages) == 1 + msg = sent_messages[0] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == {"Value": True, "ValueIDKey": 173572112} + + # Feedback on state + lock_msg.decode() + lock_msg.payload["Value"] = True + lock_msg.encode() + receive_message(lock_msg) + await hass.async_block_till_done() + + state = hass.states.get("lock.danalock_v3_btze_locked") + assert state is not None + assert state.state == "locked" + + # Test unlocking + await hass.services.async_call( + "lock", "unlock", {"entity_id": "lock.danalock_v3_btze_locked"}, blocking=True + ) + assert len(sent_messages) == 2 + msg = sent_messages[1] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == {"Value": False, "ValueIDKey": 173572112} diff --git a/tests/components/panel_iframe/test_init.py b/tests/components/panel_iframe/test_init.py index d586c4c199e..b38f3c4b1fa 100644 --- a/tests/components/panel_iframe/test_init.py +++ b/tests/components/panel_iframe/test_init.py @@ -4,6 +4,7 @@ import unittest from homeassistant import setup from homeassistant.components import frontend +from tests.async_mock import patch from tests.common import get_test_home_assistant @@ -26,38 +27,42 @@ class TestPanelIframe(unittest.TestCase): ] for conf in to_try: - assert not setup.setup_component( - self.hass, "panel_iframe", {"panel_iframe": conf} - ) + with patch( + "homeassistant.components.http.start_http_server_and_save_config" + ): + assert not setup.setup_component( + self.hass, "panel_iframe", {"panel_iframe": conf} + ) def test_correct_config(self): """Test correct config.""" - assert setup.setup_component( - self.hass, - "panel_iframe", - { - "panel_iframe": { - "router": { - "icon": "mdi:network-wireless", - "title": "Router", - "url": "http://192.168.1.1", - "require_admin": True, - }, - "weather": { - "icon": "mdi:weather", - "title": "Weather", - "url": "https://www.wunderground.com/us/ca/san-diego", - "require_admin": True, - }, - "api": {"icon": "mdi:weather", "title": "Api", "url": "/api"}, - "ftp": { - "icon": "mdi:weather", - "title": "FTP", - "url": "ftp://some/ftp", - }, - } - }, - ) + with patch("homeassistant.components.http.start_http_server_and_save_config"): + assert setup.setup_component( + self.hass, + "panel_iframe", + { + "panel_iframe": { + "router": { + "icon": "mdi:network-wireless", + "title": "Router", + "url": "http://192.168.1.1", + "require_admin": True, + }, + "weather": { + "icon": "mdi:weather", + "title": "Weather", + "url": "https://www.wunderground.com/us/ca/san-diego", + "require_admin": True, + }, + "api": {"icon": "mdi:weather", "title": "Api", "url": "/api"}, + "ftp": { + "icon": "mdi:weather", + "title": "FTP", + "url": "ftp://some/ftp", + }, + } + }, + ) panels = self.hass.data[frontend.DATA_PANELS] diff --git a/tests/components/pilight/test_sensor.py b/tests/components/pilight/test_sensor.py index 4bc1e80b07a..54c72675bc7 100644 --- a/tests/components/pilight/test_sensor.py +++ b/tests/components/pilight/test_sensor.py @@ -48,6 +48,7 @@ def test_sensor_value_from_code(): } }, ) + HASS.block_till_done() state = HASS.states.get("sensor.test") assert state.state == "unknown" @@ -77,6 +78,7 @@ def test_disregard_wrong_payload(): } }, ) + HASS.block_till_done() # Try set value from data with incorrect payload fire_pilight_message( @@ -120,6 +122,7 @@ def test_variable_missing(caplog): } }, ) + HASS.block_till_done() # Create code without sensor variable fire_pilight_message( diff --git a/tests/components/plex/const.py b/tests/components/plex/const.py index 0f91a9da23f..bfc4b8ef78e 100644 --- a/tests/components/plex/const.py +++ b/tests/components/plex/const.py @@ -2,6 +2,7 @@ from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.plex import const from homeassistant.const import ( + CONF_CLIENT_ID, CONF_HOST, CONF_PORT, CONF_TOKEN, @@ -35,7 +36,7 @@ MOCK_TOKEN = "secret_token" DEFAULT_DATA = { const.CONF_SERVER: MOCK_SERVERS[0][const.CONF_SERVER], const.PLEX_SERVER_CONFIG: { - const.CONF_CLIENT_IDENTIFIER: "00000000-0000-0000-0000-000000000000", + CONF_CLIENT_ID: "00000000-0000-0000-0000-000000000000", CONF_TOKEN: MOCK_TOKEN, CONF_URL: f"https://{MOCK_SERVERS[0][CONF_HOST]}:{MOCK_SERVERS[0][CONF_PORT]}", CONF_VERIFY_SSL: True, diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py index ec1b490ddf5..2d69801e797 100644 --- a/tests/components/plex/mock_classes.py +++ b/tests/components/plex/mock_classes.py @@ -69,6 +69,10 @@ class MockPlexAccount: """Mock the PlexAccount resources listing method.""" return self._resources + def sonos_speaker_by_id(self, machine_identifier): + """Mock the PlexAccount Sonos lookup method.""" + return MockPlexSonosClient(machine_identifier) + class MockPlexSystemAccount: """Mock a PlexSystemAccount instance.""" @@ -152,6 +156,19 @@ class MockPlexServer: """Mock version of PlexServer.""" return "1.0" + @property + def library(self): + """Mock library object of PlexServer.""" + return MockPlexLibrary() + + def playlist(self, playlist): + """Mock the playlist lookup method.""" + return MockPlexMediaItem(playlist, mediatype="playlist") + + def fetchItem(self, item): + """Mock the fetchItem method.""" + return MockPlexMediaItem("Item Name") + class MockPlexClient: """Mock a PlexClient instance.""" @@ -186,7 +203,7 @@ class MockPlexClient: @property def protocolCapabilities(self): """Mock the protocolCapabilities attribute.""" - return ["player"] + return ["playback"] @property def state(self): @@ -203,6 +220,10 @@ class MockPlexClient: """Mock the version attribute.""" return "1.0" + def playMedia(self, item): + """Mock the playMedia method.""" + pass + class MockPlexSession: """Mock a PlexServer.sessions() instance.""" @@ -259,9 +280,90 @@ class MockPlexSession: return 2020 +class MockPlexLibrary: + """Mock a Plex Library instance.""" + + def __init__(self): + """Initialize the object.""" + + def section(self, library_name): + """Mock the LibrarySection lookup.""" + return MockPlexLibrarySection(library_name) + + class MockPlexLibrarySection: """Mock a Plex LibrarySection instance.""" def __init__(self, library="Movies"): """Initialize the object.""" self.title = library + + def get(self, query): + """Mock the get lookup method.""" + if self.title == "Music": + return MockPlexArtist(query) + return MockPlexMediaItem(query) + + +class MockPlexMediaItem: + """Mock a Plex Media instance.""" + + def __init__(self, title, mediatype="video"): + """Initialize the object.""" + self.title = str(title) + self.type = mediatype + + def album(self, album): + """Mock the album lookup method.""" + return MockPlexMediaItem(album, mediatype="album") + + def track(self, track): + """Mock the track lookup method.""" + return MockPlexMediaTrack() + + def tracks(self): + """Mock the tracks lookup method.""" + for index in range(1, 10): + yield MockPlexMediaTrack(index) + + def episode(self, episode): + """Mock the episode lookup method.""" + return MockPlexMediaItem(episode, mediatype="episode") + + def season(self, season): + """Mock the season lookup method.""" + return MockPlexMediaItem(season, mediatype="season") + + +class MockPlexArtist(MockPlexMediaItem): + """Mock a Plex Artist instance.""" + + def __init__(self, artist): + """Initialize the object.""" + super().__init__(artist) + self.type = "artist" + + def get(self, track): + """Mock the track lookup method.""" + return MockPlexMediaTrack() + + +class MockPlexMediaTrack(MockPlexMediaItem): + """Mock a Plex Track instance.""" + + def __init__(self, index=1): + """Initialize the object.""" + super().__init__(f"Track {index}", "track") + self.index = index + + +class MockPlexSonosClient: + """Mock a PlexSonosClient instance.""" + + def __init__(self, machine_identifier): + """Initialize the object.""" + self.machineIdentifier = machine_identifier + + def playMedia(self, item): + """Mock the playMedia method.""" + pass diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index b43448b4d97..c51c0670525 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -37,7 +37,7 @@ from homeassistant.const import ( from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import DEFAULT_DATA, DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN -from .mock_classes import MockGDM, MockPlexAccount, MockPlexServer +from .mock_classes import MockGDM, MockPlexAccount, MockPlexServer, MockResource from tests.async_mock import patch from tests.common import MockConfigEntry @@ -75,45 +75,42 @@ async def test_bad_credentials(hass): assert result["errors"][CONF_TOKEN] == "faulty_credentials" -async def test_import_success(hass): - """Test a successful configuration import.""" - - mock_plex_server = MockPlexServer() - - with patch("plexapi.server.PlexServer", return_value=mock_plex_server): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": "import"}, - data={ - CONF_TOKEN: MOCK_TOKEN, - CONF_URL: f"https://{MOCK_SERVERS[0][CONF_HOST]}:{MOCK_SERVERS[0][CONF_PORT]}", - }, - ) - - assert result["type"] == "create_entry" - assert result["title"] == mock_plex_server.friendlyName - assert result["data"][CONF_SERVER] == mock_plex_server.friendlyName - assert result["data"][CONF_SERVER_IDENTIFIER] == mock_plex_server.machineIdentifier - assert result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server._baseurl - assert result["data"][PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN - - -async def test_import_bad_hostname(hass): +async def test_bad_hostname(hass): """Test when an invalid address is provided.""" + mock_plex_account = MockPlexAccount() + + await async_process_ha_core_config( + hass, {"internal_url": "http://example.local:8123"}, + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" with patch( - "plexapi.server.PlexServer", side_effect=requests.exceptions.ConnectionError + "plexapi.myplex.MyPlexAccount", return_value=mock_plex_account + ), patch.object( + MockResource, "connect", side_effect=requests.exceptions.ConnectionError + ), patch( + "plexauth.PlexAuth.initiate_auth" + ), patch( + "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": "import"}, - data={ - CONF_TOKEN: MOCK_TOKEN, - CONF_URL: f"http://{MOCK_SERVERS[0][CONF_HOST]}:{MOCK_SERVERS[0][CONF_PORT]}", - }, + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} ) - assert result["type"] == "abort" - assert result["reason"] == "non-interactive" + assert result["type"] == "external" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external_done" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"][CONF_HOST] == "not_found" async def test_unknown_exception(hass): @@ -311,35 +308,6 @@ async def test_adding_last_unconfigured_server(hass): assert result["data"][PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN -async def test_already_configured(hass): - """Test a duplicated successful flow.""" - - mock_plex_server = MockPlexServer() - - MockConfigEntry( - domain=DOMAIN, - data={ - CONF_SERVER: MOCK_SERVERS[0][CONF_SERVER], - CONF_SERVER_IDENTIFIER: MOCK_SERVERS[0][CONF_SERVER_IDENTIFIER], - }, - unique_id=MOCK_SERVERS[0][CONF_SERVER_IDENTIFIER], - ).add_to_hass(hass) - - with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "plexauth.PlexAuth.initiate_auth" - ), patch("plexauth.PlexAuth.token", return_value=MOCK_TOKEN): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": "import"}, - data={ - CONF_TOKEN: MOCK_TOKEN, - CONF_URL: f"http://{MOCK_SERVERS[0][CONF_HOST]}:{MOCK_SERVERS[0][CONF_PORT]}", - }, - ) - assert result["type"] == "abort" - assert result["reason"] == "already_configured" - - async def test_all_available_servers_configured(hass): """Test when all available servers are already configured.""" @@ -542,21 +510,6 @@ async def test_callback_view(hass, aiohttp_client): assert resp.status == 200 -async def test_multiple_servers_with_import(hass): - """Test importing a config with multiple servers available.""" - - with patch( - "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2) - ), patch("plexauth.PlexAuth.initiate_auth"), patch( - "plexauth.PlexAuth.token", return_value=MOCK_TOKEN - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "import"}, data={CONF_TOKEN: MOCK_TOKEN}, - ) - assert result["type"] == "abort" - assert result["reason"] == "non-interactive" - - async def test_manual_config(hass): """Test creating via manual configuration.""" await async_process_ha_core_config( @@ -576,6 +529,8 @@ async def test_manual_config(hass): config_flow.DOMAIN, context={"source": "user"} ) + assert result["type"] == "form" + assert result["step_id"] == "user" assert result["data_schema"] is None hass.config_entries.flow.async_abort(result["flow_id"]) @@ -588,9 +543,10 @@ async def test_manual_config(hass): assert result["type"] == "form" assert result["step_id"] == "user_advanced" - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"setup_method": AUTOMATIC_SETUP_STRING} - ) + with patch("plexauth.PlexAuth.initiate_auth"): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"setup_method": AUTOMATIC_SETUP_STRING} + ) assert result["type"] == "external" hass.config_entries.flow.async_abort(result["flow_id"]) diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index e34476f1813..461efe9d320 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -6,7 +6,6 @@ import ssl import plexapi import requests -from homeassistant.components.media_player import DOMAIN as MP_DOMAIN import homeassistant.components.plex.const as const from homeassistant.config_entries import ( ENTRY_STATE_LOADED, @@ -14,60 +13,16 @@ from homeassistant.config_entries import ( ENTRY_STATE_SETUP_ERROR, ENTRY_STATE_SETUP_RETRY, ) -from homeassistant.const import ( - CONF_HOST, - CONF_PORT, - CONF_SSL, - CONF_TOKEN, - CONF_URL, - CONF_VERIFY_SSL, -) +from homeassistant.const import CONF_URL, CONF_VERIFY_SSL from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from .const import DEFAULT_DATA, DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN +from .const import DEFAULT_DATA, DEFAULT_OPTIONS from .mock_classes import MockPlexAccount, MockPlexServer from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_time_changed - -async def test_setup_with_config(hass): - """Test setup component with config.""" - config = { - const.DOMAIN: { - CONF_HOST: MOCK_SERVERS[0][CONF_HOST], - CONF_PORT: MOCK_SERVERS[0][CONF_PORT], - CONF_TOKEN: MOCK_TOKEN, - CONF_SSL: True, - CONF_VERIFY_SSL: True, - MP_DOMAIN: { - const.CONF_IGNORE_NEW_SHARED_USERS: False, - const.CONF_USE_EPISODE_ART: False, - }, - }, - } - - mock_plex_server = MockPlexServer() - - with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ) as mock_listen: - assert await async_setup_component(hass, const.DOMAIN, config) is True - await hass.async_block_till_done() - - assert mock_listen.called - assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 - entry = hass.config_entries.async_entries(const.DOMAIN)[0] - assert entry.state == ENTRY_STATE_LOADED - - server_id = mock_plex_server.machineIdentifier - loaded_server = hass.data[const.DOMAIN][const.SERVERS][server_id] - - assert loaded_server.plex_server == mock_plex_server - - # class TestClockedPlex(ClockedTestCase): # """Create clock-controlled tests.async_mock class.""" diff --git a/tests/components/plex/test_playback.py b/tests/components/plex/test_playback.py new file mode 100644 index 00000000000..7a90d8dfad8 --- /dev/null +++ b/tests/components/plex/test_playback.py @@ -0,0 +1,127 @@ +"""Tests for Plex player playback methods/services.""" +from homeassistant.components.media_player.const import ( + ATTR_MEDIA_CONTENT_ID, + ATTR_MEDIA_CONTENT_TYPE, + MEDIA_TYPE_MUSIC, +) +from homeassistant.components.plex.const import DOMAIN, SERVERS, SERVICE_PLAY_ON_SONOS +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.exceptions import HomeAssistantError + +from .const import DEFAULT_DATA, DEFAULT_OPTIONS +from .mock_classes import MockPlexAccount, MockPlexServer + +from tests.async_mock import patch +from tests.common import MockConfigEntry + + +async def test_sonos_playback(hass): + """Test playing media on a Sonos speaker.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data=DEFAULT_DATA, + options=DEFAULT_OPTIONS, + unique_id=DEFAULT_DATA["server_id"], + ) + + mock_plex_server = MockPlexServer(config_entry=entry) + + with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( + "homeassistant.components.plex.PlexWebsocket.listen" + ): + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + server_id = mock_plex_server.machineIdentifier + loaded_server = hass.data[DOMAIN][SERVERS][server_id] + + with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()): + # Access and cache PlexAccount + assert loaded_server.account + + # Test Sonos integration lookup failure + with patch.object( + hass.components.sonos, "get_coordinator_id", side_effect=HomeAssistantError + ): + assert await hass.services.async_call( + DOMAIN, + SERVICE_PLAY_ON_SONOS, + { + ATTR_ENTITY_ID: "media_player.sonos_kitchen", + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album"}', + }, + True, + ) + + # Test success with dict + with patch.object( + hass.components.sonos, + "get_coordinator_id", + return_value="media_player.sonos_kitchen", + ), patch("plexapi.playqueue.PlayQueue.create"): + assert await hass.services.async_call( + DOMAIN, + SERVICE_PLAY_ON_SONOS, + { + ATTR_ENTITY_ID: "media_player.sonos_kitchen", + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_ID: "2", + }, + True, + ) + + # Test success with plex_key + with patch.object( + hass.components.sonos, + "get_coordinator_id", + return_value="media_player.sonos_kitchen", + ), patch("plexapi.playqueue.PlayQueue.create"): + assert await hass.services.async_call( + DOMAIN, + SERVICE_PLAY_ON_SONOS, + { + ATTR_ENTITY_ID: "media_player.sonos_kitchen", + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album"}', + }, + True, + ) + + # Test invalid Plex server requested + with patch.object( + hass.components.sonos, + "get_coordinator_id", + return_value="media_player.sonos_kitchen", + ): + assert await hass.services.async_call( + DOMAIN, + SERVICE_PLAY_ON_SONOS, + { + ATTR_ENTITY_ID: "media_player.sonos_kitchen", + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_ID: '{"plex_server": "unknown_plex_server", "library_name": "Music", "artist_name": "Artist", "album_name": "Album"}', + }, + True, + ) + + # Test no speakers available + with patch.object( + loaded_server.account, "sonos_speaker_by_id", return_value=None + ), patch.object( + hass.components.sonos, + "get_coordinator_id", + return_value="media_player.sonos_kitchen", + ): + assert await hass.services.async_call( + DOMAIN, + SERVICE_PLAY_ON_SONOS, + { + ATTR_ENTITY_ID: "media_player.sonos_kitchen", + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album"}', + }, + True, + ) diff --git a/tests/components/plex/test_server.py b/tests/components/plex/test_server.py index 694fcc4885e..6831b045da6 100644 --- a/tests/components/plex/test_server.py +++ b/tests/components/plex/test_server.py @@ -1,7 +1,18 @@ """Tests for Plex server.""" import copy +from plexapi.exceptions import NotFound + from homeassistant.components.media_player import DOMAIN as MP_DOMAIN +from homeassistant.components.media_player.const import ( + ATTR_MEDIA_CONTENT_ID, + ATTR_MEDIA_CONTENT_TYPE, + MEDIA_TYPE_EPISODE, + MEDIA_TYPE_MUSIC, + MEDIA_TYPE_PLAYLIST, + MEDIA_TYPE_VIDEO, + SERVICE_PLAY_MEDIA, +) from homeassistant.components.plex.const import ( CONF_IGNORE_NEW_SHARED_USERS, CONF_IGNORE_PLEX_WEB_CLIENTS, @@ -10,10 +21,17 @@ from homeassistant.components.plex.const import ( PLEX_UPDATE_PLATFORMS_SIGNAL, SERVERS, ) +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import DEFAULT_DATA, DEFAULT_OPTIONS -from .mock_classes import MockPlexServer +from .mock_classes import ( + MockPlexArtist, + MockPlexLibrary, + MockPlexLibrarySection, + MockPlexMediaItem, + MockPlexServer, +) from tests.async_mock import patch from tests.common import MockConfigEntry @@ -244,3 +262,225 @@ async def test_ignore_plex_web_client(hass): media_players = hass.states.async_entity_ids("media_player") assert len(media_players) == int(sensor.state) - 1 + + +async def test_media_lookups(hass): + """Test media lookups to Plex server.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data=DEFAULT_DATA, + options=DEFAULT_OPTIONS, + unique_id=DEFAULT_DATA["server_id"], + ) + + mock_plex_server = MockPlexServer(config_entry=entry) + + with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( + "homeassistant.components.plex.PlexWebsocket.listen" + ): + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + server_id = mock_plex_server.machineIdentifier + loaded_server = hass.data[DOMAIN][SERVERS][server_id] + + # Plex Key searches + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() + media_player_id = hass.states.async_entity_ids("media_player")[0] + with patch("homeassistant.components.plex.PlexServer.create_playqueue"): + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player_id, + ATTR_MEDIA_CONTENT_TYPE: DOMAIN, + ATTR_MEDIA_CONTENT_ID: 123, + }, + True, + ) + with patch.object(MockPlexServer, "fetchItem", side_effect=NotFound): + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player_id, + ATTR_MEDIA_CONTENT_TYPE: DOMAIN, + ATTR_MEDIA_CONTENT_ID: 123, + }, + True, + ) + + # TV show searches + with patch.object(MockPlexLibrary, "section", side_effect=NotFound): + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_EPISODE, library_name="Not a Library", show_name="A TV Show" + ) + is None + ) + with patch.object(MockPlexLibrarySection, "get", side_effect=NotFound): + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_EPISODE, library_name="TV Shows", show_name="Not a TV Show" + ) + is None + ) + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_EPISODE, library_name="TV Shows", episode_name="An Episode" + ) + is None + ) + assert loaded_server.lookup_media( + MEDIA_TYPE_EPISODE, library_name="TV Shows", show_name="A TV Show" + ) + assert loaded_server.lookup_media( + MEDIA_TYPE_EPISODE, + library_name="TV Shows", + show_name="A TV Show", + season_number=2, + ) + assert loaded_server.lookup_media( + MEDIA_TYPE_EPISODE, + library_name="TV Shows", + show_name="A TV Show", + season_number=2, + episode_number=3, + ) + with patch.object(MockPlexMediaItem, "season", side_effect=NotFound): + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_EPISODE, + library_name="TV Shows", + show_name="A TV Show", + season_number=2, + ) + is None + ) + with patch.object(MockPlexMediaItem, "episode", side_effect=NotFound): + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_EPISODE, + library_name="TV Shows", + show_name="A TV Show", + season_number=2, + episode_number=1, + ) + is None + ) + + # Music searches + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_MUSIC, library_name="Music", album_name="An Album" + ) + is None + ) + assert loaded_server.lookup_media( + MEDIA_TYPE_MUSIC, library_name="Music", artist_name="An Artist" + ) + assert loaded_server.lookup_media( + MEDIA_TYPE_MUSIC, + library_name="Music", + artist_name="An Artist", + track_name="A Track", + ) + assert loaded_server.lookup_media( + MEDIA_TYPE_MUSIC, + library_name="Music", + artist_name="An Artist", + album_name="An Album", + ) + with patch.object(MockPlexLibrarySection, "get", side_effect=NotFound): + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_MUSIC, + library_name="Music", + artist_name="Not an Artist", + album_name="An Album", + ) + is None + ) + with patch.object(MockPlexArtist, "album", side_effect=NotFound): + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_MUSIC, + library_name="Music", + artist_name="An Artist", + album_name="Not an Album", + ) + is None + ) + with patch.object(MockPlexMediaItem, "track", side_effect=NotFound): + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_MUSIC, + library_name="Music", + artist_name="An Artist", + album_name="An Album", + track_name="Not a Track", + ) + is None + ) + with patch.object(MockPlexArtist, "get", side_effect=NotFound): + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_MUSIC, + library_name="Music", + artist_name="An Artist", + track_name="Not a Track", + ) + is None + ) + assert loaded_server.lookup_media( + MEDIA_TYPE_MUSIC, + library_name="Music", + artist_name="An Artist", + album_name="An Album", + track_number=3, + ) + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_MUSIC, + library_name="Music", + artist_name="An Artist", + album_name="An Album", + track_number=30, + ) + is None + ) + assert loaded_server.lookup_media( + MEDIA_TYPE_MUSIC, + library_name="Music", + artist_name="An Artist", + album_name="An Album", + track_name="A Track", + ) + + # Playlist searches + assert loaded_server.lookup_media(MEDIA_TYPE_PLAYLIST, playlist_name="A Playlist") + assert loaded_server.lookup_media(MEDIA_TYPE_PLAYLIST) is None + with patch.object(MockPlexServer, "playlist", side_effect=NotFound): + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_PLAYLIST, playlist_name="Not a Playlist" + ) + is None + ) + + # Movie searches + assert loaded_server.lookup_media(MEDIA_TYPE_VIDEO, video_name="A Movie") is None + assert loaded_server.lookup_media(MEDIA_TYPE_VIDEO, library_name="Movies") is None + assert loaded_server.lookup_media( + MEDIA_TYPE_VIDEO, library_name="Movies", video_name="A Movie" + ) + with patch.object(MockPlexLibrarySection, "get", side_effect=NotFound): + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_VIDEO, library_name="Movies", video_name="Not a Movie" + ) + is None + ) diff --git a/tests/components/plugwise/__init__.py b/tests/components/plugwise/__init__.py new file mode 100644 index 00000000000..1904b581ab1 --- /dev/null +++ b/tests/components/plugwise/__init__.py @@ -0,0 +1 @@ +"""Tests for the Plugwise integration.""" diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py new file mode 100644 index 00000000000..b70b658bd65 --- /dev/null +++ b/tests/components/plugwise/test_config_flow.py @@ -0,0 +1,83 @@ +"""Test the Plugwise config flow.""" +from Plugwise_Smile.Smile import Smile +import pytest + +from homeassistant import config_entries, setup +from homeassistant.components.plugwise.const import DOMAIN + +from tests.async_mock import patch + + +@pytest.fixture(name="mock_smile") +def mock_smile(): + """Create a Mock Smile for testing exceptions.""" + with patch("homeassistant.components.plugwise.config_flow.Smile",) as smile_mock: + smile_mock.InvalidAuthentication = Smile.InvalidAuthentication + smile_mock.ConnectionFailedError = Smile.ConnectionFailedError + smile_mock.return_value.connect.return_value = True + yield smile_mock.return_value + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.plugwise.config_flow.Smile.connect", + return_value=True, + ), patch( + "homeassistant.components.plugwise.async_setup", return_value=True, + ) as mock_setup, patch( + "homeassistant.components.plugwise.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "1.1.1.1", "password": "test-password"}, + ) + + assert result2["type"] == "create_entry" + assert result2["data"] == { + "host": "1.1.1.1", + "password": "test-password", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass, mock_smile): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_smile.connect.side_effect = Smile.InvalidAuthentication + mock_smile.gateway_id = "0a636a4fc1704ab4a24e4f7e37fb187a" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "1.1.1.1", "password": "test-password"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass, mock_smile): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_smile.connect.side_effect = Smile.ConnectionFailedError + mock_smile.gateway_id = "0a636a4fc1704ab4a24e4f7e37fb187a" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "1.1.1.1", "password": "test-password"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/point/test_config_flow.py b/tests/components/point/test_config_flow.py index 1714dd5a352..d1f8688da24 100644 --- a/tests/components/point/test_config_flow.py +++ b/tests/components/point/test_config_flow.py @@ -5,6 +5,7 @@ import pytest from homeassistant import data_entry_flow from homeassistant.components.point import DOMAIN, config_flow +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from tests.async_mock import AsyncMock, patch @@ -86,8 +87,8 @@ async def test_full_flow_implementation( result = await flow.async_step_code("123ABC") assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"]["refresh_args"] == { - "client_id": "id", - "client_secret": "secret", + CONF_CLIENT_ID: "id", + CONF_CLIENT_SECRET: "secret", } assert result["title"] == "john.doe@example.com" assert result["data"]["token"] == {"access_token": "boo"} diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index 55c09183e63..4539948cc5a 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -26,6 +26,7 @@ async def prometheus_client(loop, hass, hass_client): await setup.async_setup_component( hass, climate.DOMAIN, {"climate": [{"platform": "demo"}]} ) + await hass.async_block_till_done() sensor1 = DemoSensor( None, "Television Energy", 74, None, ENERGY_KILO_WATT_HOUR, None diff --git a/tests/components/ps4/test_init.py b/tests/components/ps4/test_init.py index 7f7eff33ebb..75a983cc97a 100644 --- a/tests/components/ps4/test_init.py +++ b/tests/components/ps4/test_init.py @@ -201,7 +201,7 @@ def test_games_reformat_to_dict(hass): ), patch("homeassistant.components.ps4.save_json", side_effect=MagicMock()), patch( "os.path.isfile", return_value=True ): - mock_games = ps4.load_games(hass) + mock_games = ps4.load_games(hass, MOCK_ENTRY_ID) # New format is a nested dict. assert isinstance(mock_games, dict) @@ -223,7 +223,7 @@ def test_load_games(hass): ), patch("homeassistant.components.ps4.save_json", side_effect=MagicMock()), patch( "os.path.isfile", return_value=True ): - mock_games = ps4.load_games(hass) + mock_games = ps4.load_games(hass, MOCK_ENTRY_ID) assert isinstance(mock_games, dict) @@ -242,7 +242,7 @@ def test_loading_games_returns_dict(hass): ), patch("homeassistant.components.ps4.save_json", side_effect=MagicMock()), patch( "os.path.isfile", return_value=True ): - mock_games = ps4.load_games(hass) + mock_games = ps4.load_games(hass, MOCK_ENTRY_ID) assert isinstance(mock_games, dict) assert not mock_games @@ -252,7 +252,7 @@ def test_loading_games_returns_dict(hass): ), patch("homeassistant.components.ps4.save_json", side_effect=MagicMock()), patch( "os.path.isfile", return_value=True ): - mock_games = ps4.load_games(hass) + mock_games = ps4.load_games(hass, MOCK_ENTRY_ID) assert isinstance(mock_games, dict) assert not mock_games @@ -260,7 +260,7 @@ def test_loading_games_returns_dict(hass): with patch("homeassistant.components.ps4.load_json", return_value=[]), patch( "homeassistant.components.ps4.save_json", side_effect=MagicMock() ), patch("os.path.isfile", return_value=True): - mock_games = ps4.load_games(hass) + mock_games = ps4.load_games(hass, MOCK_ENTRY_ID) assert isinstance(mock_games, dict) assert not mock_games diff --git a/tests/components/qld_bushfire/test_geo_location.py b/tests/components/qld_bushfire/test_geo_location.py index 15518afbc2d..743d967c953 100644 --- a/tests/components/qld_bushfire/test_geo_location.py +++ b/tests/components/qld_bushfire/test_geo_location.py @@ -98,6 +98,7 @@ async def test_setup(hass): ) with assert_setup_component(1, geo_location.DOMAIN): assert await async_setup_component(hass, geo_location.DOMAIN, CONFIG) + await hass.async_block_till_done() # Artificially trigger update. hass.bus.async_fire(EVENT_HOMEASSISTANT_START) # Collect events. @@ -201,6 +202,7 @@ async def test_setup_with_custom_location(hass): assert await async_setup_component( hass, geo_location.DOMAIN, CONFIG_WITH_CUSTOM_LOCATION ) + await hass.async_block_till_done() # Artificially trigger update. hass.bus.async_fire(EVENT_HOMEASSISTANT_START) diff --git a/tests/components/random/test_binary_sensor.py b/tests/components/random/test_binary_sensor.py index 975da102ca6..2f15243c71e 100644 --- a/tests/components/random/test_binary_sensor.py +++ b/tests/components/random/test_binary_sensor.py @@ -24,6 +24,7 @@ class TestRandomSensor(unittest.TestCase): config = {"binary_sensor": {"platform": "random", "name": "test"}} assert setup_component(self.hass, "binary_sensor", config) + self.hass.block_till_done() state = self.hass.states.get("binary_sensor.test") @@ -37,6 +38,7 @@ class TestRandomSensor(unittest.TestCase): config = {"binary_sensor": {"platform": "random", "name": "test"}} assert setup_component(self.hass, "binary_sensor", config) + self.hass.block_till_done() state = self.hass.states.get("binary_sensor.test") diff --git a/tests/components/random/test_sensor.py b/tests/components/random/test_sensor.py index a185c774ccd..657efc9a0cc 100644 --- a/tests/components/random/test_sensor.py +++ b/tests/components/random/test_sensor.py @@ -29,6 +29,7 @@ class TestRandomSensor(unittest.TestCase): } assert setup_component(self.hass, "sensor", config) + self.hass.block_till_done() state = self.hass.states.get("sensor.test") diff --git a/tests/components/reddit/test_sensor.py b/tests/components/reddit/test_sensor.py index c2620aa906d..33c7fae76b0 100644 --- a/tests/components/reddit/test_sensor.py +++ b/tests/components/reddit/test_sensor.py @@ -15,7 +15,13 @@ from homeassistant.components.reddit.sensor import ( CONF_SORT_BY, DOMAIN, ) -from homeassistant.const import CONF_MAXIMUM, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_MAXIMUM, + CONF_PASSWORD, + CONF_USERNAME, +) from homeassistant.setup import setup_component from tests.async_mock import patch @@ -24,8 +30,8 @@ from tests.common import get_test_home_assistant VALID_CONFIG = { "sensor": { "platform": DOMAIN, - "client_id": "test_client_id", - "client_secret": "test_client_secret", + CONF_CLIENT_ID: "test_client_id", + CONF_CLIENT_SECRET: "test_client_secret", CONF_USERNAME: "test_username", CONF_PASSWORD: "test_password", "subreddits": ["worldnews", "news"], @@ -35,8 +41,8 @@ VALID_CONFIG = { VALID_LIMITED_CONFIG = { "sensor": { "platform": DOMAIN, - "client_id": "test_client_id", - "client_secret": "test_client_secret", + CONF_CLIENT_ID: "test_client_id", + CONF_CLIENT_SECRET: "test_client_secret", CONF_USERNAME: "test_username", CONF_PASSWORD: "test_password", "subreddits": ["worldnews", "news"], @@ -48,8 +54,8 @@ VALID_LIMITED_CONFIG = { INVALID_SORT_BY_CONFIG = { "sensor": { "platform": DOMAIN, - "client_id": "test_client_id", - "client_secret": "test_client_secret", + CONF_CLIENT_ID: "test_client_id", + CONF_CLIENT_SECRET: "test_client_secret", CONF_USERNAME: "test_username", CONF_PASSWORD: "test_password", "subreddits": ["worldnews", "news"], @@ -160,6 +166,7 @@ class TestRedditSetup(unittest.TestCase): def test_setup_with_valid_config(self): """Test the platform setup with Reddit configuration.""" setup_component(self.hass, "sensor", VALID_CONFIG) + self.hass.block_till_done() state = self.hass.states.get("sensor.reddit_worldnews") assert int(state.state) == MOCK_RESULTS_LENGTH diff --git a/tests/components/rest/test_binary_sensor.py b/tests/components/rest/test_binary_sensor.py index 65ae36c3843..762c1705d77 100644 --- a/tests/components/rest/test_binary_sensor.py +++ b/tests/components/rest/test_binary_sensor.py @@ -88,6 +88,7 @@ class TestRestBinarySensorSetup(unittest.TestCase): "binary_sensor", {"binary_sensor": {"platform": "rest", "resource": "http://localhost"}}, ) + self.hass.block_till_done() assert 1 == mock_req.call_count @requests_mock.Mocker() @@ -113,6 +114,7 @@ class TestRestBinarySensorSetup(unittest.TestCase): } }, ) + self.hass.block_till_done() assert 1 == mock_req.call_count @requests_mock.Mocker() @@ -139,6 +141,7 @@ class TestRestBinarySensorSetup(unittest.TestCase): } }, ) + self.hass.block_till_done() assert 1 == mock_req.call_count diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index c3ed8cea1b9..77d88f083e4 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -76,6 +76,7 @@ class TestRestSensorSetup(unittest.TestCase): "sensor", {"sensor": {"platform": "rest", "resource": "http://localhost"}}, ) + self.hass.block_till_done() assert 2 == mock_req.call_count @requests_mock.Mocker() @@ -93,6 +94,7 @@ class TestRestSensorSetup(unittest.TestCase): } }, ) + self.hass.block_till_done() assert mock_req.call_count == 2 @requests_mock.Mocker() @@ -111,6 +113,7 @@ class TestRestSensorSetup(unittest.TestCase): } }, ) + self.hass.block_till_done() @requests_mock.Mocker() def test_setup_get(self, mock_req): @@ -137,6 +140,7 @@ class TestRestSensorSetup(unittest.TestCase): } }, ) + self.hass.block_till_done() assert 2 == mock_req.call_count @requests_mock.Mocker() @@ -165,6 +169,7 @@ class TestRestSensorSetup(unittest.TestCase): } }, ) + self.hass.block_till_done() assert 2 == mock_req.call_count @requests_mock.Mocker() @@ -192,6 +197,7 @@ class TestRestSensorSetup(unittest.TestCase): } }, ) + self.hass.block_till_done() assert 2 == mock_req.call_count diff --git a/tests/components/rmvtransport/test_sensor.py b/tests/components/rmvtransport/test_sensor.py index ece4142f8c7..419eaafc5eb 100644 --- a/tests/components/rmvtransport/test_sensor.py +++ b/tests/components/rmvtransport/test_sensor.py @@ -165,6 +165,7 @@ async def test_rmvtransport_min_config(hass): "RMVtransport.RMVtransport.get_departures", return_value=get_departures_mock(), ): assert await async_setup_component(hass, "sensor", VALID_CONFIG_MINIMAL) is True + await hass.async_block_till_done() state = hass.states.get("sensor.frankfurt_main_hauptbahnhof") assert state.state == "7" @@ -184,6 +185,7 @@ async def test_rmvtransport_name_config(hass): "RMVtransport.RMVtransport.get_departures", return_value=get_departures_mock(), ): assert await async_setup_component(hass, "sensor", VALID_CONFIG_NAME) + await hass.async_block_till_done() state = hass.states.get("sensor.my_station") assert state.attributes["friendly_name"] == "My Station" @@ -195,6 +197,7 @@ async def test_rmvtransport_misc_config(hass): "RMVtransport.RMVtransport.get_departures", return_value=get_departures_mock(), ): assert await async_setup_component(hass, "sensor", VALID_CONFIG_MISC) + await hass.async_block_till_done() state = hass.states.get("sensor.frankfurt_main_hauptbahnhof") assert state.attributes["friendly_name"] == "Frankfurt (Main) Hauptbahnhof" @@ -207,6 +210,7 @@ async def test_rmvtransport_dest_config(hass): "RMVtransport.RMVtransport.get_departures", return_value=get_departures_mock(), ): assert await async_setup_component(hass, "sensor", VALID_CONFIG_DEST) + await hass.async_block_till_done() state = hass.states.get("sensor.frankfurt_main_hauptbahnhof") assert state.state == "11" @@ -225,6 +229,7 @@ async def test_rmvtransport_no_departures(hass): return_value=get_no_departures_mock(), ): assert await async_setup_component(hass, "sensor", VALID_CONFIG_MINIMAL) + await hass.async_block_till_done() state = hass.states.get("sensor.frankfurt_main_hauptbahnhof") assert not state diff --git a/tests/components/scene/test_init.py b/tests/components/scene/test_init.py index 483ab17faa6..bff88d8e660 100644 --- a/tests/components/scene/test_init.py +++ b/tests/components/scene/test_init.py @@ -23,6 +23,7 @@ class TestScene(unittest.TestCase): assert setup_component( self.hass, light.DOMAIN, {light.DOMAIN: {"platform": "test"}} ) + self.hass.block_till_done() self.light_1, self.light_2 = test_light.ENTITIES[0:2] @@ -72,6 +73,7 @@ class TestScene(unittest.TestCase): ] }, ) + self.hass.block_till_done() common.activate(self.hass, "scene.test") self.hass.block_till_done() @@ -121,6 +123,7 @@ class TestScene(unittest.TestCase): ] }, ) + self.hass.block_till_done() common.activate(self.hass, "scene.test") self.hass.block_till_done() diff --git a/tests/components/season/test_sensor.py b/tests/components/season/test_sensor.py index 2acc5f6573f..279291d6da5 100644 --- a/tests/components/season/test_sensor.py +++ b/tests/components/season/test_sensor.py @@ -207,6 +207,7 @@ class TestSeason(unittest.TestCase): """Test platform setup of northern hemisphere.""" self.hass.config.latitude = HEMISPHERE_NORTHERN["homeassistant"]["latitude"] assert setup_component(self.hass, "sensor", HEMISPHERE_NORTHERN) + self.hass.block_till_done() assert ( self.hass.config.as_dict()["latitude"] == HEMISPHERE_NORTHERN["homeassistant"]["latitude"] @@ -218,6 +219,7 @@ class TestSeason(unittest.TestCase): """Test platform setup of southern hemisphere.""" self.hass.config.latitude = HEMISPHERE_SOUTHERN["homeassistant"]["latitude"] assert setup_component(self.hass, "sensor", HEMISPHERE_SOUTHERN) + self.hass.block_till_done() assert ( self.hass.config.as_dict()["latitude"] == HEMISPHERE_SOUTHERN["homeassistant"]["latitude"] @@ -229,6 +231,7 @@ class TestSeason(unittest.TestCase): """Test platform setup of equator.""" self.hass.config.latitude = HEMISPHERE_EQUATOR["homeassistant"]["latitude"] assert setup_component(self.hass, "sensor", HEMISPHERE_EQUATOR) + self.hass.block_till_done() assert ( self.hass.config.as_dict()["latitude"] == HEMISPHERE_EQUATOR["homeassistant"]["latitude"] @@ -240,4 +243,5 @@ class TestSeason(unittest.TestCase): """Test platform setup of missing latlong.""" self.hass.config.latitude = None assert setup_component(self.hass, "sensor", HEMISPHERE_EMPTY) + self.hass.block_till_done() assert self.hass.config.as_dict()["latitude"] is None diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index 5d25e666110..f92a28d358b 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -57,6 +57,7 @@ async def test_get_conditions(hass, device_reg, entity_reg): ) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() expected_conditions = [ { @@ -93,6 +94,7 @@ async def test_get_condition_capabilities(hass, device_reg, entity_reg): ) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() expected_capabilities = { "extra_fields": [ @@ -128,6 +130,7 @@ async def test_get_condition_capabilities_none(hass, device_reg, entity_reg): config_entry.add_to_hass(hass) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() conditions = [ { @@ -160,6 +163,7 @@ async def test_if_state_not_above_below(hass, calls, caplog): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() sensor1 = platform.ENTITIES["battery"] @@ -193,6 +197,7 @@ async def test_if_state_above(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() sensor1 = platform.ENTITIES["battery"] @@ -250,6 +255,7 @@ async def test_if_state_below(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() sensor1 = platform.ENTITIES["battery"] @@ -307,6 +313,7 @@ async def test_if_state_between(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() sensor1 = platform.ENTITIES["battery"] diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index 70539871aa8..3f44e9e5e32 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -61,6 +61,7 @@ async def test_get_triggers(hass, device_reg, entity_reg): ) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() expected_triggers = [ { @@ -98,6 +99,7 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): ) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() expected_capabilities = { "extra_fields": [ @@ -134,6 +136,7 @@ async def test_get_trigger_capabilities_none(hass, device_reg, entity_reg): config_entry.add_to_hass(hass) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() triggers = [ { @@ -165,6 +168,7 @@ async def test_if_fires_not_on_above_below(hass, calls, caplog): platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() sensor1 = platform.ENTITIES["battery"] @@ -194,6 +198,7 @@ async def test_if_fires_on_state_above(hass, calls): platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() sensor1 = platform.ENTITIES["battery"] @@ -251,6 +256,7 @@ async def test_if_fires_on_state_below(hass, calls): platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() sensor1 = platform.ENTITIES["battery"] @@ -308,6 +314,7 @@ async def test_if_fires_on_state_between(hass, calls): platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() sensor1 = platform.ENTITIES["battery"] @@ -378,6 +385,7 @@ async def test_if_fires_on_state_change_with_for(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() sensor1 = platform.ENTITIES["battery"] diff --git a/tests/components/seventeentrack/test_sensor.py b/tests/components/seventeentrack/test_sensor.py index 62f93d7cd70..330f4a66152 100644 --- a/tests/components/seventeentrack/test_sensor.py +++ b/tests/components/seventeentrack/test_sensor.py @@ -2,7 +2,6 @@ import datetime from typing import Union -import mock from py17track.package import Package import pytest @@ -14,6 +13,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.setup import async_setup_component from homeassistant.util import utcnow +from tests.async_mock import MagicMock, patch from tests.common import async_fire_time_changed VALID_CONFIG_MINIMAL = { @@ -113,7 +113,7 @@ class ProfileMock: @pytest.fixture(autouse=True, name="mock_client") def fixture_mock_client(): """Mock py17track client.""" - with mock.patch( + with patch( "homeassistant.components.seventeentrack.sensor.SeventeenTrackClient", new=ClientMock, ): @@ -130,13 +130,14 @@ async def _setup_seventeentrack(hass, config=None, summary_data=None): ProfileMock.summary_data = summary_data assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() async def _goto_future(hass, future=None): """Move to future.""" if not future: future = utcnow() + datetime.timedelta(minutes=10) - with mock.patch("homeassistant.util.utcnow", return_value=future): + with patch("homeassistant.util.utcnow", return_value=future): async_fire_time_changed(hass, future) await hass.async_block_till_done() @@ -144,13 +145,14 @@ async def _goto_future(hass, future=None): async def test_full_valid_config(hass): """Ensure everything starts correctly.""" assert await async_setup_component(hass, "sensor", VALID_CONFIG_FULL) + await hass.async_block_till_done() assert len(hass.states.async_entity_ids()) == len(ProfileMock.summary_data.keys()) async def test_valid_config(hass): """Ensure everything starts correctly.""" assert await async_setup_component(hass, "sensor", VALID_CONFIG_MINIMAL) - + await hass.async_block_till_done() assert len(hass.states.async_entity_ids()) == len(ProfileMock.summary_data.keys()) @@ -243,7 +245,7 @@ async def test_delivered_not_shown(hass): ) ProfileMock.package_list = [package] - hass.components.persistent_notification = mock.MagicMock() + hass.components.persistent_notification = MagicMock() await _setup_seventeentrack(hass, VALID_CONFIG_FULL_NO_DELIVERED) assert not hass.states.async_entity_ids() hass.components.persistent_notification.create.assert_called() @@ -256,7 +258,7 @@ async def test_delivered_shown(hass): ) ProfileMock.package_list = [package] - hass.components.persistent_notification = mock.MagicMock() + hass.components.persistent_notification = MagicMock() await _setup_seventeentrack(hass, VALID_CONFIG_FULL) assert hass.states.get("sensor.seventeentrack_package_456") is not None @@ -281,7 +283,7 @@ async def test_becomes_delivered_not_shown_notification(hass): ) ProfileMock.package_list = [package_delivered] - hass.components.persistent_notification = mock.MagicMock() + hass.components.persistent_notification = MagicMock() await _goto_future(hass) hass.components.persistent_notification.create.assert_called() diff --git a/tests/components/sigfox/test_sensor.py b/tests/components/sigfox/test_sensor.py index 35534a3a126..c4af07b5799 100644 --- a/tests/components/sigfox/test_sensor.py +++ b/tests/components/sigfox/test_sensor.py @@ -50,6 +50,7 @@ class TestSigfoxSensor(unittest.TestCase): url = re.compile(API_URL + "devicetypes") mock_req.get(url, text="{}", status_code=401) assert setup_component(self.hass, "sensor", VALID_CONFIG) + self.hass.block_till_done() assert len(self.hass.states.entity_ids()) == 0 def test_valid_credentials(self): @@ -65,6 +66,7 @@ class TestSigfoxSensor(unittest.TestCase): mock_req.get(url3, text=VALID_MESSAGE) assert setup_component(self.hass, "sensor", VALID_CONFIG) + self.hass.block_till_done() assert len(self.hass.states.entity_ids()) == 1 state = self.hass.states.get("sensor.sigfox_fake_id") diff --git a/tests/components/sighthound/test_image_processing.py b/tests/components/sighthound/test_image_processing.py index 1d73ace184e..78110303702 100644 --- a/tests/components/sighthound/test_image_processing.py +++ b/tests/components/sighthound/test_image_processing.py @@ -88,6 +88,7 @@ async def test_bad_api_key(hass, caplog): "simplehound.core.cloud.detect", side_effect=hound.SimplehoundException ): await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) + await hass.async_block_till_done() assert "Sighthound error" in caplog.text assert not hass.states.get(VALID_ENTITY_ID) @@ -95,12 +96,14 @@ async def test_bad_api_key(hass, caplog): async def test_setup_platform(hass, mock_detections): """Set up platform with one entity.""" await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) + await hass.async_block_till_done() assert hass.states.get(VALID_ENTITY_ID) async def test_process_image(hass, mock_image, mock_detections): """Process an image.""" await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) + await hass.async_block_till_done() assert hass.states.get(VALID_ENTITY_ID) person_events = [] @@ -128,6 +131,7 @@ async def test_catch_bad_image( valid_config_save_file = deepcopy(VALID_CONFIG) valid_config_save_file[ip.DOMAIN].update({sh.CONF_SAVE_FILE_FOLDER: TEST_DIR}) await async_setup_component(hass, ip.DOMAIN, valid_config_save_file) + await hass.async_block_till_done() assert hass.states.get(VALID_ENTITY_ID) data = {ATTR_ENTITY_ID: VALID_ENTITY_ID} @@ -141,6 +145,7 @@ async def test_save_image(hass, mock_image, mock_detections): valid_config_save_file = deepcopy(VALID_CONFIG) valid_config_save_file[ip.DOMAIN].update({sh.CONF_SAVE_FILE_FOLDER: TEST_DIR}) await async_setup_component(hass, ip.DOMAIN, valid_config_save_file) + await hass.async_block_till_done() assert hass.states.get(VALID_ENTITY_ID) with mock.patch( @@ -166,6 +171,7 @@ async def test_save_timestamped_image(hass, mock_image, mock_detections, mock_no valid_config_save_ts_file[ip.DOMAIN].update({sh.CONF_SAVE_FILE_FOLDER: TEST_DIR}) valid_config_save_ts_file[ip.DOMAIN].update({sh.CONF_SAVE_TIMESTAMPTED_FILE: True}) await async_setup_component(hass, ip.DOMAIN, valid_config_save_ts_file) + await hass.async_block_till_done() assert hass.states.get(VALID_ENTITY_ID) with mock.patch( diff --git a/tests/components/sma/test_sensor.py b/tests/components/sma/test_sensor.py index de465d233ba..2c317520a7d 100644 --- a/tests/components/sma/test_sensor.py +++ b/tests/components/sma/test_sensor.py @@ -24,6 +24,7 @@ async def test_sma_config(hass): assert await async_setup_component( hass, DOMAIN, {DOMAIN: dict(BASE_CFG, sensors=sensors)} ) + await hass.async_block_till_done() state = hass.states.get("sensor.current_consumption") assert state diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index 83f8e4dfca1..a7ca9a4744c 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -28,8 +28,6 @@ from homeassistant.components.smartthings.const import ( CONF_INSTALLED_APP_ID, CONF_INSTANCE_ID, CONF_LOCATION_ID, - CONF_OAUTH_CLIENT_ID, - CONF_OAUTH_CLIENT_SECRET, CONF_REFRESH_TOKEN, DATA_BROKERS, DOMAIN, @@ -39,7 +37,12 @@ from homeassistant.components.smartthings.const import ( ) from homeassistant.config import async_process_ha_core_config from homeassistant.config_entries import CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_WEBHOOK_ID +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_WEBHOOK_ID, +) from homeassistant.setup import async_setup_component from tests.async_mock import Mock, patch @@ -217,8 +220,8 @@ def config_entry_fixture(hass, installed_app, location): CONF_APP_ID: installed_app.app_id, CONF_LOCATION_ID: location.location_id, CONF_REFRESH_TOKEN: str(uuid4()), - CONF_OAUTH_CLIENT_ID: str(uuid4()), - CONF_OAUTH_CLIENT_SECRET: str(uuid4()), + CONF_CLIENT_ID: str(uuid4()), + CONF_CLIENT_SECRET: str(uuid4()), } return MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index 47726bfe270..91bba9ab405 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -11,13 +11,13 @@ from homeassistant.components.smartthings.const import ( CONF_APP_ID, CONF_INSTALLED_APP_ID, CONF_LOCATION_ID, - CONF_OAUTH_CLIENT_ID, - CONF_OAUTH_CLIENT_SECRET, DOMAIN, ) from homeassistant.config import async_process_ha_core_config from homeassistant.const import ( CONF_ACCESS_TOKEN, + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, HTTP_FORBIDDEN, HTTP_NOT_FOUND, HTTP_UNAUTHORIZED, @@ -97,8 +97,8 @@ async def test_entry_created(hass, app, app_oauth_client, location, smartthings_ assert result["data"]["location_id"] == location.location_id assert result["data"]["access_token"] == token assert result["data"]["refresh_token"] == request.refresh_token - assert result["data"]["client_secret"] == app_oauth_client.client_secret - assert result["data"]["client_id"] == app_oauth_client.client_id + assert result["data"][CONF_CLIENT_SECRET] == app_oauth_client.client_secret + assert result["data"][CONF_CLIENT_ID] == app_oauth_client.client_id assert result["title"] == location.name entry = next((entry for entry in hass.config_entries.async_entries(DOMAIN)), None,) assert entry.unique_id == smartapp.format_unique_id( @@ -165,8 +165,8 @@ async def test_entry_created_from_update_event( assert result["data"]["location_id"] == location.location_id assert result["data"]["access_token"] == token assert result["data"]["refresh_token"] == request.refresh_token - assert result["data"]["client_secret"] == app_oauth_client.client_secret - assert result["data"]["client_id"] == app_oauth_client.client_id + assert result["data"][CONF_CLIENT_SECRET] == app_oauth_client.client_secret + assert result["data"][CONF_CLIENT_ID] == app_oauth_client.client_id assert result["title"] == location.name entry = next((entry for entry in hass.config_entries.async_entries(DOMAIN)), None,) assert entry.unique_id == smartapp.format_unique_id( @@ -233,8 +233,8 @@ async def test_entry_created_existing_app_new_oauth_client( assert result["data"]["location_id"] == location.location_id assert result["data"]["access_token"] == token assert result["data"]["refresh_token"] == request.refresh_token - assert result["data"]["client_secret"] == app_oauth_client.client_secret - assert result["data"]["client_id"] == app_oauth_client.client_id + assert result["data"][CONF_CLIENT_SECRET] == app_oauth_client.client_secret + assert result["data"][CONF_CLIENT_ID] == app_oauth_client.client_id assert result["title"] == location.name entry = next((entry for entry in hass.config_entries.async_entries(DOMAIN)), None,) assert entry.unique_id == smartapp.format_unique_id( @@ -262,8 +262,8 @@ async def test_entry_created_existing_app_copies_oauth_client( domain=DOMAIN, data={ CONF_APP_ID: app.app_id, - CONF_OAUTH_CLIENT_ID: oauth_client_id, - CONF_OAUTH_CLIENT_SECRET: oauth_client_secret, + CONF_CLIENT_ID: oauth_client_id, + CONF_CLIENT_SECRET: oauth_client_secret, CONF_LOCATION_ID: str(uuid4()), CONF_INSTALLED_APP_ID: str(uuid4()), CONF_ACCESS_TOKEN: token, @@ -316,8 +316,8 @@ async def test_entry_created_existing_app_copies_oauth_client( assert result["data"]["location_id"] == location.location_id assert result["data"]["access_token"] == token assert result["data"]["refresh_token"] == request.refresh_token - assert result["data"]["client_secret"] == oauth_client_secret - assert result["data"]["client_id"] == oauth_client_id + assert result["data"][CONF_CLIENT_SECRET] == oauth_client_secret + assert result["data"][CONF_CLIENT_ID] == oauth_client_id assert result["title"] == location.name entry = next( ( @@ -405,8 +405,8 @@ async def test_entry_created_with_cloudhook( assert result["data"]["location_id"] == location.location_id assert result["data"]["access_token"] == token assert result["data"]["refresh_token"] == request.refresh_token - assert result["data"]["client_secret"] == app_oauth_client.client_secret - assert result["data"]["client_id"] == app_oauth_client.client_id + assert result["data"][CONF_CLIENT_SECRET] == app_oauth_client.client_secret + assert result["data"][CONF_CLIENT_ID] == app_oauth_client.client_id assert result["title"] == location.name entry = next( (entry for entry in hass.config_entries.async_entries(DOMAIN)), None, diff --git a/tests/components/smhi/test_weather.py b/tests/components/smhi/test_weather.py index 6f5fdfd2333..2e0ad01e552 100644 --- a/tests/components/smhi/test_weather.py +++ b/tests/components/smhi/test_weather.py @@ -48,6 +48,7 @@ async def test_setup_hass(hass: HomeAssistant, aioclient_mock) -> None: entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG) await hass.config_entries.async_forward_entry_setup(entry, WEATHER_DOMAIN) + await hass.async_block_till_done() assert aioclient_mock.call_count == 1 # Testing the actual entity state for diff --git a/tests/components/somfy/test_config_flow.py b/tests/components/somfy/test_config_flow.py index 1823cb3c3ab..f5cf97bacff 100644 --- a/tests/components/somfy/test_config_flow.py +++ b/tests/components/somfy/test_config_flow.py @@ -5,14 +5,14 @@ import pytest from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.somfy import DOMAIN, config_flow +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow from tests.async_mock import patch from tests.common import MockConfigEntry -CLIENT_SECRET_VALUE = "5678" - CLIENT_ID_VALUE = "1234" +CLIENT_SECRET_VALUE = "5678" @pytest.fixture() @@ -56,18 +56,18 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock): """Check full flow.""" assert await setup.async_setup_component( hass, - "somfy", + DOMAIN, { - "somfy": { - "client_id": CLIENT_ID_VALUE, - "client_secret": CLIENT_SECRET_VALUE, + DOMAIN: { + CONF_CLIENT_ID: CLIENT_ID_VALUE, + CONF_CLIENT_SECRET: CLIENT_SECRET_VALUE, }, "http": {"base_url": "https://example.com"}, }, ) result = await hass.config_entries.flow.async_init( - "somfy", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) @@ -97,7 +97,7 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock): with patch("homeassistant.components.somfy.api.ConfigEntrySomfyApi"): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["data"]["auth_implementation"] == "somfy" + assert result["data"]["auth_implementation"] == DOMAIN result["data"]["token"].pop("expires_at") assert result["data"]["token"] == { @@ -107,8 +107,8 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock): "expires_in": 60, } - assert "somfy" in hass.config.components - entry = hass.config_entries.async_entries("somfy")[0] + assert DOMAIN in hass.config.components + entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.state == config_entries.ENTRY_STATE_LOADED assert await hass.config_entries.async_unload(entry.entry_id) diff --git a/tests/components/sonarr/__init__.py b/tests/components/sonarr/__init__.py index 573575cedc0..c7f31c67742 100644 --- a/tests/components/sonarr/__init__.py +++ b/tests/components/sonarr/__init__.py @@ -1 +1,217 @@ -"""Tests for the sonarr component.""" +"""Tests for the Sonarr component.""" +from socket import gaierror as SocketGIAError + +from homeassistant.components.sonarr.const import ( + CONF_BASE_PATH, + CONF_UPCOMING_DAYS, + CONF_WANTED_MAX_ITEMS, + DEFAULT_UPCOMING_DAYS, + DEFAULT_WANTED_MAX_ITEMS, + DOMAIN, +) +from homeassistant.const import ( + CONF_API_KEY, + CONF_HOST, + CONF_PORT, + CONF_SSL, + CONF_VERIFY_SSL, +) +from homeassistant.helpers.typing import HomeAssistantType + +from tests.common import MockConfigEntry, load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker + +HOST = "192.168.1.189" +PORT = 8989 +BASE_PATH = "/api" +API_KEY = "MOCK_API_KEY" + +MOCK_SENSOR_CONFIG = { + "platform": DOMAIN, + "host": HOST, + "api_key": API_KEY, + "days": 3, +} + +MOCK_USER_INPUT = { + CONF_HOST: HOST, + CONF_PORT: PORT, + CONF_BASE_PATH: BASE_PATH, + CONF_SSL: False, + CONF_API_KEY: API_KEY, +} + + +def mock_connection( + aioclient_mock: AiohttpClientMocker, + host: str = HOST, + port: str = PORT, + base_path: str = BASE_PATH, + error: bool = False, + invalid_auth: bool = False, + server_error: bool = False, +) -> None: + """Mock Sonarr connection.""" + if error: + mock_connection_error( + aioclient_mock, host=host, port=port, base_path=base_path, + ) + return + + if invalid_auth: + mock_connection_invalid_auth( + aioclient_mock, host=host, port=port, base_path=base_path, + ) + return + + if server_error: + mock_connection_server_error( + aioclient_mock, host=host, port=port, base_path=base_path, + ) + return + + sonarr_url = f"http://{host}:{port}{base_path}" + + aioclient_mock.get( + f"{sonarr_url}/system/status", + text=load_fixture("sonarr/system-status.json"), + headers={"Content-Type": "application/json"}, + ) + + aioclient_mock.get( + f"{sonarr_url}/diskspace", + text=load_fixture("sonarr/diskspace.json"), + headers={"Content-Type": "application/json"}, + ) + + aioclient_mock.get( + f"{sonarr_url}/calendar", + text=load_fixture("sonarr/calendar.json"), + headers={"Content-Type": "application/json"}, + ) + + aioclient_mock.get( + f"{sonarr_url}/command", + text=load_fixture("sonarr/command.json"), + headers={"Content-Type": "application/json"}, + ) + + aioclient_mock.get( + f"{sonarr_url}/queue", + text=load_fixture("sonarr/queue.json"), + headers={"Content-Type": "application/json"}, + ) + + aioclient_mock.get( + f"{sonarr_url}/series", + text=load_fixture("sonarr/series.json"), + headers={"Content-Type": "application/json"}, + ) + + aioclient_mock.get( + f"{sonarr_url}/wanted/missing", + text=load_fixture("sonarr/wanted-missing.json"), + headers={"Content-Type": "application/json"}, + ) + + +def mock_connection_error( + aioclient_mock: AiohttpClientMocker, + host: str = HOST, + port: str = PORT, + base_path: str = BASE_PATH, +) -> None: + """Mock Sonarr connection errors.""" + sonarr_url = f"http://{host}:{port}{base_path}" + + aioclient_mock.get(f"{sonarr_url}/system/status", exc=SocketGIAError) + aioclient_mock.get(f"{sonarr_url}/diskspace", exc=SocketGIAError) + aioclient_mock.get(f"{sonarr_url}/calendar", exc=SocketGIAError) + aioclient_mock.get(f"{sonarr_url}/command", exc=SocketGIAError) + aioclient_mock.get(f"{sonarr_url}/queue", exc=SocketGIAError) + aioclient_mock.get(f"{sonarr_url}/series", exc=SocketGIAError) + aioclient_mock.get(f"{sonarr_url}/missing/wanted", exc=SocketGIAError) + + +def mock_connection_invalid_auth( + aioclient_mock: AiohttpClientMocker, + host: str = HOST, + port: str = PORT, + base_path: str = BASE_PATH, +) -> None: + """Mock Sonarr invalid auth errors.""" + sonarr_url = f"http://{host}:{port}{base_path}" + + aioclient_mock.get(f"{sonarr_url}/system/status", status=403) + aioclient_mock.get(f"{sonarr_url}/diskspace", status=403) + aioclient_mock.get(f"{sonarr_url}/calendar", status=403) + aioclient_mock.get(f"{sonarr_url}/command", status=403) + aioclient_mock.get(f"{sonarr_url}/queue", status=403) + aioclient_mock.get(f"{sonarr_url}/series", status=403) + aioclient_mock.get(f"{sonarr_url}/missing/wanted", status=403) + + +def mock_connection_server_error( + aioclient_mock: AiohttpClientMocker, + host: str = HOST, + port: str = PORT, + base_path: str = BASE_PATH, +) -> None: + """Mock Sonarr server errors.""" + sonarr_url = f"http://{host}:{port}{base_path}" + + aioclient_mock.get(f"{sonarr_url}/system/status", status=500) + aioclient_mock.get(f"{sonarr_url}/diskspace", status=500) + aioclient_mock.get(f"{sonarr_url}/calendar", status=500) + aioclient_mock.get(f"{sonarr_url}/command", status=500) + aioclient_mock.get(f"{sonarr_url}/queue", status=500) + aioclient_mock.get(f"{sonarr_url}/series", status=500) + aioclient_mock.get(f"{sonarr_url}/missing/wanted", status=500) + + +async def setup_integration( + hass: HomeAssistantType, + aioclient_mock: AiohttpClientMocker, + host: str = HOST, + port: str = PORT, + base_path: str = BASE_PATH, + api_key: str = API_KEY, + unique_id: str = None, + skip_entry_setup: bool = False, + connection_error: bool = False, + invalid_auth: bool = False, + server_error: bool = False, +) -> MockConfigEntry: + """Set up the Sonarr integration in Home Assistant.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=unique_id, + data={ + CONF_HOST: host, + CONF_PORT: port, + CONF_BASE_PATH: base_path, + CONF_SSL: False, + CONF_VERIFY_SSL: False, + CONF_API_KEY: api_key, + CONF_UPCOMING_DAYS: DEFAULT_UPCOMING_DAYS, + CONF_WANTED_MAX_ITEMS: DEFAULT_WANTED_MAX_ITEMS, + }, + ) + + entry.add_to_hass(hass) + + mock_connection( + aioclient_mock, + host=host, + port=port, + base_path=base_path, + error=connection_error, + invalid_auth=invalid_auth, + server_error=server_error, + ) + + if not skip_entry_setup: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/sonarr/test_config_flow.py b/tests/components/sonarr/test_config_flow.py new file mode 100644 index 00000000000..cf5f1d15d55 --- /dev/null +++ b/tests/components/sonarr/test_config_flow.py @@ -0,0 +1,183 @@ +"""Test the Sonarr config flow.""" +from homeassistant.components.sonarr.const import ( + CONF_UPCOMING_DAYS, + CONF_WANTED_MAX_ITEMS, + DEFAULT_UPCOMING_DAYS, + DEFAULT_WANTED_MAX_ITEMS, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import CONF_HOST, CONF_SOURCE, CONF_VERIFY_SSL +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) +from homeassistant.helpers.typing import HomeAssistantType + +from tests.async_mock import patch +from tests.components.sonarr import ( + HOST, + MOCK_USER_INPUT, + mock_connection, + mock_connection_error, + mock_connection_invalid_auth, + setup_integration, +) +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_options(hass, aioclient_mock: AiohttpClientMocker): + """Test updating options.""" + entry = await setup_integration(hass, aioclient_mock) + assert entry.options[CONF_UPCOMING_DAYS] == DEFAULT_UPCOMING_DAYS + assert entry.options[CONF_WANTED_MAX_ITEMS] == DEFAULT_WANTED_MAX_ITEMS + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_UPCOMING_DAYS: 2, CONF_WANTED_MAX_ITEMS: 100}, + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_UPCOMING_DAYS] == 2 + assert result["data"][CONF_WANTED_MAX_ITEMS] == 100 + + +async def test_show_user_form(hass: HomeAssistantType) -> None: + """Test that the user set up form is served.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_USER}, + ) + + assert result["step_id"] == "user" + assert result["type"] == RESULT_TYPE_FORM + + +async def test_cannot_connect( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we show user form on connection error.""" + mock_connection_error(aioclient_mock) + + user_input = MOCK_USER_INPUT.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=user_input, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_invalid_auth( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we show user form on invalid auth.""" + mock_connection_invalid_auth(aioclient_mock) + + user_input = MOCK_USER_INPUT.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=user_input, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_unknown_error( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we show user form on unknown error.""" + user_input = MOCK_USER_INPUT.copy() + with patch( + "homeassistant.components.sonarr.config_flow.Sonarr.update", + side_effect=Exception, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=user_input, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "unknown" + + +async def test_full_import_flow_implementation( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the full manual user flow from start to finish.""" + mock_connection(aioclient_mock) + + user_input = MOCK_USER_INPUT.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_IMPORT}, data=user_input, + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == HOST + + assert result["data"] + assert result["data"][CONF_HOST] == HOST + + assert result["result"] + assert result["result"].options[CONF_UPCOMING_DAYS] == DEFAULT_UPCOMING_DAYS + assert result["result"].options[CONF_WANTED_MAX_ITEMS] == DEFAULT_WANTED_MAX_ITEMS + + +async def test_full_user_flow_implementation( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the full manual user flow from start to finish.""" + mock_connection(aioclient_mock) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_USER}, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + + user_input = MOCK_USER_INPUT.copy() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=user_input, + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == HOST + + assert result["data"] + assert result["data"][CONF_HOST] == HOST + + +async def test_full_user_flow_advanced_options( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the full manual user flow with advanced options.""" + mock_connection(aioclient_mock) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_USER, "show_advanced_options": True} + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + + user_input = { + **MOCK_USER_INPUT, + CONF_VERIFY_SSL: True, + } + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=user_input, + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == HOST + + assert result["data"] + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_VERIFY_SSL] diff --git a/tests/components/sonarr/test_init.py b/tests/components/sonarr/test_init.py new file mode 100644 index 00000000000..852befcb31c --- /dev/null +++ b/tests/components/sonarr/test_init.py @@ -0,0 +1,36 @@ +"""Tests for the Sonsrr integration.""" +from homeassistant.components.sonarr.const import DOMAIN +from homeassistant.config_entries import ( + ENTRY_STATE_LOADED, + ENTRY_STATE_NOT_LOADED, + ENTRY_STATE_SETUP_RETRY, +) +from homeassistant.core import HomeAssistant + +from tests.components.sonarr import setup_integration +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_config_entry_not_ready( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the configuration entry not ready.""" + entry = await setup_integration(hass, aioclient_mock, connection_error=True) + assert entry.state == ENTRY_STATE_SETUP_RETRY + + +async def test_unload_config_entry( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the configuration entry unloading.""" + entry = await setup_integration(hass, aioclient_mock) + + assert hass.data[DOMAIN] + assert entry.entry_id in hass.data[DOMAIN] + assert entry.state == ENTRY_STATE_LOADED + + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.entry_id not in hass.data[DOMAIN] + assert entry.state == ENTRY_STATE_NOT_LOADED diff --git a/tests/components/sonarr/test_sensor.py b/tests/components/sonarr/test_sensor.py index 96585f87068..8fbca025404 100644 --- a/tests/components/sonarr/test_sensor.py +++ b/tests/components/sonarr/test_sensor.py @@ -1,722 +1,203 @@ -"""The tests for the Sonarr platform.""" -from datetime import datetime -import time -import unittest +"""Tests for the Sonarr sensor platform.""" +from datetime import timedelta import pytest -import homeassistant.components.sonarr.sensor as sonarr -from homeassistant.const import DATA_GIGABYTES, UNIT_PERCENTAGE +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.sonarr.const import ( + CONF_BASE_PATH, + CONF_UPCOMING_DAYS, + DOMAIN, +) +from homeassistant.const import ( + ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + DATA_GIGABYTES, + STATE_UNAVAILABLE, +) +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util from tests.async_mock import patch -from tests.common import get_test_home_assistant +from tests.common import async_fire_time_changed +from tests.components.sonarr import ( + MOCK_SENSOR_CONFIG, + mock_connection, + setup_integration, +) +from tests.test_util.aiohttp import AiohttpClientMocker + +UPCOMING_ENTITY_ID = f"{SENSOR_DOMAIN}.sonarr_upcoming" -def mocked_exception(*args, **kwargs): - """Mock exception thrown by requests.get.""" - raise OSError +async def test_import_from_sensor_component( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test import from sensor platform.""" + mock_connection(aioclient_mock) + assert await async_setup_component( + hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: MOCK_SENSOR_CONFIG} + ) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + + assert entries[0].data[CONF_BASE_PATH] == "/api" + assert entries[0].options[CONF_UPCOMING_DAYS] == 3 + + assert hass.states.get(UPCOMING_ENTITY_ID) -def mocked_requests_get(*args, **kwargs): - """Mock requests.get invocations.""" +async def test_sensors( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the creation and values of the sensors.""" + entry = await setup_integration(hass, aioclient_mock, skip_entry_setup=True) + registry = await hass.helpers.entity_registry.async_get_registry() - class MockResponse: - """Class to represent a mocked response.""" + # Pre-create registry entries for disabled by default sensors + sensors = { + "commands": "sonarr_commands", + "diskspace": "sonarr_disk_space", + "queue": "sonarr_queue", + "series": "sonarr_shows", + "wanted": "sonarr_wanted", + } - def __init__(self, json_data, status_code): - """Initialize the mock response class.""" - self.json_data = json_data - self.status_code = status_code - - def json(self): - """Return the json of the response.""" - return self.json_data - - today = datetime.date(datetime.fromtimestamp(time.time())) - url = str(args[0]) - if "api/calendar" in url: - return MockResponse( - [ - { - "seriesId": 3, - "episodeFileId": 0, - "seasonNumber": 4, - "episodeNumber": 11, - "title": "Easy Com-mercial, Easy Go-mercial", - "airDate": str(today), - "airDateUtc": "2014-01-27T01:30:00Z", - "overview": "To compete with fellow “restaurateur,” Ji...", - "hasFile": "false", - "monitored": "true", - "sceneEpisodeNumber": 0, - "sceneSeasonNumber": 0, - "tvDbEpisodeId": 0, - "series": { - "tvdbId": 194031, - "tvRageId": 24607, - "imdbId": "tt1561755", - "title": "Bob's Burgers", - "cleanTitle": "bobsburgers", - "status": "continuing", - "overview": "Bob's Burgers follows a third-generation ...", - "airTime": "5:30pm", - "monitored": "true", - "qualityProfileId": 1, - "seasonFolder": "true", - "lastInfoSync": "2014-01-26T19:25:55.4555946Z", - "runtime": 30, - "images": [ - { - "coverType": "banner", - "url": "http://slurm.trakt.us/images/bann.jpg", - }, - { - "coverType": "poster", - "url": "http://slurm.trakt.us/images/poster00.jpg", - }, - { - "coverType": "fanart", - "url": "http://slurm.trakt.us/images/fan6.jpg", - }, - ], - "seriesType": "standard", - "network": "FOX", - "useSceneNumbering": "false", - "titleSlug": "bobs-burgers", - "path": "T:\\Bob's Burgers", - "year": 0, - "firstAired": "2011-01-10T01:30:00Z", - "qualityProfile": { - "value": { - "name": "SD", - "allowed": [ - {"id": 1, "name": "SDTV", "weight": 1}, - {"id": 8, "name": "WEBDL-480p", "weight": 2}, - {"id": 2, "name": "DVD", "weight": 3}, - ], - "cutoff": {"id": 1, "name": "SDTV", "weight": 1}, - "id": 1, - }, - "isLoaded": "true", - }, - "seasons": [ - {"seasonNumber": 4, "monitored": "true"}, - {"seasonNumber": 3, "monitored": "true"}, - {"seasonNumber": 2, "monitored": "true"}, - {"seasonNumber": 1, "monitored": "true"}, - {"seasonNumber": 0, "monitored": "false"}, - ], - "id": 66, - }, - "downloading": "false", - "id": 14402, - } - ], - 200, + for (unique, oid) in sensors.items(): + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + f"{entry.entry_id}_{unique}", + suggested_object_id=oid, + disabled_by=None, ) - if "api/command" in url: - return MockResponse( - [ - { - "name": "RescanSeries", - "startedOn": "0001-01-01T00:00:00Z", - "stateChangeTime": "2014-02-05T05:09:09.2366139Z", - "sendUpdatesToClient": "true", - "state": "pending", - "id": 24, - } - ], - 200, - ) - if "api/wanted/missing" in url or "totalRecords" in url: - return MockResponse( - { - "page": 1, - "pageSize": 15, - "sortKey": "airDateUtc", - "sortDirection": "descending", - "totalRecords": 1, - "records": [ - { - "seriesId": 1, - "episodeFileId": 0, - "seasonNumber": 5, - "episodeNumber": 4, - "title": "Archer Vice: House Call", - "airDate": "2014-02-03", - "airDateUtc": "2014-02-04T03:00:00Z", - "overview": "Archer has to stage an that ... ", - "hasFile": "false", - "monitored": "true", - "sceneEpisodeNumber": 0, - "sceneSeasonNumber": 0, - "tvDbEpisodeId": 0, - "absoluteEpisodeNumber": 50, - "series": { - "tvdbId": 110381, - "tvRageId": 23354, - "imdbId": "tt1486217", - "title": "Archer (2009)", - "cleanTitle": "archer2009", - "status": "continuing", - "overview": "At ISIS, an international spy ...", - "airTime": "7:00pm", - "monitored": "true", - "qualityProfileId": 1, - "seasonFolder": "true", - "lastInfoSync": "2014-02-05T04:39:28.550495Z", - "runtime": 30, - "images": [ - { - "coverType": "banner", - "url": "http://slurm.trakt.us//57.12.jpg", - }, - { - "coverType": "poster", - "url": "http://slurm.trakt.u/57.12-300.jpg", - }, - { - "coverType": "fanart", - "url": "http://slurm.trakt.us/image.12.jpg", - }, - ], - "seriesType": "standard", - "network": "FX", - "useSceneNumbering": "false", - "titleSlug": "archer-2009", - "path": "E:\\Test\\TV\\Archer (2009)", - "year": 2009, - "firstAired": "2009-09-18T02:00:00Z", - "qualityProfile": { - "value": { - "name": "SD", - "cutoff": {"id": 1, "name": "SDTV"}, - "items": [ - { - "quality": {"id": 1, "name": "SDTV"}, - "allowed": "true", - }, - { - "quality": {"id": 8, "name": "WEBDL-480p"}, - "allowed": "true", - }, - { - "quality": {"id": 2, "name": "DVD"}, - "allowed": "true", - }, - { - "quality": {"id": 4, "name": "HDTV-720p"}, - "allowed": "false", - }, - { - "quality": {"id": 9, "name": "HDTV-1080p"}, - "allowed": "false", - }, - { - "quality": {"id": 10, "name": "Raw-HD"}, - "allowed": "false", - }, - { - "quality": {"id": 5, "name": "WEBDL-720p"}, - "allowed": "false", - }, - { - "quality": {"id": 6, "name": "Bluray-720p"}, - "allowed": "false", - }, - { - "quality": {"id": 3, "name": "WEBDL-1080p"}, - "allowed": "false", - }, - { - "quality": { - "id": 7, - "name": "Bluray-1080p", - }, - "allowed": "false", - }, - ], - "id": 1, - }, - "isLoaded": "true", - }, - "seasons": [ - {"seasonNumber": 5, "monitored": "true"}, - {"seasonNumber": 4, "monitored": "true"}, - {"seasonNumber": 3, "monitored": "true"}, - {"seasonNumber": 2, "monitored": "true"}, - {"seasonNumber": 1, "monitored": "true"}, - {"seasonNumber": 0, "monitored": "false"}, - ], - "id": 1, - }, - "downloading": "false", - "id": 55, - } - ], - }, - 200, - ) - if "api/queue" in url: - return MockResponse( - [ - { - "series": { - "title": "Game of Thrones", - "sortTitle": "game thrones", - "seasonCount": 6, - "status": "continuing", - "overview": "Seven noble families fight for land ...", - "network": "HBO", - "airTime": "21:00", - "images": [ - { - "coverType": "fanart", - "url": "http://thetvdb.com/banners/fanart/-83.jpg", - }, - { - "coverType": "banner", - "url": "http://thetvdb.com/banners/-g19.jpg", - }, - { - "coverType": "poster", - "url": "http://thetvdb.com/banners/posters-34.jpg", - }, - ], - "seasons": [ - {"seasonNumber": 0, "monitored": "false"}, - {"seasonNumber": 1, "monitored": "false"}, - {"seasonNumber": 2, "monitored": "true"}, - {"seasonNumber": 3, "monitored": "false"}, - {"seasonNumber": 4, "monitored": "false"}, - {"seasonNumber": 5, "monitored": "true"}, - {"seasonNumber": 6, "monitored": "true"}, - ], - "year": 2011, - "path": "/Volumes/Media/Shows/Game of Thrones", - "profileId": 5, - "seasonFolder": "true", - "monitored": "true", - "useSceneNumbering": "false", - "runtime": 60, - "tvdbId": 121361, - "tvRageId": 24493, - "tvMazeId": 82, - "firstAired": "2011-04-16T23:00:00Z", - "lastInfoSync": "2016-02-05T16:40:11.614176Z", - "seriesType": "standard", - "cleanTitle": "gamethrones", - "imdbId": "tt0944947", - "titleSlug": "game-of-thrones", - "certification": "TV-MA", - "genres": ["Adventure", "Drama", "Fantasy"], - "tags": [], - "added": "2015-12-28T13:44:24.204583Z", - "ratings": {"votes": 1128, "value": 9.4}, - "qualityProfileId": 5, - "id": 17, - }, - "episode": { - "seriesId": 17, - "episodeFileId": 0, - "seasonNumber": 3, - "episodeNumber": 8, - "title": "Second Sons", - "airDate": "2013-05-19", - "airDateUtc": "2013-05-20T01:00:00Z", - "overview": "King’s Landing hosts a wedding, and ...", - "hasFile": "false", - "monitored": "false", - "absoluteEpisodeNumber": 28, - "unverifiedSceneNumbering": "false", - "id": 889, - }, - "quality": { - "quality": {"id": 7, "name": "Bluray-1080p"}, - "revision": {"version": 1, "real": 0}, - }, - "size": 4472186820, - "title": "Game.of.Thrones.S03E08.Second.Sons.2013.1080p.", - "sizeleft": 0, - "timeleft": "00:00:00", - "estimatedCompletionTime": "2016-02-05T22:46:52.440104Z", - "status": "Downloading", - "trackedDownloadStatus": "Ok", - "statusMessages": [], - "downloadId": "SABnzbd_nzo_Mq2f_b", - "protocol": "usenet", - "id": 1503378561, - } - ], - 200, - ) - if "api/series" in url: - return MockResponse( - [ - { - "title": "Marvel's Daredevil", - "alternateTitles": [{"title": "Daredevil", "seasonNumber": -1}], - "sortTitle": "marvels daredevil", - "seasonCount": 2, - "totalEpisodeCount": 26, - "episodeCount": 26, - "episodeFileCount": 26, - "sizeOnDisk": 79282273693, - "status": "continuing", - "overview": "Matt Murdock was blinded in a tragic accident...", - "previousAiring": "2016-03-18T04:01:00Z", - "network": "Netflix", - "airTime": "00:01", - "images": [ - { - "coverType": "fanart", - "url": "/sonarr/MediaCover/7/fanart.jpg?lastWrite=", - }, - { - "coverType": "banner", - "url": "/sonarr/MediaCover/7/banner.jpg?lastWrite=", - }, - { - "coverType": "poster", - "url": "/sonarr/MediaCover/7/poster.jpg?lastWrite=", - }, - ], - "seasons": [ - { - "seasonNumber": 1, - "monitored": "false", - "statistics": { - "previousAiring": "2015-04-10T04:01:00Z", - "episodeFileCount": 13, - "episodeCount": 13, - "totalEpisodeCount": 13, - "sizeOnDisk": 22738179333, - "percentOfEpisodes": 100, - }, - }, - { - "seasonNumber": 2, - "monitored": "false", - "statistics": { - "previousAiring": "2016-03-18T04:01:00Z", - "episodeFileCount": 13, - "episodeCount": 13, - "totalEpisodeCount": 13, - "sizeOnDisk": 56544094360, - "percentOfEpisodes": 100, - }, - }, - ], - "year": 2015, - "path": "F:\\TV_Shows\\Marvels Daredevil", - "profileId": 6, - "seasonFolder": "true", - "monitored": "true", - "useSceneNumbering": "false", - "runtime": 55, - "tvdbId": 281662, - "tvRageId": 38796, - "tvMazeId": 1369, - "firstAired": "2015-04-10T04:00:00Z", - "lastInfoSync": "2016-09-09T09:02:49.4402575Z", - "seriesType": "standard", - "cleanTitle": "marvelsdaredevil", - "imdbId": "tt3322312", - "titleSlug": "marvels-daredevil", - "certification": "TV-MA", - "genres": ["Action", "Crime", "Drama"], - "tags": [], - "added": "2015-05-15T00:20:32.7892744Z", - "ratings": {"votes": 461, "value": 8.9}, - "qualityProfileId": 6, - "id": 7, - } - ], - 200, - ) - if "api/diskspace" in url: - return MockResponse( - [ - { - "path": "/data", - "label": "", - "freeSpace": 282500067328, - "totalSpace": 499738734592, - } - ], - 200, - ) - if "api/system/status" in url: - return MockResponse( - { - "version": "2.0.0.1121", - "buildTime": "2014-02-08T20:49:36.5560392Z", - "isDebug": "false", - "isProduction": "true", - "isAdmin": "true", - "isUserInteractive": "false", - "startupPath": "C:\\ProgramData\\NzbDrone\\bin", - "appData": "C:\\ProgramData\\NzbDrone", - "osVersion": "6.2.9200.0", - "isMono": "false", - "isLinux": "false", - "isWindows": "true", - "branch": "develop", - "authentication": "false", - "startOfWeek": 0, - "urlBase": "", - }, - 200, - ) - return MockResponse({"error": "Unauthorized"}, 401) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + for (unique, oid) in sensors.items(): + entity = registry.async_get(f"sensor.{oid}") + assert entity + assert entity.unique_id == f"{entry.entry_id}_{unique}" + + state = hass.states.get("sensor.sonarr_commands") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:code-braces" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "Commands" + assert state.state == "2" + + state = hass.states.get("sensor.sonarr_disk_space") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:harddisk" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.state == "263.10" + + state = hass.states.get("sensor.sonarr_queue") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:download" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "Episodes" + assert state.state == "1" + + state = hass.states.get("sensor.sonarr_shows") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:television" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "Series" + assert state.state == "1" + + state = hass.states.get("sensor.sonarr_upcoming") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:television" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "Episodes" + assert state.state == "1" + + state = hass.states.get("sensor.sonarr_wanted") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:television" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "Episodes" + assert state.state == "2" -class TestSonarrSetup(unittest.TestCase): - """Test the Sonarr platform.""" +@pytest.mark.parametrize( + "entity_id", + ( + "sensor.sonarr_commands", + "sensor.sonarr_disk_space", + "sensor.sonarr_queue", + "sensor.sonarr_shows", + "sensor.sonarr_wanted", + ), +) +async def test_disabled_by_default_sensors( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker, entity_id: str +) -> None: + """Test the disabled by default sensors.""" + await setup_integration(hass, aioclient_mock) + registry = await hass.helpers.entity_registry.async_get_registry() + print(registry.entities) - # pylint: disable=invalid-name - DEVICES = [] + state = hass.states.get(entity_id) + assert state is None - def add_entities(self, devices, update): - """Mock add devices.""" - for device in devices: - self.DEVICES.append(device) + entry = registry.async_get(entity_id) + assert entry + assert entry.disabled + assert entry.disabled_by == "integration" - def setUp(self): - """Initialize values for this testcase class.""" - self.DEVICES = [] - self.hass = get_test_home_assistant() - self.hass.config.time_zone = "America/Los_Angeles" - def tearDown(self): # pylint: disable=invalid-name - """Stop everything that was started.""" - self.hass.stop() +async def test_availability( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test entity availability.""" + now = dt_util.utcnow() - @patch("requests.get", side_effect=mocked_requests_get) - def test_diskspace_no_paths(self, req_mock): - """Test getting all disk space.""" - config = { - "platform": "sonarr", - "api_key": "foo", - "days": "2", - "unit": DATA_GIGABYTES, - "include_paths": [], - "monitored_conditions": ["diskspace"], - } - sonarr.setup_platform(self.hass, config, self.add_entities, None) - for device in self.DEVICES: - device.update() - assert "263.10" == device.state - assert "mdi:harddisk" == device.icon - assert DATA_GIGABYTES == device.unit_of_measurement - assert "Sonarr Disk Space" == device.name - assert "263.10/465.42GB (56.53%)" == device.device_state_attributes["/data"] + with patch("homeassistant.util.dt.utcnow", return_value=now): + await setup_integration(hass, aioclient_mock) - @patch("requests.get", side_effect=mocked_requests_get) - def test_diskspace_paths(self, req_mock): - """Test getting diskspace for included paths.""" - config = { - "platform": "sonarr", - "api_key": "foo", - "days": "2", - "unit": DATA_GIGABYTES, - "include_paths": ["/data"], - "monitored_conditions": ["diskspace"], - } - sonarr.setup_platform(self.hass, config, self.add_entities, None) - for device in self.DEVICES: - device.update() - assert "263.10" == device.state - assert "mdi:harddisk" == device.icon - assert DATA_GIGABYTES == device.unit_of_measurement - assert "Sonarr Disk Space" == device.name - assert "263.10/465.42GB (56.53%)" == device.device_state_attributes["/data"] + assert hass.states.get(UPCOMING_ENTITY_ID).state == "1" - @patch("requests.get", side_effect=mocked_requests_get) - def test_commands(self, req_mock): - """Test getting running commands.""" - config = { - "platform": "sonarr", - "api_key": "foo", - "days": "2", - "unit": DATA_GIGABYTES, - "include_paths": ["/data"], - "monitored_conditions": ["commands"], - } - sonarr.setup_platform(self.hass, config, self.add_entities, None) - for device in self.DEVICES: - device.update() - assert 1 == device.state - assert "mdi:code-braces" == device.icon - assert "Commands" == device.unit_of_measurement - assert "Sonarr Commands" == device.name - assert "pending" == device.device_state_attributes["RescanSeries"] + # state to unavailable + aioclient_mock.clear_requests() + mock_connection(aioclient_mock, error=True) - @patch("requests.get", side_effect=mocked_requests_get) - def test_queue(self, req_mock): - """Test getting downloads in the queue.""" - config = { - "platform": "sonarr", - "api_key": "foo", - "days": "2", - "unit": DATA_GIGABYTES, - "include_paths": ["/data"], - "monitored_conditions": ["queue"], - } - sonarr.setup_platform(self.hass, config, self.add_entities, None) - for device in self.DEVICES: - device.update() - assert 1 == device.state - assert "mdi:download" == device.icon - assert "Episodes" == device.unit_of_measurement - assert "Sonarr Queue" == device.name - assert ( - f"100.00{UNIT_PERCENTAGE}" - == device.device_state_attributes["Game of Thrones S03E08"] - ) + future = now + timedelta(minutes=1) + with patch("homeassistant.util.dt.utcnow", return_value=future): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() - @patch("requests.get", side_effect=mocked_requests_get) - def test_series(self, req_mock): - """Test getting the number of series.""" - config = { - "platform": "sonarr", - "api_key": "foo", - "days": "2", - "unit": DATA_GIGABYTES, - "include_paths": ["/data"], - "monitored_conditions": ["series"], - } - sonarr.setup_platform(self.hass, config, self.add_entities, None) - for device in self.DEVICES: - device.update() - assert 1 == device.state - assert "mdi:television" == device.icon - assert "Shows" == device.unit_of_measurement - assert "Sonarr Series" == device.name - assert ( - "26/26 Episodes" == device.device_state_attributes["Marvel's Daredevil"] - ) + assert hass.states.get(UPCOMING_ENTITY_ID).state == STATE_UNAVAILABLE - @patch("requests.get", side_effect=mocked_requests_get) - def test_wanted(self, req_mock): - """Test getting wanted episodes.""" - config = { - "platform": "sonarr", - "api_key": "foo", - "days": "2", - "unit": DATA_GIGABYTES, - "include_paths": ["/data"], - "monitored_conditions": ["wanted"], - } - sonarr.setup_platform(self.hass, config, self.add_entities, None) - for device in self.DEVICES: - device.update() - assert 1 == device.state - assert "mdi:television" == device.icon - assert "Episodes" == device.unit_of_measurement - assert "Sonarr Wanted" == device.name - assert ( - "2014-02-03" == device.device_state_attributes["Archer (2009) S05E04"] - ) + # state to available + aioclient_mock.clear_requests() + mock_connection(aioclient_mock) - @patch("requests.get", side_effect=mocked_requests_get) - def test_upcoming_multiple_days(self, req_mock): - """Test the upcoming episodes for multiple days.""" - config = { - "platform": "sonarr", - "api_key": "foo", - "days": "2", - "unit": DATA_GIGABYTES, - "include_paths": ["/data"], - "monitored_conditions": ["upcoming"], - } - sonarr.setup_platform(self.hass, config, self.add_entities, None) - for device in self.DEVICES: - device.update() - assert 1 == device.state - assert "mdi:television" == device.icon - assert "Episodes" == device.unit_of_measurement - assert "Sonarr Upcoming" == device.name - assert "S04E11" == device.device_state_attributes["Bob's Burgers"] + future += timedelta(minutes=1) + with patch("homeassistant.util.dt.utcnow", return_value=future): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() - @pytest.mark.skip - @patch("requests.get", side_effect=mocked_requests_get) - def test_upcoming_today(self, req_mock): - """Test filtering for a single day. + assert hass.states.get(UPCOMING_ENTITY_ID).state == "1" - Sonarr needs to respond with at least 2 days - """ - config = { - "platform": "sonarr", - "api_key": "foo", - "days": "1", - "unit": DATA_GIGABYTES, - "include_paths": ["/data"], - "monitored_conditions": ["upcoming"], - } - sonarr.setup_platform(self.hass, config, self.add_entities, None) - for device in self.DEVICES: - device.update() - assert 1 == device.state - assert "mdi:television" == device.icon - assert "Episodes" == device.unit_of_measurement - assert "Sonarr Upcoming" == device.name - assert "S04E11" == device.device_state_attributes["Bob's Burgers"] + # state to unavailable + aioclient_mock.clear_requests() + mock_connection(aioclient_mock, invalid_auth=True) - @patch("requests.get", side_effect=mocked_requests_get) - def test_system_status(self, req_mock): - """Test getting system status.""" - config = { - "platform": "sonarr", - "api_key": "foo", - "days": "2", - "unit": DATA_GIGABYTES, - "include_paths": ["/data"], - "monitored_conditions": ["status"], - } - sonarr.setup_platform(self.hass, config, self.add_entities, None) - for device in self.DEVICES: - device.update() - assert "2.0.0.1121" == device.state - assert "mdi:information" == device.icon - assert "Sonarr Status" == device.name - assert "6.2.9200.0" == device.device_state_attributes["osVersion"] + future += timedelta(minutes=1) + with patch("homeassistant.util.dt.utcnow", return_value=future): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() - @pytest.mark.skip - @patch("requests.get", side_effect=mocked_requests_get) - def test_ssl(self, req_mock): - """Test SSL being enabled.""" - config = { - "platform": "sonarr", - "api_key": "foo", - "days": "1", - "unit": DATA_GIGABYTES, - "include_paths": ["/data"], - "monitored_conditions": ["upcoming"], - "ssl": "true", - } - sonarr.setup_platform(self.hass, config, self.add_entities, None) - for device in self.DEVICES: - device.update() - assert 1 == device.state - assert "s" == device.ssl - assert "mdi:television" == device.icon - assert "Episodes" == device.unit_of_measurement - assert "Sonarr Upcoming" == device.name - assert "S04E11" == device.device_state_attributes["Bob's Burgers"] + assert hass.states.get(UPCOMING_ENTITY_ID).state == STATE_UNAVAILABLE - @patch("requests.get", side_effect=mocked_exception) - def test_exception_handling(self, req_mock): - """Test exception being handled.""" - config = { - "platform": "sonarr", - "api_key": "foo", - "days": "1", - "unit": DATA_GIGABYTES, - "include_paths": ["/data"], - "monitored_conditions": ["upcoming"], - } - sonarr.setup_platform(self.hass, config, self.add_entities, None) - for device in self.DEVICES: - device.update() - assert device.state is None + # state to available + aioclient_mock.clear_requests() + mock_connection(aioclient_mock) + + future += timedelta(minutes=1) + with patch("homeassistant.util.dt.utcnow", return_value=future): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + assert hass.states.get(UPCOMING_ENTITY_ID).state == "1" diff --git a/tests/components/songpal/test_media_player.py b/tests/components/songpal/test_media_player.py index 5a7ccfd846d..e4d03d03b0a 100644 --- a/tests/components/songpal/test_media_player.py +++ b/tests/components/songpal/test_media_player.py @@ -156,9 +156,7 @@ async def test_services(hass): await _call(media_player.SERVICE_VOLUME_UP) await _call(media_player.SERVICE_VOLUME_DOWN) assert mocked_device.volume1.set_volume.call_count == 3 - mocked_device.volume1.set_volume.assert_has_calls( - [call(60), call("+1"), call("-1")] - ) + mocked_device.volume1.set_volume.assert_has_calls([call(60), call(51), call(49)]) await _call(media_player.SERVICE_VOLUME_MUTE, is_volume_muted=True) mocked_device.volume1.set_mute.assert_called_once_with(True) diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 20c1eb10320..e8441576013 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -69,4 +69,9 @@ def music_library_fixture(): @pytest.fixture(name="speaker_info") def speaker_info_fixture(): """Create speaker_info fixture.""" - return {"zone_name": "Zone A", "model_name": "Model Name"} + return { + "zone_name": "Zone A", + "model_name": "Model Name", + "software_version": "49.2-64250", + "mac_address": "00-11-22-33-44-55", + } diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index 5014ded96bb..54d54e6ed5b 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -42,3 +42,18 @@ async def test_services(hass, config_entry, config, hass_read_only_user): blocking=True, context=Context(user_id=hass_read_only_user.id), ) + + +async def test_device_registry(hass, config_entry, config, soco): + """Test sonos device registered in the device registry.""" + await setup_platform(hass, config_entry, config) + + device_registry = await hass.helpers.device_registry.async_get_registry() + reg_device = device_registry.async_get_device( + identifiers={("sonos", "RINCON_test")}, connections=set(), + ) + assert reg_device.model == "Model Name" + assert reg_device.sw_version == "49.2-64250" + assert reg_device.connections == {("mac", "00:11:22:33:44:55")} + assert reg_device.manufacturer == "Sonos" + assert reg_device.name == "Zone A" diff --git a/tests/components/spotify/test_config_flow.py b/tests/components/spotify/test_config_flow.py index 7115151451f..860840c477f 100644 --- a/tests/components/spotify/test_config_flow.py +++ b/tests/components/spotify/test_config_flow.py @@ -2,12 +2,9 @@ from spotipy import SpotifyException from homeassistant import data_entry_flow, setup -from homeassistant.components.spotify.const import ( - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, - DOMAIN, -) +from homeassistant.components.spotify.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow from tests.async_mock import patch diff --git a/tests/components/sql/test_sensor.py b/tests/components/sql/test_sensor.py index afc7bebea09..8b8bca5e37c 100644 --- a/tests/components/sql/test_sensor.py +++ b/tests/components/sql/test_sensor.py @@ -39,6 +39,7 @@ class TestSQLSensor(unittest.TestCase): } assert setup_component(self.hass, "sensor", config) + self.hass.block_till_done() state = self.hass.states.get("sensor.count_tables") assert state.state == "5" @@ -64,6 +65,7 @@ class TestSQLSensor(unittest.TestCase): } assert setup_component(self.hass, "sensor", config) + self.hass.block_till_done() state = self.hass.states.get("sensor.count_tables") assert state.state == STATE_UNKNOWN diff --git a/tests/components/startca/test_sensor.py b/tests/components/startca/test_sensor.py index f3c6af51df5..302df0492c8 100644 --- a/tests/components/startca/test_sensor.py +++ b/tests/components/startca/test_sensor.py @@ -50,6 +50,7 @@ async def test_capped_setup(hass, aioclient_mock): ) await async_setup_component(hass, "sensor", {"sensor": config}) + await hass.async_block_till_done() state = hass.states.get("sensor.start_ca_usage_ratio") assert state.attributes.get("unit_of_measurement") == UNIT_PERCENTAGE @@ -145,6 +146,7 @@ async def test_unlimited_setup(hass, aioclient_mock): ) await async_setup_component(hass, "sensor", {"sensor": config}) + await hass.async_block_till_done() state = hass.states.get("sensor.start_ca_usage_ratio") assert state.attributes.get("unit_of_measurement") == UNIT_PERCENTAGE diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 721cf71303d..ffbf4d9fcd8 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -57,6 +57,7 @@ class TestStatisticsSensor(unittest.TestCase): }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -82,6 +83,7 @@ class TestStatisticsSensor(unittest.TestCase): }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -121,6 +123,7 @@ class TestStatisticsSensor(unittest.TestCase): }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -150,6 +153,7 @@ class TestStatisticsSensor(unittest.TestCase): }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -197,6 +201,7 @@ class TestStatisticsSensor(unittest.TestCase): }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -238,6 +243,7 @@ class TestStatisticsSensor(unittest.TestCase): }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -291,6 +297,7 @@ class TestStatisticsSensor(unittest.TestCase): }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -342,6 +349,7 @@ class TestStatisticsSensor(unittest.TestCase): }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -407,6 +415,7 @@ class TestStatisticsSensor(unittest.TestCase): ) self.hass.block_till_done() + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() diff --git a/tests/components/switch/test_device_action.py b/tests/components/switch/test_device_action.py index fbd24fe2095..204b2370cb8 100644 --- a/tests/components/switch/test_device_action.py +++ b/tests/components/switch/test_device_action.py @@ -75,6 +75,7 @@ async def test_action(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() ent1, ent2, ent3 = platform.ENTITIES diff --git a/tests/components/switch/test_device_condition.py b/tests/components/switch/test_device_condition.py index e4e8564f5bb..6e542ee24d1 100644 --- a/tests/components/switch/test_device_condition.py +++ b/tests/components/switch/test_device_condition.py @@ -96,6 +96,7 @@ async def test_if_state(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() ent1, ent2, ent3 = platform.ENTITIES @@ -173,6 +174,7 @@ async def test_if_fires_on_for_condition(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() ent1, ent2, ent3 = platform.ENTITIES diff --git a/tests/components/switch/test_device_trigger.py b/tests/components/switch/test_device_trigger.py index 73d12d0a729..512942ca71b 100644 --- a/tests/components/switch/test_device_trigger.py +++ b/tests/components/switch/test_device_trigger.py @@ -96,6 +96,7 @@ async def test_if_fires_on_state_change(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() ent1, ent2, ent3 = platform.ENTITIES @@ -180,6 +181,7 @@ async def test_if_fires_on_state_change_with_for(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() ent1, ent2, ent3 = platform.ENTITIES diff --git a/tests/components/switch/test_init.py b/tests/components/switch/test_init.py index 7a14d3ac117..6605d19d46f 100644 --- a/tests/components/switch/test_init.py +++ b/tests/components/switch/test_init.py @@ -33,6 +33,7 @@ class TestSwitch(unittest.TestCase): assert setup_component( self.hass, switch.DOMAIN, {switch.DOMAIN: {CONF_PLATFORM: "test"}} ) + self.hass.block_till_done() assert switch.is_on(self.hass, self.switch_1.entity_id) assert not switch.is_on(self.hass, self.switch_2.entity_id) assert not switch.is_on(self.hass, self.switch_3.entity_id) diff --git a/tests/components/teksavvy/test_sensor.py b/tests/components/teksavvy/test_sensor.py index 049a1ddc60c..7de8651f16b 100644 --- a/tests/components/teksavvy/test_sensor.py +++ b/tests/components/teksavvy/test_sensor.py @@ -44,6 +44,7 @@ async def test_capped_setup(hass, aioclient_mock): ) await async_setup_component(hass, "sensor", {"sensor": config}) + await hass.async_block_till_done() state = hass.states.get("sensor.teksavvy_data_limit") assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES @@ -125,6 +126,7 @@ async def test_unlimited_setup(hass, aioclient_mock): ) await async_setup_component(hass, "sensor", {"sensor": config}) + await hass.async_block_till_done() state = hass.states.get("sensor.teksavvy_data_limit") assert state.attributes.get("unit_of_measurement") == DATA_GIGABYTES diff --git a/tests/components/template/test_alarm_control_panel.py b/tests/components/template/test_alarm_control_panel.py index 6d2e4e48279..1126f6b60a1 100644 --- a/tests/components/template/test_alarm_control_panel.py +++ b/tests/components/template/test_alarm_control_panel.py @@ -54,6 +54,7 @@ async def test_template_state_text(hass): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -136,6 +137,7 @@ async def test_optimistic_states(hass): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -189,6 +191,7 @@ async def test_no_action_scripts(hass): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -261,6 +264,7 @@ async def test_template_syntax_error(hass, caplog): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -305,6 +309,7 @@ async def test_invalid_name_does_not_create(hass, caplog): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -325,6 +330,7 @@ async def test_invalid_panel_does_not_create(hass, caplog): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -338,6 +344,7 @@ async def test_no_panels_does_not_create(hass, caplog): hass, "alarm_control_panel", {"alarm_control_panel": {"platform": "template"}}, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -383,6 +390,7 @@ async def test_name(hass): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -425,6 +433,7 @@ async def test_arm_home_action(hass): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -471,6 +480,7 @@ async def test_arm_away_action(hass): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -517,6 +527,7 @@ async def test_arm_night_action(hass): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -563,6 +574,7 @@ async def test_disarm_action(hass): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index b024bbc311f..698d1c2ca80 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -127,6 +127,7 @@ class TestBinarySensorTemplate(unittest.TestCase): }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -161,6 +162,7 @@ class TestBinarySensorTemplate(unittest.TestCase): }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -193,6 +195,7 @@ class TestBinarySensorTemplate(unittest.TestCase): }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -224,6 +227,7 @@ class TestBinarySensorTemplate(unittest.TestCase): }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() init_calls = len(_async_render.mock_calls) @@ -280,6 +284,7 @@ class TestBinarySensorTemplate(unittest.TestCase): with assert_setup_component(1): assert setup.setup_component(self.hass, "binary_sensor", config) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -335,6 +340,7 @@ async def test_template_delay_on(hass): } } await setup.async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() await hass.async_start() hass.states.async_set("sensor.test_state", "on") @@ -394,6 +400,7 @@ async def test_template_delay_off(hass): } hass.states.async_set("sensor.test_state", "on") await setup.async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() await hass.async_start() hass.states.async_set("sensor.test_state", "off") @@ -452,6 +459,7 @@ async def test_available_without_availability_template(hass): } } await setup.async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -475,6 +483,7 @@ async def test_availability_template(hass): } } await setup.async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -537,7 +546,7 @@ async def test_invalid_availability_template_keeps_component_available(hass, cap } }, ) - + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index 095393c8acb..56db2445f6b 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -62,6 +62,7 @@ async def test_template_state_text(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -104,6 +105,7 @@ async def test_template_state_boolean(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -137,6 +139,7 @@ async def test_template_position(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -192,6 +195,7 @@ async def test_template_tilt(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -226,6 +230,7 @@ async def test_template_out_of_bounds(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -264,6 +269,7 @@ async def test_template_mutex(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -310,6 +316,7 @@ async def test_template_open_and_close(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -347,6 +354,7 @@ async def test_template_non_numeric(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -378,6 +386,7 @@ async def test_open_action(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -416,6 +425,7 @@ async def test_close_stop_action(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -463,6 +473,7 @@ async def test_set_position(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -537,6 +548,7 @@ async def test_set_tilt_position(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -578,6 +590,7 @@ async def test_open_tilt_action(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -616,6 +629,7 @@ async def test_close_tilt_action(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -644,6 +658,7 @@ async def test_set_position_optimistic(hass, calls): } }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -708,6 +723,7 @@ async def test_set_tilt_position_optimistic(hass, calls): } }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -782,6 +798,7 @@ async def test_icon_template(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -825,6 +842,7 @@ async def test_entity_picture_template(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -866,6 +884,7 @@ async def test_availability_template(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -905,6 +924,7 @@ async def test_availability_without_availability_template(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -938,6 +958,7 @@ async def test_invalid_availability_template_keeps_component_available(hass, cap }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -972,6 +993,7 @@ async def test_device_class(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -1006,6 +1028,7 @@ async def test_invalid_device_class(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() diff --git a/tests/components/template/test_fan.py b/tests/components/template/test_fan.py index fb02e4f3227..2e44ec6f0ca 100644 --- a/tests/components/template/test_fan.py +++ b/tests/components/template/test_fan.py @@ -63,6 +63,7 @@ async def test_missing_optional_config(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -88,6 +89,7 @@ async def test_missing_value_template_config(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -113,6 +115,7 @@ async def test_missing_turn_on_config(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -138,6 +141,7 @@ async def test_missing_turn_off_config(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -161,6 +165,7 @@ async def test_invalid_config(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -202,6 +207,7 @@ async def test_templates_with_entities(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -241,6 +247,7 @@ async def test_availability_template_with_entities(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -282,6 +289,7 @@ async def test_templates_with_valid_values(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -311,6 +319,7 @@ async def test_templates_invalid_values(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -342,6 +351,7 @@ async def test_invalid_availability_template_keeps_component_available(hass, cap }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -687,5 +697,6 @@ async def _register_components(hass, speed_list=None): {"fan": {"platform": "template", "fans": {"test_fan": test_fan_config}}}, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index 9982d72326b..8e27a6ba15d 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -78,6 +78,7 @@ class TestTemplateLight: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -117,6 +118,7 @@ class TestTemplateLight: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -169,6 +171,7 @@ class TestTemplateLight: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -208,6 +211,7 @@ class TestTemplateLight: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -246,6 +250,7 @@ class TestTemplateLight: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -265,6 +270,7 @@ class TestTemplateLight: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -277,6 +283,7 @@ class TestTemplateLight: self.hass, "light", {"light": {"platform": "template"}} ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -316,6 +323,7 @@ class TestTemplateLight: del light["light"]["lights"]["light_one"][missing_key] with assert_setup_component(count, "light"): assert setup.setup_component(self.hass, "light", light) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -353,6 +361,7 @@ class TestTemplateLight: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -395,6 +404,7 @@ class TestTemplateLight: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -440,6 +450,7 @@ class TestTemplateLight: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -482,6 +493,7 @@ class TestTemplateLight: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -526,6 +538,7 @@ class TestTemplateLight: } }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -587,6 +600,7 @@ class TestTemplateLight: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -625,6 +639,7 @@ class TestTemplateLight: } }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -685,6 +700,7 @@ class TestTemplateLight: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -737,6 +753,7 @@ class TestTemplateLight: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -775,6 +792,7 @@ class TestTemplateLight: } }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -825,6 +843,7 @@ class TestTemplateLight: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -870,6 +889,7 @@ class TestTemplateLight: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -920,6 +940,7 @@ class TestTemplateLight: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -975,6 +996,7 @@ class TestTemplateLight: } }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -1046,6 +1068,7 @@ class TestTemplateLight: } }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() state = self.hass.states.get("light.test_template_light") @@ -1084,6 +1107,7 @@ async def test_available_template_with_entities(hass): } }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -1134,6 +1158,7 @@ async def test_invalid_availability_template_keeps_component_available(hass, cap }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() diff --git a/tests/components/template/test_lock.py b/tests/components/template/test_lock.py index 10f959efed8..1389040c4bb 100644 --- a/tests/components/template/test_lock.py +++ b/tests/components/template/test_lock.py @@ -57,6 +57,7 @@ class TestTemplateLock: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -94,6 +95,7 @@ class TestTemplateLock: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -122,6 +124,7 @@ class TestTemplateLock: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -150,6 +153,7 @@ class TestTemplateLock: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -178,6 +182,7 @@ class TestTemplateLock: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -192,6 +197,7 @@ class TestTemplateLock: {"lock": {"platform": "template", "value_template": "Invalid"}}, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -219,6 +225,7 @@ class TestTemplateLock: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -246,6 +253,7 @@ class TestTemplateLock: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -281,6 +289,7 @@ class TestTemplateLock: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -315,6 +324,7 @@ class TestTemplateLock: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -352,6 +362,7 @@ async def test_available_template_with_entities(hass): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -389,6 +400,7 @@ async def test_invalid_availability_template_keeps_component_available(hass, cap }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index e9033b3dfe3..0fbce50f2a3 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -42,6 +42,7 @@ class TestTemplateSensor: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -75,6 +76,7 @@ class TestTemplateSensor: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -108,6 +110,7 @@ class TestTemplateSensor: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -138,6 +141,7 @@ class TestTemplateSensor: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -168,6 +172,7 @@ class TestTemplateSensor: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -200,6 +205,7 @@ class TestTemplateSensor: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -229,6 +235,7 @@ class TestTemplateSensor: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() assert self.hass.states.all() == [] @@ -252,6 +259,7 @@ class TestTemplateSensor: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -276,6 +284,7 @@ class TestTemplateSensor: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -295,6 +304,7 @@ class TestTemplateSensor: }, ) + self.hass.block_till_done() self.hass.start() assert self.hass.states.all() == [] @@ -306,6 +316,7 @@ class TestTemplateSensor: self.hass, "sensor", {"sensor": {"platform": "template"}} ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -329,6 +340,7 @@ class TestTemplateSensor: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -401,6 +413,7 @@ class TestTemplateSensor: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -445,6 +458,7 @@ async def test_available_template_with_entities(hass): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -510,6 +524,7 @@ async def test_invalid_availability_template_keeps_component_available(hass, cap }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -549,6 +564,7 @@ async def test_no_template_match_all(hass, caplog): } }, ) + await hass.async_block_till_done() assert hass.states.get("sensor.invalid_state").state == "unknown" assert hass.states.get("sensor.invalid_icon").state == "unknown" diff --git a/tests/components/template/test_switch.py b/tests/components/template/test_switch.py index a66028a318f..1ec8960a183 100644 --- a/tests/components/template/test_switch.py +++ b/tests/components/template/test_switch.py @@ -56,6 +56,7 @@ class TestTemplateSwitch: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -97,6 +98,7 @@ class TestTemplateSwitch: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -129,6 +131,7 @@ class TestTemplateSwitch: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -164,6 +167,7 @@ class TestTemplateSwitch: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -205,6 +209,7 @@ class TestTemplateSwitch: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -243,6 +248,7 @@ class TestTemplateSwitch: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -274,6 +280,7 @@ class TestTemplateSwitch: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -293,6 +300,7 @@ class TestTemplateSwitch: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -305,6 +313,7 @@ class TestTemplateSwitch: self.hass, "switch", {"switch": {"platform": "template"}} ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -336,6 +345,7 @@ class TestTemplateSwitch: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -367,6 +377,7 @@ class TestTemplateSwitch: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -398,6 +409,7 @@ class TestTemplateSwitch: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -425,6 +437,7 @@ class TestTemplateSwitch: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -461,6 +474,7 @@ class TestTemplateSwitch: }, ) + self.hass.block_till_done() self.hass.start() self.hass.block_till_done() @@ -502,6 +516,7 @@ async def test_available_template_with_entities(hass): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -542,6 +557,7 @@ async def test_invalid_availability_template_keeps_component_available(hass, cap }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() diff --git a/tests/components/template/test_vacuum.py b/tests/components/template/test_vacuum.py index 4080b75f46a..c8ae5bdce51 100644 --- a/tests/components/template/test_vacuum.py +++ b/tests/components/template/test_vacuum.py @@ -50,6 +50,7 @@ async def test_missing_optional_config(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -70,6 +71,7 @@ async def test_missing_start_config(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -90,6 +92,7 @@ async def test_invalid_config(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -120,6 +123,7 @@ async def test_templates_with_entities(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -152,6 +156,7 @@ async def test_templates_with_valid_values(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -178,6 +183,7 @@ async def test_templates_invalid_values(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -205,6 +211,7 @@ async def test_invalid_templates(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -230,6 +237,7 @@ async def test_available_template_with_entities(hass, calls): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -266,6 +274,7 @@ async def test_invalid_availability_template_keeps_component_available(hass, cap }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -446,6 +455,7 @@ async def _register_basic_vacuum(hass): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() @@ -532,5 +542,6 @@ async def _register_components(hass): }, ) + await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() diff --git a/tests/components/tod/test_binary_sensor.py b/tests/components/tod/test_binary_sensor.py index 4febd1aa8d1..afa299ef063 100644 --- a/tests/components/tod/test_binary_sensor.py +++ b/tests/components/tod/test_binary_sensor.py @@ -79,8 +79,8 @@ class TestBinarySensorTod(unittest.TestCase): return_value=test_time, ): setup_component(self.hass, "binary_sensor", config) + self.hass.block_till_done() - self.hass.block_till_done() state = self.hass.states.get("binary_sensor.evening") assert state.state == STATE_ON @@ -97,8 +97,8 @@ class TestBinarySensorTod(unittest.TestCase): return_value=test_time, ): setup_component(self.hass, "binary_sensor", config) + self.hass.block_till_done() - self.hass.block_till_done() state = self.hass.states.get("binary_sensor.night") assert state.state == STATE_ON @@ -117,6 +117,7 @@ class TestBinarySensorTod(unittest.TestCase): return_value=test_time, ): setup_component(self.hass, "binary_sensor", config) + self.hass.block_till_done() state = self.hass.states.get("binary_sensor.night") assert state.state == STATE_OFF @@ -151,8 +152,8 @@ class TestBinarySensorTod(unittest.TestCase): return_value=test_time, ): setup_component(self.hass, "binary_sensor", config) + self.hass.block_till_done() - self.hass.block_till_done() state = self.hass.states.get("binary_sensor.night") assert state.state == STATE_OFF @@ -172,8 +173,8 @@ class TestBinarySensorTod(unittest.TestCase): return_value=test_time, ): setup_component(self.hass, "binary_sensor", config) + self.hass.block_till_done() - self.hass.block_till_done() state = self.hass.states.get("binary_sensor.night") assert state.state == STATE_OFF @@ -232,6 +233,7 @@ class TestBinarySensorTod(unittest.TestCase): return_value=testtime, ): setup_component(self.hass, "binary_sensor", config) + self.hass.block_till_done() self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -328,6 +330,7 @@ class TestBinarySensorTod(unittest.TestCase): return_value=testtime, ): setup_component(self.hass, "binary_sensor", config) + self.hass.block_till_done() self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -424,8 +427,8 @@ class TestBinarySensorTod(unittest.TestCase): return_value=testtime, ): setup_component(self.hass, "binary_sensor", config) + self.hass.block_till_done() - self.hass.block_till_done() state = self.hass.states.get(entity_id) assert state.state == STATE_OFF @@ -497,8 +500,8 @@ class TestBinarySensorTod(unittest.TestCase): return_value=testtime, ): setup_component(self.hass, "binary_sensor", config) + self.hass.block_till_done() - self.hass.block_till_done() state = self.hass.states.get(entity_id) assert state.state == STATE_OFF @@ -544,6 +547,7 @@ class TestBinarySensorTod(unittest.TestCase): return_value=testtime, ): setup_component(self.hass, "binary_sensor", config) + self.hass.block_till_done() self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -659,6 +663,7 @@ class TestBinarySensorTod(unittest.TestCase): return_value=testtime, ): setup_component(self.hass, "binary_sensor", config) + self.hass.block_till_done() self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -774,6 +779,7 @@ class TestBinarySensorTod(unittest.TestCase): return_value=testtime, ): setup_component(self.hass, "binary_sensor", config) + self.hass.block_till_done() self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -883,6 +889,7 @@ class TestBinarySensorTod(unittest.TestCase): return_value=testtime, ): setup_component(self.hass, "binary_sensor", config) + self.hass.block_till_done() self.hass.block_till_done() state = self.hass.states.get(entity_id) diff --git a/tests/components/toon/test_config_flow.py b/tests/components/toon/test_config_flow.py index fdf97243a3a..4ba74245876 100644 --- a/tests/components/toon/test_config_flow.py +++ b/tests/components/toon/test_config_flow.py @@ -10,14 +10,13 @@ from toonapilib.toonapilibexceptions import ( from homeassistant import data_entry_flow from homeassistant.components.toon import config_flow -from homeassistant.components.toon.const import ( +from homeassistant.components.toon.const import CONF_DISPLAY, CONF_TENANT, DOMAIN +from homeassistant.const import ( CONF_CLIENT_ID, CONF_CLIENT_SECRET, - CONF_DISPLAY, - CONF_TENANT, - DOMAIN, + CONF_PASSWORD, + CONF_USERNAME, ) -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.setup import async_setup_component from tests.async_mock import patch @@ -76,8 +75,8 @@ async def test_show_authenticate_form(hass): @pytest.mark.parametrize( "side_effect,reason", [ - (InvalidConsumerKey, "client_id"), - (InvalidConsumerSecret, "client_secret"), + (InvalidConsumerKey, CONF_CLIENT_ID), + (InvalidConsumerSecret, CONF_CLIENT_SECRET), (AgreementsRetrievalError, "no_agreements"), (Exception, "unknown_auth_fail"), ], diff --git a/tests/components/totalconnect/common.py b/tests/components/totalconnect/common.py new file mode 100644 index 00000000000..c2d6f92015c --- /dev/null +++ b/tests/components/totalconnect/common.py @@ -0,0 +1,129 @@ +"""Common methods used across tests for TotalConnect.""" +from total_connect_client import TotalConnectClient + +from homeassistant.components.totalconnect import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.setup import async_setup_component + +from tests.async_mock import patch +from tests.common import MockConfigEntry + +LOCATION_INFO_BASIC_NORMAL = { + "LocationID": "123456", + "LocationName": "test", + "SecurityDeviceID": "987654", + "PhotoURL": "http://www.example.com/some/path/to/file.jpg", + "LocationModuleFlags": "Security=1,Video=0,Automation=0,GPS=0,VideoPIR=0", + "DeviceList": None, +} + +LOCATIONS = {"LocationInfoBasic": [LOCATION_INFO_BASIC_NORMAL]} + +MODULE_FLAGS = "Some=0,Fake=1,Flags=2" + +USER = { + "UserID": "1234567", + "Username": "username", + "UserFeatureList": "Master=0,User Administration=0,Configuration Administration=0", +} + +RESPONSE_AUTHENTICATE = { + "ResultCode": 0, + "SessionID": 1, + "Locations": LOCATIONS, + "ModuleFlags": MODULE_FLAGS, + "UserInfo": USER, +} + +PARTITION_DISARMED = { + "PartitionID": "1", + "ArmingState": TotalConnectClient.TotalConnectLocation.DISARMED, +} + +PARTITION_ARMED_STAY = { + "PartitionID": "1", + "ArmingState": TotalConnectClient.TotalConnectLocation.ARMED_STAY, +} + +PARTITION_ARMED_AWAY = { + "PartitionID": "1", + "ArmingState": TotalConnectClient.TotalConnectLocation.ARMED_AWAY, +} + +PARTITION_INFO_DISARMED = {0: PARTITION_DISARMED} +PARTITION_INFO_ARMED_STAY = {0: PARTITION_ARMED_STAY} +PARTITION_INFO_ARMED_AWAY = {0: PARTITION_ARMED_AWAY} + +PARTITIONS_DISARMED = {"PartitionInfo": PARTITION_INFO_DISARMED} +PARTITIONS_ARMED_STAY = {"PartitionInfo": PARTITION_INFO_ARMED_STAY} +PARTITIONS_ARMED_AWAY = {"PartitionInfo": PARTITION_INFO_ARMED_AWAY} + +ZONE_NORMAL = { + "ZoneID": "1", + "ZoneDescription": "Normal", + "ZoneStatus": TotalConnectClient.ZONE_STATUS_NORMAL, + "PartitionId": "1", +} + +ZONE_INFO = [ZONE_NORMAL] +ZONES = {"ZoneInfo": ZONE_INFO} + +METADATA_DISARMED = { + "Partitions": PARTITIONS_DISARMED, + "Zones": ZONES, + "PromptForImportSecuritySettings": False, + "IsInACLoss": False, + "IsCoverTampered": False, + "Bell1SupervisionFailure": False, + "Bell2SupervisionFailure": False, + "IsInLowBattery": False, +} + +METADATA_ARMED_STAY = METADATA_DISARMED.copy() +METADATA_ARMED_STAY["Partitions"] = PARTITIONS_ARMED_STAY + +METADATA_ARMED_AWAY = METADATA_DISARMED.copy() +METADATA_ARMED_AWAY["Partitions"] = PARTITIONS_ARMED_AWAY + +RESPONSE_DISARMED = {"ResultCode": 0, "PanelMetadataAndStatus": METADATA_DISARMED} +RESPONSE_ARMED_STAY = {"ResultCode": 0, "PanelMetadataAndStatus": METADATA_ARMED_STAY} +RESPONSE_ARMED_AWAY = {"ResultCode": 0, "PanelMetadataAndStatus": METADATA_ARMED_AWAY} + +RESPONSE_ARM_SUCCESS = {"ResultCode": TotalConnectClient.TotalConnectClient.ARM_SUCCESS} +RESPONSE_ARM_FAILURE = { + "ResultCode": TotalConnectClient.TotalConnectClient.COMMAND_FAILED +} +RESPONSE_DISARM_SUCCESS = { + "ResultCode": TotalConnectClient.TotalConnectClient.DISARM_SUCCESS +} +RESPONSE_DISARM_FAILURE = { + "ResultCode": TotalConnectClient.TotalConnectClient.COMMAND_FAILED, + "ResultData": "Command Failed", +} + + +async def setup_platform(hass, platform): + """Set up the TotalConnect platform.""" + # first set up a config entry and add it to hass + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}, + ) + mock_entry.add_to_hass(hass) + + responses = [RESPONSE_AUTHENTICATE, RESPONSE_DISARMED] + + with patch("homeassistant.components.totalconnect.PLATFORMS", [platform]), patch( + "zeep.Client", autospec=True + ), patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ) as mock_request, patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.get_zone_details", + return_value=True, + ): + assert await async_setup_component(hass, DOMAIN, {}) + assert mock_request.call_count == 2 + await hass.async_block_till_done() + + return mock_entry diff --git a/tests/components/totalconnect/test_alarm_control_panel.py b/tests/components/totalconnect/test_alarm_control_panel.py new file mode 100644 index 00000000000..75e07f09bf7 --- /dev/null +++ b/tests/components/totalconnect/test_alarm_control_panel.py @@ -0,0 +1,153 @@ +"""Tests for the TotalConnect alarm control panel device.""" +import pytest + +from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_DISARM, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED, +) + +from .common import ( + RESPONSE_ARM_FAILURE, + RESPONSE_ARM_SUCCESS, + RESPONSE_ARMED_AWAY, + RESPONSE_ARMED_STAY, + RESPONSE_DISARM_FAILURE, + RESPONSE_DISARM_SUCCESS, + RESPONSE_DISARMED, + setup_platform, +) + +from tests.async_mock import patch + +ENTITY_ID = "alarm_control_panel.test" +CODE = "-1" +DATA = {ATTR_ENTITY_ID: ENTITY_ID} + + +async def test_attributes(hass): + """Test the alarm control panel attributes are correct.""" + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + return_value=RESPONSE_DISARMED, + ) as mock_request: + await setup_platform(hass, ALARM_DOMAIN) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ALARM_DISARMED + mock_request.assert_called_once() + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "test" + + +async def test_arm_home_success(hass): + """Test arm home method success.""" + responses = [RESPONSE_DISARMED, RESPONSE_ARM_SUCCESS, RESPONSE_ARMED_STAY] + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ): + await setup_platform(hass, ALARM_DOMAIN) + assert STATE_ALARM_DISARMED == hass.states.get(ENTITY_ID).state + + await hass.services.async_call( + ALARM_DOMAIN, SERVICE_ALARM_ARM_HOME, DATA, blocking=True + ) + + await hass.async_block_till_done() + assert STATE_ALARM_ARMED_HOME == hass.states.get(ENTITY_ID).state + + +async def test_arm_home_failure(hass): + """Test arm home method failure.""" + responses = [RESPONSE_DISARMED, RESPONSE_ARM_FAILURE, RESPONSE_DISARMED] + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ): + await setup_platform(hass, ALARM_DOMAIN) + assert STATE_ALARM_DISARMED == hass.states.get(ENTITY_ID).state + + with pytest.raises(Exception) as e: + await hass.services.async_call( + ALARM_DOMAIN, SERVICE_ALARM_ARM_HOME, DATA, blocking=True + ) + await hass.async_block_till_done() + assert f"{e.value}" == "TotalConnect failed to arm home test." + assert STATE_ALARM_DISARMED == hass.states.get(ENTITY_ID).state + + +async def test_arm_away_success(hass): + """Test arm away method success.""" + responses = [RESPONSE_DISARMED, RESPONSE_ARM_SUCCESS, RESPONSE_ARMED_AWAY] + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ): + await setup_platform(hass, ALARM_DOMAIN) + assert STATE_ALARM_DISARMED == hass.states.get(ENTITY_ID).state + + await hass.services.async_call( + ALARM_DOMAIN, SERVICE_ALARM_ARM_AWAY, DATA, blocking=True + ) + await hass.async_block_till_done() + assert STATE_ALARM_ARMED_AWAY == hass.states.get(ENTITY_ID).state + + +async def test_arm_away_failure(hass): + """Test arm away method failure.""" + responses = [RESPONSE_DISARMED, RESPONSE_ARM_FAILURE, RESPONSE_DISARMED] + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ): + await setup_platform(hass, ALARM_DOMAIN) + assert STATE_ALARM_DISARMED == hass.states.get(ENTITY_ID).state + + with pytest.raises(Exception) as e: + await hass.services.async_call( + ALARM_DOMAIN, SERVICE_ALARM_ARM_AWAY, DATA, blocking=True + ) + await hass.async_block_till_done() + assert f"{e.value}" == "TotalConnect failed to arm away test." + assert STATE_ALARM_DISARMED == hass.states.get(ENTITY_ID).state + + +async def test_disarm_success(hass): + """Test disarm method success.""" + responses = [RESPONSE_ARMED_AWAY, RESPONSE_DISARM_SUCCESS, RESPONSE_DISARMED] + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ): + await setup_platform(hass, ALARM_DOMAIN) + assert STATE_ALARM_ARMED_AWAY == hass.states.get(ENTITY_ID).state + + await hass.services.async_call( + ALARM_DOMAIN, SERVICE_ALARM_DISARM, DATA, blocking=True + ) + await hass.async_block_till_done() + assert STATE_ALARM_DISARMED == hass.states.get(ENTITY_ID).state + + +async def test_disarm_failure(hass): + """Test disarm method failure.""" + responses = [RESPONSE_ARMED_AWAY, RESPONSE_DISARM_FAILURE, RESPONSE_ARMED_AWAY] + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ): + await setup_platform(hass, ALARM_DOMAIN) + assert STATE_ALARM_ARMED_AWAY == hass.states.get(ENTITY_ID).state + + with pytest.raises(Exception) as e: + await hass.services.async_call( + ALARM_DOMAIN, SERVICE_ALARM_DISARM, DATA, blocking=True + ) + await hass.async_block_till_done() + assert f"{e.value}" == "TotalConnect failed to disarm test." + assert STATE_ALARM_ARMED_AWAY == hass.states.get(ENTITY_ID).state diff --git a/tests/components/tradfri/test_light.py b/tests/components/tradfri/test_light.py index 8ffc25aba5a..a5a5823fbf4 100644 --- a/tests/components/tradfri/test_light.py +++ b/tests/components/tradfri/test_light.py @@ -149,6 +149,7 @@ async def setup_gateway(hass, mock_gateway, mock_api): hass.data[tradfri.KEY_GATEWAY] = {entry.entry_id: mock_gateway} hass.data[tradfri.KEY_API] = {entry.entry_id: mock_api} await hass.config_entries.async_forward_entry_setup(entry, "light") + await hass.async_block_till_done() def mock_light(test_features={}, test_state={}, n=0): diff --git a/tests/components/transport_nsw/test_sensor.py b/tests/components/transport_nsw/test_sensor.py index ab66260a55e..0d12589372e 100644 --- a/tests/components/transport_nsw/test_sensor.py +++ b/tests/components/transport_nsw/test_sensor.py @@ -46,6 +46,7 @@ class TestRMVtransportSensor(unittest.TestCase): def test_transportnsw_config(self, mock_get_departures): """Test minimal TransportNSW configuration.""" assert setup_component(self.hass, "sensor", VALID_CONFIG) + self.hass.block_till_done() state = self.hass.states.get("sensor.next_bus") assert state.state == "16" assert state.attributes["stop_id"] == "209516" diff --git a/tests/components/trend/test_binary_sensor.py b/tests/components/trend/test_binary_sensor.py index fc93df0aacf..0f64afe27cd 100644 --- a/tests/components/trend/test_binary_sensor.py +++ b/tests/components/trend/test_binary_sensor.py @@ -35,6 +35,7 @@ class TestTrendBinarySensor: } }, ) + self.hass.block_till_done() self.hass.states.set("sensor.test_state", "1") self.hass.block_till_done() @@ -62,6 +63,7 @@ class TestTrendBinarySensor: } }, ) + self.hass.block_till_done() now = dt_util.utcnow() for val in [10, 0, 20, 30]: @@ -103,6 +105,7 @@ class TestTrendBinarySensor: } }, ) + self.hass.block_till_done() now = dt_util.utcnow() for val in [30, 20, 30, 10]: @@ -137,6 +140,7 @@ class TestTrendBinarySensor: } }, ) + self.hass.block_till_done() self.hass.states.set("sensor.test_state", "2") self.hass.block_till_done() @@ -162,6 +166,7 @@ class TestTrendBinarySensor: } }, ) + self.hass.block_till_done() self.hass.states.set("sensor.test_state", "1") self.hass.block_till_done() @@ -187,6 +192,7 @@ class TestTrendBinarySensor: } }, ) + self.hass.block_till_done() self.hass.states.set("sensor.test_state", "2") self.hass.block_till_done() @@ -212,6 +218,7 @@ class TestTrendBinarySensor: } }, ) + self.hass.block_till_done() self.hass.states.set("sensor.test_state", "State", {"attr": "1"}) self.hass.block_till_done() self.hass.states.set("sensor.test_state", "State", {"attr": "2"}) @@ -236,6 +243,7 @@ class TestTrendBinarySensor: } }, ) + self.hass.block_till_done() self.hass.states.set("sensor.test_state", "State", {"attr": "2"}) self.hass.block_till_done() @@ -262,6 +270,7 @@ class TestTrendBinarySensor: } }, ) + self.hass.block_till_done() for val in [0, 1, 2, 3, 2, 1]: self.hass.states.set("sensor.test_state", val) @@ -285,6 +294,7 @@ class TestTrendBinarySensor: } }, ) + self.hass.block_till_done() self.hass.states.set("sensor.test_state", "Non") self.hass.block_till_done() @@ -310,6 +320,7 @@ class TestTrendBinarySensor: } }, ) + self.hass.block_till_done() self.hass.states.set("sensor.test_state", "State", {"attr": "2"}) self.hass.block_till_done() diff --git a/tests/components/twitch/test_twitch.py b/tests/components/twitch/test_twitch.py index 3e777fa3d03..f66014d6557 100644 --- a/tests/components/twitch/test_twitch.py +++ b/tests/components/twitch/test_twitch.py @@ -3,6 +3,7 @@ from requests import HTTPError from twitch.resources import Channel, Follow, Stream, Subscription, User from homeassistant.components import sensor +from homeassistant.const import CONF_CLIENT_ID from homeassistant.setup import async_setup_component from tests.async_mock import MagicMock, patch @@ -11,14 +12,14 @@ ENTITY_ID = "sensor.channel123" CONFIG = { sensor.DOMAIN: { "platform": "twitch", - "client_id": "1234", + CONF_CLIENT_ID: "1234", "channels": ["channel123"], } } CONFIG_WITH_OAUTH = { sensor.DOMAIN: { "platform": "twitch", - "client_id": "1234", + CONF_CLIENT_ID: "1234", "channels": ["channel123"], "token": "9876", } @@ -54,6 +55,7 @@ async def test_init(hass): "homeassistant.components.twitch.sensor.TwitchClient", return_value=twitch_mock ): assert await async_setup_component(hass, sensor.DOMAIN, CONFIG) is True + await hass.async_block_till_done() sensor_state = hass.states.get(ENTITY_ID) assert sensor_state.state == "offline" @@ -76,6 +78,7 @@ async def test_offline(hass): "homeassistant.components.twitch.sensor.TwitchClient", return_value=twitch_mock, ): assert await async_setup_component(hass, sensor.DOMAIN, CONFIG) is True + await hass.async_block_till_done() sensor_state = hass.states.get(ENTITY_ID) assert sensor_state.state == "offline" @@ -94,6 +97,7 @@ async def test_streaming(hass): "homeassistant.components.twitch.sensor.TwitchClient", return_value=twitch_mock, ): assert await async_setup_component(hass, sensor.DOMAIN, CONFIG) is True + await hass.async_block_till_done() sensor_state = hass.states.get(ENTITY_ID) assert sensor_state.state == "streaming" @@ -116,9 +120,8 @@ async def test_oauth_without_sub_and_follow(hass): with patch( "homeassistant.components.twitch.sensor.TwitchClient", return_value=twitch_mock, ): - assert ( - await async_setup_component(hass, sensor.DOMAIN, CONFIG_WITH_OAUTH) is True - ) + assert await async_setup_component(hass, sensor.DOMAIN, CONFIG_WITH_OAUTH) + await hass.async_block_till_done() sensor_state = hass.states.get(ENTITY_ID) assert sensor_state.attributes["subscribed"] is False @@ -139,9 +142,8 @@ async def test_oauth_with_sub(hass): with patch( "homeassistant.components.twitch.sensor.TwitchClient", return_value=twitch_mock, ): - assert ( - await async_setup_component(hass, sensor.DOMAIN, CONFIG_WITH_OAUTH) is True - ) + assert await async_setup_component(hass, sensor.DOMAIN, CONFIG_WITH_OAUTH) + await hass.async_block_till_done() sensor_state = hass.states.get(ENTITY_ID) assert sensor_state.attributes["subscribed"] is True @@ -164,9 +166,8 @@ async def test_oauth_with_follow(hass): with patch( "homeassistant.components.twitch.sensor.TwitchClient", return_value=twitch_mock, ): - assert ( - await async_setup_component(hass, sensor.DOMAIN, CONFIG_WITH_OAUTH) is True - ) + assert await async_setup_component(hass, sensor.DOMAIN, CONFIG_WITH_OAUTH) + await hass.async_block_till_done() sensor_state = hass.states.get(ENTITY_ID) assert sensor_state.attributes["subscribed"] is False diff --git a/tests/components/uk_transport/test_sensor.py b/tests/components/uk_transport/test_sensor.py index b8eddb9c785..7385592fe93 100644 --- a/tests/components/uk_transport/test_sensor.py +++ b/tests/components/uk_transport/test_sensor.py @@ -61,6 +61,7 @@ class TestUkTransportSensor(unittest.TestCase): uri = re.compile(UkTransportSensor.TRANSPORT_API_URL_BASE + "*") mock_req.get(uri, text=load_fixture("uk_transport_bus.json")) assert setup_component(self.hass, "sensor", {"sensor": self.config}) + self.hass.block_till_done() bus_state = self.hass.states.get("sensor.next_bus_to_wantage") @@ -85,6 +86,7 @@ class TestUkTransportSensor(unittest.TestCase): uri = re.compile(UkTransportSensor.TRANSPORT_API_URL_BASE + "*") mock_req.get(uri, text=load_fixture("uk_transport_train.json")) assert setup_component(self.hass, "sensor", {"sensor": self.config}) + self.hass.block_till_done() train_state = self.hass.states.get("sensor.next_train_to_WAT") diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 33f296478c8..8f0df236c75 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -293,7 +293,7 @@ async def test_tracked_devices(hass): device_2 = hass.states.get("device_tracker.device_2") assert device_2.state == "home" - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=40)) + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=90)) await hass.async_block_till_done() device_1 = hass.states.get("device_tracker.device_1") diff --git a/tests/components/usgs_earthquakes_feed/test_geo_location.py b/tests/components/usgs_earthquakes_feed/test_geo_location.py index 9bd718d1933..cfd8ae40bcd 100644 --- a/tests/components/usgs_earthquakes_feed/test_geo_location.py +++ b/tests/components/usgs_earthquakes_feed/test_geo_location.py @@ -121,6 +121,7 @@ async def test_setup(hass): ) with assert_setup_component(1, geo_location.DOMAIN): assert await async_setup_component(hass, geo_location.DOMAIN, CONFIG) + await hass.async_block_till_done() # Artificially trigger update. hass.bus.async_fire(EVENT_HOMEASSISTANT_START) # Collect events. @@ -228,6 +229,7 @@ async def test_setup_with_custom_location(hass): assert await async_setup_component( hass, geo_location.DOMAIN, CONFIG_WITH_CUSTOM_LOCATION ) + await hass.async_block_till_done() # Artificially trigger update. hass.bus.async_fire(EVENT_HOMEASSISTANT_START) diff --git a/tests/components/uvc/test_camera.py b/tests/components/uvc/test_camera.py index 1556a165447..22df898f006 100644 --- a/tests/components/uvc/test_camera.py +++ b/tests/components/uvc/test_camera.py @@ -54,6 +54,7 @@ class TestUVCSetup(unittest.TestCase): mock_remote.return_value.server_version = (3, 2, 0) assert setup_component(self.hass, "camera", {"camera": config}) + self.hass.block_till_done() assert mock_remote.call_count == 1 assert mock_remote.call_args == mock.call("foo", 123, "secret", ssl=False) @@ -78,6 +79,7 @@ class TestUVCSetup(unittest.TestCase): mock_remote.return_value.server_version = (3, 2, 0) assert setup_component(self.hass, "camera", {"camera": config}) + self.hass.block_till_done() assert mock_remote.call_count == 1 assert mock_remote.call_args == mock.call("foo", 7080, "secret", ssl=False) @@ -102,6 +104,7 @@ class TestUVCSetup(unittest.TestCase): mock_remote.return_value.server_version = (3, 1, 3) assert setup_component(self.hass, "camera", {"camera": config}) + self.hass.block_till_done() assert mock_remote.call_count == 1 assert mock_remote.call_args == mock.call("foo", 7080, "secret", ssl=False) @@ -116,14 +119,20 @@ class TestUVCSetup(unittest.TestCase): def test_setup_incomplete_config(self, mock_uvc): """Test the setup with incomplete configuration.""" assert setup_component(self.hass, "camera", {"platform": "uvc", "nvr": "foo"}) + self.hass.block_till_done() + assert not mock_uvc.called assert setup_component( self.hass, "camera", {"platform": "uvc", "key": "secret"} ) + self.hass.block_till_done() + assert not mock_uvc.called assert setup_component( self.hass, "camera", {"platform": "uvc", "port": "invalid"} ) + self.hass.block_till_done() + assert not mock_uvc.called @mock.patch.object(uvc, "UnifiVideoCamera") @@ -133,6 +142,8 @@ class TestUVCSetup(unittest.TestCase): config = {"platform": "uvc", "nvr": "foo", "key": "secret"} mock_remote.return_value.index.side_effect = error assert setup_component(self.hass, "camera", {"camera": config}) + self.hass.block_till_done() + assert not mock_uvc.called def test_setup_nvr_error_during_indexing_notauthorized(self): @@ -157,6 +168,8 @@ class TestUVCSetup(unittest.TestCase): mock_remote.return_value = None mock_remote.side_effect = error assert setup_component(self.hass, "camera", {"camera": config}) + self.hass.block_till_done() + assert not mock_remote.index.called assert not mock_uvc.called diff --git a/tests/components/vera/common.py b/tests/components/vera/common.py index 5574c93c515..31e7c706ec9 100644 --- a/tests/components/vera/common.py +++ b/tests/components/vera/common.py @@ -2,13 +2,13 @@ from typing import Callable, Dict, NamedTuple, Tuple -from mock import MagicMock import pyvera as pv from homeassistant.components.vera.const import CONF_CONTROLLER, DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from tests.async_mock import MagicMock from tests.common import MockConfigEntry SetupCallback = Callable[[pv.VeraController, dict], None] diff --git a/tests/components/vera/conftest.py b/tests/components/vera/conftest.py index 2c15d3e4182..193c0733736 100644 --- a/tests/components/vera/conftest.py +++ b/tests/components/vera/conftest.py @@ -1,10 +1,11 @@ """Fixtures for tests.""" -from mock import patch import pytest from .common import ComponentFactory +from tests.async_mock import patch + @pytest.fixture() def vera_component_factory(): diff --git a/tests/components/vera/test_config_flow.py b/tests/components/vera/test_config_flow.py index 3915d4d0577..793e313125c 100644 --- a/tests/components/vera/test_config_flow.py +++ b/tests/components/vera/test_config_flow.py @@ -1,5 +1,4 @@ """Vera tests.""" -from mock import patch from requests.exceptions import RequestException from homeassistant import config_entries, data_entry_flow @@ -12,7 +11,7 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_FORM, ) -from tests.async_mock import MagicMock +from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry diff --git a/tests/components/vizio/test_config_flow.py b/tests/components/vizio/test_config_flow.py index 0c057ba1a71..2b1908dc770 100644 --- a/tests/components/vizio/test_config_flow.py +++ b/tests/components/vizio/test_config_flow.py @@ -316,7 +316,7 @@ async def test_user_error_on_could_not_connect( ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "cant_connect"} + assert result["errors"] == {"base": "cannot_connect"} async def test_user_tv_pairing_no_apps( @@ -363,7 +363,7 @@ async def test_user_start_pairing_failure( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" - assert result["errors"] == {"base": "cant_connect"} + assert result["errors"] == {"base": "cannot_connect"} async def test_user_invalid_pin( @@ -471,7 +471,7 @@ async def test_import_entity_already_configured( ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_setup" + assert result["reason"] == "already_configured_device" async def test_import_flow_update_options( @@ -778,7 +778,7 @@ async def test_zeroconf_flow_already_configured( # Flow should abort because device is already setup assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_setup" + assert result["reason"] == "already_configured_device" async def test_zeroconf_dupe_fail( diff --git a/tests/components/wake_on_lan/test_switch.py b/tests/components/wake_on_lan/test_switch.py index ed4045bcb44..d99bc1ccb4f 100644 --- a/tests/components/wake_on_lan/test_switch.py +++ b/tests/components/wake_on_lan/test_switch.py @@ -57,6 +57,7 @@ class TestWolSwitch(unittest.TestCase): } }, ) + self.hass.block_till_done() state = self.hass.states.get("switch.wake_on_lan") assert STATE_OFF == state.state @@ -93,6 +94,7 @@ class TestWolSwitch(unittest.TestCase): } }, ) + self.hass.block_till_done() state = self.hass.states.get("switch.wake_on_lan") assert STATE_OFF == state.state @@ -130,6 +132,7 @@ class TestWolSwitch(unittest.TestCase): } }, ) + self.hass.block_till_done() state = self.hass.states.get("switch.wake_on_lan") assert STATE_OFF == state.state @@ -155,6 +158,7 @@ class TestWolSwitch(unittest.TestCase): } }, ) + self.hass.block_till_done() calls = mock_service(self.hass, "shell_command", "turn_off_target") state = self.hass.states.get("switch.wake_on_lan") @@ -196,6 +200,7 @@ class TestWolSwitch(unittest.TestCase): } }, ) + self.hass.block_till_done() state = self.hass.states.get("switch.wake_on_lan") assert STATE_OFF == state.state diff --git a/tests/components/weather/test_weather.py b/tests/components/weather/test_weather.py index fd960b594a0..d026cd6ee86 100644 --- a/tests/components/weather/test_weather.py +++ b/tests/components/weather/test_weather.py @@ -32,6 +32,7 @@ class TestWeather(unittest.TestCase): assert setup_component( self.hass, weather.DOMAIN, {"weather": {"platform": "demo"}} ) + self.hass.block_till_done() def tearDown(self): """Stop down everything that was started.""" diff --git a/tests/components/webostv/test_media_player.py b/tests/components/webostv/test_media_player.py index 099927c6c1f..3b213303281 100644 --- a/tests/components/webostv/test_media_player.py +++ b/tests/components/webostv/test_media_player.py @@ -12,6 +12,7 @@ from homeassistant.components.media_player.const import ( from homeassistant.components.webostv.const import ( ATTR_BUTTON, ATTR_COMMAND, + ATTR_PAYLOAD, DOMAIN, SERVICE_BUTTON, SERVICE_COMMAND, @@ -102,8 +103,7 @@ async def test_button(hass, client): async def test_command(hass, client): - """Test generic button functionality.""" - + """Test generic command functionality.""" await setup_webostv(hass) data = { @@ -113,4 +113,21 @@ async def test_command(hass, client): await hass.services.async_call(DOMAIN, SERVICE_COMMAND, data) await hass.async_block_till_done() - client.request.assert_called_with("test") + client.request.assert_called_with("test", payload=None) + + +async def test_command_with_optional_arg(hass, client): + """Test generic command functionality.""" + await setup_webostv(hass) + + data = { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_COMMAND: "test", + ATTR_PAYLOAD: {"target": "https://www.google.com"}, + } + await hass.services.async_call(DOMAIN, SERVICE_COMMAND, data) + await hass.async_block_till_done() + + client.request.assert_called_with( + "test", payload={"target": "https://www.google.com"} + ) diff --git a/tests/components/websocket_api/conftest.py b/tests/components/websocket_api/conftest.py index 93538f2b00b..016fdfebc11 100644 --- a/tests/components/websocket_api/conftest.py +++ b/tests/components/websocket_api/conftest.py @@ -16,6 +16,7 @@ async def websocket_client(hass, hass_ws_client): async def no_auth_websocket_client(hass, aiohttp_client): """Websocket connection that requires authentication.""" assert await async_setup_component(hass, "websocket_api", {}) + await hass.async_block_till_done() client = await aiohttp_client(hass.http.app) ws = await client.ws_connect(URL) @@ -23,6 +24,7 @@ async def no_auth_websocket_client(hass, aiohttp_client): auth_ok = await ws.receive_json() assert auth_ok["type"] == TYPE_AUTH_REQUIRED + ws.client = client yield ws if not ws.closed: diff --git a/tests/components/websocket_api/test_auth.py b/tests/components/websocket_api/test_auth.py index 2a0bc9f8c5a..c0313794783 100644 --- a/tests/components/websocket_api/test_auth.py +++ b/tests/components/websocket_api/test_auth.py @@ -1,6 +1,8 @@ """Test auth of websocket API.""" from unittest.mock import patch +import pytest + from homeassistant.components.websocket_api.auth import ( TYPE_AUTH, TYPE_AUTH_INVALID, @@ -12,33 +14,51 @@ from homeassistant.components.websocket_api.const import ( SIGNAL_WEBSOCKET_DISCONNECTED, URL, ) +from homeassistant.core import callback from homeassistant.setup import async_setup_component from tests.common import mock_coro -async def test_auth_events( - hass, no_auth_websocket_client, legacy_auth, hass_access_token -): - """Test authenticating.""" +@pytest.fixture +def track_connected(hass): + """Track connected and disconnected events.""" connected_evt = [] + + @callback + def track_connected(): + connected_evt.append(1) + hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_WEBSOCKET_CONNECTED, lambda: connected_evt.append(1) + SIGNAL_WEBSOCKET_CONNECTED, track_connected ) disconnected_evt = [] + + @callback + def track_disconnected(): + disconnected_evt.append(1) + hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_WEBSOCKET_DISCONNECTED, lambda: disconnected_evt.append(1) + SIGNAL_WEBSOCKET_DISCONNECTED, track_disconnected ) + return {"connected": connected_evt, "disconnected": disconnected_evt} + + +async def test_auth_events( + hass, no_auth_websocket_client, legacy_auth, hass_access_token, track_connected +): + """Test authenticating.""" + await test_auth_active_with_token(hass, no_auth_websocket_client, hass_access_token) - assert len(connected_evt) == 1 - assert not disconnected_evt + assert len(track_connected["connected"]) == 1 + assert not track_connected["disconnected"] await no_auth_websocket_client.close() await hass.async_block_till_done() - assert len(disconnected_evt) == 1 + assert len(track_connected["disconnected"]) == 1 async def test_auth_via_msg_incorrect_pass(no_auth_websocket_client): @@ -58,27 +78,18 @@ async def test_auth_via_msg_incorrect_pass(no_auth_websocket_client): assert msg["message"] == "Invalid access token or password" -async def test_auth_events_incorrect_pass(hass, no_auth_websocket_client): +async def test_auth_events_incorrect_pass(no_auth_websocket_client, track_connected): """Test authenticating.""" - connected_evt = [] - hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_WEBSOCKET_CONNECTED, lambda: connected_evt.append(1) - ) - disconnected_evt = [] - hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_WEBSOCKET_DISCONNECTED, lambda: disconnected_evt.append(1) - ) await test_auth_via_msg_incorrect_pass(no_auth_websocket_client) - assert not connected_evt - assert not disconnected_evt + assert not track_connected["connected"] + assert not track_connected["disconnected"] await no_auth_websocket_client.close() - await hass.async_block_till_done() - assert not connected_evt - assert not disconnected_evt + assert not track_connected["connected"] + assert not track_connected["disconnected"] async def test_pre_auth_only_auth_allowed(no_auth_websocket_client): @@ -102,13 +113,11 @@ async def test_auth_active_with_token( hass, no_auth_websocket_client, hass_access_token ): """Test authenticating with a token.""" - assert await async_setup_component(hass, "websocket_api", {}) - await no_auth_websocket_client.send_json( {"type": TYPE_AUTH, "access_token": hass_access_token} ) - auth_msg = await no_auth_websocket_client.receive_json() + assert auth_msg["type"] == TYPE_AUTH_OK @@ -117,6 +126,7 @@ async def test_auth_active_user_inactive(hass, aiohttp_client, hass_access_token refresh_token = await hass.auth.async_validate_access_token(hass_access_token) refresh_token.user.is_active = False assert await async_setup_component(hass, "websocket_api", {}) + await hass.async_block_till_done() client = await aiohttp_client(hass.http.app) @@ -133,6 +143,7 @@ async def test_auth_active_user_inactive(hass, aiohttp_client, hass_access_token async def test_auth_active_with_password_not_allow(hass, aiohttp_client): """Test authenticating with a token.""" assert await async_setup_component(hass, "websocket_api", {}) + await hass.async_block_till_done() client = await aiohttp_client(hass.http.app) @@ -149,6 +160,7 @@ async def test_auth_active_with_password_not_allow(hass, aiohttp_client): async def test_auth_legacy_support_with_password(hass, aiohttp_client, legacy_auth): """Test authenticating with a token.""" assert await async_setup_component(hass, "websocket_api", {}) + await hass.async_block_till_done() client = await aiohttp_client(hass.http.app) @@ -165,6 +177,7 @@ async def test_auth_legacy_support_with_password(hass, aiohttp_client, legacy_au async def test_auth_with_invalid_token(hass, aiohttp_client): """Test authenticating with a token.""" assert await async_setup_component(hass, "websocket_api", {}) + await hass.async_block_till_done() client = await aiohttp_client(hass.http.app) diff --git a/tests/components/websocket_api/test_sensor.py b/tests/components/websocket_api/test_sensor.py index 2c711737851..429876cd365 100644 --- a/tests/components/websocket_api/test_sensor.py +++ b/tests/components/websocket_api/test_sensor.py @@ -1,30 +1,37 @@ """Test cases for the API stream sensor.""" from homeassistant.bootstrap import async_setup_component +from homeassistant.components.websocket_api.auth import TYPE_AUTH_REQUIRED +from homeassistant.components.websocket_api.http import URL from .test_auth import test_auth_active_with_token -from tests.common import assert_setup_component - -async def test_websocket_api( - hass, no_auth_websocket_client, hass_access_token, legacy_auth -): +async def test_websocket_api(hass, aiohttp_client, hass_access_token, legacy_auth): """Test API streams.""" - with assert_setup_component(1): - await async_setup_component( - hass, "sensor", {"sensor": {"platform": "websocket_api"}} - ) + await async_setup_component( + hass, "sensor", {"sensor": {"platform": "websocket_api"}} + ) + await hass.async_block_till_done() + + client = await aiohttp_client(hass.http.app) + ws = await client.ws_connect(URL) + + auth_ok = await ws.receive_json() + + assert auth_ok["type"] == TYPE_AUTH_REQUIRED + + ws.client = client state = hass.states.get("sensor.connected_clients") assert state.state == "0" - await test_auth_active_with_token(hass, no_auth_websocket_client, hass_access_token) + await test_auth_active_with_token(hass, ws, hass_access_token) state = hass.states.get("sensor.connected_clients") assert state.state == "1" - await no_auth_websocket_client.close() + await ws.close() await hass.async_block_till_done() state = hass.states.get("sensor.connected_clients") diff --git a/tests/components/wiffi/test_config_flow.py b/tests/components/wiffi/test_config_flow.py index ef6ce528623..5c3e96eb959 100644 --- a/tests/components/wiffi/test_config_flow.py +++ b/tests/components/wiffi/test_config_flow.py @@ -4,15 +4,19 @@ import errno from asynctest import patch import pytest -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.components.wiffi.const import DOMAIN -from homeassistant.const import CONF_PORT +from homeassistant.const import CONF_PORT, CONF_TIMEOUT from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM, ) +from tests.common import MockConfigEntry + +MOCK_CONFIG = {CONF_PORT: 8765} + @pytest.fixture(name="dummy_tcp_server") def mock_dummy_tcp_server(): @@ -78,7 +82,7 @@ async def test_form(hass, dummy_tcp_server): assert result["step_id"] == config_entries.SOURCE_USER result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_PORT: 8765}, + result["flow_id"], user_input=MOCK_CONFIG, ) assert result2["type"] == RESULT_TYPE_CREATE_ENTRY @@ -90,7 +94,7 @@ async def test_form_addr_in_use(hass, addr_in_use): ) result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_PORT: 8765}, + result["flow_id"], user_input=MOCK_CONFIG, ) assert result2["type"] == RESULT_TYPE_ABORT assert result2["reason"] == "addr_in_use" @@ -103,7 +107,28 @@ async def test_form_start_server_failed(hass, start_server_failed): ) result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_PORT: 8765}, + result["flow_id"], user_input=MOCK_CONFIG, ) assert result2["type"] == RESULT_TYPE_ABORT assert result2["reason"] == "start_server_failed" + + +async def test_option_flow(hass): + """Test option flow.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG) + entry.add_to_hass(hass) + + assert not entry.options + + result = await hass.config_entries.options.async_init(entry.entry_id, data=None) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_TIMEOUT: 9} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "" + assert result["data"][CONF_TIMEOUT] == 9 diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index fc30820d5d1..ca3fef6159e 100644 --- a/tests/components/withings/common.py +++ b/tests/components/withings/common.py @@ -19,7 +19,13 @@ import homeassistant.components.http as http import homeassistant.components.withings.const as const from homeassistant.config import async_process_ha_core_config from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_METRIC +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_EXTERNAL_URL, + CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_METRIC, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.setup import async_setup_component @@ -51,13 +57,16 @@ async def setup_hass(hass: HomeAssistant) -> dict: profiles = ["Person0", "Person1", "Person2", "Person3", "Person4"] hass_config = { - "homeassistant": {CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC}, - api.DOMAIN: {"base_url": "http://localhost/"}, + "homeassistant": { + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + CONF_EXTERNAL_URL: "http://example.local/", + }, + api.DOMAIN: {}, http.DOMAIN: {"server_port": 8080}, const.DOMAIN: { - const.CLIENT_ID: "my_client_id", - const.CLIENT_SECRET: "my_client_secret", - const.PROFILES: profiles, + CONF_CLIENT_ID: "my_client_id", + CONF_CLIENT_SECRET: "my_client_secret", + const.CONF_PROFILES: profiles, }, } diff --git a/tests/components/withings/test_init.py b/tests/components/withings/test_init.py index 7d61c74c50a..b65e175913d 100644 --- a/tests/components/withings/test_init.py +++ b/tests/components/withings/test_init.py @@ -15,7 +15,7 @@ from homeassistant.components.withings import ( const, ) from homeassistant.config import async_process_ha_core_config -from homeassistant.const import STATE_UNKNOWN +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, STATE_UNKNOWN from homeassistant.core import HomeAssistant from .common import ( @@ -55,9 +55,9 @@ def test_config_schema_basic_config() -> None: """Test schema.""" config_schema_validate( { - const.CLIENT_ID: "my_client_id", - const.CLIENT_SECRET: "my_client_secret", - const.PROFILES: ["Person 1", "Person 2"], + CONF_CLIENT_ID: "my_client_id", + CONF_CLIENT_SECRET: "my_client_secret", + const.CONF_PROFILES: ["Person 1", "Person 2"], } ) @@ -66,22 +66,22 @@ def test_config_schema_client_id() -> None: """Test schema.""" config_schema_assert_fail( { - const.CLIENT_SECRET: "my_client_secret", - const.PROFILES: ["Person 1", "Person 2"], + CONF_CLIENT_SECRET: "my_client_secret", + const.CONF_PROFILES: ["Person 1", "Person 2"], } ) config_schema_assert_fail( { - const.CLIENT_SECRET: "my_client_secret", - const.CLIENT_ID: "", - const.PROFILES: ["Person 1"], + CONF_CLIENT_SECRET: "my_client_secret", + CONF_CLIENT_ID: "", + const.CONF_PROFILES: ["Person 1"], } ) config_schema_validate( { - const.CLIENT_SECRET: "my_client_secret", - const.CLIENT_ID: "my_client_id", - const.PROFILES: ["Person 1"], + CONF_CLIENT_SECRET: "my_client_secret", + CONF_CLIENT_ID: "my_client_id", + const.CONF_PROFILES: ["Person 1"], } ) @@ -89,20 +89,20 @@ def test_config_schema_client_id() -> None: def test_config_schema_client_secret() -> None: """Test schema.""" config_schema_assert_fail( - {const.CLIENT_ID: "my_client_id", const.PROFILES: ["Person 1"]} + {CONF_CLIENT_ID: "my_client_id", const.CONF_PROFILES: ["Person 1"]} ) config_schema_assert_fail( { - const.CLIENT_ID: "my_client_id", - const.CLIENT_SECRET: "", - const.PROFILES: ["Person 1"], + CONF_CLIENT_ID: "my_client_id", + CONF_CLIENT_SECRET: "", + const.CONF_PROFILES: ["Person 1"], } ) config_schema_validate( { - const.CLIENT_ID: "my_client_id", - const.CLIENT_SECRET: "my_client_secret", - const.PROFILES: ["Person 1"], + CONF_CLIENT_ID: "my_client_id", + CONF_CLIENT_SECRET: "my_client_secret", + const.CONF_PROFILES: ["Person 1"], } ) @@ -110,41 +110,41 @@ def test_config_schema_client_secret() -> None: def test_config_schema_profiles() -> None: """Test schema.""" config_schema_assert_fail( - {const.CLIENT_ID: "my_client_id", const.CLIENT_SECRET: "my_client_secret"} + {CONF_CLIENT_ID: "my_client_id", CONF_CLIENT_SECRET: "my_client_secret"} ) config_schema_assert_fail( { - const.CLIENT_ID: "my_client_id", - const.CLIENT_SECRET: "my_client_secret", - const.PROFILES: "", + CONF_CLIENT_ID: "my_client_id", + CONF_CLIENT_SECRET: "my_client_secret", + const.CONF_PROFILES: "", } ) config_schema_assert_fail( { - const.CLIENT_ID: "my_client_id", - const.CLIENT_SECRET: "my_client_secret", - const.PROFILES: [], + CONF_CLIENT_ID: "my_client_id", + CONF_CLIENT_SECRET: "my_client_secret", + const.CONF_PROFILES: [], } ) config_schema_assert_fail( { - const.CLIENT_ID: "my_client_id", - const.CLIENT_SECRET: "my_client_secret", - const.PROFILES: ["Person 1", "Person 1"], + CONF_CLIENT_ID: "my_client_id", + CONF_CLIENT_SECRET: "my_client_secret", + const.CONF_PROFILES: ["Person 1", "Person 1"], } ) config_schema_validate( { - const.CLIENT_ID: "my_client_id", - const.CLIENT_SECRET: "my_client_secret", - const.PROFILES: ["Person 1"], + CONF_CLIENT_ID: "my_client_id", + CONF_CLIENT_SECRET: "my_client_secret", + const.CONF_PROFILES: ["Person 1"], } ) config_schema_validate( { - const.CLIENT_ID: "my_client_id", - const.CLIENT_SECRET: "my_client_secret", - const.PROFILES: ["Person 1", "Person 2"], + CONF_CLIENT_ID: "my_client_id", + CONF_CLIENT_SECRET: "my_client_secret", + const.CONF_PROFILES: ["Person 1", "Person 2"], } ) @@ -163,7 +163,7 @@ async def test_upgrade_token( ) -> None: """Test upgrading from old config data format to new one.""" config = await setup_hass(hass) - profiles = config[const.DOMAIN][const.PROFILES] + profiles = config[const.DOMAIN][const.CONF_PROFILES] await async_process_ha_core_config( hass, {"internal_url": "http://example.local"}, @@ -197,7 +197,7 @@ async def test_upgrade_token( "token_expiry": token.get("expires_at"), "token_type": token.get("type"), "userid": token.get("userid"), - "client_id": token.get("my_client_id"), + CONF_CLIENT_ID: token.get("my_client_id"), "consumer_secret": token.get("my_consumer_secret"), }, }, @@ -228,7 +228,7 @@ async def test_upgrade_token( assert token.get("expires_at") > time.time() assert token.get("type") == "Bearer" assert token.get("userid") == "myuserid" - assert not token.get("client_id") + assert not token.get(CONF_CLIENT_ID) assert not token.get("consumer_secret") @@ -237,7 +237,7 @@ async def test_auth_failure( ) -> None: """Test auth failure.""" config = await setup_hass(hass) - profiles = config[const.DOMAIN][const.PROFILES] + profiles = config[const.DOMAIN][const.CONF_PROFILES] await async_process_ha_core_config( hass, {"internal_url": "http://example.local"}, @@ -276,7 +276,7 @@ async def test_auth_failure( async def test_full_setup(hass: HomeAssistant, aiohttp_client, aioclient_mock) -> None: """Test the whole component lifecycle.""" config = await setup_hass(hass) - profiles = config[const.DOMAIN][const.PROFILES] + profiles = config[const.DOMAIN][const.CONF_PROFILES] await async_process_ha_core_config( hass, {"internal_url": "http://example.local"}, diff --git a/tests/components/wled/__init__.py b/tests/components/wled/__init__.py index f6bd0643450..487ccd9ab9e 100644 --- a/tests/components/wled/__init__.py +++ b/tests/components/wled/__init__.py @@ -1,5 +1,7 @@ """Tests for the WLED integration.""" +import json + from homeassistant.components.wled.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_MAC from homeassistant.core import HomeAssistant @@ -17,27 +19,29 @@ async def init_integration( """Set up the WLED integration in Home Assistant.""" fixture = "wled/rgb.json" if not rgbw else "wled/rgbw.json" + data = json.loads(load_fixture(fixture)) + aioclient_mock.get( "http://192.168.1.123:80/json/", - text=load_fixture(fixture), + json=data, headers={"Content-Type": "application/json"}, ) aioclient_mock.post( "http://192.168.1.123:80/json/state", - json={}, + json=data["state"], headers={"Content-Type": "application/json"}, ) aioclient_mock.get( "http://192.168.1.123:80/json/info", - json={}, + json=data["info"], headers={"Content-Type": "application/json"}, ) aioclient_mock.get( "http://192.168.1.123:80/json/state", - json={}, + json=data["state"], headers={"Content-Type": "application/json"}, ) diff --git a/tests/components/wled/test_config_flow.py b/tests/components/wled/test_config_flow.py index 6de14a024d4..f5f1ec3099c 100644 --- a/tests/components/wled/test_config_flow.py +++ b/tests/components/wled/test_config_flow.py @@ -1,5 +1,6 @@ """Tests for the WLED config flow.""" import aiohttp +from wled import WLEDConnectionError from homeassistant import data_entry_flow from homeassistant.components.wled import config_flow @@ -9,6 +10,7 @@ from homeassistant.core import HomeAssistant from . import init_integration +from tests.async_mock import MagicMock, patch from tests.common import load_fixture from tests.test_util.aiohttp import AiohttpClientMocker @@ -59,8 +61,9 @@ async def test_show_zerconf_form( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM +@patch("homeassistant.components.wled.WLED.update", side_effect=WLEDConnectionError) async def test_connection_error( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we show user form on WLED connection error.""" aioclient_mock.get("http://example.com/json/", exc=aiohttp.ClientError) @@ -76,8 +79,9 @@ async def test_connection_error( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM +@patch("homeassistant.components.wled.WLED.update", side_effect=WLEDConnectionError) async def test_zeroconf_connection_error( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort zeroconf flow on WLED connection error.""" aioclient_mock.get("http://192.168.1.123/json/", exc=aiohttp.ClientError) @@ -92,8 +96,9 @@ async def test_zeroconf_connection_error( assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT +@patch("homeassistant.components.wled.WLED.update", side_effect=WLEDConnectionError) async def test_zeroconf_confirm_connection_error( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort zeroconf flow on WLED connection error.""" aioclient_mock.get("http://192.168.1.123:80/json/", exc=aiohttp.ClientError) @@ -112,8 +117,9 @@ async def test_zeroconf_confirm_connection_error( assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT +@patch("homeassistant.components.wled.WLED.update", side_effect=WLEDConnectionError) async def test_zeroconf_no_data( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort if zeroconf provides no data.""" flow = config_flow.WLEDFlowHandler() diff --git a/tests/components/wled/test_init.py b/tests/components/wled/test_init.py index 053c5ebaca0..8ed043530ae 100644 --- a/tests/components/wled/test_init.py +++ b/tests/components/wled/test_init.py @@ -1,20 +1,20 @@ """Tests for the WLED integration.""" -import aiohttp +from wled import WLEDConnectionError from homeassistant.components.wled.const import DOMAIN from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY from homeassistant.core import HomeAssistant +from tests.async_mock import MagicMock, patch from tests.components.wled import init_integration from tests.test_util.aiohttp import AiohttpClientMocker +@patch("homeassistant.components.wled.WLED.update", side_effect=WLEDConnectionError) async def test_config_entry_not_ready( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + mock_update: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test the WLED configuration entry not ready.""" - aioclient_mock.get("http://192.168.1.123:80/json/", exc=aiohttp.ClientError) - entry = await init_integration(hass, aioclient_mock) assert entry.state == ENTRY_STATE_SETUP_RETRY diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py index f2cc5514a2e..35e8ea0f7ef 100644 --- a/tests/components/wled/test_light.py +++ b/tests/components/wled/test_light.py @@ -1,5 +1,7 @@ """Tests for the WLED light platform.""" -import aiohttp +import json + +from wled import Device as WLEDDevice, WLEDConnectionError from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -11,6 +13,7 @@ from homeassistant.components.light import ( ATTR_WHITE_VALUE, DOMAIN as LIGHT_DOMAIN, ) +from homeassistant.components.wled import SCAN_INTERVAL from homeassistant.components.wled.const import ( ATTR_INTENSITY, ATTR_PALETTE, @@ -26,12 +29,15 @@ from homeassistant.const import ( ATTR_ICON, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_OFF, STATE_ON, STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant +import homeassistant.util.dt as dt_util from tests.async_mock import patch +from tests.common import async_fire_time_changed, load_fixture from tests.components.wled import init_integration from tests.test_util.aiohttp import AiohttpClientMocker @@ -45,7 +51,7 @@ async def test_rgb_light_state( entity_registry = await hass.helpers.entity_registry.async_get_registry() # First segment of the strip - state = hass.states.get("light.wled_rgb_light") + state = hass.states.get("light.wled_rgb_light_segment_0") assert state assert state.attributes.get(ATTR_BRIGHTNESS) == 127 assert state.attributes.get(ATTR_EFFECT) == "Solid" @@ -59,12 +65,12 @@ async def test_rgb_light_state( assert state.attributes.get(ATTR_SPEED) == 32 assert state.state == STATE_ON - entry = entity_registry.async_get("light.wled_rgb_light") + entry = entity_registry.async_get("light.wled_rgb_light_segment_0") assert entry assert entry.unique_id == "aabbccddeeff_0" # Second segment of the strip - state = hass.states.get("light.wled_rgb_light_1") + state = hass.states.get("light.wled_rgb_light_segment_1") assert state assert state.attributes.get(ATTR_BRIGHTNESS) == 127 assert state.attributes.get(ATTR_EFFECT) == "Blink" @@ -78,22 +84,32 @@ async def test_rgb_light_state( assert state.attributes.get(ATTR_SPEED) == 16 assert state.state == STATE_ON - entry = entity_registry.async_get("light.wled_rgb_light_1") + entry = entity_registry.async_get("light.wled_rgb_light_segment_1") assert entry assert entry.unique_id == "aabbccddeeff_1" + # Test master control of the lightstrip + state = hass.states.get("light.wled_rgb_light_master") + assert state + assert state.attributes.get(ATTR_BRIGHTNESS) == 127 + assert state.state == STATE_ON -async def test_switch_change_state( + entry = entity_registry.async_get("light.wled_rgb_light_master") + assert entry + assert entry.unique_id == "aabbccddeeff" + + +async def test_segment_change_state( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog ) -> None: - """Test the change of state of the WLED switches.""" + """Test the change of state of the WLED segments.""" await init_integration(hass, aioclient_mock) - with patch("wled.WLED.light") as light_mock: + with patch("wled.WLED.segment") as light_mock: await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_TRANSITION: 5}, + {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_TRANSITION: 5}, blocking=True, ) await hass.async_block_till_done() @@ -101,14 +117,14 @@ async def test_switch_change_state( on=False, segment_id=0, transition=50, ) - with patch("wled.WLED.light") as light_mock: + with patch("wled.WLED.segment") as light_mock: await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, { ATTR_BRIGHTNESS: 42, ATTR_EFFECT: "Chase", - ATTR_ENTITY_ID: "light.wled_rgb_light", + ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_RGB_COLOR: [255, 0, 0], ATTR_TRANSITION: 5, }, @@ -124,11 +140,11 @@ async def test_switch_change_state( transition=50, ) - with patch("wled.WLED.light") as light_mock: + with patch("wled.WLED.segment") as light_mock: await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_COLOR_TEMP: 400}, + {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_COLOR_TEMP: 400}, blocking=True, ) await hass.async_block_till_done() @@ -137,6 +153,180 @@ async def test_switch_change_state( ) +async def test_master_change_state( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog +) -> None: + """Test the change of state of the WLED master light control.""" + await init_integration(hass, aioclient_mock) + + with patch("wled.WLED.master") as light_mock: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.wled_rgb_light_master", ATTR_TRANSITION: 5}, + blocking=True, + ) + await hass.async_block_till_done() + light_mock.assert_called_once_with( + on=False, transition=50, + ) + + with patch("wled.WLED.master") as light_mock: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_BRIGHTNESS: 42, + ATTR_ENTITY_ID: "light.wled_rgb_light_master", + ATTR_TRANSITION: 5, + }, + blocking=True, + ) + await hass.async_block_till_done() + light_mock.assert_called_once_with( + brightness=42, on=True, transition=50, + ) + + with patch("wled.WLED.master") as light_mock: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.wled_rgb_light_master", ATTR_TRANSITION: 5}, + blocking=True, + ) + await hass.async_block_till_done() + light_mock.assert_called_once_with( + on=False, transition=50, + ) + + with patch("wled.WLED.master") as light_mock: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_BRIGHTNESS: 42, + ATTR_ENTITY_ID: "light.wled_rgb_light_master", + ATTR_TRANSITION: 5, + }, + blocking=True, + ) + await hass.async_block_till_done() + light_mock.assert_called_once_with( + brightness=42, on=True, transition=50, + ) + + +async def test_dynamically_handle_segments( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test if a new/deleted segment is dynamically added/removed.""" + await init_integration(hass, aioclient_mock) + + assert hass.states.get("light.wled_rgb_light_master") + assert hass.states.get("light.wled_rgb_light_segment_0") + assert hass.states.get("light.wled_rgb_light_segment_1") + + data = json.loads(load_fixture("wled/rgb_single_segment.json")) + device = WLEDDevice(data) + + # Test removal if segment went missing, including the master entity + with patch( + "homeassistant.components.wled.WLED.update", return_value=device, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + assert hass.states.get("light.wled_rgb_light_segment_0") + assert not hass.states.get("light.wled_rgb_light_segment_1") + assert not hass.states.get("light.wled_rgb_light_master") + + # Test adding if segment shows up again, including the master entity + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + assert hass.states.get("light.wled_rgb_light_master") + assert hass.states.get("light.wled_rgb_light_segment_0") + assert hass.states.get("light.wled_rgb_light_segment_1") + + +async def test_single_segment_behavior( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog +) -> None: + """Test the behavior of the integration with a single segment.""" + await init_integration(hass, aioclient_mock) + + data = json.loads(load_fixture("wled/rgb_single_segment.json")) + device = WLEDDevice(data) + + # Test absent master + with patch( + "homeassistant.components.wled.WLED.update", return_value=device, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + assert not hass.states.get("light.wled_rgb_light_master") + + state = hass.states.get("light.wled_rgb_light_segment_0") + assert state + assert state.state == STATE_ON + + # Test segment brightness takes master into account + device.state.brightness = 100 + device.state.segments[0].brightness = 255 + with patch( + "homeassistant.components.wled.WLED.update", return_value=device, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get("light.wled_rgb_light_segment_0") + assert state + assert state.attributes.get(ATTR_BRIGHTNESS) == 100 + + # Test segment is off when master is off + device.state.on = False + with patch( + "homeassistant.components.wled.WLED.update", return_value=device, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + state = hass.states.get("light.wled_rgb_light_segment_0") + assert state + assert state.state == STATE_OFF + + # Test master is turned off when turning off a single segment + with patch("wled.WLED.master") as master_mock: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_TRANSITION: 5}, + blocking=True, + ) + await hass.async_block_till_done() + master_mock.assert_called_once_with( + on=False, transition=50, + ) + + # Test master is turned on when turning on a single segment, and segment + # brightness is set to 255. + with patch("wled.WLED.master") as master_mock, patch( + "wled.WLED.segment" + ) as segment_mock: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", + ATTR_TRANSITION: 5, + ATTR_BRIGHTNESS: 42, + }, + blocking=True, + ) + await hass.async_block_till_done() + master_mock.assert_called_once_with(on=True, transition=50, brightness=42) + segment_mock.assert_called_once_with(on=True, segment_id=0, brightness=255) + + async def test_light_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog ) -> None: @@ -144,16 +334,16 @@ async def test_light_error( aioclient_mock.post("http://192.168.1.123:80/json/state", text="", status=400) await init_integration(hass, aioclient_mock) - with patch("homeassistant.components.wled.WLEDDataUpdateCoordinator.async_refresh"): + with patch("homeassistant.components.wled.WLED.update"): await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.wled_rgb_light"}, + {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0"}, blocking=True, ) await hass.async_block_till_done() - state = hass.states.get("light.wled_rgb_light") + state = hass.states.get("light.wled_rgb_light_segment_0") assert state.state == STATE_ON assert "Invalid response from API" in caplog.text @@ -162,19 +352,20 @@ async def test_light_connection_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test error handling of the WLED switches.""" - aioclient_mock.post("http://192.168.1.123:80/json/state", exc=aiohttp.ClientError) await init_integration(hass, aioclient_mock) - with patch("homeassistant.components.wled.WLEDDataUpdateCoordinator.async_refresh"): + with patch("homeassistant.components.wled.WLED.update"), patch( + "homeassistant.components.wled.WLED.segment", side_effect=WLEDConnectionError + ): await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.wled_rgb_light"}, + {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0"}, blocking=True, ) await hass.async_block_till_done() - state = hass.states.get("light.wled_rgb_light") + state = hass.states.get("light.wled_rgb_light_segment_0") assert state.state == STATE_UNAVAILABLE @@ -189,7 +380,7 @@ async def test_rgbw_light( assert state.attributes.get(ATTR_HS_COLOR) == (0.0, 100.0) assert state.attributes.get(ATTR_WHITE_VALUE) == 139 - with patch("wled.WLED.light") as light_mock: + with patch("wled.WLED.segment") as light_mock: await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, @@ -201,7 +392,7 @@ async def test_rgbw_light( on=True, segment_id=0, color_primary=(255, 159, 70, 139), ) - with patch("wled.WLED.light") as light_mock: + with patch("wled.WLED.segment") as light_mock: await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, @@ -213,7 +404,7 @@ async def test_rgbw_light( color_primary=(255, 0, 0, 100), on=True, segment_id=0, ) - with patch("wled.WLED.light") as light_mock: + with patch("wled.WLED.segment") as light_mock: await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, @@ -236,13 +427,13 @@ async def test_effect_service( """Test the effect service of a WLED light.""" await init_integration(hass, aioclient_mock) - with patch("wled.WLED.light") as light_mock: + with patch("wled.WLED.segment") as light_mock: await hass.services.async_call( DOMAIN, SERVICE_EFFECT, { ATTR_EFFECT: "Rainbow", - ATTR_ENTITY_ID: "light.wled_rgb_light", + ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_INTENSITY: 200, ATTR_REVERSE: True, ATTR_SPEED: 100, @@ -254,11 +445,11 @@ async def test_effect_service( effect="Rainbow", intensity=200, reverse=True, segment_id=0, speed=100, ) - with patch("wled.WLED.light") as light_mock: + with patch("wled.WLED.segment") as light_mock: await hass.services.async_call( DOMAIN, SERVICE_EFFECT, - {ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_EFFECT: 9}, + {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_EFFECT: 9}, blocking=True, ) await hass.async_block_till_done() @@ -266,12 +457,12 @@ async def test_effect_service( segment_id=0, effect=9, ) - with patch("wled.WLED.light") as light_mock: + with patch("wled.WLED.segment") as light_mock: await hass.services.async_call( DOMAIN, SERVICE_EFFECT, { - ATTR_ENTITY_ID: "light.wled_rgb_light", + ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_INTENSITY: 200, ATTR_REVERSE: True, ATTR_SPEED: 100, @@ -283,13 +474,13 @@ async def test_effect_service( intensity=200, reverse=True, segment_id=0, speed=100, ) - with patch("wled.WLED.light") as light_mock: + with patch("wled.WLED.segment") as light_mock: await hass.services.async_call( DOMAIN, SERVICE_EFFECT, { ATTR_EFFECT: "Rainbow", - ATTR_ENTITY_ID: "light.wled_rgb_light", + ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_REVERSE: True, ATTR_SPEED: 100, }, @@ -300,13 +491,13 @@ async def test_effect_service( effect="Rainbow", reverse=True, segment_id=0, speed=100, ) - with patch("wled.WLED.light") as light_mock: + with patch("wled.WLED.segment") as light_mock: await hass.services.async_call( DOMAIN, SERVICE_EFFECT, { ATTR_EFFECT: "Rainbow", - ATTR_ENTITY_ID: "light.wled_rgb_light", + ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_INTENSITY: 200, ATTR_SPEED: 100, }, @@ -317,13 +508,13 @@ async def test_effect_service( effect="Rainbow", intensity=200, segment_id=0, speed=100, ) - with patch("wled.WLED.light") as light_mock: + with patch("wled.WLED.segment") as light_mock: await hass.services.async_call( DOMAIN, SERVICE_EFFECT, { ATTR_EFFECT: "Rainbow", - ATTR_ENTITY_ID: "light.wled_rgb_light", + ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_INTENSITY: 200, ATTR_REVERSE: True, }, @@ -342,15 +533,15 @@ async def test_effect_service_error( aioclient_mock.post("http://192.168.1.123:80/json/state", text="", status=400) await init_integration(hass, aioclient_mock) - with patch("homeassistant.components.wled.WLEDDataUpdateCoordinator.async_refresh"): + with patch("homeassistant.components.wled.WLED.update"): await hass.services.async_call( DOMAIN, SERVICE_EFFECT, - {ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_EFFECT: 9}, + {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_EFFECT: 9}, blocking=True, ) await hass.async_block_till_done() - state = hass.states.get("light.wled_rgb_light") + state = hass.states.get("light.wled_rgb_light_segment_0") assert state.state == STATE_ON assert "Invalid response from API" in caplog.text diff --git a/tests/components/wled/test_switch.py b/tests/components/wled/test_switch.py index 7d411984cff..388e3317b39 100644 --- a/tests/components/wled/test_switch.py +++ b/tests/components/wled/test_switch.py @@ -1,5 +1,5 @@ """Tests for the WLED switch platform.""" -import aiohttp +from wled import WLEDConnectionError from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.wled.const import ( @@ -142,7 +142,7 @@ async def test_switch_error( aioclient_mock.post("http://192.168.1.123:80/json/state", text="", status=400) await init_integration(hass, aioclient_mock) - with patch("homeassistant.components.wled.WLEDDataUpdateCoordinator.async_refresh"): + with patch("homeassistant.components.wled.WLED.update"): await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, @@ -160,10 +160,11 @@ async def test_switch_connection_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test error handling of the WLED switches.""" - aioclient_mock.post("http://192.168.1.123:80/json/state", exc=aiohttp.ClientError) await init_integration(hass, aioclient_mock) - with patch("homeassistant.components.wled.WLEDDataUpdateCoordinator.async_refresh"): + with patch("homeassistant.components.wled.WLED.update"), patch( + "homeassistant.components.wled.WLED.nightlight", side_effect=WLEDConnectionError + ): await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, diff --git a/tests/components/workday/test_binary_sensor.py b/tests/components/workday/test_binary_sensor.py index c476c9fd0e0..1c4ebb29b5a 100644 --- a/tests/components/workday/test_binary_sensor.py +++ b/tests/components/workday/test_binary_sensor.py @@ -109,6 +109,7 @@ class TestWorkdaySetup: """Set up workday component.""" with assert_setup_component(1, "binary_sensor"): setup_component(self.hass, "binary_sensor", self.config_province) + self.hass.block_till_done() entity = self.hass.states.get("binary_sensor.workday_sensor") assert entity is not None @@ -119,6 +120,7 @@ class TestWorkdaySetup: """Test if workdays are reported correctly.""" with assert_setup_component(1, "binary_sensor"): setup_component(self.hass, "binary_sensor", self.config_province) + self.hass.block_till_done() self.hass.start() @@ -131,6 +133,7 @@ class TestWorkdaySetup: """Test if weekends are reported correctly.""" with assert_setup_component(1, "binary_sensor"): setup_component(self.hass, "binary_sensor", self.config_province) + self.hass.block_till_done() self.hass.start() @@ -143,6 +146,7 @@ class TestWorkdaySetup: """Test if public holidays are reported correctly.""" with assert_setup_component(1, "binary_sensor"): setup_component(self.hass, "binary_sensor", self.config_province) + self.hass.block_till_done() self.hass.start() @@ -153,6 +157,7 @@ class TestWorkdaySetup: """Set up workday component.""" with assert_setup_component(1, "binary_sensor"): setup_component(self.hass, "binary_sensor", self.config_noprovince) + self.hass.block_till_done() entity = self.hass.states.get("binary_sensor.workday_sensor") assert entity is not None @@ -163,6 +168,7 @@ class TestWorkdaySetup: """Test if public holidays are reported correctly.""" with assert_setup_component(1, "binary_sensor"): setup_component(self.hass, "binary_sensor", self.config_noprovince) + self.hass.block_till_done() self.hass.start() diff --git a/tests/components/worldclock/test_sensor.py b/tests/components/worldclock/test_sensor.py index b0e8119035d..3d5fc7ab5a7 100644 --- a/tests/components/worldclock/test_sensor.py +++ b/tests/components/worldclock/test_sensor.py @@ -18,6 +18,7 @@ class TestWorldClockSensor(unittest.TestCase): config = {"sensor": {"platform": "worldclock", "time_zone": "America/New_York"}} assert setup_component(self.hass, "sensor", config) + self.hass.block_till_done() def tearDown(self): """Stop everything that was started.""" diff --git a/tests/components/wunderground/test_sensor.py b/tests/components/wunderground/test_sensor.py index a2f681f8e97..b4fb30d25c5 100644 --- a/tests/components/wunderground/test_sensor.py +++ b/tests/components/wunderground/test_sensor.py @@ -61,6 +61,7 @@ async def test_setup(hass, aioclient_mock): with assert_setup_component(1, "sensor"): await async_setup_component(hass, "sensor", {"sensor": VALID_CONFIG}) + await hass.async_block_till_done() async def test_setup_pws(hass, aioclient_mock): @@ -84,6 +85,7 @@ async def test_sensor(hass, aioclient_mock): aioclient_mock.get(URL, text=load_fixture("wunderground-valid.json")) await async_setup_component(hass, "sensor", {"sensor": VALID_CONFIG}) + await hass.async_block_till_done() state = hass.states.get("sensor.pws_weather") assert state.state == "Clear" @@ -136,6 +138,7 @@ async def test_invalid_data(hass, aioclient_mock): aioclient_mock.get(URL, text=load_fixture("wunderground-invalid.json")) await async_setup_component(hass, "sensor", {"sensor": VALID_CONFIG}) + await hass.async_block_till_done() for condition in VALID_CONFIG["monitored_conditions"]: state = hass.states.get(f"sensor.pws_{condition}") diff --git a/tests/components/wwlln/__init__.py b/tests/components/wwlln/__init__.py deleted file mode 100644 index c44245e5988..00000000000 --- a/tests/components/wwlln/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Define tests for the WWLLN component.""" diff --git a/tests/components/wwlln/conftest.py b/tests/components/wwlln/conftest.py deleted file mode 100644 index 787b68aebcc..00000000000 --- a/tests/components/wwlln/conftest.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Define various utilities for WWLLN tests.""" -import pytest - -from homeassistant.components.wwlln import CONF_WINDOW, DOMAIN -from homeassistant.const import ( - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_RADIUS, - CONF_UNIT_SYSTEM, -) - -from tests.common import MockConfigEntry - - -@pytest.fixture -def config_entry(): - """Create a mock WWLLN config entry.""" - return MockConfigEntry( - domain=DOMAIN, - data={ - CONF_LATITUDE: 39.128712, - CONF_LONGITUDE: -104.9812612, - CONF_RADIUS: 25, - CONF_UNIT_SYSTEM: "metric", - CONF_WINDOW: 3600, - }, - title="39.128712, -104.9812612", - ) diff --git a/tests/components/wwlln/test_config_flow.py b/tests/components/wwlln/test_config_flow.py deleted file mode 100644 index b5d34f542e3..00000000000 --- a/tests/components/wwlln/test_config_flow.py +++ /dev/null @@ -1,135 +0,0 @@ -"""Define tests for the WWLLN config flow.""" -from homeassistant import data_entry_flow -from homeassistant.components.wwlln import ( - CONF_WINDOW, - DATA_CLIENT, - DOMAIN, - async_setup_entry, -) -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS - -from tests.async_mock import patch -from tests.common import MockConfigEntry - - -async def test_duplicate_error(hass, config_entry): - """Test that errors are shown when duplicates are added.""" - conf = {CONF_LATITUDE: 39.128712, CONF_LONGITUDE: -104.9812612, CONF_RADIUS: 25} - - MockConfigEntry( - domain=DOMAIN, unique_id="39.128712, -104.9812612", data=conf - ).add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=conf - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - -async def test_show_form(hass): - """Test that the form is served with no input.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - - -async def test_step_import(hass): - """Test that the import step works.""" - conf = { - CONF_LATITUDE: 39.128712, - CONF_LONGITUDE: -104.9812612, - CONF_RADIUS: 25, - } - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=conf - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "39.128712, -104.9812612" - assert result["data"] == { - CONF_LATITUDE: 39.128712, - CONF_LONGITUDE: -104.9812612, - CONF_RADIUS: 25, - CONF_WINDOW: 3600.0, - } - - -async def test_step_user(hass): - """Test that the user step works.""" - conf = {CONF_LATITUDE: 39.128712, CONF_LONGITUDE: -104.9812612, CONF_RADIUS: 25} - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=conf - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "39.128712, -104.9812612" - assert result["data"] == { - CONF_LATITUDE: 39.128712, - CONF_LONGITUDE: -104.9812612, - CONF_RADIUS: 25, - CONF_WINDOW: 3600.0, - } - - -async def test_different_unit_system(hass): - """Test that the config flow picks up the HASS unit system.""" - conf = { - CONF_LATITUDE: 39.128712, - CONF_LONGITUDE: -104.9812612, - CONF_RADIUS: 25, - } - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=conf - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "39.128712, -104.9812612" - assert result["data"] == { - CONF_LATITUDE: 39.128712, - CONF_LONGITUDE: -104.9812612, - CONF_RADIUS: 25, - CONF_WINDOW: 3600.0, - } - - -async def test_custom_window(hass): - """Test that a custom window is stored correctly.""" - conf = { - CONF_LATITUDE: 39.128712, - CONF_LONGITUDE: -104.9812612, - CONF_RADIUS: 25, - CONF_WINDOW: 7200, - } - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=conf - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "39.128712, -104.9812612" - assert result["data"] == { - CONF_LATITUDE: 39.128712, - CONF_LONGITUDE: -104.9812612, - CONF_RADIUS: 25, - CONF_WINDOW: 7200, - } - - -async def test_component_load_config_entry(hass, config_entry): - """Test that loading an existing config entry yields a client.""" - config_entry.add_to_hass(hass) - with patch.object(hass.config_entries, "async_forward_entry_setup") as forward_mock: - assert await async_setup_entry(hass, config_entry) - - await hass.async_block_till_done() - assert forward_mock.call_count == 1 - assert len(hass.data[DOMAIN][DATA_CLIENT]) == 1 diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index 619293be676..8ae2f424f2e 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -2,19 +2,25 @@ from miio import DeviceException from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.components.xiaomi_miio import config_flow, const from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN from tests.async_mock import Mock, patch +ZEROCONF_NAME = "name" +ZEROCONF_PROP = "properties" +ZEROCONF_MAC = "mac" + TEST_HOST = "1.2.3.4" TEST_TOKEN = "12345678901234567890123456789012" TEST_NAME = "Test_Gateway" TEST_MODEL = "model5" -TEST_MAC = "AB-CD-EF-GH-IJ-KL" -TEST_GATEWAY_ID = f"{TEST_MODEL}-{TEST_MAC}-gateway" +TEST_MAC = "ab:cd:ef:gh:ij:kl" +TEST_GATEWAY_ID = TEST_MAC TEST_HARDWARE_VERSION = "AB123" TEST_FIRMWARE_VERSION = "1.2.3_456" +TEST_ZEROCONF_NAME = "lumi-gateway-v3_miio12345678._miio._udp.local." def get_mock_info( @@ -119,7 +125,83 @@ async def test_config_flow_gateway_success(hass): config_flow.CONF_FLOW_TYPE: config_flow.CONF_GATEWAY, CONF_HOST: TEST_HOST, CONF_TOKEN: TEST_TOKEN, - "gateway_id": TEST_GATEWAY_ID, "model": TEST_MODEL, "mac": TEST_MAC, } + + +async def test_zeroconf_gateway_success(hass): + """Test a successful zeroconf discovery of a gateway.""" + result = await hass.config_entries.flow.async_init( + const.DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data={ + zeroconf.ATTR_HOST: TEST_HOST, + ZEROCONF_NAME: TEST_ZEROCONF_NAME, + ZEROCONF_PROP: {ZEROCONF_MAC: TEST_MAC}, + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "gateway" + assert result["errors"] == {} + + mock_info = get_mock_info() + + with patch( + "homeassistant.components.xiaomi_miio.gateway.gateway.Gateway.info", + return_value=mock_info, + ), patch( + "homeassistant.components.xiaomi_miio.async_setup_entry", return_value=True + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_NAME: TEST_NAME, CONF_TOKEN: TEST_TOKEN}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == TEST_NAME + assert result["data"] == { + config_flow.CONF_FLOW_TYPE: config_flow.CONF_GATEWAY, + CONF_HOST: TEST_HOST, + CONF_TOKEN: TEST_TOKEN, + "model": TEST_MODEL, + "mac": TEST_MAC, + } + + +async def test_zeroconf_unknown_device(hass): + """Test a failed zeroconf discovery because of a unknown device.""" + result = await hass.config_entries.flow.async_init( + const.DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data={ + zeroconf.ATTR_HOST: TEST_HOST, + ZEROCONF_NAME: "not-a-xiaomi-miio-device", + ZEROCONF_PROP: {ZEROCONF_MAC: TEST_MAC}, + }, + ) + + assert result["type"] == "abort" + assert result["reason"] == "not_xiaomi_miio" + + +async def test_zeroconf_no_data(hass): + """Test a failed zeroconf discovery because of no data.""" + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data={} + ) + + assert result["type"] == "abort" + assert result["reason"] == "not_xiaomi_miio" + + +async def test_zeroconf_missing_data(hass): + """Test a failed zeroconf discovery because of missing data.""" + result = await hass.config_entries.flow.async_init( + const.DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data={zeroconf.ATTR_HOST: TEST_HOST, ZEROCONF_NAME: TEST_ZEROCONF_NAME}, + ) + + assert result["type"] == "abort" + assert result["reason"] == "not_xiaomi_miio" diff --git a/tests/components/yamaha/test_media_player.py b/tests/components/yamaha/test_media_player.py index 2f47ddc7355..c0a296bb25b 100644 --- a/tests/components/yamaha/test_media_player.py +++ b/tests/components/yamaha/test_media_player.py @@ -62,6 +62,7 @@ class TestYamahaMediaPlayer(unittest.TestCase): config = {"media_player": {"platform": "yamaha", "host": "127.0.0.1"}} assert setup_component(self.hass, mp.DOMAIN, config) + self.hass.block_till_done() @patch("rxv.RXV") def test_enable_output(self, mock_rxv): diff --git a/tests/components/yandex_transport/test_yandex_transport_sensor.py b/tests/components/yandex_transport/test_yandex_transport_sensor.py index f60e7ba5adf..3583dfa0bdf 100644 --- a/tests/components/yandex_transport/test_yandex_transport_sensor.py +++ b/tests/components/yandex_transport/test_yandex_transport_sensor.py @@ -50,6 +50,7 @@ async def assert_setup_sensor(hass, config, count=1): """Set up the sensor and assert it's been created.""" with assert_setup_component(count): assert await async_setup_component(hass, sensor.DOMAIN, config) + await hass.async_block_till_done() async def test_setup_platform_valid_config(hass, mock_requester): diff --git a/tests/components/yr/test_sensor.py b/tests/components/yr/test_sensor.py index d8dcbe367de..cb0345641b7 100644 --- a/tests/components/yr/test_sensor.py +++ b/tests/components/yr/test_sensor.py @@ -23,6 +23,7 @@ async def test_default_setup(hass, aioclient_mock): "homeassistant.components.yr.sensor.dt_util.utcnow", return_value=NOW ), assert_setup_component(1): await async_setup_component(hass, "sensor", {"sensor": config}) + await hass.async_block_till_done() state = hass.states.get("sensor.yr_symbol") @@ -53,6 +54,7 @@ async def test_custom_setup(hass, aioclient_mock): "homeassistant.components.yr.sensor.dt_util.utcnow", return_value=NOW ), assert_setup_component(1): await async_setup_component(hass, "sensor", {"sensor": config}) + await hass.async_block_till_done() state = hass.states.get("sensor.yr_pressure") assert state.attributes.get("unit_of_measurement") == "hPa" @@ -99,6 +101,7 @@ async def test_forecast_setup(hass, aioclient_mock): "homeassistant.components.yr.sensor.dt_util.utcnow", return_value=NOW ), assert_setup_component(1): await async_setup_component(hass, "sensor", {"sensor": config}) + await hass.async_block_till_done() state = hass.states.get("sensor.yr_pressure") assert state.attributes.get("unit_of_measurement") == "hPa" diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 74069fa5faa..45b1d9b1171 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -40,7 +40,7 @@ def get_service_info_mock(service_type, name): return ServiceInfo( service_type, name, - address=b"\n\x00\x00\x14", + addresses=[b"\n\x00\x00\x14"], port=80, weight=0, priority=0, @@ -56,7 +56,7 @@ def get_homekit_info_mock(model, pairing_status): return ServiceInfo( service_type, name, - address=b"\n\x00\x00\x14", + addresses=[b"\n\x00\x00\x14"], port=80, weight=0, priority=0, diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index f10ee25018f..fd5621137ae 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -75,7 +75,9 @@ def patch_cluster(cluster): cluster.read_attributes = AsyncMock(return_value=[{}, {}]) cluster.read_attributes_raw = Mock() cluster.unbind = AsyncMock(return_value=[0]) - cluster.write_attributes = AsyncMock(return_value=[0]) + cluster.write_attributes = AsyncMock( + return_value=[zcl_f.WriteAttributesResponse.deserialize(b"\x00")[0]] + ) if cluster.cluster_id == 4: cluster.add = AsyncMock(return_value=[0]) diff --git a/tests/components/zha/test_climate.py b/tests/components/zha/test_climate.py new file mode 100644 index 00000000000..8ca949cd44e --- /dev/null +++ b/tests/components/zha/test_climate.py @@ -0,0 +1,1112 @@ +"""Test zha climate.""" + +import pytest +import zigpy.zcl.clusters +from zigpy.zcl.clusters.hvac import Thermostat +import zigpy.zcl.foundation as zcl_f + +from homeassistant.components.climate.const import ( + ATTR_CURRENT_TEMPERATURE, + ATTR_FAN_MODE, + ATTR_FAN_MODES, + ATTR_HVAC_ACTION, + ATTR_HVAC_MODE, + ATTR_HVAC_MODES, + ATTR_PRESET_MODE, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + CURRENT_HVAC_COOL, + CURRENT_HVAC_FAN, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, + FAN_AUTO, + FAN_LOW, + FAN_ON, + HVAC_MODE_AUTO, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + PRESET_AWAY, + PRESET_NONE, + SERVICE_SET_FAN_MODE, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_PRESET_MODE, + SERVICE_SET_TEMPERATURE, +) +from homeassistant.components.zha.climate import ( + DOMAIN, + HVAC_MODE_2_SYSTEM, + SEQ_OF_OPERATION, +) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_UNKNOWN + +from .common import async_enable_traffic, find_entity_id, send_attributes_report + +from tests.async_mock import patch + +CLIMATE = { + 1: { + "device_type": zigpy.profiles.zha.DeviceType.THERMOSTAT, + "in_clusters": [ + zigpy.zcl.clusters.general.Basic.cluster_id, + zigpy.zcl.clusters.general.Identify.cluster_id, + zigpy.zcl.clusters.hvac.Thermostat.cluster_id, + zigpy.zcl.clusters.hvac.UserInterface.cluster_id, + ], + "out_clusters": [zigpy.zcl.clusters.general.Ota.cluster_id], + } +} + +CLIMATE_FAN = { + 1: { + "device_type": zigpy.profiles.zha.DeviceType.THERMOSTAT, + "in_clusters": [ + zigpy.zcl.clusters.general.Basic.cluster_id, + zigpy.zcl.clusters.general.Identify.cluster_id, + zigpy.zcl.clusters.hvac.Fan.cluster_id, + zigpy.zcl.clusters.hvac.Thermostat.cluster_id, + zigpy.zcl.clusters.hvac.UserInterface.cluster_id, + ], + "out_clusters": [zigpy.zcl.clusters.general.Ota.cluster_id], + } +} + +CLIMATE_SINOPE = { + 1: { + "device_type": zigpy.profiles.zha.DeviceType.THERMOSTAT, + "in_clusters": [ + zigpy.zcl.clusters.general.Basic.cluster_id, + zigpy.zcl.clusters.general.Identify.cluster_id, + zigpy.zcl.clusters.hvac.Thermostat.cluster_id, + zigpy.zcl.clusters.hvac.UserInterface.cluster_id, + 65281, + ], + "out_clusters": [zigpy.zcl.clusters.general.Ota.cluster_id, 65281], + "profile_id": 260, + }, +} + +CLIMATE_ZEN = { + 1: { + "device_type": zigpy.profiles.zha.DeviceType.THERMOSTAT, + "in_clusters": [ + zigpy.zcl.clusters.general.Basic.cluster_id, + zigpy.zcl.clusters.general.Identify.cluster_id, + zigpy.zcl.clusters.hvac.Fan.cluster_id, + zigpy.zcl.clusters.hvac.Thermostat.cluster_id, + zigpy.zcl.clusters.hvac.UserInterface.cluster_id, + ], + "out_clusters": [zigpy.zcl.clusters.general.Ota.cluster_id], + } +} +MANUF_SINOPE = "Sinope Technologies" +MANUF_ZEN = "Zen Within" + +ZCL_ATTR_PLUG = { + "abs_min_heat_setpoint_limit": 800, + "abs_max_heat_setpoint_limit": 3000, + "abs_min_cool_setpoint_limit": 2000, + "abs_max_cool_setpoint_limit": 4000, + "ctrl_seqe_of_oper": Thermostat.ControlSequenceOfOperation.Cooling_and_Heating, + "local_temp": None, + "max_cool_setpoint_limit": 3900, + "max_heat_setpoint_limit": 2900, + "min_cool_setpoint_limit": 2100, + "min_heat_setpoint_limit": 700, + "occupancy": 1, + "occupied_cooling_setpoint": 2500, + "occupied_heating_setpoint": 2200, + "pi_cooling_demand": None, + "pi_heating_demand": None, + "running_mode": Thermostat.RunningMode.Off, + "running_state": None, + "system_mode": Thermostat.SystemMode.Off, + "unoccupied_heating_setpoint": 2200, + "unoccupied_cooling_setpoint": 2300, +} + + +@pytest.fixture +def device_climate_mock(hass, zigpy_device_mock, zha_device_joined): + """Test regular thermostat device.""" + + async def _dev(clusters, plug=None, manuf=None): + if plug is None: + plugged_attrs = ZCL_ATTR_PLUG + else: + plugged_attrs = {**ZCL_ATTR_PLUG, **plug} + + async def _read_attr(attrs, *args, **kwargs): + res = {} + failed = {} + + for attr in attrs: + if attr in plugged_attrs: + res[attr] = plugged_attrs[attr] + else: + failed[attr] = zcl_f.Status.UNSUPPORTED_ATTRIBUTE + return res, failed + + zigpy_device = zigpy_device_mock(clusters, manufacturer=manuf) + zigpy_device.endpoints[1].thermostat.read_attributes.side_effect = _read_attr + zha_device = await zha_device_joined(zigpy_device) + await async_enable_traffic(hass, [zha_device]) + await hass.async_block_till_done() + return zha_device + + return _dev + + +@pytest.fixture +async def device_climate(device_climate_mock): + """Plain Climate device.""" + + return await device_climate_mock(CLIMATE) + + +@pytest.fixture +async def device_climate_fan(device_climate_mock): + """Test thermostat with fan device.""" + + return await device_climate_mock(CLIMATE_FAN) + + +@pytest.fixture +@patch.object( + zigpy.zcl.clusters.manufacturer_specific.ManufacturerSpecificCluster, + "ep_attribute", + "sinope_manufacturer_specific", +) +async def device_climate_sinope(device_climate_mock): + """Sinope thermostat.""" + + return await device_climate_mock(CLIMATE_SINOPE, manuf=MANUF_SINOPE) + + +@pytest.fixture +async def device_climate_zen(device_climate_mock): + """Zen Within thermostat.""" + + return await device_climate_mock(CLIMATE_ZEN, manuf=MANUF_ZEN) + + +def test_sequence_mappings(): + """Test correct mapping between control sequence -> HVAC Mode -> Sysmode.""" + + for hvac_modes in SEQ_OF_OPERATION.values(): + for hvac_mode in hvac_modes: + assert hvac_mode in HVAC_MODE_2_SYSTEM + assert Thermostat.SystemMode(HVAC_MODE_2_SYSTEM[hvac_mode]) is not None + + +async def test_climate_local_temp(hass, device_climate): + """Test local temperature.""" + + thrm_cluster = device_climate.device.endpoints[1].thermostat + entity_id = await find_entity_id(DOMAIN, device_climate, hass) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_CURRENT_TEMPERATURE] is None + + await send_attributes_report(hass, thrm_cluster, {0: 2100}) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 21.0 + + +async def test_climate_hvac_action_running_state(hass, device_climate): + """Test hvac action via running state.""" + + thrm_cluster = device_climate.device.endpoints[1].thermostat + entity_id = await find_entity_id(DOMAIN, device_climate, hass) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + + await send_attributes_report( + hass, thrm_cluster, {0x001E: Thermostat.RunningMode.Off} + ) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + + await send_attributes_report( + hass, thrm_cluster, {0x001C: Thermostat.SystemMode.Auto} + ) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + + await send_attributes_report( + hass, thrm_cluster, {0x001E: Thermostat.RunningMode.Cool} + ) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_COOL + + await send_attributes_report( + hass, thrm_cluster, {0x001E: Thermostat.RunningMode.Heat} + ) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT + + await send_attributes_report( + hass, thrm_cluster, {0x001E: Thermostat.RunningMode.Off} + ) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + + await send_attributes_report( + hass, thrm_cluster, {0x0029: Thermostat.RunningState.Fan_State_On} + ) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_FAN + + +async def test_climate_hvac_action_running_state_zen(hass, device_climate_zen): + """Test Zen hvac action via running state.""" + + thrm_cluster = device_climate_zen.device.endpoints[1].thermostat + entity_id = await find_entity_id(DOMAIN, device_climate_zen, hass) + + state = hass.states.get(entity_id) + assert ATTR_HVAC_ACTION not in state.attributes + + await send_attributes_report( + hass, thrm_cluster, {0x0029: Thermostat.RunningState.Cool_2nd_Stage_On} + ) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_COOL + + await send_attributes_report( + hass, thrm_cluster, {0x0029: Thermostat.RunningState.Fan_State_On} + ) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_FAN + + await send_attributes_report( + hass, thrm_cluster, {0x0029: Thermostat.RunningState.Heat_2nd_Stage_On} + ) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT + + await send_attributes_report( + hass, thrm_cluster, {0x0029: Thermostat.RunningState.Fan_2nd_Stage_On} + ) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_FAN + + await send_attributes_report( + hass, thrm_cluster, {0x0029: Thermostat.RunningState.Cool_State_On} + ) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_COOL + + await send_attributes_report( + hass, thrm_cluster, {0x0029: Thermostat.RunningState.Fan_3rd_Stage_On} + ) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_FAN + + await send_attributes_report( + hass, thrm_cluster, {0x0029: Thermostat.RunningState.Heat_State_On} + ) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT + + await send_attributes_report( + hass, thrm_cluster, {0x0029: Thermostat.RunningState.Idle} + ) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + + await send_attributes_report( + hass, thrm_cluster, {0x001C: Thermostat.SystemMode.Heat} + ) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + + +async def test_climate_hvac_action_pi_demand(hass, device_climate): + """Test hvac action based on pi_heating/cooling_demand attrs.""" + + thrm_cluster = device_climate.device.endpoints[1].thermostat + entity_id = await find_entity_id(DOMAIN, device_climate, hass) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + + await send_attributes_report(hass, thrm_cluster, {0x0007: 10}) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_COOL + + await send_attributes_report(hass, thrm_cluster, {0x0008: 20}) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT + + await send_attributes_report(hass, thrm_cluster, {0x0007: 0}) + await send_attributes_report(hass, thrm_cluster, {0x0008: 0}) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + + await send_attributes_report( + hass, thrm_cluster, {0x001C: Thermostat.SystemMode.Heat} + ) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + + await send_attributes_report( + hass, thrm_cluster, {0x001C: Thermostat.SystemMode.Cool} + ) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + + +@pytest.mark.parametrize( + "sys_mode, hvac_mode", + ( + (Thermostat.SystemMode.Auto, HVAC_MODE_HEAT_COOL), + (Thermostat.SystemMode.Cool, HVAC_MODE_COOL), + (Thermostat.SystemMode.Heat, HVAC_MODE_HEAT), + (Thermostat.SystemMode.Pre_cooling, HVAC_MODE_COOL), + (Thermostat.SystemMode.Fan_only, HVAC_MODE_FAN_ONLY), + (Thermostat.SystemMode.Dry, HVAC_MODE_DRY), + ), +) +async def test_hvac_mode(hass, device_climate, sys_mode, hvac_mode): + """Test HVAC modee.""" + + thrm_cluster = device_climate.device.endpoints[1].thermostat + entity_id = await find_entity_id(DOMAIN, device_climate, hass) + + state = hass.states.get(entity_id) + assert state.state == HVAC_MODE_OFF + + await send_attributes_report(hass, thrm_cluster, {0x001C: sys_mode}) + state = hass.states.get(entity_id) + assert state.state == hvac_mode + + await send_attributes_report( + hass, thrm_cluster, {0x001C: Thermostat.SystemMode.Off} + ) + state = hass.states.get(entity_id) + assert state.state == HVAC_MODE_OFF + + await send_attributes_report(hass, thrm_cluster, {0x001C: 0xFF}) + state = hass.states.get(entity_id) + assert state.state == STATE_UNKNOWN + + +@pytest.mark.parametrize( + "seq_of_op, modes", + ( + (0xFF, {HVAC_MODE_OFF}), + (0x00, {HVAC_MODE_OFF, HVAC_MODE_COOL}), + (0x01, {HVAC_MODE_OFF, HVAC_MODE_COOL}), + (0x02, {HVAC_MODE_OFF, HVAC_MODE_HEAT}), + (0x03, {HVAC_MODE_OFF, HVAC_MODE_HEAT}), + (0x04, {HVAC_MODE_OFF, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL}), + (0x05, {HVAC_MODE_OFF, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL}), + ), +) +async def test_hvac_modes(hass, device_climate_mock, seq_of_op, modes): + """Test HVAC modes from sequence of operations.""" + + device_climate = await device_climate_mock( + CLIMATE, {"ctrl_seqe_of_oper": seq_of_op} + ) + entity_id = await find_entity_id(DOMAIN, device_climate, hass) + state = hass.states.get(entity_id) + assert set(state.attributes[ATTR_HVAC_MODES]) == modes + + +@pytest.mark.parametrize( + "sys_mode, preset, target_temp", + ( + (Thermostat.SystemMode.Heat, None, 22), + (Thermostat.SystemMode.Heat, PRESET_AWAY, 16), + (Thermostat.SystemMode.Cool, None, 25), + (Thermostat.SystemMode.Cool, PRESET_AWAY, 27), + ), +) +async def test_target_temperature( + hass, device_climate_mock, sys_mode, preset, target_temp +): + """Test target temperature property.""" + + with patch.object( + zigpy.zcl.clusters.manufacturer_specific.ManufacturerSpecificCluster, + "ep_attribute", + "sinope_manufacturer_specific", + ): + device_climate = await device_climate_mock( + CLIMATE_SINOPE, + { + "occupied_cooling_setpoint": 2500, + "occupied_heating_setpoint": 2200, + "system_mode": sys_mode, + "unoccupied_heating_setpoint": 1600, + "unoccupied_cooling_setpoint": 2700, + }, + manuf=MANUF_SINOPE, + ) + entity_id = await find_entity_id(DOMAIN, device_climate, hass) + if preset: + await hass.services.async_call( + DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: preset}, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_TEMPERATURE] == target_temp + + +@pytest.mark.parametrize( + "preset, unoccupied, target_temp", + ((None, 1800, 17), (PRESET_AWAY, 1800, 18), (PRESET_AWAY, None, None),), +) +async def test_target_temperature_high( + hass, device_climate_mock, preset, unoccupied, target_temp +): + """Test target temperature high property.""" + + with patch.object( + zigpy.zcl.clusters.manufacturer_specific.ManufacturerSpecificCluster, + "ep_attribute", + "sinope_manufacturer_specific", + ): + device_climate = await device_climate_mock( + CLIMATE_SINOPE, + { + "occupied_cooling_setpoint": 1700, + "system_mode": Thermostat.SystemMode.Auto, + "unoccupied_cooling_setpoint": unoccupied, + }, + manuf=MANUF_SINOPE, + ) + entity_id = await find_entity_id(DOMAIN, device_climate, hass) + if preset: + await hass.services.async_call( + DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: preset}, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_TARGET_TEMP_HIGH] == target_temp + + +@pytest.mark.parametrize( + "preset, unoccupied, target_temp", + ((None, 1600, 21), (PRESET_AWAY, 1600, 16), (PRESET_AWAY, None, None),), +) +async def test_target_temperature_low( + hass, device_climate_mock, preset, unoccupied, target_temp +): + """Test target temperature low property.""" + + with patch.object( + zigpy.zcl.clusters.manufacturer_specific.ManufacturerSpecificCluster, + "ep_attribute", + "sinope_manufacturer_specific", + ): + device_climate = await device_climate_mock( + CLIMATE_SINOPE, + { + "occupied_heating_setpoint": 2100, + "system_mode": Thermostat.SystemMode.Auto, + "unoccupied_heating_setpoint": unoccupied, + }, + manuf=MANUF_SINOPE, + ) + entity_id = await find_entity_id(DOMAIN, device_climate, hass) + if preset: + await hass.services.async_call( + DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: preset}, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_TARGET_TEMP_LOW] == target_temp + + +@pytest.mark.parametrize( + "hvac_mode, sys_mode", + ( + (HVAC_MODE_AUTO, None), + (HVAC_MODE_COOL, Thermostat.SystemMode.Cool), + (HVAC_MODE_DRY, None), + (HVAC_MODE_FAN_ONLY, None), + (HVAC_MODE_HEAT, Thermostat.SystemMode.Heat), + (HVAC_MODE_HEAT_COOL, Thermostat.SystemMode.Auto), + ), +) +async def test_set_hvac_mode(hass, device_climate, hvac_mode, sys_mode): + """Test setting hvac mode.""" + + thrm_cluster = device_climate.device.endpoints[1].thermostat + entity_id = await find_entity_id(DOMAIN, device_climate, hass) + + state = hass.states.get(entity_id) + assert state.state == HVAC_MODE_OFF + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: hvac_mode}, + blocking=True, + ) + state = hass.states.get(entity_id) + if sys_mode is not None: + assert state.state == hvac_mode + assert thrm_cluster.write_attributes.call_count == 1 + assert thrm_cluster.write_attributes.call_args[0][0] == { + "system_mode": sys_mode + } + else: + assert thrm_cluster.write_attributes.call_count == 0 + assert state.state == HVAC_MODE_OFF + + # turn off + thrm_cluster.write_attributes.reset_mock() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: HVAC_MODE_OFF}, + blocking=True, + ) + state = hass.states.get(entity_id) + assert state.state == HVAC_MODE_OFF + assert thrm_cluster.write_attributes.call_count == 1 + assert thrm_cluster.write_attributes.call_args[0][0] == { + "system_mode": Thermostat.SystemMode.Off + } + + +async def test_preset_setting(hass, device_climate_sinope): + """Test preset setting.""" + + entity_id = await find_entity_id(DOMAIN, device_climate_sinope, hass) + thrm_cluster = device_climate_sinope.device.endpoints[1].thermostat + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE + + # unsuccessful occupancy change + thrm_cluster.write_attributes.return_value = [ + zcl_f.WriteAttributesResponse.deserialize(b"\x01\x00\x00")[0] + ] + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE + assert thrm_cluster.write_attributes.call_count == 1 + assert thrm_cluster.write_attributes.call_args[0][0] == {"set_occupancy": 0} + + # successful occupancy change + thrm_cluster.write_attributes.reset_mock() + thrm_cluster.write_attributes.return_value = [ + zcl_f.WriteAttributesResponse.deserialize(b"\x00")[0] + ] + await hass.services.async_call( + DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PRESET_MODE] == PRESET_AWAY + assert thrm_cluster.write_attributes.call_count == 1 + assert thrm_cluster.write_attributes.call_args[0][0] == {"set_occupancy": 0} + + # unsuccessful occupancy change + thrm_cluster.write_attributes.reset_mock() + thrm_cluster.write_attributes.return_value = [ + zcl_f.WriteAttributesResponse.deserialize(b"\x01\x01\x01")[0] + ] + await hass.services.async_call( + DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_NONE}, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PRESET_MODE] == PRESET_AWAY + assert thrm_cluster.write_attributes.call_count == 1 + assert thrm_cluster.write_attributes.call_args[0][0] == {"set_occupancy": 1} + + # successful occupancy change + thrm_cluster.write_attributes.reset_mock() + thrm_cluster.write_attributes.return_value = [ + zcl_f.WriteAttributesResponse.deserialize(b"\x00")[0] + ] + await hass.services.async_call( + DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_NONE}, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE + assert thrm_cluster.write_attributes.call_count == 1 + assert thrm_cluster.write_attributes.call_args[0][0] == {"set_occupancy": 1} + + +async def test_preset_setting_invalid(hass, device_climate_sinope): + """Test invalid preset setting.""" + + entity_id = await find_entity_id(DOMAIN, device_climate_sinope, hass) + thrm_cluster = device_climate_sinope.device.endpoints[1].thermostat + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: "invalid_preset"}, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE + assert thrm_cluster.write_attributes.call_count == 0 + + +async def test_set_temperature_hvac_mode(hass, device_climate): + """Test setting HVAC mode in temperature service call.""" + + entity_id = await find_entity_id(DOMAIN, device_climate, hass) + thrm_cluster = device_climate.device.endpoints[1].thermostat + + state = hass.states.get(entity_id) + assert state.state == HVAC_MODE_OFF + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_HVAC_MODE: HVAC_MODE_HEAT_COOL, + ATTR_TEMPERATURE: 20, + }, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state.state == HVAC_MODE_HEAT_COOL + assert thrm_cluster.write_attributes.await_count == 1 + assert thrm_cluster.write_attributes.call_args[0][0] == { + "system_mode": Thermostat.SystemMode.Auto + } + + +async def test_set_temperature_heat_cool(hass, device_climate_mock): + """Test setting temperature service call in heating/cooling HVAC mode.""" + + with patch.object( + zigpy.zcl.clusters.manufacturer_specific.ManufacturerSpecificCluster, + "ep_attribute", + "sinope_manufacturer_specific", + ): + device_climate = await device_climate_mock( + CLIMATE_SINOPE, + { + "occupied_cooling_setpoint": 2500, + "occupied_heating_setpoint": 2000, + "system_mode": Thermostat.SystemMode.Auto, + "unoccupied_heating_setpoint": 1600, + "unoccupied_cooling_setpoint": 2700, + }, + manuf=MANUF_SINOPE, + ) + entity_id = await find_entity_id(DOMAIN, device_climate, hass) + thrm_cluster = device_climate.device.endpoints[1].thermostat + + state = hass.states.get(entity_id) + assert state.state == HVAC_MODE_HEAT_COOL + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 21}, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_TARGET_TEMP_LOW] == 20.0 + assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 25.0 + assert thrm_cluster.write_attributes.await_count == 0 + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_TARGET_TEMP_HIGH: 26, + ATTR_TARGET_TEMP_LOW: 19, + }, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_TARGET_TEMP_LOW] == 19.0 + assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 26.0 + assert thrm_cluster.write_attributes.await_count == 2 + assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { + "occupied_heating_setpoint": 1900 + } + assert thrm_cluster.write_attributes.call_args_list[1][0][0] == { + "occupied_cooling_setpoint": 2600 + } + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, + blocking=True, + ) + thrm_cluster.write_attributes.reset_mock() + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_TARGET_TEMP_HIGH: 30, + ATTR_TARGET_TEMP_LOW: 15, + }, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_TARGET_TEMP_LOW] == 15.0 + assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 30.0 + assert thrm_cluster.write_attributes.await_count == 2 + assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { + "unoccupied_heating_setpoint": 1500 + } + assert thrm_cluster.write_attributes.call_args_list[1][0][0] == { + "unoccupied_cooling_setpoint": 3000 + } + + +async def test_set_temperature_heat(hass, device_climate_mock): + """Test setting temperature service call in heating HVAC mode.""" + + with patch.object( + zigpy.zcl.clusters.manufacturer_specific.ManufacturerSpecificCluster, + "ep_attribute", + "sinope_manufacturer_specific", + ): + device_climate = await device_climate_mock( + CLIMATE_SINOPE, + { + "occupied_cooling_setpoint": 2500, + "occupied_heating_setpoint": 2000, + "system_mode": Thermostat.SystemMode.Heat, + "unoccupied_heating_setpoint": 1600, + "unoccupied_cooling_setpoint": 2700, + }, + manuf=MANUF_SINOPE, + ) + entity_id = await find_entity_id(DOMAIN, device_climate, hass) + thrm_cluster = device_climate.device.endpoints[1].thermostat + + state = hass.states.get(entity_id) + assert state.state == HVAC_MODE_HEAT + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_TARGET_TEMP_HIGH: 30, + ATTR_TARGET_TEMP_LOW: 15, + }, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_TARGET_TEMP_LOW] is None + assert state.attributes[ATTR_TARGET_TEMP_HIGH] is None + assert state.attributes[ATTR_TEMPERATURE] == 20.0 + assert thrm_cluster.write_attributes.await_count == 0 + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 21}, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_TARGET_TEMP_LOW] is None + assert state.attributes[ATTR_TARGET_TEMP_HIGH] is None + assert state.attributes[ATTR_TEMPERATURE] == 21.0 + assert thrm_cluster.write_attributes.await_count == 1 + assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { + "occupied_heating_setpoint": 2100 + } + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, + blocking=True, + ) + thrm_cluster.write_attributes.reset_mock() + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 22}, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_TARGET_TEMP_LOW] is None + assert state.attributes[ATTR_TARGET_TEMP_HIGH] is None + assert state.attributes[ATTR_TEMPERATURE] == 22.0 + assert thrm_cluster.write_attributes.await_count == 1 + assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { + "unoccupied_heating_setpoint": 2200 + } + + +async def test_set_temperature_cool(hass, device_climate_mock): + """Test setting temperature service call in cooling HVAC mode.""" + + with patch.object( + zigpy.zcl.clusters.manufacturer_specific.ManufacturerSpecificCluster, + "ep_attribute", + "sinope_manufacturer_specific", + ): + device_climate = await device_climate_mock( + CLIMATE_SINOPE, + { + "occupied_cooling_setpoint": 2500, + "occupied_heating_setpoint": 2000, + "system_mode": Thermostat.SystemMode.Cool, + "unoccupied_cooling_setpoint": 1600, + "unoccupied_heating_setpoint": 2700, + }, + manuf=MANUF_SINOPE, + ) + entity_id = await find_entity_id(DOMAIN, device_climate, hass) + thrm_cluster = device_climate.device.endpoints[1].thermostat + + state = hass.states.get(entity_id) + assert state.state == HVAC_MODE_COOL + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: entity_id, + ATTR_TARGET_TEMP_HIGH: 30, + ATTR_TARGET_TEMP_LOW: 15, + }, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_TARGET_TEMP_LOW] is None + assert state.attributes[ATTR_TARGET_TEMP_HIGH] is None + assert state.attributes[ATTR_TEMPERATURE] == 25.0 + assert thrm_cluster.write_attributes.await_count == 0 + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 21}, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_TARGET_TEMP_LOW] is None + assert state.attributes[ATTR_TARGET_TEMP_HIGH] is None + assert state.attributes[ATTR_TEMPERATURE] == 21.0 + assert thrm_cluster.write_attributes.await_count == 1 + assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { + "occupied_cooling_setpoint": 2100 + } + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, + blocking=True, + ) + thrm_cluster.write_attributes.reset_mock() + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 22}, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_TARGET_TEMP_LOW] is None + assert state.attributes[ATTR_TARGET_TEMP_HIGH] is None + assert state.attributes[ATTR_TEMPERATURE] == 22.0 + assert thrm_cluster.write_attributes.await_count == 1 + assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { + "unoccupied_cooling_setpoint": 2200 + } + + +async def test_set_temperature_wrong_mode(hass, device_climate_mock): + """Test setting temperature service call for wrong HVAC mode.""" + + with patch.object( + zigpy.zcl.clusters.manufacturer_specific.ManufacturerSpecificCluster, + "ep_attribute", + "sinope_manufacturer_specific", + ): + device_climate = await device_climate_mock( + CLIMATE_SINOPE, + { + "occupied_cooling_setpoint": 2500, + "occupied_heating_setpoint": 2000, + "system_mode": Thermostat.SystemMode.Dry, + "unoccupied_cooling_setpoint": 1600, + "unoccupied_heating_setpoint": 2700, + }, + manuf=MANUF_SINOPE, + ) + entity_id = await find_entity_id(DOMAIN, device_climate, hass) + thrm_cluster = device_climate.device.endpoints[1].thermostat + + state = hass.states.get(entity_id) + assert state.state == HVAC_MODE_DRY + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 24}, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_TARGET_TEMP_LOW] is None + assert state.attributes[ATTR_TARGET_TEMP_HIGH] is None + assert state.attributes[ATTR_TEMPERATURE] is None + assert thrm_cluster.write_attributes.await_count == 0 + + +async def test_occupancy_reset(hass, device_climate_sinope): + """Test away preset reset.""" + + entity_id = await find_entity_id(DOMAIN, device_climate_sinope, hass) + thrm_cluster = device_climate_sinope.device.endpoints[1].thermostat + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, + blocking=True, + ) + thrm_cluster.write_attributes.reset_mock() + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PRESET_MODE] == PRESET_AWAY + + thrm_cluster.read_attributes.return_value = [True], {} + await send_attributes_report( + hass, thrm_cluster, {"occupied_heating_setpoint": 1950} + ) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE + + +async def test_fan_mode(hass, device_climate_fan): + """Test fan mode.""" + + entity_id = await find_entity_id(DOMAIN, device_climate_fan, hass) + thrm_cluster = device_climate_fan.device.endpoints[1].thermostat + + state = hass.states.get(entity_id) + assert set(state.attributes[ATTR_FAN_MODES]) == {FAN_AUTO, FAN_ON} + assert state.attributes[ATTR_FAN_MODE] == FAN_AUTO + + await send_attributes_report( + hass, thrm_cluster, {"running_state": Thermostat.RunningState.Fan_State_On} + ) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_FAN_MODE] == FAN_ON + + await send_attributes_report( + hass, thrm_cluster, {"running_state": Thermostat.RunningState.Idle} + ) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_FAN_MODE] == FAN_AUTO + + await send_attributes_report( + hass, thrm_cluster, {"running_state": Thermostat.RunningState.Fan_2nd_Stage_On} + ) + state = hass.states.get(entity_id) + assert state.attributes[ATTR_FAN_MODE] == FAN_ON + + +async def test_set_fan_mode_not_supported(hass, device_climate_fan): + """Test fan setting unsupported mode.""" + + entity_id = await find_entity_id(DOMAIN, device_climate_fan, hass) + fan_cluster = device_climate_fan.device.endpoints[1].fan + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: FAN_LOW}, + blocking=True, + ) + assert fan_cluster.write_attributes.await_count == 0 + + +async def test_set_fan_mode(hass, device_climate_fan): + """Test fan mode setting.""" + + entity_id = await find_entity_id(DOMAIN, device_climate_fan, hass) + fan_cluster = device_climate_fan.device.endpoints[1].fan + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_FAN_MODE] == FAN_AUTO + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: FAN_ON}, + blocking=True, + ) + assert fan_cluster.write_attributes.await_count == 1 + assert fan_cluster.write_attributes.call_args[0][0] == {"fan_mode": 4} + + fan_cluster.write_attributes.reset_mock() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: FAN_AUTO}, + blocking=True, + ) + assert fan_cluster.write_attributes.await_count == 1 + assert fan_cluster.write_attributes.call_args[0][0] == {"fan_mode": 5} diff --git a/tests/components/zha/test_cover.py b/tests/components/zha/test_cover.py index b4c72fd82d4..2c497f6880f 100644 --- a/tests/components/zha/test_cover.py +++ b/tests/components/zha/test_cover.py @@ -1,21 +1,38 @@ """Test zha cover.""" +import asyncio + import pytest import zigpy.types import zigpy.zcl.clusters.closures as closures +import zigpy.zcl.clusters.general as general import zigpy.zcl.foundation as zcl_f -from homeassistant.components.cover import DOMAIN -from homeassistant.const import STATE_CLOSED, STATE_OPEN, STATE_UNAVAILABLE +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + DOMAIN, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, + SERVICE_SET_COVER_POSITION, + SERVICE_STOP_COVER, +) +from homeassistant.const import ( + ATTR_COMMAND, + STATE_CLOSED, + STATE_OPEN, + STATE_UNAVAILABLE, +) +from homeassistant.core import CoreState, State from .common import ( async_enable_traffic, async_test_rejoin, find_entity_id, + make_zcl_header, send_attributes_report, ) -from tests.async_mock import MagicMock, call, patch -from tests.common import mock_coro +from tests.async_mock import AsyncMock, MagicMock, call, patch +from tests.common import async_capture_events, mock_coro, mock_restore_cache @pytest.fixture @@ -32,6 +49,54 @@ def zigpy_cover_device(zigpy_device_mock): return zigpy_device_mock(endpoints) +@pytest.fixture +def zigpy_cover_remote(zigpy_device_mock): + """Zigpy cover remote device.""" + + endpoints = { + 1: { + "device_type": 0x0203, + "in_clusters": [], + "out_clusters": [closures.WindowCovering.cluster_id], + } + } + return zigpy_device_mock(endpoints) + + +@pytest.fixture +def zigpy_shade_device(zigpy_device_mock): + """Zigpy shade device.""" + + endpoints = { + 1: { + "device_type": 512, + "in_clusters": [ + closures.Shade.cluster_id, + general.LevelControl.cluster_id, + general.OnOff.cluster_id, + ], + "out_clusters": [], + } + } + return zigpy_device_mock(endpoints) + + +@pytest.fixture +def zigpy_keen_vent(zigpy_device_mock): + """Zigpy Keen Vent device.""" + + endpoints = { + 1: { + "device_type": 3, + "in_clusters": [general.LevelControl.cluster_id, general.OnOff.cluster_id], + "out_clusters": [], + } + } + return zigpy_device_mock( + endpoints, manufacturer="Keen Home Inc", model="SV02-612-MP-1.3" + ) + + @patch( "homeassistant.components.zha.core.channels.closures.WindowCovering.async_initialize" ) @@ -74,7 +139,7 @@ async def test_cover(m1, hass, zha_device_joined_restored, zigpy_cover_device): "zigpy.zcl.Cluster.request", return_value=mock_coro([0x1, zcl_f.Status.SUCCESS]) ): await hass.services.async_call( - DOMAIN, "close_cover", {"entity_id": entity_id}, blocking=True + DOMAIN, SERVICE_CLOSE_COVER, {"entity_id": entity_id}, blocking=True ) assert cluster.request.call_count == 1 assert cluster.request.call_args == call( @@ -86,7 +151,7 @@ async def test_cover(m1, hass, zha_device_joined_restored, zigpy_cover_device): "zigpy.zcl.Cluster.request", return_value=mock_coro([0x0, zcl_f.Status.SUCCESS]) ): await hass.services.async_call( - DOMAIN, "open_cover", {"entity_id": entity_id}, blocking=True + DOMAIN, SERVICE_OPEN_COVER, {"entity_id": entity_id}, blocking=True ) assert cluster.request.call_count == 1 assert cluster.request.call_args == call( @@ -99,7 +164,7 @@ async def test_cover(m1, hass, zha_device_joined_restored, zigpy_cover_device): ): await hass.services.async_call( DOMAIN, - "set_cover_position", + SERVICE_SET_COVER_POSITION, {"entity_id": entity_id, "position": 47}, blocking=True, ) @@ -119,7 +184,7 @@ async def test_cover(m1, hass, zha_device_joined_restored, zigpy_cover_device): "zigpy.zcl.Cluster.request", return_value=mock_coro([0x2, zcl_f.Status.SUCCESS]) ): await hass.services.async_call( - DOMAIN, "stop_cover", {"entity_id": entity_id}, blocking=True + DOMAIN, SERVICE_STOP_COVER, {"entity_id": entity_id}, blocking=True ) assert cluster.request.call_count == 1 assert cluster.request.call_args == call( @@ -129,3 +194,232 @@ async def test_cover(m1, hass, zha_device_joined_restored, zigpy_cover_device): # test rejoin await async_test_rejoin(hass, zigpy_cover_device, [cluster], (1,)) assert hass.states.get(entity_id).state == STATE_OPEN + + +async def test_shade(hass, zha_device_joined_restored, zigpy_shade_device): + """Test zha cover platform for shade device type.""" + + # load up cover domain + zha_device = await zha_device_joined_restored(zigpy_shade_device) + + cluster_on_off = zigpy_shade_device.endpoints.get(1).on_off + cluster_level = zigpy_shade_device.endpoints.get(1).level + entity_id = await find_entity_id(DOMAIN, zha_device, hass) + assert entity_id is not None + + # test that the cover was created and that it is unavailable + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, [zha_device]) + await hass.async_block_till_done() + + # test that the state has changed from unavailable to off + await send_attributes_report(hass, cluster_on_off, {8: 0, 0: False, 1: 1}) + assert hass.states.get(entity_id).state == STATE_CLOSED + + # test to see if it opens + await send_attributes_report(hass, cluster_on_off, {8: 0, 0: True, 1: 1}) + assert hass.states.get(entity_id).state == STATE_OPEN + + # close from UI command fails + with patch("zigpy.zcl.Cluster.request", side_effect=asyncio.TimeoutError): + await hass.services.async_call( + DOMAIN, SERVICE_CLOSE_COVER, {"entity_id": entity_id}, blocking=True + ) + assert cluster_on_off.request.call_count == 1 + assert cluster_on_off.request.call_args[0][0] is False + assert cluster_on_off.request.call_args[0][1] == 0x0000 + assert hass.states.get(entity_id).state == STATE_OPEN + + with patch( + "zigpy.zcl.Cluster.request", AsyncMock(return_value=[0x1, zcl_f.Status.SUCCESS]) + ): + await hass.services.async_call( + DOMAIN, SERVICE_CLOSE_COVER, {"entity_id": entity_id}, blocking=True + ) + assert cluster_on_off.request.call_count == 1 + assert cluster_on_off.request.call_args[0][0] is False + assert cluster_on_off.request.call_args[0][1] == 0x0000 + assert hass.states.get(entity_id).state == STATE_CLOSED + + # open from UI command fails + assert ATTR_CURRENT_POSITION not in hass.states.get(entity_id).attributes + await send_attributes_report(hass, cluster_level, {0: 0}) + with patch("zigpy.zcl.Cluster.request", side_effect=asyncio.TimeoutError): + await hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER, {"entity_id": entity_id}, blocking=True + ) + assert cluster_on_off.request.call_count == 1 + assert cluster_on_off.request.call_args[0][0] is False + assert cluster_on_off.request.call_args[0][1] == 0x0001 + assert hass.states.get(entity_id).state == STATE_CLOSED + + # open from UI succeeds + with patch( + "zigpy.zcl.Cluster.request", AsyncMock(return_value=[0x0, zcl_f.Status.SUCCESS]) + ): + await hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER, {"entity_id": entity_id}, blocking=True + ) + assert cluster_on_off.request.call_count == 1 + assert cluster_on_off.request.call_args[0][0] is False + assert cluster_on_off.request.call_args[0][1] == 0x0001 + assert hass.states.get(entity_id).state == STATE_OPEN + + # set position UI command fails + with patch("zigpy.zcl.Cluster.request", side_effect=asyncio.TimeoutError): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_COVER_POSITION, + {"entity_id": entity_id, "position": 47}, + blocking=True, + ) + assert cluster_level.request.call_count == 1 + assert cluster_level.request.call_args[0][0] is False + assert cluster_level.request.call_args[0][1] == 0x0004 + assert int(cluster_level.request.call_args[0][3] * 100 / 255) == 47 + assert hass.states.get(entity_id).attributes[ATTR_CURRENT_POSITION] == 0 + + # set position UI success + with patch( + "zigpy.zcl.Cluster.request", AsyncMock(return_value=[0x5, zcl_f.Status.SUCCESS]) + ): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_COVER_POSITION, + {"entity_id": entity_id, "position": 47}, + blocking=True, + ) + assert cluster_level.request.call_count == 1 + assert cluster_level.request.call_args[0][0] is False + assert cluster_level.request.call_args[0][1] == 0x0004 + assert int(cluster_level.request.call_args[0][3] * 100 / 255) == 47 + assert hass.states.get(entity_id).attributes[ATTR_CURRENT_POSITION] == 47 + + # report position change + await send_attributes_report(hass, cluster_level, {8: 0, 0: 100, 1: 1}) + assert hass.states.get(entity_id).attributes[ATTR_CURRENT_POSITION] == int( + 100 * 100 / 255 + ) + + # test rejoin + await async_test_rejoin( + hass, zigpy_shade_device, [cluster_level, cluster_on_off], (1,) + ) + assert hass.states.get(entity_id).state == STATE_OPEN + + # test cover stop + with patch("zigpy.zcl.Cluster.request", side_effect=asyncio.TimeoutError): + await hass.services.async_call( + DOMAIN, SERVICE_STOP_COVER, {"entity_id": entity_id}, blocking=True, + ) + assert cluster_level.request.call_count == 1 + assert cluster_level.request.call_args[0][0] is False + assert cluster_level.request.call_args[0][1] in (0x0003, 0x0007) + + +async def test_restore_state(hass, zha_device_restored, zigpy_shade_device): + """Ensure states are restored on startup.""" + + mock_restore_cache( + hass, + ( + State( + "cover.fakemanufacturer_fakemodel_e769900a_level_on_off_shade", + STATE_OPEN, + {ATTR_CURRENT_POSITION: 50}, + ), + ), + ) + + hass.state = CoreState.starting + + zha_device = await zha_device_restored(zigpy_shade_device) + entity_id = await find_entity_id(DOMAIN, zha_device, hass) + assert entity_id is not None + + # test that the cover was created and that it is unavailable + assert hass.states.get(entity_id).state == STATE_OPEN + assert hass.states.get(entity_id).attributes[ATTR_CURRENT_POSITION] == 50 + + +async def test_keen_vent(hass, zha_device_joined_restored, zigpy_keen_vent): + """Test keen vent.""" + + # load up cover domain + zha_device = await zha_device_joined_restored(zigpy_keen_vent) + + cluster_on_off = zigpy_keen_vent.endpoints.get(1).on_off + cluster_level = zigpy_keen_vent.endpoints.get(1).level + entity_id = await find_entity_id(DOMAIN, zha_device, hass) + assert entity_id is not None + + # test that the cover was created and that it is unavailable + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, [zha_device]) + await hass.async_block_till_done() + + # test that the state has changed from unavailable to off + await send_attributes_report(hass, cluster_on_off, {8: 0, 0: False, 1: 1}) + assert hass.states.get(entity_id).state == STATE_CLOSED + + # open from UI command fails + p1 = patch.object(cluster_on_off, "request", side_effect=asyncio.TimeoutError) + p2 = patch.object(cluster_level, "request", AsyncMock(return_value=[4, 0])) + + with p1, p2: + await hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER, {"entity_id": entity_id}, blocking=True + ) + assert cluster_on_off.request.call_count == 1 + assert cluster_on_off.request.call_args[0][0] is False + assert cluster_on_off.request.call_args[0][1] == 0x0001 + assert cluster_level.request.call_count == 1 + assert hass.states.get(entity_id).state == STATE_CLOSED + + # open from UI command success + p1 = patch.object(cluster_on_off, "request", AsyncMock(return_value=[1, 0])) + p2 = patch.object(cluster_level, "request", AsyncMock(return_value=[4, 0])) + + with p1, p2: + await hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER, {"entity_id": entity_id}, blocking=True + ) + await asyncio.sleep(0) + assert cluster_on_off.request.call_count == 1 + assert cluster_on_off.request.call_args[0][0] is False + assert cluster_on_off.request.call_args[0][1] == 0x0001 + assert cluster_level.request.call_count == 1 + assert hass.states.get(entity_id).state == STATE_OPEN + assert hass.states.get(entity_id).attributes[ATTR_CURRENT_POSITION] == 100 + + +async def test_cover_remote(hass, zha_device_joined_restored, zigpy_cover_remote): + """Test zha cover remote.""" + + # load up cover domain + await zha_device_joined_restored(zigpy_cover_remote) + + cluster = zigpy_cover_remote.endpoints[1].out_clusters[ + closures.WindowCovering.cluster_id + ] + zha_events = async_capture_events(hass, "zha_event") + + # up command + hdr = make_zcl_header(0, global_command=False) + cluster.handle_message(hdr, []) + await hass.async_block_till_done() + + assert len(zha_events) == 1 + assert zha_events[0].data[ATTR_COMMAND] == "up_open" + + # down command + hdr = make_zcl_header(1, global_command=False) + cluster.handle_message(hdr, []) + await hass.async_block_till_done() + + assert len(zha_events) == 2 + assert zha_events[1].data[ATTR_COMMAND] == "down_close" diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py index 1d88ba69e8d..d4ea1377d97 100644 --- a/tests/components/zha/zha_devices_list.py +++ b/tests/components/zha/zha_devices_list.py @@ -813,7 +813,7 @@ DEVICES = [ "entity_id": "sensor.ikea_of_sweden_tradfri_on_off_switch_77665544_power", } }, - "event_channels": ["1:0x0006", "1:0x0008", "1:0x0019"], + "event_channels": ["1:0x0006", "1:0x0008", "1:0x0019", "1:0x0102"], "manufacturer": "IKEA of Sweden", "model": "TRADFRI on/off switch", "node_descriptor": b"\x02@\x80|\x11RR\x00\x00,R\x00\x00", @@ -1036,16 +1036,16 @@ DEVICES = [ } }, "entities": [ - "light.keen_home_inc_sv02_610_mp_1_3_77665544_level_on_off", + "cover.keen_home_inc_sv02_610_mp_1_3_77665544_level_on_off", "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_power", "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_pressure", "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_temperature", ], "entity_map": { - ("light", "00:11:22:33:44:55:66:77-1"): { + ("cover", "00:11:22:33:44:55:66:77-1"): { "channels": ["level", "on_off"], - "entity_class": "Light", - "entity_id": "light.keen_home_inc_sv02_610_mp_1_3_77665544_level_on_off", + "entity_class": "KeenVent", + "entity_id": "cover.keen_home_inc_sv02_610_mp_1_3_77665544_level_on_off", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { "channels": ["power"], @@ -1094,16 +1094,16 @@ DEVICES = [ } }, "entities": [ - "light.keen_home_inc_sv02_612_mp_1_2_77665544_level_on_off", + "cover.keen_home_inc_sv02_612_mp_1_2_77665544_level_on_off", "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_power", "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_pressure", "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_temperature", ], "entity_map": { - ("light", "00:11:22:33:44:55:66:77-1"): { + ("cover", "00:11:22:33:44:55:66:77-1"): { "channels": ["level", "on_off"], - "entity_class": "Light", - "entity_id": "light.keen_home_inc_sv02_612_mp_1_2_77665544_level_on_off", + "entity_class": "KeenVent", + "entity_id": "cover.keen_home_inc_sv02_612_mp_1_2_77665544_level_on_off", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { "channels": ["power"], @@ -1152,16 +1152,16 @@ DEVICES = [ } }, "entities": [ - "light.keen_home_inc_sv02_612_mp_1_3_77665544_level_on_off", + "cover.keen_home_inc_sv02_612_mp_1_3_77665544_level_on_off", "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_power", "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_pressure", "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_temperature", ], "entity_map": { - ("light", "00:11:22:33:44:55:66:77-1"): { + ("cover", "00:11:22:33:44:55:66:77-1"): { "channels": ["level", "on_off"], - "entity_class": "Light", - "entity_id": "light.keen_home_inc_sv02_612_mp_1_3_77665544_level_on_off", + "entity_class": "KeenVent", + "entity_id": "cover.keen_home_inc_sv02_612_mp_1_3_77665544_level_on_off", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { "channels": ["power"], @@ -3100,10 +3100,16 @@ DEVICES = [ }, }, "entities": [ + "climate.sinope_technologies_th1123zb_77665544_thermostat", "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement", "sensor.sinope_technologies_th1123zb_77665544_temperature", ], "entity_map": { + ("climate", "00:11:22:33:44:55:66:77-1"): { + "channels": ["thermostat"], + "entity_class": "Thermostat", + "entity_id": "climate.sinope_technologies_th1123zb_77665544_thermostat", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { "channels": ["temperature"], "entity_class": "Temperature", @@ -3142,8 +3148,14 @@ DEVICES = [ "entities": [ "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement", "sensor.sinope_technologies_th1124zb_77665544_temperature", + "climate.sinope_technologies_th1124zb_77665544_thermostat", ], "entity_map": { + ("climate", "00:11:22:33:44:55:66:77-1"): { + "channels": ["thermostat"], + "entity_class": "Thermostat", + "entity_id": "climate.sinope_technologies_th1124zb_77665544_thermostat", + }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { "channels": ["temperature"], "entity_class": "Temperature", @@ -3326,7 +3338,7 @@ DEVICES = [ } }, "entities": [ - "fan.zen_within_zen_01_77665544_fan", + "climate.zen_within_zen_01_77665544_fan_thermostat", "sensor.zen_within_zen_01_77665544_power", ], "entity_map": { @@ -3335,10 +3347,10 @@ DEVICES = [ "entity_class": "Battery", "entity_id": "sensor.zen_within_zen_01_77665544_power", }, - ("fan", "00:11:22:33:44:55:66:77-1-514"): { - "channels": ["fan"], - "entity_class": "ZhaFan", - "entity_id": "fan.zen_within_zen_01_77665544_fan", + ("climate", "00:11:22:33:44:55:66:77-1"): { + "channels": ["thermostat", "fan"], + "entity_class": "ZenWithinThermostat", + "entity_id": "climate.zen_within_zen_01_77665544_fan_thermostat", }, }, "event_channels": ["1:0x0019"], @@ -3540,4 +3552,28 @@ DEVICES = [ "model": "Z01-A19NAE26", "node_descriptor": b"\x02@\x8c`\x11RR\x00\x00\x00R\x00\x00", }, + { + "device_no": 97, + "endpoints": { + 1: { + "device_type": 512, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 8, 10, 21, 256, 64544, 64545], + "out_clusters": [3, 64544], + "profile_id": 260, + } + }, + "entities": ["cover.unk_manufacturer_unk_model_77665544_level_on_off_shade"], + "entity_map": { + ("cover", "00:11:22:33:44:55:66:77-1"): { + "channels": ["level", "on_off", "shade"], + "entity_class": "Shade", + "entity_id": "cover.unk_manufacturer_unk_model_77665544_level_on_off_shade", + } + }, + "event_channels": [], + "manufacturer": "unk_manufacturer", + "model": "unk_model", + "node_descriptor": b"\x01@\x8e\x10\x11RR\x00\x00\x00R\x00\x00", + }, ] diff --git a/tests/fixtures/ozw/climate.json b/tests/fixtures/ozw/climate.json new file mode 100644 index 00000000000..652dc9aef26 --- /dev/null +++ b/tests/fixtures/ozw/climate.json @@ -0,0 +1,54 @@ +{ + "topic": "OpenZWave/1/node/7/instance/1/commandclass/64/value/122683412/", + "payload": { + "Label": "Mode", + "Value": { + "List": [ + { + "Value": 0, + "Label": "Off" + }, + { + "Value": 1, + "Label": "Heat" + }, + { + "Value": 2, + "Label": "Cool" + }, + { + "Value": 3, + "Label": "Auto" + }, + { + "Value": 11, + "Label": "Heat Econ" + }, + { + "Value": 12, + "Label": "Cool Econ" + } + ], + "Selected": "Auto", + "Selected_id": 3 + }, + "Units": "", + "Min": 0, + "Max": 0, + "Type": "List", + "Instance": 1, + "CommandClass": "COMMAND_CLASS_THERMOSTAT_MODE", + "Index": 0, + "Node": 7, + "Genre": "User", + "Help": "Set the Thermostat Mode", + "ValueIDKey": 122683412, + "ReadOnly": false, + "WriteOnly": false, + "ValueSet": false, + "ValuePolled": false, + "ChangeVerified": false, + "Event": "valueAdded", + "TimeStamp": 1588264894 + } +} \ No newline at end of file diff --git a/tests/fixtures/ozw/climate_network_dump.csv b/tests/fixtures/ozw/climate_network_dump.csv new file mode 100644 index 00000000000..c865e6438de --- /dev/null +++ b/tests/fixtures/ozw/climate_network_dump.csv @@ -0,0 +1,140 @@ +OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1008", "OZWDeamon_Version": "0.1", "QTOpenZWave_Version": "1.0.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1579566933, "ManufacturerSpecificDBReady": true, "homeID": 3245146787, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 3.95", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/zwave"} +OpenZWave/1/node/7/,{ "NodeID": 7, "NodeQueryStage": "Complete", "isListening": true, "isFlirs": false, "isBeaming": false, "isRouting": false, "isSecurityv1": false, "isZWavePlus": true, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "", "ZWAProductURL": "", "ProductPic": "", "Description": "", "ProductManualURL": "", "ProductPageURL": "", "InclusionHelp": "", "ExclusionHelp": "", "ResetHelp": "", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "", "ProductPicBase64": "" }, "Event": "nodeQueriesComplete", "TimeStamp": 1588264908, "NodeManufacturerName": "2GIG Technologies", "NodeProductName": "CT32 Thermostat", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Thermostat", "NodeGeneric": 8, "NodeSpecificString": "General Thermostat V2", "NodeSpecific": 6, "NodeManufacturerID": "0x0098", "NodeProductType": "0x2002", "NodeProductID": "0x0100", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "Thermostat HVAC", "NodeDeviceType": 4608, "NodeRole": 7, "NodeRoleString": "Listening Sleeping Slave", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 2 ]} +OpenZWave/1/node/7/instance/1/,{ "Instance": 1, "TimeStamp": 1588264894} +OpenZWave/1/node/7/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/112/value/281475104374804/,{ "Label": "Temperature Reporting Threshold", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "0.5F" }, { "Value": 2, "Label": "1.0F" }, { "Value": 3, "Label": "1.5F" }, { "Value": 4, "Label": "2.0F" } ], "Selected": "1.0F", "Selected_id": 2 }, "Units": "", "Min": 0, "Max": 4, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 7, "Genre": "Config", "Help": "The Temperature Reporting Threshold Configuration Set Command sets the reporting threshold for changes in the ambient temperature as detected by the thermostat.", "ValueIDKey": 281475104374804, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/112/value/562950081085460/,{ "Label": "HVAC Settings", "Value": { "List": [ { "Value": 17891585, "Label": "HVAC: Normal, Aux Stages: 1, Aux Setup: Gas, Heat Pump Stages: 1, Cool Stages: 1" }, { "Value": 34668801, "Label": "HVAC: Heat Pump, Aux Stages: 1, Aux Setup: Gas, Heat Pump Stages: 1, Cool Stages: 1" }, { "Value": 18940161, "Label": "HVAC: Normal, Aux Stages: 2, Aux Setup: Gas, Heat Pump Stages: 1, Cool Stages: 1" }, { "Value": 35717377, "Label": "HVAC: Heat Pump, Aux Stages: 2, Aux Setup: Gas, Heat Pump Stages: 1, Cool Stages: 1" }, { "Value": 17957121, "Label": "HVAC: Normal, Aux Stages: 1, Aux Setup: Elec, Heat Pump Stages: 1, Cool Stages: 1" }, { "Value": 34734337, "Label": "HVAC: Heat Pump, Aux Stages: 1, Aux Setup: Elec, Heat Pump Stages: 1, Cool Stages: 1" }, { "Value": 19005697, "Label": "HVAC: Normal, Aux Stages: 2, Aux Setup: Elec, Heat Pump Stages: 1, Cool Stages: 1" }, { "Value": 35782913, "Label": "HVAC: Heat Pump, Aux Stages: 2, Aux Setup: Elec, Heat Pump Stages: 1, Cool Stages: 1" }, { "Value": 17891841, "Label": "HVAC: Normal, Aux Stages: 1, Aux Setup: Gas, Heat Pump Stages: 2, Cool Stages: 1" }, { "Value": 34669057, "Label": "HVAC: Heat Pump, Aux Stages: 1, Aux Setup: Gas, Heat Pump Stages: 2, Cool Stages: 1" }, { "Value": 18940417, "Label": "HVAC: Normal, Aux Stages: 2, Aux Setup: Gas, Heat Pump Stages: 2, Cool Stages: 1" }, { "Value": 35717633, "Label": "HVAC: Heat Pump, Aux Stages: 2, Aux Setup: Gas, Heat Pump Stages: 2, Cool Stages: 1" }, { "Value": 17957377, "Label": "HVAC: Normal, Aux Stages: 1, Aux Setup: Elec, Heat Pump Stages: 2, Cool Stages: 1" }, { "Value": 34734593, "Label": "HVAC: Heat Pump, Aux Stages: 1, Aux Setup: Elec, Heat Pump Stages: 2, Cool Stages: 1" }, { "Value": 19005953, "Label": "HVAC: Normal, Aux Stages: 2, Aux Setup: Elec, Heat Pump Stages: 2, Cool Stages: 1" }, { "Value": 35783169, "Label": "HVAC: Heat Pump, Aux Stages: 2, Aux Setup: Elec, Heat Pump Stages: 2, Cool Stages: 1" }, { "Value": 17891586, "Label": "HVAC: Normal, Aux Stages: 1, Aux Setup: Gas, Heat Pump Stages: 1, Cool Stages: 2" }, { "Value": 34668802, "Label": "HVAC: Heat Pump, Aux Stages: 1, Aux Setup: Gas, Heat Pump Stages: 1, Cool Stages: 2" }, { "Value": 18940162, "Label": "HVAC: Normal, Aux Stages: 2, Aux Setup: Gas, Heat Pump Stages: 1, Cool Stages: 2" }, { "Value": 35717378, "Label": "HVAC: Heat Pump, Aux Stages: 2, Aux Setup: Gas, Heat Pump Stages: 1, Cool Stages: 2" }, { "Value": 17957122, "Label": "HVAC: Normal, Aux Stages: 1, Aux Setup: Elec, Heat Pump Stages: 1, Cool Stages: 2" }, { "Value": 34734338, "Label": "HVAC: Heat Pump, Aux Stages: 1, Aux Setup: Elec, Heat Pump Stages: 1, Cool Stages: 2" }, { "Value": 19005698, "Label": "HVAC: Normal, Aux Stages: 2, Aux Setup: Elec, Heat Pump Stages: 1, Cool Stages: 2" }, { "Value": 35782914, "Label": "HVAC: Heat Pump, Aux Stages: 2, Aux Setup: Elec, Heat Pump Stages: 1, Cool Stages: 2" }, { "Value": 17891842, "Label": "HVAC: Normal, Aux Stages: 1, Aux Setup: Gas, Heat Pump Stages: 2, Cool Stages: 2" }, { "Value": 34669058, "Label": "HVAC: Heat Pump, Aux Stages: 1, Aux Setup: Gas, Heat Pump Stages: 2, Cool Stages: 2" }, { "Value": 18940418, "Label": "HVAC: Normal, Aux Stages: 2, Aux Setup: Gas, Heat Pump Stages: 2, Cool Stages: 2" }, { "Value": 35717634, "Label": "HVAC: Heat Pump, Aux Stages: 2, Aux Setup: Gas, Heat Pump Stages: 2, Cool Stages: 2" }, { "Value": 17957378, "Label": "HVAC: Normal, Aux Stages: 1, Aux Setup: Elec, Heat Pump Stages: 2, Cool Stages: 2" }, { "Value": 34734594, "Label": "HVAC: Heat Pump, Aux Stages: 1, Aux Setup: Elec, Heat Pump Stages: 2, Cool Stages: 2" }, { "Value": 19005954, "Label": "HVAC: Normal, Aux Stages: 2, Aux Setup: Elec, Heat Pump Stages: 2, Cool Stages: 2" }, { "Value": 35783170, "Label": "HVAC: Heat Pump, Aux Stages: 2, Aux Setup: Elec, Heat Pump Stages: 2, Cool Stages: 2" } ], "Selected": "HVAC: Normal, Aux Stages: 1, Aux Setup: Gas, Heat Pump Stages: 1, Cool Stages: 1", "Selected_id": 17891585 }, "Units": "", "Min": 0, "Max": 2147483647, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 2, "Node": 7, "Genre": "Config", "Help": "Bits 0 - 7 -> HVAC Setup: Normal (0x01) or Heat Pump (0x02) Bits 8 - 11 -> Number of Auxiliary Stages (Heat Pump) / Number of Heat Stages (Normal) Bits 12 - 15 -> Aux Setup: Gas (0x01) or Electric (0x02) Bits 16 - 23 -> Number of Heat Pump Stages Bits 24 - 31 -> Number of Cool Stages", "ValueIDKey": 562950081085460, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/112/value/844425057796116/,{ "Label": "Utility Lock", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 7, "Genre": "Config", "Help": "The Utility Lock Configuration Set command enables or disables the utility lock. If the utility lock is enabled, the setpoint cannot be modified directly via the thermostat screen.", "ValueIDKey": 844425057796116, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/112/value/1125900034506772/,{ "Label": "C-Wire/Battery Status", "Value": { "List": [ { "Value": 1, "Label": "C-Wire" }, { "Value": 2, "Label": "Battery" } ], "Selected": "C-Wire", "Selected_id": 1 }, "Units": "", "Min": 1, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 4, "Node": 7, "Genre": "Config", "Help": "1 -> C-Wire 2 -> Battery", "ValueIDKey": 1125900034506772, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/112/value/1407375011217428/,{ "Label": "Humidity Reporting Threshold", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "3% RH" }, { "Value": 2, "Label": "5% RH" }, { "Value": 3, "Label": "10% RH" } ], "Selected": "5% RH", "Selected_id": 2 }, "Units": "", "Min": 0, "Max": 3, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 5, "Node": 7, "Genre": "Config", "Help": "The Humidity Reporting Threshold Configuration Set Command sets the reporting threshold for changes in the ambient humidity as detected by the thermostat.", "ValueIDKey": 1407375011217428, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/112/value/1688849987928084/,{ "Label": "Auxiliary/Emergency heat", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Enabled" } ], "Selected": "Disabled", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 6, "Node": 7, "Genre": "Config", "Help": "The Auxiliary/Emergency configuration command enables or disables auxiliary / emergency heating in the thermostat. Auxiliary / emergency heating is only available if the thermostat is configured in heat pump mode and with at least one stage of auxiliary heating. This command enables auxiliary / emergency heating when the thermostat is in Auto mode. The Thermostat Set Mode command with mode Auxiliary/Emergency Heat will enable emergency heating but only if the thermostat is in Heat mode. This command should only be used on thermsotats that support Auxiliary/Emergency Heat thermostat mode.", "ValueIDKey": 1688849987928084, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/112/value/1970324964638740/,{ "Label": "Thermostat Swing Temperature", "Value": { "List": [ { "Value": 1, "Label": "0.5F" }, { "Value": 2, "Label": "1.0F" }, { "Value": 3, "Label": "1.5F" }, { "Value": 4, "Label": "2.0F" }, { "Value": 5, "Label": "2.5F" }, { "Value": 6, "Label": "3.0F" }, { "Value": 7, "Label": "3.5F" }, { "Value": 8, "Label": "4.0F" } ], "Selected": "1.0F", "Selected_id": 2 }, "Units": "", "Min": 1, "Max": 8, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 7, "Node": 7, "Genre": "Config", "Help": "The Temperate Swing (HVAC cycling rate) is the desired variance in temperature between the thermostat setting and the room temperature required before the heating or cooling system will turn on.", "ValueIDKey": 1970324964638740, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/112/value/2251799941349396/,{ "Label": "Thermostat Differential Temperature", "Value": { "List": [ { "Value": 4, "Label": "2.0F Heat" }, { "Value": 6, "Label": "3.0F Heat" }, { "Value": 8, "Label": "4.0F Heat" }, { "Value": 10, "Label": "5.0F Heat" }, { "Value": 12, "Label": "6.0F Heat" }, { "Value": 260, "Label": "2.0F Cool" }, { "Value": 262, "Label": "3.0F Cool" }, { "Value": 264, "Label": "4.0F Cool" }, { "Value": 266, "Label": "5.0F Cool" }, { "Value": 268, "Label": "6.0F Cool" } ], "Selected": "2.0F Heat", "Selected_id": 4 }, "Units": "F", "Min": 2, "Max": 32767, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 8, "Node": 7, "Genre": "Config", "Help": "(Set Only) The Thermostat Differential Temperature configuration command sets the differential temperature for multi-stage HVAC systems. The differential temperature delta defines when the thermostat will turn on additional stages. There are two differential temperatures, one for multistage cool systems and one for multistage heat systems. If the thermostat is not configured for multistage HVAC systems then these parameters have no effect.", "ValueIDKey": 2251799941349396, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/112/value/2533274918060052/,{ "Label": "Thermostat Recovery Mode", "Value": { "List": [ { "Value": 1, "Label": "Fast" }, { "Value": 2, "Label": "Economy" } ], "Selected": "Economy", "Selected_id": 2 }, "Units": "", "Min": 1, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 9, "Node": 7, "Genre": "Config", "Help": "The Thermostat Recovery Mode configuration command sets the HVAC recovery mode type. The recovery mode determines when additional HVAC stages are turned off as the ambient temperature returns to the target temperature. If the recovery mode is set to economy, the thermostat will turn off additional HVAC stages when the ambient temperature reaches the target temperature plus/minus the differential temperature. If the recovery mode is set to fast, the thermostat will leave all stages on (assuming they were already on) until the ambient temperature reaches the target temperature.", "ValueIDKey": 2533274918060052, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/112/value/2814749894770710/,{ "Label": "Temperature Reporting Filter", "Value": 124, "Units": "F", "Min": 0, "Max": 124, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 10, "Node": 7, "Genre": "Config", "Help": "The Temperature Reporting Filter configuration command sets upper and lower bounds of the ambient temperature reporting. The thermostat won't report ambient temperature changes if the ambient temperature falls between these bounds. For example, if the upper bound is 80F and the lower bound is 60F, the thermostat will not send SENSOR_MULTI_LEVEL_REPORTS for ambient temperature values between 60F and 80F. The thermostat will only send ambient temperature changes if the thermostat has been added to an association group (see Command Class Association) and the temperature reporting threshold is non-zero (see Temperature Reporting Threshold). Input in hexadecimal only like so: 0x09 0x05 0x09 0x0A. It must always have four 1 byte sized numbers. The first two bytes control the lower temperature bound for the Temperature Reporting Filter the last two control the upper temperature bound. The first byte in the byte pair always refers to temperature scale (Celsius 0x01 or Fahrenheit 0x09). While the second byte in each byte pair is the bound temperature. The max/min temp you can use is 127 degrees. To convert decimal to hex goto: https://www.binaryhexconverter.com/decimal-to-hex-converter or you can use the built in Windows calculator program in Programmer mode. If you mess up your thermostat copy and paste 0x09 0x00 0x09 0x00 (for a F Thermostat) or 0x01 0x00 0x01 0x00 (for a C Thermostat). This will remove any bounds.", "ValueIDKey": 2814749894770710, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/112/value/3096224871481364/,{ "Label": "Simple UI Mode", "Value": { "List": [ { "Value": 0, "Label": "Enable" }, { "Value": 1, "Label": "Disable" } ], "Selected": "Disable", "Selected_id": 1 }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 11, "Node": 7, "Genre": "Config", "Help": "If the value is set to Disable then Normal Mode is enabled. If the value is set to Enable then Simple Mode is enabled.", "ValueIDKey": 3096224871481364, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/112/value/3377699848192017/,{ "Label": "Multicast", "Value": 0, "Units": "", "Min": 0, "Max": 1, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 12, "Node": 7, "Genre": "Config", "Help": "If set to 0, multicast is disabled, if set to 1, will enable the multicast.", "ValueIDKey": 3377699848192017, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/66/,{ "Instance": 1, "CommandClassId": 66, "CommandClass": "COMMAND_CLASS_THERMOSTAT_OPERATING_STATE", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/66/value/122716183/,{ "Label": "Operating State", "Value": "Idle", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_OPERATING_STATE", "Index": 0, "Node": 7, "Genre": "User", "Help": "Set the Thermostat Operating State", "ValueIDKey": 122716183, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/69/,{ "Instance": 1, "CommandClassId": 69, "CommandClass": "COMMAND_CLASS_THERMOSTAT_FAN_STATE", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/69/value/122765335/,{ "Label": "Fan State", "Value": "Idle", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_FAN_STATE", "Index": 0, "Node": 7, "Genre": "User", "Help": "Set the Fan State", "ValueIDKey": 122765335, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/94/value/131563537/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 7, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 131563537, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264894} +OpenZWave/1/node/7/instance/1/commandclass/94/value/281475108274198/,{ "Label": "InstallerIcon", "Value": 4608, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 7, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475108274198, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264894} +OpenZWave/1/node/7/instance/1/commandclass/94/value/562950084984854/,{ "Label": "UserIcon", "Value": 4608, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 7, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950084984854, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264894} +OpenZWave/1/node/7/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/114/value/131891219/,{ "Label": "Loaded Config Revision", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 7, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 131891219, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/114/value/281475108601875/,{ "Label": "Config File Revision", "Value": 6, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 7, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475108601875, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/114/value/562950085312531/,{ "Label": "Latest Available Config File Revision", "Value": 6, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 7, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950085312531, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/114/value/844425062023191/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 7, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425062023191, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/114/value/1125900038733847/,{ "Label": "Serial Number", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 7, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900038733847, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/115/value/131907604/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 7, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 131907604, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/115/value/281475108618257/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 7, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475108618257, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/115/value/562950085328920/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 7, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950085328920, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/115/value/844425062039569/,{ "Label": "Test Node", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 7, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425062039569, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/115/value/1125900038750228/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 7, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900038750228, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/115/value/1407375015460886/,{ "Label": "Frame Count", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 7, "Genre": "System", "Help": "How Many Messages to send to the Node for the Test", "ValueIDKey": 1407375015460886, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/115/value/1688849992171544/,{ "Label": "Test", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 7, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688849992171544, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/115/value/1970324968882200/,{ "Label": "Report", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 7, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970324968882200, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/115/value/2251799945592852/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 7, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251799945592852, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/115/value/2533274922303510/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 7, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533274922303510, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/128/,{ "Instance": 1, "CommandClassId": 128, "CommandClass": "COMMAND_CLASS_BATTERY", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/128/value/123731985/,{ "Label": "Battery Level", "Value": 65, "Units": "%", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BATTERY", "Index": 0, "Node": 7, "Genre": "User", "Help": "Current Battery Level", "ValueIDKey": 123731985, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264908} +OpenZWave/1/node/7/instance/1/commandclass/129/,{ "Instance": 1, "CommandClassId": 129, "CommandClass": "COMMAND_CLASS_CLOCK", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/129/value/123748372/,{ "Label": "Day", "Value": { "List": [ { "Value": 1, "Label": "Monday" }, { "Value": 2, "Label": "Tuesday" }, { "Value": 3, "Label": "Wednesday" }, { "Value": 4, "Label": "Thursday" }, { "Value": 5, "Label": "Friday" }, { "Value": 6, "Label": "Saturday" }, { "Value": 7, "Label": "Sunday" } ], "Selected": "Thursday", "Selected_id": 4 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLOCK", "Index": 0, "Node": 7, "Genre": "User", "Help": "Day of Week", "ValueIDKey": 123748372, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264908} +OpenZWave/1/node/7/instance/1/commandclass/129/value/281475100459025/,{ "Label": "Hour", "Value": 2, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLOCK", "Index": 1, "Node": 7, "Genre": "User", "Help": "Hour", "ValueIDKey": 281475100459025, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264908} +OpenZWave/1/node/7/instance/1/commandclass/129/value/562950077169681/,{ "Label": "Minute", "Value": 17, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLOCK", "Index": 2, "Node": 7, "Genre": "User", "Help": "Minute", "ValueIDKey": 562950077169681, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264908} +OpenZWave/1/node/7/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/134/value/132218903/,{ "Label": "Library Version", "Value": "3", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 7, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 132218903, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264905} +OpenZWave/1/node/7/instance/1/commandclass/134/value/281475108929559/,{ "Label": "Protocol Version", "Value": "3.83", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 7, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475108929559, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264905} +OpenZWave/1/node/7/instance/1/commandclass/134/value/562950085640215/,{ "Label": "Application Version", "Value": "10.00", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 7, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950085640215, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264905} +OpenZWave/1/node/7/instance/1/commandclass/135/,{ "Instance": 1, "CommandClassId": 135, "CommandClass": "COMMAND_CLASS_INDICATOR", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/135/value/123846673/,{ "Label": "Indicator", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_INDICATOR", "Index": 0, "Node": 7, "Genre": "User", "Help": "Current Indicator State", "ValueIDKey": 123846673, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/49/,{ "Instance": 1, "CommandClassId": 49, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/49/value/281475099148306/,{ "Label": "Instance 1: Air Temperature", "Value": 73.5, "Units": "F", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 1, "Node": 7, "Genre": "User", "Help": "Air Temperature Sensor Value", "ValueIDKey": 281475099148306, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588266231} +OpenZWave/1/node/7/instance/1/commandclass/49/value/72057594168754196/,{ "Label": "Instance 1: Air Temperature Units", "Value": { "List": [ { "Value": 1, "Label": "Fahrenheit" } ], "Selected": "Fahrenheit", "Selected_id": 1 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 256, "Node": 7, "Genre": "System", "Help": "Air Temperature Sensor Available Units", "ValueIDKey": 72057594168754196, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/49/value/1407375005990930/,{ "Label": "Instance 1: Humidity", "Value": 55.0, "Units": "%", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 5, "Node": 7, "Genre": "User", "Help": "Humidity Sensor Value", "ValueIDKey": 1407375005990930, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588266231} +OpenZWave/1/node/7/instance/1/commandclass/49/value/73183494075596820/,{ "Label": "Instance 1: Humidity Units", "Value": { "List": [ { "Value": 0, "Label": "Percent" } ], "Selected": "Percent", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 260, "Node": 7, "Genre": "System", "Help": "Humidity Sensor Available Units", "ValueIDKey": 73183494075596820, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264893} +OpenZWave/1/node/7/instance/1/commandclass/64/,{ "Instance": 1, "CommandClassId": 64, "CommandClass": "COMMAND_CLASS_THERMOSTAT_MODE", "TimeStamp": 1588264894} +OpenZWave/1/node/7/instance/1/commandclass/64/value/122683412/,{ "Label": "Mode", "Value": { "List": [ { "Value": 0, "Label": "Off" }, { "Value": 1, "Label": "Heat" }, { "Value": 2, "Label": "Cool" }, { "Value": 3, "Label": "Auto" }, { "Value": 11, "Label": "Heat Econ" }, { "Value": 12, "Label": "Cool Econ" } ], "Selected": "Heat", "Selected_id": 1 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_MODE", "Index": 0, "Node": 7, "Genre": "User", "Help": "Set the Thermostat Mode", "ValueIDKey": 122683412, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264894} +OpenZWave/1/node/7/instance/1/commandclass/67/,{ "Instance": 1, "CommandClassId": 67, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "TimeStamp": 1588264894} +OpenZWave/1/node/7/instance/1/commandclass/67/value/281475099443218/,{ "Label": "Heating 1", "Value": 70.0, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 1, "Node": 7, "Genre": "User", "Help": "Set the Thermostat Setpoint Heating 1", "ValueIDKey": 281475099443218, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264906} +OpenZWave/1/node/7/instance/1/commandclass/67/value/562950076153874/,{ "Label": "Cooling 1", "Value": 78.0, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 2, "Node": 7, "Genre": "User", "Help": "Set the Thermostat Setpoint Cooling 1", "ValueIDKey": 562950076153874, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264906} +OpenZWave/1/node/7/instance/1/commandclass/67/value/3096224866549778/,{ "Label": "Heating Econ", "Value": 62.0, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 11, "Node": 7, "Genre": "User", "Help": "Set the Thermostat Setpoint Heating Econ", "ValueIDKey": 3096224866549778, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264906} +OpenZWave/1/node/7/instance/1/commandclass/67/value/3377699843260434/,{ "Label": "Cooling Econ", "Value": 85.0, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 12, "Node": 7, "Genre": "User", "Help": "Set the Thermostat Setpoint Cooling Econ", "ValueIDKey": 3377699843260434, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264906} +OpenZWave/1/node/7/instance/1/commandclass/68/,{ "Instance": 1, "CommandClassId": 68, "CommandClass": "COMMAND_CLASS_THERMOSTAT_FAN_MODE", "TimeStamp": 1588264894} +OpenZWave/1/node/7/instance/1/commandclass/68/value/122748948/,{ "Label": "Fan Mode", "Value": { "List": [ { "Value": 0, "Label": "Auto Low" }, { "Value": 1, "Label": "On Low" } ], "Selected": "Auto Low", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_FAN_MODE", "Index": 0, "Node": 7, "Genre": "User", "Help": "Set the Fan Mode", "ValueIDKey": 122748948, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264894} +OpenZWave/1/node/7/instance/2/,{ "Instance": 2, "TimeStamp": 1588264894} +OpenZWave/1/node/7/instance/2/commandclass/49/,{ "Instance": 2, "CommandClassId": 49, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "TimeStamp": 1588264894} +OpenZWave/1/node/7/instance/2/commandclass/49/value/281475099148322/,{ "Label": "Instance 2: Air Temperature", "Value": 72.5, "Units": "F", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 2, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 1, "Node": 7, "Genre": "User", "Help": "Air Temperature Sensor Value", "ValueIDKey": 281475099148322, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264906} +OpenZWave/1/node/7/instance/2/commandclass/49/value/72057594168754212/,{ "Label": "Instance 2: Air Temperature Units", "Value": { "List": [ { "Value": 1, "Label": "Fahrenheit" } ], "Selected": "Fahrenheit", "Selected_id": 1 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 2, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 256, "Node": 7, "Genre": "System", "Help": "Air Temperature Sensor Available Units", "ValueIDKey": 72057594168754212, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264894} +OpenZWave/1/node/7/instance/2/commandclass/49/value/1407375005990946/,{ "Label": "Instance 2: Humidity", "Value": 56.0, "Units": "%", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 2, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 5, "Node": 7, "Genre": "User", "Help": "Humidity Sensor Value", "ValueIDKey": 1407375005990946, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588264907} +OpenZWave/1/node/7/instance/2/commandclass/49/value/73183494075596836/,{ "Label": "Instance 2: Humidity Units", "Value": { "List": [ { "Value": 0, "Label": "Percent" } ], "Selected": "Percent", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 2, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 260, "Node": 7, "Genre": "System", "Help": "Humidity Sensor Available Units", "ValueIDKey": 73183494075596836, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588264894} +OpenZWave/1/node/7/association/1/,{ "Name": "Reporting", "Help": "", "MaxAssociations": 2, "Members": [ "1.0" ], "TimeStamp": 1588264906} +OpenZWave/1/node/16/,{ "NodeID": 16, "NodeQueryStage": "Complete", "isListening": false, "isFlirs": true, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": true, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0148:0001:0003", "ZWAProductURL": "https://products.z-wavealliance.org/products/2543/", "ProductPic": "images/eurotronic/eur_spiritz.png", "Description": "• Easy control for water radiators from any Z-Wave Controller • Fits most European water radiators (wide range of additional adaptors for different manufacturers available) • FLiRS for quick response time • LED Backlit LCD • Metal nut for reliable connection to the radiator • 2 buttons for easy temperature regulation • Battery level indicator • Child Lock • Over the Air update • UK-Mode for upside down installation • Open Window detection • Automatic frost protection", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2650/Spirit_Z-Wave_BAL_web_EN_view_05.pdf", "ProductPageURL": "", "InclusionHelp": "Start Inclusion mode of your primary Z-Wave Controller. Press the Boost-Button.", "ExclusionHelp": "Start Exclusion mode of your primary Z-Wave Controller. Now press and hold the boost button of the Spirit Z-Wave Plus for at least 5 seconds.", "ResetHelp": "Please use this procedure only when the network primary controller is missing or otherwise inoperable. Remove batteries. Press and hold boost button. While still holding boost button insert batteries. The LCD shows RES. Release boost button. To perform the factory reset press boost button.", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "UAE", "Name": "KOMFORTHAUS Spirit Z-Wave Plus", "ProductPicBase64": "iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAIAAAAiOjnJAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAgAElEQVR4nO29e5Mlx3UfeLKq7vveft1+9zQwGAzxIIknSQMgCVJeSSF6Hd6QvevY2JXlpVa78kdx7LfYcGysvQ5rHRLXBGGBpClCMiXRAiECBvEYDOY909Pd93bfvrfvoypz/8i52afOOZm3unsAQxHOiOmpm3Xy5Hn88pysrKwqpbWGWUUpZYyZSVakiKwC/PEpQna2VmcTowiBjycAkHqxskg5syOKm+5hyXDqDowx1i5nLufn8KmW/yLizez0/ASfhlSBolzEOi2KeZckSLhx6X66v4TM1vjCjOvIkeFjMRgAMgqXBDcsEqUC3HxsRT5i1wGDFJQqYArR+D6jBbwwkwnnkHDb2SJyJ8pwu9hKVyOqSuQ4lTKuU1vD3YyLo+SSOP68YXiYivggBCIrLCqwAcBHEVaZt+JdcAKiiD3mbEmPXAyuHXcKJ044fklnoj6YjJ8VsQh+IDr3E7m5y7nOBD2inoGGXK+ApoTzTK+L/hMpiY7h5pgAj3AiMHc8793HlrDilAFg2HrFJ+8car7KMxfiknN2Icb2h8XqPNweFudARHeV9mBmJhVZiZL4+IRbucpIDAxiyCU6+KIaDz+kCzXNQWQ0iHFF7IIQcN34uPdxILK5sS4KJo51XE8qCRNiEBxRuA0xHx6Vxd6dbX2mUyyT8C58VhUF4NnmpBK3F+dSWAgsPfETHivElD5WhIwUcf4kxvzAnIwjTMw1YW5cU9EUPkqiu5IiDZchIIDPVjMpw72LvguXQI9CKqQUp7lUCUdsTOkk8w2Rgh2JEvp6D2jhGz+8vrhgAbZFuBURsrgAPkfwGOYjCFAK/RJgnVbtgOf4mBPVK+7785eZvZ9Whpn+KCLD56ScUzDi34icLhKW8N9AiiQZkBAQPpB3UoCAy4P5i5Q8KDpKIh6udKw4gc9c+KwoFbeATxdc42MV7lfk7NMF1/uahAnM9PrUnk1wM9+MBIPRl30J1AjIMJmY43FzUSRRycBkRewxQCP+5AeALBPWxYc55Y/QZF7MKzl/IqFvuinO9rjiWC/ehCsFzC+uVWRQUahw3UQDETxxKQMTLE5DdCDjlYwJDvEwgHyuDYsXIHPyEE84AgIILqEPmk738CSPKILtw+HL+XOpXKeEkvgxoJc7dbLyjvvjUQe7E/JjAvLO9tFgKYl1uKrcNIAGB+mO8OFMeBcBkHFnY7FF2HGvzxxOIFlJDD++Vpyta+VrSOzAXQN5iPiyU0Br1yo37EByMOn4VKdmnj0P8eehnE1gHphFVg/XGp+xbb3rWKcSSzwrmu+/ls9P+VSdEsE0ePqyXpEiyifmlOKShYnFOMwHyWnL2VqJYpBJwhn6mqmjSHker51NWlGqCGfcmRxPa3eetmc2ITke8h7CA4D3xSccpy1hIwT+YsGwPQNi4HAuGnmmjtxWhK0of7hSrA/HNj6XBwCltQ774PwBM5BhCzYnxjobK6LIOaWayf/MTT6H04YziJTwxuGgx32MD8jQESdw4fxI+HAawpZc94k0RAbI44kLxtmKZ8lBwCxckplTjoBZsLIFO+JWFeXnHEje8NlHsIy9pUMsWPA6RRz03NCiUYpcIhQhmylkkRJgeNrA9hBlI0bDzQNScXoCnYIiic7iIvlY0eWGsJJFzvr65oKKMCqopwhfQi8SzAR3QMHA8Cie1IoD5VROOYM6wGzuk6c4c0cZWoMurljYguecNJyz+cMqxYfWGXgWx+UZmD9EyuJFuCvEs6lPgsDkA4Lpn5BBgekRlyTAiosxc5Ixs3A5sSRhsX0mCuglahcQ1RcFcVuiNY9VYuoUmfh8epIK7VVhONT7Qi4P4KKPIe9ssbkvQRRMdqJU4jDgpieUxeccYr4InPVJReqLSBXwkc8sPj6Q9yChLJh2+Nmzp8KZUPBJedrAG4BjuNXMPC7ymZmVfBAHz3B6KKxm6jhzPHzahXTk3UHKUwxuXzB9BAqPtMXP+iTk9L4MGO5UJCuesknXkIeaKNhMHcWc6BMpnEAD0VGUE/KGDUviaiIRzq4PEicoKqcEalpcHwoVTm9ZOZk4JT7r/mJVeeERiLfl8hMOAWc7kbit+HEAN9wmuB7/BORREUlOYK4d6YXwJL3g5lxgMb5gVriVq4kg71oOJgIX3LGYTTjqeeEYwtJzo2CUE8SLGvLmkPcKPxb5k45Ef/iMrjxJnxsQH/uGLpGTiydqjWXAfyE/TrD1OLZ86nDVsCKUS8AHPkY+7gH68ISgyByFUIpseXMx4BfRa6b6xe0jxpiApqfqNGzYmYJBMMCfqhSaY7kaLMF/wcJl+zyw/ZSkOmcpLlVByoJkJw+sBuK8LS5nhSMwEYIc88DuaxWQissQZlXkmKc8X334mHAgmau4tJg5YRhmRU758ho/wFNeYJbnUVbU0dWfPKVD5hZEMWJBLrqINp5YxXkb+cltwaXic3/RCr6+xCTC9RK5YQmJamTWgtly9wArvkjgxnNgUuuKOAclfnR8RK25uQiNKAOhKfRcIbFRgKxgJVeDz5yKB3DI+4/4gJwKHweEnClGwY7sX6211jpN0yzL7Bp1HMdxHCdJEkWRj3PxfgP0pIYPfi5tuBfZ7yKwiqDhbJXFeZ6tPERWYW5n6MgYk2WZRZILb1EUWRdkWWYtn0xLAGGfhpo4lp9hhFOe4uRdjNikY7GJmCNEjEM+H3G2POrwUYUpxQwVTj1cTpAsGxaVm4V3aoyZTCZpmnLVXAADAIski7Aoihy8cAojUvnswMUWJecAEvFUxAj0rJsfkDZEGtErAUoRCqK2hFhsHgC62HymbBysYo7w+Y/0G0Cwy3eErW9wZllmkWSjmgtgOHoV9M7Ms0UK8R0EoZLTxcbkQDNxNIMEmiIA4h2Jg4bXC2OiQFwJm0yECDCMBtryse5otNaTyaTgu4Mhj0g763JgskAslUpOsMB4KKKvWBMgEJ2FaTjl7LfN+JTnBOFkXJxATE8zG5JWPsTz4zB8w1qISllIuSjlEhmm4ZGA96i1ttN5F73sBJ/IE7bPTLN/SiXU6zkD6fnj8MPl8ykVbMAsy1ziE+OfGADCzLXWpVIJEEDjOBbB6pp89ubinT58OBe32uehPJRZiHX/NPEZpSDLzHA4StMsiiCKonhacIIrzhwA7Fze5kTIr0udSqPPLFjkHqbAjcloC2TZIn37ck1YBxAvN4pduxUUSWQbbkuGYpZlk8nEVWbZZDgcHh+P0lRHkYqik1mRzWvVajVJTl6ZgVNVYNJjEVYul+M4dpbxYctnH1FTn6NFVtwOPoHp5N1XZvqGd3aGwTFzOlnkVMFeTjXTJ83t3zRN0zR1sEjTdDgcDgbHaZpqnRmjoyjGYcaWKIpKpVK1Wi2Xy4GM5us0juNyuXwyR86jsyCrgtCZySFgQyEVFsxlRTQ5bVosDqzTsjpPIYPbGD2ZpFmWGQNKgTFmMkkHg/5oNJpMJlkGOks73b333nu33mg8++Vn642GUtGJHQCUiiy8avVaqVQiydEXS2zRWkdR5EAJ0hzu4ZazjcBcxDLSFRPuAFBuwhkTQLg8CUR1KoSUzsTJn/Jc4hEZfGKLGsl2kRYRjDGTyUTr1BiwJ8fjSb/fH45GWZoak6Vpur/Xef317//ox2/s7u6Bgd/49W/9k//lu+3ltXJSAQ0QARhQAAoipUAlUbVardfrNsHxTn2Z3RhTqVTsilcuAUleED3lc4QzYMA4RcrJAinpFTxQJclOlGlmk0CCEynF+VZASH7MJRTjv09+e7k3fR2B0dqMRqPBYDAcjrLM3vWb7Ozc/cEPfvDDn/x4NDqu16tJkmSZPup1V5dW/tkf/LPnnnuhXK0pZa/vAAwAgIpAKVUqlRqNRrlcFtwjAQKmV4uVSsVO17gXfE7hBifI8xnNZ3+RIeBU6EtbZ5h/FJmNfR6KTyo8YBykYLoyPhwOB4PBaDSyd/0mk8mNmzdee+37P/uLP890Vpmr1evVSqVULpezTB/3B0d73clg9Jvf+c4/+h/+8eLCShwnAMrdzoHpvL7RaFSrVTwh86Ecw71ardqLTWz5wNgL2yFgkFNNaQBfFfLI/4BCSkxiQpnZVuyFVPIRUzA5+qI3MbovypL476KUq9FaW0iNx5MsS7XW4/H4o48+/Hff///eevuvVazqrVqtUas2Ss1WvV6rRnGcZXrYHx52DnudXnfvYGt987u/93tffubZUqkWRSUrmpMkiqJqtdpoNOyUS5we4GITosWWvUQIqBxIF4HIxP+ShoF5kfBcIe/PpxsUQ5Wve0wmqi2GaJ/+pJWv60BUdoLZRU5jjM1YaZodHw8Hg/54MtFam0yPx6O/+eUvX3v9++/853fictxqNWuNcrlaHo0n24+s1xtJXFJGgzFKT3R/MDzoHPU7R93dzmh0/A/+2//uH/72P2q22nGpAkoDKAADxtgZfaVSaTabZHk9XNI0rVardhGVmzRsAZ/7+HDlyCNOJG56MHkX/TQzs3LH+/I0jyU++IM0zsJhyWdHzop3R6KUWze3lZPJ5Ph4cHw8Go8nADpLs0H/6K//+q++/9prH1+/VmlUanPVer1aq1ZbrVqqzV/+1S/rtfIzz31hbX1BRaCzyOhMaz0epYeHxwfd/uH+YW9398Lqxne/+wdPf+mZcrWqohgAQIECFalIKVUul1utlktwYnIgPyeTSb1etzcTMVZE3UlzHlBEe4ZZceJPceVd7A8YYgJ8oNiMeybPQO+2F6213Sk1pdGTyWQwOB4Ox+kkTXWmdXZ40P2Ln/3HN/796zd2btdb9XqzVqkljWatVa9XayWV6MEo/fl/+mAwHEdR9siFlWeffSKJjTZ2g4POUhj0R4fd/kGnd7B3qAfjv/ebv/Xbv/3fLy4tqaRiVAQKInggW6VSabVa9qKPGJNnFZgOiWazSebyBX30aZTZ9wrJCHCngIUK3lYkDpTw8Ark++IMiVJ4552tH4/H/f7RaGQv90yaZoedvR/+6Ic//slP9nv71VatVi/XGpVGozrXqpcrSSUpxaVEx2YwSq9cubfbOQSTJVH69ZefKZU0qNgYrY3RmckmZnycHhwNu7uH/f2DQae3vrrxu7/zT59/8WulSi2KY5gCSynFcyKP1gZd0duxMTc3Z7GIE9FJevJfuc/0QqCV6JTc5J1kHF/exSWQCgO5GVcW5AwMWz56sSPSI0z3ILgarbVdQRiNRlrrLEvTNL1//96fvPEnP/nTHw/H41qjWWtVa42k2ao1mrVKJS4lcZIkKorGafTRtZsff/LJaKiN0QsL9ccvbW1vrcTKKIiMMnaFQqcmS81kMjk+nhzsHx3sH/X2Dyb90a//2q//T//zP2ktLJbKVSezm29hZHCtsfz2uNVq2Zp79+4ppVZWVvjdyfDUAvIACNg8wEoGFq/hUCCKcRQSQ4iTMMzEN0o4KMOZMSA2TFeALKQsWZZl4/FoMBiMx6nW2mTZeDK5dv36G2+89uc/+zMDptxopJk52O/Esbn8xMVRNp6br29eaJfLpePj9Mq1a1c/uTccQxSZhcXWpYvbq8tz5ZKJYzVNbpkCYwxobbQBnWWTiR4Nda83PNw/7O/1jjq99mL7f/3933/2uRcr9UYUxQDKpkG7fCqGZ5wHYZoltdblcrlWqwFAmqZvvfVWvV6/dOlSrVbLhZPg5CQwewn7JYcHnLYJx5lzoCJkpyUuEqiBhaJwX870bgVBKWUMaJ2NRqPj4+F4PMqyNEshnYyvXPng3732vb9++60oiRuN5nic7d7vDAfDqBSVG8mjj29/fO1mFMHCUm19Y+XD96+PMh1F8cry4qVLW0tLjVipWJlIGaUiUAqMBgBlABQYUJkxoCFLTTbRaZoN+6PD/aNe5+iw2xsdD37t23/3d37nuwuL7aRcBniwLaJWq1UqFaw7MQVWEADwZKvf73/wwQdKqUceeWRpaQnvHOS2cmzDCYcjT3TZSTI+Q/GFxyJnz198wYxTmunlnlM7TdPhcDwcDieTcZZlaZZOJpP3fvk3r//gtXfee1dVVK1ZPx6Yvfvd4+FxOVHVRmXr8oWLly+Uy+rW7XvdznGappVK6fbte+2lpUuPX1harJZiEykDBkApUJGBGJTa3985OhglcalcjtvLCxAnoCdgtMmUzmAyGacj0zsYdPcPD7qHh53e2sLK//77f/DcV18sV2tJUra7ZUjQwgGDYAum42d+ft7S3Lt3b3d3FwA2Njba7TZeJAuYFNcAA3QRvxTdQUp6FZMm5KFdUIHwxK64VKIA7nIvD6nhcDhMU7ukng5Hx2+//fYff++Pr175sFotVxutXu949353PMmSUtRaKD36xIWtxzbLtaScmDKYVKdpGisoa5MZA0mioiiLlI4gBhNrlRoVZTrZ2e18+OGVzu4hmDiJ1daF5S888SgopVRaLccKIqMhyzKdwXikjwfjvb3uoNvv7/aG/dG3fv2/+d1/+nsrK2uVSqVWq9nwE5hg4QNbkiRpNpsWZB999FGn01lcXFxZWWm3287OgQkDd4dvmuSbFOVWcUSI8GM+3xIrCTFRg9uoCIF4IJrbPcXgaLIsG41Gw+HQ7htO00nv6PDnP//LH/7wjWvXrldq5aSRlCvVo+5xd69vQJVr8cUnNje32+WaShKVKAMalIoNZACxAgVKg4kV2EUKpQ0AqLHWN27fv3L15kG3B0aVk9LGxkZ7ceH+7v27d2+tr63u7uy2mrVnnnlyYbGptdE6yyY6m8B4OO52Dnvd415ncNjtXbz4+D//5//H9va2bzJAoEAqJ5PJwsKC3dbc7XY7nc7h4aFS6uLFiwsLC2S+xY3M/eLLmL6GiYhTLi7+6asUp+TiqAKpiASiPHwwYD74cg+PrdFoOBgMs1RPJpPDg/0/ffMnb/zwjd3OXq1aWViaK9fL1YVqrdEYT+4c3e3FpaqBuN4q15pRrEAZAB0bBcYuaBoFShkTAQCYRAEYpUHBJzfvvfvex4PjkTFQq1W3trbm5xt3bt/7xTt/A1kEOq6UG+PJ/n5n9Oaf/eLy5e3Ll7ejODLGRFGkVHkpXqhVm0sL6qWvvvyd3/r7GxuboiOI1uJBHMdHR0c2Ic7NzX388cedTieKomazGUWRXZUIuBhYxBLjpSjGg+YFX7wmYvYMRcziASQFMiM/RTZzQn5UKaVHI727u/ev/59/+eP/8CeDYT+pJNV6tVop1RqVWqtcaZSjJAEd376995/fu5pNJk8/vf3UU4+oSCsVKYiUMgA6MpEyoJUxCkBBZiDSMRiTGfUX/+ndW3d36o369vZ2o1G/cePm7u6+McaYdKW9cPmxRxcXljrdw9t37uo0AzO5sL2+staOIqUyo7K4pOrPPfO1V1/59lxzAQBAAQQHZBhbWZbV6/VqtWqM2dvbu3PnztLSUqlUStN0aWmp0WjMNPhMZ3GPuN5z9wqdZOJUMXD9RVQtft13tsIHWX5by4NipxGYEEBpo3765p/+i//r/9w/uF+uJY1mpdWqVitRVFIqibWK0olO4tLh4fG1T25sX1hZWV2MY1CglNEKAECDSQBAq0yraHCcXbl57dqV21EGrbnaM88+O5wcj8bmwyvX9vf2lYEojldWlx6/dGFhoVZO4tiUJzrTWoM2kVIQxTdv3FqaX1prr3312a+99NVXG/U5oyP14BNHGuvolAI0lwpEdGOM1tomvizL9vf3LY1d8VpfX8c7pIvY/FReEwBIZmTicZGeONiL8HTEBKMcTJbG7Q/mM0oCLKXAGDCgDOhev/fav//jn7/9l3E5jeM0iZXWar971D8exVF04cKqMRmYKIqyB6uVBpQ2oIxWClSkAXr98Xvvf3Lr1u5xlsaQLM03nri8sbnR1gaufHLn2vV7WaZXlxcuPrI6P9csJQkoUEopnWkw2kAEERhldDQ4nPzmr/29r7z48uLcvNLGQAQKALR5sJfQa1XRcfisBVa9XrdbmY+OjobDoTVIr9dbXFxcXl7mWwUx8yJI8sEgIX6aGbemTlLiRRzxLp7oEKOIGPVpQnhaPO3u7na7Xa11kiTlcrnRaDQaDQKmPIhBWXQZmK/P/Y//8Hf+zksvv/bGH93buwF6DCZLkrJS6Z07d+bn6uVSkpTKSRIbo8GAMfZflEK8f9j74INrN2/eTzMTxWpjef6xRx7ZWF2slHRkYAzRZJT1eoM4hka9vLzQSiJlTAYQGQNGawOgIFKmVKu0vvb817/6/NeatXkND+IPGAXGgN3yINmBxyfyE/9VSvX7/VKpZIypVCr9fn8wGHS7XbvNplartVotMbWJ45lfBhIyORXixuDPtQR5PmxBsXwc0MrXNsuy+/fv37592wYq/NaDUqnUbrcXFhbELeEndjFgjDFglIrHevjj//jGz37+p4eHu/vdTr3RGg8ng/6gWks2NlaTREVKZRqU1pHRmVYH/fEP3ngzU7GKko2VhScvbbcXa+VSKQbLGNIsvrd/+Fdvv3s8PE4i81vffnm+XgcwWplMaWOU0uV6ZeGrz730d77ySrVcB5iuJwLYVdSp7qFLP/wTu4BELDtJqNVq5XJZa72zs3N4eNhqtYbDYb/fX15efvTRR+3tSHyhTZwr1gccbX8m3AHumNfwn2KkEXmKJUDAU6SdSNm51OLi4sHBweHhodba3lCzcWtnZ0drvbGx4TS04Ov1etVq1c5kAQCUDRumrEq/8epvXXzk0v/7R/+y0ZjTajI6HrWXWqVSFMUwjSORicBo0AoOj/pJJd5YXr702PbyQrMaQ6RiBQBKa6W0UhqyqJLMLyyqw6RRLcUAqclMpDITmSyZry999YWXv/r8S/VyEwCM0WAz3hQP4EcSCVeYAF+BEYRFUXR8fGzfaTM3N9ftdm/fvm232O/s7MzNzS0vLxObi2MyUM/9qMJBxQl6zrl2mMnMU24i5Ral8Eja39+/cePG0dFRu92u1+ubm5vuBggATCaTbrd7dHRkvdJsNh/clFWRglhBCqCNibVSB/3733vtDz++/p6JRgAGzINkpFRswBjQYEyqo09u7ux3Dp956guV2CRKqyhWOtKQmshkkBwejd778MMbN3eyTCnIVpYa3/jKC0mtpKJSs7z0yovffPHZr9WqdWOmt3o8izuBHAdB5Il/bdCKoijLsmvXru3v79vMWKvVarXaU089FUXRqR6jLVK8D6wSujPAy9eET7CcABzlaZryFQSSl2H6Fg08wRoOh91u9+7du7du3RqNRnEcl0qlra2t+fn51dXVer2hVAyQKjBgSgBRakYaJm/+xY9++rM3MhgZo0EZAGP3vUQRGANaxaORHo30tavXIhVVK+Vmq7m20gIFe93+rz68fuv2fQMZGNNemrt8aXttdakcN1ut+ZdeePnl575ejav6wQzKPLiQMDE2AnhA4zvApsDxDB9Y/9pZfJqm/X7/xo0bpVLJPkamlLp06ZKdxfsyoOgysd7VGMNeY+SbnYnFx9dHif/6CNxPDKlwqiVTjcFgcHBwsLu7++abbw4Gg6effrrdbgPArVu3Pv7448uXL7/wwgutVmtpsR3FkZ3PA4DRBiDSkL7/8bvf+8EfHo32jcqMMgYg0ipSAAq0NmDUJDW/unLznfevpplOIHv2i5f3O72bd/c0mDjSq4vzly9tL7cXy3F1cX7l5Re/8ZVnX6okDa21iTKlDJhIPZhTGQO5yS9W3+VxbCJyTJDkw581Zr1etzvPrl+/Ph6P7cu6kiRpNBrPP/+8jVg+AM30Ly+53Q0PvYiwEGlcsfuihsNhmqY2ApEXKJKZH27e6/W63e5oNAKA119/HQC+853v2LZ2gN6/f/8nP/nJSy+99Oijj8ZRtLK6On0MwUxNAaD0zv69f/v9f3Vn92NtJqBjAwoirUArpYyOtI5Gabrb6f3il+/1Dofbm+t37u6YSC2vzF28uL622C4n5ZWF9Zee/8YLz32tmpRBK1CRMcYorcx02YOpT8DBT3EyHwE3qb12juPYXk3b29KlUimO49Fo9JWvfMXu4jrDdMVXTpbISCTE6YYfixcLXEleL150wHRU2YeJLYENzuPxeDKZAEClUnFvPQApdfZ6vQ8//HB5eTmOYzsb29rasrNUM73nOjc3Nzc39yA7GHPnzp12u91oNACc1QwYtbq4/rv/+H/73uv/5r0P3wIYaQMmi0DFGgBUBEqXSvHq0sKrL3/l9u27lVql2Uray8uL8/OVOFld3Pz6S9985unny1EtghhMatellFtA8INgZu4jB754JtaMRqN6vQ4A9Xq9Vqs5y8RxfOfOHbc9kEwzeBAlzuVoeUBMUiHmAqwQKADDOEcPbuWD7GQyGQ6H4/EYv1PKFjsPSNP06OjIPqJpV/xw6DbTK6O333779u3bTz75ZLlcvn379vvvv7+xsXHhwgW78a3b7b7//vsA8Morr9RqNWPM3t5eHMdPP/00lR8ADGSQ/vjPXv/zv/phmg0AwECsFRiVxVEEJjYmytJURWZilAKITbKytPHNl3/t2adfiFWsjFImAgCjNEiwwD2SSl/oEmNbkVxpuxiNRo1Gw96b/+CDD+yTjDY5lMvlb37zm3jdYWaZmSW9V4W+LGakedLMUGnycyxXbyFlH/7EbPFM3F6zKKVGo1G/39da2+fv8ANPruzu7v70pz/d2tra2trq9XofffTR3bt37RvMSqXSxYsXL1++HEXR7u7ucDjc3t5eX1/nVxIAGowBiLXKfvHOz1//4b8djg80aA1aKQUqAvuUvIl1BgrK66ub33jp1S898UwclcFMF6PAgAEDnHnRiz7fwamyp6Oxl9W1Wi1N006ns7u7a0dylmVxHH/rW9+am5sDKVKcKgmeeLDg67hJ1vPFHjEyicXui7KQwnhyDAHBS6HXSg0Gg8PDQzvg7L16DtY333xzNBo988wztq0NhHYPyf3799M03d7eXllZIW9McKYBpQCUMp+sGxIAACAASURBVEZDBpG5euOjf/NH/3evvwMmNTrWSkGkAQxAdWPt8W+9/OpTj3+xpKrGaFCZnV0YY8A+5WymPEE+wD85GsQpfEFumI8t/X5/bm4uTdNut/urX/3KvrLL0jz//POXLl0i7sYMpeEn3Ghxf3PA4hS4nnQpcodZxWpo71s5nra4JQPHFvIIc+8PtrdUx+NxrVabm5uzC1fOB5bm3Xfffffdd1955ZVKpWKj3b1797TWly5dsheJgYHo3DL9z+zs3flXf/gvdvZvGpVpnUWqemH90re/+RtfePzJ2NiN7WpKfDI3N1MmM5Mdrw8nvoKQcgf2xnO3211eXs6ybDwe/+IXv3DTCQC4ePHiiy++aO0s+pHMalxD78RLVM+XAfkBhhcwtAGLYaPR6PDw0F7x4c0I4tvuHKrMgx1LJ/nRjj87SVpYWLBP4RHJ79y586Mf/ehLX/rSeDw2xnzhC19ot9s+7bioJ2KA0pAdHu394R/966vXr25feOTVb/zdy489pUxJAYChb0QO5yb+kx/zKBWoFPngny52dLvdZrNp3z7yy1/+0o5tY4zWut1uf/vb33YvoSQAIhEH8qgilA+Mhkc5OS1yDJeA2wAgTdP9/f3p0+snwmHouBmVrXFMcOhylZPJ5Pbt23a3ZLvddu/qdD0eHR3t7u4uLCzY3SMYQFYAO5l1LySW1TQKAEDpSTbudLvtdhs0AMTKgDbakReBThhPkIeLWOk7wFoTzs7Fk8nk+Ph4fn7eGPPxxx93u11jTJqm9jmzr3/96/atJJgnNUXhQodXINCJjvH1zcFqjOl0OqPRyI08HkJdsrMvN8eYxlDDiNRaX7lyZTAYLC8vr62t2eFIJCTR0ZYsy/r9vkX5aDRaX18nwpw0MfatVtoAGIjAGAXmwW0+Y+yHrkRU8Uoe0jgxjkw+Pr6zvpDm6geDwdzc3GQy2d/f/+STTxqNxsrKytLS0u7u7pNPPtlqtYo4tEisSbCqIozwT4IVLDFJhUQCALALCi4sE0qYzgOUUnYhyr4tmIhOwpjt9/Lly9evX9/b2wOA5eVlu7hA5CTqjEajnZ2dpaUll3/v3bu3vLxsF2aHw2GpVFpcXJw2yeDB1hkAZcBExihj9QWlZk19wtGI14TBFG4rysADWBRFCwsLTz/9tN1HarfTjEYjvIuGoIJEHFLPjZxbb4T8WAeSNYOXCQFK+/f4+NiiCkcgLJOazr6t5lEUjcdjG7ocWx41bc2jjz7a6XT29vZ2d3cXFxfr9TpZqSfGjeP4xo0b9nFQAKjValrrvb09u0hWLpf39vbs+9DgpNjoBQAaFCgAM11LEF2IS9jxkMdHmNtMSjEouhqbMQDAZr2Dg4ODgwO7BG1v8vgiSIA5sNshQD4rRzwNDGeksUjv8OEobVqx0tt6hzDeO6Bntuw+GRznRBPbrhcXF9fX14fD4f7+fr/fFzOLaxVF0QsvvPDWW29NJhPbvF6vz83N2UVCY8zi4uLdu3fd128wK16IPD4CyEOBEIQpuYPIT1cjOt4Vu3HIGJOm6a1bt3Z2dobDoZ2B4O+yYN+BNAtyBEQG5/0ImIO5MhwBTgexnrOyL0RwTQhqnUz2wF7fOXiRb4eQWTZm0mq1Njc3x+Nxp9Pp9XrudQbEQ7aLarX6pS99qdPpQB7odhjYKUi/34dpjuaY4D9FQ4EfbYEa7gVgYMJ+JWYnxVUmSTKZTNwnx5RS9t1a9qlr4jXcHYkXYX0BIHENfMmSBCd+9REeIra4COwo+Wxa5GaBlSSJTYt2+zaX0AlZq9W2trZu3rzZ6XSMMeTeqpPf7tBaWlpyHMw0QR8dHb3zzjtpmj7xxBPNZtPdEuCyYTtgH/Nx7PNEuEbESvgskYcoDgBRFE0mE7vDttls9vt9O4xNfiZjWOQTuwu4PnIGdaRcEwIyNS1ijaN0P23soR1LO8tEQY0xdheyfeKUDFkisDGmUqlsb2/bLX69Xo8MPjtS7c0y3IWTtlQqJUny6quvbm5uYguQ8UpCBflLhrvvrA9VvG2gUjQjYYv963zh9n3YXTTiQzu4oehrX2UkGk7k6zMo15b8tMAirGDqTh8oDQq/drXJ3u2yadF4hpStLJVKFy5cGI1GdvsouRQ1xjz22GNvv/227TRN06tXr169etV2lCTJ448/bh+WwlYjenFzic4OQI0jTDQgb8h75DYXizWaW9OJosiuVC8sLNTrdfLSNtejcwfuJSy/sXveiawzkx2XPqyPWxEFdjVgAef8Z/K5HD/oYaabIZVSdklT7NRVlsvl7e3t69evdzodpRR+j48xZn5+/uLFi7/61a/m5uY+/PDDvb29paWl1dXVarUKAK1Wa29vbzQa2ZtFHDGiNc52IB4XRO3MGp9frGGTJJmfn69UKvaxMKKsj5uYVXjlg3Ussb1PMpy/MRB9GdfOkzBzjDPMh8Rhoowzt7116r7pYPKXEe64XC5fuHDBzreUUhY0TqTV1dU0Te/cufPUU09tbm5mWXb9+vW1tTU3sb1y5coXv/jFgGoiIMgpHorEyoK4CQxgJU3scEMypO2nMfr9/sHBgd24THaLEP8qNonkAMANEx8jI829AvoE1MavixX7Aj8oCWdjjI1/pVJpMplUKhUjTVGd8NVqdWNj486dO91ud3Fx0b2k36JnfX3dLbhHUbSysmLvpl29evXo6OjJJ5+c6W/i9ZkhDcdjflZk5WMophEyxkT+eDTapRz7AA95DbhBSTDw1/Lk4Ms9sAqeaES4QB4ZuF6Mdu77fTYCu4zmegQ/KAlozDSBAkCpVBqNRu4eDoGp42nvWtj3Ji4tLYmvBHKUx8fH169fn5ub+/KXv+wuL3yyiVHKF7TcMa4nw52c8nXqG+SQ94urIazcBMvOXO1Zu+ODkGH/4r+EAPLesSURG+ADQkCE9mlIVFJKfkkE75FsnhHtaMnshcx4PBaxhQWYn58fDocHBwd2VoFvOBIDLS8vYxOL+CNQmBmlZp4Sz/qKaD38qBXvkXCOoshuU7Y23N3d3drawvcYuH8DYYXLBu6q8FQlDCaQMEqs4EzJWRl0KxBHUM7QGDOZTOwGefz9CFEAAFhdXa1UKgcHB3ZRXvQfqcfzQneKHHBwBChdiAIGL1EA0pYLLHLwkeHY425mDAaDKIouX74s+iKM8jASTg2ssBpigncr4Di68jzos7gYimxxqw/uvSCkdyyhnaG7x3iIkBwEEEQMd7kPUlwkH6qIAcW2cI5i0LTJ1sRxvLm5aW/G8/BTMDWJBF5g+caKGA+4pUSTYWRA/sFwyCOJSjmdV3Ji9wIjO6n3IUMpVSqVNjc37VOHnJgILDLh9ZgeJMzxU+4GUbgQC2BTG4ZaAhdSMKXJ3wpbXl5+/PHH8VDHqvGOAoXY5GS5gRfiyIJzRsM2L5AuHaV7+paw4pWG4RVPjNwTiG7pjwtmfzabzYWFhYODg3K57F7/inshP7nYXKriTYo0V/nrJGxG3IrggJuLnFXossYYY69g4ji2m7G4xcjsiuOSF2JM6gnSwakKkcl3gOdJYmp3B9wT4sTLGOMeOPENd+cJ+1jOwcHBcDh0OZpHi5nHpFIUvmBznwGJ9Xz8OQ2GkRiK7MwhiqL19XU8Grk7fD4lhcAd7L1CLjcwv4rjg7sQm49375YbyAjgYvGfZjqvF4cUANgHftxGF+5CW5RSm5ub/X6/2+26BTZjjF3O4fL7AEFQRaAj2pC3mtlQPMsL8ZTxXEq7A7cDIBw+OCJJj04qElmNMTRi+aQX2/M5Vlgrnih9KOGFPCthWPQyxtiLRLKn3tG7ylar1Wq1Dg8PB4OBb8YDCAeYhltWrBH5hI0c8LFBVzzhmBFghVFiny7hNuTcfB5xDcV0pJSKiECB+MHb+0zA6THyCD3WASSjEye5XpxpXI+TyQS/jY0YAoNyY2NjPB4fHByQpQpgOPDJw+k5LkFCHmGFmYhpgRtNJBMNy3u3ZG7lb6abXH04tvFCl6ExIycKIcDdY5qAcCp/GchpCE+iiZKmtI4Stx2Px/wSkmgEAOVyeXV1dWdnp16vz8/Pc5Uh73XMR7SjSCMiSTRggLlIzFGLbYJrRBl8N9cBYcjHnHTEMWB/njysR8IV7hUHIZOPnyIQuUXIAf4SHw8YkIcF7w5XAsOQm8gTniQ8rK2tAcDBwYHNC5jMzbdw+OGhiIco0oUogOhFXjgmuEf4WZOfLXAafGODH0B+YBPzEubY9QTQQNaxCPS4to4F+YsJyAGgPX1q+kgMH3m+SAB5QBOvkFZOPLdlHjzAtcQbGxu9Xs9uYnZFxA3HEKEhczUiIZFclIfodaocxPHhI8NhnnwgmMd4yBufo0d0vT2mj1KR8CDK6gOyGE4AmRWbzGcRDlbSCntCxDEAuOV48SkMx7PdbidJcnh4aGf9BO6YmByLI4GjCteLYpCGTlPFsh4uxMGij3HB9ZiSf8GVuAP3gnGGBVMs+T5g7pMePC4kBEQyPKZP+kArJUQ+0T2BYWfyOZdv83IMLaocYkSvK6VWV1ftU3WOA0xThi/8kBqROafxaSTqyJGN+czEtCits9twOLQ1eHc4F554QcQfpiStIhEKPtsRZchfrABPhY4DeXhGTBM+T4ihEdhFH0xfY4eDFlbK2cLuZeh2u+7y0MrpUgYwnIkFm0uUPGDbU+nuoydhhhMbY8bj8Xg8rlQq9u3cbgeRYZlXBACXmYcr9zMB5loRsD5VObEINR8N78XXO67n/YqssiyLomg4HNqHM7nJACCKouXl5fv37w8GA/dWOyjgfvAgwAcLwhM7kviGpyHiM9Es5Cz5aecG9uEcd8q9aY1zJvGJezMgxgPD+niRNhiw2DTOCsTTOISQkOva8teKkmmBr54PA/B41MYt+6ysb2K+srKSpil+DlEcnUSFAIACheMb2PAgQyisY8DNttg3BsRxjN9Sblu5yTsJV0qapCs0UQaPx/HfBJOSxlhQN5gwtgL6QH7MAQI1t6arwQ7DP51ievoSaawbFwMPfbsQb19iyyU3xthH6Y+OjkajkX1QWIwQYSTNxBkJToDMzvn4kCQGEuwa3JG9wZUkiftsgtNLoVvRXCTso4J6cT7CckO4fZFZDin2rUu+wUdCFGdia/CD1JCPfCSekYsdpZQdtbx3y1Nrvba2dnx8bJ9iBQmyuCGJZ76xK2oBHiMTxPCMIbb19WgfFQSAarXq3tmHJVfTd1GLnfqMAPkYIUruWJ34gDgp4G8+VrhdcN/2DR/E+gFjYRTivhxnzEqxuA1oGDgCu5cB0NP0ePvh/Px8HMe9Xs8tq2LQiPcKeRHHLj5LQOkzAjkrKkgkBIQGN50izwkSR7tn6iHvUOICXMNZkRrsr4hLRoyo8ukPnxKbcKtF00JkJWblbwoBhC2CJ9ElgLDOu8DYctzc8cLCwmAwcC+5I86DfPEhDFuSUPqCUKC43sUQSMgAwL663L693fVCYgRMb+fjBzN9DiWiKmk6hI8xgXBbLaw/F7RI8aVz8hNDx+SjDu804HLy1+bi4+NjjFFSlpaWxuOxm8LjAsiCvk5xPRkJvBUh4JbBrJQ0MyNN7DpCkiTk2UACDowA/JQl+DEgSstbGTZBp8+riM18HuX+9h3j1/OZfPZ0rPBzbeAPbASCOPtgzvjtNLa4mYcLjbaVvfljX7pydHRkF7REO2K9MG6wHXzGwWdJk4DlIT/AuFMmkwl5ixjhI6IKANyX94gjsNFEeUDyskKTIltzcq2E4xj3FuHC1cZNFLuacBdlOJBi5Uk9l4R36nO8qLwt9gNrBu2rwTo2Go1erzcej907XsVCBsZMwbBqIlsysEXj8+Z27de+UptIEsCEmcZvF9uMZ+ZAEAPIrYEY4SgjyMOTw5ZLxrsnTYiSSilyAcKNi0Unkcl4ZmDYhUR+7iErwHA4tM/7GzQld7ef7TvQ7e4//m4tYgQfrEkrMlpw4XzIWVyJI6J9n7a1qg+pvC+FAnm5XOZwBAZxfMzBxyGFz57cK+TnsINJN2Lh1nfHcRy79zBxMvGpCkAjAzw3tgD5xpnPnXWPcTpsJUlip/CkWAL7alf3dhosqspPIEQTBdQXgWhQ7giMYVtvplnbXvSJz3PjJk5skFxmX5DJe+RKcV1EhrzmZO2bSGM8C1ciO5GG8OSfanGW5Zo4Mvf8ND9Lhhdm9UC3KBeP7V87zcKByh3bz6LYqIaFJAKTUyKqCpaA4pjGFrtvzL7UikcBEqTJAZGfzNydGMTjxL9FdHFlxusJzlMIT/cKZK4zGWeQBw3Bny1uvdRxw+DDoc5V2iUPe4fH7WB2odqisNlsjsdj/Ho3kk3EiOuLWGKEBgZWXENA5iBlcY+t4WjEwMORZ4wZjUZ2Id430+eVxQtpnlu3FLXyjUge6okdSU/k7f6YP0YYBopC83qdf7UkJnM1AdOY6XJzqVQ6Pj42aIEUi91oNNI0PT4+5s1FiGBb+XrnBgyEdlLsbRn3tTMCo4C+vlNRFDUaDR7ysZA+3ItexmS4JNxb5GfAmpjpTLzbiCW+0oiDW6GLDo48Mhw1ej8bVxXzx8FSo3eDOz72Red2/o5vDRHtRDQHxp7PhuJZM51O2bmUyl9/OacWTE+OLMsy+y1MmwfFFEmOCVlg/PDms9/dIFqWEBRR0l4Yix9KBb9XsG98sVBsSH7aJlYA+9ltgmCLM7sLwK3RQ36Yin2FIxmwEUh0IaxgGqWiKMI3zh2qipiajFVb7GsQG40G2ZGMyXyuLBIjCUFk8qPKqUcMSsJjwNCc1YOeosjeRhDDAA453II+z/FQR1g5SVwSsffI8KOqKj9zqtVqdtWR8wEGDtEUAdP5dLGVdgnX3cXDsdlZLIxO8IxYd4FsP0p42sLV9HnEldynaUQpRRCQFEAs6Btb+IYDFtQxFP3ELUW6szV4zYKD0s2oMLAwgWtiX8Ft5++KzdZ5DZbB52yiDk7KML1EtbuoyRN/JNo5qxIXcC2IfeyOIPtdY7Eh7gJbmOguHnNW4B7/ImqDPyr6LCV2TDjgHYzcuGb6ATCfgcRi0ORDsZmWKBvkn6smTeznT+wEn6hTBDe8hH1gpg9wu5vHAflVfpbt64VgRWtdKpXm5+ft8hVviEFJAqQIhiIyJCTM+Hp1x9gZYhNff0op92JxhSakuIlmXxXATOyEGj94w4n56/CJPDhcafQKAzWdzttvPPFtDoFOse64X3fMgy5MIaWmd1eITTATkZUoEjapO5hMJqurqwsLC/jll4QtCfYcFZietCKjWpHdDSafzklLUWiiGFZeBHgURS5ocdST3sWDAPIARRQyHsgAMNO86cTGYLUrPfjL52IvvBAyX9C1x/Z1hPZjBbg+rB35y0UCtgN7NBqtrKwsLi7yLzYQf4mQ5UAXpeUHkUIzRF+XuBL7w6A0BHn8khHgCt55jXmSgrdMcZV80YiMNtGIMJ1m4S2pCr3GU2ttv8iNJ/icoVgC4dapZj87hbe4+CCIawKo4sTuOI7j7e3tdrvNA4HIioABu974g7Hoa+ErFxwuYiABCafWdr5wbYyxa332ZUMm/8Q3llLUnDiAZD2TX1AFT7HmNtOJPCdQSpVKJTt/d8MgHE7wWBJjuZmunI3HYxylzlYMyxhYSJgap91u25fLBd6Gz/kQn/p+8lOEifyIfVjowFlSyaWxa3QnARM9csh7J3GUzK7wT1+AJAQYdnaLGG7lTjUaDa21u7ET0BEXIq1rZW9E2u+fVatVNx6Kc+YdkS5Mfv90q9V69NFHbfrDY9XH0CeMkxMfiKw4POQvrPrimzsVIBC7wcfVatVeB/EkK8ZYnODEfkkvYWyBZ6g5bKnp91HsUhZRnFjAl3xxxLXhuVKpWIRxebgwoti+gn1fq9XsN2bxkOaYEOM6187XkSgw8cLJe95xA186A+Q5kq04CFwhUlar1aOjI5W/qYKx6GqI51ylu7QMyAMSFh29DRtZlvGHNo0x9grD3vrlHiJWJgkaU9rnGe1cCgtM7Cw6GFuPt8K2sqVUKrXbbfeBCXGABfIaiS8iPSbzhRtXIyw3+AALEoaIcAT1oh1LpZLdncKVtAsKPpsaacnKFzOAjU6iFAmN+K99NEp8xTdW3zdkzfQrnvib1jw6kkrChHdnphd9+JT92W63FxcXfbspiadEI3ABxMDsQyTkna7sA6u8P9I3iQS+StIZiRxY4kqlQmYwtuDNgOKYEMVQ00KMQhCJPYQfRyPS2n4rlYqNWDgAcKAD8429eWwXVkSz8MqAdiD5xZnI7npdXV0lz1CQHnkIJCpwLSDvNWJJrjsPuidXhURD0oHYNylioMI83UGtVuv1emIvxPqEocMc5unc7yh9AYyAiUQp15ddcbA7/vhX/BxqMQc1/QqLjXairQh68EDnvifHWH0ASNO0Vqutra3V6/WwX0hcx51ywU5VePQC5Ef6MAVXDJA1MUID+ojBAPPHmwhwMND511k5hzkdNHt1DEy/Moeh6dY/yXDHmHD8zXQtwIUxY4z9RrA41zb5jGBbjUYju9mLW89nMV848dnTSm6/8LixsYFX0vmQxkhyzbEvCNSAgY8LyVmRJrg+wfYif0UriJDnVuCsMH0URfV6fTAYEKOLsZMPL25xXMl30JNQ4Qjw+juOeW6vKfmKPS9KKTsV44nPZxkyPgOUWGwLKa318vLyysoKWYEjgZAMM04AeTQQYTAZrxfdyn8mzkA+JUXIn79UKhX7zUFg9sXIcPGMYALyyMPRkdiFjwQOO8zKVtoMiO/q8PRkPU2ilDgmSXfheA/I97bYdYpms7m5uSl+bh1zJoOQE/tGHRc7ICSO/WIgyK28i30QRxbBVhGaOI4bjUa32/WNDCIAATdednf0Gn25TrS+QVkSj1cMSnvKfmKT3y50KdtOp+x1n2sIEo4L5juuu50nZFlWqVQ2NjbsdIoMOSI85+aTpEj+IRgVO+XE9iD3IUyTz3ckchJ9xG54wuYGdZX1ev3w8JDsueO94LbAQMAV45Uk4HF7YUpLbFMbB5aZTqdc7iOhjgjDjcCLOJDcHlcyneKmIJb3+Z77l8vPAUCGnyiw2CT3IUz8lxiLnOIKcOG4CQjbOI7r9bp7eRCeO/vMjV+67AMQERXfjsRn7QZl0VswffWSzj+nPxqNlFK1Wg2jk4wWEeuiWQLD3c7t2u326uqq6A5XE/YCoEEFkhP5T4wtH2XgwJWid0MDIUSs5wlCNEGz2ez1emZaAv2SIGqmL2HDxHr6rR4cmchc3qHWTC8GiVR22m45u7P2HpR77z63xsxCzEJ0dGztyur8/Pza2poLimJf2PdivORjDPJjUoQU7pEEQneqiPpFgXVaO0J+rIAHiOVyuV6v9/t9kYOzHXE/XnQIh1UlzR5cpUMeX+aA6Vef7EJupVKxO224CpAHfcDo/BQOVPbdJPV6fX19vVar8U/kcS1mngqEgyIMw7EjzERY/fP9LFJIkgqMCVfm5+ctsAgKeboBhAkLOD70Mf58Y8sXSt1P+9yVfaDF7Rgm4OPFlwRJ1+TYRakkSTY3N+fm5orYXMRoEWvPZHgGp4utEm7WMN+ZEpDhQnI2nx7ZoHV0dER6IayweDh3AAoYLjW4XXuAfEm4cd9jyKrpcjwJVDOjkQ9SvCP3dD8ArK6uLi8vi/vDxC64C3wRgRvcJw8w3Kv8BCbAh8dI+eZDoPiiEa8UE7xic0ljzOLi4mAwIJdgriHZjixOUNwxDwmExoGDgBXyAHU5twiqRMG41lgeF6gWFhbW1tbsXaCCuS/sAqJOEf/yhMCbF8SJnAqLtz9b8TFPkmR+fn5/f58EOXtWvDNN5pJ4I03YTGTk2XciAPMHf5hspteJ2JBHNo6ydhmsXq9vbm6KT86cp5zTgw8LAIUm7zMnpAGXq/xMmYxLVzk3N9fr9ew7esibcF1zcT7k0gqBjk9acuDbveR7aqPIFIRbA6PKbqfZ3Ny0+4bFiE66EGvcsWjPgGy4lagO8SA+FiczopyJyQcJX5rwiSiiisRPX/rHJY7jxcXFe/fuGfb+Dw4yYADyzW9mggxnKOtmDD6enoitiAzYJq4LnX+j7srKip1O8czrwxN3xGlBT1iJp8Tu+FRBBCI+ZX8KC6TA4MzRI/qAxCdRSTII8M9ms9nv9+2yFqckJhDtOxOOnNigtS4SC12WJJx9OmIwwRSyLkoZYxYXF+3eKdKRyD+gmq/4XE6ihg+g4eMAakVRE5MHLHGeD92YgNOT0cwhwuOiPWi324PBwH0vCRuFwJFbkJjAHYvGxeq4rTtujd5FF7dJiw9r3js5dhd9WZY1Go2trS27vsoHtziQyKAlxzxac3tCfgyIVuI8fSF5ppzESicPyWD5CJ5IJCPjjNNjcXns5fZypVQqra6uEjFcX7w70jV/5RXXi8iDCfDVPv5QCiYG5FQfwsx0P4K9Uf3II49cunSJvDyIaOeK6GNReD7CA0bm4xn3yAcGiRp8TBJQcl8nxFsEqoEILEKbIz2gD1beNW82m/Pz83bXA5eEWxyYp01+5zHuFD/gz02Gedo3StpHxPita9wvMaA9sB8eW19fX1paIu/Z4tLykAwISdjaXF/Rqtwyoqm5KwOUxJLc6YAGv7Ib/XAwELvhheOURDVSOAHuF/K4WVlZOT4+tq/Otqd0/kvG3L64Bsd/bg5y7KtxrxPyRVzSxBUbq5aWllZXV+2mLm4ZPvT5UJkJGm5M3pZ3xDmIfES7cRfwA8dBeKMfjkOiKXk9iUlcW/yTQJkLEEXR1tbWtWvX7JN95KnLQCjlXcyMu9hwTgZjzGQysU/BB+wAeUgZY9I0tdvx7BcSxb58NvHRBNQkMvPRHuhR/OmDqXgqLB59YBVLKQZ/PjLEcO1o+E+RP6FMkmRjY+PGjRsG7WIgz+Nzq/mchE2MZea3/9y9oPF4bN8GQJoTmV2UyrKsXC5ftgwsxQAAC69JREFUvHix2WyGNSVycs/hjriReUIkdiBNsGd9wUwckASg3Fk+gNomCfnti0Y+mnArUROONhzAXHN7n//mzZtuH8vMIBrIfaR3EqL4qX6/T74caTHkKF2x0/yNjY12u8234/HIISLGHfNRijslkZ4bFnMgGIUg0Hnv5C8xqeh33JGw8s6FJvGMmMkHcB5diNG50KRmfn5+PB7fu3fPocoGrcBTgfaAoxDLGba1bTUcDhcXF4EBEe/YsRtdFhYWNjc33c0+HiFEx/tcAmx4OEyTSmI3Hsk4RrnZi4xDn4+4PLgm95QOhwhXmIdlYnqCMGduHj/DqLdleXk5TdP9/X3MnI9FwpAIiV3O4yUwv7o9WD5iu5TQaDQ2Nzfti9p8qOJKkfgqkokDgHPgP4llSIwIVELeg0RgbmSREosh3ysUBw3k3SACGWOR24WIiPviARKm3lpbW9NadzodkmjEmCyqygePSOwS7tHRUalUIu+Igymk7M2+Rx55ZGFhgTiYWM8nCTGgzyz4gCQNDgvOXywE09gO3PiYkg9pbmpcmbse5mPIN2gK1oR/krGLD7AaURRtbGxorQ8ODgB9r9X3dR3SBeQHBnEnfogeppOno6Ojer1OnkJzqNrY2FhaWuLfiSDWE+NxwAIQdAQf6uRYlATTEIByOX3xggtAjEkQYlnl3lMIDPK4Sy4uKeGzREoiLgn+xChxHF+4cMEYc3BwYKZ3qfk+GV+osLBw/EmWIc9AZ1k2GAzW1tYALeXbRwjtdAq/KEGMxMRoon1EAn7sC64+Y5JKX1DEwOUe98kwUyT8M3cTmo8wx0I8DlP6SngcixxskwsXLkRR1Ol0VH5xC7flw06ssQUYMrTWw+FQTR/FsafcdMqtTvEQRWKtT5GApjyWhI1GKnnUJx356n09BrQQzxI75J4rJBRcLFF6PpKIuYnJeJQuwhAA4jje3NxUSu3t7dl634PRLkRBftT6UIvHYqfTqdfrdkeynU5dvHix1WqRjMmzwMyIDh4villGlJPbRFQf0xORuJC+QUjk5KNIHFeu0A+dgzSqfNDhknECoh6PH6JWPp5xHG9tbcVxvLOzQ2Qmutka/DlgMfiTjuyHMNfX1+0+6fX19Xa7TTbP+OIBsYYYTvBPMciFrcoNgjnzLiBvf94jb8i5cYBiJr44ffK24LA5uGl4Kz58CT2WL2w432hwmNjd3b1z5w5MtyS4G71OTxwgidVMfi+h46m13tnZsXf6lpaW1tfX3XccxLAt6gUeB4MUD0T3iwFAZCjWi4HK1xchIDJgaUXxQn35QndAtyKUxZsX4Q/SZKjX6127dg2mqIL8ly9djYhj8n4iWwaDQafTeeyxx7a2tvCnIgM+Dpw9c3mIpvv0OM9klXuKHCORIBckLItZ33UsdCbBHwsamKCQs/Z4OBx+8skn9vsOajqjx3ELS4Uv/TBA7U/7/dLLly/bz/mRvfCBaOSTmYzvIqqFoz7RHTcHKXiIDgqHW5/MPmxwO5wkioDCYglD9TxnTyWDO8iy7ObNmwcHBxYNODNaGhy0FHqjrhXG3pZJkmR9fX1+ft731bvPRqPzsPr04tzZOhJeXgCeMVGEexj4IGEfMxRnJL5pCpZzf3//1q1bIKVFogUOWvatySsrK3Y7Hld2pgxiSCASEmW51iDFGyKMrxUPbIFK8BSfI0RWIkNOf4olzc/heMKhazwe3759+/Dw0G37JDkRS2K3JKysrLgtCZzyzCIVYVKEjNOc34wiQM/GOSweXVUnYYYPMrEDYC4JjJiCEauIYpAf5caYo6Ojmzdv2o/VAItbrsnS0hJeRwAWZQsKcAY3B2Yn5+zroYze8ytliyJ7wHkDCF4WBfAkQvNUGD2tkm7+dHR01Ol07JNkLhQppSqVyvz8vPsWyAMTzFq0LCgtbyjmQbFHX048LdrI3MM38eAqiKwCAsy0WC5iiY3D4zhgoJkHM89yMWb2js9mWTYej+3biCqVSrlcFl9Be4bMVUSYQGXBMrPT8OwqMLADUXOmQwsKEAJWgIV46qFPpD6lmdlD6egzk+1UZaabfH/F5gUDuchBeGCVNCPgE2M1T2GuLf5JeBYJdSIrHu1571zUIqV4W3IqrKlPZrFhuFMsp8jNF4E4kkQ8hUMaoX8QnMQAKTb4r+WhlHPa81TNC0aXh9tvgPLkSegiXAixqxHJZvLhlQH64qdmCsDbEo2KFG4Hfsznf8X5n7YEUMW79rmSsJrZUAyuD07p/NclAzE5nCh9BD7iM1TOLGI+OjOrcAo7J6szF5LsivMX869vSlPQj4HpGt3dcM5CpjsziR9ivzOvLc7PihCEO7UHIsEZ0lZBLc5sf1EkyEOwoBiWJlH5Uc4HhKvnHfjMN3Nchi8IYFbACAwpMpJEeXyBDVCoxxy4AAE7+IZyWCrfTNlnB5/8WEJRDHHSzVN2wEfgxxxM1wuNm7yL4D1DJZeS6yOi3sdKdKEYe2ey4haZyYqYmyNDZOXjcFqpgI3YgmY/QxGHwXk4zH7QxZ1yBIGfJJbQvMuuWjElMPQ4Mo7+sMCYJ7DhjscWNoqoOBHGNsciceGJZXzCiMSYbKaaoq3EA94RriEqE/Wxvtw42DXYfSdvmxGVdBb32UWUCXvRZxouilgf4IAbcmRwFUhfGF5Wdz4qeC9i15ge2FgnwnMQcG5c5iIYcrqIXWNLitDnFiDd8VFE7JyzqghGn1bA8ERAIJ7irIh1xF5IKywe74XbjhOQaMeDnxgkeNecj8jNZwdgviGsuKiQL4SMuJYYgftOdJlvWHJWPjBQ7/tuQgd6JSYW0cD58LzDOyUEgWNfTbjfE7UlyWfyJJx5Q5BsxfWdKZVINtNWvibiqJ4Z4wOtxChAOs2tY0Ee+HysBOrDp4pQimHvbPxn9ivGfF8gdDXgiQThjoCh52xazJS5oBZhI5+Wlbf5OV0FpxlYgeEVbv5ZlocoQ3FWgczwt65YrXPPF7hjUkOakQMODswK0+Phy/siHMiBjz9vLsrpE8mnpk9ZkbMoOWfFuRFin/2LsA24L9BdcQLuNd+BKriO5drwGQAgVBkpGhmW0UkrQs/5YLmLsCKUhIAzn9lvWOywhD7+oq3OwEqUGZjjRNOF58dOhjOzigi1Y0QAx6FGpp+uXjzrmAe8wqUkTQhn7gxSlFJhIbFg5BTvC7MKUJJeRHNxW3HPiaxIJbEhdq2ZriJhyX0Dg9fjVu4nqSRSEVYnSzgi3dnKw+LzqZa/FUL+7So59Iu7G8QYSMjIZJwMPhIeCX+Rkicd3jsXLJwycBMsp6hyQUU487B2oqiEoS+WE/kDbX0WE4UJ2yrMjdgBk+HmIZUeYiT77IPiQ+kozIQnwYdSzqnaQ4zE52EVORa+v7zytPR8qsEnT76pjzsgx24iSPoF5G8fB8yfjKuZlfwsEZtLHubP7YZHO+8LpHAiih2WkPwVC+loJhJwzUNYxypSTIG1rs++GJbQHwrD89N8fspppXX0uYjFidxBYAyJo4EUHD/ClD5x+RAJcxOHIxnZZ0OVjzN41j5msgJPMDgVK96c8ORd+HxaRAyROaBplvwODGcm3ECkBBYw8c9wiMaswoZzF7o4XwS4BfiANHWdWUiqAqYmSN4SmYvzWl5Jfp4Bsny9APJjSfQpOetdTWDMiX1yn033Kczn/+ERhkHmeiXS8+DB2XL+jiHk8cF7D1wGisecmPcrNiEE4YJlC/geGNTCOOOV5CoSWxKDADxO9BGIvrbFedngOVZg7J4hWYSZnCpOnFMqTmbOPa9ybYk1zzYXeehSnYEgBwj/VpdT9XLyZQreAeFoaXDHZATweM7jkBh1RYlJdCQ8CwospmluCNGmHECiKXlC5ww5f8WmGZxSJAvYioMVC8mF4bYNd8Sh4jOOMf5XRZ6tPJTw9qky/NyWz7mmp42p9NuhOB+LOT581nccmC5wBYgyAVZhPjMpi/P8DFidQVORAOeHIjQFCXxTTB+r/x/orEKbtlVUngAAAABJRU5ErkJggg==" }, "Event": "nodeQueriesComplete", "TimeStamp": 1588422766, "NodeManufacturerName": "EUROtronic", "NodeProductName": "EUR_SPIRITZ Wall Radiator Thermostat", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Thermostat", "NodeGeneric": 8, "NodeSpecificString": "General Thermostat V2", "NodeSpecific": 6, "NodeManufacturerID": "0x0148", "NodeProductType": "0x0003", "NodeProductID": "0x0001", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 1, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "Thermostat HVAC", "NodeDeviceType": 4608, "NodeRole": 7, "NodeRoleString": "Listening Sleeping Slave", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 3, 7, 8, 9, 10, 12, 13, 14 ]} +OpenZWave/1/node/16/instance/1/,{ "Instance": 1, "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/38/value/273252369/,{ "Label": "Level", "Value": 94, "Units": "%", "Min": 0, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 16, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 273252369, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588422759} +OpenZWave/1/node/16/instance/1/commandclass/38/value/281475249963032/,{ "Label": "Up", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 16, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475249963032, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/38/value/562950226673688/,{ "Label": "Down", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 16, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950226673688, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/38/value/844425211772944/,{ "Label": "Ignore Start Level", "Value": true, "Units": "", "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 16, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425211772944, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/38/value/1125900188483601/,{ "Label": "Start Level", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 16, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900188483601, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/64/,{ "Instance": 1, "CommandClassId": 64, "CommandClass": "COMMAND_CLASS_THERMOSTAT_MODE", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/64/value/273678356/,{ "Label": "Mode", "Value": { "List": [ { "Value": 0, "Label": "Off" }, { "Value": 1, "Label": "Heat" }, { "Value": 11, "Label": "Heat Eco" }, { "Value": 15, "Label": "Full Power" }, { "Value": 31, "Label": "Manufacturer Specific" } ], "Selected": "Heat", "Selected_id": 1 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_MODE", "Index": 0, "Node": 16, "Genre": "User", "Help": "Off: No heating, only frost protection. Heat: Room temperature will be kept at the configured setpoint. Heat Eco: Energy save heating mode. Room temperature will be lowered to the configured eco setpoint in order to save energy. Full Power: Full power heating. This mode is left automatically after 5 minutes. Manufacturer Specific: Direct valve control mode. The valve opening percentage can be controlled using the switch multilevel command class.", "ValueIDKey": 273678356, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/67/,{ "Instance": 1, "CommandClassId": 67, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/67/value/281475250438162/,{ "Label": "Heating 1", "Value": 19.0, "Units": "C", "Min": 8, "Max": 28, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 1, "Node": 16, "Genre": "User", "Help": "Set the Thermostat Setpoint Heating 1", "ValueIDKey": 281475250438162, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/67/value/3096225017544722/,{ "Label": "Heating Econ", "Value": 18.0, "Units": "C", "Min": 8, "Max": 28, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 11, "Node": 16, "Genre": "User", "Help": "Set the Thermostat Setpoint Heating Econ", "ValueIDKey": 3096225017544722, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/67/value/28428972921503762/,{ "Label": "Heating 1_minimum", "Value": 8.0, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 101, "Node": 16, "Genre": "User", "Help": "", "ValueIDKey": 28428972921503762, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/67/value/56576470592569362/,{ "Label": "Heating 1_maximum", "Value": 28.0, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 201, "Node": 16, "Genre": "User", "Help": "", "ValueIDKey": 56576470592569362, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/67/value/31243722688610322/,{ "Label": "Heating Econ_minimum", "Value": 8.0, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 111, "Node": 16, "Genre": "User", "Help": "", "ValueIDKey": 31243722688610322, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/67/value/59391220359675922/,{ "Label": "Heating Econ_maximum", "Value": 28.0, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_THERMOSTAT_SETPOINT", "Index": 211, "Node": 16, "Genre": "User", "Help": "", "ValueIDKey": 59391220359675922, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/112/value/281475255369748/,{ "Label": "LCD Invert", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "Upside Down" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 16, "Genre": "Config", "Help": "Allows rotating the LCD contents by 180 degrees. Default: Normal", "ValueIDKey": 281475255369748, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/112/value/562950232080401/,{ "Label": "LCD Timeout", "Value": 0, "Units": "sec", "Min": 0, "Max": 30, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 2, "Node": 16, "Genre": "Config", "Help": "0: No Timeout, LCD always on. 5-30: Timeout after 5-30s. Default: 0 (LCD always on)", "ValueIDKey": 562950232080401, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/112/value/844425208791060/,{ "Label": "Backlight", "Value": { "List": [ { "Value": 0, "Label": "Backlight disabled" }, { "Value": 1, "Label": "Backlight enabled" } ], "Selected": "Backlight enabled", "Selected_id": 1 }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 16, "Genre": "Config", "Help": "Default: Backlight enabled", "ValueIDKey": 844425208791060, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/112/value/1125900185501716/,{ "Label": "Battery Report", "Value": { "List": [ { "Value": 0, "Label": "Only send battery status as notification" }, { "Value": 1, "Label": "Send once a day" } ], "Selected": "Send once a day", "Selected_id": 1 }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 4, "Node": 16, "Genre": "Config", "Help": "Default: Send once a day", "ValueIDKey": 1125900185501716, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/112/value/1407375162212369/,{ "Label": "Temperature Report Threshold", "Value": 1, "Units": "0.1°C", "Min": 0, "Max": 50, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 5, "Node": 16, "Genre": "Config", "Help": "0: Don't send temperature automatically. 1-50: Report temperature at 0.1-5.0°C temperature difference. Default: 5 (Delta = 0.5°C)", "ValueIDKey": 1407375162212369, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588422810} +OpenZWave/1/node/16/instance/1/commandclass/112/value/1688850138923025/,{ "Label": "Valve Opening Percentage Report", "Value": 5, "Units": "", "Min": 0, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 6, "Node": 16, "Genre": "Config", "Help": "0: Don't send Valve opening percentage automatically. 1-100: Report valve opening percentage at a delta of 1-100%. Default: 0", "ValueIDKey": 1688850138923025, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588422811} +OpenZWave/1/node/16/instance/1/commandclass/112/value/1970325115633684/,{ "Label": "Open Window Detection", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Low sensibility" }, { "Value": 2, "Label": "Medium sensibility" }, { "Value": 3, "Label": "High sensibility" } ], "Selected": "Medium sensibility", "Selected_id": 2 }, "Units": "", "Min": 0, "Max": 3, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 7, "Node": 16, "Genre": "Config", "Help": "Default: Medium sensibility", "ValueIDKey": 1970325115633684, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/112/value/2251800092344337/,{ "Label": "Measured Temperature Offset", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 8, "Node": 16, "Genre": "Config", "Help": "206-255: -5.0 to -0.1°C. 0-50: 0°C-5°C. 128: External Temperature Sensor. Default: 0 (0.0°C Offset)", "ValueIDKey": 2251800092344337, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/94/value/282558481/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 16, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 282558481, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/94/value/281475259269142/,{ "Label": "InstallerIcon", "Value": 4608, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 16, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475259269142, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/94/value/562950235979798/,{ "Label": "UserIcon", "Value": 4608, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 16, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950235979798, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/114/value/282886163/,{ "Label": "Loaded Config Revision", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 16, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 282886163, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/114/value/281475259596819/,{ "Label": "Config File Revision", "Value": 5, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 16, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475259596819, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/114/value/562950236307475/,{ "Label": "Latest Available Config File Revision", "Value": 5, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 16, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950236307475, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/114/value/844425213018135/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 16, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425213018135, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/114/value/1125900189728791/,{ "Label": "Serial Number", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 16, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900189728791, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/115/value/282902548/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 16, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 282902548, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/115/value/281475259613201/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 16, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475259613201, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/115/value/562950236323864/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 16, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950236323864, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/115/value/844425213034513/,{ "Label": "Test Node", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 16, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425213034513, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/115/value/1125900189745172/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 16, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900189745172, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/115/value/1407375166455830/,{ "Label": "Frame Count", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 16, "Genre": "System", "Help": "How Many Messages to send to the Node for the Test", "ValueIDKey": 1407375166455830, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/115/value/1688850143166488/,{ "Label": "Test", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 16, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850143166488, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/115/value/1970325119877144/,{ "Label": "Report", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 16, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325119877144, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/115/value/2251800096587796/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 16, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800096587796, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/115/value/2533275073298454/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 16, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275073298454, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/117/,{ "Instance": 1, "CommandClassId": 117, "CommandClass": "COMMAND_CLASS_PROTECTION", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/117/value/282935316/,{ "Label": "Protection", "Value": { "List": [ { "Value": 0, "Label": "Unprotected" }, { "Value": 1, "Label": "Protection by Sequence" }, { "Value": 2, "Label": "No Operation Possible" } ], "Selected": "Protection by Sequence", "Selected_id": 1 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_PROTECTION", "Index": 0, "Node": 16, "Genre": "System", "Help": "Protect a device against unintentional control", "ValueIDKey": 282935316, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/128/,{ "Instance": 1, "CommandClassId": 128, "CommandClass": "COMMAND_CLASS_BATTERY", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/128/value/274726929/,{ "Label": "Battery Level", "Value": 90, "Units": "%", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BATTERY", "Index": 0, "Node": 16, "Genre": "User", "Help": "Current Battery Level", "ValueIDKey": 274726929, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/134/value/283213847/,{ "Label": "Library Version", "Value": "3", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 16, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 283213847, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/134/value/281475259924503/,{ "Label": "Protocol Version", "Value": "4.61", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 16, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475259924503, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/134/value/562950236635159/,{ "Label": "Application Version", "Value": "0.15", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 16, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950236635159, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/152/,{ "Instance": 1, "CommandClassId": 152, "CommandClass": "COMMAND_CLASS_SECURITY", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/152/value/283508752/,{ "Label": "Secured", "Value": true, "Units": "", "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SECURITY", "Index": 0, "Node": 16, "Genre": "System", "Help": "Is Communication with Device Encrypted", "ValueIDKey": 283508752, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/49/,{ "Instance": 1, "CommandClassId": 49, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/49/value/281475250143250/,{ "Label": "Air Temperature", "Value": 17.260000228881837, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 1, "Node": 16, "Genre": "User", "Help": "Air Temperature Sensor Value", "ValueIDKey": 281475250143250, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1588422760} +OpenZWave/1/node/16/instance/1/commandclass/49/value/72057594319749140/,{ "Label": "Air Temperature Units", "Value": { "List": [ { "Value": 0, "Label": "Celsius" } ], "Selected": "Celsius", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 256, "Node": 16, "Genre": "System", "Help": "Air Temperature Sensor Available Units", "ValueIDKey": 72057594319749140, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/113/,{ "Instance": 1, "CommandClassId": 113, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/113/value/72057594312409105/,{ "Label": "Previous Event Cleared", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 256, "Node": 16, "Genre": "User", "Help": "Previous Event that was sent", "ValueIDKey": 72057594312409105, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/113/value/2251800088166420/,{ "Label": "Power Management", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 10, "Label": "Replace Battery Soon" }, { "Value": 11, "Label": "Replace Battery Now" } ], "Selected": "Clear", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 8, "Node": 16, "Genre": "User", "Help": "Power Management Alerts", "ValueIDKey": 2251800088166420, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/113/value/74872344079515671/,{ "Label": "Error Code", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 266, "Node": 16, "Genre": "User", "Help": "The Error Code returned by the device", "ValueIDKey": 74872344079515671, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/instance/1/commandclass/113/value/2533275064877076/,{ "Label": "System", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 3, "Label": "Hardware Failure Code" } ], "Selected": "Clear", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 9, "Node": 16, "Genre": "User", "Help": "System Alerts", "ValueIDKey": 2533275064877076, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1588422682} +OpenZWave/1/node/16/association/1/,{ "Name": "Group 1", "Help": "", "MaxAssociations": 1, "Members": [ "1.0" ], "TimeStamp": 1588422682} \ No newline at end of file diff --git a/tests/fixtures/ozw/fan.json b/tests/fixtures/ozw/fan.json new file mode 100644 index 00000000000..2684e5f7385 --- /dev/null +++ b/tests/fixtures/ozw/fan.json @@ -0,0 +1,25 @@ +{ + "topic": "OpenZWave/1/node/10/instance/1/commandclass/38/value/172589073/", + "payload": { + "Label": "Level", + "Value": 41, + "Units": "", + "ValueSet": false, + "ValuePolled": false, + "ChangeVerified": false, + "Min": 0, + "Max": 255, + "Type": "Byte", + "Instance": 1, + "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", + "Index": 0, + "Node": 10, + "Genre": "User", + "Help": "The Current Level of the Device", + "ValueIDKey": 172589073, + "ReadOnly": false, + "WriteOnly": false, + "Event": "valueAdded", + "TimeStamp": 1589997977 + } +} diff --git a/tests/fixtures/ozw/fan_network_dump.csv b/tests/fixtures/ozw/fan_network_dump.csv new file mode 100644 index 00000000000..54541271d14 --- /dev/null +++ b/tests/fixtures/ozw/fan_network_dump.csv @@ -0,0 +1,51 @@ +OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1123", "OZWDaemon_Version": "0.1.98", "QTOpenZWave_Version": "1.0.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1589998153, "ManufacturerSpecificDBReady": true, "homeID": 4188283268, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": false, "getControllerLibraryVersion": "Z-Wave 4.54", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/ttyACM0"} +OpenZWave/1/node/10/,{ "NodeID": 10, "NodeQueryStage": "Complete", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": true, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0063:3031:4944", "ZWAProductURL": "", "ProductPic": "images/ge/12724-dimmer.png", "Description": "Transform any home into a smart home with the GE Z-Wave Smart Fan Control. The in-wall fan control easily replaces any standard in-wall switch remotely controls a ceiling fan in your home and features a three-speed control system. Your home will be equipped with ultimate flexibility with the GE Z-Wave Smart Fan Control, capable of being used by itself or with up to four GE add-on switches. Screw terminal installation provides improved space efficiency when replacing existing switches and the integrated LED indicator light allows you to easily locate the switch in a dark room. The GE Z-Wave Smart Fan Control is compatible with any Z-Wave certified gateway, providing access to many popular home automation systems. Take control of your home lighting with GE Z-Wave Smart Lighting Controls!", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2506/Binder2.pdf", "ProductPageURL": "http://www.ezzwave.com", "InclusionHelp": "1. Follow the instructions for your Z-Wave certified controller to include a device to the Z-Wave network. 2. Once the controller is ready to include your device, press and release the top or bottom of the smart fan control switch (rocker) to include it in the network. 3. Once your controller has confirmed the device has been included, refresh the Z-Wave network to optimize performance.", "ExclusionHelp": "1. Follow the instructions for your Z-Wave certified controller to exclude a device from the Z-Wave network. 2. Once the controller is ready to Exclude your device, press and release the top or bottom of the wireless smart switch (rocker) to exclude it from the network.", "ResetHelp": "1. Quickly press ON (Top) button three (3) times then immediately press the OFF (Bottom) button three (3) times. The LED will flash ON/OFF 5 times when completed successfully. Note: This should only be used in the event your network’s primary controller is missing or otherwise inoperable.", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "In-Wall Smart Fan Control" }, "Event": "nodeQueriesComplete", "TimeStamp": 1589998151, "NodeManufacturerName": "GE (Jasco Products)", "NodeProductName": "14287 Fan Control Switch", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Multilevel Switch", "NodeGeneric": 17, "NodeSpecificString": "Fan Switch", "NodeSpecific": 8, "NodeManufacturerID": "0x0063", "NodeProductType": "0x4944", "NodeProductID": "0x3131", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 3, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "Fan Switch", "NodeDeviceType": 1024, "NodeRole": 5, "NodeRoleString": "Always On Slave", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 29, 30, 32, 33 ]} +OpenZWave/1/node/10/instance/1/,{ "Instance": 1, "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "CommandClassVersion": 1, "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/38/value/172589073/,{ "Label": "Level", "Value": 41, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 10, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 172589073, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/38/value/281475149299736/,{ "Label": "Bright", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 10, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475149299736, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/38/value/562950126010392/,{ "Label": "Dim", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 10, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950126010392, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/38/value/844425111109648/,{ "Label": "Ignore Start Level", "Value": true, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 10, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425111109648, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/38/value/1125900087820305/,{ "Label": "Start Level", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 10, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900087820305, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/39/,{ "Instance": 1, "CommandClassId": 39, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "CommandClassVersion": 1, "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/39/value/180994068/,{ "Label": "Switch All", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Off Enabled" }, { "Value": 2, "Label": "On Enabled" }, { "Value": 255, "Label": "On and Off Enabled" } ], "Selected": "On and Off Enabled", "Selected_id": 255 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "Index": 0, "Node": 10, "Genre": "System", "Help": "Switch All Devices On/Off", "ValueIDKey": 180994068, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/43/,{ "Instance": 1, "CommandClassId": 43, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "CommandClassVersion": 1, "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/43/value/172670995/,{ "Label": "Scene", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 0, "Node": 10, "Genre": "User", "Help": "", "ValueIDKey": 172670995, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/43/value/281475149381651/,{ "Label": "Duration", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 1, "Node": 10, "Genre": "User", "Help": "", "ValueIDKey": 281475149381651, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "CommandClassVersion": 1, "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/94/value/181895185/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 10, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 181895185, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/94/value/281475158605846/,{ "Label": "InstallerIcon", "Value": 1024, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 10, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475158605846, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/94/value/562950135316502/,{ "Label": "UserIcon", "Value": 1024, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 10, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950135316502, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "CommandClassVersion": 1, "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/112/value/844425108127764/,{ "Label": "LED Light", "Value": { "List": [ { "Value": 0, "Label": "LED on when light off" }, { "Value": 1, "Label": "LED on when light on" }, { "Value": 2, "Label": "LED always off" } ], "Selected": "LED on when light off", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 10, "Genre": "Config", "Help": "Sets when the LED on the switch is lit.", "ValueIDKey": 844425108127764, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/112/value/1125900084838420/,{ "Label": "Invert Switch", "Value": { "List": [ { "Value": 0, "Label": "No" }, { "Value": 1, "Label": "Yes" } ], "Selected": "No", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 4, "Node": 10, "Genre": "Config", "Help": "Change the top of the switch to OFF and the bottom of the switch to ON, if the switch was installed upside down.", "ValueIDKey": 1125900084838420, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/112/value/1970325014970385/,{ "Label": "Z-Wave Command Dim Step", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 99, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 7, "Node": 10, "Genre": "Config", "Help": "Indicates how many levels the dimmer will change for each dimming step.", "ValueIDKey": 1970325014970385, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/112/value/2251799991681041/,{ "Label": "Z-Wave Command Dim Rate", "Value": 3, "Units": "x 10 milliseconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 8, "Node": 10, "Genre": "Config", "Help": "This value indicates in 10 millisecond resolution, how often the dim level will change. For example, if you set this parameter to 1, then every 10ms the dim level will change. If you set it to 255, then every 2.55 seconds the dim level will change.", "ValueIDKey": 2251799991681041, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/112/value/2533274968391697/,{ "Label": "Local Control Dim Step", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 99, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 9, "Node": 10, "Genre": "Config", "Help": "Indicates how many levels the dimmer will change for each dimming step.", "ValueIDKey": 2533274968391697, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/112/value/2814749945102353/,{ "Label": "Local Control Dim Rate", "Value": 3, "Units": "x 10 milliseconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 10, "Node": 10, "Genre": "Config", "Help": "This value indicates in 10 millisecond resolution, how often the dim level will change. For example, if you set this parameter to 1, then every 10ms the dim level will change. If you set it to 255, then every 2.55 seconds the dim level will change.", "ValueIDKey": 2814749945102353, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/112/value/3096224921813009/,{ "Label": "ALL ON/ALL OFF Dim Step", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 99, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 11, "Node": 10, "Genre": "Config", "Help": "Indicates how many levels the dimmer will change for each dimming step.", "ValueIDKey": 3096224921813009, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/112/value/3377699898523665/,{ "Label": "ALL ON/ALL OFF Dim Rate", "Value": 3, "Units": "x 10 milliseconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 1, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 12, "Node": 10, "Genre": "Config", "Help": "This value indicates in 10 millisecond resolution, how often the dim level will change. For example, if you set this parameter to 1, then every 10ms the dim level will change. If you set it to 255, then every 2.55 seconds the dim level will change.", "ValueIDKey": 3377699898523665, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "CommandClassVersion": 2, "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/114/value/182222867/,{ "Label": "Loaded Config Revision", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 10, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 182222867, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/114/value/281475158933523/,{ "Label": "Config File Revision", "Value": 9, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 10, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475158933523, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/114/value/562950135644179/,{ "Label": "Latest Available Config File Revision", "Value": 9, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 10, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950135644179, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/114/value/844425112354839/,{ "Label": "Device ID", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 10, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425112354839, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/114/value/1125900089065495/,{ "Label": "Serial Number", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 10, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900089065495, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "CommandClassVersion": 1, "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/115/value/182239252/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 10, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 182239252, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/115/value/281475158949905/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 10, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475158949905, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/115/value/562950135660568/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 10, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950135660568, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/115/value/844425112371217/,{ "Label": "Test Node", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 10, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425112371217, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/115/value/1125900089081876/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 10, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900089081876, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/115/value/1407375065792534/,{ "Label": "Frame Count", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 10, "Genre": "System", "Help": "How Many Messages to send to the Node for the Test", "ValueIDKey": 1407375065792534, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/115/value/1688850042503192/,{ "Label": "Test", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 10, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850042503192, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/115/value/1970325019213848/,{ "Label": "Report", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 10, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325019213848, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/115/value/2251799995924500/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 10, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251799995924500, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/115/value/2533274972635158/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 10, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533274972635158, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "CommandClassVersion": 1, "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/134/value/182550551/,{ "Label": "Library Version", "Value": "3", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 10, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 182550551, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/134/value/281475159261207/,{ "Label": "Protocol Version", "Value": "4.54", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 10, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475159261207, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/instance/1/commandclass/134/value/562950135971863/,{ "Label": "Application Version", "Value": "5.22", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 10, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950135971863, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1589997977} +OpenZWave/1/node/10/association/1/,{ "Name": "Group 1", "Help": "", "MaxAssociations": 5, "Members": [ "1.0" ], "TimeStamp": 1589997977} +OpenZWave/1/node/10/association/2/,{ "Name": "Group 2", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1589998004} +OpenZWave/1/node/10/association/3/,{ "Name": "Group 3", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1589998004} diff --git a/tests/fixtures/ozw/lock.json b/tests/fixtures/ozw/lock.json new file mode 100644 index 00000000000..1ec2187abcb --- /dev/null +++ b/tests/fixtures/ozw/lock.json @@ -0,0 +1,25 @@ +{ + "topic": "OpenZWave/1/node/10/instance/1/commandclass/98/value/173572112/", + "payload": { + "Label": "Lock", + "Value": false, + "Units": "", + "Min": 0, + "Max": 0, + "Type": "Bool", + "Instance": 1, + "CommandClass": "COMMAND_CLASS_DOOR_LOCK", + "Index": 0, + "Node": 10, + "Genre": "User", + "Help": "Lock / Unlock Device", + "ValueIDKey": 173572112, + "ReadOnly": false, + "WriteOnly": false, + "ValueSet": false, + "ValuePolled": false, + "ChangeVerified": false, + "Event": "valueAdded", + "TimeStamp": 1579566891 + } +} diff --git a/tests/fixtures/ozw/lock_network_dump.csv b/tests/fixtures/ozw/lock_network_dump.csv new file mode 100644 index 00000000000..fdb4ce7353e --- /dev/null +++ b/tests/fixtures/ozw/lock_network_dump.csv @@ -0,0 +1,79 @@ +OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1131", "OZWDaemon_Version": "0.1.101", "QTOpenZWave_Version": "1.0.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueriedSomeDead", "TimeStamp": 1590178891, "ManufacturerSpecificDBReady": true, "homeID": 4075923038, "getControllerNodeId": 1, "getSUCNodeId": 0, "isPrimaryController": false, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 4.05", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/zwave"} +OpenZWave/1/node/10/,{ "NodeID": 10, "NodeQueryStage": "Complete", "isListening": false, "isFlirs": true, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": true, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "", "ZWAProductURL": "", "ProductPic": "", "Description": "", "ProductManualURL": "", "ProductPageURL": "", "InclusionHelp": "", "ExclusionHelp": "", "ResetHelp": "", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "", "ProductPicBase64": "" }, "Event": "nodeQueriesComplete", "TimeStamp": 1590178891, "NodeManufacturerName": "Poly-control", "NodeProductName": "Danalock V3 BTZE", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Entry Control", "NodeGeneric": 64, "NodeSpecificString": "Secure Keypad Door Lock", "NodeSpecific": 3, "NodeManufacturerID": "0x010e", "NodeProductType": "0x0009", "NodeProductID": "0x0001", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeName": "", "NodeLocation": "", "NodeGroups": 1, "NodeDeviceTypeString": "Door Lock Keypad", "NodeDeviceType": 768, "NodeRole": 7, "NodeRoleString": "Listening Sleeping Slave", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 5, 9 ]} +OpenZWave/1/node/10/instance/1/,{ "Instance": 1, "TimeStamp": 1590178857} +OpenZWave/1/node/10/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "CommandClassVersion": 1, "TimeStamp": 1590178855} +OpenZWave/1/node/10/instance/1/commandclass/112/value/281475154706452/,{ "Label": "Twist Assist", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Enabled" } ], "Selected": "Disabled", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 10, "Genre": "Config", "Help": "", "ValueIDKey": 281475154706452, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178855} +OpenZWave/1/node/10/instance/1/commandclass/112/value/562950131417107/,{ "Label": "Hold and Release", "Value": 0, "Units": "seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 2, "Node": 10, "Genre": "Config", "Help": "0 Disable. 1 to 2147483647 Enable, time in seconds.", "ValueIDKey": 562950131417107, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178855} +OpenZWave/1/node/10/instance/1/commandclass/112/value/844425108127764/,{ "Label": "Block to Block", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 10, "Genre": "Config", "Help": "", "ValueIDKey": 844425108127764, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178855} +OpenZWave/1/node/10/instance/1/commandclass/112/value/1125900084838419/,{ "Label": "BLE Temporary Allowed", "Value": 0, "Units": "seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 4, "Node": 10, "Genre": "Config", "Help": "0 Disable. 1 to 2147483647 Enable, time in seconds.", "ValueIDKey": 1125900084838419, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178855} +OpenZWave/1/node/10/instance/1/commandclass/112/value/1407375061549076/,{ "Label": "BLE Always Allowed", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 5, "Node": 10, "Genre": "Config", "Help": "", "ValueIDKey": 1407375061549076, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178855} +OpenZWave/1/node/10/instance/1/commandclass/112/value/1688850038259731/,{ "Label": "Autolock", "Value": 0, "Units": "seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 6, "Node": 10, "Genre": "Config", "Help": "0 Disable. 1 to 2147483647 Enable, time in seconds.", "ValueIDKey": 1688850038259731, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "CommandClassVersion": 1, "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/94/value/181895185/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 10, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 181895185, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/94/value/281475158605846/,{ "Label": "InstallerIcon", "Value": 768, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 10, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475158605846, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/94/value/562950135316502/,{ "Label": "UserIcon", "Value": 768, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 10, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950135316502, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/98/,{ "Instance": 1, "CommandClassId": 98, "CommandClass": "COMMAND_CLASS_DOOR_LOCK", "CommandClassVersion": 1, "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/98/value/173572112/,{ "Label": "Locked", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_DOOR_LOCK", "Index": 0, "Node": 10, "Genre": "User", "Help": "State of the Lock", "ValueIDKey": 173572112, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590345913} +OpenZWave/1/node/10/instance/1/commandclass/98/value/281475150282772/,{ "Label": "Locked (Advanced)", "Value": { "List": [ { "Value": 0, "Label": "Unsecure" }, { "Value": 1, "Label": "Unsecured with Timeout" }, { "Value": 2, "Label": "Inside Handle Unsecured" }, { "Value": 3, "Label": "Inside Handle Unsecured with Timeout" }, { "Value": 4, "Label": "Outside Handle Unsecured" }, { "Value": 5, "Label": "Outside Handle Unsecured with Timeout" }, { "Value": 6, "Label": "Secured" }, { "Value": 255, "Label": "Invalid" } ], "Selected": "Unsecure", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_DOOR_LOCK", "Index": 1, "Node": 10, "Genre": "User", "Help": "State of the Lock (Advanced)", "ValueIDKey": 281475150282772, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590345913} +OpenZWave/1/node/10/instance/1/commandclass/98/value/562950135382036/,{ "Label": "Timeout Mode", "Value": { "List": [ { "Value": 1, "Label": "No Timeout" }, { "Value": 2, "Label": "Secure Lock after Timeout" } ], "Selected": "No Timeout", "Selected_id": 1 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_DOOR_LOCK", "Index": 2, "Node": 10, "Genre": "System", "Help": "Timeout Mode for Reverting Lock State", "ValueIDKey": 562950135382036, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/98/value/1407375065514001/,{ "Label": "Outside Handle Control", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_DOOR_LOCK", "Index": 5, "Node": 10, "Genre": "System", "Help": "State of the Exterior Handle Control", "ValueIDKey": 1407375065514001, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/98/value/1688850042224657/,{ "Label": "Inside Handle Control", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_DOOR_LOCK", "Index": 6, "Node": 10, "Genre": "System", "Help": "State of the Interior Handle Control", "ValueIDKey": 1688850042224657, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/99/,{ "Instance": 1, "CommandClassId": 99, "CommandClass": "COMMAND_CLASS_USER_CODE", "CommandClassVersion": 1, "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/99/value/72339069196615702/,{ "Label": "Code Count", "Value": 20, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 257, "Node": 10, "Genre": "System", "Help": "Number of User Codes supported by the Device", "ValueIDKey": 72339069196615702, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/99/value/71776119243194392/,{ "Label": "Refresh All UserCodes", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 255, "Node": 10, "Genre": "System", "Help": "Refresh All UserCodes Stored on Device", "ValueIDKey": 71776119243194392, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/99/value/72057594219905046/,{ "Label": "Remove User Code", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 256, "Node": 10, "Genre": "System", "Help": "Remove A UserCode at the Specified Index", "ValueIDKey": 72057594219905046, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/99/value/173588503/,{ "Label": "Enrollment Code", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 0, "Node": 10, "Genre": "User", "Help": "Enrollment Code", "ValueIDKey": 173588503, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/99/value/281475150299159/,{ "Label": "Code 1:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 1, "Node": 10, "Genre": "User", "Help": "UserCode 1", "ValueIDKey": 281475150299159, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/99/value/562950127009815/,{ "Label": "Code 2:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 2, "Node": 10, "Genre": "User", "Help": "UserCode 2", "ValueIDKey": 562950127009815, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/99/value/844425103720471/,{ "Label": "Code 3:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 3, "Node": 10, "Genre": "User", "Help": "UserCode 3", "ValueIDKey": 844425103720471, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/99/value/1125900080431127/,{ "Label": "Code 4:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 4, "Node": 10, "Genre": "User", "Help": "UserCode 4", "ValueIDKey": 1125900080431127, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/99/value/1407375057141783/,{ "Label": "Code 5:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 5, "Node": 10, "Genre": "User", "Help": "UserCode 5", "ValueIDKey": 1407375057141783, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/99/value/1688850033852439/,{ "Label": "Code 6:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 6, "Node": 10, "Genre": "User", "Help": "UserCode 6", "ValueIDKey": 1688850033852439, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/99/value/1970325010563095/,{ "Label": "Code 7:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 7, "Node": 10, "Genre": "User", "Help": "UserCode 7", "ValueIDKey": 1970325010563095, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/99/value/2251799987273751/,{ "Label": "Code 8:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 8, "Node": 10, "Genre": "User", "Help": "UserCode 8", "ValueIDKey": 2251799987273751, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/99/value/2533274963984407/,{ "Label": "Code 9:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 9, "Node": 10, "Genre": "User", "Help": "UserCode 9", "ValueIDKey": 2533274963984407, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/99/value/2814749940695063/,{ "Label": "Code 10:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 10, "Node": 10, "Genre": "User", "Help": "UserCode 10", "ValueIDKey": 2814749940695063, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/99/value/3096224917405719/,{ "Label": "Code 11:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 11, "Node": 10, "Genre": "User", "Help": "UserCode 11", "ValueIDKey": 3096224917405719, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/99/value/3377699894116375/,{ "Label": "Code 12:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 12, "Node": 10, "Genre": "User", "Help": "UserCode 12", "ValueIDKey": 3377699894116375, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/99/value/3659174870827031/,{ "Label": "Code 13:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 13, "Node": 10, "Genre": "User", "Help": "UserCode 13", "ValueIDKey": 3659174870827031, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/99/value/3940649847537687/,{ "Label": "Code 14:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 14, "Node": 10, "Genre": "User", "Help": "UserCode 14", "ValueIDKey": 3940649847537687, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/99/value/4222124824248343/,{ "Label": "Code 15:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 15, "Node": 10, "Genre": "User", "Help": "UserCode 15", "ValueIDKey": 4222124824248343, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/99/value/4503599800958999/,{ "Label": "Code 16:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 16, "Node": 10, "Genre": "User", "Help": "UserCode 16", "ValueIDKey": 4503599800958999, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/99/value/4785074777669655/,{ "Label": "Code 17:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 17, "Node": 10, "Genre": "User", "Help": "UserCode 17", "ValueIDKey": 4785074777669655, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/99/value/5066549754380311/,{ "Label": "Code 18:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 18, "Node": 10, "Genre": "User", "Help": "UserCode 18", "ValueIDKey": 5066549754380311, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/99/value/5348024731090967/,{ "Label": "Code 19:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 19, "Node": 10, "Genre": "User", "Help": "UserCode 19", "ValueIDKey": 5348024731090967, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/99/value/5629499707801623/,{ "Label": "Code 20:", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_USER_CODE", "Index": 20, "Node": 10, "Genre": "User", "Help": "UserCode 20", "ValueIDKey": 5629499707801623, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "CommandClassVersion": 2, "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/114/value/182222867/,{ "Label": "Loaded Config Revision", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 10, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 182222867, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/114/value/281475158933523/,{ "Label": "Config File Revision", "Value": 15, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 10, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475158933523, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/114/value/562950135644179/,{ "Label": "Latest Available Config File Revision", "Value": 15, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 10, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950135644179, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/114/value/844425112354839/,{ "Label": "Device ID", "Value": "", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 10, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425112354839, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/114/value/1125900089065495/,{ "Label": "Serial Number", "Value": "3b548b972bf8", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 10, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900089065495, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178857} +OpenZWave/1/node/10/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "CommandClassVersion": 1, "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/115/value/182239252/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 10, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 182239252, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/115/value/281475158949905/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 10, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475158949905, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/115/value/562950135660568/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 10, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950135660568, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/115/value/844425112371217/,{ "Label": "Test Node", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 10, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425112371217, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/115/value/1125900089081876/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal", "Selected_id": 0 }, "Units": "dB", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 10, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900089081876, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/115/value/1407375065792534/,{ "Label": "Frame Count", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 10, "Genre": "System", "Help": "How Many Messages to send to the Node for the Test", "ValueIDKey": 1407375065792534, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/115/value/1688850042503192/,{ "Label": "Test", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 10, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850042503192, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/115/value/1970325019213848/,{ "Label": "Report", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 10, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325019213848, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/115/value/2251799995924500/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed", "Selected_id": 0 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 10, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251799995924500, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/115/value/2533274972635158/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 10, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533274972635158, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/128/,{ "Instance": 1, "CommandClassId": 128, "CommandClass": "COMMAND_CLASS_BATTERY", "CommandClassVersion": 1, "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/128/value/174063633/,{ "Label": "Battery Level", "Value": 94, "Units": "%", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BATTERY", "Index": 0, "Node": 10, "Genre": "User", "Help": "Current Battery Level", "ValueIDKey": 174063633, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178891} +OpenZWave/1/node/10/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "CommandClassVersion": 1, "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/134/value/182550551/,{ "Label": "Library Version", "Value": "3", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 10, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 182550551, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178857} +OpenZWave/1/node/10/instance/1/commandclass/134/value/281475159261207/,{ "Label": "Protocol Version", "Value": "4.61", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 10, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475159261207, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178857} +OpenZWave/1/node/10/instance/1/commandclass/134/value/562950135971863/,{ "Label": "Application Version", "Value": "1.02", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 10, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950135971863, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178857} +OpenZWave/1/node/10/instance/1/commandclass/139/,{ "Instance": 1, "CommandClassId": 139, "CommandClass": "COMMAND_CLASS_TIME_PARAMETERS", "CommandClassVersion": 1, "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/139/value/182632471/,{ "Label": "Date", "Value": "22/05/2020", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_TIME_PARAMETERS", "Index": 0, "Node": 10, "Genre": "System", "Help": "Current Date", "ValueIDKey": 182632471, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178858} +OpenZWave/1/node/10/instance/1/commandclass/139/value/281475159343127/,{ "Label": "Time", "Value": "20:20:57", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_TIME_PARAMETERS", "Index": 1, "Node": 10, "Genre": "System", "Help": "Current Time", "ValueIDKey": 281475159343127, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590178858} +OpenZWave/1/node/10/instance/1/commandclass/139/value/562950136053784/,{ "Label": "Set Date/Time", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_TIME_PARAMETERS", "Index": 2, "Node": 10, "Genre": "System", "Help": "Set the Date/Time", "ValueIDKey": 562950136053784, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/139/value/844425112764440/,{ "Label": "Refresh Date/Time", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_TIME_PARAMETERS", "Index": 3, "Node": 10, "Genre": "System", "Help": "Refresh the Date/Time", "ValueIDKey": 844425112764440, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/152/,{ "Instance": 1, "CommandClassId": 152, "CommandClass": "COMMAND_CLASS_SECURITY", "CommandClassVersion": 1, "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/152/value/182845456/,{ "Label": "Secured", "Value": true, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SECURITY", "Index": 0, "Node": 10, "Genre": "System", "Help": "Is Communication with Device Encrypted", "ValueIDKey": 182845456, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178856} +OpenZWave/1/node/10/instance/1/commandclass/113/,{ "Instance": 1, "CommandClassId": 113, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "CommandClassVersion": 8, "TimeStamp": 1590178857} +OpenZWave/1/node/10/instance/1/commandclass/113/value/72057594211745809/,{ "Label": "Previous Event Cleared", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 256, "Node": 10, "Genre": "User", "Help": "Previous Event that was sent", "ValueIDKey": 72057594211745809, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1590178857} +OpenZWave/1/node/10/instance/1/commandclass/113/value/1688850034081812/,{ "Label": "Access Control", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 1, "Label": "Manual Lock Operation" }, { "Value": 2, "Label": "Manual Unlock Operation" }, { "Value": 3, "Label": "Wireless Lock Operation" }, { "Value": 4, "Label": "Wireless Unlock Operation" }, { "Value": 9, "Label": "Auto Lock" }, { "Value": 11, "Label": "Lock Jammed" } ], "Selected": "Wireless Unlock Operation", "Selected_id": 4 }, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 6, "Node": 10, "Genre": "User", "Help": "Access Control Alerts", "ValueIDKey": 1688850034081812, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1590345911} +OpenZWave/1/node/10/association/1/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 1, "Members": [ "1.0" ], "TimeStamp": 1590178858} \ No newline at end of file diff --git a/tests/fixtures/sonarr/calendar.json b/tests/fixtures/sonarr/calendar.json new file mode 100644 index 00000000000..e24a48d227a --- /dev/null +++ b/tests/fixtures/sonarr/calendar.json @@ -0,0 +1,116 @@ +[ + { + "seriesId": 3, + "episodeFileId": 0, + "seasonNumber": 4, + "episodeNumber": 11, + "title": "Easy Com-mercial, Easy Go-mercial", + "airDate": "2014-01-26", + "airDateUtc": "2014-01-27T01:30:00Z", + "overview": "To compete with fellow \"restaurateur,\" Jimmy Pesto, and his blowout Super Bowl event, Bob is determined to create a Bob's Burgers commercial to air during the \"big game.\" In an effort to outshine Pesto, the Belchers recruit Randy, a documentarian, to assist with the filmmaking and hire on former pro football star Connie Frye to be the celebrity endorser.", + "hasFile": false, + "monitored": true, + "sceneEpisodeNumber": 0, + "sceneSeasonNumber": 0, + "tvDbEpisodeId": 0, + "series": { + "tvdbId": 194031, + "tvRageId": 24607, + "imdbId": "tt1561755", + "title": "Bob's Burgers", + "sortTitle": "bob burgers", + "cleanTitle": "bobsburgers", + "seasonCount": 4, + "status": "continuing", + "overview": "Bob's Burgers follows a third-generation restaurateur, Bob, as he runs Bob's Burgers with the help of his wife and their three kids. Bob and his quirky family have big ideas about burgers, but fall short on service and sophistication. Despite the greasy counters, lousy location and a dearth of customers, Bob and his family are determined to make Bob's Burgers \"grand re-re-re-opening\" a success.", + "airTime": "17:30", + "monitored": true, + "qualityProfileId": 1, + "seasonFolder": true, + "lastInfoSync": "2014-01-26T19:25:55.455594Z", + "runtime": 30, + "images": [ + { + "coverType": "banner", + "url": "http://slurm.trakt.us/images/banners/1387.6.jpg" + }, + { + "coverType": "poster", + "url": "http://slurm.trakt.us/images/posters/1387.6-300.jpg" + }, + { + "coverType": "fanart", + "url": "http://slurm.trakt.us/images/fanart/1387.6.jpg" + } + ], + "seriesType": "standard", + "network": "FOX", + "useSceneNumbering": false, + "titleSlug": "bobs-burgers", + "certification": "TV-14", + "path": "T:\\Bob's Burgers", + "year": 2011, + "firstAired": "2011-01-10T01:30:00Z", + "genres": [ + "Animation", + "Comedy" + ], + "tags": [], + "added": "2011-01-26T19:25:55.455594Z", + "qualityProfile": { + "value": { + "name": "SD", + "allowed": [ + { + "id": 1, + "name": "SDTV", + "weight": 1 + }, + { + "id": 8, + "name": "WEBDL-480p", + "weight": 2 + }, + { + "id": 2, + "name": "DVD", + "weight": 3 + } + ], + "cutoff": { + "id": 1, + "name": "SDTV", + "weight": 1 + }, + "id": 1 + }, + "isLoaded": true + }, + "seasons": [ + { + "seasonNumber": 4, + "monitored": true + }, + { + "seasonNumber": 3, + "monitored": true + }, + { + "seasonNumber": 2, + "monitored": true + }, + { + "seasonNumber": 1, + "monitored": true + }, + { + "seasonNumber": 0, + "monitored": false + } + ], + "id": 66 + }, + "downloading": false, + "id": 14402 + } +] diff --git a/tests/fixtures/sonarr/command.json b/tests/fixtures/sonarr/command.json new file mode 100644 index 00000000000..97acc2f9f82 --- /dev/null +++ b/tests/fixtures/sonarr/command.json @@ -0,0 +1,36 @@ +[ + { + "name": "RefreshSeries", + "body": { + "isNewSeries": false, + "sendUpdatesToClient": true, + "updateScheduledTask": true, + "completionMessage": "Completed", + "requiresDiskAccess": false, + "isExclusive": false, + "name": "RefreshSeries", + "trigger": "manual", + "suppressMessages": false + }, + "priority": "normal", + "status": "started", + "queued": "2020-04-06T16:54:06.41945Z", + "started": "2020-04-06T16:54:06.421322Z", + "trigger": "manual", + "state": "started", + "manual": true, + "startedOn": "2020-04-06T16:54:06.41945Z", + "stateChangeTime": "2020-04-06T16:54:06.421322Z", + "sendUpdatesToClient": true, + "updateScheduledTask": true, + "id": 368621 + }, + { + "name": "RefreshSeries", + "state": "started", + "startedOn": "2020-04-06T16:57:51.406504Z", + "stateChangeTime": "2020-04-06T16:57:51.417931Z", + "sendUpdatesToClient": true, + "id": 368629 + } +] diff --git a/tests/fixtures/sonarr/diskspace.json b/tests/fixtures/sonarr/diskspace.json new file mode 100644 index 00000000000..bc867cf21e5 --- /dev/null +++ b/tests/fixtures/sonarr/diskspace.json @@ -0,0 +1,8 @@ +[ + { + "path": "C:\\", + "label": "", + "freeSpace": 282500067328, + "totalSpace": 499738734592 + } +] diff --git a/tests/fixtures/sonarr/queue.json b/tests/fixtures/sonarr/queue.json new file mode 100644 index 00000000000..1a8eb0924c3 --- /dev/null +++ b/tests/fixtures/sonarr/queue.json @@ -0,0 +1,129 @@ +[ + { + "series": { + "title": "The Andy Griffith Show", + "sortTitle": "andy griffith show", + "seasonCount": 8, + "status": "ended", + "overview": "Down-home humor and an endearing cast of characters helped make The Andy Griffith Show one of the most beloved comedies in the history of TV. The show centered around widower Andy Taylor, who divided his time between raising his young son Opie, and his job as sheriff of the sleepy North Carolina town, Mayberry. Andy and Opie live with Andy's Aunt Bee, who serves as a surrogate mother to both father and son. Andy's nervous cousin, Barney Fife, is his deputy sheriff whose incompetence is tolerated because Mayberry is virtually crime-free.", + "network": "CBS", + "airTime": "21:30", + "images": [ + { + "coverType": "fanart", + "url": "https://artworks.thetvdb.com/banners/fanart/original/77754-5.jpg" + }, + { + "coverType": "banner", + "url": "https://artworks.thetvdb.com/banners/graphical/77754-g.jpg" + }, + { + "coverType": "poster", + "url": "https://artworks.thetvdb.com/banners/posters/77754-4.jpg" + } + ], + "seasons": [ + { + "seasonNumber": 0, + "monitored": false + }, + { + "seasonNumber": 1, + "monitored": false + }, + { + "seasonNumber": 2, + "monitored": true + }, + { + "seasonNumber": 3, + "monitored": false + }, + { + "seasonNumber": 4, + "monitored": false + }, + { + "seasonNumber": 5, + "monitored": true + }, + { + "seasonNumber": 6, + "monitored": true + }, + { + "seasonNumber": 7, + "monitored": true + }, + { + "seasonNumber": 8, + "monitored": true + } + ], + "year": 1960, + "path": "F:\\The Andy Griffith Show", + "profileId": 5, + "seasonFolder": true, + "monitored": true, + "useSceneNumbering": false, + "runtime": 25, + "tvdbId": 77754, + "tvRageId": 5574, + "tvMazeId": 3853, + "firstAired": "1960-02-15T06:00:00Z", + "lastInfoSync": "2016-02-05T16:40:11.614176Z", + "seriesType": "standard", + "cleanTitle": "theandygriffithshow", + "imdbId": "", + "titleSlug": "the-andy-griffith-show", + "certification": "TV-G", + "genres": [ + "Comedy" + ], + "tags": [], + "added": "2008-02-04T13:44:24.204583Z", + "ratings": { + "votes": 547, + "value": 8.6 + }, + "qualityProfileId": 5, + "id": 17 + }, + "episode": { + "seriesId": 17, + "episodeFileId": 0, + "seasonNumber": 1, + "episodeNumber": 1, + "title": "The New Housekeeper", + "airDate": "1960-10-03", + "airDateUtc": "1960-10-03T01:00:00Z", + "overview": "Sheriff Andy Taylor and his young son Opie are in need of a new housekeeper. Andy's Aunt Bee looks like the perfect candidate and moves in, but her presence causes friction with Opie.", + "hasFile": false, + "monitored": false, + "absoluteEpisodeNumber": 1, + "unverifiedSceneNumbering": false, + "id": 889 + }, + "quality": { + "quality": { + "id": 7, + "name": "SD" + }, + "revision": { + "version": 1, + "real": 0 + } + }, + "size": 4472186820, + "title": "The.Andy.Griffith.Show.S01E01.x264-GROUP", + "sizeleft": 0, + "timeleft": "00:00:00", + "estimatedCompletionTime": "2016-02-05T22:46:52.440104Z", + "status": "Downloading", + "trackedDownloadStatus": "Ok", + "statusMessages": [], + "downloadId": "SABnzbd_nzo_Mq2f_b", + "protocol": "usenet", + "id": 1503378561 + } +] diff --git a/tests/fixtures/sonarr/series.json b/tests/fixtures/sonarr/series.json new file mode 100644 index 00000000000..ea727c14a97 --- /dev/null +++ b/tests/fixtures/sonarr/series.json @@ -0,0 +1,163 @@ +[ + { + "title": "The Andy Griffith Show", + "alternateTitles": [], + "sortTitle": "andy griffith show", + "seasonCount": 8, + "totalEpisodeCount": 253, + "episodeCount": 0, + "episodeFileCount": 0, + "sizeOnDisk": 0, + "status": "ended", + "overview": "Down-home humor and an endearing cast of characters helped make The Andy Griffith Show one of the most beloved comedies in the history of TV. The show centered around widower Andy Taylor, who divided his time between raising his young son Opie, and his job as sheriff of the sleepy North Carolina town, Mayberry. Andy and Opie live with Andy's Aunt Bee, who serves as a surrogate mother to both father and son. Andy's nervous cousin, Barney Fife, is his deputy sheriff whose incompetence is tolerated because Mayberry is virtually crime-free.", + "network": "CBS", + "airTime": "21:30", + "images": [ + { + "coverType": "fanart", + "url": "/MediaCover/105/fanart.jpg?lastWrite=637217160281262470", + "remoteUrl": "https://artworks.thetvdb.com/banners/fanart/original/77754-5.jpg" + }, + { + "coverType": "banner", + "url": "/MediaCover/105/banner.jpg?lastWrite=637217160301222320", + "remoteUrl": "https://artworks.thetvdb.com/banners/graphical/77754-g.jpg" + }, + { + "coverType": "poster", + "url": "/MediaCover/105/poster.jpg?lastWrite=637217160322182160", + "remoteUrl": "https://artworks.thetvdb.com/banners/posters/77754-1.jpg" + } + ], + "seasons": [ + { + "seasonNumber": 0, + "monitored": false, + "statistics": { + "episodeFileCount": 0, + "episodeCount": 0, + "totalEpisodeCount": 4, + "sizeOnDisk": 0, + "percentOfEpisodes": 0.0 + } + }, + { + "seasonNumber": 1, + "monitored": false, + "statistics": { + "episodeFileCount": 0, + "episodeCount": 0, + "totalEpisodeCount": 32, + "sizeOnDisk": 0, + "percentOfEpisodes": 0.0 + } + }, + { + "seasonNumber": 2, + "monitored": false, + "statistics": { + "episodeFileCount": 0, + "episodeCount": 0, + "totalEpisodeCount": 31, + "sizeOnDisk": 0, + "percentOfEpisodes": 0.0 + } + }, + { + "seasonNumber": 3, + "monitored": false, + "statistics": { + "episodeFileCount": 8, + "episodeCount": 8, + "totalEpisodeCount": 32, + "sizeOnDisk": 8000000000, + "percentOfEpisodes": 100.0 + } + }, + { + "seasonNumber": 4, + "monitored": false, + "statistics": { + "episodeFileCount": 0, + "episodeCount": 0, + "totalEpisodeCount": 32, + "sizeOnDisk": 0, + "percentOfEpisodes": 0.0 + } + }, + { + "seasonNumber": 5, + "monitored": false, + "statistics": { + "episodeFileCount": 0, + "episodeCount": 0, + "totalEpisodeCount": 32, + "sizeOnDisk": 0, + "percentOfEpisodes": 0.0 + } + }, + { + "seasonNumber": 6, + "monitored": false, + "statistics": { + "episodeFileCount": 0, + "episodeCount": 0, + "totalEpisodeCount": 30, + "sizeOnDisk": 0, + "percentOfEpisodes": 0.0 + } + }, + { + "seasonNumber": 7, + "monitored": false, + "statistics": { + "episodeFileCount": 0, + "episodeCount": 0, + "totalEpisodeCount": 30, + "sizeOnDisk": 0, + "percentOfEpisodes": 0.0 + } + }, + { + "seasonNumber": 8, + "monitored": true, + "statistics": { + "episodeFileCount": 0, + "episodeCount": 0, + "totalEpisodeCount": 30, + "sizeOnDisk": 0, + "percentOfEpisodes": 0.0 + } + } + ], + "year": 1960, + "path": "F:\\The Andy Griffith Show", + "profileId": 2, + "languageProfileId": 1, + "seasonFolder": true, + "monitored": true, + "useSceneNumbering": false, + "runtime": 25, + "tvdbId": 77754, + "tvRageId": 5574, + "tvMazeId": 3853, + "firstAired": "1960-02-15T06:00:00Z", + "lastInfoSync": "2020-04-05T20:40:21.545669Z", + "seriesType": "standard", + "cleanTitle": "theandygriffithshow", + "imdbId": "tt0053479", + "titleSlug": "the-andy-griffith-show", + "certification": "TV-G", + "genres": [ + "Comedy" + ], + "tags": [], + "added": "2020-04-05T20:40:20.050044Z", + "ratings": { + "votes": 547, + "value": 8.6 + }, + "qualityProfileId": 2, + "id": 105 + } +] diff --git a/tests/fixtures/sonarr/system-status.json b/tests/fixtures/sonarr/system-status.json new file mode 100644 index 00000000000..c3969df08fe --- /dev/null +++ b/tests/fixtures/sonarr/system-status.json @@ -0,0 +1,18 @@ +{ + "version": "2.0.0.1121", + "buildTime": "2014-02-08T20:49:36.5560392Z", + "isDebug": false, + "isProduction": true, + "isAdmin": true, + "isUserInteractive": false, + "startupPath": "C:\\ProgramData\\NzbDrone\\bin", + "appData": "C:\\ProgramData\\NzbDrone", + "osVersion": "6.2.9200.0", + "isMono": false, + "isLinux": false, + "isWindows": true, + "branch": "develop", + "authentication": false, + "startOfWeek": 0, + "urlBase": "" +} diff --git a/tests/fixtures/sonarr/wanted-missing.json b/tests/fixtures/sonarr/wanted-missing.json new file mode 100644 index 00000000000..5db7c52f469 --- /dev/null +++ b/tests/fixtures/sonarr/wanted-missing.json @@ -0,0 +1,253 @@ +{ + "page": 1, + "pageSize": 10, + "sortKey": "airDateUtc", + "sortDirection": "descending", + "totalRecords": 2, + "records": [ + { + "seriesId": 3, + "episodeFileId": 0, + "seasonNumber": 4, + "episodeNumber": 11, + "title": "Easy Com-mercial, Easy Go-mercial", + "airDate": "2014-01-26", + "airDateUtc": "2014-01-27T01:30:00Z", + "overview": "To compete with fellow \"restaurateur,\" Jimmy Pesto, and his blowout Super Bowl event, Bob is determined to create a Bob's Burgers commercial to air during the \"big game.\" In an effort to outshine Pesto, the Belchers recruit Randy, a documentarian, to assist with the filmmaking and hire on former pro football star Connie Frye to be the celebrity endorser.", + "hasFile": false, + "monitored": true, + "sceneEpisodeNumber": 0, + "sceneSeasonNumber": 0, + "tvDbEpisodeId": 0, + "series": { + "tvdbId": 194031, + "tvRageId": 24607, + "imdbId": "tt1561755", + "title": "Bob's Burgers", + "sortTitle": "bob burgers", + "cleanTitle": "bobsburgers", + "seasonCount": 4, + "status": "continuing", + "overview": "Bob's Burgers follows a third-generation restaurateur, Bob, as he runs Bob's Burgers with the help of his wife and their three kids. Bob and his quirky family have big ideas about burgers, but fall short on service and sophistication. Despite the greasy counters, lousy location and a dearth of customers, Bob and his family are determined to make Bob's Burgers \"grand re-re-re-opening\" a success.", + "airTime": "17:30", + "monitored": true, + "qualityProfileId": 1, + "seasonFolder": true, + "lastInfoSync": "2014-01-26T19:25:55.455594Z", + "runtime": 30, + "images": [ + { + "coverType": "banner", + "url": "http://slurm.trakt.us/images/banners/1387.6.jpg" + }, + { + "coverType": "poster", + "url": "http://slurm.trakt.us/images/posters/1387.6-300.jpg" + }, + { + "coverType": "fanart", + "url": "http://slurm.trakt.us/images/fanart/1387.6.jpg" + } + ], + "seriesType": "standard", + "network": "FOX", + "useSceneNumbering": false, + "titleSlug": "bobs-burgers", + "certification": "TV-14", + "path": "T:\\Bob's Burgers", + "year": 2011, + "firstAired": "2011-01-10T01:30:00Z", + "genres": [ + "Animation", + "Comedy" + ], + "tags": [], + "added": "2011-01-26T19:25:55.455594Z", + "qualityProfile": { + "value": { + "name": "SD", + "allowed": [ + { + "id": 1, + "name": "SDTV", + "weight": 1 + }, + { + "id": 8, + "name": "WEBDL-480p", + "weight": 2 + }, + { + "id": 2, + "name": "DVD", + "weight": 3 + } + ], + "cutoff": { + "id": 1, + "name": "SDTV", + "weight": 1 + }, + "id": 1 + }, + "isLoaded": true + }, + "seasons": [ + { + "seasonNumber": 4, + "monitored": true + }, + { + "seasonNumber": 3, + "monitored": true + }, + { + "seasonNumber": 2, + "monitored": true + }, + { + "seasonNumber": 1, + "monitored": true + }, + { + "seasonNumber": 0, + "monitored": false + } + ], + "id": 66 + }, + "downloading": false, + "id": 14402 + }, + { + "seriesId": 17, + "episodeFileId": 0, + "seasonNumber": 1, + "episodeNumber": 1, + "title": "The New Housekeeper", + "airDate": "1960-10-03", + "airDateUtc": "1960-10-03T01:00:00Z", + "overview": "Sheriff Andy Taylor and his young son Opie are in need of a new housekeeper. Andy's Aunt Bee looks like the perfect candidate and moves in, but her presence causes friction with Opie.", + "hasFile": false, + "monitored": true, + "sceneEpisodeNumber": 0, + "sceneSeasonNumber": 0, + "tvDbEpisodeId": 0, + "series": { + "imdbId": "", + "tvdbId": 77754, + "tvRageId": 5574, + "tvMazeId": 3853, + "title": "The Andy Griffith Show", + "sortTitle": "andy griffith show", + "cleanTitle": "theandygriffithshow", + "seasonCount": 8, + "status": "ended", + "overview": "Down-home humor and an endearing cast of characters helped make The Andy Griffith Show one of the most beloved comedies in the history of TV. The show centered around widower Andy Taylor, who divided his time between raising his young son Opie, and his job as sheriff of the sleepy North Carolina town, Mayberry. Andy and Opie live with Andy's Aunt Bee, who serves as a surrogate mother to both father and son. Andy's nervous cousin, Barney Fife, is his deputy sheriff whose incompetence is tolerated because Mayberry is virtually crime-free.", + "airTime": "21:30", + "monitored": true, + "qualityProfileId": 1, + "seasonFolder": true, + "lastInfoSync": "2016-02-05T16:40:11.614176Z", + "runtime": 25, + "images": [ + { + "coverType": "fanart", + "url": "https://artworks.thetvdb.com/banners/fanart/original/77754-5.jpg" + }, + { + "coverType": "banner", + "url": "https://artworks.thetvdb.com/banners/graphical/77754-g.jpg" + }, + { + "coverType": "poster", + "url": "https://artworks.thetvdb.com/banners/posters/77754-4.jpg" + } + ], + "seriesType": "standard", + "network": "CBS", + "useSceneNumbering": false, + "titleSlug": "the-andy-griffith-show", + "certification": "TV-G", + "path": "F:\\The Andy Griffith Show", + "year": 1960, + "firstAired": "1960-02-15T06:00:00Z", + "genres": [ + "Comedy" + ], + "tags": [], + "added": "2008-02-04T13:44:24.204583Z", + "qualityProfile": { + "value": { + "name": "SD", + "allowed": [ + { + "id": 1, + "name": "SDTV", + "weight": 1 + }, + { + "id": 8, + "name": "WEBDL-480p", + "weight": 2 + }, + { + "id": 2, + "name": "DVD", + "weight": 3 + } + ], + "cutoff": { + "id": 1, + "name": "SDTV", + "weight": 1 + }, + "id": 1 + }, + "isLoaded": true + }, + "seasons": [ + { + "seasonNumber": 0, + "monitored": false + }, + { + "seasonNumber": 1, + "monitored": false + }, + { + "seasonNumber": 2, + "monitored": true + }, + { + "seasonNumber": 3, + "monitored": false + }, + { + "seasonNumber": 4, + "monitored": false + }, + { + "seasonNumber": 5, + "monitored": true + }, + { + "seasonNumber": 6, + "monitored": true + }, + { + "seasonNumber": 7, + "monitored": true + }, + { + "seasonNumber": 8, + "monitored": true + } + ], + "id": 17 + }, + "downloading": false, + "id": 889 + } + ] +} diff --git a/tests/fixtures/wled/rgb_single_segment.json b/tests/fixtures/wled/rgb_single_segment.json new file mode 100644 index 00000000000..e53ce680ece --- /dev/null +++ b/tests/fixtures/wled/rgb_single_segment.json @@ -0,0 +1,202 @@ +{ + "state": { + "on": true, + "bri": 127, + "transition": 7, + "ps": -1, + "pl": -1, + "nl": { + "on": false, + "dur": 60, + "fade": true, + "tbri": 0 + }, + "udpn": { + "send": false, + "recv": true + }, + "seg": [ + { + "id": 0, + "start": 0, + "stop": 30, + "len": 20, + "col": [[255, 159, 0], [0, 0, 0], [0, 0, 0]], + "fx": 0, + "sx": 32, + "ix": 128, + "pal": 0, + "sel": true, + "rev": false, + "cln": -1 + } + ] + }, + "info": { + "ver": "0.8.5", + "vid": 1909122, + "leds": { + "count": 30, + "rgbw": false, + "pin": [2], + "pwr": 470, + "maxpwr": 850, + "maxseg": 10 + }, + "name": "WLED RGB Light", + "udpport": 21324, + "live": false, + "fxcount": 81, + "palcount": 50, + "wifi": { + "bssid": "AA:AA:AA:AA:AA:BB", + "rssi": -62, + "signal": 76, + "channel": 11 + }, + "arch": "esp8266", + "core": "2_4_2", + "freeheap": 14600, + "uptime": 32, + "opt": 119, + "brand": "WLED", + "product": "DIY light", + "btype": "bin", + "mac": "aabbccddeeff" + }, + "effects": [ + "Solid", + "Blink", + "Breathe", + "Wipe", + "Wipe Random", + "Random Colors", + "Sweep", + "Dynamic", + "Colorloop", + "Rainbow", + "Scan", + "Dual Scan", + "Fade", + "Chase", + "Chase Rainbow", + "Running", + "Saw", + "Twinkle", + "Dissolve", + "Dissolve Rnd", + "Sparkle", + "Dark Sparkle", + "Sparkle+", + "Strobe", + "Strobe Rainbow", + "Mega Strobe", + "Blink Rainbow", + "Android", + "Chase", + "Chase Random", + "Chase Rainbow", + "Chase Flash", + "Chase Flash Rnd", + "Rainbow Runner", + "Colorful", + "Traffic Light", + "Sweep Random", + "Running 2", + "Red & Blue", + "Stream", + "Scanner", + "Lighthouse", + "Fireworks", + "Rain", + "Merry Christmas", + "Fire Flicker", + "Gradient", + "Loading", + "In Out", + "In In", + "Out Out", + "Out In", + "Circus", + "Halloween", + "Tri Chase", + "Tri Wipe", + "Tri Fade", + "Lightning", + "ICU", + "Multi Comet", + "Dual Scanner", + "Stream 2", + "Oscillate", + "Pride 2015", + "Juggle", + "Palette", + "Fire 2012", + "Colorwaves", + "BPM", + "Fill Noise", + "Noise 1", + "Noise 2", + "Noise 3", + "Noise 4", + "Colortwinkle", + "Lake", + "Meteor", + "Smooth Meteor", + "Railway", + "Ripple", + "Twinklefox" + ], + "palettes": [ + "Default", + "Random Cycle", + "Primary Color", + "Based on Primary", + "Set Colors", + "Based on Set", + "Party", + "Cloud", + "Lava", + "Ocean", + "Forest", + "Rainbow", + "Rainbow Bands", + "Sunset", + "Rivendell", + "Breeze", + "Red & Blue", + "Yellowout", + "Analogous", + "Splash", + "Pastel", + "Sunset 2", + "Beech", + "Vintage", + "Departure", + "Landscape", + "Beach", + "Sherbet", + "Hult", + "Hult 64", + "Drywet", + "Jul", + "Grintage", + "Rewhi", + "Tertiary", + "Fire", + "Icefire", + "Cyane", + "Light Pink", + "Autumn", + "Magenta", + "Magred", + "Yelmag", + "Yelblu", + "Orange & Teal", + "Tiamat", + "April Night", + "Orangery", + "C9", + "Sakura" + ] +} diff --git a/tests/helpers/test_aiohttp_client.py b/tests/helpers/test_aiohttp_client.py index 1b5121d75e0..0f37ebd7b3b 100644 --- a/tests/helpers/test_aiohttp_client.py +++ b/tests/helpers/test_aiohttp_client.py @@ -27,6 +27,7 @@ def camera_client_fixture(hass, hass_client): }, ) ) + hass.loop.run_until_complete(hass.async_block_till_done()) yield hass.loop.run_until_complete(hass_client()) diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 72eb61bbacb..d0f19f356ae 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -548,8 +548,7 @@ def test_deprecated_with_no_optionals(caplog, schema): "homeassistant.helpers.config_validation", ] assert ( - "The 'mars' option (with value 'True') is deprecated, " - "please remove it from your configuration" + "The 'mars' option is deprecated, please remove it from your configuration" ) in caplog.text assert test_data == output @@ -582,8 +581,7 @@ def test_deprecated_with_replacement_key(caplog, schema): output = deprecated_schema(test_data.copy()) assert len(caplog.records) == 1 assert ( - "The 'mars' option (with value 'True') is deprecated, " - "please replace it with 'jupiter'" + "The 'mars' option is deprecated, please replace it with 'jupiter'" ) in caplog.text assert {"jupiter": True} == output @@ -617,7 +615,7 @@ def test_deprecated_with_invalidation_version(caplog, schema, version): ) message = ( - "The 'mars' option (with value 'True') is deprecated, " + "The 'mars' option is deprecated, " "please remove it from your configuration. " "This option will become invalid in version 1.0.0" ) @@ -643,7 +641,7 @@ def test_deprecated_with_invalidation_version(caplog, schema, version): with pytest.raises(vol.MultipleInvalid) as exc_info: invalidated_schema(test_data) assert str(exc_info.value) == ( - "The 'mars' option (with value 'True') is deprecated, " + "The 'mars' option is deprecated, " "please remove it from your configuration. This option will " "become invalid in version 0.1.0" ) @@ -671,7 +669,7 @@ def test_deprecated_with_replacement_key_and_invalidation_version( ) warning = ( - "The 'mars' option (with value 'True') is deprecated, " + "The 'mars' option is deprecated, " "please replace it with 'jupiter'. This option will become " "invalid in version 1.0.0" ) @@ -703,7 +701,7 @@ def test_deprecated_with_replacement_key_and_invalidation_version( with pytest.raises(vol.MultipleInvalid) as exc_info: invalidated_schema(test_data) assert str(exc_info.value) == ( - "The 'mars' option (with value 'True') is deprecated, " + "The 'mars' option is deprecated, " "please replace it with 'jupiter'. This option will become " "invalid in version 0.1.0" ) @@ -725,8 +723,7 @@ def test_deprecated_with_default(caplog, schema): assert len(caplog.records) == 1 assert caplog.records[0].name == __name__ assert ( - "The 'mars' option (with value 'True') is deprecated, " - "please remove it from your configuration" + "The 'mars' option is deprecated, please remove it from your configuration" ) in caplog.text assert test_data == output @@ -759,8 +756,7 @@ def test_deprecated_with_replacement_key_and_default(caplog, schema): output = deprecated_schema(test_data.copy()) assert len(caplog.records) == 1 assert ( - "The 'mars' option (with value 'True') is deprecated, " - "please replace it with 'jupiter'" + "The 'mars' option is deprecated, please replace it with 'jupiter'" ) in caplog.text assert {"jupiter": True} == output @@ -792,8 +788,7 @@ def test_deprecated_with_replacement_key_and_default(caplog, schema): output = deprecated_schema_with_default(test_data.copy()) assert len(caplog.records) == 1 assert ( - "The 'mars' option (with value 'True') is deprecated, " - "please replace it with 'jupiter'" + "The 'mars' option is deprecated, please replace it with 'jupiter'" ) in caplog.text assert {"jupiter": True} == output @@ -828,7 +823,7 @@ def test_deprecated_with_replacement_key_invalidation_version_default( output = deprecated_schema(test_data.copy()) assert len(caplog.records) == 1 assert ( - "The 'mars' option (with value 'True') is deprecated, " + "The 'mars' option is deprecated, " "please replace it with 'jupiter'. This option will become " "invalid in version 1.0.0" ) in caplog.text @@ -855,7 +850,7 @@ def test_deprecated_with_replacement_key_invalidation_version_default( with pytest.raises(vol.MultipleInvalid) as exc_info: invalidated_schema(test_data) assert str(exc_info.value) == ( - "The 'mars' option (with value 'True') is deprecated, " + "The 'mars' option is deprecated, " "please replace it with 'jupiter'. This option will become " "invalid in version 0.1.0" ) diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 3fbb73a2aa8..82fadc35dd2 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -153,11 +153,21 @@ async def test_loading_from_storage(hass, hass_storage): "area_id": "12345A", "name_by_user": "Test Friendly Name", } - ] + ], + "deleted_devices": [ + { + "config_entries": ["1234"], + "connections": [["Zigbee", "23.45.67.89.01"]], + "id": "bcdefghijklmn", + "identifiers": [["serial", "34:56:AB:CD:EF:12"]], + } + ], }, } registry = await device_registry.async_get_registry(hass) + assert len(registry.devices) == 1 + assert len(registry.deleted_devices) == 1 entry = registry.async_get_or_create( config_entry_id="1234", @@ -171,6 +181,20 @@ async def test_loading_from_storage(hass, hass_storage): assert entry.name_by_user == "Test Friendly Name" assert entry.entry_type == "service" assert isinstance(entry.config_entries, set) + assert isinstance(entry.connections, set) + assert isinstance(entry.identifiers, set) + + entry = registry.async_get_or_create( + config_entry_id="1234", + connections={("Zigbee", "23.45.67.89.01")}, + identifiers={("serial", "34:56:AB:CD:EF:12")}, + manufacturer="manufacturer", + model="model", + ) + assert entry.id == "bcdefghijklmn" + assert isinstance(entry.config_entries, set) + assert isinstance(entry.connections, set) + assert isinstance(entry.identifiers, set) async def test_removing_config_entries(hass, registry, update_events): @@ -224,6 +248,79 @@ async def test_removing_config_entries(hass, registry, update_events): assert update_events[4]["device_id"] == entry3.id +async def test_deleted_device_removing_config_entries(hass, registry, update_events): + """Make sure we do not get duplicate entries.""" + entry = registry.async_get_or_create( + config_entry_id="123", + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + entry2 = registry.async_get_or_create( + config_entry_id="456", + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + entry3 = registry.async_get_or_create( + config_entry_id="123", + connections={(device_registry.CONNECTION_NETWORK_MAC, "34:56:78:CD:EF:12")}, + identifiers={("bridgeid", "4567")}, + manufacturer="manufacturer", + model="model", + ) + + assert len(registry.devices) == 2 + assert len(registry.deleted_devices) == 0 + assert entry.id == entry2.id + assert entry.id != entry3.id + assert entry2.config_entries == {"123", "456"} + + registry.async_remove_device(entry.id) + registry.async_remove_device(entry3.id) + + assert len(registry.devices) == 0 + assert len(registry.deleted_devices) == 2 + + await hass.async_block_till_done() + assert len(update_events) == 5 + assert update_events[0]["action"] == "create" + assert update_events[0]["device_id"] == entry.id + assert update_events[1]["action"] == "update" + assert update_events[1]["device_id"] == entry2.id + assert update_events[2]["action"] == "create" + assert update_events[2]["device_id"] == entry3.id + assert update_events[3]["action"] == "remove" + assert update_events[3]["device_id"] == entry.id + assert update_events[4]["action"] == "remove" + assert update_events[4]["device_id"] == entry3.id + + registry.async_clear_config_entry("123") + assert len(registry.devices) == 0 + assert len(registry.deleted_devices) == 1 + + registry.async_clear_config_entry("456") + assert len(registry.devices) == 0 + assert len(registry.deleted_devices) == 0 + + # No event when a deleted device is purged + await hass.async_block_till_done() + assert len(update_events) == 5 + + # Re-add, expect new device id + entry2 = registry.async_get_or_create( + config_entry_id="123", + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + + assert entry.id != entry2.id + + async def test_removing_area_id(registry): """Make sure we can clear area id.""" entry = registry.async_get_or_create( @@ -243,6 +340,36 @@ async def test_removing_area_id(registry): assert entry_w_area != entry_wo_area +async def test_deleted_device_removing_area_id(registry): + """Make sure we can clear area id of deleted device.""" + entry = registry.async_get_or_create( + config_entry_id="123", + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + + entry_w_area = registry.async_update_device(entry.id, area_id="12345A") + + registry.async_remove_device(entry.id) + registry.async_clear_area_id("12345A") + + entry2 = registry.async_get_or_create( + config_entry_id="123", + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + assert entry.id == entry2.id + + entry_wo_area = registry.async_get_device({("bridgeid", "0123")}, set()) + + assert not entry_wo_area.area_id + assert entry_w_area != entry_wo_area + + async def test_specifying_via_device_create(registry): """Test specifying a via_device and updating.""" via = registry.async_get_or_create( @@ -320,7 +447,19 @@ async def test_loading_saving_data(hass, registry): via_device=("hue", "0123"), ) + orig_light2 = registry.async_get_or_create( + config_entry_id="456", + connections=set(), + identifiers={("hue", "789")}, + manufacturer="manufacturer", + model="light", + via_device=("hue", "0123"), + ) + + registry.async_remove_device(orig_light2.id) + assert len(registry.devices) == 2 + assert len(registry.deleted_devices) == 1 orig_via = registry.async_update_device( orig_via.id, area_id="mock-area-id", name_by_user="mock-name-by-user" @@ -333,6 +472,7 @@ async def test_loading_saving_data(hass, registry): # Ensure same order assert list(registry.devices) == list(registry2.devices) + assert list(registry.deleted_devices) == list(registry2.deleted_devices) new_via = registry2.async_get_device({("hue", "0123")}, set()) new_light = registry2.async_get_device({("hue", "456")}, set()) @@ -584,3 +724,103 @@ async def test_cleanup_entity_registry_change(hass): ent_reg.async_remove(entity.entity_id) await hass.async_block_till_done() assert len(mock_call.mock_calls) == 2 + + +async def test_restore_device(hass, registry, update_events): + """Make sure device id is stable.""" + entry = registry.async_get_or_create( + config_entry_id="123", + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + + assert len(registry.devices) == 1 + assert len(registry.deleted_devices) == 0 + + registry.async_remove_device(entry.id) + + assert len(registry.devices) == 0 + assert len(registry.deleted_devices) == 1 + + entry2 = registry.async_get_or_create( + config_entry_id="123", + connections={(device_registry.CONNECTION_NETWORK_MAC, "34:56:78:CD:EF:12")}, + identifiers={("bridgeid", "4567")}, + manufacturer="manufacturer", + model="model", + ) + entry3 = registry.async_get_or_create( + config_entry_id="123", + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + + assert entry.id == entry3.id + assert entry.id != entry2.id + assert len(registry.devices) == 2 + assert len(registry.deleted_devices) == 0 + + assert isinstance(entry3.config_entries, set) + assert isinstance(entry3.connections, set) + assert isinstance(entry3.identifiers, set) + + await hass.async_block_till_done() + + assert len(update_events) == 4 + assert update_events[0]["action"] == "create" + assert update_events[0]["device_id"] == entry.id + assert update_events[1]["action"] == "remove" + assert update_events[1]["device_id"] == entry.id + assert update_events[2]["action"] == "create" + assert update_events[2]["device_id"] == entry2.id + assert update_events[3]["action"] == "create" + assert update_events[3]["device_id"] == entry3.id + + +async def test_restore_simple_device(hass, registry, update_events): + """Make sure device id is stable.""" + entry = registry.async_get_or_create( + config_entry_id="123", + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + ) + + assert len(registry.devices) == 1 + assert len(registry.deleted_devices) == 0 + + registry.async_remove_device(entry.id) + + assert len(registry.devices) == 0 + assert len(registry.deleted_devices) == 1 + + entry2 = registry.async_get_or_create( + config_entry_id="123", + connections={(device_registry.CONNECTION_NETWORK_MAC, "34:56:78:CD:EF:12")}, + identifiers={("bridgeid", "4567")}, + ) + entry3 = registry.async_get_or_create( + config_entry_id="123", + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + ) + + assert entry.id == entry3.id + assert entry.id != entry2.id + assert len(registry.devices) == 2 + assert len(registry.deleted_devices) == 0 + + await hass.async_block_till_done() + + assert len(update_events) == 4 + assert update_events[0]["action"] == "create" + assert update_events[0]["device_id"] == entry.id + assert update_events[1]["action"] == "remove" + assert update_events[1]["device_id"] == entry.id + assert update_events[2]["action"] == "create" + assert update_events[2]["device_id"] == entry2.id + assert update_events[3]["action"] == "create" + assert update_events[3]["device_id"] == entry3.id diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 625f21d9d9f..8935b25830e 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -180,7 +180,7 @@ async def test_platform_not_ready(hass): component = EntityComponent(_LOGGER, DOMAIN, hass) await component.async_setup({DOMAIN: {"platform": "mod1"}}) - + await hass.async_block_till_done() assert len(platform1_setup.mock_calls) == 1 assert "test_domain.mod1" not in hass.config.components @@ -280,7 +280,7 @@ async def test_setup_dependencies_platform(hass): component = EntityComponent(_LOGGER, DOMAIN, hass) await component.async_setup({DOMAIN: {"platform": "test_component"}}) - + await hass.async_block_till_done() assert "test_component" in hass.config.components assert "test_component2" in hass.config.components assert "test_domain.test_component" in hass.config.components diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index eb24ea971a7..11dded7416f 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -184,11 +184,12 @@ async def test_platform_warn_slow_setup(hass): with patch.object(hass.loop, "call_later") as mock_call: await component.async_setup({DOMAIN: {"platform": "platform"}}) + await hass.async_block_till_done() assert mock_call.called # mock_calls[0] is the warning message for component setup - # mock_calls[3] is the warning message for platform setup - timeout, logger_method = mock_call.mock_calls[3][1][:2] + # mock_calls[5] is the warning message for platform setup + timeout, logger_method = mock_call.mock_calls[5][1][:2] assert timeout == entity_platform.SLOW_SETUP_WARNING assert logger_method == _LOGGER.warning @@ -209,6 +210,7 @@ async def test_platform_error_slow_setup(hass, caplog): component = EntityComponent(_LOGGER, DOMAIN, hass) mock_entity_platform(hass, "test_domain.test_platform", platform) await component.async_setup({DOMAIN: {"platform": "test_platform"}}) + await hass.async_block_till_done() assert len(called) == 1 assert "test_domain.test_platform" not in hass.config.components assert "test_platform is taking longer than 0 seconds" in caplog.text @@ -242,6 +244,7 @@ async def test_parallel_updates_async_platform(hass): component._platforms = {} await component.async_setup({DOMAIN: {"platform": "platform"}}) + await hass.async_block_till_done() handle = list(component._platforms.values())[-1] assert handle.parallel_updates is None @@ -268,6 +271,7 @@ async def test_parallel_updates_async_platform_with_constant(hass): component._platforms = {} await component.async_setup({DOMAIN: {"platform": "platform"}}) + await hass.async_block_till_done() handle = list(component._platforms.values())[-1] @@ -293,6 +297,7 @@ async def test_parallel_updates_sync_platform(hass): component._platforms = {} await component.async_setup({DOMAIN: {"platform": "platform"}}) + await hass.async_block_till_done() handle = list(component._platforms.values())[-1] @@ -319,6 +324,7 @@ async def test_parallel_updates_sync_platform_with_constant(hass): component._platforms = {} await component.async_setup({DOMAIN: {"platform": "platform"}}) + await hass.async_block_till_done() handle = list(component._platforms.values())[-1] diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index 77e55a1d6ed..440b3d75439 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -113,6 +113,7 @@ async def test_get_translations(hass, mock_config_flows): assert translations == {} assert await async_setup_component(hass, "switch", {"switch": {"platform": "test"}}) + await hass.async_block_till_done() translations = await translation.async_get_translations(hass, "en", "state") diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index a639b16893b..e14afdca28a 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -1,5 +1,6 @@ """Test the bootstrapping.""" # pylint: disable=protected-access +import asyncio import logging import os from unittest.mock import Mock @@ -37,6 +38,15 @@ async def test_home_assistant_core_config_validation(hass): assert result is None +async def test_async_enable_logging(hass): + """Test to ensure logging is migrated to the queue handlers.""" + with patch("logging.getLogger"), patch( + "homeassistant.bootstrap.async_activate_log_queue_handler" + ) as mock_async_activate_log_queue_handler: + bootstrap.async_enable_logging(hass) + mock_async_activate_log_queue_handler.assert_called_once() + + async def test_load_hassio(hass): """Test that we load Hass.io component.""" with patch.dict(os.environ, {}, clear=True): @@ -240,6 +250,7 @@ async def test_setup_hass( mock_mount_local_lib_path, mock_ensure_config_exists, mock_process_ha_config_upgrade, + caplog, ): """Test it works.""" verbose = Mock() @@ -250,6 +261,8 @@ async def test_setup_hass( with patch( "homeassistant.config.async_hass_config_yaml", return_value={"browser": {}, "frontend": {}}, + ), patch.object(bootstrap, "LOG_SLOW_STARTUP_INTERVAL", 5000), patch( + "homeassistant.components.http.start_http_server_and_save_config" ): hass = await bootstrap.async_setup_hass( config_dir=get_test_config_dir(), @@ -261,6 +274,8 @@ async def test_setup_hass( safe_mode=False, ) + assert "Waiting on integrations to complete setup" not in caplog.text + assert "browser" in hass.config.components assert "safe_mode" not in hass.config.components @@ -277,6 +292,46 @@ async def test_setup_hass( assert len(mock_process_ha_config_upgrade.mock_calls) == 1 +async def test_setup_hass_takes_longer_than_log_slow_startup( + mock_enable_logging, + mock_is_virtual_env, + mock_mount_local_lib_path, + mock_ensure_config_exists, + mock_process_ha_config_upgrade, + caplog, +): + """Test it works.""" + verbose = Mock() + log_rotate_days = Mock() + log_file = Mock() + log_no_color = Mock() + + async def _async_setup_that_blocks_startup(*args, **kwargs): + await asyncio.sleep(0.6) + return True + + with patch( + "homeassistant.config.async_hass_config_yaml", + return_value={"browser": {}, "frontend": {}}, + ), patch.object(bootstrap, "LOG_SLOW_STARTUP_INTERVAL", 0.3), patch( + "homeassistant.components.frontend.async_setup", + side_effect=_async_setup_that_blocks_startup, + ), patch( + "homeassistant.components.http.start_http_server_and_save_config" + ): + await bootstrap.async_setup_hass( + config_dir=get_test_config_dir(), + verbose=verbose, + log_rotate_days=log_rotate_days, + log_file=log_file, + log_no_color=log_no_color, + skip_pip=True, + safe_mode=False, + ) + + assert "Waiting on integrations to complete setup" in caplog.text + + async def test_setup_hass_invalid_yaml( mock_enable_logging, mock_is_virtual_env, @@ -287,7 +342,7 @@ async def test_setup_hass_invalid_yaml( """Test it works.""" with patch( "homeassistant.config.async_hass_config_yaml", side_effect=HomeAssistantError - ): + ), patch("homeassistant.components.http.start_http_server_and_save_config"): hass = await bootstrap.async_setup_hass( config_dir=get_test_config_dir(), verbose=False, @@ -340,7 +395,9 @@ async def test_setup_hass_safe_mode( hass.config_entries._async_schedule_save() await flush_store(hass.config_entries._store) - with patch("homeassistant.components.browser.setup") as browser_setup: + with patch("homeassistant.components.browser.setup") as browser_setup, patch( + "homeassistant.components.http.start_http_server_and_save_config" + ): hass = await bootstrap.async_setup_hass( config_dir=get_test_config_dir(), verbose=False, @@ -370,7 +427,7 @@ async def test_setup_hass_invalid_core_config( with patch( "homeassistant.config.async_hass_config_yaml", return_value={"homeassistant": {"non-existing": 1}}, - ): + ), patch("homeassistant.components.http.start_http_server_and_save_config"): hass = await bootstrap.async_setup_hass( config_dir=get_test_config_dir(), verbose=False, @@ -400,7 +457,7 @@ async def test_setup_safe_mode_if_no_frontend( with patch( "homeassistant.config.async_hass_config_yaml", return_value={"map": {}, "person": {"invalid": True}}, - ): + ), patch("homeassistant.components.http.start_http_server_and_save_config"): hass = await bootstrap.async_setup_hass( config_dir=get_test_config_dir(), verbose=verbose, diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 592bc1d4656..12b9c7308aa 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -365,6 +365,28 @@ async def test_remove_entry_if_not_loaded(hass, manager): assert len(mock_unload_entry.mock_calls) == 0 +async def test_remove_entry_if_integration_deleted(hass, manager): + """Test that we can remove an entry when the integration is deleted.""" + mock_unload_entry = AsyncMock(return_value=True) + + MockConfigEntry(domain="test", entry_id="test1").add_to_manager(manager) + MockConfigEntry(domain="comp", entry_id="test2").add_to_manager(manager) + MockConfigEntry(domain="test", entry_id="test3").add_to_manager(manager) + + assert [item.entry_id for item in manager.async_entries()] == [ + "test1", + "test2", + "test3", + ] + + result = await manager.async_remove("test2") + + assert result == {"require_restart": False} + assert [item.entry_id for item in manager.async_entries()] == ["test1", "test3"] + + assert len(mock_unload_entry.mock_calls) == 0 + + async def test_add_entry_calls_setup_entry(hass, manager): """Test we call setup_config_entry.""" mock_setup_entry = AsyncMock(return_value=True) diff --git a/tests/test_core.py b/tests/test_core.py index 3bc001b78b6..9fc257eaf2d 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -35,7 +35,7 @@ from homeassistant.exceptions import InvalidEntityFormatError, InvalidStateError import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import METRIC_SYSTEM -from tests.async_mock import MagicMock, Mock, patch +from tests.async_mock import MagicMock, Mock, PropertyMock, patch from tests.common import async_mock_service, get_test_home_assistant PST = pytz.timezone("America/Los_Angeles") @@ -901,6 +901,8 @@ class TestConfig(unittest.TestCase): def test_as_dict(self): """Test as dict.""" self.config.config_dir = "/test/ha-config" + self.config.hass = MagicMock() + type(self.config.hass.state).value = PropertyMock(return_value="RUNNING") expected = { "latitude": 0, "longitude": 0, @@ -914,6 +916,7 @@ class TestConfig(unittest.TestCase): "version": __version__, "config_source": "default", "safe_mode": False, + "state": "RUNNING", "external_url": None, "internal_url": None, } diff --git a/tests/test_requirements.py b/tests/test_requirements.py index f98485e8006..20202f91e89 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -221,8 +221,10 @@ async def test_discovery_requirements_ssdp(hass): ) as mock_process: await async_get_integration_with_requirements(hass, "ssdp_comp") - assert len(mock_process.mock_calls) == 1 + assert len(mock_process.mock_calls) == 3 assert mock_process.mock_calls[0][1][2] == ssdp.requirements + # Ensure zeroconf is a dep for ssdp + assert mock_process.mock_calls[1][1][1] == "zeroconf" @pytest.mark.parametrize( diff --git a/tests/test_setup.py b/tests/test_setup.py index 4197fe7370a..4ff380d0cc8 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -358,6 +358,7 @@ class TestSetup: "switch", {"comp_a": {"valid": True}, "switch": {"platform": "platform_a"}}, ) + self.hass.block_till_done() assert "comp_a" in self.hass.config.components def test_platform_specific_config_validation(self): @@ -380,6 +381,7 @@ class TestSetup: "switch", {"switch": {"platform": "platform_a", "invalid": True}}, ) + self.hass.block_till_done() assert mock_setup.call_count == 0 self.hass.data.pop(setup.DATA_SETUP) @@ -397,6 +399,7 @@ class TestSetup: } }, ) + self.hass.block_till_done() assert mock_setup.call_count == 0 self.hass.data.pop(setup.DATA_SETUP) @@ -408,6 +411,7 @@ class TestSetup: "switch", {"switch": {"platform": "platform_a", "valid": True}}, ) + self.hass.block_till_done() assert mock_setup.call_count == 1 def test_disable_component_if_invalid_return(self): @@ -489,13 +493,16 @@ async def test_component_warn_slow_setup(hass): result = await setup.async_setup_component(hass, "test_component1", {}) assert result assert mock_call.called - assert len(mock_call.mock_calls) == 3 + assert len(mock_call.mock_calls) == 5 timeout, logger_method = mock_call.mock_calls[0][1][:2] assert timeout == setup.SLOW_SETUP_WARNING assert logger_method == setup._LOGGER.warning + timeout, function = mock_call.mock_calls[1][1][:2] + assert timeout == setup.SLOW_SETUP_MAX_WAIT + assert mock_call().cancel.called @@ -507,7 +514,26 @@ async def test_platform_no_warn_slow(hass): with patch.object(hass.loop, "call_later") as mock_call: result = await setup.async_setup_component(hass, "test_component1", {}) assert result - assert not mock_call.called + timeout, function = mock_call.mock_calls[0][1][:2] + assert timeout == setup.SLOW_SETUP_MAX_WAIT + + +async def test_platform_error_slow_setup(hass, caplog): + """Don't block startup more than SLOW_SETUP_MAX_WAIT.""" + + with patch.object(setup, "SLOW_SETUP_MAX_WAIT", 1): + called = [] + + async def async_setup(*args): + """Tracking Setup.""" + called.append(1) + await asyncio.sleep(2) + + mock_integration(hass, MockModule("test_component1", async_setup=async_setup)) + result = await setup.async_setup_component(hass, "test_component1", {}) + assert len(called) == 1 + assert not result + assert "test_component1 is taking longer than 1 seconds" in caplog.text async def test_when_setup_already_loaded(hass): diff --git a/tests/util/test_color.py b/tests/util/test_color.py index 99ebcd72554..8f520f4a7ec 100644 --- a/tests/util/test_color.py +++ b/tests/util/test_color.py @@ -288,28 +288,20 @@ def test_gamut(): assert not color_util.check_valid_gamut(GAMUT_INVALID_4) -def test_should_return_25000_kelvin_when_input_is_40_mired(): - """Function should return 25000K if given 40 mired.""" - kelvin = color_util.color_temperature_mired_to_kelvin(40) - assert kelvin == 25000 +def test_color_temperature_mired_to_kelvin(): + """Test color_temperature_mired_to_kelvin.""" + assert color_util.color_temperature_mired_to_kelvin(40) == 25000 + assert color_util.color_temperature_mired_to_kelvin(200) == 5000 + with pytest.raises(ZeroDivisionError): + assert color_util.color_temperature_mired_to_kelvin(0) -def test_should_return_5000_kelvin_when_input_is_200_mired(): - """Function should return 5000K if given 200 mired.""" - kelvin = color_util.color_temperature_mired_to_kelvin(200) - assert kelvin == 5000 - - -def test_should_return_40_mired_when_input_is_25000_kelvin(): - """Function should return 40 mired when given 25000 Kelvin.""" - mired = color_util.color_temperature_kelvin_to_mired(25000) - assert mired == 40 - - -def test_should_return_200_mired_when_input_is_5000_kelvin(): - """Function should return 200 mired when given 5000 Kelvin.""" - mired = color_util.color_temperature_kelvin_to_mired(5000) - assert mired == 200 +def test_color_temperature_kelvin_to_mired(): + """Test color_temperature_kelvin_to_mired.""" + assert color_util.color_temperature_kelvin_to_mired(25000) == 40 + assert color_util.color_temperature_kelvin_to_mired(5000) == 200 + with pytest.raises(ZeroDivisionError): + assert color_util.color_temperature_kelvin_to_mired(0) def test_returns_same_value_for_any_two_temperatures_below_1000(): diff --git a/tests/util/test_logging.py b/tests/util/test_logging.py index 2d05157e26f..04d6f133381 100644 --- a/tests/util/test_logging.py +++ b/tests/util/test_logging.py @@ -1,12 +1,14 @@ """Test Home Assistant logging util methods.""" import asyncio import logging -import threading +import queue import pytest import homeassistant.util.logging as logging_util +from tests.async_mock import patch + def test_sensitive_data_filter(): """Test the logging sensitive data filter.""" @@ -21,50 +23,51 @@ def test_sensitive_data_filter(): assert sensitive_record.msg == "******* log" -async def test_async_handler_loop_log(loop): - """Test logging data inside from inside the event loop.""" - loop._thread_ident = threading.get_ident() +async def test_logging_with_queue_handler(): + """Test logging with HomeAssistantQueueHandler.""" - queue = asyncio.Queue(loop=loop) - base_handler = logging.handlers.QueueHandler(queue) - handler = logging_util.AsyncHandler(loop, base_handler) - - # Test passthrough props and noop functions - assert handler.createLock() is None - assert handler.acquire() is None - assert handler.release() is None - assert handler.formatter is base_handler.formatter - assert handler.name is base_handler.get_name() - handler.name = "mock_name" - assert base_handler.get_name() == "mock_name" + simple_queue = queue.SimpleQueue() # type: ignore + handler = logging_util.HomeAssistantQueueHandler(simple_queue) log_record = logging.makeLogRecord({"msg": "Test Log Record"}) + handler.emit(log_record) - await handler.async_close(True) - assert queue.get_nowait().msg == "Test Log Record" - assert queue.empty() - -async def test_async_handler_thread_log(loop): - """Test logging data from a thread.""" - loop._thread_ident = threading.get_ident() - - queue = asyncio.Queue(loop=loop) - base_handler = logging.handlers.QueueHandler(queue) - handler = logging_util.AsyncHandler(loop, base_handler) - - log_record = logging.makeLogRecord({"msg": "Test Log Record"}) - - def add_log(): - """Emit a mock log.""" + with pytest.raises(asyncio.CancelledError), patch.object( + handler, "enqueue", side_effect=asyncio.CancelledError + ): handler.emit(log_record) - handler.close() - await loop.run_in_executor(None, add_log) - await handler.async_close(True) + with patch.object(handler, "emit") as emit_mock: + handler.handle(log_record) + emit_mock.assert_called_once() - assert queue.get_nowait().msg == "Test Log Record" - assert queue.empty() + with patch.object(handler, "filter") as filter_mock, patch.object( + handler, "emit" + ) as emit_mock: + filter_mock.return_value = False + handler.handle(log_record) + emit_mock.assert_not_called() + + with patch.object(handler, "enqueue", side_effect=OSError), patch.object( + handler, "handleError" + ) as mock_handle_error: + handler.emit(log_record) + mock_handle_error.assert_called_once() + + handler.close() + + assert simple_queue.get_nowait().msg == "Test Log Record" + assert simple_queue.empty() + + +async def test_migrate_log_handler(hass): + """Test migrating log handlers.""" + + logging_util.async_activate_log_queue_handler(hass) + + assert len(logging.root.handlers) == 1 + assert isinstance(logging.root.handlers[0], logging_util.HomeAssistantQueueHandler) @pytest.mark.no_fail_on_log_exception