diff --git a/.coveragerc b/.coveragerc
index 2827607a95a..76d48720110 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -48,7 +48,9 @@ omit =
homeassistant/components/anel_pwrctrl/switch.py
homeassistant/components/anthemav/media_player.py
homeassistant/components/apcupsd/*
- homeassistant/components/apple_tv/*
+ homeassistant/components/apple_tv/__init__.py
+ homeassistant/components/apple_tv/media_player.py
+ homeassistant/components/apple_tv/remote.py
homeassistant/components/aqualogic/*
homeassistant/components/aquostv/media_player.py
homeassistant/components/arcam_fmj/media_player.py
@@ -65,6 +67,9 @@ omit =
homeassistant/components/asterisk_mbox/*
homeassistant/components/aten_pe/*
homeassistant/components/atome/*
+ homeassistant/components/aurora/__init__.py
+ homeassistant/components/aurora/binary_sensor.py
+ homeassistant/components/aurora/const.py
homeassistant/components/aurora_abb_powerone/sensor.py
homeassistant/components/avea/light.py
homeassistant/components/avion/light.py
@@ -259,6 +264,11 @@ omit =
homeassistant/components/fibaro/*
homeassistant/components/filesize/sensor.py
homeassistant/components/fints/sensor.py
+ homeassistant/components/fireservicerota/__init__.py
+ homeassistant/components/fireservicerota/binary_sensor.py
+ homeassistant/components/fireservicerota/const.py
+ homeassistant/components/fireservicerota/sensor.py
+ homeassistant/components/fireservicerota/switch.py
homeassistant/components/firmata/__init__.py
homeassistant/components/firmata/binary_sensor.py
homeassistant/components/firmata/board.py
@@ -533,12 +543,16 @@ omit =
homeassistant/components/minio/*
homeassistant/components/mitemp_bt/sensor.py
homeassistant/components/mjpeg/camera.py
- homeassistant/components/mobile_app/*
homeassistant/components/mochad/*
homeassistant/components/modbus/climate.py
homeassistant/components/modbus/cover.py
homeassistant/components/modbus/switch.py
+ homeassistant/components/modbus/sensor.py
homeassistant/components/modem_callerid/sensor.py
+ homeassistant/components/motion_blinds/__init__.py
+ homeassistant/components/motion_blinds/const.py
+ homeassistant/components/motion_blinds/cover.py
+ homeassistant/components/motion_blinds/sensor.py
homeassistant/components/mpchc/media_player.py
homeassistant/components/mpd/media_player.py
homeassistant/components/mqtt_room/sensor.py
@@ -560,7 +574,18 @@ omit =
homeassistant/components/neato/vacuum.py
homeassistant/components/nederlandse_spoorwegen/sensor.py
homeassistant/components/nello/lock.py
- homeassistant/components/nest/*
+ homeassistant/components/nest/__init__.py
+ homeassistant/components/nest/api.py
+ homeassistant/components/nest/binary_sensor.py
+ homeassistant/components/nest/camera.py
+ homeassistant/components/nest/camera_legacy.py
+ homeassistant/components/nest/camera_sdm.py
+ homeassistant/components/nest/climate.py
+ homeassistant/components/nest/climate_legacy.py
+ homeassistant/components/nest/climate_sdm.py
+ homeassistant/components/nest/local_auth.py
+ homeassistant/components/nest/sensor.py
+ homeassistant/components/nest/sensor_legacy.py
homeassistant/components/netatmo/__init__.py
homeassistant/components/netatmo/api.py
homeassistant/components/netatmo/camera.py
@@ -666,6 +691,7 @@ omit =
homeassistant/components/pjlink/media_player.py
homeassistant/components/plaato/*
homeassistant/components/plex/media_player.py
+ homeassistant/components/plex/models.py
homeassistant/components/plex/sensor.py
homeassistant/components/plum_lightpad/light.py
homeassistant/components/pocketcasts/sensor.py
@@ -708,6 +734,7 @@ omit =
homeassistant/components/rainforest_eagle/sensor.py
homeassistant/components/raspihats/*
homeassistant/components/raspyrfm/*
+ homeassistant/components/recollect_waste/__init__.py
homeassistant/components/recollect_waste/sensor.py
homeassistant/components/recswitch/switch.py
homeassistant/components/reddit/*
@@ -745,7 +772,6 @@ omit =
homeassistant/components/russound_rnet/media_player.py
homeassistant/components/sabnzbd/*
homeassistant/components/saj/sensor.py
- homeassistant/components/salt/device_tracker.py
homeassistant/components/satel_integra/*
homeassistant/components/schluter/*
homeassistant/components/scrape/sensor.py
@@ -821,6 +847,7 @@ omit =
homeassistant/components/spotcrime/sensor.py
homeassistant/components/spotify/__init__.py
homeassistant/components/spotify/media_player.py
+ homeassistant/components/spotify/system_health.py
homeassistant/components/squeezebox/__init__.py
homeassistant/components/squeezebox/browse_media.py
homeassistant/components/squeezebox/media_player.py
@@ -935,7 +962,6 @@ omit =
homeassistant/components/twilio_call/notify.py
homeassistant/components/twilio_sms/notify.py
homeassistant/components/twitter/notify.py
- homeassistant/components/ubee/device_tracker.py
homeassistant/components/ubus/device_tracker.py
homeassistant/components/ue_smart_radio/media_player.py
homeassistant/components/unifiled/*
@@ -1054,7 +1080,6 @@ omit =
homeassistant/components/zha/core/device.py
homeassistant/components/zha/core/gateway.py
homeassistant/components/zha/core/helpers.py
- homeassistant/components/zha/core/patches.py
homeassistant/components/zha/core/registries.py
homeassistant/components/zha/core/typing.py
homeassistant/components/zha/entity.py
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index fc07d32bfc8..e01a97425e1 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -12,17 +12,21 @@
"redhat.vscode-yaml",
"esbenp.prettier-vscode"
],
+ // Please keep this file in sync with settings in home-assistant/.vscode/settings.default.json
"settings": {
"python.pythonPath": "/usr/local/bin/python",
"python.linting.pylintEnabled": true,
"python.linting.enabled": true,
"python.formatting.provider": "black",
+ "python.testing.pytestArgs": ["--no-cov"],
+ "python.testing.pytestEnabled": true,
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"files.trimTrailingWhitespace": true,
"terminal.integrated.shell.linux": "/bin/bash",
"yaml.customTags": [
+ "!input scalar",
"!secret scalar",
"!include_dir_named scalar",
"!include_dir_list scalar",
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 9320da66dd4..56b181aa02a 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -741,7 +741,7 @@ jobs:
-p no:sugar \
tests
- name: Upload coverage artifact
- uses: actions/upload-artifact@v2.2.0
+ uses: actions/upload-artifact@v2.2.1
with:
name: coverage-${{ matrix.python-version }}-group${{ matrix.group }}
path: .coverage
@@ -785,4 +785,4 @@ jobs:
coverage report --fail-under=94
coverage xml
- name: Upload coverage to Codecov
- uses: codecov/codecov-action@v1.0.14
+ uses: codecov/codecov-action@v1.0.15
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index 1fe635ac57f..519353c81a9 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -12,7 +12,7 @@ jobs:
# The 90 day stale policy
# Used for: Everything (unless 30 day policy below beats it)
- name: 90 days stale policy
- uses: actions/stale@v3.0.13
+ uses: actions/stale@v3.0.14
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 90
@@ -48,7 +48,7 @@ jobs:
# - Issues that are pending more information (incomplete issues)
# - PRs that are not marked as new-integration
- name: 30 days stale policy
- uses: actions/stale@v3.0.13
+ uses: actions/stale@v3.0.14
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
# PRs have a CLA signed label, we can misuse it to apply this policy
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 43dcb903650..9a69f0b444c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -48,6 +48,7 @@ repos:
- id: check-executables-have-shebangs
stages: [manual]
- id: check-json
+ exclude: (.vscode|.devcontainer)
- id: no-commit-to-branch
args:
- --branch=dev
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 00000000000..6976e26ebb2
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,15 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Home Assistant",
+ "type": "python",
+ "request": "launch",
+ "module": "homeassistant",
+ "args": ["--debug", "-c", "config"]
+ }
+ ]
+}
diff --git a/.vscode/settings.default.json b/.vscode/settings.default.json
new file mode 100644
index 00000000000..85cf4e8b83a
--- /dev/null
+++ b/.vscode/settings.default.json
@@ -0,0 +1,9 @@
+{
+ // Please keep this file in sync with settings in home-assistant/.devcontainer/devcontainer.json
+ "python.formatting.provider": "black",
+ // Added --no-cov to work around TypeError: message must be set
+ // https://github.com/microsoft/vscode-python/issues/14067
+ "python.testing.pytestArgs": ["--no-cov"],
+ // https://code.visualstudio.com/docs/python/testing#_pytest-configuration-settings
+ "python.testing.pytestEnabled": true
+}
diff --git a/CODEOWNERS b/CODEOWNERS
index e0096e7f217..27614c3d49d 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -37,6 +37,7 @@ homeassistant/components/amcrest/* @pnbruckner
homeassistant/components/androidtv/* @JeffLIrion
homeassistant/components/apache_kafka/* @bachya
homeassistant/components/api/* @home-assistant/core
+homeassistant/components/apple_tv/* @postlund
homeassistant/components/apprise/* @caronc
homeassistant/components/aprs/* @PhilRW
homeassistant/components/arcam_fmj/* @elupus
@@ -48,6 +49,7 @@ homeassistant/components/atag/* @MatsNL
homeassistant/components/aten_pe/* @mtdcr
homeassistant/components/atome/* @baqs
homeassistant/components/august/* @bdraco
+homeassistant/components/aurora/* @djtimca
homeassistant/components/aurora_abb_powerone/* @davet2001
homeassistant/components/auth/* @home-assistant/core
homeassistant/components/automation/* @home-assistant/core
@@ -116,7 +118,6 @@ homeassistant/components/dunehd/* @bieniu
homeassistant/components/dwd_weather_warnings/* @runningman84 @stephan192 @Hummel95
homeassistant/components/dweet/* @fabaff
homeassistant/components/dynalite/* @ziv1234
-homeassistant/components/dyson/* @etheralm
homeassistant/components/eafm/* @Jc2k
homeassistant/components/ecobee/* @marthoc
homeassistant/components/ecovacs/* @OverloadUT
@@ -131,6 +132,7 @@ homeassistant/components/emoncms/* @borpin
homeassistant/components/emulated_kasa/* @kbickar
homeassistant/components/enigma2/* @fbradyirl
homeassistant/components/enocean/* @bdurrer
+homeassistant/components/enphase_envoy/* @gtdiehl
homeassistant/components/entur_public_transport/* @hfurubotten
homeassistant/components/environment_canada/* @michaeldavie
homeassistant/components/ephember/* @ttroy50
@@ -144,6 +146,7 @@ homeassistant/components/ezviz/* @baqs
homeassistant/components/fastdotcom/* @rohankapoorcom
homeassistant/components/file/* @fabaff
homeassistant/components/filter/* @dgomes
+homeassistant/components/fireservicerota/* @cyberjunky
homeassistant/components/firmata/* @DaAwesomeP
homeassistant/components/fixer/* @fabaff
homeassistant/components/flick_electric/* @ZephireNZ
@@ -239,6 +242,7 @@ homeassistant/components/keyboard_remote/* @bendavid
homeassistant/components/knx/* @Julius2342 @farmio @marvin-w
homeassistant/components/kodi/* @OnFreund @cgtobi
homeassistant/components/konnected/* @heythisisnate @kit-klein
+homeassistant/components/kulersky/* @emlove
homeassistant/components/lametric/* @robbiet480
homeassistant/components/launch_library/* @ludeeus
homeassistant/components/lcn/* @alengwenus
@@ -275,6 +279,7 @@ homeassistant/components/mobile_app/* @robbiet480
homeassistant/components/modbus/* @adamchengtkc @janiversen @vzahradnik
homeassistant/components/monoprice/* @etsinko @OnFreund
homeassistant/components/moon/* @fabaff
+homeassistant/components/motion_blinds/* @starkillerOG
homeassistant/components/mpd/* @fabaff
homeassistant/components/mqtt/* @home-assistant/core @emontnemery
homeassistant/components/msteams/* @peroyvind
@@ -305,6 +310,7 @@ homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte
homeassistant/components/nuheat/* @bdraco
homeassistant/components/nuki/* @pschmitt @pvizeli
homeassistant/components/numato/* @clssn
+homeassistant/components/number/* @home-assistant/core @Shulyaka
homeassistant/components/nut/* @bdraco
homeassistant/components/nws/* @MatthewFlamm
homeassistant/components/nzbget/* @chriscla
@@ -335,7 +341,7 @@ homeassistant/components/pi_hole/* @fabaff @johnluetke @shenxn
homeassistant/components/pilight/* @trekky12
homeassistant/components/plaato/* @JohNan
homeassistant/components/plex/* @jjlawren
-homeassistant/components/plugwise/* @CoMPaTech @bouwew
+homeassistant/components/plugwise/* @CoMPaTech @bouwew @brefra
homeassistant/components/plum_lightpad/* @ColinHarrington @prystupa
homeassistant/components/point/* @fredrike
homeassistant/components/poolsense/* @haemishkyd
@@ -360,6 +366,7 @@ homeassistant/components/raincloud/* @vanstinator
homeassistant/components/rainforest_eagle/* @gtdiehl @jcalbert
homeassistant/components/rainmachine/* @bachya
homeassistant/components/random/* @fabaff
+homeassistant/components/recollect_waste/* @bachya
homeassistant/components/rejseplanen/* @DarkFox
homeassistant/components/repetier/* @MTrab
homeassistant/components/rfxtrx/* @danielhiversen @elupus @RobBie1221
@@ -374,7 +381,6 @@ homeassistant/components/rpi_power/* @shenxn @swetoast
homeassistant/components/ruckus_unleashed/* @gabe565
homeassistant/components/safe_mode/* @home-assistant/core
homeassistant/components/saj/* @fredericvl
-homeassistant/components/salt/* @bjornorri
homeassistant/components/samsungtv/* @escoand
homeassistant/components/scene/* @home-assistant/core
homeassistant/components/schluter/* @prairieapps
@@ -398,6 +404,7 @@ homeassistant/components/simplisafe/* @bachya
homeassistant/components/sinch/* @bendikrb
homeassistant/components/sisyphus/* @jkeljo
homeassistant/components/sky_hub/* @rogerselwyn
+homeassistant/components/slack/* @bachya
homeassistant/components/slide/* @ualex73
homeassistant/components/sma/* @kellerza
homeassistant/components/smappee/* @bsmappee
@@ -422,6 +429,7 @@ homeassistant/components/splunk/* @Bre77
homeassistant/components/spotify/* @frenck
homeassistant/components/sql/* @dgomes
homeassistant/components/squeezebox/* @rajlaud
+homeassistant/components/srp_energy/* @briglx
homeassistant/components/starline/* @anonym-tsk
homeassistant/components/statistics/* @fabaff
homeassistant/components/stiebel_eltron/* @fucm
@@ -469,7 +477,7 @@ homeassistant/components/transmission/* @engrbm87 @JPHutchins
homeassistant/components/tts/* @pvizeli
homeassistant/components/tuya/* @ollo69
homeassistant/components/twentemilieu/* @frenck
-homeassistant/components/ubee/* @mzdrale
+homeassistant/components/twinkly/* @dr1rrb
homeassistant/components/unifi/* @Kane610
homeassistant/components/unifiled/* @florisvdk
homeassistant/components/upb/* @gwww
@@ -516,7 +524,6 @@ homeassistant/components/yamaha_musiccast/* @jalmeroth
homeassistant/components/yandex_transport/* @rishatik92 @devbis
homeassistant/components/yeelight/* @rytilahti @zewelor @shenxn
homeassistant/components/yeelightsunflower/* @lindsaymarkward
-homeassistant/components/yessssms/* @flowolf
homeassistant/components/yi/* @bachya
homeassistant/components/zeroconf/* @bdraco
homeassistant/components/zerproc/* @emlove
diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py
index d2ce63f9490..e36eb6800fa 100644
--- a/homeassistant/auth/__init__.py
+++ b/homeassistant/auth/__init__.py
@@ -284,6 +284,7 @@ class AuthManager:
self,
user: models.User,
name: Optional[str] = None,
+ is_active: Optional[bool] = None,
group_ids: Optional[List[str]] = None,
) -> None:
"""Update a user."""
@@ -294,6 +295,12 @@ class AuthManager:
kwargs["group_ids"] = group_ids
await self._store.async_update_user(user, **kwargs)
+ if is_active is not None:
+ if is_active is True:
+ await self.async_activate_user(user)
+ else:
+ await self.async_deactivate_user(user)
+
async def async_activate_user(self, user: models.User) -> None:
"""Activate a user."""
await self._store.async_activate_user(user)
diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py
index 5299ac0d301..b68fc9d17ac 100644
--- a/homeassistant/bootstrap.py
+++ b/homeassistant/bootstrap.py
@@ -15,11 +15,7 @@ import yarl
from homeassistant import config as conf_util, config_entries, core, loader
from homeassistant.components import http
-from homeassistant.const import (
- EVENT_HOMEASSISTANT_STOP,
- REQUIRED_NEXT_PYTHON_DATE,
- REQUIRED_NEXT_PYTHON_VER,
-)
+from homeassistant.const import REQUIRED_NEXT_PYTHON_DATE, REQUIRED_NEXT_PYTHON_VER
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.typing import ConfigType
from homeassistant.setup import (
@@ -142,11 +138,9 @@ async def async_setup_hass(
_LOGGER.warning("Detected that frontend did not load. Activating safe mode")
# Ask integrations to shut down. It's messy but we can't
# do a clean stop without knowing what is broken
- hass.async_track_tasks()
- hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP, {})
with contextlib.suppress(asyncio.TimeoutError):
async with hass.timeout.async_timeout(10):
- await hass.async_block_till_done()
+ await hass.async_stop()
safe_mode = True
old_config = hass.config
diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py
index 2ac52c87131..529e3ff7189 100644
--- a/homeassistant/components/abode/__init__.py
+++ b/homeassistant/components/abode/__init__.py
@@ -4,12 +4,12 @@ from copy import deepcopy
from functools import partial
from abodepy import Abode
-from abodepy.exceptions import AbodeException
+from abodepy.exceptions import AbodeAuthenticationException, AbodeException
import abodepy.helpers.timeline as TIMELINE
from requests.exceptions import ConnectTimeout, HTTPError
import voluptuous as vol
-from homeassistant.config_entries import SOURCE_IMPORT
+from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_DATE,
@@ -110,18 +110,34 @@ async def async_setup_entry(hass, config_entry):
username = config_entry.data.get(CONF_USERNAME)
password = config_entry.data.get(CONF_PASSWORD)
polling = config_entry.data.get(CONF_POLLING)
+ cache = hass.config.path(DEFAULT_CACHEDB)
+
+ # For previous config entries where unique_id is None
+ if config_entry.unique_id is None:
+ hass.config_entries.async_update_entry(
+ config_entry, unique_id=config_entry.data[CONF_USERNAME]
+ )
try:
- cache = hass.config.path(DEFAULT_CACHEDB)
abode = await hass.async_add_executor_job(
Abode, username, password, True, True, True, cache
)
- hass.data[DOMAIN] = AbodeSystem(abode, polling)
+
+ except AbodeAuthenticationException as ex:
+ LOGGER.error("Invalid credentials: %s", ex)
+ await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_REAUTH},
+ data=config_entry.data,
+ )
+ return False
except (AbodeException, ConnectTimeout, HTTPError) as ex:
- LOGGER.error("Unable to connect to Abode: %s", str(ex))
+ LOGGER.error("Unable to connect to Abode: %s", ex)
raise ConfigEntryNotReady from ex
+ hass.data[DOMAIN] = AbodeSystem(abode, polling)
+
for platform in ABODE_PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, platform)
diff --git a/homeassistant/components/abode/config_flow.py b/homeassistant/components/abode/config_flow.py
index 72e7ec1d9eb..76c23f7f705 100644
--- a/homeassistant/components/abode/config_flow.py
+++ b/homeassistant/components/abode/config_flow.py
@@ -1,15 +1,16 @@
"""Config flow for the Abode Security System component."""
from abodepy import Abode
-from abodepy.exceptions import AbodeException
+from abodepy.exceptions import AbodeAuthenticationException, AbodeException
+from abodepy.helpers.errors import MFA_CODE_REQUIRED
from requests.exceptions import ConnectTimeout, HTTPError
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, HTTP_BAD_REQUEST
-from homeassistant.core import callback
from .const import DEFAULT_CACHEDB, DOMAIN, LOGGER # pylint: disable=unused-import
+CONF_MFA = "mfa_code"
CONF_POLLING = "polling"
@@ -25,53 +26,146 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
+ self.mfa_data_schema = {
+ vol.Required(CONF_MFA): str,
+ }
+
+ self._cache = None
+ self._mfa_code = None
+ self._password = None
+ self._polling = False
+ self._username = None
+
+ async def _async_abode_login(self, step_id):
+ """Handle login with Abode."""
+ self._cache = self.hass.config.path(DEFAULT_CACHEDB)
+ errors = {}
+
+ try:
+ await self.hass.async_add_executor_job(
+ Abode, self._username, self._password, True, False, False, self._cache
+ )
+
+ except (AbodeException, ConnectTimeout, HTTPError) as ex:
+ if ex.errcode == MFA_CODE_REQUIRED[0]:
+ return await self.async_step_mfa()
+
+ LOGGER.error("Unable to connect to Abode: %s", ex)
+
+ if ex.errcode == HTTP_BAD_REQUEST:
+ errors = {"base": "invalid_auth"}
+
+ else:
+ errors = {"base": "cannot_connect"}
+
+ if errors:
+ return self.async_show_form(
+ step_id=step_id, data_schema=vol.Schema(self.data_schema), errors=errors
+ )
+
+ return await self._async_create_entry()
+
+ async def _async_abode_mfa_login(self):
+ """Handle multi-factor authentication (MFA) login with Abode."""
+ try:
+ # Create instance to access login method for passing MFA code
+ abode = Abode(
+ auto_login=False,
+ get_devices=False,
+ get_automations=False,
+ cache_path=self._cache,
+ )
+ await self.hass.async_add_executor_job(
+ abode.login, self._username, self._password, self._mfa_code
+ )
+
+ except AbodeAuthenticationException:
+ return self.async_show_form(
+ step_id="mfa",
+ data_schema=vol.Schema(self.mfa_data_schema),
+ errors={"base": "invalid_mfa_code"},
+ )
+
+ return await self._async_create_entry()
+
+ async def _async_create_entry(self):
+ """Create the config entry."""
+ config_data = {
+ CONF_USERNAME: self._username,
+ CONF_PASSWORD: self._password,
+ CONF_POLLING: self._polling,
+ }
+ existing_entry = await self.async_set_unique_id(self._username)
+
+ if existing_entry:
+ self.hass.config_entries.async_update_entry(
+ existing_entry, data=config_data
+ )
+ # Reload the Abode config entry otherwise devices will remain unavailable
+ self.hass.async_create_task(
+ self.hass.config_entries.async_reload(existing_entry.entry_id)
+ )
+
+ return self.async_abort(reason="reauth_successful")
+
+ return self.async_create_entry(title=self._username, data=config_data)
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
- if not user_input:
- return self._show_form()
-
- username = user_input[CONF_USERNAME]
- password = user_input[CONF_PASSWORD]
- polling = user_input.get(CONF_POLLING, False)
- cache = self.hass.config.path(DEFAULT_CACHEDB)
-
- try:
- await self.hass.async_add_executor_job(
- Abode, username, password, True, True, True, cache
+ if user_input is None:
+ return self.async_show_form(
+ step_id="user", data_schema=vol.Schema(self.data_schema)
)
- except (AbodeException, ConnectTimeout, HTTPError) as ex:
- LOGGER.error("Unable to connect to Abode: %s", str(ex))
- if ex.errcode == HTTP_BAD_REQUEST:
- return self._show_form({"base": "invalid_auth"})
- return self._show_form({"base": "cannot_connect"})
+ self._username = user_input[CONF_USERNAME]
+ self._password = user_input[CONF_PASSWORD]
- return self.async_create_entry(
- title=user_input[CONF_USERNAME],
- data={
- CONF_USERNAME: username,
- CONF_PASSWORD: password,
- CONF_POLLING: polling,
- },
- )
+ return await self._async_abode_login(step_id="user")
- @callback
- def _show_form(self, errors=None):
- """Show the form to the user."""
- return self.async_show_form(
- step_id="user",
- data_schema=vol.Schema(self.data_schema),
- errors=errors if errors else {},
- )
+ async def async_step_mfa(self, user_input=None):
+ """Handle a multi-factor authentication (MFA) flow."""
+ if user_input is None:
+ return self.async_show_form(
+ step_id="mfa", data_schema=vol.Schema(self.mfa_data_schema)
+ )
+
+ self._mfa_code = user_input[CONF_MFA]
+
+ return await self._async_abode_mfa_login()
+
+ async def async_step_reauth(self, config):
+ """Handle reauthorization request from Abode."""
+ self._username = config[CONF_USERNAME]
+
+ return await self.async_step_reauth_confirm()
+
+ async def async_step_reauth_confirm(self, user_input=None):
+ """Handle reauthorization flow."""
+ if user_input is None:
+ return self.async_show_form(
+ step_id="reauth_confirm",
+ data_schema=vol.Schema(
+ {
+ vol.Required(CONF_USERNAME, default=self._username): str,
+ vol.Required(CONF_PASSWORD): str,
+ }
+ ),
+ )
+
+ self._username = user_input[CONF_USERNAME]
+ self._password = user_input[CONF_PASSWORD]
+
+ return await self._async_abode_login(step_id="reauth_confirm")
async def async_step_import(self, import_config):
"""Import a config entry from configuration.yaml."""
if self._async_current_entries():
- LOGGER.warning("Only one configuration of abode is allowed.")
+ LOGGER.warning("Already configured. Only a single configuration possible.")
return self.async_abort(reason="single_instance_allowed")
+ self._polling = import_config.get(CONF_POLLING, False)
+
return await self.async_step_user(import_config)
diff --git a/homeassistant/components/abode/manifest.json b/homeassistant/components/abode/manifest.json
index e9a871035e6..b7c962dac38 100644
--- a/homeassistant/components/abode/manifest.json
+++ b/homeassistant/components/abode/manifest.json
@@ -3,7 +3,7 @@
"name": "Abode",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/abode",
- "requirements": ["abodepy==1.1.0"],
+ "requirements": ["abodepy==1.2.0"],
"codeowners": ["@shred86"],
"homekit": {
"models": ["Abode", "Iota"]
diff --git a/homeassistant/components/abode/strings.json b/homeassistant/components/abode/strings.json
index 63b62fefcec..14a60f827c3 100644
--- a/homeassistant/components/abode/strings.json
+++ b/homeassistant/components/abode/strings.json
@@ -7,14 +7,30 @@
"username": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]"
}
+ },
+ "mfa": {
+ "title": "Enter your MFA code for Abode",
+ "data": {
+ "mfa_code": "MFA code (6-digits)"
+ }
+ },
+ "reauth_confirm": {
+ "title": "Fill in your Abode login information",
+ "data": {
+ "username": "[%key:common::config_flow::data::email%]",
+ "password": "[%key:common::config_flow::data::password%]"
+ }
}
},
"error": {
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
- "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_mfa_code": "Invalid MFA code"
+
},
"abort": {
- "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
+ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/abode/translations/cs.json b/homeassistant/components/abode/translations/cs.json
index 36ff5f8b08f..30ffaa74a32 100644
--- a/homeassistant/components/abode/translations/cs.json
+++ b/homeassistant/components/abode/translations/cs.json
@@ -1,13 +1,28 @@
{
"config": {
"abort": {
+ "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9",
"single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace."
},
"error": {
"cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit",
- "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed"
+ "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed",
+ "invalid_mfa_code": "Neplatn\u00fd k\u00f3d MFA"
},
"step": {
+ "mfa": {
+ "data": {
+ "mfa_code": "K\u00f3d MFA (6 \u010d\u00edslic)"
+ },
+ "title": "Zadejte k\u00f3d MFA pro Abode"
+ },
+ "reauth_confirm": {
+ "data": {
+ "password": "Heslo",
+ "username": "E-mail"
+ },
+ "title": "Vypl\u0148te sv\u00e9 p\u0159ihla\u0161ovac\u00ed \u00fadaje do Abode"
+ },
"user": {
"data": {
"password": "Heslo",
diff --git a/homeassistant/components/abode/translations/en.json b/homeassistant/components/abode/translations/en.json
index 36f8bbb10e4..c1deaf0a00c 100644
--- a/homeassistant/components/abode/translations/en.json
+++ b/homeassistant/components/abode/translations/en.json
@@ -1,13 +1,28 @@
{
"config": {
"abort": {
+ "reauth_successful": "Re-authentication was successful",
"single_instance_allowed": "Already configured. Only a single configuration possible."
},
"error": {
"cannot_connect": "Failed to connect",
- "invalid_auth": "Invalid authentication"
+ "invalid_auth": "Invalid authentication",
+ "invalid_mfa_code": "Invalid MFA code"
},
"step": {
+ "mfa": {
+ "data": {
+ "mfa_code": "MFA code (6-digits)"
+ },
+ "title": "Enter your MFA code for Abode"
+ },
+ "reauth_confirm": {
+ "data": {
+ "password": "Password",
+ "username": "Email"
+ },
+ "title": "Fill in your Abode login information"
+ },
"user": {
"data": {
"password": "Password",
diff --git a/homeassistant/components/abode/translations/es.json b/homeassistant/components/abode/translations/es.json
index 2e5d21707b5..9fa8cd8b06b 100644
--- a/homeassistant/components/abode/translations/es.json
+++ b/homeassistant/components/abode/translations/es.json
@@ -1,13 +1,28 @@
{
"config": {
"abort": {
+ "reauth_successful": "La reautenticaci\u00f3n fue exitosa",
"single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n."
},
"error": {
"cannot_connect": "No se pudo conectar",
- "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida"
+ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida",
+ "invalid_mfa_code": "C\u00f3digo MFA inv\u00e1lido"
},
"step": {
+ "mfa": {
+ "data": {
+ "mfa_code": "C\u00f3digo MFA (6 d\u00edgitos)"
+ },
+ "title": "Introduce tu c\u00f3digo MFA para Abode"
+ },
+ "reauth_confirm": {
+ "data": {
+ "password": "Contrase\u00f1a",
+ "username": "Correo electronico"
+ },
+ "title": "Rellene su informaci\u00f3n de inicio de sesi\u00f3n de Abode"
+ },
"user": {
"data": {
"password": "Contrase\u00f1a",
diff --git a/homeassistant/components/abode/translations/et.json b/homeassistant/components/abode/translations/et.json
index 7c711e252c8..f44b4ae25c4 100644
--- a/homeassistant/components/abode/translations/et.json
+++ b/homeassistant/components/abode/translations/et.json
@@ -1,13 +1,28 @@
{
"config": {
"abort": {
+ "reauth_successful": "Taastuvastamine \u00f5nnestus",
"single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine."
},
"error": {
"cannot_connect": "\u00dchendamine nurjus",
- "invalid_auth": "Tuvastamise viga"
+ "invalid_auth": "Tuvastamise viga",
+ "invalid_mfa_code": "Kehtetu MFA-kood"
},
"step": {
+ "mfa": {
+ "data": {
+ "mfa_code": "MFA kood (6-kohaline)"
+ },
+ "title": "Sisesta oma Abode MFA kood"
+ },
+ "reauth_confirm": {
+ "data": {
+ "password": "Salas\u00f5na",
+ "username": "E-post"
+ },
+ "title": "Sisesta oma Abode sisselogimisteave"
+ },
"user": {
"data": {
"password": "Salas\u00f5na",
diff --git a/homeassistant/components/abode/translations/hu.json b/homeassistant/components/abode/translations/hu.json
index 160810c8211..77ce53abef7 100644
--- a/homeassistant/components/abode/translations/hu.json
+++ b/homeassistant/components/abode/translations/hu.json
@@ -3,6 +3,9 @@
"abort": {
"single_instance_allowed": "Csak egyetlen Abode konfigur\u00e1ci\u00f3 enged\u00e9lyezett."
},
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/abode/translations/it.json b/homeassistant/components/abode/translations/it.json
index e1af6d27fc8..a3e5aa4d7a8 100644
--- a/homeassistant/components/abode/translations/it.json
+++ b/homeassistant/components/abode/translations/it.json
@@ -1,13 +1,28 @@
{
"config": {
"abort": {
+ "reauth_successful": "La riautenticazione ha avuto successo",
"single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
},
"error": {
"cannot_connect": "Impossibile connettersi",
- "invalid_auth": "Autenticazione non valida"
+ "invalid_auth": "Autenticazione non valida",
+ "invalid_mfa_code": "Codice MFA non valido"
},
"step": {
+ "mfa": {
+ "data": {
+ "mfa_code": "Codice MFA (6 cifre)"
+ },
+ "title": "Inserisci il tuo codice MFA per Abode"
+ },
+ "reauth_confirm": {
+ "data": {
+ "password": "Password",
+ "username": "E-mail"
+ },
+ "title": "Inserisci le tue informazioni di accesso Abode"
+ },
"user": {
"data": {
"password": "Password",
diff --git a/homeassistant/components/abode/translations/lb.json b/homeassistant/components/abode/translations/lb.json
index 8a6d671b7f0..2058d3353c9 100644
--- a/homeassistant/components/abode/translations/lb.json
+++ b/homeassistant/components/abode/translations/lb.json
@@ -1,13 +1,28 @@
{
"config": {
"abort": {
+ "reauth_successful": "Re-authentifikatioun war erfollegr\u00e4ich",
"single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech."
},
"error": {
"cannot_connect": "Feeler beim verbannen",
- "invalid_auth": "Ong\u00eblteg Authentifikatioun"
+ "invalid_auth": "Ong\u00eblteg Authentifikatioun",
+ "invalid_mfa_code": "Ong\u00ebltege MFA Code"
},
"step": {
+ "mfa": {
+ "data": {
+ "mfa_code": "MFA code (6 Zifferen)"
+ },
+ "title": "G\u00ebff dain MFA code fir Abode un"
+ },
+ "reauth_confirm": {
+ "data": {
+ "password": "Passwuert",
+ "username": "E-Mail"
+ },
+ "title": "F\u00ebll deng Abode Login Informatiounen aus"
+ },
"user": {
"data": {
"password": "Passwuert",
diff --git a/homeassistant/components/abode/translations/no.json b/homeassistant/components/abode/translations/no.json
index cda0a6aa221..c215ec7dae9 100644
--- a/homeassistant/components/abode/translations/no.json
+++ b/homeassistant/components/abode/translations/no.json
@@ -1,13 +1,28 @@
{
"config": {
"abort": {
+ "reauth_successful": "Reautentisering var vellykket",
"single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
},
"error": {
"cannot_connect": "Tilkobling mislyktes",
- "invalid_auth": "Ugyldig godkjenning"
+ "invalid_auth": "Ugyldig godkjenning",
+ "invalid_mfa_code": "Ugyldig MFA-kode"
},
"step": {
+ "mfa": {
+ "data": {
+ "mfa_code": "MFA-kode (6-sifre)"
+ },
+ "title": "Skriv inn din MFA-kode for Abode"
+ },
+ "reauth_confirm": {
+ "data": {
+ "password": "Passord",
+ "username": "E-post"
+ },
+ "title": "Fyll ut innloggingsinformasjonen for Abode"
+ },
"user": {
"data": {
"password": "Passord",
diff --git a/homeassistant/components/abode/translations/pl.json b/homeassistant/components/abode/translations/pl.json
index 8331a58090f..79966f14d9c 100644
--- a/homeassistant/components/abode/translations/pl.json
+++ b/homeassistant/components/abode/translations/pl.json
@@ -1,13 +1,28 @@
{
"config": {
"abort": {
+ "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119",
"single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja."
},
"error": {
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
- "invalid_auth": "Niepoprawne uwierzytelnienie"
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "invalid_mfa_code": "Nieprawid\u0142owy kod uwierzytelniania wielosk\u0142adnikowego"
},
"step": {
+ "mfa": {
+ "data": {
+ "mfa_code": "6-cyfrowy kod uwierzytelniania wielosk\u0142adnikowego"
+ },
+ "title": "Wprowad\u017a kod uwierzytelniania wielosk\u0142adnikowego dla Abode"
+ },
+ "reauth_confirm": {
+ "data": {
+ "password": "Has\u0142o",
+ "username": "Adres e-mail"
+ },
+ "title": "Wprowad\u017a informacje logowania Abode"
+ },
"user": {
"data": {
"password": "Has\u0142o",
diff --git a/homeassistant/components/abode/translations/ru.json b/homeassistant/components/abode/translations/ru.json
index 2a92a44e1b7..04efaa6e519 100644
--- a/homeassistant/components/abode/translations/ru.json
+++ b/homeassistant/components/abode/translations/ru.json
@@ -1,13 +1,28 @@
{
"config": {
"abort": {
+ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e.",
"single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e."
},
"error": {
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
- "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f."
+ "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "invalid_mfa_code": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043a\u043e\u0434 MFA."
},
"step": {
+ "mfa": {
+ "data": {
+ "mfa_code": "\u041a\u043e\u0434 MFA (6 \u0446\u0438\u0444\u0440)"
+ },
+ "title": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 MFA \u0434\u043b\u044f Abode"
+ },
+ "reauth_confirm": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b"
+ },
+ "title": "Abode"
+ },
"user": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
diff --git a/homeassistant/components/abode/translations/zh-Hant.json b/homeassistant/components/abode/translations/zh-Hant.json
index b23eb9d3707..d3e1db007f5 100644
--- a/homeassistant/components/abode/translations/zh-Hant.json
+++ b/homeassistant/components/abode/translations/zh-Hant.json
@@ -1,13 +1,28 @@
{
"config": {
"abort": {
+ "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f",
"single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002"
},
"error": {
"cannot_connect": "\u9023\u7dda\u5931\u6557",
- "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548"
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
+ "invalid_mfa_code": "\u591a\u6b65\u9a5f\u8a8d\u8b49\u78bc\u7121\u6548"
},
"step": {
+ "mfa": {
+ "data": {
+ "mfa_code": "\u591a\u6b65\u9a5f\u8a8d\u8b49\u78bc\uff086 \u4f4d\uff09"
+ },
+ "title": "\u8f38\u5165 Abode \u591a\u6b65\u9a5f\u8a8d\u8b49\u78bc"
+ },
+ "reauth_confirm": {
+ "data": {
+ "password": "\u5bc6\u78bc",
+ "username": "\u96fb\u5b50\u90f5\u4ef6"
+ },
+ "title": "\u586b\u5beb Abode \u767b\u5165\u8cc7\u8a0a"
+ },
"user": {
"data": {
"password": "\u5bc6\u78bc",
diff --git a/homeassistant/components/accuweather/const.py b/homeassistant/components/accuweather/const.py
index aac37604584..fa9ed6b467f 100644
--- a/homeassistant/components/accuweather/const.py
+++ b/homeassistant/components/accuweather/const.py
@@ -1,4 +1,20 @@
"""Constants for AccuWeather integration."""
+from homeassistant.components.weather import (
+ ATTR_CONDITION_CLEAR_NIGHT,
+ ATTR_CONDITION_CLOUDY,
+ ATTR_CONDITION_EXCEPTIONAL,
+ ATTR_CONDITION_FOG,
+ ATTR_CONDITION_HAIL,
+ ATTR_CONDITION_LIGHTNING,
+ ATTR_CONDITION_LIGHTNING_RAINY,
+ ATTR_CONDITION_PARTLYCLOUDY,
+ ATTR_CONDITION_POURING,
+ ATTR_CONDITION_RAINY,
+ ATTR_CONDITION_SNOWY,
+ ATTR_CONDITION_SNOWY_RAINY,
+ ATTR_CONDITION_SUNNY,
+ ATTR_CONDITION_WINDY,
+)
from homeassistant.const import (
ATTR_DEVICE_CLASS,
CONCENTRATION_PARTS_PER_CUBIC_METER,
@@ -29,20 +45,20 @@ NAME = "AccuWeather"
UNDO_UPDATE_LISTENER = "undo_update_listener"
CONDITION_CLASSES = {
- "clear-night": [33, 34, 37],
- "cloudy": [7, 8, 38],
- "exceptional": [24, 30, 31],
- "fog": [11],
- "hail": [25],
- "lightning": [15],
- "lightning-rainy": [16, 17, 41, 42],
- "partlycloudy": [4, 6, 35, 36],
- "pouring": [18],
- "rainy": [12, 13, 14, 26, 39, 40],
- "snowy": [19, 20, 21, 22, 23, 43, 44],
- "snowy-rainy": [29],
- "sunny": [1, 2, 3, 5],
- "windy": [32],
+ ATTR_CONDITION_CLEAR_NIGHT: [33, 34, 37],
+ ATTR_CONDITION_CLOUDY: [7, 8, 38],
+ ATTR_CONDITION_EXCEPTIONAL: [24, 30, 31],
+ ATTR_CONDITION_FOG: [11],
+ ATTR_CONDITION_HAIL: [25],
+ ATTR_CONDITION_LIGHTNING: [15],
+ ATTR_CONDITION_LIGHTNING_RAINY: [16, 17, 41, 42],
+ ATTR_CONDITION_PARTLYCLOUDY: [4, 6, 35, 36],
+ ATTR_CONDITION_POURING: [18],
+ ATTR_CONDITION_RAINY: [12, 13, 14, 26, 39, 40],
+ ATTR_CONDITION_SNOWY: [19, 20, 21, 22, 23, 43, 44],
+ ATTR_CONDITION_SNOWY_RAINY: [29],
+ ATTR_CONDITION_SUNNY: [1, 2, 3, 5],
+ ATTR_CONDITION_WINDY: [32],
}
FORECAST_DAYS = [0, 1, 2, 3, 4]
diff --git a/homeassistant/components/accuweather/strings.json b/homeassistant/components/accuweather/strings.json
index 7bf61de8476..65aa2a9ed91 100644
--- a/homeassistant/components/accuweather/strings.json
+++ b/homeassistant/components/accuweather/strings.json
@@ -31,5 +31,11 @@
}
}
}
+ },
+ "system_health": {
+ "info": {
+ "can_reach_server": "Reach AccuWeather server",
+ "remaining_requests": "Remaining allowed requests"
+ }
}
}
diff --git a/homeassistant/components/accuweather/system_health.py b/homeassistant/components/accuweather/system_health.py
new file mode 100644
index 00000000000..58c9ba35881
--- /dev/null
+++ b/homeassistant/components/accuweather/system_health.py
@@ -0,0 +1,27 @@
+"""Provide info to system health."""
+from accuweather.const import ENDPOINT
+
+from homeassistant.components import system_health
+from homeassistant.core import HomeAssistant, callback
+
+from .const import COORDINATOR, DOMAIN
+
+
+@callback
+def async_register(
+ hass: HomeAssistant, register: system_health.SystemHealthRegistration
+) -> None:
+ """Register system health callbacks."""
+ register.async_register_info(system_health_info)
+
+
+async def system_health_info(hass):
+ """Get info for the info page."""
+ remaining_requests = list(hass.data[DOMAIN].values())[0][
+ COORDINATOR
+ ].accuweather.requests_remaining
+
+ return {
+ "can_reach_server": system_health.async_check_can_reach_url(hass, ENDPOINT),
+ "remaining_requests": remaining_requests,
+ }
diff --git a/homeassistant/components/accuweather/translations/en.json b/homeassistant/components/accuweather/translations/en.json
index b9986eda30f..b737c420a2d 100644
--- a/homeassistant/components/accuweather/translations/en.json
+++ b/homeassistant/components/accuweather/translations/en.json
@@ -31,5 +31,11 @@
"title": "AccuWeather Options"
}
}
+ },
+ "system_health": {
+ "info": {
+ "can_reach_server": "Reach AccuWeather server",
+ "remaining_requests": "Remaining allowed requests"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/accuweather/translations/es-419.json b/homeassistant/components/accuweather/translations/es-419.json
new file mode 100644
index 00000000000..5af58867ebf
--- /dev/null
+++ b/homeassistant/components/accuweather/translations/es-419.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "error": {
+ "requests_exceeded": "Se super\u00f3 el n\u00famero permitido de solicitudes a la API de Accuweather. Tiene que esperar o cambiar la clave de API."
+ },
+ "step": {
+ "user": {
+ "description": "Si necesita ayuda con la configuraci\u00f3n, eche un vistazo aqu\u00ed: https://www.home-assistant.io/integrations/accuweather/ \n\nAlgunos sensores no est\u00e1n habilitados de forma predeterminada. Puede habilitarlos en el registro de entidades despu\u00e9s de la configuraci\u00f3n de integraci\u00f3n. La previsi\u00f3n meteorol\u00f3gica no est\u00e1 habilitada de forma predeterminada. Puede habilitarlo en las opciones de integraci\u00f3n.",
+ "title": "AccuWeather"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "user": {
+ "data": {
+ "forecast": "Pron\u00f3stico del tiempo"
+ },
+ "description": "Debido a las limitaciones de la versi\u00f3n gratuita de la clave API de AccuWeather, cuando habilita el pron\u00f3stico del tiempo, las actualizaciones de datos se realizar\u00e1n cada 64 minutos en lugar de cada 32 minutos."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/accuweather/translations/et.json b/homeassistant/components/accuweather/translations/et.json
index 1b0bdbe643a..ebbceb69b0a 100644
--- a/homeassistant/components/accuweather/translations/et.json
+++ b/homeassistant/components/accuweather/translations/et.json
@@ -31,5 +31,11 @@
"title": "AccuWeatheri valikud"
}
}
+ },
+ "system_health": {
+ "info": {
+ "can_reach_server": "\u00dchendu Accuweatheri serveriga",
+ "remaining_requests": "Lubatud taotlusi on j\u00e4\u00e4nud"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/acmeda/manifest.json b/homeassistant/components/acmeda/manifest.json
index 8b76af0c57e..f1858f9fd5a 100644
--- a/homeassistant/components/acmeda/manifest.json
+++ b/homeassistant/components/acmeda/manifest.json
@@ -3,7 +3,7 @@
"name": "Rollease Acmeda Automate",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/acmeda",
- "requirements": ["aiopulse==0.4.0"],
+ "requirements": ["aiopulse==0.4.2"],
"codeowners": [
"@atmurray"
]
diff --git a/homeassistant/components/acmeda/translations/no.json b/homeassistant/components/acmeda/translations/no.json
index 51eb5668bc9..45d764e8112 100644
--- a/homeassistant/components/acmeda/translations/no.json
+++ b/homeassistant/components/acmeda/translations/no.json
@@ -6,7 +6,7 @@
"step": {
"user": {
"data": {
- "id": "Verts-ID"
+ "id": "Vert ID"
},
"title": "Velg en hub du vil legge til"
}
diff --git a/homeassistant/components/adguard/translations/hu.json b/homeassistant/components/adguard/translations/hu.json
index 1ca56c7684f..3f67c765850 100644
--- a/homeassistant/components/adguard/translations/hu.json
+++ b/homeassistant/components/adguard/translations/hu.json
@@ -1,5 +1,8 @@
{
"config": {
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/adguard/translations/pl.json b/homeassistant/components/adguard/translations/pl.json
index 2f8a134c04d..41cb2c019dd 100644
--- a/homeassistant/components/adguard/translations/pl.json
+++ b/homeassistant/components/adguard/translations/pl.json
@@ -9,7 +9,7 @@
},
"step": {
"hassio_confirm": {
- "description": "Czy chcesz skonfigurowa\u0107 Home Assistant, aby po\u0142\u0105czy\u0142 si\u0119 z AdGuard Home przez dodatek Hass.io {addon}?",
+ "description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby po\u0142\u0105czy\u0142 si\u0119 z AdGuard Home przez dodatek Hass.io {addon}?",
"title": "AdGuard Home przez dodatek Hass.io"
},
"user": {
diff --git a/homeassistant/components/advantage_air/translations/es-419.json b/homeassistant/components/advantage_air/translations/es-419.json
new file mode 100644
index 00000000000..f2f9a463527
--- /dev/null
+++ b/homeassistant/components/advantage_air/translations/es-419.json
@@ -0,0 +1,9 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "title": "Conectar"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/advantage_air/translations/hu.json b/homeassistant/components/advantage_air/translations/hu.json
new file mode 100644
index 00000000000..0da6d0d5304
--- /dev/null
+++ b/homeassistant/components/advantage_air/translations/hu.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "ip_address": "IP c\u00edm",
+ "port": "Port"
+ },
+ "description": "Csatlakozzon az Advantage Air fali t\u00e1blag\u00e9p API-j\u00e1hoz.",
+ "title": "Csatlakoz\u00e1s"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/advantage_air/translations/ka.json b/homeassistant/components/advantage_air/translations/ka.json
new file mode 100644
index 00000000000..4216ece47d2
--- /dev/null
+++ b/homeassistant/components/advantage_air/translations/ka.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u10db\u10d0\u10e0\u10ea\u10ee\u10d8 \u10e8\u10d4\u10d4\u10e0\u10d7\u10d4\u10d1\u10d8\u10e1\u10d0\u10e1"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "ip_address": "IP \u10db\u10d8\u10e1\u10d0\u10db\u10d0\u10e0\u10d7\u10d8",
+ "port": "\u10de\u10dd\u10e0\u10e2\u10d8"
+ },
+ "description": "\u10d3\u10d0\u10e3\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d3\u10d8\u10d7 \u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 Advantage Air API-\u10e1 \u10d9\u10d4\u10d3\u10d4\u10da\u10d6\u10d4 \u10d3\u10d0\u10db\u10dd\u10dc\u10e2\u10d0\u10df\u10d4\u10d1\u10e3\u10da\u10d8 \u10e2\u10d0\u10d1\u10da\u10d4\u10e2\u10d8\u10d7",
+ "title": "\u10d3\u10d0\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d4\u10d1\u10d0"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/advantage_air/translations/sl.json b/homeassistant/components/advantage_air/translations/sl.json
new file mode 100644
index 00000000000..3e080b3db31
--- /dev/null
+++ b/homeassistant/components/advantage_air/translations/sl.json
@@ -0,0 +1,9 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "title": "Pove\u017eite se"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/agent_dvr/translations/hu.json b/homeassistant/components/agent_dvr/translations/hu.json
index 45918735010..1d28556ba1a 100644
--- a/homeassistant/components/agent_dvr/translations/hu.json
+++ b/homeassistant/components/agent_dvr/translations/hu.json
@@ -1,5 +1,8 @@
{
"config": {
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/agent_dvr/translations/ka.json b/homeassistant/components/agent_dvr/translations/ka.json
new file mode 100644
index 00000000000..fa4c9c0abd3
--- /dev/null
+++ b/homeassistant/components/agent_dvr/translations/ka.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u10db\u10d0\u10e0\u10ea\u10ee\u10d8 \u10e8\u10d4\u10d4\u10e0\u10d7\u10d4\u10d1\u10d8\u10e1\u10d0\u10e1"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/airly/strings.json b/homeassistant/components/airly/strings.json
index 3453fb7b38a..afda73ae887 100644
--- a/homeassistant/components/airly/strings.json
+++ b/homeassistant/components/airly/strings.json
@@ -19,5 +19,10 @@
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_location%]"
}
+ },
+ "system_health": {
+ "info": {
+ "can_reach_server": "Reach Airly server"
+ }
}
}
diff --git a/homeassistant/components/airly/system_health.py b/homeassistant/components/airly/system_health.py
new file mode 100644
index 00000000000..6b683518ebd
--- /dev/null
+++ b/homeassistant/components/airly/system_health.py
@@ -0,0 +1,22 @@
+"""Provide info to system health."""
+from airly import Airly
+
+from homeassistant.components import system_health
+from homeassistant.core import HomeAssistant, callback
+
+
+@callback
+def async_register(
+ hass: HomeAssistant, register: system_health.SystemHealthRegistration
+) -> None:
+ """Register system health callbacks."""
+ register.async_register_info(system_health_info)
+
+
+async def system_health_info(hass):
+ """Get info for the info page."""
+ return {
+ "can_reach_server": system_health.async_check_can_reach_url(
+ hass, Airly.AIRLY_API_URL
+ )
+ }
diff --git a/homeassistant/components/airly/translations/en.json b/homeassistant/components/airly/translations/en.json
index 4aa35c3cada..720f68f8349 100644
--- a/homeassistant/components/airly/translations/en.json
+++ b/homeassistant/components/airly/translations/en.json
@@ -19,5 +19,10 @@
"title": "Airly"
}
}
+ },
+ "system_health": {
+ "info": {
+ "can_reach_server": "Reach Airly server"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/airly/translations/et.json b/homeassistant/components/airly/translations/et.json
index 270c97004f8..0d46a0f7643 100644
--- a/homeassistant/components/airly/translations/et.json
+++ b/homeassistant/components/airly/translations/et.json
@@ -19,5 +19,10 @@
"title": ""
}
}
+ },
+ "system_health": {
+ "info": {
+ "can_reach_server": "\u00dchendu Airly serveriga"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py
index 17a96629d60..956b168a665 100644
--- a/homeassistant/components/airvisual/__init__.py
+++ b/homeassistant/components/airvisual/__init__.py
@@ -49,7 +49,7 @@ DATA_LISTENER = "listener"
DEFAULT_ATTRIBUTION = "Data provided by AirVisual"
DEFAULT_NODE_PRO_UPDATE_INTERVAL = timedelta(minutes=1)
-CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.119")
+CONFIG_SCHEMA = cv.deprecated(DOMAIN)
@callback
diff --git a/homeassistant/components/airvisual/translations/hu.json b/homeassistant/components/airvisual/translations/hu.json
index 655060337e1..53ab734e505 100644
--- a/homeassistant/components/airvisual/translations/hu.json
+++ b/homeassistant/components/airvisual/translations/hu.json
@@ -1,6 +1,7 @@
{
"config": {
"error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
"general_error": "Ismeretlen hiba t\u00f6rt\u00e9nt."
},
"step": {
@@ -15,6 +16,11 @@
"data": {
"password": "Jelsz\u00f3"
}
+ },
+ "reauth_confirm": {
+ "data": {
+ "api_key": "API kulcs"
+ }
}
}
}
diff --git a/homeassistant/components/airvisual/translations/ka.json b/homeassistant/components/airvisual/translations/ka.json
new file mode 100644
index 00000000000..cb01b5d0d14
--- /dev/null
+++ b/homeassistant/components/airvisual/translations/ka.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "reauth_successful": "\u10ec\u10d0\u10e0\u10db\u10d0\u10e2\u10d4\u10d1\u10e3\u10da\u10d8 \u10e0\u10d4-\u10d0\u10d5\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0"
+ },
+ "step": {
+ "reauth_confirm": {
+ "data": {
+ "api_key": "API Key"
+ },
+ "title": "AirVisual \u10e0\u10d4-\u10d0\u10d5\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarm_control_panel/translations/et.json b/homeassistant/components/alarm_control_panel/translations/et.json
index 76b0c845d01..cc4bb6f1ea3 100644
--- a/homeassistant/components/alarm_control_panel/translations/et.json
+++ b/homeassistant/components/alarm_control_panel/translations/et.json
@@ -15,9 +15,9 @@
"is_triggered": "{entity_name} on h\u00e4iret andnud"
},
"trigger_type": {
- "armed_away": "{entity_name} valvestatus",
- "armed_home": "{entity_name} valvestatus kodure\u017eiimis",
- "armed_night": "{entity_name} valvestatus \u00f6\u00f6re\u017eiimis",
+ "armed_away": "{entity_name} valvestati",
+ "armed_home": "{entity_name} valvestati kodure\u017eiimis",
+ "armed_night": "{entity_name} valvestati \u00f6\u00f6re\u017eiimis",
"disarmed": "{entity_name} v\u00f5eti valvest maha",
"triggered": "{entity_name} andis h\u00e4iret"
}
diff --git a/homeassistant/components/alarmdecoder/translations/es-419.json b/homeassistant/components/alarmdecoder/translations/es-419.json
new file mode 100644
index 00000000000..39344beb289
--- /dev/null
+++ b/homeassistant/components/alarmdecoder/translations/es-419.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "create_entry": {
+ "default": "Conectado con \u00e9xito a AlarmDecoder."
+ },
+ "step": {
+ "protocol": {
+ "data": {
+ "device_baudrate": "Tasa de baudios del dispositivo",
+ "device_path": "Ruta del dispositivo"
+ },
+ "title": "Configurar los ajustes de conexi\u00f3n"
+ },
+ "user": {
+ "data": {
+ "protocol": "Protocolo"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "arm_settings": {
+ "data": {
+ "alt_night_mode": "Modo nocturno alternativo"
+ }
+ },
+ "init": {
+ "data": {
+ "edit_select": "Editar"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarmdecoder/translations/hu.json b/homeassistant/components/alarmdecoder/translations/hu.json
new file mode 100644
index 00000000000..2d5f91cf373
--- /dev/null
+++ b/homeassistant/components/alarmdecoder/translations/hu.json
@@ -0,0 +1,10 @@
+{
+ "config": {
+ "create_entry": {
+ "default": "Sikeres csatlakoz\u00e1s az AlarmDecoderhez."
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alarmdecoder/translations/sl.json b/homeassistant/components/alarmdecoder/translations/sl.json
new file mode 100644
index 00000000000..73dfc60865e
--- /dev/null
+++ b/homeassistant/components/alarmdecoder/translations/sl.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Naprava je \u017ee nastavljena"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py
index 1163d0de101..008870c8dd9 100644
--- a/homeassistant/components/alexa/capabilities.py
+++ b/homeassistant/components/alexa/capabilities.py
@@ -412,10 +412,17 @@ class AlexaLockController(AlexaCapability):
"en-AU",
"en-CA",
"en-GB",
+ "en-IN",
"en-US",
"es-ES",
+ "es-MX",
+ "es-US",
+ "fr-CA",
+ "fr-FR",
+ "hi-IN",
"it-IT",
"ja-JP",
+ "pt-BR",
}
def name(self):
@@ -454,6 +461,7 @@ class AlexaSceneController(AlexaCapability):
supported_locales = {
"de-DE",
+ "en-AU",
"en-CA",
"en-GB",
"en-IN",
@@ -461,6 +469,7 @@ class AlexaSceneController(AlexaCapability):
"es-ES",
"fr-FR",
"it-IT",
+ "ja-JP",
}
def __init__(self, entity, supports_deactivation):
@@ -488,8 +497,10 @@ class AlexaBrightnessController(AlexaCapability):
"en-US",
"es-ES",
"fr-FR",
+ "hi-IN",
"it-IT",
"ja-JP",
+ "pt-BR",
}
def name(self):
@@ -532,8 +543,10 @@ class AlexaColorController(AlexaCapability):
"en-US",
"es-ES",
"fr-FR",
+ "hi-IN",
"it-IT",
"ja-JP",
+ "pt-BR",
}
def name(self):
@@ -581,8 +594,10 @@ class AlexaColorTemperatureController(AlexaCapability):
"en-US",
"es-ES",
"fr-FR",
+ "hi-IN",
"it-IT",
"ja-JP",
+ "pt-BR",
}
def name(self):
@@ -669,7 +684,18 @@ class AlexaSpeaker(AlexaCapability):
https://developer.amazon.com/docs/device-apis/alexa-speaker.html
"""
- supported_locales = {"de-DE", "en-AU", "en-CA", "en-GB", "en-IN", "en-US"}
+ supported_locales = {
+ "de-DE",
+ "en-AU",
+ "en-CA",
+ "en-GB",
+ "en-IN",
+ "en-US",
+ "es-ES",
+ "es-MX",
+ "it-IT",
+ "ja-JP",
+ }
def name(self):
"""Return the Alexa API name of this interface."""
@@ -716,7 +742,16 @@ class AlexaStepSpeaker(AlexaCapability):
https://developer.amazon.com/docs/device-apis/alexa-stepspeaker.html
"""
- supported_locales = {"de-DE", "en-AU", "en-CA", "en-GB", "en-IN", "en-US"}
+ supported_locales = {
+ "de-DE",
+ "en-AU",
+ "en-CA",
+ "en-GB",
+ "en-IN",
+ "en-US",
+ "es-ES",
+ "it-IT",
+ }
def name(self):
"""Return the Alexa API name of this interface."""
@@ -866,7 +901,16 @@ class AlexaContactSensor(AlexaCapability):
https://developer.amazon.com/docs/device-apis/alexa-contactsensor.html
"""
- supported_locales = {"en-CA", "en-US", "it-IT"}
+ supported_locales = {
+ "de-DE",
+ "en-AU",
+ "en-CA",
+ "en-IN",
+ "en-US",
+ "es-ES",
+ "it-IT",
+ "ja-JP",
+ }
def __init__(self, hass, entity):
"""Initialize the entity."""
@@ -905,7 +949,17 @@ class AlexaMotionSensor(AlexaCapability):
https://developer.amazon.com/docs/device-apis/alexa-motionsensor.html
"""
- supported_locales = {"en-CA", "en-US", "it-IT"}
+ supported_locales = {
+ "de-DE",
+ "en-AU",
+ "en-CA",
+ "en-IN",
+ "en-US",
+ "es-ES",
+ "it-IT",
+ "ja-JP",
+ "pt-BR",
+ }
def __init__(self, hass, entity):
"""Initialize the entity."""
@@ -955,6 +1009,7 @@ class AlexaThermostatController(AlexaCapability):
"fr-FR",
"it-IT",
"ja-JP",
+ "pt-BR",
}
def __init__(self, hass, entity):
@@ -1127,7 +1182,7 @@ class AlexaSecurityPanelController(AlexaCapability):
"fr-FR",
"it-IT",
"ja-JP",
- "pt_BR",
+ "pt-BR",
}
def __init__(self, hass, entity):
@@ -1509,7 +1564,7 @@ class AlexaRangeController(AlexaCapability):
min_value = float(self.entity.attributes[input_number.ATTR_MIN])
max_value = float(self.entity.attributes[input_number.ATTR_MAX])
precision = float(self.entity.attributes.get(input_number.ATTR_STEP, 1))
- unit = self.entity.attributes.get(input_number.ATTR_UNIT_OF_MEASUREMENT)
+ unit = self.entity.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
self._resource = AlexaPresetResource(
["Value", AlexaGlobalCatalog.SETTING_PRESET],
@@ -1623,6 +1678,7 @@ class AlexaToggleController(AlexaCapability):
"fr-FR",
"it-IT",
"ja-JP",
+ "pt-BR",
}
def __init__(self, entity, instance, non_controllable=False):
@@ -1679,7 +1735,21 @@ class AlexaChannelController(AlexaCapability):
https://developer.amazon.com/docs/device-apis/alexa-channelcontroller.html
"""
- supported_locales = {"de-DE", "en-AU", "en-CA", "en-GB", "en-IN", "en-US"}
+ supported_locales = {
+ "de-DE",
+ "en-AU",
+ "en-CA",
+ "en-GB",
+ "en-IN",
+ "en-US",
+ "es-ES",
+ "es-MX",
+ "fr-FR",
+ "hi-IN",
+ "it-IT",
+ "ja-JP",
+ "pt-BR",
+ }
def name(self):
"""Return the Alexa API name of this interface."""
@@ -1693,7 +1763,6 @@ class AlexaDoorbellEventSource(AlexaCapability):
"""
supported_locales = {
- "en-US",
"de-DE",
"en-AU",
"en-CA",
@@ -1702,8 +1771,10 @@ class AlexaDoorbellEventSource(AlexaCapability):
"en-US",
"es-ES",
"es-MX",
+ "es-US",
"fr-CA",
"fr-FR",
+ "hi-IN",
"it-IT",
"ja-JP",
}
@@ -1723,7 +1794,7 @@ class AlexaPlaybackStateReporter(AlexaCapability):
https://developer.amazon.com/docs/device-apis/alexa-playbackstatereporter.html
"""
- supported_locales = {"de-DE", "en-GB", "en-US", "fr-FR"}
+ supported_locales = {"de-DE", "en-GB", "en-US", "es-MX", "fr-FR"}
def name(self):
"""Return the Alexa API name of this interface."""
@@ -1761,7 +1832,7 @@ class AlexaSeekController(AlexaCapability):
https://developer.amazon.com/docs/device-apis/alexa-seekcontroller.html
"""
- supported_locales = {"de-DE", "en-GB", "en-US"}
+ supported_locales = {"de-DE", "en-GB", "en-US", "es-MX"}
def name(self):
"""Return the Alexa API name of this interface."""
@@ -1833,7 +1904,7 @@ class AlexaEqualizerController(AlexaCapability):
https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-equalizercontroller.html
"""
- supported_locales = {"en-US"}
+ supported_locales = {"de-DE", "en-IN", "en-US", "es-ES", "it-IT", "ja-JP", "pt-BR"}
VALID_SOUND_MODES = {
"MOVIE",
"MUSIC",
@@ -1929,8 +2000,10 @@ class AlexaCameraStreamController(AlexaCapability):
"en-US",
"es-ES",
"fr-FR",
+ "hi-IN",
"it-IT",
"ja-JP",
+ "pt-BR",
}
def name(self):
diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py
index 65c48ba0206..8837210b6ad 100644
--- a/homeassistant/components/alexa/handlers.py
+++ b/homeassistant/components/alexa/handlers.py
@@ -995,14 +995,14 @@ async def async_api_set_mode(hass, config, directive, context):
# Fan Direction
if instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}":
- _, direction = mode.split(".")
+ direction = mode.split(".")[1]
if direction in (fan.DIRECTION_REVERSE, fan.DIRECTION_FORWARD):
service = fan.SERVICE_SET_DIRECTION
data[fan.ATTR_DIRECTION] = direction
# Cover Position
elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
- _, position = mode.split(".")
+ position = mode.split(".")[1]
if position == cover.STATE_CLOSED:
service = cover.SERVICE_CLOSE_COVER
diff --git a/homeassistant/components/ambiclimate/translations/hu.json b/homeassistant/components/ambiclimate/translations/hu.json
new file mode 100644
index 00000000000..19f706be1c8
--- /dev/null
+++ b/homeassistant/components/ambiclimate/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "create_entry": {
+ "default": "Sikeres autentik\u00e1ci\u00f3"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambiclimate/translations/ka.json b/homeassistant/components/ambiclimate/translations/ka.json
new file mode 100644
index 00000000000..ed77d38ab45
--- /dev/null
+++ b/homeassistant/components/ambiclimate/translations/ka.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u10d0\u10dc\u10d2\u10d0\u10e0\u10d8\u10e8\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0",
+ "missing_configuration": "\u10d4\u10e1 \u10d9\u10dd\u10db\u10de\u10dd\u10dc\u10d4\u10dc\u10e2\u10d8 \u10d0\u10e0 \u10d0\u10e0\u10d8\u10e1 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8. \u10d2\u10d7\u10ee\u10dd\u10d5\u10d7 \u10db\u10d8\u10e7\u10d5\u10d4\u10d7 \u10d3\u10dd\u10d9\u10e3\u10db\u10d4\u10dc\u10e2\u10d0\u10ea\u10d8\u10d0\u10e1"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py
index 9fabed7c30a..4a5558c5963 100644
--- a/homeassistant/components/ambient_station/__init__.py
+++ b/homeassistant/components/ambient_station/__init__.py
@@ -1,6 +1,5 @@
"""Support for Ambient Weather Station Service."""
import asyncio
-import logging
from aioambient import Client
from aioambient.errors import WebsocketError
@@ -39,12 +38,11 @@ from .const import (
CONF_APP_KEY,
DATA_CLIENT,
DOMAIN,
+ LOGGER,
TYPE_BINARY_SENSOR,
TYPE_SENSOR,
)
-_LOGGER = logging.getLogger(__name__)
-
DATA_CONFIG = "config"
DEFAULT_SOCKET_MIN_RETRY = 15
@@ -307,7 +305,7 @@ async def async_setup_entry(hass, config_entry):
hass.loop.create_task(ambient.ws_connect())
hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = ambient
except WebsocketError as err:
- _LOGGER.error("Config entry failed: %s", err)
+ LOGGER.error("Config entry failed: %s", err)
raise ConfigEntryNotReady from err
async def _async_disconnect_websocket(*_):
@@ -337,7 +335,7 @@ async def async_migrate_entry(hass, config_entry):
"""Migrate old entry."""
version = config_entry.version
- _LOGGER.debug("Migrating from version %s", version)
+ LOGGER.debug("Migrating from version %s", version)
# 1 -> 2: Unique ID format changed, so delete and re-import:
if version == 1:
@@ -350,7 +348,7 @@ async def async_migrate_entry(hass, config_entry):
version = config_entry.version = 2
hass.config_entries.async_update_entry(config_entry)
- _LOGGER.info("Migration to version %s successful", version)
+ LOGGER.info("Migration to version %s successful", version)
return True
@@ -377,7 +375,7 @@ class AmbientStation:
try:
await connect()
except WebsocketError as err:
- _LOGGER.error("Error with the websocket connection: %s", err)
+ LOGGER.error("Error with the websocket connection: %s", err)
self._ws_reconnect_delay = min(2 * self._ws_reconnect_delay, 480)
async_call_later(self._hass, self._ws_reconnect_delay, connect)
@@ -386,13 +384,13 @@ class AmbientStation:
def on_connect():
"""Define a handler to fire when the websocket is connected."""
- _LOGGER.info("Connected to websocket")
+ LOGGER.info("Connected to websocket")
def on_data(data):
"""Define a handler to fire when the data is received."""
mac_address = data["macAddress"]
if data != self.stations[mac_address][ATTR_LAST_DATA]:
- _LOGGER.debug("New data received: %s", data)
+ LOGGER.debug("New data received: %s", data)
self.stations[mac_address][ATTR_LAST_DATA] = data
async_dispatcher_send(
self._hass, f"ambient_station_data_update_{mac_address}"
@@ -400,7 +398,7 @@ class AmbientStation:
def on_disconnect():
"""Define a handler to fire when the websocket is disconnected."""
- _LOGGER.info("Disconnected from websocket")
+ LOGGER.info("Disconnected from websocket")
def on_subscribed(data):
"""Define a handler to fire when the subscription is set."""
@@ -408,7 +406,7 @@ class AmbientStation:
if station["macAddress"] in self.stations:
continue
- _LOGGER.debug("New station subscription: %s", data)
+ LOGGER.debug("New station subscription: %s", data)
# Only create entities based on the data coming through the socket.
# If the user is monitoring brightness (in W/m^2), make sure we also
diff --git a/homeassistant/components/ambient_station/const.py b/homeassistant/components/ambient_station/const.py
index 3b1990ae837..e59f926eac3 100644
--- a/homeassistant/components/ambient_station/const.py
+++ b/homeassistant/components/ambient_station/const.py
@@ -1,5 +1,8 @@
"""Define constants for the Ambient PWS component."""
+import logging
+
DOMAIN = "ambient_station"
+LOGGER = logging.getLogger(__package__)
ATTR_LAST_DATA = "last_data"
ATTR_MONITORED_CONDITIONS = "monitored_conditions"
diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py
index 5ac6acb2071..a8aabc233d1 100644
--- a/homeassistant/components/amcrest/camera.py
+++ b/homeassistant/components/amcrest/camera.py
@@ -236,7 +236,7 @@ class AmcrestCam(Camera):
# streaming via ffmpeg
streaming_url = self._rtsp_url
- stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop)
+ stream = CameraMjpeg(self._ffmpeg.binary)
await stream.open_camera(streaming_url, extra_cmd=self._ffmpeg_arguments)
try:
diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json
index eaa59f50db8..6adea1af5af 100644
--- a/homeassistant/components/androidtv/manifest.json
+++ b/homeassistant/components/androidtv/manifest.json
@@ -4,7 +4,7 @@
"documentation": "https://www.home-assistant.io/integrations/androidtv",
"requirements": [
"adb-shell[async]==0.2.1",
- "androidtv[async]==0.0.54",
+ "androidtv[async]==0.0.56",
"pure-python-adb[async]==0.3.0.dev0"
],
"codeowners": ["@JeffLIrion"]
diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py
index 72fb2636067..f383f982abc 100644
--- a/homeassistant/components/api/__init__.py
+++ b/homeassistant/components/api/__init__.py
@@ -56,7 +56,7 @@ STREAM_PING_PAYLOAD = "ping"
STREAM_PING_INTERVAL = 50 # seconds
-def setup(hass, config):
+async def async_setup(hass, config):
"""Register the API with the HTTP interface."""
hass.http.register_view(APIStatusView)
hass.http.register_view(APIEventStream)
diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py
index 80c09606dbf..14170fdd8cd 100644
--- a/homeassistant/components/apple_tv/__init__.py
+++ b/homeassistant/components/apple_tv/__init__.py
@@ -1,273 +1,363 @@
-"""Support for Apple TV."""
+"""The Apple TV integration."""
import asyncio
import logging
-from typing import Sequence, TypeVar, Union
+from random import randrange
-from pyatv import AppleTVDevice, connect_to_apple_tv, scan_for_apple_tvs
-from pyatv.exceptions import DeviceAuthenticationError
-import voluptuous as vol
+from pyatv import connect, exceptions, scan
+from pyatv.const import Protocol
-from homeassistant.components.discovery import SERVICE_APPLE_TV
-from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME
-from homeassistant.helpers import discovery
+from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
+from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN
+from homeassistant.const import (
+ CONF_ADDRESS,
+ CONF_NAME,
+ CONF_PROTOCOL,
+ 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.dispatcher import (
+ async_dispatcher_connect,
+ async_dispatcher_send,
+)
+from homeassistant.helpers.entity import Entity
+
+from .const import CONF_CREDENTIALS, CONF_IDENTIFIER, CONF_START_OFF, DOMAIN
_LOGGER = logging.getLogger(__name__)
-DOMAIN = "apple_tv"
-
-SERVICE_SCAN = "apple_tv_scan"
-SERVICE_AUTHENTICATE = "apple_tv_authenticate"
-
-ATTR_ATV = "atv"
-ATTR_POWER = "power"
-
-CONF_LOGIN_ID = "login_id"
-CONF_START_OFF = "start_off"
-CONF_CREDENTIALS = "credentials"
-
DEFAULT_NAME = "Apple TV"
-DATA_APPLE_TV = "data_apple_tv"
-DATA_ENTITIES = "data_apple_tv_entities"
+BACKOFF_TIME_UPPER_LIMIT = 300 # Five minutes
-KEY_CONFIG = "apple_tv_configuring"
+NOTIFICATION_TITLE = "Apple TV Notification"
+NOTIFICATION_ID = "apple_tv_notification"
-NOTIFICATION_AUTH_ID = "apple_tv_auth_notification"
-NOTIFICATION_AUTH_TITLE = "Apple TV Authentication"
-NOTIFICATION_SCAN_ID = "apple_tv_scan_notification"
-NOTIFICATION_SCAN_TITLE = "Apple TV Scan"
+SOURCE_REAUTH = "reauth"
-T = TypeVar("T")
+SIGNAL_CONNECTED = "apple_tv_connected"
+SIGNAL_DISCONNECTED = "apple_tv_disconnected"
-
-# This version of ensure_list interprets an empty dict as no value
-def ensure_list(value: Union[T, Sequence[T]]) -> Sequence[T]:
- """Wrap value in list if it is not one."""
- if value is None or (isinstance(value, dict) and not value):
- return []
- return value if isinstance(value, list) else [value]
-
-
-CONFIG_SCHEMA = vol.Schema(
- {
- DOMAIN: vol.All(
- ensure_list,
- [
- vol.Schema(
- {
- vol.Required(CONF_HOST): cv.string,
- vol.Required(CONF_LOGIN_ID): cv.string,
- vol.Optional(CONF_CREDENTIALS): cv.string,
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(CONF_START_OFF, default=False): cv.boolean,
- }
- )
- ],
- )
- },
- extra=vol.ALLOW_EXTRA,
-)
-
-# Currently no attributes but it might change later
-APPLE_TV_SCAN_SCHEMA = vol.Schema({})
-
-APPLE_TV_AUTHENTICATE_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
-
-
-def request_configuration(hass, config, atv, credentials):
- """Request configuration steps from the user."""
- configurator = hass.components.configurator
-
- async def configuration_callback(callback_data):
- """Handle the submitted configuration."""
-
- pin = callback_data.get("pin")
-
- try:
- await atv.airplay.finish_authentication(pin)
- hass.components.persistent_notification.async_create(
- f"Authentication succeeded!
"
- f"Add the following to credentials: "
- f"in your apple_tv configuration:
{credentials}",
- title=NOTIFICATION_AUTH_TITLE,
- notification_id=NOTIFICATION_AUTH_ID,
- )
- except DeviceAuthenticationError as ex:
- hass.components.persistent_notification.async_create(
- f"Authentication failed! Did you enter correct PIN?
Details: {ex}",
- title=NOTIFICATION_AUTH_TITLE,
- notification_id=NOTIFICATION_AUTH_ID,
- )
-
- hass.async_add_job(configurator.request_done, instance)
-
- instance = configurator.request_config(
- "Apple TV Authentication",
- configuration_callback,
- description="Please enter PIN code shown on screen.",
- submit_caption="Confirm",
- fields=[{"id": "pin", "name": "PIN Code", "type": "password"}],
- )
-
-
-async def scan_apple_tvs(hass):
- """Scan for devices and present a notification of the ones found."""
-
- atvs = await scan_for_apple_tvs(hass.loop, timeout=3)
-
- devices = []
- for atv in atvs:
- login_id = atv.login_id
- if login_id is None:
- login_id = "Home Sharing disabled"
- devices.append(
- f"Name: {atv.name}
Host: {atv.address}
Login ID: {login_id}"
- )
-
- if not devices:
- devices = ["No device(s) found"]
-
- found_devices = "
".join(devices)
-
- hass.components.persistent_notification.async_create(
- f"The following devices were found:
{found_devices}",
- title=NOTIFICATION_SCAN_TITLE,
- notification_id=NOTIFICATION_SCAN_ID,
- )
+PLATFORMS = [MP_DOMAIN, REMOTE_DOMAIN]
async def async_setup(hass, config):
- """Set up the Apple TV component."""
- if DATA_APPLE_TV not in hass.data:
- hass.data[DATA_APPLE_TV] = {}
+ """Set up the Apple TV integration."""
+ return True
- async def async_service_handler(service):
- """Handle service calls."""
- entity_ids = service.data.get(ATTR_ENTITY_ID)
- if service.service == SERVICE_SCAN:
- hass.async_add_job(scan_apple_tvs, hass)
- return
+async def async_setup_entry(hass, entry):
+ """Set up a config entry for Apple TV."""
+ manager = AppleTVManager(hass, entry)
+ hass.data.setdefault(DOMAIN, {})[entry.unique_id] = manager
- if entity_ids:
- devices = [
- device
- for device in hass.data[DATA_ENTITIES]
- if device.entity_id in entity_ids
+ async def on_hass_stop(event):
+ """Stop push updates when hass stops."""
+ await manager.disconnect()
+
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop)
+
+ async def setup_platforms():
+ """Set up platforms and initiate connection."""
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_setup(entry, component)
+ for component in PLATFORMS
]
- else:
- devices = hass.data[DATA_ENTITIES]
-
- for device in devices:
- if service.service != SERVICE_AUTHENTICATE:
- continue
-
- atv = device.atv
- credentials = await atv.airplay.generate_credentials()
- await atv.airplay.load_credentials(credentials)
- _LOGGER.debug("Generated new credentials: %s", credentials)
- await atv.airplay.start_authentication()
- hass.async_add_job(request_configuration, hass, config, atv, credentials)
-
- async def atv_discovered(service, info):
- """Set up an Apple TV that was auto discovered."""
- await _setup_atv(
- hass,
- config,
- {
- CONF_NAME: info["name"],
- CONF_HOST: info["host"],
- CONF_LOGIN_ID: info["properties"]["hG"],
- CONF_START_OFF: False,
- },
)
+ await manager.init()
- discovery.async_listen(hass, SERVICE_APPLE_TV, atv_discovered)
-
- tasks = [_setup_atv(hass, config, conf) for conf in config.get(DOMAIN, [])]
- if tasks:
- await asyncio.wait(tasks)
-
- hass.services.async_register(
- DOMAIN, SERVICE_SCAN, async_service_handler, schema=APPLE_TV_SCAN_SCHEMA
- )
-
- hass.services.async_register(
- DOMAIN,
- SERVICE_AUTHENTICATE,
- async_service_handler,
- schema=APPLE_TV_AUTHENTICATE_SCHEMA,
- )
+ hass.async_create_task(setup_platforms())
return True
-async def _setup_atv(hass, hass_config, atv_config):
- """Set up an Apple TV."""
-
- name = atv_config.get(CONF_NAME)
- host = atv_config.get(CONF_HOST)
- login_id = atv_config.get(CONF_LOGIN_ID)
- start_off = atv_config.get(CONF_START_OFF)
- credentials = atv_config.get(CONF_CREDENTIALS)
-
- if host in hass.data[DATA_APPLE_TV]:
- return
-
- details = AppleTVDevice(name, host, login_id)
- session = async_get_clientsession(hass)
- atv = connect_to_apple_tv(details, hass.loop, session=session)
- if credentials:
- await atv.airplay.load_credentials(credentials)
-
- power = AppleTVPowerManager(hass, atv, start_off)
- hass.data[DATA_APPLE_TV][host] = {ATTR_ATV: atv, ATTR_POWER: power}
-
- hass.async_create_task(
- discovery.async_load_platform(
- hass, "media_player", DOMAIN, atv_config, hass_config
+async def async_unload_entry(hass, entry):
+ """Unload an Apple TV config entry."""
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in PLATFORMS
+ ]
)
)
+ if unload_ok:
+ manager = hass.data[DOMAIN].pop(entry.unique_id)
+ await manager.disconnect()
- hass.async_create_task(
- discovery.async_load_platform(hass, "remote", DOMAIN, atv_config, hass_config)
- )
+ return unload_ok
-class AppleTVPowerManager:
- """Manager for global power management of an Apple TV.
+class AppleTVEntity(Entity):
+ """Device that sends commands to an Apple TV."""
- An instance is used per device to share the same power state between
- several platforms.
- """
+ def __init__(self, name, identifier, manager):
+ """Initialize device."""
+ self.atv = None
+ self.manager = manager
+ self._name = name
+ self._identifier = identifier
- def __init__(self, hass, atv, is_off):
- """Initialize power manager."""
- self.hass = hass
- self.atv = atv
- self.listeners = []
- self._is_on = not is_off
+ async def async_added_to_hass(self):
+ """Handle when an entity is about to be added to Home Assistant."""
- def init(self):
- """Initialize power management."""
- if self._is_on:
- self.atv.push_updater.start()
+ @callback
+ def _async_connected(atv):
+ """Handle that a connection was made to a device."""
+ self.atv = atv
+ self.async_device_connected(atv)
+ self.async_write_ha_state()
+
+ @callback
+ def _async_disconnected():
+ """Handle that a connection to a device was lost."""
+ self.async_device_disconnected()
+ self.atv = None
+ self.async_write_ha_state()
+
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass, f"{SIGNAL_CONNECTED}_{self._identifier}", _async_connected
+ )
+ )
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass,
+ f"{SIGNAL_DISCONNECTED}_{self._identifier}",
+ _async_disconnected,
+ )
+ )
+
+ def async_device_connected(self, atv):
+ """Handle when connection is made to device."""
+
+ def async_device_disconnected(self):
+ """Handle when connection was lost to device."""
@property
- def turned_on(self):
- """Return true if device is on or off."""
- return self._is_on
+ def device_info(self):
+ """Return the device info."""
+ return {
+ "identifiers": {(DOMAIN, self._identifier)},
+ "manufacturer": "Apple",
+ "name": self.name,
+ }
- def set_power_on(self, value):
- """Change if a device is on or off."""
- if value != self._is_on:
- self._is_on = value
- if not self._is_on:
+ @property
+ def name(self):
+ """Return the name of the device."""
+ return self._name
+
+ @property
+ def unique_id(self):
+ """Return a unique ID."""
+ return self._identifier
+
+ @property
+ def should_poll(self):
+ """No polling needed for Apple TV."""
+ return False
+
+
+class AppleTVManager:
+ """Connection and power manager for an Apple TV.
+
+ An instance is used per device to share the same power state between
+ several platforms. It also manages scanning and connection establishment
+ in case of problems.
+ """
+
+ def __init__(self, hass, config_entry):
+ """Initialize power manager."""
+ self.config_entry = config_entry
+ self.hass = hass
+ self.atv = None
+ self._is_on = not config_entry.options.get(CONF_START_OFF, False)
+ self._connection_attempts = 0
+ self._connection_was_lost = False
+ self._task = None
+
+ async def init(self):
+ """Initialize power management."""
+ if self._is_on:
+ await self.connect()
+
+ def connection_lost(self, _):
+ """Device was unexpectedly disconnected.
+
+ This is a callback function from pyatv.interface.DeviceListener.
+ """
+ _LOGGER.warning('Connection lost to Apple TV "%s"', self.atv.name)
+ if self.atv:
+ self.atv.close()
+ self.atv = None
+ self._connection_was_lost = True
+ self._dispatch_send(SIGNAL_DISCONNECTED)
+ self._start_connect_loop()
+
+ def connection_closed(self):
+ """Device connection was (intentionally) closed.
+
+ This is a callback function from pyatv.interface.DeviceListener.
+ """
+ if self.atv:
+ self.atv.close()
+ self.atv = None
+ self._dispatch_send(SIGNAL_DISCONNECTED)
+ self._start_connect_loop()
+
+ async def connect(self):
+ """Connect to device."""
+ self._is_on = True
+ self._start_connect_loop()
+
+ async def disconnect(self):
+ """Disconnect from device."""
+ _LOGGER.debug("Disconnecting from device")
+ self._is_on = False
+ try:
+ if self.atv:
+ self.atv.push_updater.listener = None
self.atv.push_updater.stop()
- else:
- self.atv.push_updater.start()
+ self.atv.close()
+ self.atv = None
+ if self._task:
+ self._task.cancel()
+ self._task = None
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("An error occurred while disconnecting")
- for listener in self.listeners:
- self.hass.async_create_task(listener.async_update_ha_state())
+ def _start_connect_loop(self):
+ """Start background connect loop to device."""
+ if not self._task and self.atv is None and self._is_on:
+ self._task = asyncio.create_task(self._connect_loop())
+ else:
+ _LOGGER.debug(
+ "Not starting connect loop (%s, %s)", self.atv is None, self._is_on
+ )
+
+ async def _connect_loop(self):
+ """Connect loop background task function."""
+ _LOGGER.debug("Starting connect loop")
+
+ # Try to find device and connect as long as the user has said that
+ # we are allowed to connect and we are not already connected.
+ while self._is_on and self.atv is None:
+ try:
+ conf = await self._scan()
+ if conf:
+ await self._connect(conf)
+ except exceptions.AuthenticationError:
+ self._auth_problem()
+ break
+ except asyncio.CancelledError:
+ pass
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Failed to connect")
+ self.atv = None
+
+ if self.atv is None:
+ self._connection_attempts += 1
+ backoff = min(
+ randrange(2 ** self._connection_attempts), BACKOFF_TIME_UPPER_LIMIT
+ )
+
+ _LOGGER.debug("Reconnecting in %d seconds", backoff)
+ await asyncio.sleep(backoff)
+
+ _LOGGER.debug("Connect loop ended")
+ self._task = None
+
+ def _auth_problem(self):
+ """Problem to authenticate occurred that needs intervention."""
+ _LOGGER.debug("Authentication error, reconfigure integration")
+
+ name = self.config_entry.data.get(CONF_NAME)
+ identifier = self.config_entry.unique_id
+
+ self.hass.components.persistent_notification.create(
+ "An irrecoverable connection problem occurred when connecting to "
+ f"`f{name}`. Please go to the Integrations page and reconfigure it",
+ title=NOTIFICATION_TITLE,
+ notification_id=NOTIFICATION_ID,
+ )
+
+ # Add to event queue as this function is called from a task being
+ # cancelled from disconnect
+ asyncio.create_task(self.disconnect())
+
+ self.hass.async_create_task(
+ self.hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_REAUTH},
+ data={CONF_NAME: name, CONF_IDENTIFIER: identifier},
+ )
+ )
+
+ async def _scan(self):
+ """Try to find device by scanning for it."""
+ identifier = self.config_entry.unique_id
+ address = self.config_entry.data[CONF_ADDRESS]
+ protocol = Protocol(self.config_entry.data[CONF_PROTOCOL])
+
+ _LOGGER.debug("Discovering device %s", identifier)
+ atvs = await scan(
+ self.hass.loop, identifier=identifier, protocol=protocol, hosts=[address]
+ )
+ if atvs:
+ return atvs[0]
+
+ _LOGGER.debug(
+ "Failed to find device %s with address %s, trying to scan",
+ identifier,
+ address,
+ )
+
+ atvs = await scan(self.hass.loop, identifier=identifier, protocol=protocol)
+ if atvs:
+ return atvs[0]
+
+ _LOGGER.debug("Failed to find device %s, trying later", identifier)
+
+ return None
+
+ async def _connect(self, conf):
+ """Connect to device."""
+ credentials = self.config_entry.data[CONF_CREDENTIALS]
+ session = async_get_clientsession(self.hass)
+
+ for protocol, creds in credentials.items():
+ conf.set_credentials(Protocol(int(protocol)), creds)
+
+ _LOGGER.debug("Connecting to device %s", self.config_entry.data[CONF_NAME])
+ self.atv = await connect(conf, self.hass.loop, session=session)
+ self.atv.listener = self
+
+ self._dispatch_send(SIGNAL_CONNECTED, self.atv)
+ self._address_updated(str(conf.address))
+
+ self._connection_attempts = 0
+ if self._connection_was_lost:
+ _LOGGER.info(
+ 'Connection was re-established to Apple TV "%s"', self.atv.service.name
+ )
+ self._connection_was_lost = False
+
+ @property
+ def is_connecting(self):
+ """Return true if connection is in progress."""
+ return self._task is not None
+
+ def _address_updated(self, address):
+ """Update cached address in config entry."""
+ _LOGGER.debug("Changing address to %s", address)
+ self.hass.config_entries.async_update_entry(
+ self.config_entry, data={**self.config_entry.data, CONF_ADDRESS: address}
+ )
+
+ def _dispatch_send(self, signal, *args):
+ """Dispatch a signal to all entities managed by this manager."""
+ async_dispatcher_send(
+ self.hass, f"{signal}_{self.config_entry.unique_id}", *args
+ )
diff --git a/homeassistant/components/apple_tv/config_flow.py b/homeassistant/components/apple_tv/config_flow.py
new file mode 100644
index 00000000000..9c2f25b6d53
--- /dev/null
+++ b/homeassistant/components/apple_tv/config_flow.py
@@ -0,0 +1,408 @@
+"""Config flow for Apple TV integration."""
+from ipaddress import ip_address
+import logging
+from random import randrange
+
+from pyatv import exceptions, pair, scan
+from pyatv.const import Protocol
+from pyatv.convert import protocol_str
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import (
+ CONF_ADDRESS,
+ CONF_NAME,
+ CONF_PIN,
+ CONF_PROTOCOL,
+ CONF_TYPE,
+)
+from homeassistant.core import callback
+from homeassistant.data_entry_flow import AbortFlow
+from homeassistant.exceptions import HomeAssistantError
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+
+from .const import CONF_CREDENTIALS, CONF_IDENTIFIER, CONF_START_OFF
+from .const import DOMAIN # pylint: disable=unused-import
+
+_LOGGER = logging.getLogger(__name__)
+
+DEVICE_INPUT = "device_input"
+
+INPUT_PIN_SCHEMA = vol.Schema({vol.Required(CONF_PIN, default=None): int})
+
+DEFAULT_START_OFF = False
+PROTOCOL_PRIORITY = [Protocol.MRP, Protocol.DMAP, Protocol.AirPlay]
+
+
+async def device_scan(identifier, loop, cache=None):
+ """Scan for a specific device using identifier as filter."""
+
+ def _filter_device(dev):
+ if identifier is None:
+ return True
+ if identifier == str(dev.address):
+ return True
+ if identifier == dev.name:
+ return True
+ return any([service.identifier == identifier for service in dev.services])
+
+ def _host_filter():
+ try:
+ return [ip_address(identifier)]
+ except ValueError:
+ return None
+
+ if cache:
+ matches = [atv for atv in cache if _filter_device(atv)]
+ if matches:
+ return cache, matches[0]
+
+ for hosts in [_host_filter(), None]:
+ scan_result = await scan(loop, timeout=3, hosts=hosts)
+ matches = [atv for atv in scan_result if _filter_device(atv)]
+
+ if matches:
+ return scan_result, matches[0]
+
+ return scan_result, None
+
+
+def is_valid_credentials(credentials):
+ """Verify that credentials are valid for establishing a connection."""
+ return (
+ credentials.get(Protocol.MRP.value) is not None
+ or credentials.get(Protocol.DMAP.value) is not None
+ )
+
+
+class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for Apple TV."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
+
+ @staticmethod
+ @callback
+ def async_get_options_flow(config_entry):
+ """Get options flow for this handler."""
+ return AppleTVOptionsFlow(config_entry)
+
+ def __init__(self):
+ """Initialize a new AppleTVConfigFlow."""
+ self.target_device = None
+ self.scan_result = None
+ self.atv = None
+ self.protocol = None
+ self.pairing = None
+ self.credentials = {} # Protocol -> credentials
+
+ async def async_step_reauth(self, info):
+ """Handle initial step when updating invalid credentials."""
+ await self.async_set_unique_id(info[CONF_IDENTIFIER])
+ self.target_device = info[CONF_IDENTIFIER]
+
+ # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
+ self.context["title_placeholders"] = {"name": info[CONF_NAME]}
+
+ # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
+ self.context["identifier"] = self.unique_id
+ return await self.async_step_reconfigure()
+
+ async def async_step_reconfigure(self, user_input=None):
+ """Inform user that reconfiguration is about to start."""
+ if user_input is not None:
+ return await self.async_find_device_wrapper(
+ self.async_begin_pairing, allow_exist=True
+ )
+
+ return self.async_show_form(step_id="reconfigure")
+
+ async def async_step_user(self, user_input=None):
+ """Handle the initial step."""
+ # Be helpful to the user and look for devices
+ if self.scan_result is None:
+ self.scan_result, _ = await device_scan(None, self.hass.loop)
+
+ errors = {}
+ default_suggestion = self._prefill_identifier()
+ if user_input is not None:
+ self.target_device = user_input[DEVICE_INPUT]
+ try:
+ await self.async_find_device()
+ except DeviceNotFound:
+ errors["base"] = "no_devices_found"
+ except DeviceAlreadyConfigured:
+ errors["base"] = "already_configured"
+ except exceptions.NoServiceError:
+ errors["base"] = "no_usable_service"
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception")
+ errors["base"] = "unknown"
+ else:
+ await self.async_set_unique_id(
+ self.atv.identifier, raise_on_progress=False
+ )
+ return await self.async_step_confirm()
+
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema(
+ {vol.Required(DEVICE_INPUT, default=default_suggestion): str}
+ ),
+ errors=errors,
+ description_placeholders={"devices": self._devices_str()},
+ )
+
+ async def async_step_zeroconf(self, discovery_info):
+ """Handle device found via zeroconf."""
+ service_type = discovery_info[CONF_TYPE]
+ properties = discovery_info["properties"]
+
+ if service_type == "_mediaremotetv._tcp.local.":
+ identifier = properties["UniqueIdentifier"]
+ name = properties["Name"]
+ elif service_type == "_touch-able._tcp.local.":
+ identifier = discovery_info["name"].split(".")[0]
+ name = properties["CtlN"]
+ else:
+ return self.async_abort(reason="unknown")
+
+ await self.async_set_unique_id(identifier)
+ self._abort_if_unique_id_configured()
+
+ # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
+ self.context["identifier"] = self.unique_id
+ self.context["title_placeholders"] = {"name": name}
+ self.target_device = identifier
+ return await self.async_find_device_wrapper(self.async_step_confirm)
+
+ async def async_find_device_wrapper(self, next_func, allow_exist=False):
+ """Find a specific device and call another function when done.
+
+ This function will do error handling and bail out when an error
+ occurs.
+ """
+ try:
+ await self.async_find_device(allow_exist)
+ except DeviceNotFound:
+ return self.async_abort(reason="no_devices_found")
+ except DeviceAlreadyConfigured:
+ return self.async_abort(reason="already_configured")
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception")
+ return self.async_abort(reason="unknown")
+
+ return await next_func()
+
+ async def async_find_device(self, allow_exist=False):
+ """Scan for the selected device to discover services."""
+ self.scan_result, self.atv = await device_scan(
+ self.target_device, self.hass.loop, cache=self.scan_result
+ )
+ if not self.atv:
+ raise DeviceNotFound()
+
+ self.protocol = self.atv.main_service().protocol
+
+ if not allow_exist:
+ for identifier in self.atv.all_identifiers:
+ if identifier in self._async_current_ids():
+ raise DeviceAlreadyConfigured()
+
+ # If credentials were found, save them
+ for service in self.atv.services:
+ if service.credentials:
+ self.credentials[service.protocol.value] = service.credentials
+
+ async def async_step_confirm(self, user_input=None):
+ """Handle user-confirmation of discovered node."""
+ if user_input is not None:
+ return await self.async_begin_pairing()
+ return self.async_show_form(
+ step_id="confirm", description_placeholders={"name": self.atv.name}
+ )
+
+ async def async_begin_pairing(self):
+ """Start pairing process for the next available protocol."""
+ self.protocol = self._next_protocol_to_pair()
+
+ # Dispose previous pairing sessions
+ if self.pairing is not None:
+ await self.pairing.close()
+ self.pairing = None
+
+ # Any more protocols to pair? Else bail out here
+ if not self.protocol:
+ await self.async_set_unique_id(self.atv.main_service().identifier)
+ return self._async_get_entry(
+ self.atv.main_service().protocol,
+ self.atv.name,
+ self.credentials,
+ self.atv.address,
+ )
+
+ # Initiate the pairing process
+ abort_reason = None
+ session = async_get_clientsession(self.hass)
+ self.pairing = await pair(
+ self.atv, self.protocol, self.hass.loop, session=session
+ )
+ try:
+ await self.pairing.begin()
+ except exceptions.ConnectionFailedError:
+ return await self.async_step_service_problem()
+ except exceptions.BackOffError:
+ abort_reason = "backoff"
+ except exceptions.PairingError:
+ _LOGGER.exception("Authentication problem")
+ abort_reason = "invalid_auth"
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception")
+ abort_reason = "unknown"
+
+ if abort_reason:
+ if self.pairing:
+ await self.pairing.close()
+ return self.async_abort(reason=abort_reason)
+
+ # Choose step depending on if PIN is required from user or not
+ if self.pairing.device_provides_pin:
+ return await self.async_step_pair_with_pin()
+
+ return await self.async_step_pair_no_pin()
+
+ async def async_step_pair_with_pin(self, user_input=None):
+ """Handle pairing step where a PIN is required from the user."""
+ errors = {}
+ if user_input is not None:
+ try:
+ self.pairing.pin(user_input[CONF_PIN])
+ await self.pairing.finish()
+ self.credentials[self.protocol.value] = self.pairing.service.credentials
+ return await self.async_begin_pairing()
+ except exceptions.PairingError:
+ _LOGGER.exception("Authentication problem")
+ errors["base"] = "invalid_auth"
+ except AbortFlow:
+ raise
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception")
+ errors["base"] = "unknown"
+
+ return self.async_show_form(
+ step_id="pair_with_pin",
+ data_schema=INPUT_PIN_SCHEMA,
+ errors=errors,
+ description_placeholders={"protocol": protocol_str(self.protocol)},
+ )
+
+ async def async_step_pair_no_pin(self, user_input=None):
+ """Handle step where user has to enter a PIN on the device."""
+ if user_input is not None:
+ await self.pairing.finish()
+ if self.pairing.has_paired:
+ self.credentials[self.protocol.value] = self.pairing.service.credentials
+ return await self.async_begin_pairing()
+
+ await self.pairing.close()
+ return self.async_abort(reason="device_did_not_pair")
+
+ pin = randrange(1000, stop=10000)
+ self.pairing.pin(pin)
+ return self.async_show_form(
+ step_id="pair_no_pin",
+ description_placeholders={
+ "protocol": protocol_str(self.protocol),
+ "pin": pin,
+ },
+ )
+
+ async def async_step_service_problem(self, user_input=None):
+ """Inform user that a service will not be added."""
+ if user_input is not None:
+ self.credentials[self.protocol.value] = None
+ return await self.async_begin_pairing()
+
+ return self.async_show_form(
+ step_id="service_problem",
+ description_placeholders={"protocol": protocol_str(self.protocol)},
+ )
+
+ def _async_get_entry(self, protocol, name, credentials, address):
+ if not is_valid_credentials(credentials):
+ return self.async_abort(reason="invalid_config")
+
+ data = {
+ CONF_PROTOCOL: protocol.value,
+ CONF_NAME: name,
+ CONF_CREDENTIALS: credentials,
+ CONF_ADDRESS: str(address),
+ }
+
+ self._abort_if_unique_id_configured(reload_on_update=False, updates=data)
+
+ return self.async_create_entry(title=name, data=data)
+
+ def _next_protocol_to_pair(self):
+ def _needs_pairing(protocol):
+ if self.atv.get_service(protocol) is None:
+ return False
+ return protocol.value not in self.credentials
+
+ for protocol in PROTOCOL_PRIORITY:
+ if _needs_pairing(protocol):
+ return protocol
+ return None
+
+ def _devices_str(self):
+ return ", ".join(
+ [
+ f"`{atv.name} ({atv.address})`"
+ for atv in self.scan_result
+ if atv.identifier not in self._async_current_ids()
+ ]
+ )
+
+ def _prefill_identifier(self):
+ # Return identifier (address) of one device that has not been paired with
+ for atv in self.scan_result:
+ if atv.identifier not in self._async_current_ids():
+ return str(atv.address)
+ return ""
+
+
+class AppleTVOptionsFlow(config_entries.OptionsFlow):
+ """Handle Apple TV options."""
+
+ def __init__(self, config_entry):
+ """Initialize Apple TV options flow."""
+ self.config_entry = config_entry
+ self.options = dict(config_entry.options)
+
+ async def async_step_init(self, user_input=None):
+ """Manage the Apple TV options."""
+ if user_input is not None:
+ self.options[CONF_START_OFF] = user_input[CONF_START_OFF]
+ return self.async_create_entry(title="", data=self.options)
+
+ return self.async_show_form(
+ step_id="init",
+ data_schema=vol.Schema(
+ {
+ vol.Optional(
+ CONF_START_OFF,
+ default=self.config_entry.options.get(
+ CONF_START_OFF, DEFAULT_START_OFF
+ ),
+ ): bool,
+ }
+ ),
+ )
+
+
+class DeviceNotFound(HomeAssistantError):
+ """Error to indicate device could not be found."""
+
+
+class DeviceAlreadyConfigured(HomeAssistantError):
+ """Error to indicate device is already configured."""
diff --git a/homeassistant/components/apple_tv/const.py b/homeassistant/components/apple_tv/const.py
new file mode 100644
index 00000000000..ac04cc1b937
--- /dev/null
+++ b/homeassistant/components/apple_tv/const.py
@@ -0,0 +1,11 @@
+"""Constants for the Apple TV integration."""
+
+DOMAIN = "apple_tv"
+
+CONF_IDENTIFIER = "identifier"
+CONF_CREDENTIALS = "credentials"
+CONF_CREDENTIALS_MRP = "mrp"
+CONF_CREDENTIALS_DMAP = "dmap"
+CONF_CREDENTIALS_AIRPLAY = "airplay"
+
+CONF_START_OFF = "start_off"
diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json
index 8ca42beab61..21b2df308d3 100644
--- a/homeassistant/components/apple_tv/manifest.json
+++ b/homeassistant/components/apple_tv/manifest.json
@@ -1,9 +1,17 @@
{
"domain": "apple_tv",
"name": "Apple TV",
+ "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
- "requirements": ["pyatv==0.3.13"],
- "dependencies": ["configurator"],
+ "requirements": [
+ "pyatv==0.7.5"
+ ],
+ "zeroconf": [
+ "_mediaremotetv._tcp.local.",
+ "_touch-able._tcp.local."
+ ],
"after_dependencies": ["discovery"],
- "codeowners": []
+ "codeowners": [
+ "@postlund"
+ ]
}
diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py
index 72e7d88b364..b7486af50e9 100644
--- a/homeassistant/components/apple_tv/media_player.py
+++ b/homeassistant/components/apple_tv/media_player.py
@@ -1,7 +1,7 @@
"""Support for Apple TV media player."""
import logging
-import pyatv.const as atv_const
+from pyatv.const import DeviceState, MediaType
from homeassistant.components.media_player import MediaPlayerEntity
from homeassistant.components.media_player.const import (
@@ -19,9 +19,7 @@ from homeassistant.components.media_player.const import (
SUPPORT_TURN_ON,
)
from homeassistant.const import (
- CONF_HOST,
CONF_NAME,
- EVENT_HOMEASSISTANT_STOP,
STATE_IDLE,
STATE_OFF,
STATE_PAUSED,
@@ -31,10 +29,13 @@ from homeassistant.const import (
from homeassistant.core import callback
import homeassistant.util.dt as dt_util
-from . import ATTR_ATV, ATTR_POWER, DATA_APPLE_TV, DATA_ENTITIES
+from . import AppleTVEntity
+from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
+PARALLEL_UPDATES = 0
+
SUPPORT_APPLE_TV = (
SUPPORT_TURN_ON
| SUPPORT_TURN_OFF
@@ -48,108 +49,61 @@ SUPPORT_APPLE_TV = (
)
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Set up the Apple TV platform."""
- if not discovery_info:
- return
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Load Apple TV media player based on a config entry."""
+ name = config_entry.data[CONF_NAME]
+ manager = hass.data[DOMAIN][config_entry.unique_id]
+ async_add_entities([AppleTvMediaPlayer(name, config_entry.unique_id, manager)])
- # Manage entity cache for service handler
- if DATA_ENTITIES not in hass.data:
- hass.data[DATA_ENTITIES] = []
- name = discovery_info[CONF_NAME]
- host = discovery_info[CONF_HOST]
- atv = hass.data[DATA_APPLE_TV][host][ATTR_ATV]
- power = hass.data[DATA_APPLE_TV][host][ATTR_POWER]
- entity = AppleTvDevice(atv, name, power)
+class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity):
+ """Representation of an Apple TV media player."""
+
+ def __init__(self, name, identifier, manager, **kwargs):
+ """Initialize the Apple TV media player."""
+ super().__init__(name, identifier, manager, **kwargs)
+ self._playing = None
@callback
- def on_hass_stop(event):
- """Stop push updates when hass stops."""
- atv.push_updater.stop()
-
- hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop)
-
- if entity not in hass.data[DATA_ENTITIES]:
- hass.data[DATA_ENTITIES].append(entity)
-
- async_add_entities([entity])
-
-
-class AppleTvDevice(MediaPlayerEntity):
- """Representation of an Apple TV device."""
-
- def __init__(self, atv, name, power):
- """Initialize the Apple TV device."""
- self.atv = atv
- self._name = name
- self._playing = None
- self._power = power
- self._power.listeners.append(self)
+ def async_device_connected(self, atv):
+ """Handle when connection is made to device."""
self.atv.push_updater.listener = self
+ self.atv.push_updater.start()
- async def async_added_to_hass(self):
- """Handle when an entity is about to be added to Home Assistant."""
- self._power.init()
-
- @property
- def name(self):
- """Return the name of the device."""
- return self._name
-
- @property
- def unique_id(self):
- """Return a unique ID."""
- return self.atv.metadata.device_id
-
- @property
- def should_poll(self):
- """No polling needed."""
- return False
+ @callback
+ def async_device_disconnected(self):
+ """Handle when connection was lost to device."""
+ self.atv.push_updater.stop()
+ self.atv.push_updater.listener = None
@property
def state(self):
"""Return the state of the device."""
- if not self._power.turned_on:
+ if self.manager.is_connecting:
+ return None
+ if self.atv is None:
return STATE_OFF
-
if self._playing:
-
- state = self._playing.play_state
- if state in (
- atv_const.PLAY_STATE_IDLE,
- atv_const.PLAY_STATE_NO_MEDIA,
- atv_const.PLAY_STATE_LOADING,
- ):
+ state = self._playing.device_state
+ if state in (DeviceState.Idle, DeviceState.Loading):
return STATE_IDLE
- if state == atv_const.PLAY_STATE_PLAYING:
+ if state == DeviceState.Playing:
return STATE_PLAYING
- if state in (
- atv_const.PLAY_STATE_PAUSED,
- atv_const.PLAY_STATE_FAST_FORWARD,
- atv_const.PLAY_STATE_FAST_BACKWARD,
- atv_const.PLAY_STATE_STOPPED,
- ):
- # Catch fast forward/backward here so "play" is default action
+ if state in (DeviceState.Paused, DeviceState.Seeking, DeviceState.Stopped):
return STATE_PAUSED
return STATE_STANDBY # Bad or unknown state?
+ return None
@callback
- def playstatus_update(self, updater, playing):
+ def playstatus_update(self, _, playing):
"""Print what is currently playing when it changes."""
self._playing = playing
self.async_write_ha_state()
@callback
- def playstatus_error(self, updater, exception):
+ def playstatus_error(self, _, exception):
"""Inform about an error and restart push updates."""
_LOGGER.warning("A %s error occurred: %s", exception.__class__, exception)
-
- # This will wait 10 seconds before restarting push updates. If the
- # connection continues to fail, it will flood the log (every 10
- # seconds) until it succeeds. A better approach should probably be
- # implemented here later.
- updater.start(initial_delay=10)
self._playing = None
self.async_write_ha_state()
@@ -157,50 +111,53 @@ class AppleTvDevice(MediaPlayerEntity):
def media_content_type(self):
"""Content type of current playing media."""
if self._playing:
-
- media_type = self._playing.media_type
- if media_type == atv_const.MEDIA_TYPE_VIDEO:
- return MEDIA_TYPE_VIDEO
- if media_type == atv_const.MEDIA_TYPE_MUSIC:
- return MEDIA_TYPE_MUSIC
- if media_type == atv_const.MEDIA_TYPE_TV:
- return MEDIA_TYPE_TVSHOW
+ return {
+ MediaType.Video: MEDIA_TYPE_VIDEO,
+ MediaType.Music: MEDIA_TYPE_MUSIC,
+ MediaType.TV: MEDIA_TYPE_TVSHOW,
+ }.get(self._playing.media_type)
+ return None
@property
def media_duration(self):
"""Duration of current playing media in seconds."""
if self._playing:
return self._playing.total_time
+ return None
@property
def media_position(self):
"""Position of current playing media in seconds."""
if self._playing:
return self._playing.position
+ return None
@property
def media_position_updated_at(self):
"""Last valid time of media position."""
- state = self.state
- if state in (STATE_PLAYING, STATE_PAUSED):
+ if self.state in (STATE_PLAYING, STATE_PAUSED):
return dt_util.utcnow()
+ return None
async def async_play_media(self, media_type, media_id, **kwargs):
"""Send the play_media command to the media player."""
- await self.atv.airplay.play_url(media_id)
+ await self.atv.stream.play_url(media_id)
@property
def media_image_hash(self):
"""Hash value for media image."""
state = self.state
- if self._playing and state not in [STATE_OFF, STATE_IDLE]:
- return self._playing.hash
+ if self._playing and state not in [None, STATE_OFF, STATE_IDLE]:
+ return self.atv.metadata.artwork_id
+ return None
async def async_get_media_image(self):
"""Fetch media image of current playing image."""
state = self.state
if self._playing and state not in [STATE_OFF, STATE_IDLE]:
- return (await self.atv.metadata.artwork()), "image/png"
+ artwork = await self.atv.metadata.artwork()
+ if artwork:
+ return artwork.bytes, artwork.mimetype
return None, None
@@ -208,12 +165,8 @@ class AppleTvDevice(MediaPlayerEntity):
def media_title(self):
"""Title of current playing media."""
if self._playing:
- if self.state == STATE_IDLE:
- return "Nothing playing"
- title = self._playing.title
- return title if title else "No title"
-
- return f"Establishing a connection to {self._name}..."
+ return self._playing.title
+ return None
@property
def supported_features(self):
@@ -222,22 +175,22 @@ class AppleTvDevice(MediaPlayerEntity):
async def async_turn_on(self):
"""Turn the media player on."""
- self._power.set_power_on(True)
+ await self.manager.connect()
async def async_turn_off(self):
"""Turn the media player off."""
self._playing = None
- self._power.set_power_on(False)
+ await self.manager.disconnect()
async def async_media_play_pause(self):
"""Pause media on media player."""
- if not self._playing:
- return
- state = self.state
- if state == STATE_PAUSED:
- await self.atv.remote_control.play()
- elif state == STATE_PLAYING:
- await self.atv.remote_control.pause()
+ if self._playing:
+ state = self.state
+ if state == STATE_PAUSED:
+ await self.atv.remote_control.play()
+ elif state == STATE_PLAYING:
+ await self.atv.remote_control.pause()
+ return None
async def async_media_play(self):
"""Play media."""
diff --git a/homeassistant/components/apple_tv/remote.py b/homeassistant/components/apple_tv/remote.py
index 4f935ba0ab8..a76c4c6a208 100644
--- a/homeassistant/components/apple_tv/remote.py
+++ b/homeassistant/components/apple_tv/remote.py
@@ -1,46 +1,32 @@
"""Remote control support for Apple TV."""
-from homeassistant.components import remote
-from homeassistant.const import CONF_HOST, CONF_NAME
-from . import ATTR_ATV, ATTR_POWER, DATA_APPLE_TV
+import logging
+
+from homeassistant.components.remote import RemoteEntity
+from homeassistant.const import CONF_NAME
+
+from . import AppleTVEntity
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+PARALLEL_UPDATES = 0
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Set up the Apple TV remote platform."""
- if not discovery_info:
- return
-
- name = discovery_info[CONF_NAME]
- host = discovery_info[CONF_HOST]
- atv = hass.data[DATA_APPLE_TV][host][ATTR_ATV]
- power = hass.data[DATA_APPLE_TV][host][ATTR_POWER]
- async_add_entities([AppleTVRemote(atv, power, name)])
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Load Apple TV remote based on a config entry."""
+ name = config_entry.data[CONF_NAME]
+ manager = hass.data[DOMAIN][config_entry.unique_id]
+ async_add_entities([AppleTVRemote(name, config_entry.unique_id, manager)])
-class AppleTVRemote(remote.RemoteEntity):
+class AppleTVRemote(AppleTVEntity, RemoteEntity):
"""Device that sends commands to an Apple TV."""
- def __init__(self, atv, power, name):
- """Initialize device."""
- self._atv = atv
- self._name = name
- self._power = power
- self._power.listeners.append(self)
-
- @property
- def name(self):
- """Return the name of the device."""
- return self._name
-
- @property
- def unique_id(self):
- """Return a unique ID."""
- return self._atv.metadata.device_id
-
@property
def is_on(self):
"""Return true if device is on."""
- return self._power.turned_on
+ return self.atv is not None
@property
def should_poll(self):
@@ -48,23 +34,21 @@ class AppleTVRemote(remote.RemoteEntity):
return False
async def async_turn_on(self, **kwargs):
- """Turn the device on.
-
- This method is a coroutine.
- """
- self._power.set_power_on(True)
+ """Turn the device on."""
+ await self.manager.connect()
async def async_turn_off(self, **kwargs):
- """Turn the device off.
-
- This method is a coroutine.
- """
- self._power.set_power_on(False)
+ """Turn the device off."""
+ await self.manager.disconnect()
async def async_send_command(self, command, **kwargs):
"""Send a command to one device."""
+ if not self.is_on:
+ _LOGGER.error("Unable to send commands, not connected to %s", self._name)
+ return
+
for single_command in command:
- if not hasattr(self._atv.remote_control, single_command):
+ if not hasattr(self.atv.remote_control, single_command):
continue
- await getattr(self._atv.remote_control, single_command)()
+ await getattr(self.atv.remote_control, single_command)()
diff --git a/homeassistant/components/apple_tv/services.yaml b/homeassistant/components/apple_tv/services.yaml
deleted file mode 100644
index af1e052fa33..00000000000
--- a/homeassistant/components/apple_tv/services.yaml
+++ /dev/null
@@ -1,8 +0,0 @@
-apple_tv_authenticate:
- description: Start AirPlay device authentication.
- fields:
- entity_id:
- description: Name(s) of entities to authenticate with.
- example: media_player.apple_tv
-apple_tv_scan:
- description: Scan for Apple TV devices.
diff --git a/homeassistant/components/apple_tv/strings.json b/homeassistant/components/apple_tv/strings.json
new file mode 100644
index 00000000000..e990fa0de06
--- /dev/null
+++ b/homeassistant/components/apple_tv/strings.json
@@ -0,0 +1,64 @@
+{
+ "title": "Apple TV",
+ "config": {
+ "flow_title": "Apple TV: {name}",
+ "step": {
+ "user": {
+ "title": "Setup a new Apple TV",
+ "description": "Start by entering the device name (e.g. Kitchen or Bedroom) or IP address of the Apple TV you want to add. If any devices were automatically found on your network, they are shown below.\n\nIf you cannot see your device or experience any issues, try specifying the device IP address.\n\n{devices}",
+ "data": {
+ "device_input": "Device"
+ }
+ },
+ "reconfigure": {
+ "title": "Device reconfiguration",
+ "description": "This Apple TV is experiencing some connection difficulties and must be reconfigured."
+ },
+ "pair_with_pin": {
+ "title": "Pairing",
+ "description": "Pairing is required for the `{protocol}` protocol. Please enter the PIN code displayed on screen. Leading zeros shall be omitted, i.e. enter 123 if the displayed code is 0123.",
+ "data": {
+ "pin": "[%key:common::config_flow::data::pin%]"
+ }
+ },
+ "pair_no_pin": {
+ "title": "Pairing",
+ "description": "Pairing is required for the `{protocol}` service. Please enter PIN {pin} on your Apple TV to continue."
+ },
+ "service_problem": {
+ "title": "Failed to add service",
+ "description": "A problem occurred while pairing protocol `{protocol}`. It will be ignored."
+ },
+ "confirm": {
+ "title": "Confirm adding Apple TV",
+ "description": "You are about to add the Apple TV named `{name}` to Home Assistant.\n\n**To complete the process, you may have to enter multiple PIN codes.**\n\nPlease note that you will *not* be able to power off your Apple TV with this integration. Only the media player in Home Assistant will turn off!"
+ }
+ },
+ "error": {
+ "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+ "no_usable_service": "A device was found but could not identify any way to establish a connection to it. If you keep seeing this message, try specifying its IP address or restarting your Apple TV.",
+ "unknown": "[%key:common::config_flow::error::unknown%]",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
+ },
+ "abort": {
+ "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
+ "already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]",
+ "device_did_not_pair": "No attempt to finish pairing process was made from the device.",
+ "backoff": "Device does not accept pairing reqests at this time (you might have entered an invalid PIN code too many times), try again later.",
+ "invalid_config": "The configuration for this device is incomplete. Please try adding it again.",
+ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "description": "Configure general device settings",
+ "data": {
+ "start_off": "Do not turn device on when starting Home Assistant"
+ }
+ }
+ }
+ }
+}
diff --git a/homeassistant/components/apple_tv/translations/en.json b/homeassistant/components/apple_tv/translations/en.json
new file mode 100644
index 00000000000..0fe914c3d86
--- /dev/null
+++ b/homeassistant/components/apple_tv/translations/en.json
@@ -0,0 +1,64 @@
+{
+ "config": {
+ "abort": {
+ "already_configured_device": "Device is already configured",
+ "already_in_progress": "Configuration flow is already in progress",
+ "backoff": "Device does not accept pairing reqests at this time (you might have entered an invalid PIN code too many times), try again later.",
+ "device_did_not_pair": "No attempt to finish pairing process was made from the device.",
+ "invalid_config": "The configuration for this device is incomplete. Please try adding it again.",
+ "no_devices_found": "No devices found on the network",
+ "unknown": "Unexpected error"
+ },
+ "error": {
+ "already_configured": "Device is already configured",
+ "invalid_auth": "Invalid authentication",
+ "no_devices_found": "No devices found on the network",
+ "no_usable_service": "A device was found but could not identify any way to establish a connection to it. If you keep seeing this message, try specifying its IP address or restarting your Apple TV.",
+ "unknown": "Unexpected error"
+ },
+ "flow_title": "Apple TV: {name}",
+ "step": {
+ "confirm": {
+ "description": "You are about to add the Apple TV named `{name}` to Home Assistant.\n\n**To complete the process, you may have to enter multiple PIN codes.**\n\nPlease note that you will *not* be able to power off your Apple TV with this integration. Only the media player in Home Assistant will turn off!",
+ "title": "Confirm adding Apple TV"
+ },
+ "pair_no_pin": {
+ "description": "Pairing is required for the `{protocol}` service. Please enter PIN {pin} on your Apple TV to continue.",
+ "title": "Pairing"
+ },
+ "pair_with_pin": {
+ "data": {
+ "pin": "PIN Code"
+ },
+ "description": "Pairing is required for the `{protocol}` protocol. Please enter the PIN code displayed on screen. Leading zeros shall be omitted, i.e. enter 123 if the displayed code is 0123.",
+ "title": "Pairing"
+ },
+ "reconfigure": {
+ "description": "This Apple TV is experiencing some connection difficulties and must be reconfigured.",
+ "title": "Device reconfiguration"
+ },
+ "service_problem": {
+ "description": "A problem occurred while pairing protocol `{protocol}`. It will be ignored.",
+ "title": "Failed to add service"
+ },
+ "user": {
+ "data": {
+ "device_input": "Device"
+ },
+ "description": "Start by entering the device name (e.g. Kitchen or Bedroom) or IP address of the Apple TV you want to add. If any devices were automatically found on your network, they are shown below.\n\nIf you cannot see your device or experience any issues, try specifying the device IP address.\n\n{devices}",
+ "title": "Setup a new Apple TV"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "start_off": "Do not turn device on when starting Home Assistant"
+ },
+ "description": "Configure general device settings"
+ }
+ }
+ },
+ "title": "Apple TV"
+}
\ No newline at end of file
diff --git a/homeassistant/components/apple_tv/translations/et.json b/homeassistant/components/apple_tv/translations/et.json
new file mode 100644
index 00000000000..597c4756907
--- /dev/null
+++ b/homeassistant/components/apple_tv/translations/et.json
@@ -0,0 +1,64 @@
+{
+ "config": {
+ "abort": {
+ "already_configured_device": "Seade on juba h\u00e4\u00e4lestatud",
+ "already_in_progress": "Seadistamine on juba k\u00e4imas",
+ "backoff": "Seade ei aktsepteeri praegu sidumisn\u00f5udeid (v\u00f5ib-olla oled liiga palju kordi vale PIN-koodi sisestanud), proovi hiljem uuesti.",
+ "device_did_not_pair": "Seade ei \u00fcritatud sidumisprotsessi l\u00f5pule viia.",
+ "invalid_config": "Selle seadme s\u00e4tted on puudulikud. Proovi see uuesti lisada.",
+ "no_devices_found": "V\u00f5rgust ei leitud \u00fchtegi seadet",
+ "unknown": "Ootamatu t\u00f5rge"
+ },
+ "error": {
+ "already_configured": "Seade on juba h\u00e4\u00e4lestatud",
+ "invalid_auth": "Vigane autentimine",
+ "no_devices_found": "V\u00f5rgust ei leitud \u00fchtegi seadet",
+ "no_usable_service": "Leiti seade kuid ei suudetud tuvastada moodust \u00fchenduse loomiseks. Kui n\u00e4ed seda teadet pidevalt, proovi m\u00e4\u00e4rata seadme IP-aadress v\u00f5i taask\u00e4ivita Apple TV.",
+ "unknown": "Ootamatu t\u00f5rge"
+ },
+ "flow_title": "Apple TV: {name}",
+ "step": {
+ "confirm": {
+ "description": "Oled Home Assistantile lisamas Apple TV-d nimega {name}.\n\n**Protsessi l\u00f5puleviimiseks pead v\u00f5ib-olla sisestama mitu PIN-koodi.**\n\nPane t\u00e4hele, et selle sidumisega * ei saa * v\u00e4lja l\u00fclitada oma Apple TV-d. Ainult Home Assistant-i meediam\u00e4ngija l\u00fclitub v\u00e4lja!",
+ "title": "Kinnita Apple TV lisamine"
+ },
+ "pair_no_pin": {
+ "description": "Teenuse {protocol} sidumine on vajalik. J\u00e4tkamiseks sisesta oma Apple TV-s PIN-kood {pin} .",
+ "title": "Sidumine"
+ },
+ "pair_with_pin": {
+ "data": {
+ "pin": "PIN kood"
+ },
+ "description": "Vajalik on protokolli {protocol} sidumine. Sisesta ekraanil kuvatav PIN-kood. Alguse nullid j\u00e4etakse v\u00e4lja, st. sisesta 123, kui kuvatav kood on 0123.",
+ "title": "Sidumine"
+ },
+ "reconfigure": {
+ "description": "Sellel Apple TV-l on \u00fchendusprobleemid ja see tuleb uuesti seadistada.",
+ "title": "Seadme \u00fcmberseadistamine"
+ },
+ "service_problem": {
+ "description": "Protokolli {protocol} sidumisel ilmnes probleem. Seda ignoreeritakse.",
+ "title": "Teenuse lisamine eba\u00f5nnestus."
+ },
+ "user": {
+ "data": {
+ "device_input": "Seade"
+ },
+ "description": "Alustuseks sisesta lisatava Apple TV seadme nimi (nt K\u00f6\u00f6k v\u00f5i Magamistuba) v\u00f5i IP-aadress. Kui m\u00f5ni seade leiti teie v\u00f5rgust automaatselt kuvatakse see allpool. \n\n Kui ei n\u00e4e oma seadet v\u00f5i on probleeme, proovi m\u00e4\u00e4rata seadme IP-aadress. \n\n {devices}",
+ "title": "Seadista uus Apple TV sidumine"
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "start_off": "\u00c4ra l\u00fclita seadet Home Assistanti k\u00e4ivitamisel sisse"
+ },
+ "description": "Seadme \u00fclds\u00e4tete seadistamine"
+ }
+ }
+ },
+ "title": ""
+}
\ No newline at end of file
diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py
index 0875e094352..0175dfd6586 100644
--- a/homeassistant/components/arcam_fmj/__init__.py
+++ b/homeassistant/components/arcam_fmj/__init__.py
@@ -23,7 +23,7 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
-CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.115")
+CONFIG_SCHEMA = cv.deprecated(DOMAIN)
async def _await_cancel(task):
diff --git a/homeassistant/components/arcam_fmj/translations/hu.json b/homeassistant/components/arcam_fmj/translations/hu.json
new file mode 100644
index 00000000000..563ede56155
--- /dev/null
+++ b/homeassistant/components/arcam_fmj/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/arcam_fmj/translations/it.json b/homeassistant/components/arcam_fmj/translations/it.json
index 649bc0f2b39..f5cef4cd8b0 100644
--- a/homeassistant/components/arcam_fmj/translations/it.json
+++ b/homeassistant/components/arcam_fmj/translations/it.json
@@ -5,10 +5,6 @@
"already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso",
"cannot_connect": "Impossibile connettersi"
},
- "error": {
- "one": "uno",
- "other": "altri"
- },
"flow_title": "Arcam FMJ su {host}",
"step": {
"confirm": {
diff --git a/homeassistant/components/arcam_fmj/translations/pl.json b/homeassistant/components/arcam_fmj/translations/pl.json
index 8cf2bb5b4eb..af28552d892 100644
--- a/homeassistant/components/arcam_fmj/translations/pl.json
+++ b/homeassistant/components/arcam_fmj/translations/pl.json
@@ -14,7 +14,7 @@
"flow_title": "Arcam FMJ na {host}",
"step": {
"confirm": {
- "description": "Czy chcesz doda\u0107 Arcam FMJ na \"{host}\" do Home Assistant?"
+ "description": "Czy chcesz doda\u0107 Arcam FMJ na \"{host}\" do Home Assistanta?"
},
"user": {
"data": {
diff --git a/homeassistant/components/arlo/camera.py b/homeassistant/components/arlo/camera.py
index 6f7e3796309..36e32181702 100644
--- a/homeassistant/components/arlo/camera.py
+++ b/homeassistant/components/arlo/camera.py
@@ -88,7 +88,7 @@ class ArloCam(Camera):
_LOGGER.error(error_msg)
return
- stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop)
+ stream = CameraMjpeg(self._ffmpeg.binary)
await stream.open_camera(video.video_url, extra_cmd=self._ffmpeg_arguments)
try:
diff --git a/homeassistant/components/arlo/manifest.json b/homeassistant/components/arlo/manifest.json
index 41d4fc40e5f..f046f84f94d 100644
--- a/homeassistant/components/arlo/manifest.json
+++ b/homeassistant/components/arlo/manifest.json
@@ -2,7 +2,7 @@
"domain": "arlo",
"name": "Arlo",
"documentation": "https://www.home-assistant.io/integrations/arlo",
- "requirements": ["pyarlo==0.2.3"],
+ "requirements": ["pyarlo==0.2.4"],
"dependencies": ["ffmpeg"],
"codeowners": []
}
diff --git a/homeassistant/components/asuswrt/manifest.json b/homeassistant/components/asuswrt/manifest.json
index 26b8d49ddb1..9afb7849f8c 100644
--- a/homeassistant/components/asuswrt/manifest.json
+++ b/homeassistant/components/asuswrt/manifest.json
@@ -2,6 +2,6 @@
"domain": "asuswrt",
"name": "ASUSWRT",
"documentation": "https://www.home-assistant.io/integrations/asuswrt",
- "requirements": ["aioasuswrt==1.3.0"],
+ "requirements": ["aioasuswrt==1.3.1"],
"codeowners": ["@kennedyshead"]
}
diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py
index f226b953c53..aa13bee81d0 100644
--- a/homeassistant/components/asuswrt/sensor.py
+++ b/homeassistant/components/asuswrt/sensor.py
@@ -1,184 +1,168 @@
"""Asuswrt status sensors."""
+from datetime import timedelta
+import enum
import logging
+from typing import Any, Dict, List, Optional
from aioasuswrt.asuswrt import AsusWrt
from homeassistant.const import DATA_GIGABYTES, DATA_RATE_MEGABITS_PER_SECOND
-from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.update_coordinator import (
+ CoordinatorEntity,
+ DataUpdateCoordinator,
+)
from . import DATA_ASUSWRT
-_LOGGER = logging.getLogger(__name__)
-
UPLOAD_ICON = "mdi:upload-network"
DOWNLOAD_ICON = "mdi:download-network"
+_LOGGER = logging.getLogger(__name__)
-async def async_setup_platform(hass, config, add_entities, discovery_info=None):
+
+@enum.unique
+class _SensorTypes(enum.Enum):
+ DEVICES = "devices"
+ UPLOAD = "upload"
+ DOWNLOAD = "download"
+ DOWNLOAD_SPEED = "download_speed"
+ UPLOAD_SPEED = "upload_speed"
+
+ @property
+ def unit_of_measurement(self) -> Optional[str]:
+ """Return a string with the unit of the sensortype."""
+ if self in (_SensorTypes.UPLOAD, _SensorTypes.DOWNLOAD):
+ return DATA_GIGABYTES
+ if self in (_SensorTypes.UPLOAD_SPEED, _SensorTypes.DOWNLOAD_SPEED):
+ return DATA_RATE_MEGABITS_PER_SECOND
+ return None
+
+ @property
+ def icon(self) -> Optional[str]:
+ """Return the expected icon for the sensortype."""
+ if self in (_SensorTypes.UPLOAD, _SensorTypes.UPLOAD_SPEED):
+ return UPLOAD_ICON
+ if self in (_SensorTypes.DOWNLOAD, _SensorTypes.DOWNLOAD_SPEED):
+ return DOWNLOAD_ICON
+ return None
+
+ @property
+ def sensor_name(self) -> Optional[str]:
+ """Return the name of the sensor."""
+ if self is _SensorTypes.DEVICES:
+ return "Asuswrt Devices Connected"
+ if self is _SensorTypes.UPLOAD:
+ return "Asuswrt Upload"
+ if self is _SensorTypes.DOWNLOAD:
+ return "Asuswrt Download"
+ if self is _SensorTypes.UPLOAD_SPEED:
+ return "Asuswrt Upload Speed"
+ if self is _SensorTypes.DOWNLOAD_SPEED:
+ return "Asuswrt Download Speed"
+ return None
+
+ @property
+ def is_speed(self) -> bool:
+ """Return True if the type is an upload/download speed."""
+ return self in (_SensorTypes.UPLOAD_SPEED, _SensorTypes.DOWNLOAD_SPEED)
+
+ @property
+ def is_size(self) -> bool:
+ """Return True if the type is the total upload/download size."""
+ return self in (_SensorTypes.UPLOAD, _SensorTypes.DOWNLOAD)
+
+
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the asuswrt sensors."""
if discovery_info is None:
return
- api = hass.data[DATA_ASUSWRT]
+ api: AsusWrt = hass.data[DATA_ASUSWRT]
- devices = []
+ # Let's discover the valid sensor types.
+ sensors = [_SensorTypes(x) for x in discovery_info]
- if "devices" in discovery_info:
- devices.append(AsuswrtDevicesSensor(api))
- if "download" in discovery_info:
- devices.append(AsuswrtTotalRXSensor(api))
- if "upload" in discovery_info:
- devices.append(AsuswrtTotalTXSensor(api))
- if "download_speed" in discovery_info:
- devices.append(AsuswrtRXSensor(api))
- if "upload_speed" in discovery_info:
- devices.append(AsuswrtTXSensor(api))
+ data_handler = AsuswrtDataHandler(sensors, api)
+ coordinator = DataUpdateCoordinator(
+ hass,
+ _LOGGER,
+ name="sensor",
+ update_method=data_handler.update_data,
+ # Polling interval. Will only be polled if there are subscribers.
+ update_interval=timedelta(seconds=30),
+ )
- add_entities(devices)
+ await coordinator.async_refresh()
+ async_add_entities([AsuswrtSensor(coordinator, x) for x in sensors])
-class AsuswrtSensor(Entity):
- """Representation of a asuswrt sensor."""
+class AsuswrtDataHandler:
+ """Class handling the API updates."""
- _name = "generic"
-
- def __init__(self, api: AsusWrt):
- """Initialize the sensor."""
+ def __init__(self, sensors: List[_SensorTypes], api: AsusWrt):
+ """Initialize the handler class."""
self._api = api
- self._state = None
- self._devices = None
- self._rates = None
- self._speed = None
- self._connect_error = False
+ self._sensors = sensors
+ self._connected = True
- @property
- def name(self):
- """Return the name of the sensor."""
- return self._name
+ async def update_data(self) -> Dict[_SensorTypes, Any]:
+ """Fetch the relevant data from the router."""
+ ret_dict: Dict[_SensorTypes, Any] = {}
+ try:
+ if _SensorTypes.DEVICES in self._sensors:
+ # Let's check the nr of devices.
+ devices = await self._api.async_get_connected_devices()
+ ret_dict[_SensorTypes.DEVICES] = len(devices)
+
+ if any(x.is_speed for x in self._sensors):
+ # Let's check the upload and download speed
+ speed = await self._api.async_get_current_transfer_rates()
+ ret_dict[_SensorTypes.DOWNLOAD_SPEED] = round(speed[0] / 125000, 2)
+ ret_dict[_SensorTypes.UPLOAD_SPEED] = round(speed[1] / 125000, 2)
+
+ if any(x.is_size for x in self._sensors):
+ rates = await self._api.async_get_bytes_total()
+ ret_dict[_SensorTypes.DOWNLOAD] = round(rates[0] / 1000000000, 1)
+ ret_dict[_SensorTypes.UPLOAD] = round(rates[1] / 1000000000, 1)
+
+ if not self._connected:
+ # Log a successful reconnect
+ self._connected = True
+ _LOGGER.warning("Successfully reconnected to ASUS router")
+
+ except OSError as err:
+ if self._connected:
+ # Log the first time connection was lost
+ _LOGGER.warning("Lost connection to router error due to: '%s'", err)
+ self._connected = False
+
+ return ret_dict
+
+
+class AsuswrtSensor(CoordinatorEntity):
+ """The asuswrt specific sensor class."""
+
+ def __init__(self, coordinator: DataUpdateCoordinator, sensor_type: _SensorTypes):
+ """Initialize the sensor class."""
+ super().__init__(coordinator)
+ self._type = sensor_type
@property
def state(self):
"""Return the state of the sensor."""
- return self._state
-
- async def async_update(self):
- """Fetch status from asuswrt."""
- try:
- self._devices = await self._api.async_get_connected_devices()
- self._rates = await self._api.async_get_bytes_total()
- self._speed = await self._api.async_get_current_transfer_rates()
- if self._connect_error:
- self._connect_error = False
- _LOGGER.info("Reconnected to ASUS router for %s update", self.entity_id)
- except OSError as err:
- if not self._connect_error:
- self._connect_error = True
- _LOGGER.error(
- "Error connecting to ASUS router for %s update: %s",
- self.entity_id,
- err,
- )
-
-
-class AsuswrtDevicesSensor(AsuswrtSensor):
- """Representation of a asuswrt download speed sensor."""
-
- _name = "Asuswrt Devices Connected"
-
- async def async_update(self):
- """Fetch new state data for the sensor."""
- await super().async_update()
- if self._devices:
- self._state = len(self._devices)
-
-
-class AsuswrtRXSensor(AsuswrtSensor):
- """Representation of a asuswrt download speed sensor."""
-
- _name = "Asuswrt Download Speed"
- _unit = DATA_RATE_MEGABITS_PER_SECOND
+ return self.coordinator.data.get(self._type)
@property
- def icon(self):
- """Return the icon."""
- return DOWNLOAD_ICON
+ def name(self) -> str:
+ """Return the name of the sensor."""
+ return self._type.sensor_name
@property
- def unit_of_measurement(self):
- """Return the unit of measurement."""
- return self._unit
-
- async def async_update(self):
- """Fetch new state data for the sensor."""
- await super().async_update()
- if self._speed:
- self._state = round(self._speed[0] / 125000, 2)
-
-
-class AsuswrtTXSensor(AsuswrtSensor):
- """Representation of a asuswrt upload speed sensor."""
-
- _name = "Asuswrt Upload Speed"
- _unit = DATA_RATE_MEGABITS_PER_SECOND
+ def icon(self) -> Optional[str]:
+ """Return the icon to use in the frontend."""
+ return self._type.icon
@property
- def icon(self):
- """Return the icon."""
- return UPLOAD_ICON
-
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement."""
- return self._unit
-
- async def async_update(self):
- """Fetch new state data for the sensor."""
- await super().async_update()
- if self._speed:
- self._state = round(self._speed[1] / 125000, 2)
-
-
-class AsuswrtTotalRXSensor(AsuswrtSensor):
- """Representation of a asuswrt total download sensor."""
-
- _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."""
- return self._unit
-
- async def async_update(self):
- """Fetch new state data for the sensor."""
- await super().async_update()
- if self._rates:
- self._state = round(self._rates[0] / 1000000000, 1)
-
-
-class AsuswrtTotalTXSensor(AsuswrtSensor):
- """Representation of a asuswrt total upload sensor."""
-
- _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."""
- return self._unit
-
- async def async_update(self):
- """Fetch new state data for the sensor."""
- await super().async_update()
- if self._rates:
- self._state = round(self._rates[1] / 1000000000, 1)
+ def unit_of_measurement(self) -> Optional[str]:
+ """Return the unit of measurement of this entity, if any."""
+ return self._type.unit_of_measurement
diff --git a/homeassistant/components/atag/translations/hu.json b/homeassistant/components/atag/translations/hu.json
index 45918735010..1d28556ba1a 100644
--- a/homeassistant/components/atag/translations/hu.json
+++ b/homeassistant/components/atag/translations/hu.json
@@ -1,5 +1,8 @@
{
"config": {
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/atag/translations/ka.json b/homeassistant/components/atag/translations/ka.json
new file mode 100644
index 00000000000..fa4c9c0abd3
--- /dev/null
+++ b/homeassistant/components/atag/translations/ka.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u10db\u10d0\u10e0\u10ea\u10ee\u10d8 \u10e8\u10d4\u10d4\u10e0\u10d7\u10d4\u10d1\u10d8\u10e1\u10d0\u10e1"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/august/translations/et.json b/homeassistant/components/august/translations/et.json
index efff8b8fe0f..0b455b06d00 100644
--- a/homeassistant/components/august/translations/et.json
+++ b/homeassistant/components/august/translations/et.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Konto on juba seadistatud",
- "reauth_successful": "Taasautentimine \u00f5nnestus"
+ "reauth_successful": "Taastuvastamine \u00f5nnestus"
},
"error": {
"cannot_connect": "\u00dchendamine nurjus",
diff --git a/homeassistant/components/aurora/__init__.py b/homeassistant/components/aurora/__init__.py
index 2b3caa06843..260a3bd735d 100644
--- a/homeassistant/components/aurora/__init__.py
+++ b/homeassistant/components/aurora/__init__.py
@@ -1 +1,130 @@
"""The aurora component."""
+
+import asyncio
+from datetime import timedelta
+import logging
+
+from auroranoaa import AuroraForecast
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
+from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers import aiohttp_client
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
+
+from .const import (
+ AURORA_API,
+ CONF_THRESHOLD,
+ COORDINATOR,
+ DEFAULT_POLLING_INTERVAL,
+ DEFAULT_THRESHOLD,
+ DOMAIN,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+PLATFORMS = ["binary_sensor"]
+
+
+async def async_setup(hass: HomeAssistant, config: dict):
+ """Set up the Aurora component."""
+ hass.data.setdefault(DOMAIN, {})
+
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Set up Aurora from a config entry."""
+
+ conf = entry.data
+ options = entry.options
+
+ session = aiohttp_client.async_get_clientsession(hass)
+ api = AuroraForecast(session)
+
+ longitude = conf[CONF_LONGITUDE]
+ latitude = conf[CONF_LATITUDE]
+ polling_interval = DEFAULT_POLLING_INTERVAL
+ threshold = options.get(CONF_THRESHOLD, DEFAULT_THRESHOLD)
+ name = conf[CONF_NAME]
+
+ coordinator = AuroraDataUpdateCoordinator(
+ hass=hass,
+ name=name,
+ polling_interval=polling_interval,
+ api=api,
+ latitude=latitude,
+ longitude=longitude,
+ threshold=threshold,
+ )
+
+ await coordinator.async_refresh()
+
+ if not coordinator.last_update_success:
+ raise ConfigEntryNotReady
+
+ hass.data[DOMAIN][entry.entry_id] = {
+ COORDINATOR: coordinator,
+ AURORA_API: api,
+ }
+
+ for component in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, component)
+ )
+
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Unload a config entry."""
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, component)
+ for component in PLATFORMS
+ ]
+ )
+ )
+ if unload_ok:
+ hass.data[DOMAIN].pop(entry.entry_id)
+
+ return unload_ok
+
+
+class AuroraDataUpdateCoordinator(DataUpdateCoordinator):
+ """Class to manage fetching data from the NOAA Aurora API."""
+
+ def __init__(
+ self,
+ hass: HomeAssistant,
+ name: str,
+ polling_interval: int,
+ api: str,
+ latitude: float,
+ longitude: float,
+ threshold: float,
+ ):
+ """Initialize the data updater."""
+
+ super().__init__(
+ hass=hass,
+ logger=_LOGGER,
+ name=name,
+ update_interval=timedelta(minutes=polling_interval),
+ )
+
+ self.api = api
+ self.name = name
+ self.latitude = int(latitude)
+ self.longitude = int(longitude)
+ self.threshold = int(threshold)
+
+ async def _async_update_data(self):
+ """Fetch the data from the NOAA Aurora Forecast."""
+
+ try:
+ return await self.api.get_forecast_data(self.longitude, self.latitude)
+ except ConnectionError as error:
+ raise UpdateFailed(f"Error updating from NOAA: {error}") from error
diff --git a/homeassistant/components/aurora/binary_sensor.py b/homeassistant/components/aurora/binary_sensor.py
index 1d5a6e83ec1..82be366ce6d 100644
--- a/homeassistant/components/aurora/binary_sensor.py
+++ b/homeassistant/components/aurora/binary_sensor.py
@@ -1,146 +1,75 @@
"""Support for aurora forecast data sensor."""
-from datetime import timedelta
import logging
-from math import floor
-from aiohttp.hdrs import USER_AGENT
-import requests
-import voluptuous as vol
+from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.const import ATTR_NAME
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
-from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity
-from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME
-import homeassistant.helpers.config_validation as cv
-from homeassistant.util import Throttle
+from . import AuroraDataUpdateCoordinator
+from .const import (
+ ATTR_IDENTIFIERS,
+ ATTR_MANUFACTURER,
+ ATTR_MODEL,
+ ATTRIBUTION,
+ COORDINATOR,
+ DOMAIN,
+)
_LOGGER = logging.getLogger(__name__)
-ATTRIBUTION = "Data provided by the National Oceanic and Atmospheric Administration"
-CONF_THRESHOLD = "forecast_threshold"
-DEFAULT_DEVICE_CLASS = "visible"
-DEFAULT_NAME = "Aurora Visibility"
-DEFAULT_THRESHOLD = 75
+async def async_setup_entry(hass, entry, async_add_entries):
+ """Set up the binary_sensor platform."""
+ coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR]
+ name = coordinator.name
-HA_USER_AGENT = "Home Assistant Aurora Tracker v.0.1.0"
+ entity = AuroraSensor(coordinator, name)
-MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
-
-URL = "http://services.swpc.noaa.gov/text/aurora-nowcast-map.txt"
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(CONF_THRESHOLD, default=DEFAULT_THRESHOLD): cv.positive_int,
- }
-)
+ async_add_entries([entity])
-def setup_platform(hass, config, add_entities, discovery_info=None):
- """Set up the aurora sensor."""
- if None in (hass.config.latitude, hass.config.longitude):
- _LOGGER.error("Lat. or long. not set in Home Assistant config")
- return False
-
- name = config[CONF_NAME]
- threshold = config[CONF_THRESHOLD]
-
- try:
- aurora_data = AuroraData(hass.config.latitude, hass.config.longitude, threshold)
- aurora_data.update()
- except requests.exceptions.HTTPError as error:
- _LOGGER.error("Connection to aurora forecast service failed: %s", error)
- return False
-
- add_entities([AuroraSensor(aurora_data, name)], True)
-
-
-class AuroraSensor(BinarySensorEntity):
+class AuroraSensor(CoordinatorEntity, BinarySensorEntity):
"""Implementation of an aurora sensor."""
- def __init__(self, aurora_data, name):
- """Initialize the sensor."""
- self.aurora_data = aurora_data
+ def __init__(self, coordinator: AuroraDataUpdateCoordinator, name):
+ """Define the binary sensor for the Aurora integration."""
+ super().__init__(coordinator=coordinator)
+
self._name = name
+ self.coordinator = coordinator
+ self._unique_id = f"{self.coordinator.latitude}_{self.coordinator.longitude}"
+
+ @property
+ def unique_id(self):
+ """Define the unique id based on the latitude and longitude."""
+ return self._unique_id
@property
def name(self):
"""Return the name of the sensor."""
- return f"{self._name}"
+ return self._name
@property
def is_on(self):
"""Return true if aurora is visible."""
- return self.aurora_data.is_visible if self.aurora_data else False
-
- @property
- def device_class(self):
- """Return the class of this device."""
- return DEFAULT_DEVICE_CLASS
+ return self.coordinator.data > self.coordinator.threshold
@property
def device_state_attributes(self):
"""Return the state attributes."""
- attrs = {}
+ return {"attribution": ATTRIBUTION}
- if self.aurora_data:
- attrs["visibility_level"] = self.aurora_data.visibility_level
- attrs["message"] = self.aurora_data.is_visible_text
- attrs[ATTR_ATTRIBUTION] = ATTRIBUTION
- return attrs
+ @property
+ def icon(self):
+ """Return the icon for the sensor."""
+ return "mdi:hazard-lights"
- def update(self):
- """Get the latest data from Aurora API and updates the states."""
- self.aurora_data.update()
-
-
-class AuroraData:
- """Get aurora forecast."""
-
- def __init__(self, latitude, longitude, threshold):
- """Initialize the data object."""
- self.latitude = latitude
- self.longitude = longitude
- self.headers = {USER_AGENT: HA_USER_AGENT}
- self.threshold = int(threshold)
- self.is_visible = None
- self.is_visible_text = None
- self.visibility_level = None
-
- @Throttle(MIN_TIME_BETWEEN_UPDATES)
- def update(self):
- """Get the latest data from the Aurora service."""
- try:
- self.visibility_level = self.get_aurora_forecast()
- if int(self.visibility_level) > self.threshold:
- self.is_visible = True
- self.is_visible_text = "visible!"
- else:
- self.is_visible = False
- self.is_visible_text = "nothing's out"
-
- except requests.exceptions.HTTPError as error:
- _LOGGER.error("Connection to aurora forecast service failed: %s", error)
- return False
-
- def get_aurora_forecast(self):
- """Get forecast data and parse for given long/lat."""
- raw_data = requests.get(URL, headers=self.headers, timeout=5).text
- # We discard comment rows (#)
- # We split the raw text by line (\n)
- # For each line we trim leading spaces and split by spaces
- forecast_table = [
- row.strip().split()
- for row in raw_data.split("\n")
- if not row.startswith("#")
- ]
-
- # Convert lat and long for data points in table
- # Assumes self.latitude belongs to [-90;90[ (South to North)
- # Assumes self.longitude belongs to [-180;180[ (West to East)
- # No assumptions made regarding the number of rows and columns
- converted_latitude = floor((self.latitude + 90) * len(forecast_table) / 180)
- converted_longitude = floor(
- (self.longitude + 180) * len(forecast_table[converted_latitude]) / 360
- )
-
- return forecast_table[converted_latitude][converted_longitude]
+ @property
+ def device_info(self):
+ """Define the device based on name."""
+ return {
+ ATTR_IDENTIFIERS: {(DOMAIN, self._unique_id)},
+ ATTR_NAME: self.coordinator.name,
+ ATTR_MANUFACTURER: "NOAA",
+ ATTR_MODEL: "Aurora Visibility Sensor",
+ }
diff --git a/homeassistant/components/aurora/config_flow.py b/homeassistant/components/aurora/config_flow.py
new file mode 100644
index 00000000000..37885cc87cf
--- /dev/null
+++ b/homeassistant/components/aurora/config_flow.py
@@ -0,0 +1,110 @@
+"""Config flow for SpaceX Launches and Starman."""
+import logging
+
+from auroranoaa import AuroraForecast
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
+from homeassistant.core import callback
+from homeassistant.helpers import aiohttp_client
+
+from .const import CONF_THRESHOLD, DEFAULT_NAME, DEFAULT_THRESHOLD, DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for NOAA Aurora Integration."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
+ @staticmethod
+ @callback
+ def async_get_options_flow(config_entry):
+ """Get the options flow for this handler."""
+ return OptionsFlowHandler(config_entry)
+
+ async def async_step_user(self, user_input=None):
+ """Handle the initial step."""
+ errors = {}
+
+ if user_input is not None:
+ name = user_input[CONF_NAME]
+ longitude = user_input[CONF_LONGITUDE]
+ latitude = user_input[CONF_LATITUDE]
+
+ session = aiohttp_client.async_get_clientsession(self.hass)
+ api = AuroraForecast(session=session)
+
+ try:
+ await api.get_forecast_data(longitude, latitude)
+ except ConnectionError:
+ errors["base"] = "cannot_connect"
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception")
+ errors["base"] = "unknown"
+ else:
+ await self.async_set_unique_id(
+ f"{DOMAIN}_{user_input[CONF_LONGITUDE]}_{user_input[CONF_LATITUDE]}"
+ )
+ self._abort_if_unique_id_configured()
+ return self.async_create_entry(
+ title=f"Aurora - {name}", data=user_input
+ )
+
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema(
+ {
+ vol.Required(CONF_NAME, default=DEFAULT_NAME): str,
+ vol.Required(
+ CONF_LONGITUDE,
+ default=self.hass.config.longitude,
+ ): vol.All(
+ vol.Coerce(float),
+ vol.Range(min=-180, max=180),
+ ),
+ vol.Required(
+ CONF_LATITUDE,
+ default=self.hass.config.latitude,
+ ): vol.All(
+ vol.Coerce(float),
+ vol.Range(min=-90, max=90),
+ ),
+ }
+ ),
+ errors=errors,
+ )
+
+
+class OptionsFlowHandler(config_entries.OptionsFlow):
+ """Handle options flow changes."""
+
+ def __init__(self, config_entry):
+ """Initialize options flow."""
+ self.config_entry = config_entry
+
+ async def async_step_init(self, user_input=None):
+ """Manage 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.Required(
+ CONF_THRESHOLD,
+ default=self.config_entry.options.get(
+ CONF_THRESHOLD, DEFAULT_THRESHOLD
+ ),
+ ): vol.All(
+ vol.Coerce(int),
+ vol.Range(min=0, max=100),
+ ),
+ }
+ ),
+ )
diff --git a/homeassistant/components/aurora/const.py b/homeassistant/components/aurora/const.py
new file mode 100644
index 00000000000..f4451de863d
--- /dev/null
+++ b/homeassistant/components/aurora/const.py
@@ -0,0 +1,13 @@
+"""Constants for the Aurora integration."""
+
+DOMAIN = "aurora"
+COORDINATOR = "coordinator"
+AURORA_API = "aurora_api"
+ATTR_IDENTIFIERS = "identifiers"
+ATTR_MANUFACTURER = "manufacturer"
+ATTR_MODEL = "model"
+DEFAULT_POLLING_INTERVAL = 5
+CONF_THRESHOLD = "forecast_threshold"
+DEFAULT_THRESHOLD = 75
+ATTRIBUTION = "Data provided by the National Oceanic and Atmospheric Administration"
+DEFAULT_NAME = "Aurora Visibility"
diff --git a/homeassistant/components/aurora/manifest.json b/homeassistant/components/aurora/manifest.json
index 3e7a9359614..8d7d856e50c 100644
--- a/homeassistant/components/aurora/manifest.json
+++ b/homeassistant/components/aurora/manifest.json
@@ -2,5 +2,7 @@
"domain": "aurora",
"name": "Aurora",
"documentation": "https://www.home-assistant.io/integrations/aurora",
- "codeowners": []
+ "config_flow": true,
+ "codeowners": ["@djtimca"],
+ "requirements": ["auroranoaa==0.0.2"]
}
diff --git a/homeassistant/components/aurora/strings.json b/homeassistant/components/aurora/strings.json
new file mode 100644
index 00000000000..31af19748d6
--- /dev/null
+++ b/homeassistant/components/aurora/strings.json
@@ -0,0 +1,26 @@
+{
+ "title": "NOAA Aurora Sensor",
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "name": "[%key:common::config_flow::data::name%]",
+ "longitude": "[%key:common::config_flow::data::longitude%]",
+ "latitude": "[%key:common::config_flow::data::latitude%]"
+ }
+ }
+ },
+ "error": {
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "threshold": "Threshold (%)"
+ }
+ }
+ }
+ }
+ }
\ No newline at end of file
diff --git a/homeassistant/components/aurora/translations/en.json b/homeassistant/components/aurora/translations/en.json
new file mode 100644
index 00000000000..e3e36574608
--- /dev/null
+++ b/homeassistant/components/aurora/translations/en.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Failed to connect"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "Latitude",
+ "longitude": "Longitude",
+ "name": "Name"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "threshold": "Threshold (%)"
+ }
+ }
+ }
+ },
+ "title": "NOAA Aurora Sensor"
+}
\ No newline at end of file
diff --git a/homeassistant/components/aurora/translations/hu.json b/homeassistant/components/aurora/translations/hu.json
new file mode 100644
index 00000000000..d5363860cbd
--- /dev/null
+++ b/homeassistant/components/aurora/translations/hu.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Nem siker\u00fclt csatlakozni"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "Sz\u00e9less\u00e9g",
+ "longitude": "Hossz\u00fas\u00e1g",
+ "name": "N\u00e9v"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "threshold": "K\u00fcsz\u00f6b (%)"
+ }
+ }
+ }
+ },
+ "title": "Nemzeti \u00d3ce\u00e1n- \u00e9s L\u00e9gk\u00f6rkutat\u00e1si Hivatal (NOAA) Aurora \u00e9rz\u00e9kel\u0151"
+}
\ No newline at end of file
diff --git a/homeassistant/components/aurora/translations/it.json b/homeassistant/components/aurora/translations/it.json
new file mode 100644
index 00000000000..4350fbf555a
--- /dev/null
+++ b/homeassistant/components/aurora/translations/it.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Impossibile connettersi"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "Latitudine",
+ "longitude": "Logitudine",
+ "name": "Nome"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "threshold": "Soglia (%)"
+ }
+ }
+ }
+ },
+ "title": "Sensore NOAA Aurora"
+}
\ No newline at end of file
diff --git a/homeassistant/components/aurora/translations/ka.json b/homeassistant/components/aurora/translations/ka.json
new file mode 100644
index 00000000000..f677f54e32e
--- /dev/null
+++ b/homeassistant/components/aurora/translations/ka.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d8 \u10d5\u10d4\u10e0 \u10d3\u10d0\u10db\u10e7\u10d0\u10e0\u10d3\u10d0"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "\u10d2\u10d0\u10dc\u10d4\u10d3\u10d8",
+ "longitude": "\u10d2\u10e0\u10eb\u10d4\u10d3\u10d8",
+ "name": "\u10e1\u10d0\u10ee\u10d4\u10da\u10d8"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "threshold": "\u10d6\u10e6\u10d5\u10d0\u10e0\u10d8 (%)"
+ }
+ }
+ }
+ },
+ "title": "NOAA Aurora \u10e1\u10d4\u10dc\u10e1\u10dd\u10e0\u10d8"
+}
\ No newline at end of file
diff --git a/homeassistant/components/aurora/translations/lb.json b/homeassistant/components/aurora/translations/lb.json
new file mode 100644
index 00000000000..e03a50e0183
--- /dev/null
+++ b/homeassistant/components/aurora/translations/lb.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Feeler beim verbannen"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "L\u00e4ngregraad",
+ "longitude": "Breedegrad",
+ "name": "Numm"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "threshold": "Grenzw\u00e4ert (%)"
+ }
+ }
+ }
+ },
+ "title": "NOAA Aurora Sensor"
+}
\ No newline at end of file
diff --git a/homeassistant/components/aurora/translations/pt.json b/homeassistant/components/aurora/translations/pt.json
new file mode 100644
index 00000000000..aad75b3bed0
--- /dev/null
+++ b/homeassistant/components/aurora/translations/pt.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "Latitude",
+ "longitude": "Longitude",
+ "name": "Nome"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/aurora/translations/sl.json b/homeassistant/components/aurora/translations/sl.json
new file mode 100644
index 00000000000..d4e640e4069
--- /dev/null
+++ b/homeassistant/components/aurora/translations/sl.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "name": "Ime"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/aurora/translations/zh-Hans.json b/homeassistant/components/aurora/translations/zh-Hans.json
new file mode 100644
index 00000000000..e28e3121f38
--- /dev/null
+++ b/homeassistant/components/aurora/translations/zh-Hans.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "name": "\u540d\u79f0"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/aurora/translations/zh-Hant.json b/homeassistant/components/aurora/translations/zh-Hant.json
new file mode 100644
index 00000000000..e1824a7ff4a
--- /dev/null
+++ b/homeassistant/components/aurora/translations/zh-Hant.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "latitude": "\u7def\u5ea6",
+ "longitude": "\u7d93\u5ea6",
+ "name": "\u540d\u7a31"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "threshold": "\u95a5\u503c (%)"
+ }
+ }
+ }
+ },
+ "title": "NOAA Aurora \u50b3\u611f\u5668"
+}
\ No newline at end of file
diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py
index 0989ed43495..e693d2ed814 100644
--- a/homeassistant/components/automation/__init__.py
+++ b/homeassistant/components/automation/__init__.py
@@ -43,40 +43,37 @@ from homeassistant.helpers.script import (
ATTR_MODE,
CONF_MAX,
CONF_MAX_EXCEEDED,
- SCRIPT_MODE_SINGLE,
Script,
- make_script_schema,
)
from homeassistant.helpers.script_variables import ScriptVariables
from homeassistant.helpers.service import async_register_admin_service
-from homeassistant.helpers.singleton import singleton
from homeassistant.helpers.trigger import async_initialize_triggers
from homeassistant.helpers.typing import TemplateVarsType
from homeassistant.loader import bind_hass
from homeassistant.util.dt import parse_datetime
+# Not used except by packages to check config structure
+from .config import PLATFORM_SCHEMA # noqa
+from .config import async_validate_config_item
+from .const import (
+ CONF_ACTION,
+ CONF_CONDITION,
+ CONF_INITIAL_STATE,
+ CONF_TRIGGER,
+ DEFAULT_INITIAL_STATE,
+ DOMAIN,
+ LOGGER,
+)
+from .helpers import async_get_blueprints
+
# mypy: allow-untyped-calls, allow-untyped-defs
# mypy: no-check-untyped-defs, no-warn-return-any
-DOMAIN = "automation"
ENTITY_ID_FORMAT = DOMAIN + ".{}"
-DATA_BLUEPRINTS = "automation_blueprints"
-CONF_DESCRIPTION = "description"
-CONF_HIDE_ENTITY = "hide_entity"
-
-CONF_CONDITION = "condition"
-CONF_ACTION = "action"
-CONF_TRIGGER = "trigger"
-CONF_CONDITION_TYPE = "condition_type"
-CONF_INITIAL_STATE = "initial_state"
CONF_SKIP_CONDITION = "skip_condition"
CONF_STOP_ACTIONS = "stop_actions"
-CONF_BLUEPRINT = "blueprint"
-CONF_INPUT = "input"
-
-DEFAULT_INITIAL_STATE = True
DEFAULT_STOP_ACTIONS = True
EVENT_AUTOMATION_RELOADED = "automation_reloaded"
@@ -87,38 +84,8 @@ ATTR_SOURCE = "source"
ATTR_VARIABLES = "variables"
SERVICE_TRIGGER = "trigger"
-_LOGGER = logging.getLogger(__name__)
-
AutomationActionType = Callable[[HomeAssistant, TemplateVarsType], Awaitable[None]]
-_CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA])
-
-PLATFORM_SCHEMA = vol.All(
- cv.deprecated(CONF_HIDE_ENTITY, invalidation_version="0.110"),
- make_script_schema(
- {
- # str on purpose
- CONF_ID: str,
- CONF_ALIAS: cv.string,
- vol.Optional(CONF_DESCRIPTION): cv.string,
- vol.Optional(CONF_INITIAL_STATE): cv.boolean,
- vol.Optional(CONF_HIDE_ENTITY): cv.boolean,
- vol.Required(CONF_TRIGGER): cv.TRIGGER_SCHEMA,
- vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA,
- vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA,
- vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA,
- },
- SCRIPT_MODE_SINGLE,
- ),
-)
-
-
-@singleton(DATA_BLUEPRINTS)
-@callback
-def async_get_blueprints(hass: HomeAssistant) -> blueprint.DomainBlueprints: # type: ignore
- """Get automation blueprints."""
- return blueprint.DomainBlueprints(hass, DOMAIN, _LOGGER) # type: ignore
-
@bind_hass
def is_on(hass, entity_id):
@@ -194,9 +161,13 @@ def devices_in_automation(hass: HomeAssistant, entity_id: str) -> List[str]:
async def async_setup(hass, config):
"""Set up the automation."""
- hass.data[DOMAIN] = component = EntityComponent(_LOGGER, DOMAIN, hass)
+ hass.data[DOMAIN] = component = EntityComponent(LOGGER, DOMAIN, hass)
- await _async_process_config(hass, config, component)
+ # To register the automation blueprints
+ async_get_blueprints(hass)
+
+ if not await _async_process_config(hass, config, component):
+ await async_get_blueprints(hass).async_populate()
async def trigger_service_handler(entity, service_call):
"""Handle automation triggers."""
@@ -263,7 +234,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
self._is_enabled = False
self._referenced_entities: Optional[Set[str]] = None
self._referenced_devices: Optional[Set[str]] = None
- self._logger = _LOGGER
+ self._logger = LOGGER
self._variables: ScriptVariables = variables
@property
@@ -517,12 +488,13 @@ async def _async_process_config(
hass: HomeAssistant,
config: Dict[str, Any],
component: EntityComponent,
-) -> None:
+) -> bool:
"""Process config and add automations.
- This method is a coroutine.
+ Returns if blueprints were used.
"""
entities = []
+ blueprints_used = False
for config_key in extract_domain_configs(config, DOMAIN):
conf: List[Union[Dict[str, Any], blueprint.BlueprintInputs]] = config[ # type: ignore
@@ -531,15 +503,18 @@ async def _async_process_config(
for list_no, config_block in enumerate(conf):
if isinstance(config_block, blueprint.BlueprintInputs): # type: ignore
+ blueprints_used = True
blueprint_inputs = config_block
try:
config_block = cast(
Dict[str, Any],
- PLATFORM_SCHEMA(blueprint_inputs.async_substitute()),
+ await async_validate_config_item(
+ hass, blueprint_inputs.async_substitute()
+ ),
)
except vol.Invalid as err:
- _LOGGER.error(
+ LOGGER.error(
"Blueprint %s generated invalid automation with inputs %s: %s",
blueprint_inputs.blueprint.name,
blueprint_inputs.inputs,
@@ -561,7 +536,7 @@ async def _async_process_config(
script_mode=config_block[CONF_MODE],
max_runs=config_block[CONF_MAX],
max_exceeded=config_block[CONF_MAX_EXCEEDED],
- logger=_LOGGER,
+ logger=LOGGER,
# We don't pass variables here
# Automation will already render them to use them in the condition
# and so will pass them on to the script.
@@ -590,6 +565,8 @@ async def _async_process_config(
if entities:
await component.async_add_entities(entities)
+ return blueprints_used
+
async def _async_process_if(hass, config, p_config):
"""Process if checks."""
@@ -600,7 +577,7 @@ async def _async_process_if(hass, config, p_config):
try:
checks.append(await condition.async_from_config(hass, if_config, False))
except HomeAssistantError as ex:
- _LOGGER.warning("Invalid condition: %s", ex)
+ LOGGER.warning("Invalid condition: %s", ex)
return None
def if_action(variables=None):
diff --git a/homeassistant/components/automation/blueprints/motion_light.yaml b/homeassistant/components/automation/blueprints/motion_light.yaml
new file mode 100644
index 00000000000..c11d22d974e
--- /dev/null
+++ b/homeassistant/components/automation/blueprints/motion_light.yaml
@@ -0,0 +1,50 @@
+blueprint:
+ name: Motion-activated Light
+ description: Turn on a light when motion is detected.
+ domain: automation
+ source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/motion_light.yaml
+ input:
+ motion_entity:
+ name: Motion Sensor
+ selector:
+ entity:
+ domain: binary_sensor
+ device_class: motion
+ light_target:
+ name: Light
+ selector:
+ target:
+ entity:
+ domain: light
+ no_motion_wait:
+ name: Wait time
+ description: Time to leave the light on after last motion is detected.
+ default: 120
+ selector:
+ number:
+ min: 0
+ max: 3600
+ unit_of_measurement: seconds
+
+# If motion is detected within the delay,
+# we restart the script.
+mode: restart
+max_exceeded: silent
+
+trigger:
+ platform: state
+ entity_id: !input motion_entity
+ from: "off"
+ to: "on"
+
+action:
+ - service: light.turn_on
+ target: !input light_target
+ - wait_for_trigger:
+ platform: state
+ entity_id: !input motion_entity
+ from: "on"
+ to: "off"
+ - delay: !input no_motion_wait
+ - service: light.turn_off
+ target: !input light_target
diff --git a/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml b/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml
new file mode 100644
index 00000000000..d3a70d773ee
--- /dev/null
+++ b/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml
@@ -0,0 +1,43 @@
+blueprint:
+ name: Zone Notification
+ description: Send a notification to a device when a person leaves a specific zone.
+ domain: automation
+ source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml
+ input:
+ person_entity:
+ name: Person
+ selector:
+ entity:
+ domain: person
+ zone_entity:
+ name: Zone
+ selector:
+ entity:
+ domain: zone
+ notify_device:
+ name: Device to notify
+ description: Device needs to run the official Home Assistant app to receive notifications.
+ selector:
+ device:
+ integration: mobile_app
+
+trigger:
+ platform: state
+ entity_id: !input person_entity
+
+variables:
+ zone_entity: !input zone_entity
+ # This is the state of the person when it's in this zone.
+ zone_state: "{{ states[zone_entity].name }}"
+ person_entity: !input person_entity
+ person_name: "{{ states[person_entity].name }}"
+
+condition:
+ condition: template
+ value_template: "{{ trigger.from_state.state == zone_state and trigger.to_state.state != zone_state }}"
+
+action:
+ domain: mobile_app
+ type: notify
+ device_id: !input notify_device
+ message: "{{ person_name }} has left {{ zone_state }}"
diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py
index c5aa8a62a15..9c26f3552aa 100644
--- a/homeassistant/components/automation/config.py
+++ b/homeassistant/components/automation/config.py
@@ -8,25 +8,48 @@ from homeassistant.components.device_automation.exceptions import (
InvalidDeviceAutomationConfig,
)
from homeassistant.config import async_log_exception, config_without_domain
+from homeassistant.const import CONF_ALIAS, CONF_ID, CONF_VARIABLES
from homeassistant.exceptions import HomeAssistantError
-from homeassistant.helpers import config_per_platform
+from homeassistant.helpers import config_per_platform, config_validation as cv, script
from homeassistant.helpers.condition import async_validate_condition_config
-from homeassistant.helpers.script import async_validate_actions_config
from homeassistant.helpers.trigger import async_validate_trigger_config
from homeassistant.loader import IntegrationNotFound
-from . import (
+from .const import (
CONF_ACTION,
CONF_CONDITION,
+ CONF_DESCRIPTION,
+ CONF_HIDE_ENTITY,
+ CONF_INITIAL_STATE,
CONF_TRIGGER,
DOMAIN,
- PLATFORM_SCHEMA,
- async_get_blueprints,
)
+from .helpers import async_get_blueprints
# mypy: allow-untyped-calls, allow-untyped-defs
# mypy: no-check-untyped-defs, no-warn-return-any
+_CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA])
+
+PLATFORM_SCHEMA = vol.All(
+ cv.deprecated(CONF_HIDE_ENTITY),
+ script.make_script_schema(
+ {
+ # str on purpose
+ CONF_ID: str,
+ CONF_ALIAS: cv.string,
+ vol.Optional(CONF_DESCRIPTION): cv.string,
+ vol.Optional(CONF_INITIAL_STATE): cv.boolean,
+ vol.Optional(CONF_HIDE_ENTITY): cv.boolean,
+ vol.Required(CONF_TRIGGER): cv.TRIGGER_SCHEMA,
+ vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA,
+ vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA,
+ vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA,
+ },
+ script.SCRIPT_MODE_SINGLE,
+ ),
+)
+
async def async_validate_config_item(hass, config, full_config=None):
"""Validate config item."""
@@ -48,7 +71,9 @@ async def async_validate_config_item(hass, config, full_config=None):
]
)
- config[CONF_ACTION] = await async_validate_actions_config(hass, config[CONF_ACTION])
+ config[CONF_ACTION] = await script.async_validate_actions_config(
+ hass, config[CONF_ACTION]
+ )
return config
diff --git a/homeassistant/components/automation/const.py b/homeassistant/components/automation/const.py
new file mode 100644
index 00000000000..c8db3aa01e5
--- /dev/null
+++ b/homeassistant/components/automation/const.py
@@ -0,0 +1,19 @@
+"""Constants for the automation integration."""
+import logging
+
+CONF_CONDITION = "condition"
+CONF_ACTION = "action"
+CONF_TRIGGER = "trigger"
+DOMAIN = "automation"
+
+CONF_DESCRIPTION = "description"
+CONF_HIDE_ENTITY = "hide_entity"
+
+CONF_CONDITION_TYPE = "condition_type"
+CONF_INITIAL_STATE = "initial_state"
+CONF_BLUEPRINT = "blueprint"
+CONF_INPUT = "input"
+
+DEFAULT_INITIAL_STATE = True
+
+LOGGER = logging.getLogger(__package__)
diff --git a/homeassistant/components/automation/helpers.py b/homeassistant/components/automation/helpers.py
new file mode 100644
index 00000000000..688f051861e
--- /dev/null
+++ b/homeassistant/components/automation/helpers.py
@@ -0,0 +1,15 @@
+"""Helpers for automation integration."""
+from homeassistant.components import blueprint
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers.singleton import singleton
+
+from .const import DOMAIN, LOGGER
+
+DATA_BLUEPRINTS = "automation_blueprints"
+
+
+@singleton(DATA_BLUEPRINTS)
+@callback
+def async_get_blueprints(hass: HomeAssistant) -> blueprint.DomainBlueprints: # type: ignore
+ """Get automation blueprints."""
+ return blueprint.DomainBlueprints(hass, DOMAIN, LOGGER) # type: ignore
diff --git a/homeassistant/components/awair/manifest.json b/homeassistant/components/awair/manifest.json
index 8ae89951442..f95e1c19d42 100644
--- a/homeassistant/components/awair/manifest.json
+++ b/homeassistant/components/awair/manifest.json
@@ -2,7 +2,7 @@
"domain": "awair",
"name": "Awair",
"documentation": "https://www.home-assistant.io/integrations/awair",
- "requirements": ["python_awair==0.1.1"],
+ "requirements": ["python_awair==0.2.1"],
"codeowners": ["@ahayworth", "@danielsjf"],
"config_flow": true
}
diff --git a/homeassistant/components/awair/translations/et.json b/homeassistant/components/awair/translations/et.json
index ad96a767f2a..374db23e18e 100644
--- a/homeassistant/components/awair/translations/et.json
+++ b/homeassistant/components/awair/translations/et.json
@@ -3,7 +3,7 @@
"abort": {
"already_configured": "Konto on juba seadistatud",
"no_devices_found": "V\u00f5rgust ei leitud Awair seadmeid",
- "reauth_successful": "Taasautentimine \u00f5nnestus"
+ "reauth_successful": "Taastuvastamine \u00f5nnestus"
},
"error": {
"invalid_access_token": "Vigane juurdep\u00e4\u00e4sut\u00f5end",
diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py
index ea1db54855b..8d52b7f8d9f 100644
--- a/homeassistant/components/axis/config_flow.py
+++ b/homeassistant/components/axis/config_flow.py
@@ -5,6 +5,7 @@ from ipaddress import ip_address
import voluptuous as vol
from homeassistant import config_entries
+from homeassistant.config_entries import SOURCE_IGNORE
from homeassistant.const import (
CONF_HOST,
CONF_MAC,
@@ -122,7 +123,7 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN):
same_model = [
entry.data[CONF_NAME]
for entry in self.hass.config_entries.async_entries(AXIS_DOMAIN)
- if entry.data[CONF_MODEL] == model
+ if entry.source != SOURCE_IGNORE and entry.data[CONF_MODEL] == model
]
name = model
diff --git a/homeassistant/components/axis/translations/hu.json b/homeassistant/components/axis/translations/hu.json
index bb33a36195d..659c50e49e7 100644
--- a/homeassistant/components/axis/translations/hu.json
+++ b/homeassistant/components/axis/translations/hu.json
@@ -1,7 +1,8 @@
{
"config": {
"error": {
- "already_configured": "Az eszk\u00f6zt m\u00e1r konfigur\u00e1ltuk"
+ "already_configured": "Az eszk\u00f6zt m\u00e1r konfigur\u00e1ltuk",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
"flow_title": "Axis eszk\u00f6z: {name} ({host})",
"step": {
@@ -14,5 +15,14 @@
}
}
}
+ },
+ "options": {
+ "step": {
+ "configure_stream": {
+ "data": {
+ "stream_profile": "V\u00e1lassza ki a haszn\u00e1lni k\u00edv\u00e1nt adatfolyam-profilt"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/azure_devops/translations/et.json b/homeassistant/components/azure_devops/translations/et.json
index f9f9334ec5c..63ec0276d89 100644
--- a/homeassistant/components/azure_devops/translations/et.json
+++ b/homeassistant/components/azure_devops/translations/et.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "Konto on juba seadistatud",
- "reauth_successful": "Taasautentimine \u00f5nnestus"
+ "reauth_successful": "Taastuvastamine \u00f5nnestus"
},
"error": {
"cannot_connect": "\u00dchendus nurjus",
diff --git a/homeassistant/components/azure_devops/translations/hu.json b/homeassistant/components/azure_devops/translations/hu.json
index 436e8b1fb7d..6bd42409877 100644
--- a/homeassistant/components/azure_devops/translations/hu.json
+++ b/homeassistant/components/azure_devops/translations/hu.json
@@ -2,6 +2,11 @@
"config": {
"abort": {
"already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van"
+ },
+ "step": {
+ "reauth": {
+ "description": "A(z) {project_url} hiteles\u00edt\u00e9se nem siker\u00fclt. K\u00e9rj\u00fck, adja meg jelenlegi hiteles\u00edt\u0151 adatait."
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/azure_devops/translations/ka.json b/homeassistant/components/azure_devops/translations/ka.json
new file mode 100644
index 00000000000..dd48647ed7b
--- /dev/null
+++ b/homeassistant/components/azure_devops/translations/ka.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u10d3\u10d0\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d4\u10d1\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0",
+ "invalid_auth": "\u10db\u10ea\u10d3\u10d0\u10e0\u10d8 \u10d0\u10e3\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/beewi_smartclim/manifest.json b/homeassistant/components/beewi_smartclim/manifest.json
index 169132515d2..29f70b11352 100644
--- a/homeassistant/components/beewi_smartclim/manifest.json
+++ b/homeassistant/components/beewi_smartclim/manifest.json
@@ -2,6 +2,6 @@
"domain": "beewi_smartclim",
"name": "BeeWi SmartClim BLE sensor",
"documentation": "https://www.home-assistant.io/integrations/beewi_smartclim",
- "requirements": ["beewi_smartclim==0.0.7"],
+ "requirements": ["beewi_smartclim==0.0.10"],
"codeowners": ["@alemuro"]
}
diff --git a/homeassistant/components/binary_sensor/translations/ca.json b/homeassistant/components/binary_sensor/translations/ca.json
index 8eaa35284c2..9c92a50246a 100644
--- a/homeassistant/components/binary_sensor/translations/ca.json
+++ b/homeassistant/components/binary_sensor/translations/ca.json
@@ -127,7 +127,7 @@
"on": "Calent"
},
"light": {
- "off": "Sense llum",
+ "off": "No s'ha detectat llum",
"on": "Llum detectada"
},
"lock": {
diff --git a/homeassistant/components/binary_sensor/translations/hu.json b/homeassistant/components/binary_sensor/translations/hu.json
index bb4904f12dc..c4395ca806c 100644
--- a/homeassistant/components/binary_sensor/translations/hu.json
+++ b/homeassistant/components/binary_sensor/translations/hu.json
@@ -98,6 +98,10 @@
"off": "Norm\u00e1l",
"on": "Alacsony"
},
+ "battery_charging": {
+ "off": "Nem t\u00f6lt\u0151dik",
+ "on": "T\u00f6lt\u0151dik"
+ },
"cold": {
"off": "Norm\u00e1l",
"on": "Hideg"
@@ -122,6 +126,10 @@
"off": "Norm\u00e1l",
"on": "Meleg"
},
+ "light": {
+ "off": "Nincs f\u00e9ny",
+ "on": "F\u00e9ny \u00e9szlelve"
+ },
"lock": {
"off": "Bez\u00e1rva",
"on": "Kinyitva"
@@ -134,6 +142,10 @@
"off": "Norm\u00e1l",
"on": "\u00c9szlelve"
},
+ "moving": {
+ "off": "Nincs mozg\u00e1sban",
+ "on": "Mozg\u00e1sban"
+ },
"occupancy": {
"off": "Norm\u00e1l",
"on": "\u00c9szlelve"
@@ -142,6 +154,10 @@
"off": "Z\u00e1rva",
"on": "Nyitva"
},
+ "plug": {
+ "off": "Kih\u00fazva",
+ "on": "Bedugva"
+ },
"presence": {
"off": "T\u00e1vol",
"on": "Otthon"
diff --git a/homeassistant/components/binary_sensor/translations/ka.json b/homeassistant/components/binary_sensor/translations/ka.json
new file mode 100644
index 00000000000..25f9ea4e702
--- /dev/null
+++ b/homeassistant/components/binary_sensor/translations/ka.json
@@ -0,0 +1,20 @@
+{
+ "state": {
+ "battery_charging": {
+ "off": "\u10d0\u10e0 \u10d8\u10e2\u10d4\u10dc\u10d4\u10d1\u10d0",
+ "on": "\u10d8\u10e2\u10d4\u10dc\u10d4\u10d1\u10d0"
+ },
+ "light": {
+ "off": "\u10e1\u10d8\u10dc\u10d0\u10d7\u10da\u10d4 \u1c90\u10e0 \u10d0\u10e0\u10d8\u10e1",
+ "on": "\u10d0\u10e6\u10db\u10dd\u10e9\u10d4\u10dc\u10d8\u10da\u10d8\u10d0 \u10e1\u10d8\u10dc\u10d0\u10d7\u10da\u10d4"
+ },
+ "moving": {
+ "off": "\u10d0\u10e0 \u10db\u10dd\u10eb\u10e0\u10d0\u10dd\u10d1\u10e1",
+ "on": "\u10db\u10dd\u10eb\u10e0\u10d0\u10dd\u10d1\u10d0"
+ },
+ "plug": {
+ "off": "\u10d2\u10d0\u10db\u10dd\u10d4\u10e0\u10d7\u10d4\u10d1\u10e3\u10da\u10d8",
+ "on": "\u1ca8\u10d4\u10d4\u10e0\u10d7\u10d4\u10d1\u10e3\u10da\u10d8"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/binary_sensor/translations/nl.json b/homeassistant/components/binary_sensor/translations/nl.json
index e99c41a473c..dc8ff4ec511 100644
--- a/homeassistant/components/binary_sensor/translations/nl.json
+++ b/homeassistant/components/binary_sensor/translations/nl.json
@@ -122,6 +122,9 @@
"off": "Normaal",
"on": "Heet"
},
+ "light": {
+ "on": "Licht gedetecteerd"
+ },
"lock": {
"off": "Vergrendeld",
"on": "Ontgrendeld"
diff --git a/homeassistant/components/binary_sensor/translations/ru.json b/homeassistant/components/binary_sensor/translations/ru.json
index 6b80fafe5dd..fe9e6773547 100644
--- a/homeassistant/components/binary_sensor/translations/ru.json
+++ b/homeassistant/components/binary_sensor/translations/ru.json
@@ -8,7 +8,7 @@
"is_hot": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043d\u0430\u0433\u0440\u0435\u0432",
"is_light": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0441\u0432\u0435\u0442",
"is_locked": "{entity_name} \u0432 \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438",
- "is_moist": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u043b\u0430\u0433\u0443",
+ "is_moist": "{entity_name} \u0432 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438 \"\u0412\u043b\u0430\u0436\u043d\u043e\"",
"is_motion": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435",
"is_moving": "{entity_name} \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0430\u0435\u0442\u0441\u044f",
"is_no_gas": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0433\u0430\u0437",
@@ -23,7 +23,7 @@
"is_not_connected": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435",
"is_not_hot": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043d\u0430\u0433\u0440\u0435\u0432",
"is_not_locked": "{entity_name} \u0432 \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438",
- "is_not_moist": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u043b\u0430\u0433\u0443",
+ "is_not_moist": "{entity_name} \u0432 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438 \"\u0421\u0443\u0445\u043e\"",
"is_not_moving": "{entity_name} \u043d\u0435 \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0430\u0435\u0442\u0441\u044f",
"is_not_occupied": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435",
"is_not_open": "{entity_name} \u0432 \u0437\u0430\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438",
@@ -52,7 +52,7 @@
"hot": "{entity_name} \u043d\u0430\u0433\u0440\u0435\u0432\u0430\u0435\u0442\u0441\u044f",
"light": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0441\u0432\u0435\u0442",
"locked": "{entity_name} \u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0435\u0442\u0441\u044f",
- "moist": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u043b\u0430\u0433\u0443",
+ "moist": "{entity_name} \u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0441\u044f \u0432\u043b\u0430\u0436\u043d\u044b\u043c",
"motion": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435",
"moving": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0435\u043d\u0438\u0435",
"no_gas": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0433\u0430\u0437",
@@ -67,7 +67,7 @@
"not_connected": "{entity_name} \u043e\u0442\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f",
"not_hot": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043d\u0430\u0433\u0440\u0435\u0432\u0430\u0442\u044c\u0441\u044f",
"not_locked": "{entity_name} \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0435\u0442\u0441\u044f",
- "not_moist": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u043b\u0430\u0433\u0443",
+ "not_moist": "{entity_name} \u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0441\u044f \u0441\u0443\u0445\u0438\u043c",
"not_moving": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0435\u043d\u0438\u0435",
"not_occupied": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435",
"not_opened": "{entity_name} \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f",
@@ -126,6 +126,10 @@
"off": "\u041d\u043e\u0440\u043c\u0430",
"on": "\u041d\u0430\u0433\u0440\u0435\u0432"
},
+ "light": {
+ "off": "\u0412\u044b\u043a\u043b",
+ "on": "\u0412\u043a\u043b"
+ },
"lock": {
"off": "\u0417\u0430\u043a\u0440\u044b\u0442",
"on": "\u041e\u0442\u043a\u0440\u044b\u0442"
@@ -138,6 +142,10 @@
"off": "\u041d\u0435\u0442 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f ",
"on": "\u0414\u0432\u0438\u0436\u0435\u043d\u0438\u0435"
},
+ "moving": {
+ "off": "\u041d\u0435 \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0430\u0435\u0442\u0441\u044f",
+ "on": "\u041f\u0435\u0440\u0435\u043c\u0435\u0449\u0430\u0435\u0442\u0441\u044f"
+ },
"occupancy": {
"off": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e",
"on": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e"
@@ -146,6 +154,10 @@
"off": "\u0417\u0430\u043a\u0440\u044b\u0442\u043e",
"on": "\u041e\u0442\u043a\u0440\u044b\u0442\u043e"
},
+ "plug": {
+ "off": "\u041e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u043e",
+ "on": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e"
+ },
"presence": {
"off": "\u041d\u0435 \u0434\u043e\u043c\u0430",
"on": "\u0414\u043e\u043c\u0430"
diff --git a/homeassistant/components/binary_sensor/translations/sl.json b/homeassistant/components/binary_sensor/translations/sl.json
index a340b62ac99..02c4eedeba9 100644
--- a/homeassistant/components/binary_sensor/translations/sl.json
+++ b/homeassistant/components/binary_sensor/translations/sl.json
@@ -98,6 +98,9 @@
"off": "Normalno",
"on": "Nizko"
},
+ "battery_charging": {
+ "off": "Se ne polni"
+ },
"cold": {
"off": "Normalno",
"on": "Hladno"
@@ -122,6 +125,10 @@
"off": "Normalno",
"on": "Vro\u010de"
},
+ "light": {
+ "off": "Ni lu\u010di",
+ "on": "Zaznana svetloba"
+ },
"lock": {
"off": "Zaklenjeno",
"on": "Odklenjeno"
@@ -134,6 +141,9 @@
"off": "\u010cisto",
"on": "Zaznano"
},
+ "moving": {
+ "on": "Premikanje"
+ },
"occupancy": {
"off": "\u010cisto",
"on": "Zaznano"
@@ -142,6 +152,10 @@
"off": "Zaprto",
"on": "Odprto"
},
+ "plug": {
+ "off": "Odklopljeno",
+ "on": "Priklopljeno"
+ },
"presence": {
"off": "Odsoten",
"on": "Doma"
diff --git a/homeassistant/components/binary_sensor/translations/zh-Hans.json b/homeassistant/components/binary_sensor/translations/zh-Hans.json
index fe16bd685ca..9254f667a48 100644
--- a/homeassistant/components/binary_sensor/translations/zh-Hans.json
+++ b/homeassistant/components/binary_sensor/translations/zh-Hans.json
@@ -71,6 +71,10 @@
"off": "\u6b63\u5e38",
"on": "\u4f4e"
},
+ "battery_charging": {
+ "off": "\u672a\u5728\u5145\u7535",
+ "on": "\u6b63\u5728\u5145\u7535"
+ },
"cold": {
"off": "\u6b63\u5e38",
"on": "\u8fc7\u51b7"
@@ -95,6 +99,10 @@
"off": "\u6b63\u5e38",
"on": "\u8fc7\u70ed"
},
+ "light": {
+ "off": "\u6ca1\u6709\u5149\u7ebf",
+ "on": "\u68c0\u6d4b\u5230\u5149\u7ebf"
+ },
"lock": {
"off": "\u4e0a\u9501",
"on": "\u89e3\u9501"
@@ -107,6 +115,10 @@
"off": "\u672a\u89e6\u53d1",
"on": "\u89e6\u53d1"
},
+ "moving": {
+ "off": "\u9759\u6b62",
+ "on": "\u6b63\u5728\u79fb\u52a8"
+ },
"occupancy": {
"off": "\u672a\u89e6\u53d1",
"on": "\u5df2\u89e6\u53d1"
@@ -115,6 +127,10 @@
"off": "\u5173\u95ed",
"on": "\u5f00\u542f"
},
+ "plug": {
+ "off": "\u5df2\u62d4\u51fa",
+ "on": "\u5df2\u63d2\u5165"
+ },
"presence": {
"off": "\u79bb\u5f00",
"on": "\u5728\u5bb6"
diff --git a/homeassistant/components/blebox/translations/pl.json b/homeassistant/components/blebox/translations/pl.json
index 4cdd35f4ac3..9f61e308f64 100644
--- a/homeassistant/components/blebox/translations/pl.json
+++ b/homeassistant/components/blebox/translations/pl.json
@@ -16,7 +16,7 @@
"host": "Adres IP",
"port": "Port"
},
- "description": "Skonfiguruj BleBox, aby zintegrowa\u0107 si\u0119 z Home Assistant.",
+ "description": "Skonfiguruj BleBox, aby zintegrowa\u0107 si\u0119 z Home Assistantem.",
"title": "Konfiguracja urz\u0105dzenia BleBox"
}
}
diff --git a/homeassistant/components/blebox/translations/sl.json b/homeassistant/components/blebox/translations/sl.json
index f34d8d57a18..194f4b29f96 100644
--- a/homeassistant/components/blebox/translations/sl.json
+++ b/homeassistant/components/blebox/translations/sl.json
@@ -13,6 +13,7 @@
"step": {
"user": {
"data": {
+ "host": "IP naslov",
"port": "Vrata"
},
"description": "Nastavite svoj BleBox za integracijo s Home Assistant.",
diff --git a/homeassistant/components/blink/binary_sensor.py b/homeassistant/components/blink/binary_sensor.py
index 1841dbbc438..f69c94f0f5e 100644
--- a/homeassistant/components/blink/binary_sensor.py
+++ b/homeassistant/components/blink/binary_sensor.py
@@ -44,6 +44,11 @@ class BlinkBinarySensor(BinarySensorEntity):
"""Return the name of the blink sensor."""
return self._name
+ @property
+ def unique_id(self):
+ """Return the unique id of the sensor."""
+ return self._unique_id
+
@property
def device_class(self):
"""Return the class of this device."""
diff --git a/homeassistant/components/blink/manifest.json b/homeassistant/components/blink/manifest.json
index ca3f1f6efee..17d737bcaf3 100644
--- a/homeassistant/components/blink/manifest.json
+++ b/homeassistant/components/blink/manifest.json
@@ -2,7 +2,7 @@
"domain": "blink",
"name": "Blink",
"documentation": "https://www.home-assistant.io/integrations/blink",
- "requirements": ["blinkpy==0.16.3"],
+ "requirements": ["blinkpy==0.16.4"],
"codeowners": ["@fronzbot"],
"config_flow": true
}
diff --git a/homeassistant/components/blink/translations/sl.json b/homeassistant/components/blink/translations/sl.json
new file mode 100644
index 00000000000..118bd7b6a61
--- /dev/null
+++ b/homeassistant/components/blink/translations/sl.json
@@ -0,0 +1,9 @@
+{
+ "config": {
+ "step": {
+ "2fa": {
+ "description": "Vnesite pin, poslan na va\u0161 e-po\u0161tni naslov"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/blueprint/__init__.py b/homeassistant/components/blueprint/__init__.py
index 12a6782065b..9e8b1260eff 100644
--- a/homeassistant/components/blueprint/__init__.py
+++ b/homeassistant/components/blueprint/__init__.py
@@ -7,7 +7,7 @@ from .errors import ( # noqa
FailedToLoad,
InvalidBlueprint,
InvalidBlueprintInputs,
- MissingPlaceholder,
+ MissingInput,
)
from .models import Blueprint, BlueprintInputs, DomainBlueprints # noqa
from .schemas import is_blueprint_instance_config # noqa
diff --git a/homeassistant/components/blueprint/const.py b/homeassistant/components/blueprint/const.py
index fe4ee5b7ce6..60df20dda36 100644
--- a/homeassistant/components/blueprint/const.py
+++ b/homeassistant/components/blueprint/const.py
@@ -5,5 +5,8 @@ CONF_BLUEPRINT = "blueprint"
CONF_USE_BLUEPRINT = "use_blueprint"
CONF_INPUT = "input"
CONF_SOURCE_URL = "source_url"
+CONF_DESCRIPTION = "description"
+CONF_HOMEASSISTANT = "homeassistant"
+CONF_MIN_VERSION = "min_version"
DOMAIN = "blueprint"
diff --git a/homeassistant/components/blueprint/errors.py b/homeassistant/components/blueprint/errors.py
index dff65b5263d..b422b2dcbe3 100644
--- a/homeassistant/components/blueprint/errors.py
+++ b/homeassistant/components/blueprint/errors.py
@@ -66,15 +66,23 @@ class InvalidBlueprintInputs(BlueprintException):
)
-class MissingPlaceholder(BlueprintWithNameException):
- """When we miss a placeholder."""
+class MissingInput(BlueprintWithNameException):
+ """When we miss an input."""
def __init__(
- self, domain: str, blueprint_name: str, placeholder_names: Iterable[str]
+ self, domain: str, blueprint_name: str, input_names: Iterable[str]
) -> None:
"""Initialize blueprint exception."""
super().__init__(
domain,
blueprint_name,
- f"Missing placeholder {', '.join(sorted(placeholder_names))}",
+ f"Missing input {', '.join(sorted(input_names))}",
)
+
+
+class FileAlreadyExists(BlueprintWithNameException):
+ """Error when file already exists."""
+
+ def __init__(self, domain: str, blueprint_name: str) -> None:
+ """Initialize blueprint exception."""
+ super().__init__(domain, blueprint_name, "Blueprint already exists")
diff --git a/homeassistant/components/blueprint/importer.py b/homeassistant/components/blueprint/importer.py
index 0f229ec8a07..217851df980 100644
--- a/homeassistant/components/blueprint/importer.py
+++ b/homeassistant/components/blueprint/importer.py
@@ -1,5 +1,6 @@
"""Import logic for blueprint."""
from dataclasses import dataclass
+import html
import re
from typing import Optional
@@ -25,7 +26,6 @@ COMMUNITY_CODE_BLOCK = re.compile(
GITHUB_FILE_PATTERN = re.compile(
r"^https://github.com/(?P.+)/blob/(?P.+)$"
)
-GITHUB_RAW_FILE_PATTERN = re.compile(r"^https://raw.githubusercontent.com/")
COMMUNITY_TOPIC_SCHEMA = vol.Schema(
{
@@ -37,11 +37,14 @@ COMMUNITY_TOPIC_SCHEMA = vol.Schema(
)
+class UnsupportedUrl(HomeAssistantError):
+ """When the function doesn't support the url."""
+
+
@dataclass(frozen=True)
class ImportedBlueprint:
"""Imported blueprint."""
- url: str
suggested_filename: str
raw_data: str
blueprint: Blueprint
@@ -52,14 +55,13 @@ def _get_github_import_url(url: str) -> str:
Async friendly.
"""
- match = GITHUB_RAW_FILE_PATTERN.match(url)
- if match is not None:
+ if url.startswith("https://raw.githubusercontent.com/"):
return url
match = GITHUB_FILE_PATTERN.match(url)
if match is None:
- raise ValueError("Not a GitHub file url")
+ raise UnsupportedUrl("Not a GitHub file url")
repo, path = match.groups()
@@ -73,7 +75,7 @@ def _get_community_post_import_url(url: str) -> str:
"""
match = COMMUNITY_TOPIC_PATTERN.match(url)
if match is None:
- raise ValueError("Not a topic url")
+ raise UnsupportedUrl("Not a topic url")
_topic, post = match.groups()
@@ -106,7 +108,7 @@ def _extract_blueprint_from_community_topic(
if block_syntax not in ("auto", "yaml"):
continue
- block_content = block_content.strip()
+ block_content = html.unescape(block_content.strip())
try:
data = yaml.parse_yaml(block_content)
@@ -123,9 +125,13 @@ def _extract_blueprint_from_community_topic(
break
if blueprint is None:
- return None
+ raise HomeAssistantError(
+ "No valid blueprint found in the topic. Blueprint syntax blocks need to be marked as YAML or no syntax."
+ )
- return ImportedBlueprint(url, topic["slug"], block_content, blueprint)
+ return ImportedBlueprint(
+ f'{post["username"]}/{topic["slug"]}', block_content, blueprint
+ )
async def fetch_blueprint_from_community_post(
@@ -159,19 +165,69 @@ async def fetch_blueprint_from_github_url(
blueprint = Blueprint(data)
parsed_import_url = yarl.URL(import_url)
- suggested_filename = f"{parsed_import_url.parts[1]}-{parsed_import_url.parts[-1]}"
+ suggested_filename = f"{parsed_import_url.parts[1]}/{parsed_import_url.parts[-1]}"
if suggested_filename.endswith(".yaml"):
suggested_filename = suggested_filename[:-5]
- return ImportedBlueprint(url, suggested_filename, raw_yaml, blueprint)
+ return ImportedBlueprint(suggested_filename, raw_yaml, blueprint)
+
+
+async def fetch_blueprint_from_github_gist_url(
+ hass: HomeAssistant, url: str
+) -> ImportedBlueprint:
+ """Get a blueprint from a Github Gist."""
+ if not url.startswith("https://gist.github.com/"):
+ raise UnsupportedUrl("Not a GitHub gist url")
+
+ parsed_url = yarl.URL(url)
+ session = aiohttp_client.async_get_clientsession(hass)
+
+ resp = await session.get(
+ f"https://api.github.com/gists/{parsed_url.parts[2]}",
+ headers={"Accept": "application/vnd.github.v3+json"},
+ raise_for_status=True,
+ )
+ gist = await resp.json()
+
+ blueprint = None
+ filename = None
+ content = None
+
+ for filename, info in gist["files"].items():
+ if not filename.endswith(".yaml"):
+ continue
+
+ content = info["content"]
+ data = yaml.parse_yaml(content)
+
+ if not is_blueprint_config(data):
+ continue
+
+ blueprint = Blueprint(data)
+ break
+
+ if blueprint is None:
+ raise HomeAssistantError(
+ "No valid blueprint found in the gist. The blueprint file needs to end with '.yaml'"
+ )
+
+ return ImportedBlueprint(
+ f"{gist['owner']['login']}/{filename[:-5]}", content, blueprint
+ )
async def fetch_blueprint_from_url(hass: HomeAssistant, url: str) -> ImportedBlueprint:
"""Get a blueprint from a url."""
- for func in (fetch_blueprint_from_community_post, fetch_blueprint_from_github_url):
+ for func in (
+ fetch_blueprint_from_community_post,
+ fetch_blueprint_from_github_url,
+ fetch_blueprint_from_github_gist_url,
+ ):
try:
- return await func(hass, url)
- except ValueError:
+ imported_bp = await func(hass, url)
+ imported_bp.blueprint.update_metadata(source_url=url)
+ return imported_bp
+ except UnsupportedUrl:
pass
raise HomeAssistantError("Unsupported url")
diff --git a/homeassistant/components/blueprint/models.py b/homeassistant/components/blueprint/models.py
index 06401a34d7d..32fc30b60b9 100644
--- a/homeassistant/components/blueprint/models.py
+++ b/homeassistant/components/blueprint/models.py
@@ -2,21 +2,31 @@
import asyncio
import logging
import pathlib
-from typing import Any, Dict, Optional, Union
+import shutil
+from typing import Any, Dict, List, Optional, Union
+from pkg_resources import parse_version
import voluptuous as vol
from voluptuous.humanize import humanize_error
-from homeassistant.const import CONF_DOMAIN, CONF_NAME, CONF_PATH
-from homeassistant.core import HomeAssistant, callback
+from homeassistant import loader
+from homeassistant.const import (
+ CONF_DEFAULT,
+ CONF_DOMAIN,
+ CONF_NAME,
+ CONF_PATH,
+ __version__,
+)
+from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
-from homeassistant.helpers import placeholder
from homeassistant.util import yaml
from .const import (
BLUEPRINT_FOLDER,
CONF_BLUEPRINT,
+ CONF_HOMEASSISTANT,
CONF_INPUT,
+ CONF_MIN_VERSION,
CONF_SOURCE_URL,
CONF_USE_BLUEPRINT,
DOMAIN,
@@ -24,9 +34,10 @@ from .const import (
from .errors import (
BlueprintException,
FailedToLoad,
+ FileAlreadyExists,
InvalidBlueprint,
InvalidBlueprintInputs,
- MissingPlaceholder,
+ MissingInput,
)
from .schemas import BLUEPRINT_INSTANCE_FIELDS, BLUEPRINT_SCHEMA
@@ -47,8 +58,6 @@ class Blueprint:
except vol.Invalid as err:
raise InvalidBlueprint(expected_domain, path, data, err) from err
- self.placeholders = placeholder.extract_placeholders(data)
-
# In future, we will treat this as "incorrect" and allow to recover from this
data_domain = data[CONF_BLUEPRINT][CONF_DOMAIN]
if expected_domain is not None and data_domain != expected_domain:
@@ -61,7 +70,7 @@ class Blueprint:
self.domain = data_domain
- missing = self.placeholders - set(data[CONF_BLUEPRINT].get(CONF_INPUT, {}))
+ missing = yaml.extract_inputs(data) - set(data[CONF_BLUEPRINT][CONF_INPUT])
if missing:
raise InvalidBlueprint(
@@ -76,6 +85,11 @@ class Blueprint:
"""Return blueprint name."""
return self.data[CONF_BLUEPRINT][CONF_NAME]
+ @property
+ def inputs(self) -> dict:
+ """Return blueprint inputs."""
+ return self.data[CONF_BLUEPRINT][CONF_INPUT]
+
@property
def metadata(self) -> dict:
"""Return blueprint metadata."""
@@ -86,6 +100,27 @@ class Blueprint:
if source_url is not None:
self.data[CONF_BLUEPRINT][CONF_SOURCE_URL] = source_url
+ def yaml(self) -> str:
+ """Dump blueprint as YAML."""
+ return yaml.dump(self.data)
+
+ @callback
+ def validate(self) -> Optional[List[str]]:
+ """Test if the Home Assistant installation supports this blueprint.
+
+ Return list of errors if not valid.
+ """
+ errors = []
+ metadata = self.metadata
+ min_version = metadata.get(CONF_HOMEASSISTANT, {}).get(CONF_MIN_VERSION)
+
+ if min_version is not None and parse_version(__version__) < parse_version(
+ min_version
+ ):
+ errors.append(f"Requires at least Home Assistant {min_version}")
+
+ return errors or None
+
class BlueprintInputs:
"""Inputs for a blueprint."""
@@ -102,14 +137,26 @@ class BlueprintInputs:
"""Return the inputs."""
return self.config_with_inputs[CONF_USE_BLUEPRINT][CONF_INPUT]
+ @property
+ def inputs_with_default(self):
+ """Return the inputs and fallback to defaults."""
+ no_input = set(self.blueprint.inputs) - set(self.inputs)
+
+ inputs_with_default = dict(self.inputs)
+
+ for inp in no_input:
+ blueprint_input = self.blueprint.inputs[inp]
+ if isinstance(blueprint_input, dict) and CONF_DEFAULT in blueprint_input:
+ inputs_with_default[inp] = blueprint_input[CONF_DEFAULT]
+
+ return inputs_with_default
+
def validate(self) -> None:
"""Validate the inputs."""
- missing = self.blueprint.placeholders - set(self.inputs)
+ missing = set(self.blueprint.inputs) - set(self.inputs_with_default)
if missing:
- raise MissingPlaceholder(
- self.blueprint.domain, self.blueprint.name, missing
- )
+ raise MissingInput(self.blueprint.domain, self.blueprint.name, missing)
# In future we can see if entities are correct domain, areas exist etc
# using the new selector helper.
@@ -117,8 +164,8 @@ class BlueprintInputs:
@callback
def async_substitute(self) -> dict:
"""Get the blueprint value with the inputs substituted."""
- processed = placeholder.substitute(self.blueprint.data, self.inputs)
- combined = {**self.config_with_inputs, **processed}
+ processed = yaml.substitute(self.blueprint.data, self.inputs_with_default)
+ combined = {**processed, **self.config_with_inputs}
# From config_with_inputs
combined.pop(CONF_USE_BLUEPRINT)
# From blueprint
@@ -144,6 +191,11 @@ class DomainBlueprints:
hass.data.setdefault(DOMAIN, {})[domain] = self
+ @property
+ def blueprint_folder(self) -> pathlib.Path:
+ """Return the blueprint folder."""
+ return pathlib.Path(self.hass.config.path(BLUEPRINT_FOLDER, self.domain))
+
@callback
def async_reset_cache(self) -> None:
"""Reset the blueprint cache."""
@@ -152,10 +204,14 @@ class DomainBlueprints:
def _load_blueprint(self, blueprint_path) -> Blueprint:
"""Load a blueprint."""
try:
- blueprint_data = yaml.load_yaml(
- self.hass.config.path(BLUEPRINT_FOLDER, self.domain, blueprint_path)
- )
- except (HomeAssistantError, FileNotFoundError) as err:
+ blueprint_data = yaml.load_yaml(self.blueprint_folder / blueprint_path)
+ except FileNotFoundError as err:
+ raise FailedToLoad(
+ self.domain,
+ blueprint_path,
+ FileNotFoundError(f"Unable to find {blueprint_path}"),
+ ) from err
+ except HomeAssistantError as err:
raise FailedToLoad(self.domain, blueprint_path, err) from err
return Blueprint(
@@ -194,13 +250,25 @@ class DomainBlueprints:
async def async_get_blueprint(self, blueprint_path: str) -> Blueprint:
"""Get a blueprint."""
+
+ def load_from_cache():
+ """Load blueprint from cache."""
+ blueprint = self._blueprints[blueprint_path]
+ if blueprint is None:
+ raise FailedToLoad(
+ self.domain,
+ blueprint_path,
+ FileNotFoundError(f"Unable to find {blueprint_path}"),
+ )
+ return blueprint
+
if blueprint_path in self._blueprints:
- return self._blueprints[blueprint_path]
+ return load_from_cache()
async with self._load_lock:
# Check it again
if blueprint_path in self._blueprints:
- return self._blueprints[blueprint_path]
+ return load_from_cache()
try:
blueprint = await self.hass.async_add_executor_job(
@@ -229,3 +297,49 @@ class DomainBlueprints:
inputs = BlueprintInputs(blueprint, config_with_blueprint)
inputs.validate()
return inputs
+
+ async def async_remove_blueprint(self, blueprint_path: str) -> None:
+ """Remove a blueprint file."""
+ path = self.blueprint_folder / blueprint_path
+ await self.hass.async_add_executor_job(path.unlink)
+ self._blueprints[blueprint_path] = None
+
+ def _create_file(self, blueprint: Blueprint, blueprint_path: str) -> None:
+ """Create blueprint file."""
+
+ path = pathlib.Path(
+ self.hass.config.path(BLUEPRINT_FOLDER, self.domain, blueprint_path)
+ )
+ if path.exists():
+ raise FileAlreadyExists(self.domain, blueprint_path)
+
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(blueprint.yaml())
+
+ async def async_add_blueprint(
+ self, blueprint: Blueprint, blueprint_path: str
+ ) -> None:
+ """Add a blueprint."""
+ if not blueprint_path.endswith(".yaml"):
+ blueprint_path = f"{blueprint_path}.yaml"
+
+ await self.hass.async_add_executor_job(
+ self._create_file, blueprint, blueprint_path
+ )
+
+ self._blueprints[blueprint_path] = blueprint
+
+ async def async_populate(self) -> None:
+ """Create folder if it doesn't exist and populate with examples."""
+ integration = await loader.async_get_integration(self.hass, self.domain)
+
+ def populate():
+ if self.blueprint_folder.exists():
+ return
+
+ shutil.copytree(
+ integration.file_path / BLUEPRINT_FOLDER,
+ self.blueprint_folder / HA_DOMAIN,
+ )
+
+ await self.hass.async_add_executor_job(populate)
diff --git a/homeassistant/components/blueprint/schemas.py b/homeassistant/components/blueprint/schemas.py
index 275a2cf242a..07d8e8b0128 100644
--- a/homeassistant/components/blueprint/schemas.py
+++ b/homeassistant/components/blueprint/schemas.py
@@ -3,11 +3,45 @@ from typing import Any
import voluptuous as vol
-from homeassistant.const import CONF_DOMAIN, CONF_NAME, CONF_PATH
+from homeassistant.const import (
+ CONF_DEFAULT,
+ CONF_DOMAIN,
+ CONF_NAME,
+ CONF_PATH,
+ CONF_SELECTOR,
+)
from homeassistant.core import callback
-from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers import config_validation as cv, selector
-from .const import CONF_BLUEPRINT, CONF_INPUT, CONF_USE_BLUEPRINT
+from .const import (
+ CONF_BLUEPRINT,
+ CONF_DESCRIPTION,
+ CONF_HOMEASSISTANT,
+ CONF_INPUT,
+ CONF_MIN_VERSION,
+ CONF_SOURCE_URL,
+ CONF_USE_BLUEPRINT,
+)
+
+
+def version_validator(value):
+ """Validate a Home Assistant version."""
+ if not isinstance(value, str):
+ raise vol.Invalid("Version needs to be a string")
+
+ parts = value.split(".")
+
+ if len(parts) != 3:
+ raise vol.Invalid("Version needs to be formatted as {major}.{minor}.{patch}")
+
+ try:
+ parts = [int(p) for p in parts]
+ except ValueError:
+ raise vol.Invalid(
+ "Major, minor and patch version needs to be an integer"
+ ) from None
+
+ return value
@callback
@@ -22,14 +56,32 @@ def is_blueprint_instance_config(config: Any) -> bool:
return isinstance(config, dict) and CONF_USE_BLUEPRINT in config
+BLUEPRINT_INPUT_SCHEMA = vol.Schema(
+ {
+ vol.Optional(CONF_NAME): str,
+ vol.Optional(CONF_DESCRIPTION): str,
+ vol.Optional(CONF_DEFAULT): cv.match_all,
+ vol.Optional(CONF_SELECTOR): selector.validate_selector,
+ }
+)
+
BLUEPRINT_SCHEMA = vol.Schema(
{
- # No definition yet for the inputs.
vol.Required(CONF_BLUEPRINT): vol.Schema(
{
vol.Required(CONF_NAME): str,
+ vol.Optional(CONF_DESCRIPTION): str,
vol.Required(CONF_DOMAIN): str,
- vol.Optional(CONF_INPUT, default=dict): {str: None},
+ vol.Optional(CONF_SOURCE_URL): cv.url,
+ vol.Optional(CONF_HOMEASSISTANT): {
+ vol.Optional(CONF_MIN_VERSION): version_validator
+ },
+ vol.Optional(CONF_INPUT, default=dict): {
+ str: vol.Any(
+ None,
+ BLUEPRINT_INPUT_SCHEMA,
+ )
+ },
}
),
},
@@ -49,7 +101,7 @@ BLUEPRINT_INSTANCE_FIELDS = vol.Schema(
vol.Required(CONF_USE_BLUEPRINT): vol.Schema(
{
vol.Required(CONF_PATH): vol.All(cv.path, validate_yaml_suffix),
- vol.Required(CONF_INPUT): {str: cv.match_all},
+ vol.Required(CONF_INPUT, default=dict): {str: cv.match_all},
}
)
},
diff --git a/homeassistant/components/blueprint/websocket_api.py b/homeassistant/components/blueprint/websocket_api.py
index 51bec0eb2a0..6968d4530cd 100644
--- a/homeassistant/components/blueprint/websocket_api.py
+++ b/homeassistant/components/blueprint/websocket_api.py
@@ -1,5 +1,4 @@
"""Websocket API for blueprint."""
-import asyncio
import logging
from typing import Dict, Optional
@@ -8,10 +7,13 @@ import voluptuous as vol
from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant, callback
+from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
+from homeassistant.util import yaml
from . import importer, models
from .const import DOMAIN
+from .errors import FileAlreadyExists
_LOGGER = logging.getLogger(__package__)
@@ -21,12 +23,15 @@ def async_setup(hass: HomeAssistant):
"""Set up the websocket API."""
websocket_api.async_register_command(hass, ws_list_blueprints)
websocket_api.async_register_command(hass, ws_import_blueprint)
+ websocket_api.async_register_command(hass, ws_save_blueprint)
+ websocket_api.async_register_command(hass, ws_delete_blueprint)
@websocket_api.async_response
@websocket_api.websocket_command(
{
vol.Required("type"): "blueprint/list",
+ vol.Required("domain"): cv.string,
}
)
async def ws_list_blueprints(hass, connection, msg):
@@ -36,21 +41,19 @@ async def ws_list_blueprints(hass, connection, msg):
)
results = {}
- for domain, domain_results in zip(
- domain_blueprints,
- await asyncio.gather(
- *[db.async_get_blueprints() for db in domain_blueprints.values()]
- ),
- ):
- for path, value in domain_results.items():
- if isinstance(value, models.Blueprint):
- domain_results[path] = {
- "metadata": value.metadata,
- }
- else:
- domain_results[path] = {"error": str(value)}
+ if msg["domain"] not in domain_blueprints:
+ connection.send_result(msg["id"], results)
+ return
- results[domain] = domain_results
+ domain_results = await domain_blueprints[msg["domain"]].async_get_blueprints()
+
+ for path, value in domain_results.items():
+ if isinstance(value, models.Blueprint):
+ results[path] = {
+ "metadata": value.metadata,
+ }
+ else:
+ results[path] = {"error": str(value)}
connection.send_result(msg["id"], results)
@@ -76,11 +79,94 @@ async def ws_import_blueprint(hass, connection, msg):
connection.send_result(
msg["id"],
{
- "url": imported_blueprint.url,
"suggested_filename": imported_blueprint.suggested_filename,
"raw_data": imported_blueprint.raw_data,
"blueprint": {
"metadata": imported_blueprint.blueprint.metadata,
},
+ "validation_errors": imported_blueprint.blueprint.validate(),
},
)
+
+
+@websocket_api.async_response
+@websocket_api.websocket_command(
+ {
+ vol.Required("type"): "blueprint/save",
+ vol.Required("domain"): cv.string,
+ vol.Required("path"): cv.path,
+ vol.Required("yaml"): cv.string,
+ vol.Optional("source_url"): cv.url,
+ }
+)
+async def ws_save_blueprint(hass, connection, msg):
+ """Save a blueprint."""
+
+ path = msg["path"]
+ domain = msg["domain"]
+
+ domain_blueprints: Optional[Dict[str, models.DomainBlueprints]] = hass.data.get(
+ DOMAIN, {}
+ )
+
+ if domain not in domain_blueprints:
+ connection.send_error(
+ msg["id"], websocket_api.ERR_INVALID_FORMAT, "Unsupported domain"
+ )
+
+ try:
+ blueprint = models.Blueprint(
+ yaml.parse_yaml(msg["yaml"]), expected_domain=domain
+ )
+ if "source_url" in msg:
+ blueprint.update_metadata(source_url=msg["source_url"])
+ except HomeAssistantError as err:
+ connection.send_error(msg["id"], websocket_api.ERR_INVALID_FORMAT, str(err))
+ return
+
+ try:
+ await domain_blueprints[domain].async_add_blueprint(blueprint, path)
+ except FileAlreadyExists:
+ connection.send_error(msg["id"], "already_exists", "File already exists")
+ return
+ except OSError as err:
+ connection.send_error(msg["id"], websocket_api.ERR_UNKNOWN_ERROR, str(err))
+ return
+
+ connection.send_result(
+ msg["id"],
+ )
+
+
+@websocket_api.async_response
+@websocket_api.websocket_command(
+ {
+ vol.Required("type"): "blueprint/delete",
+ vol.Required("domain"): cv.string,
+ vol.Required("path"): cv.path,
+ }
+)
+async def ws_delete_blueprint(hass, connection, msg):
+ """Delete a blueprint."""
+
+ path = msg["path"]
+ domain = msg["domain"]
+
+ domain_blueprints: Optional[Dict[str, models.DomainBlueprints]] = hass.data.get(
+ DOMAIN, {}
+ )
+
+ if domain not in domain_blueprints:
+ connection.send_error(
+ msg["id"], websocket_api.ERR_INVALID_FORMAT, "Unsupported domain"
+ )
+
+ try:
+ await domain_blueprints[domain].async_remove_blueprint(path)
+ except OSError as err:
+ connection.send_error(msg["id"], websocket_api.ERR_UNKNOWN_ERROR, str(err))
+ return
+
+ connection.send_result(
+ msg["id"],
+ )
diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py
index 995a6a6ef86..4668b1da6eb 100644
--- a/homeassistant/components/bmw_connected_drive/sensor.py
+++ b/homeassistant/components/bmw_connected_drive/sensor.py
@@ -125,7 +125,7 @@ class BMWConnectedDriveSensor(Entity):
@property
def unit_of_measurement(self) -> str:
"""Get the unit of measurement."""
- _, unit = self._attribute_info.get(self._attribute, [None, None])
+ unit = self._attribute_info.get(self._attribute, [None, None])[1]
return unit
@property
diff --git a/homeassistant/components/braviatv/translations/hu.json b/homeassistant/components/braviatv/translations/hu.json
index cbf055e2fba..a87786df1e8 100644
--- a/homeassistant/components/braviatv/translations/hu.json
+++ b/homeassistant/components/braviatv/translations/hu.json
@@ -1,6 +1,11 @@
{
"config": {
"step": {
+ "authorize": {
+ "data": {
+ "pin": "PIN k\u00f3d"
+ }
+ },
"user": {
"data": {
"host": "Hoszt"
diff --git a/homeassistant/components/braviatv/translations/pl.json b/homeassistant/components/braviatv/translations/pl.json
index 0088cf604ad..1aa5d7cb58a 100644
--- a/homeassistant/components/braviatv/translations/pl.json
+++ b/homeassistant/components/braviatv/translations/pl.json
@@ -14,7 +14,7 @@
"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 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": {
diff --git a/homeassistant/components/broadlink/config_flow.py b/homeassistant/components/broadlink/config_flow.py
index e1a294d746d..a2e770d6c4f 100644
--- a/homeassistant/components/broadlink/config_flow.py
+++ b/homeassistant/components/broadlink/config_flow.py
@@ -41,8 +41,8 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Define a device for the config flow."""
supported_types = {
device_type
- for _, device_types in DOMAINS_AND_TYPES
- for device_type in device_types
+ for device_types in DOMAINS_AND_TYPES
+ for device_type in device_types[1]
}
if device.type not in supported_types:
_LOGGER.error(
diff --git a/homeassistant/components/broadlink/const.py b/homeassistant/components/broadlink/const.py
index adb5a437ae5..b10f7e74ba7 100644
--- a/homeassistant/components/broadlink/const.py
+++ b/homeassistant/components/broadlink/const.py
@@ -8,7 +8,7 @@ DOMAIN = "broadlink"
DOMAINS_AND_TYPES = (
(REMOTE_DOMAIN, ("RM2", "RM4")),
(SENSOR_DOMAIN, ("A1", "RM2", "RM4")),
- (SWITCH_DOMAIN, ("MP1", "RM2", "RM4", "SP1", "SP2", "SP4", "SP4B")),
+ (SWITCH_DOMAIN, ("BG1", "MP1", "RM2", "RM4", "SP1", "SP2", "SP4", "SP4B")),
)
DEFAULT_PORT = 80
diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py
index cd86244cea8..5d0d618d7be 100644
--- a/homeassistant/components/broadlink/remote.py
+++ b/homeassistant/components/broadlink/remote.py
@@ -18,6 +18,7 @@ import voluptuous as vol
from homeassistant.components.remote import (
ATTR_ALTERNATIVE,
ATTR_COMMAND,
+ ATTR_COMMAND_TYPE,
ATTR_DELAY_SECS,
ATTR_DEVICE,
ATTR_NUM_REPEATS,
@@ -41,6 +42,10 @@ _LOGGER = logging.getLogger(__name__)
LEARNING_TIMEOUT = timedelta(seconds=30)
+COMMAND_TYPE_IR = "ir"
+COMMAND_TYPE_RF = "rf"
+COMMAND_TYPES = [COMMAND_TYPE_IR, COMMAND_TYPE_RF]
+
CODE_STORAGE_VERSION = 1
FLAG_STORAGE_VERSION = 1
FLAG_SAVE_DELAY = 15
@@ -64,6 +69,7 @@ SERVICE_SEND_SCHEMA = COMMAND_SCHEMA.extend(
SERVICE_LEARN_SCHEMA = COMMAND_SCHEMA.extend(
{
vol.Required(ATTR_DEVICE): vol.All(cv.string, vol.Length(min=1)),
+ vol.Optional(ATTR_COMMAND_TYPE, default=COMMAND_TYPE_IR): vol.In(COMMAND_TYPES),
vol.Optional(ATTR_ALTERNATIVE, default=False): cv.boolean,
}
)
@@ -266,11 +272,11 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity):
await self._device.async_request(self._device.api.send_data, code)
except (AuthorizationError, NetworkTimeoutError, OSError) as err:
- _LOGGER.error("Failed to send '%s': %s", command, err)
+ _LOGGER.error("Failed to send '%s': %s", cmd, err)
break
except BroadlinkException as err:
- _LOGGER.error("Failed to send '%s': %s", command, err)
+ _LOGGER.error("Failed to send '%s': %s", cmd, err)
should_delay = False
continue
@@ -284,6 +290,7 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity):
"""Learn a list of commands from a remote."""
kwargs = SERVICE_LEARN_SCHEMA(kwargs)
commands = kwargs[ATTR_COMMAND]
+ command_type = kwargs[ATTR_COMMAND_TYPE]
device = kwargs[ATTR_DEVICE]
toggle = kwargs[ATTR_ALTERNATIVE]
@@ -293,13 +300,18 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity):
)
return
+ if command_type == COMMAND_TYPE_IR:
+ learn_command = self._async_learn_ir_command
+ else:
+ learn_command = self._async_learn_rf_command
+
should_store = False
for command in commands:
try:
- code = await self._async_learn_command(command)
+ code = await learn_command(command)
if toggle:
- code = [code, await self._async_learn_command(command)]
+ code = [code, await learn_command(command)]
except (AuthorizationError, NetworkTimeoutError, OSError) as err:
_LOGGER.error("Failed to learn '%s': %s", command, err)
@@ -315,8 +327,8 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity):
if should_store:
await self._code_storage.async_save(self._codes)
- async def _async_learn_command(self, command):
- """Learn a command from a remote."""
+ async def _async_learn_ir_command(self, command):
+ """Learn an infrared command."""
try:
await self._device.async_request(self._device.api.enter_learning)
@@ -336,12 +348,87 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity):
await asyncio.sleep(1)
try:
code = await self._device.async_request(self._device.api.check_data)
-
except (ReadError, StorageError):
continue
-
return b64encode(code).decode("utf8")
- raise TimeoutError("No code received")
+
+ raise TimeoutError(
+ "No infrared code received within "
+ f"{LEARNING_TIMEOUT.seconds} seconds"
+ )
+
+ finally:
+ self.hass.components.persistent_notification.async_dismiss(
+ notification_id="learn_command"
+ )
+
+ async def _async_learn_rf_command(self, command):
+ """Learn a radiofrequency command."""
+ try:
+ await self._device.async_request(self._device.api.sweep_frequency)
+
+ except (BroadlinkException, OSError) as err:
+ _LOGGER.debug("Failed to sweep frequency: %s", err)
+ raise
+
+ self.hass.components.persistent_notification.async_create(
+ f"Press and hold the '{command}' button.",
+ title="Sweep frequency",
+ notification_id="sweep_frequency",
+ )
+
+ try:
+ start_time = utcnow()
+ while (utcnow() - start_time) < LEARNING_TIMEOUT:
+ await asyncio.sleep(1)
+ found = await self._device.async_request(
+ self._device.api.check_frequency
+ )
+ if found:
+ break
+ else:
+ await self._device.async_request(
+ self._device.api.cancel_sweep_frequency
+ )
+ raise TimeoutError(
+ "No radiofrequency found within "
+ f"{LEARNING_TIMEOUT.seconds} seconds"
+ )
+
+ finally:
+ self.hass.components.persistent_notification.async_dismiss(
+ notification_id="sweep_frequency"
+ )
+
+ await asyncio.sleep(1)
+
+ try:
+ await self._device.async_request(self._device.api.find_rf_packet)
+
+ except (BroadlinkException, OSError) as err:
+ _LOGGER.debug("Failed to enter learning mode: %s", err)
+ raise
+
+ self.hass.components.persistent_notification.async_create(
+ f"Press the '{command}' button again.",
+ title="Learn command",
+ notification_id="learn_command",
+ )
+
+ try:
+ start_time = utcnow()
+ while (utcnow() - start_time) < LEARNING_TIMEOUT:
+ await asyncio.sleep(1)
+ try:
+ code = await self._device.async_request(self._device.api.check_data)
+ except (ReadError, StorageError):
+ continue
+ return b64encode(code).decode("utf8")
+
+ raise TimeoutError(
+ "No radiofrequency code received within "
+ f"{LEARNING_TIMEOUT.seconds} seconds"
+ )
finally:
self.hass.components.persistent_notification.async_dismiss(
diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py
index 644255d7d17..b4cd43ac493 100644
--- a/homeassistant/components/broadlink/switch.py
+++ b/homeassistant/components/broadlink/switch.py
@@ -1,5 +1,6 @@
"""Support for Broadlink switches."""
from abc import ABC, abstractmethod
+from functools import partial
import logging
from broadlink.exceptions import BroadlinkException
@@ -124,6 +125,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
elif device.api.type in {"SP4", "SP4B"}:
switches = [BroadlinkSP4Switch(device)]
+ elif device.api.type == "BG1":
+ switches = [BroadlinkBG1Slot(device, slot) for slot in range(1, 3)]
+
elif device.api.type == "MP1":
switches = [BroadlinkMP1Slot(device, slot) for slot in range(1, 5)]
@@ -360,3 +364,46 @@ class BroadlinkMP1Slot(BroadlinkSwitch):
_LOGGER.error("Failed to send packet: %s", err)
return False
return True
+
+
+class BroadlinkBG1Slot(BroadlinkSwitch):
+ """Representation of a Broadlink BG1 slot."""
+
+ def __init__(self, device, slot):
+ """Initialize the switch."""
+ super().__init__(device, 1, 0)
+ self._slot = slot
+ self._state = self._coordinator.data[f"pwr{slot}"]
+ self._device_class = DEVICE_CLASS_OUTLET
+
+ @property
+ def unique_id(self):
+ """Return the unique id of the slot."""
+ return f"{self._device.unique_id}-s{self._slot}"
+
+ @property
+ def name(self):
+ """Return the name of the switch."""
+ return f"{self._device.name} S{self._slot}"
+
+ @property
+ def assumed_state(self):
+ """Return True if unable to access real state of the switch."""
+ return False
+
+ @callback
+ def update_data(self):
+ """Update data."""
+ if self._coordinator.last_update_success:
+ self._state = self._coordinator.data[f"pwr{self._slot}"]
+ self.async_write_ha_state()
+
+ async def _async_send_packet(self, packet):
+ """Send a packet to the device."""
+ set_state = partial(self._device.api.set_state, **{f"pwr{self._slot}": packet})
+ try:
+ await self._device.async_request(set_state)
+ except (BroadlinkException, OSError) as err:
+ _LOGGER.error("Failed to send packet: %s", err)
+ return False
+ return True
diff --git a/homeassistant/components/broadlink/translations/ru.json b/homeassistant/components/broadlink/translations/ru.json
index 19470d5a66d..65ee1f4db1d 100644
--- a/homeassistant/components/broadlink/translations/ru.json
+++ b/homeassistant/components/broadlink/translations/ru.json
@@ -25,7 +25,7 @@
"title": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430"
},
"reset": {
- "description": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e {name} ({model}, {host}) \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043e. \u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u043c \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c, \u0447\u0442\u043e\u0431\u044b \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0435\u0433\u043e: \n 1. \u041e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 Broadlink. \n 2. \u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e. \n 3. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 `...` \u0432 \u043f\u0440\u0430\u0432\u043e\u043c \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u0443\u0433\u043b\u0443. \n 4. \u041f\u0440\u043e\u043a\u0440\u0443\u0442\u0438\u0442\u0435 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0432\u043d\u0438\u0437.\n 5. \u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0437\u0430\u043c\u043e\u043a.",
+ "description": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e {name} ({model}, {host}) \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043e. \u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u043c \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c, \u0447\u0442\u043e\u0431\u044b \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0435\u0433\u043e: \n 1. \u041e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 Broadlink. \n 2. \u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e. \n 3. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 `...` \u0432 \u043f\u0440\u0430\u0432\u043e\u043c \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u0443\u0433\u043b\u0443. \n 4. \u041f\u0440\u043e\u043a\u0440\u0443\u0442\u0438\u0442\u0435 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0432\u043d\u0438\u0437.\n 5. \u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0443.",
"title": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430"
},
"unlock": {
diff --git a/homeassistant/components/broadlink/updater.py b/homeassistant/components/broadlink/updater.py
index eb42e688a59..c9b273218b5 100644
--- a/homeassistant/components/broadlink/updater.py
+++ b/homeassistant/components/broadlink/updater.py
@@ -26,6 +26,7 @@ def get_update_manager(device):
update_managers = {
"A1": BroadlinkA1UpdateManager,
+ "BG1": BroadlinkBG1UpdateManager,
"MP1": BroadlinkMP1UpdateManager,
"RM2": BroadlinkRMUpdateManager,
"RM4": BroadlinkRMUpdateManager,
@@ -161,6 +162,14 @@ class BroadlinkSP2UpdateManager(BroadlinkUpdateManager):
return data
+class BroadlinkBG1UpdateManager(BroadlinkUpdateManager):
+ """Manages updates for Broadlink BG1 devices."""
+
+ async def async_fetch_data(self):
+ """Fetch data from the device."""
+ return await self.device.async_request(self.device.api.get_state)
+
+
class BroadlinkSP4UpdateManager(BroadlinkUpdateManager):
"""Manages updates for Broadlink SP4 devices."""
diff --git a/homeassistant/components/brother/const.py b/homeassistant/components/brother/const.py
index 9aa0a4f4a00..5aecde16327 100644
--- a/homeassistant/components/brother/const.py
+++ b/homeassistant/components/brother/const.py
@@ -18,6 +18,7 @@ ATTR_DRUM_COUNTER = "drum_counter"
ATTR_DRUM_REMAINING_LIFE = "drum_remaining_life"
ATTR_DRUM_REMAINING_PAGES = "drum_remaining_pages"
ATTR_DUPLEX_COUNTER = "duplex_unit_pages_counter"
+ATTR_ENABLED = "enabled"
ATTR_FUSER_REMAINING_LIFE = "fuser_remaining_life"
ATTR_ICON = "icon"
ATTR_LABEL = "label"
@@ -51,116 +52,144 @@ SENSOR_TYPES = {
ATTR_ICON: "mdi:printer",
ATTR_LABEL: ATTR_STATUS.title(),
ATTR_UNIT: None,
+ ATTR_ENABLED: True,
},
ATTR_PAGE_COUNTER: {
ATTR_ICON: "mdi:file-document-outline",
ATTR_LABEL: ATTR_PAGE_COUNTER.replace("_", " ").title(),
ATTR_UNIT: UNIT_PAGES,
+ ATTR_ENABLED: True,
},
ATTR_BW_COUNTER: {
ATTR_ICON: "mdi:file-document-outline",
ATTR_LABEL: ATTR_BW_COUNTER.replace("_", " ").title(),
ATTR_UNIT: UNIT_PAGES,
+ ATTR_ENABLED: True,
},
ATTR_COLOR_COUNTER: {
ATTR_ICON: "mdi:file-document-outline",
ATTR_LABEL: ATTR_COLOR_COUNTER.replace("_", " ").title(),
ATTR_UNIT: UNIT_PAGES,
+ ATTR_ENABLED: True,
},
ATTR_DUPLEX_COUNTER: {
ATTR_ICON: "mdi:file-document-outline",
ATTR_LABEL: ATTR_DUPLEX_COUNTER.replace("_", " ").title(),
ATTR_UNIT: UNIT_PAGES,
+ ATTR_ENABLED: True,
},
ATTR_DRUM_REMAINING_LIFE: {
ATTR_ICON: "mdi:chart-donut",
ATTR_LABEL: ATTR_DRUM_REMAINING_LIFE.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
+ ATTR_ENABLED: True,
},
ATTR_BLACK_DRUM_REMAINING_LIFE: {
ATTR_ICON: "mdi:chart-donut",
ATTR_LABEL: ATTR_BLACK_DRUM_REMAINING_LIFE.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
+ ATTR_ENABLED: True,
},
ATTR_CYAN_DRUM_REMAINING_LIFE: {
ATTR_ICON: "mdi:chart-donut",
ATTR_LABEL: ATTR_CYAN_DRUM_REMAINING_LIFE.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
+ ATTR_ENABLED: True,
},
ATTR_MAGENTA_DRUM_REMAINING_LIFE: {
ATTR_ICON: "mdi:chart-donut",
ATTR_LABEL: ATTR_MAGENTA_DRUM_REMAINING_LIFE.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
+ ATTR_ENABLED: True,
},
ATTR_YELLOW_DRUM_REMAINING_LIFE: {
ATTR_ICON: "mdi:chart-donut",
ATTR_LABEL: ATTR_YELLOW_DRUM_REMAINING_LIFE.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
+ ATTR_ENABLED: True,
},
ATTR_BELT_UNIT_REMAINING_LIFE: {
ATTR_ICON: "mdi:current-ac",
ATTR_LABEL: ATTR_BELT_UNIT_REMAINING_LIFE.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
+ ATTR_ENABLED: True,
},
ATTR_FUSER_REMAINING_LIFE: {
ATTR_ICON: "mdi:water-outline",
ATTR_LABEL: ATTR_FUSER_REMAINING_LIFE.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
+ ATTR_ENABLED: True,
},
ATTR_LASER_REMAINING_LIFE: {
ATTR_ICON: "mdi:spotlight-beam",
ATTR_LABEL: ATTR_LASER_REMAINING_LIFE.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
+ ATTR_ENABLED: True,
},
ATTR_PF_KIT_1_REMAINING_LIFE: {
ATTR_ICON: "mdi:printer-3d",
ATTR_LABEL: ATTR_PF_KIT_1_REMAINING_LIFE.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
+ ATTR_ENABLED: True,
},
ATTR_PF_KIT_MP_REMAINING_LIFE: {
ATTR_ICON: "mdi:printer-3d",
ATTR_LABEL: ATTR_PF_KIT_MP_REMAINING_LIFE.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
+ ATTR_ENABLED: True,
},
ATTR_BLACK_TONER_REMAINING: {
ATTR_ICON: "mdi:printer-3d-nozzle",
ATTR_LABEL: ATTR_BLACK_TONER_REMAINING.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
+ ATTR_ENABLED: True,
},
ATTR_CYAN_TONER_REMAINING: {
ATTR_ICON: "mdi:printer-3d-nozzle",
ATTR_LABEL: ATTR_CYAN_TONER_REMAINING.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
+ ATTR_ENABLED: True,
},
ATTR_MAGENTA_TONER_REMAINING: {
ATTR_ICON: "mdi:printer-3d-nozzle",
ATTR_LABEL: ATTR_MAGENTA_TONER_REMAINING.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
+ ATTR_ENABLED: True,
},
ATTR_YELLOW_TONER_REMAINING: {
ATTR_ICON: "mdi:printer-3d-nozzle",
ATTR_LABEL: ATTR_YELLOW_TONER_REMAINING.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
+ ATTR_ENABLED: True,
},
ATTR_BLACK_INK_REMAINING: {
ATTR_ICON: "mdi:printer-3d-nozzle",
ATTR_LABEL: ATTR_BLACK_INK_REMAINING.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
+ ATTR_ENABLED: True,
},
ATTR_CYAN_INK_REMAINING: {
ATTR_ICON: "mdi:printer-3d-nozzle",
ATTR_LABEL: ATTR_CYAN_INK_REMAINING.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
+ ATTR_ENABLED: True,
},
ATTR_MAGENTA_INK_REMAINING: {
ATTR_ICON: "mdi:printer-3d-nozzle",
ATTR_LABEL: ATTR_MAGENTA_INK_REMAINING.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
+ ATTR_ENABLED: True,
},
ATTR_YELLOW_INK_REMAINING: {
ATTR_ICON: "mdi:printer-3d-nozzle",
ATTR_LABEL: ATTR_YELLOW_INK_REMAINING.replace("_", " ").title(),
ATTR_UNIT: PERCENTAGE,
+ ATTR_ENABLED: True,
+ },
+ ATTR_UPTIME: {
+ ATTR_ICON: None,
+ ATTR_LABEL: ATTR_UPTIME.title(),
+ ATTR_UNIT: None,
+ ATTR_ENABLED: False,
},
- ATTR_UPTIME: {ATTR_ICON: None, ATTR_LABEL: ATTR_UPTIME.title(), ATTR_UNIT: None},
}
diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json
index bc63de34b2a..0e534147cb1 100644
--- a/homeassistant/components/brother/manifest.json
+++ b/homeassistant/components/brother/manifest.json
@@ -3,7 +3,7 @@
"name": "Brother Printer",
"documentation": "https://www.home-assistant.io/integrations/brother",
"codeowners": ["@bieniu"],
- "requirements": ["brother==0.1.18"],
+ "requirements": ["brother==0.1.20"],
"zeroconf": [{"type": "_printer._tcp.local.", "name":"Brother*"}],
"config_flow": true,
"quality_scale": "platinum"
diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py
index 7239976f85e..40e2deae67d 100644
--- a/homeassistant/components/brother/sensor.py
+++ b/homeassistant/components/brother/sensor.py
@@ -1,9 +1,6 @@
"""Support for the Brother service."""
-from datetime import timedelta
-
from homeassistant.const import DEVICE_CLASS_TIMESTAMP
from homeassistant.helpers.update_coordinator import CoordinatorEntity
-from homeassistant.util.dt import utcnow
from .const import (
ATTR_BLACK_DRUM_COUNTER,
@@ -15,6 +12,7 @@ from .const import (
ATTR_DRUM_COUNTER,
ATTR_DRUM_REMAINING_LIFE,
ATTR_DRUM_REMAINING_PAGES,
+ ATTR_ENABLED,
ATTR_ICON,
ATTR_LABEL,
ATTR_MAGENTA_DRUM_COUNTER,
@@ -78,8 +76,7 @@ class BrotherPrinterSensor(CoordinatorEntity):
def state(self):
"""Return the state."""
if self.kind == ATTR_UPTIME:
- uptime = utcnow() - timedelta(seconds=self.coordinator.data.get(self.kind))
- return uptime.replace(microsecond=0).isoformat()
+ return self.coordinator.data.get(self.kind).isoformat()
return self.coordinator.data.get(self.kind)
@property
@@ -139,4 +136,4 @@ class BrotherPrinterSensor(CoordinatorEntity):
@property
def entity_registry_enabled_default(self):
"""Return if the entity should be enabled when first added to the entity registry."""
- return True
+ return SENSOR_TYPES[self.kind][ATTR_ENABLED]
diff --git a/homeassistant/components/brother/translations/hu.json b/homeassistant/components/brother/translations/hu.json
index 2869d74fd04..dd5711cc516 100644
--- a/homeassistant/components/brother/translations/hu.json
+++ b/homeassistant/components/brother/translations/hu.json
@@ -5,6 +5,7 @@
"unsupported_model": "Ez a nyomtat\u00f3modell nem t\u00e1mogatott."
},
"error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
"snmp_error": "Az SNMP szerver ki van kapcsolva, vagy a nyomtat\u00f3 nem t\u00e1mogatott.",
"wrong_host": "\u00c9rv\u00e9nytelen \u00e1llom\u00e1sn\u00e9v vagy IP-c\u00edm."
},
diff --git a/homeassistant/components/brother/translations/pl.json b/homeassistant/components/brother/translations/pl.json
index 9b62e9afed4..c4c1c3d7d7a 100644
--- a/homeassistant/components/brother/translations/pl.json
+++ b/homeassistant/components/brother/translations/pl.json
@@ -22,7 +22,7 @@
"data": {
"type": "Typ drukarki"
},
- "description": "Czy chcesz doda\u0107 drukark\u0119 Brother {model} o numerze seryjnym `{serial_number}` do Home Assistant?",
+ "description": "Czy chcesz doda\u0107 drukark\u0119 Brother {model} o numerze seryjnym `{serial_number}` do Home Assistanta?",
"title": "Wykryto drukark\u0119 Brother"
}
}
diff --git a/homeassistant/components/bsblan/__init__.py b/homeassistant/components/bsblan/__init__.py
index 9f4bb38e315..bab4af29422 100644
--- a/homeassistant/components/bsblan/__init__.py
+++ b/homeassistant/components/bsblan/__init__.py
@@ -5,7 +5,7 @@ from bsblan import BSBLan, BSBLanConnectionError
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_HOST, CONF_PORT
+from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -29,6 +29,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.data[CONF_HOST],
passkey=entry.data[CONF_PASSKEY],
port=entry.data[CONF_PORT],
+ username=entry.data.get(CONF_USERNAME),
+ password=entry.data.get(CONF_PASSWORD),
session=session,
)
diff --git a/homeassistant/components/bsblan/config_flow.py b/homeassistant/components/bsblan/config_flow.py
index faca81bb6a7..dee04e6ef85 100644
--- a/homeassistant/components/bsblan/config_flow.py
+++ b/homeassistant/components/bsblan/config_flow.py
@@ -6,7 +6,7 @@ from bsblan import BSBLan, BSBLanError, Info
import voluptuous as vol
from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow
-from homeassistant.const import CONF_HOST, CONF_PORT
+from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
@@ -37,6 +37,8 @@ class BSBLanFlowHandler(ConfigFlow, domain=DOMAIN):
host=user_input[CONF_HOST],
port=user_input[CONF_PORT],
passkey=user_input.get(CONF_PASSKEY),
+ username=user_input.get(CONF_USERNAME),
+ password=user_input.get(CONF_PASSWORD),
)
except BSBLanError:
return self._show_setup_form({"base": "cannot_connect"})
@@ -52,6 +54,8 @@ class BSBLanFlowHandler(ConfigFlow, domain=DOMAIN):
CONF_PORT: user_input[CONF_PORT],
CONF_PASSKEY: user_input.get(CONF_PASSKEY),
CONF_DEVICE_IDENT: info.device_identification,
+ CONF_USERNAME: user_input.get(CONF_USERNAME),
+ CONF_PASSWORD: user_input.get(CONF_PASSWORD),
},
)
@@ -64,16 +68,30 @@ class BSBLanFlowHandler(ConfigFlow, domain=DOMAIN):
vol.Required(CONF_HOST): str,
vol.Optional(CONF_PORT, default=80): int,
vol.Optional(CONF_PASSKEY): str,
+ vol.Optional(CONF_USERNAME): str,
+ vol.Optional(CONF_PASSWORD): str,
}
),
errors=errors or {},
)
async def _get_bsblan_info(
- self, host: str, passkey: Optional[str], port: int
+ self,
+ host: str,
+ username: Optional[str],
+ password: Optional[str],
+ passkey: Optional[str],
+ port: int,
) -> Info:
"""Get device information from an BSBLan device."""
session = async_get_clientsession(self.hass)
_LOGGER.debug("request bsblan.info:")
- bsblan = BSBLan(host, passkey=passkey, port=port, session=session)
+ bsblan = BSBLan(
+ host,
+ username=username,
+ password=password,
+ passkey=passkey,
+ port=port,
+ session=session,
+ )
return await bsblan.info()
diff --git a/homeassistant/components/bsblan/strings.json b/homeassistant/components/bsblan/strings.json
index d9510808fc1..0bb084fb20d 100644
--- a/homeassistant/components/bsblan/strings.json
+++ b/homeassistant/components/bsblan/strings.json
@@ -8,7 +8,9 @@
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]",
- "passkey": "Passkey string"
+ "passkey": "Passkey string",
+ "username": "[%key:common::config_flow::data::username%]",
+ "password": "[%key:common::config_flow::data::password%]"
}
}
},
diff --git a/homeassistant/components/bsblan/translations/cs.json b/homeassistant/components/bsblan/translations/cs.json
index 3df55116d19..f0bce62991f 100644
--- a/homeassistant/components/bsblan/translations/cs.json
+++ b/homeassistant/components/bsblan/translations/cs.json
@@ -11,7 +11,9 @@
"user": {
"data": {
"host": "Hostitel",
- "port": "Port"
+ "password": "Heslo",
+ "port": "Port",
+ "username": "U\u017eivatelsk\u00e9 jm\u00e9no"
},
"title": "P\u0159ipojen\u00ed k za\u0159\u00edzen\u00ed BSB-Lan"
}
diff --git a/homeassistant/components/bsblan/translations/en.json b/homeassistant/components/bsblan/translations/en.json
index b0773c5bb5e..4aa8b881cb4 100644
--- a/homeassistant/components/bsblan/translations/en.json
+++ b/homeassistant/components/bsblan/translations/en.json
@@ -12,7 +12,9 @@
"data": {
"host": "Host",
"passkey": "Passkey string",
- "port": "Port"
+ "password": "Password",
+ "port": "Port",
+ "username": "Username"
},
"description": "Set up you BSB-Lan device to integrate with Home Assistant.",
"title": "Connect to the BSB-Lan device"
diff --git a/homeassistant/components/bsblan/translations/es.json b/homeassistant/components/bsblan/translations/es.json
index 287bd0fb49d..691136f5441 100644
--- a/homeassistant/components/bsblan/translations/es.json
+++ b/homeassistant/components/bsblan/translations/es.json
@@ -12,7 +12,9 @@
"data": {
"host": "Host",
"passkey": "Clave de acceso",
- "port": "Puerto"
+ "password": "Contrase\u00f1a",
+ "port": "Puerto",
+ "username": "Usuario"
},
"description": "Configura tu dispositivo BSB-Lan para integrarse con Home Assistant.",
"title": "Conectar con el dispositivo BSB-Lan"
diff --git a/homeassistant/components/bsblan/translations/et.json b/homeassistant/components/bsblan/translations/et.json
index 70f35535112..22ff91e7e1b 100644
--- a/homeassistant/components/bsblan/translations/et.json
+++ b/homeassistant/components/bsblan/translations/et.json
@@ -12,7 +12,9 @@
"data": {
"host": "",
"passkey": "Juurdep\u00e4\u00e4sut\u00f5endi string",
- "port": ""
+ "password": "Salas\u00f5na",
+ "port": "",
+ "username": "Kasutajanimi"
},
"description": "Seadista oma BSB-Lan seadme sidumine Home Assistant'iga.",
"title": "\u00dchendu BSB-Lan seadmega"
diff --git a/homeassistant/components/bsblan/translations/hu.json b/homeassistant/components/bsblan/translations/hu.json
index 45918735010..1d28556ba1a 100644
--- a/homeassistant/components/bsblan/translations/hu.json
+++ b/homeassistant/components/bsblan/translations/hu.json
@@ -1,5 +1,8 @@
{
"config": {
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/bsblan/translations/lb.json b/homeassistant/components/bsblan/translations/lb.json
index 56ace699cee..3f8da84cea0 100644
--- a/homeassistant/components/bsblan/translations/lb.json
+++ b/homeassistant/components/bsblan/translations/lb.json
@@ -12,7 +12,9 @@
"data": {
"host": "Host",
"passkey": "Passkey Zeechefolleg",
- "port": "Port"
+ "password": "Passwuert",
+ "port": "Port",
+ "username": "Benotzernumm"
},
"description": "BSB-Lan Apparat ariichten fir d'Integratioun mam Home Assistant.",
"title": "Mam BSB-Lan Apparat verbannen"
diff --git a/homeassistant/components/bsblan/translations/no.json b/homeassistant/components/bsblan/translations/no.json
index 685f3afb1f4..40981e2b77c 100644
--- a/homeassistant/components/bsblan/translations/no.json
+++ b/homeassistant/components/bsblan/translations/no.json
@@ -12,7 +12,9 @@
"data": {
"host": "Vert",
"passkey": "Tilgangsn\u00f8kkel streng",
- "port": "Port"
+ "password": "Passord",
+ "port": "Port",
+ "username": "Brukernavn"
},
"description": "Konfigurer din BSB-Lan-enhet for \u00e5 integrere med Home Assistant.",
"title": "Koble til BSB-Lan-enheten"
diff --git a/homeassistant/components/bsblan/translations/pl.json b/homeassistant/components/bsblan/translations/pl.json
index f95f2695883..5cf79db3fba 100644
--- a/homeassistant/components/bsblan/translations/pl.json
+++ b/homeassistant/components/bsblan/translations/pl.json
@@ -12,9 +12,11 @@
"data": {
"host": "Nazwa hosta lub adres IP",
"passkey": "Ci\u0105g klucza dost\u0119pu",
- "port": "Port"
+ "password": "Has\u0142o",
+ "port": "Port",
+ "username": "Nazwa u\u017cytkownika"
},
- "description": "Konfiguracja urz\u0105dzenia BSB-LAN w celu integracji z Home Assistant.",
+ "description": "Konfiguracja urz\u0105dzenia BSB-LAN w celu integracji z Home Assistantem.",
"title": "Po\u0142\u0105czenie z urz\u0105dzeniem BSB-Lan"
}
}
diff --git a/homeassistant/components/bsblan/translations/ru.json b/homeassistant/components/bsblan/translations/ru.json
index 7d7bcb9fa0e..76aa715a9de 100644
--- a/homeassistant/components/bsblan/translations/ru.json
+++ b/homeassistant/components/bsblan/translations/ru.json
@@ -12,7 +12,9 @@
"data": {
"host": "\u0425\u043e\u0441\u0442",
"passkey": "\u041f\u0430\u0440\u043e\u043b\u044c",
- "port": "\u041f\u043e\u0440\u0442"
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "port": "\u041f\u043e\u0440\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 BSB-Lan.",
"title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443"
diff --git a/homeassistant/components/bsblan/translations/sl.json b/homeassistant/components/bsblan/translations/sl.json
new file mode 100644
index 00000000000..2bf2dd68b44
--- /dev/null
+++ b/homeassistant/components/bsblan/translations/sl.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "port": "Vrata"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/bsblan/translations/zh-Hant.json b/homeassistant/components/bsblan/translations/zh-Hant.json
index 3d3bcb44ac7..7ada76c1d21 100644
--- a/homeassistant/components/bsblan/translations/zh-Hant.json
+++ b/homeassistant/components/bsblan/translations/zh-Hant.json
@@ -12,7 +12,9 @@
"data": {
"host": "\u4e3b\u6a5f\u7aef",
"passkey": "Passkey \u5b57\u4e32",
- "port": "\u901a\u8a0a\u57e0"
+ "password": "\u5bc6\u78bc",
+ "port": "\u901a\u8a0a\u57e0",
+ "username": "\u4f7f\u7528\u8005\u540d\u7a31"
},
"description": "\u8a2d\u5b9a BSB-Lan \u8a2d\u5099\u4ee5\u6574\u5408\u81f3 Home Assistant\u3002",
"title": "\u9023\u7dda\u81f3 BSB-Lan \u8a2d\u5099"
diff --git a/homeassistant/components/buienradar/weather.py b/homeassistant/components/buienradar/weather.py
index d0a0c0e18b4..4b0391c3190 100644
--- a/homeassistant/components/buienradar/weather.py
+++ b/homeassistant/components/buienradar/weather.py
@@ -14,6 +14,20 @@ from buienradar.constants import (
import voluptuous as vol
from homeassistant.components.weather import (
+ ATTR_CONDITION_CLOUDY,
+ ATTR_CONDITION_EXCEPTIONAL,
+ ATTR_CONDITION_FOG,
+ ATTR_CONDITION_HAIL,
+ ATTR_CONDITION_LIGHTNING,
+ ATTR_CONDITION_LIGHTNING_RAINY,
+ ATTR_CONDITION_PARTLYCLOUDY,
+ ATTR_CONDITION_POURING,
+ ATTR_CONDITION_RAINY,
+ ATTR_CONDITION_SNOWY,
+ ATTR_CONDITION_SNOWY_RAINY,
+ ATTR_CONDITION_SUNNY,
+ ATTR_CONDITION_WINDY,
+ ATTR_CONDITION_WINDY_VARIANT,
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_PRECIPITATION,
ATTR_FORECAST_TEMP,
@@ -40,20 +54,20 @@ CONF_FORECAST = "forecast"
CONDITION_CLASSES = {
- "cloudy": ["c", "p"],
- "fog": ["d", "n"],
- "hail": [],
- "lightning": ["g"],
- "lightning-rainy": ["s"],
- "partlycloudy": ["b", "j", "o", "r"],
- "pouring": ["l", "q"],
- "rainy": ["f", "h", "k", "m"],
- "snowy": ["u", "i", "v", "t"],
- "snowy-rainy": ["w"],
- "sunny": ["a"],
- "windy": [],
- "windy-variant": [],
- "exceptional": [],
+ ATTR_CONDITION_CLOUDY: ["c", "p"],
+ ATTR_CONDITION_FOG: ["d", "n"],
+ ATTR_CONDITION_HAIL: [],
+ ATTR_CONDITION_LIGHTNING: ["g"],
+ ATTR_CONDITION_LIGHTNING_RAINY: ["s"],
+ ATTR_CONDITION_PARTLYCLOUDY: ["b", "j", "o", "r"],
+ ATTR_CONDITION_POURING: ["l", "q"],
+ ATTR_CONDITION_RAINY: ["f", "h", "k", "m"],
+ ATTR_CONDITION_SNOWY: ["u", "i", "v", "t"],
+ ATTR_CONDITION_SNOWY_RAINY: ["w"],
+ ATTR_CONDITION_SUNNY: ["a"],
+ ATTR_CONDITION_WINDY: [],
+ ATTR_CONDITION_WINDY_VARIANT: [],
+ ATTR_CONDITION_EXCEPTIONAL: [],
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py
index a90eb5a2825..fd2f08c1488 100644
--- a/homeassistant/components/canary/camera.py
+++ b/homeassistant/components/canary/camera.py
@@ -30,7 +30,7 @@ from .coordinator import CanaryDataUpdateCoordinator
MIN_TIME_BETWEEN_SESSION_RENEW = timedelta(seconds=90)
PLATFORM_SCHEMA = vol.All(
- cv.deprecated(CONF_FFMPEG_ARGUMENTS, invalidation_version="0.118"),
+ cv.deprecated(CONF_FFMPEG_ARGUMENTS),
PLATFORM_SCHEMA.extend(
{
vol.Optional(
@@ -128,7 +128,7 @@ class CanaryCamera(CoordinatorEntity, Camera):
"""Return a still image response from the camera."""
await self.hass.async_add_executor_job(self.renew_live_stream_session)
- ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop)
+ ffmpeg = ImageFrame(self._ffmpeg.binary)
image = await asyncio.shield(
ffmpeg.get_image(
self._live_stream_session.live_stream_url,
@@ -143,7 +143,7 @@ class CanaryCamera(CoordinatorEntity, Camera):
if self._live_stream_session is None:
return
- stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop)
+ stream = CameraMjpeg(self._ffmpeg.binary)
await stream.open_camera(
self._live_stream_session.live_stream_url, extra_cmd=self._ffmpeg_arguments
)
diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py
index 97956965b66..b76dbcaf20b 100644
--- a/homeassistant/components/cast/media_player.py
+++ b/homeassistant/components/cast/media_player.py
@@ -601,7 +601,7 @@ class CastDevice(MediaPlayerEntity):
@property
def state(self):
"""Return the state of the player."""
- media_status, _ = self._media_status()
+ media_status = self._media_status()[0]
if media_status is None:
return None
@@ -633,13 +633,13 @@ class CastDevice(MediaPlayerEntity):
@property
def media_content_id(self):
"""Content ID of current playing media."""
- media_status, _ = self._media_status()
+ media_status = self._media_status()[0]
return media_status.content_id if media_status else None
@property
def media_content_type(self):
"""Content type of current playing media."""
- media_status, _ = self._media_status()
+ media_status = self._media_status()[0]
if media_status is None:
return None
if media_status.media_is_tvshow:
@@ -653,13 +653,13 @@ class CastDevice(MediaPlayerEntity):
@property
def media_duration(self):
"""Duration of current playing media in seconds."""
- media_status, _ = self._media_status()
+ media_status = self._media_status()[0]
return media_status.duration if media_status else None
@property
def media_image_url(self):
"""Image url of current playing media."""
- media_status, _ = self._media_status()
+ media_status = self._media_status()[0]
if media_status is None:
return None
@@ -677,49 +677,49 @@ class CastDevice(MediaPlayerEntity):
@property
def media_title(self):
"""Title of current playing media."""
- media_status, _ = self._media_status()
+ media_status = self._media_status()[0]
return media_status.title if media_status else None
@property
def media_artist(self):
"""Artist of current playing media (Music track only)."""
- media_status, _ = self._media_status()
+ media_status = self._media_status()[0]
return media_status.artist if media_status else None
@property
def media_album_name(self):
"""Album of current playing media (Music track only)."""
- media_status, _ = self._media_status()
+ media_status = self._media_status()[0]
return media_status.album_name if media_status else None
@property
def media_album_artist(self):
"""Album artist of current playing media (Music track only)."""
- media_status, _ = self._media_status()
+ media_status = self._media_status()[0]
return media_status.album_artist if media_status else None
@property
def media_track(self):
"""Track number of current playing media (Music track only)."""
- media_status, _ = self._media_status()
+ media_status = self._media_status()[0]
return media_status.track if media_status else None
@property
def media_series_title(self):
"""Return the title of the series of current playing media."""
- media_status, _ = self._media_status()
+ media_status = self._media_status()[0]
return media_status.series_title if media_status else None
@property
def media_season(self):
"""Season of current playing media (TV Show only)."""
- media_status, _ = self._media_status()
+ media_status = self._media_status()[0]
return media_status.season if media_status else None
@property
def media_episode(self):
"""Episode of current playing media (TV Show only)."""
- media_status, _ = self._media_status()
+ media_status = self._media_status()[0]
return media_status.episode if media_status else None
@property
@@ -736,7 +736,7 @@ class CastDevice(MediaPlayerEntity):
def supported_features(self):
"""Flag media player features that are supported."""
support = SUPPORT_CAST
- media_status, _ = self._media_status()
+ media_status = self._media_status()[0]
if media_status:
if media_status.supports_queue_next:
@@ -754,7 +754,7 @@ class CastDevice(MediaPlayerEntity):
@property
def media_position(self):
"""Position of current playing media in seconds."""
- media_status, _ = self._media_status()
+ media_status = self._media_status()[0]
if media_status is None or not (
media_status.player_is_playing
or media_status.player_is_paused
@@ -769,7 +769,7 @@ class CastDevice(MediaPlayerEntity):
Returns value from homeassistant.util.dt.utcnow().
"""
- _, media_status_recevied = self._media_status()
+ media_status_recevied = self._media_status()[1]
return media_status_recevied
@property
diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py
index 3d993fb2bc0..0e329b1898f 100644
--- a/homeassistant/components/cert_expiry/sensor.py
+++ b/homeassistant/components/cert_expiry/sensor.py
@@ -10,13 +10,11 @@ from homeassistant.const import (
CONF_PORT,
DEVICE_CLASS_TIMESTAMP,
EVENT_HOMEASSISTANT_START,
- TIME_DAYS,
)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.update_coordinator import CoordinatorEntity
-from homeassistant.util import dt
from .const import DEFAULT_PORT, DOMAIN
@@ -55,7 +53,6 @@ async def async_setup_entry(hass, entry, async_add_entities):
coordinator = hass.data[DOMAIN][entry.entry_id]
sensors = [
- SSLCertificateDays(coordinator),
SSLCertificateTimestamp(coordinator),
]
@@ -79,34 +76,6 @@ class CertExpiryEntity(CoordinatorEntity):
}
-class SSLCertificateDays(CertExpiryEntity):
- """Implementation of the Cert Expiry days sensor."""
-
- @property
- def name(self):
- """Return the name of the sensor."""
- return f"Cert Expiry ({self.coordinator.name})"
-
- @property
- def state(self):
- """Return the state of the sensor."""
- if not self.coordinator.is_cert_valid:
- return 0
-
- expiry = self.coordinator.data - dt.utcnow()
- return expiry.days
-
- @property
- def unique_id(self):
- """Return a unique id for the sensor."""
- return f"{self.coordinator.host}:{self.coordinator.port}"
-
- @property
- def unit_of_measurement(self):
- """Return the unit this state is expressed in."""
- return TIME_DAYS
-
-
class SSLCertificateTimestamp(CertExpiryEntity):
"""Implementation of the Cert Expiry timestamp sensor."""
diff --git a/homeassistant/components/cert_expiry/translations/hu.json b/homeassistant/components/cert_expiry/translations/hu.json
index 22e9312e778..5bad24ecb6a 100644
--- a/homeassistant/components/cert_expiry/translations/hu.json
+++ b/homeassistant/components/cert_expiry/translations/hu.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "import_failed": "Nem siker\u00fclt import\u00e1lni a konfigur\u00e1ci\u00f3t"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/climate/translations/ru.json b/homeassistant/components/climate/translations/ru.json
index 3e4ff1844d6..4f8efaa5858 100644
--- a/homeassistant/components/climate/translations/ru.json
+++ b/homeassistant/components/climate/translations/ru.json
@@ -2,11 +2,11 @@
"device_automation": {
"action_type": {
"set_hvac_mode": "{entity_name}: \u0441\u043c\u0435\u043d\u0438\u0442\u044c \u0440\u0435\u0436\u0438\u043c \u0440\u0430\u0431\u043e\u0442\u044b",
- "set_preset_mode": "{entity_name}: \u0441\u043c\u0435\u043d\u0438\u0442\u044c \u043f\u0440\u0435\u0434\u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0443"
+ "set_preset_mode": "{entity_name}: \u0441\u043c\u0435\u043d\u0438\u0442\u044c \u043f\u0440\u0435\u0441\u0435\u0442"
},
"condition_type": {
"is_hvac_mode": "{entity_name} \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0432 \u0437\u0430\u0434\u0430\u043d\u043d\u043e\u043c \u0440\u0435\u0436\u0438\u043c\u0435 \u0440\u0430\u0431\u043e\u0442\u044b",
- "is_preset_mode": "{entity_name} \u0432 \u043f\u0440\u0435\u0434\u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u043e\u043c \u0440\u0435\u0436\u0438\u043c\u0435"
+ "is_preset_mode": "{entity_name} \u0432 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u043e\u043c \u043f\u0440\u0435\u0441\u0435\u0442\u0435"
},
"trigger_type": {
"current_humidity_changed": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u043d\u043e\u0439 \u0432\u043b\u0430\u0436\u043d\u043e\u0441\u0442\u0438",
diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json
index f3c79a470ea..03bf2761857 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.37.2"],
+ "requirements": ["hass-nabucasa==0.39.0"],
"dependencies": ["http", "webhook", "alexa"],
"after_dependencies": ["google_assistant"],
"codeowners": ["@home-assistant/cloud"]
diff --git a/homeassistant/components/cloud/translations/hu.json b/homeassistant/components/cloud/translations/hu.json
new file mode 100644
index 00000000000..5dfc087c7bb
--- /dev/null
+++ b/homeassistant/components/cloud/translations/hu.json
@@ -0,0 +1,11 @@
+{
+ "system_health": {
+ "info": {
+ "alexa_enabled": "Alexa enged\u00e9lyezve",
+ "can_reach_cloud_auth": "Hiteles\u00edt\u00e9si kiszolg\u00e1l\u00f3 el\u00e9r\u00e9se",
+ "google_enabled": "Google enged\u00e9lyezve",
+ "logged_in": "Bejelentkezve",
+ "subscription_expiration": "El\u0151fizet\u00e9s lej\u00e1rata"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cloud/translations/ka.json b/homeassistant/components/cloud/translations/ka.json
new file mode 100644
index 00000000000..44507245daa
--- /dev/null
+++ b/homeassistant/components/cloud/translations/ka.json
@@ -0,0 +1,16 @@
+{
+ "system_health": {
+ "info": {
+ "alexa_enabled": "Alexa \u10d3\u10d0\u10e8\u10d5\u10d4\u10d1\u10e3\u10da\u10d8\u10d0",
+ "can_reach_cert_server": "\u10db\u10d8\u10d4\u10e6\u10ec\u10d0 \u10e1\u10d4\u10e0\u10e2\u10d8\u10e4\u10d8\u10d9\u10d0\u10e2\u10d8\u10e1 \u10e1\u10d4\u10e0\u10d5\u10d4\u10e0\u10e1",
+ "can_reach_cloud": "\u10db\u10d8\u10d4\u10e6\u10ec\u10d0 Home Assistant \u10e6\u10e0\u10e3\u10d1\u10d4\u10da\u10e1",
+ "can_reach_cloud_auth": "\u10db\u10d8\u10d4\u10e6\u10ec\u10d0 \u10d0\u10d5\u10d7\u10d4\u10dc\u10e2\u10d8\u10e4\u10d8\u10d9\u10d0\u10ea\u10d8\u10d8\u10e1 \u10e1\u10d4\u10e0\u10d5\u10d4\u10e0\u10e1",
+ "google_enabled": "Google \u10d3\u10d0\u10e8\u10d5\u10d4\u10d1\u10e3\u10da\u10d8\u10d0",
+ "logged_in": "\u10e8\u10d4\u10e1\u10e3\u10da\u10d8",
+ "relayer_connected": "\u10e0\u10d4\u10da\u10d4 \u10db\u10d8\u10d4\u10e0\u10d7\u10d4\u10d1\u10e3\u10da\u10d8\u10d0",
+ "remote_connected": "\u10d3\u10d8\u10e1\u10e2\u10d0\u10dc\u10ea\u10d8\u10e3\u10e0\u10d0\u10d3 \u10d3\u10d0\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0",
+ "remote_enabled": "\u10d3\u10d8\u10e1\u10e2\u10d0\u10dc\u10ea\u10d8\u10e3\u10e0\u10d8 \u10d3\u10d0\u10e8\u10d5\u10d4\u10d1\u10e3\u10da\u10d8\u10d0",
+ "subscription_expiration": "\u10d2\u10d0\u10db\u10dd\u10ec\u10d4\u10e0\u10d8\u10e1 \u10d5\u10d0\u10d3\u10d8\u10e1 \u10d2\u10d0\u10e1\u10d5\u10da\u10d0"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cloud/translations/ru.json b/homeassistant/components/cloud/translations/ru.json
index b66e2ca51fa..b2d8c55369b 100644
--- a/homeassistant/components/cloud/translations/ru.json
+++ b/homeassistant/components/cloud/translations/ru.json
@@ -7,6 +7,9 @@
"can_reach_cloud_auth": "\u0414\u043e\u0441\u0442\u0443\u043f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438",
"google_enabled": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0441 Google",
"logged_in": "\u0412\u0445\u043e\u0434 \u0432 \u0441\u0438\u0441\u0442\u0435\u043c\u0443",
+ "relayer_connected": "Relayer \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d",
+ "remote_connected": "\u0423\u0434\u0430\u043b\u0451\u043d\u043d\u044b\u0439 \u0434\u043e\u0441\u0442\u0443\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d",
+ "remote_enabled": "\u0423\u0434\u0430\u043b\u0451\u043d\u043d\u044b\u0439 \u0434\u043e\u0441\u0442\u0443\u043f \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u043d",
"subscription_expiration": "\u0421\u0440\u043e\u043a \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438"
}
}
diff --git a/homeassistant/components/cloud/translations/sl.json b/homeassistant/components/cloud/translations/sl.json
new file mode 100644
index 00000000000..a87094324f9
--- /dev/null
+++ b/homeassistant/components/cloud/translations/sl.json
@@ -0,0 +1,9 @@
+{
+ "system_health": {
+ "info": {
+ "alexa_enabled": "Alexa omogo\u010dena",
+ "google_enabled": "Google omogo\u010den",
+ "logged_in": "Prijavljen kot"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cloud/translations/zh-Hans.json b/homeassistant/components/cloud/translations/zh-Hans.json
new file mode 100644
index 00000000000..eb1daf5e4f3
--- /dev/null
+++ b/homeassistant/components/cloud/translations/zh-Hans.json
@@ -0,0 +1,16 @@
+{
+ "system_health": {
+ "info": {
+ "alexa_enabled": "\u5df2\u542f\u7528 Alexa",
+ "can_reach_cert_server": "\u53ef\u8bbf\u95ee\u8bc1\u4e66\u670d\u52a1\u5668",
+ "can_reach_cloud": "\u53ef\u8bbf\u95ee Home Assistant Cloud",
+ "can_reach_cloud_auth": "\u53ef\u8bbf\u95ee\u8ba4\u8bc1\u670d\u52a1\u5668",
+ "google_enabled": "\u5df2\u542f\u7528 Google",
+ "logged_in": "\u5df2\u767b\u5f55",
+ "relayer_connected": "\u901a\u8fc7\u4ee3\u7406\u8fde\u63a5",
+ "remote_connected": "\u8fdc\u7a0b\u8fde\u63a5",
+ "remote_enabled": "\u5df2\u542f\u7528\u8fdc\u7a0b\u63a7\u5236",
+ "subscription_expiration": "\u8ba2\u9605\u5230\u671f\u65f6\u95f4"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cloud/tts.py b/homeassistant/components/cloud/tts.py
index ea769c6a054..9dd392a12c5 100644
--- a/homeassistant/components/cloud/tts.py
+++ b/homeassistant/components/cloud/tts.py
@@ -1,7 +1,7 @@
"""Support for the cloud for text to speech service."""
from hass_nabucasa import Cloud
-from hass_nabucasa.voice import VoiceError
+from hass_nabucasa.voice import MAP_VOICE, VoiceError
import voluptuous as vol
from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider
@@ -10,17 +10,36 @@ from .const import DOMAIN
CONF_GENDER = "gender"
-SUPPORT_LANGUAGES = ["en-US", "de-DE", "es-ES"]
-SUPPORT_GENDER = ["male", "female"]
+SUPPORT_LANGUAGES = list({key[0] for key in MAP_VOICE})
DEFAULT_LANG = "en-US"
DEFAULT_GENDER = "female"
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES),
- vol.Optional(CONF_GENDER, default=DEFAULT_GENDER): vol.In(SUPPORT_GENDER),
- }
+
+def validate_lang(value):
+ """Validate chosen gender or language."""
+ lang = value[CONF_LANG]
+ gender = value.get(CONF_GENDER)
+
+ if gender is None:
+ gender = value[CONF_GENDER] = next(
+ (chk_gender for chk_lang, chk_gender in MAP_VOICE if chk_lang == lang), None
+ )
+
+ if (lang, gender) not in MAP_VOICE:
+ raise vol.Invalid("Unsupported language and gender specified.")
+
+ return value
+
+
+PLATFORM_SCHEMA = vol.All(
+ PLATFORM_SCHEMA.extend(
+ {
+ vol.Optional(CONF_LANG, default=DEFAULT_LANG): str,
+ vol.Optional(CONF_GENDER): str,
+ }
+ ),
+ validate_lang,
)
diff --git a/homeassistant/components/cloudflare/__init__.py b/homeassistant/components/cloudflare/__init__.py
index 3ebb919393a..446890887c1 100644
--- a/homeassistant/components/cloudflare/__init__.py
+++ b/homeassistant/components/cloudflare/__init__.py
@@ -33,10 +33,10 @@ _LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.All(
- cv.deprecated(CONF_EMAIL, invalidation_version="0.119"),
- cv.deprecated(CONF_API_KEY, invalidation_version="0.119"),
- cv.deprecated(CONF_ZONE, invalidation_version="0.119"),
- cv.deprecated(CONF_RECORDS, invalidation_version="0.119"),
+ cv.deprecated(CONF_EMAIL),
+ cv.deprecated(CONF_API_KEY),
+ cv.deprecated(CONF_ZONE),
+ cv.deprecated(CONF_RECORDS),
vol.Schema(
{
vol.Optional(CONF_EMAIL): cv.string,
diff --git a/homeassistant/components/cloudflare/translations/hu.json b/homeassistant/components/cloudflare/translations/hu.json
new file mode 100644
index 00000000000..fa13d00617f
--- /dev/null
+++ b/homeassistant/components/cloudflare/translations/hu.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "unknown": "V\u00e1ratlan hiba"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen autentik\u00e1ci\u00f3",
+ "invalid_zone": "\u00c9rv\u00e9nytelen z\u00f3na"
+ },
+ "flow_title": "Cloudflare: {name}",
+ "step": {
+ "records": {
+ "data": {
+ "records": "Rekordok"
+ },
+ "title": "V\u00e1lassza a friss\u00edteni k\u00edv\u00e1nt rekordokat"
+ },
+ "user": {
+ "data": {
+ "api_token": "API Token"
+ },
+ "title": "Csatlakoz\u00e1s a Cloudflare szolg\u00e1ltat\u00e1shoz"
+ },
+ "zone": {
+ "data": {
+ "zone": "Z\u00f3na"
+ },
+ "title": "V\u00e1lassza ki a friss\u00edtend\u0151 z\u00f3n\u00e1t"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cloudflare/translations/ka.json b/homeassistant/components/cloudflare/translations/ka.json
new file mode 100644
index 00000000000..6ba93fd16ea
--- /dev/null
+++ b/homeassistant/components/cloudflare/translations/ka.json
@@ -0,0 +1,35 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u10e3\u10d9\u10d5\u10d4 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0. \u10e8\u10d4\u10e1\u10d0\u10eb\u10da\u10d4\u10d1\u10d4\u10da\u10d8\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10d8.",
+ "unknown": "\u10d2\u10d0\u10e3\u10d7\u10d5\u10d0\u10da\u10d8\u10e1\u10ec\u10d8\u10dc\u10d4\u10d1\u10d4\u10da\u10d8 \u10e8\u10d4\u10ea\u10d3\u10dd\u10db\u10d0"
+ },
+ "error": {
+ "cannot_connect": "\u10d3\u10d0\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d4\u10d1\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0",
+ "invalid_auth": "\u10d0\u10e0\u10d0\u10e1\u10ec\u10dd\u10e0\u10d8 \u10d0\u10e3\u10d7\u10d4\u10dc\u10d7\u10d8\u10e4\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0",
+ "invalid_zone": "\u10d0\u10e0\u10d0\u10e1\u10ec\u10dd\u10e0\u10d8 \u10d6\u10dd\u10dc\u10d0"
+ },
+ "flow_title": "Cloudflare: {name}",
+ "step": {
+ "records": {
+ "data": {
+ "records": "\u10e9\u10d0\u10dc\u10d0\u10ec\u10d4\u10e0\u10d4\u10d1\u10d8"
+ },
+ "title": "\u10d0\u10d8\u10e0\u10e9\u10d8\u10d4\u10d7 \u10e9\u10d0\u10dc\u10d0\u10ec\u10d4\u10e0\u10d4\u10d1\u10d8 \u10d2\u10d0\u10dc\u10d0\u10ee\u10da\u10d4\u10d1\u10d8\u10e1\u10d7\u10d5\u10d8\u10e1"
+ },
+ "user": {
+ "data": {
+ "api_token": "API \u10e2\u10dd\u10d9\u10d4\u10dc\u10d8"
+ },
+ "description": "\u10d4\u10e1 \u10d8\u10dc\u10e2\u10d4\u10d2\u10e0\u10d0\u10ea\u10d8\u10d0 \u10db\u10dd\u10d8\u10d7\u10ee\u10dd\u10d5\u10e1 API \u10e2\u10dd\u10d9\u10d4\u10dc\u10e1, \u10e0\u10dd\u10db\u10d4\u10da\u10d8\u10ea \u10e8\u10d4\u10e5\u10db\u10dc\u10d8\u10da\u10d8\u10d0 \u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 \u10d0\u10dc\u10d2\u10d0\u10e0\u10d8\u10e8\u10d8\u10e1 Zone: Zone: Read \u10d3\u10d0 Zone: DNS:Edit \u10dc\u10d4\u10d1\u10d0\u10e0\u10d7\u10d5\u10d4\u10d1\u10d8\u10e1 \u10e7\u10d5\u10d4\u10da\u10d0 \u10d6\u10dd\u10dc\u10d4\u10d1\u10d8\u10e1\u10d0\u10d7\u10d5\u10d8\u10e1",
+ "title": "Cloudflare- \u10e1\u10d7\u10d0\u10dc \u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d8"
+ },
+ "zone": {
+ "data": {
+ "zone": "\u10d6\u10dd\u10dc\u10d0"
+ },
+ "title": "\u10d0\u10d8\u10e0\u10e9\u10d8\u10d4\u10d7 \u10d6\u10dd\u10dc\u10d0 \u10d2\u10d0\u10dc\u10d0\u10ee\u10da\u10d4\u10d1\u10d8\u10e1\u10d7\u10d5\u10d8\u10e1"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cloudflare/translations/lb.json b/homeassistant/components/cloudflare/translations/lb.json
index 753406d8d92..c32e7e7603c 100644
--- a/homeassistant/components/cloudflare/translations/lb.json
+++ b/homeassistant/components/cloudflare/translations/lb.json
@@ -11,16 +11,24 @@
},
"flow_title": "Cloudflare: {name}",
"step": {
+ "records": {
+ "data": {
+ "records": "Enregistrement"
+ },
+ "title": "Wiel d'Enregistrments aus fir ze ver\u00e4nneren"
+ },
"user": {
"data": {
"api_token": "API Jeton"
},
+ "description": "D\u00ebs Integratioun ben\u00e9idget een API Jeton dee mat Zone:Zone:Read a Zone:DNS:Edit Rechter fir all Zone an dengem Kont erstallt gouf.",
"title": "Mat Cloudflare verbannen"
},
"zone": {
"data": {
"zone": "Zon"
- }
+ },
+ "title": "Wiel d'Zone aus d\u00e9i aktualis\u00e9iert soll ginn."
}
}
}
diff --git a/homeassistant/components/cloudflare/translations/sl.json b/homeassistant/components/cloudflare/translations/sl.json
new file mode 100644
index 00000000000..02fcd2e18dc
--- /dev/null
+++ b/homeassistant/components/cloudflare/translations/sl.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "api_token": "API \u017eeton"
+ },
+ "title": "Pove\u017eite se z Cloudflare"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/config/auth.py b/homeassistant/components/config/auth.py
index d5bbb60e27d..c1d43a5d4a9 100644
--- a/homeassistant/components/config/auth.py
+++ b/homeassistant/components/config/auth.py
@@ -86,6 +86,7 @@ async def websocket_create(hass, connection, msg):
vol.Required("type"): "config/auth/update",
vol.Required("user_id"): str,
vol.Optional("name"): str,
+ vol.Optional("is_active"): bool,
vol.Optional("group_ids"): [str],
}
)
@@ -111,6 +112,16 @@ async def websocket_update(hass, connection, msg):
)
return
+ if user.is_owner and msg["is_active"] is False:
+ connection.send_message(
+ websocket_api.error_message(
+ msg["id"],
+ "cannot_deactivate_owner",
+ "Unable to deactivate owner.",
+ )
+ )
+ return
+
msg.pop("type")
msg_id = msg.pop("id")
@@ -123,8 +134,19 @@ async def websocket_update(hass, connection, msg):
def _user_info(user):
"""Format a user."""
+
+ ha_username = next(
+ (
+ cred.data.get("username")
+ for cred in user.credentials
+ if cred.auth_provider_type == "homeassistant"
+ ),
+ None,
+ )
+
return {
"id": user.id,
+ "username": ha_username,
"name": user.name,
"is_owner": user.is_owner,
"is_active": user.is_active,
diff --git a/homeassistant/components/config/auth_provider_homeassistant.py b/homeassistant/components/config/auth_provider_homeassistant.py
index 44ac6f23e2d..a8421c4c0f6 100644
--- a/homeassistant/components/config/auth_provider_homeassistant.py
+++ b/homeassistant/components/config/auth_provider_homeassistant.py
@@ -124,7 +124,9 @@ async def websocket_change_password(hass, connection, msg):
try:
await provider.async_validate_login(username, msg["current_password"])
except auth_ha.InvalidAuth:
- connection.send_error(msg["id"], "invalid_password", "Invalid password")
+ connection.send_error(
+ msg["id"], "invalid_current_password", "Invalid current password"
+ )
return
await provider.async_change_password(username, msg["new_password"])
diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py
index 6216a52fc13..01e22297c0d 100644
--- a/homeassistant/components/config/automation.py
+++ b/homeassistant/components/config/automation.py
@@ -2,8 +2,11 @@
from collections import OrderedDict
import uuid
-from homeassistant.components.automation import DOMAIN, PLATFORM_SCHEMA
-from homeassistant.components.automation.config import async_validate_config_item
+from homeassistant.components.automation.config import (
+ DOMAIN,
+ PLATFORM_SCHEMA,
+ async_validate_config_item,
+)
from homeassistant.config import AUTOMATION_CONFIG_PATH
from homeassistant.const import CONF_ID, SERVICE_RELOAD
from homeassistant.helpers import config_validation as cv, entity_registry
diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py
index de1f38f3e57..a43a863444a 100644
--- a/homeassistant/components/config/device_registry.py
+++ b/homeassistant/components/config/device_registry.py
@@ -21,6 +21,8 @@ SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
vol.Required("device_id"): str,
vol.Optional("area_id"): vol.Any(str, None),
vol.Optional("name_by_user"): vol.Any(str, None),
+ # We only allow setting disabled_by user via API.
+ vol.Optional("disabled_by"): vol.Any("user", None),
}
)
@@ -77,4 +79,5 @@ def _entry_dict(entry):
"via_device_id": entry.via_device_id,
"area_id": entry.area_id,
"name_by_user": entry.name_by_user,
+ "disabled_by": entry.disabled_by,
}
diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py
index 73327ecf23c..8d1c488bfa0 100644
--- a/homeassistant/components/config/entity_registry.py
+++ b/homeassistant/components/config/entity_registry.py
@@ -107,6 +107,19 @@ async def websocket_update_entity(hass, connection, msg):
)
return
+ if "disabled_by" in msg and msg["disabled_by"] is None:
+ entity = registry.entities[msg["entity_id"]]
+ if entity.device_id:
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+ device = device_registry.async_get(entity.device_id)
+ if device.disabled:
+ connection.send_message(
+ websocket_api.error_message(
+ msg["id"], "invalid_info", "Device is disabled"
+ )
+ )
+ return
+
try:
if changes:
entry = registry.async_update_entity(msg["entity_id"], **changes)
diff --git a/homeassistant/components/coolmaster/translations/hu.json b/homeassistant/components/coolmaster/translations/hu.json
index cbf055e2fba..cf688d6fdeb 100644
--- a/homeassistant/components/coolmaster/translations/hu.json
+++ b/homeassistant/components/coolmaster/translations/hu.json
@@ -1,5 +1,8 @@
{
"config": {
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/cover/translations/it.json b/homeassistant/components/cover/translations/it.json
index 90322b9f122..dbcb6425238 100644
--- a/homeassistant/components/cover/translations/it.json
+++ b/homeassistant/components/cover/translations/it.json
@@ -5,35 +5,35 @@
"close_tilt": "Chiudi l'inclinazione di {entity_name}",
"open": "Apri {entity_name}",
"open_tilt": "Apri l'inclinazione di {entity_name}",
- "set_position": "Imposta la posizione di {entity_name}",
- "set_tilt_position": "Imposta la posizione di inclinazione di {entity_name}",
+ "set_position": "Imposta l'apertura di {entity_name}",
+ "set_tilt_position": "Imposta l'inclinazione di {entity_name}",
"stop": "Ferma {entity_name}"
},
"condition_type": {
- "is_closed": "{entity_name} \u00e8 chiuso",
+ "is_closed": "{entity_name} \u00e8 chiusa",
"is_closing": "{entity_name} si sta chiudendo",
- "is_open": "{entity_name} \u00e8 aperto",
+ "is_open": "{entity_name} \u00e8 aperta",
"is_opening": "{entity_name} si sta aprendo",
- "is_position": "La posizione attuale di {entity_name} \u00e8",
- "is_tilt_position": "La posizione d'inclinazione attuale di {entity_name} \u00e8"
+ "is_position": "L'apertura attuale di {entity_name} \u00e8",
+ "is_tilt_position": "L'inclinazione attuale di {entity_name} \u00e8"
},
"trigger_type": {
- "closed": "{entity_name} chiuso",
+ "closed": "{entity_name} chiusa",
"closing": "{entity_name} in chiusura",
- "opened": "{entity_name} aperto",
+ "opened": "{entity_name} aperta",
"opening": "{entity_name} in apertura",
- "position": "{entity_name} cambiamenti della posizione",
- "tilt_position": "{entity_name} cambiamenti della posizione d'inclinazione"
+ "position": "{entity_name} variazioni di apertura",
+ "tilt_position": "{entity_name} variazioni d'inclinazione"
}
},
"state": {
"_": {
- "closed": "Chiuso",
+ "closed": "Chiusa",
"closing": "In chiusura",
- "open": "Aperto",
+ "open": "Aperta",
"opening": "In apertura",
- "stopped": "Arrestato"
+ "stopped": "Arrestata"
}
},
- "title": "Scuri"
+ "title": "Serrande"
}
\ No newline at end of file
diff --git a/homeassistant/components/cover/translations/zh-Hans.json b/homeassistant/components/cover/translations/zh-Hans.json
index ccc1edd42c5..7c5675dad31 100644
--- a/homeassistant/components/cover/translations/zh-Hans.json
+++ b/homeassistant/components/cover/translations/zh-Hans.json
@@ -1,9 +1,15 @@
{
"device_automation": {
+ "action_type": {
+ "stop": "\u505c\u6b62 {entity_name}"
+ },
"condition_type": {
"is_closed": "{entity_name} \u5df2\u5173\u95ed",
- "is_closing": "{entity_name}\u6b63\u5728\u5173\u95ed",
- "is_open": "{entity_name}\u4e3a\u5f00\u653e"
+ "is_closing": "{entity_name} \u6b63\u5728\u5173\u95ed",
+ "is_open": "{entity_name} \u5df2\u6253\u5f00",
+ "is_opening": "{entity_name} \u6b63\u5728\u6253\u5f00",
+ "is_position": "{entity_name} \u5f53\u524d\u4f4d\u7f6e\u4e3a",
+ "is_tilt_position": "{entity_name} \u5f53\u524d\u503e\u659c\u4f4d\u7f6e\u4e3a"
},
"trigger_type": {
"closed": "{entity_name}\u5df2\u5173\u95ed"
diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py
index 7b9c1ded673..b4950b8b05b 100644
--- a/homeassistant/components/daikin/__init__.py
+++ b/homeassistant/components/daikin/__init__.py
@@ -30,7 +30,7 @@ COMPONENT_TYPES = ["climate", "sensor", "switch"]
CONFIG_SCHEMA = vol.Schema(
vol.All(
- cv.deprecated(DOMAIN, invalidation_version="0.113.0"),
+ cv.deprecated(DOMAIN),
{
DOMAIN: vol.Schema(
{
diff --git a/homeassistant/components/daikin/translations/hu.json b/homeassistant/components/daikin/translations/hu.json
index 149a1f713f4..ef589eb7f6d 100644
--- a/homeassistant/components/daikin/translations/hu.json
+++ b/homeassistant/components/daikin/translations/hu.json
@@ -3,9 +3,14 @@
"abort": {
"already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
},
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "unknown": "V\u00e1ratlan hiba"
+ },
"step": {
"user": {
"data": {
+ "api_key": "API kulcs",
"host": "Hoszt",
"password": "Jelsz\u00f3"
},
diff --git a/homeassistant/components/daikin/translations/ka.json b/homeassistant/components/daikin/translations/ka.json
new file mode 100644
index 00000000000..e777a22e8ce
--- /dev/null
+++ b/homeassistant/components/daikin/translations/ka.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u10db\u10d0\u10e0\u10ea\u10ee\u10d8 \u10e8\u10d4\u10d4\u10e0\u10d7\u10d4\u10d1\u10d8\u10e1\u10d0\u10e1",
+ "invalid_auth": "\u10db\u10ea\u10d3\u10d0\u10e0\u10d8 \u10d0\u10d5\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0",
+ "unknown": "\u10d2\u10d0\u10e3\u10d7\u10d5\u10d0\u10da\u10d8\u10e1\u10ec\u10d8\u10dc\u10d4\u10d1\u10d4\u10da\u10d8 \u10e8\u10d4\u10ea\u10d3\u10dd\u10db\u10d0"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API Key"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/darksky/weather.py b/homeassistant/components/darksky/weather.py
index fee7d60a2c3..0ad448ddfbd 100644
--- a/homeassistant/components/darksky/weather.py
+++ b/homeassistant/components/darksky/weather.py
@@ -7,6 +7,17 @@ from requests.exceptions import ConnectionError as ConnectError, HTTPError, Time
import voluptuous as vol
from homeassistant.components.weather import (
+ ATTR_CONDITION_CLEAR_NIGHT,
+ ATTR_CONDITION_CLOUDY,
+ ATTR_CONDITION_FOG,
+ ATTR_CONDITION_HAIL,
+ ATTR_CONDITION_LIGHTNING,
+ ATTR_CONDITION_PARTLYCLOUDY,
+ ATTR_CONDITION_RAINY,
+ ATTR_CONDITION_SNOWY,
+ ATTR_CONDITION_SNOWY_RAINY,
+ ATTR_CONDITION_SUNNY,
+ ATTR_CONDITION_WINDY,
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_PRECIPITATION,
ATTR_FORECAST_TEMP,
@@ -40,18 +51,18 @@ ATTRIBUTION = "Powered by Dark Sky"
FORECAST_MODE = ["hourly", "daily"]
MAP_CONDITION = {
- "clear-day": "sunny",
- "clear-night": "clear-night",
- "rain": "rainy",
- "snow": "snowy",
- "sleet": "snowy-rainy",
- "wind": "windy",
- "fog": "fog",
- "cloudy": "cloudy",
- "partly-cloudy-day": "partlycloudy",
- "partly-cloudy-night": "partlycloudy",
- "hail": "hail",
- "thunderstorm": "lightning",
+ "clear-day": ATTR_CONDITION_SUNNY,
+ "clear-night": ATTR_CONDITION_CLEAR_NIGHT,
+ "rain": ATTR_CONDITION_RAINY,
+ "snow": ATTR_CONDITION_SNOWY,
+ "sleet": ATTR_CONDITION_SNOWY_RAINY,
+ "wind": ATTR_CONDITION_WINDY,
+ "fog": ATTR_CONDITION_FOG,
+ "cloudy": ATTR_CONDITION_CLOUDY,
+ "partly-cloudy-day": ATTR_CONDITION_PARTLYCLOUDY,
+ "partly-cloudy-night": ATTR_CONDITION_PARTLYCLOUDY,
+ "hail": ATTR_CONDITION_HAIL,
+ "thunderstorm": ATTR_CONDITION_LIGHTNING,
"tornado": None,
}
diff --git a/homeassistant/components/debugpy/manifest.json b/homeassistant/components/debugpy/manifest.json
index 18583186c3b..27b110b1f68 100644
--- a/homeassistant/components/debugpy/manifest.json
+++ b/homeassistant/components/debugpy/manifest.json
@@ -2,7 +2,7 @@
"domain": "debugpy",
"name": "Remote Python Debugger",
"documentation": "https://www.home-assistant.io/integrations/debugpy",
- "requirements": ["debugpy==1.1.0"],
+ "requirements": ["debugpy==1.2.0"],
"codeowners": ["@frenck"],
"quality_scale": "internal"
}
diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py
index 5b536aeb74c..616206949ed 100644
--- a/homeassistant/components/deconz/binary_sensor.py
+++ b/homeassistant/components/deconz/binary_sensor.py
@@ -39,7 +39,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
gateway.entities[DOMAIN] = set()
@callback
- def async_add_sensor(sensors):
+ def async_add_sensor(sensors=gateway.api.sensors.values()):
"""Add binary sensor from deCONZ."""
entities = []
@@ -84,7 +84,7 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorEntity):
@property
def is_on(self):
"""Return true if sensor is on."""
- return self._device.is_tripped
+ return self._device.state
@property
def device_class(self):
diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py
index 4a6f2bd4937..3e1e1748737 100644
--- a/homeassistant/components/deconz/climate.py
+++ b/homeassistant/components/deconz/climate.py
@@ -1,11 +1,25 @@
"""Support for deCONZ climate devices."""
+from typing import Optional
+
from pydeconz.sensor import Thermostat
from homeassistant.components.climate import DOMAIN, ClimateEntity
from homeassistant.components.climate.const import (
+ FAN_AUTO,
+ FAN_HIGH,
+ FAN_LOW,
+ FAN_MEDIUM,
+ FAN_OFF,
+ FAN_ON,
HVAC_MODE_AUTO,
+ HVAC_MODE_COOL,
HVAC_MODE_HEAT,
HVAC_MODE_OFF,
+ PRESET_BOOST,
+ PRESET_COMFORT,
+ PRESET_ECO,
+ SUPPORT_FAN_MODE,
+ SUPPORT_PRESET_MODE,
SUPPORT_TARGET_TEMPERATURE,
)
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
@@ -16,7 +30,39 @@ from .const import ATTR_OFFSET, ATTR_VALVE, NEW_SENSOR
from .deconz_device import DeconzDevice
from .gateway import get_gateway_from_config_entry
-HVAC_MODES = {HVAC_MODE_AUTO: "auto", HVAC_MODE_HEAT: "heat", HVAC_MODE_OFF: "off"}
+DECONZ_FAN_SMART = "smart"
+
+FAN_MODES = {
+ DECONZ_FAN_SMART: "smart",
+ FAN_AUTO: "auto",
+ FAN_HIGH: "high",
+ FAN_MEDIUM: "medium",
+ FAN_LOW: "low",
+ FAN_ON: "on",
+ FAN_OFF: "off",
+}
+
+HVAC_MODES = {
+ HVAC_MODE_AUTO: "auto",
+ HVAC_MODE_COOL: "cool",
+ HVAC_MODE_HEAT: "heat",
+ HVAC_MODE_OFF: "off",
+}
+
+DECONZ_PRESET_AUTO = "auto"
+DECONZ_PRESET_COMPLEX = "complex"
+DECONZ_PRESET_HOLIDAY = "holiday"
+DECONZ_PRESET_MANUAL = "manual"
+
+PRESET_MODES = {
+ DECONZ_PRESET_AUTO: "auto",
+ PRESET_BOOST: "boost",
+ PRESET_COMFORT: "comfort",
+ DECONZ_PRESET_COMPLEX: "complex",
+ PRESET_ECO: "eco",
+ DECONZ_PRESET_HOLIDAY: "holiday",
+ DECONZ_PRESET_MANUAL: "manual",
+}
async def async_setup_entry(hass, config_entry, async_add_entities):
@@ -28,7 +74,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
gateway.entities[DOMAIN] = set()
@callback
- def async_add_climate(sensors):
+ def async_add_climate(sensors=gateway.api.sensors.values()):
"""Add climate devices from deCONZ."""
entities = []
@@ -53,7 +99,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
)
)
- async_add_climate(gateway.api.sensors.values())
+ async_add_climate()
class DeconzThermostat(DeconzDevice, ClimateEntity):
@@ -61,10 +107,61 @@ class DeconzThermostat(DeconzDevice, ClimateEntity):
TYPE = DOMAIN
+ def __init__(self, device, gateway):
+ """Set up thermostat device."""
+ super().__init__(device, gateway)
+
+ self._hvac_modes = dict(HVAC_MODES)
+ if "mode" not in device.raw["config"]:
+ self._hvac_modes = {
+ HVAC_MODE_HEAT: True,
+ HVAC_MODE_OFF: False,
+ }
+ elif "coolsetpoint" not in device.raw["config"]:
+ self._hvac_modes.pop(HVAC_MODE_COOL)
+
+ self._features = SUPPORT_TARGET_TEMPERATURE
+
+ if "fanmode" in device.raw["config"]:
+ self._features |= SUPPORT_FAN_MODE
+
+ if "preset" in device.raw["config"]:
+ self._features |= SUPPORT_PRESET_MODE
+
@property
def supported_features(self):
"""Return the list of supported features."""
- return SUPPORT_TARGET_TEMPERATURE
+ return self._features
+
+ # Fan control
+
+ @property
+ def fan_mode(self) -> str:
+ """Return fan operation."""
+ for hass_fan_mode, fan_mode in FAN_MODES.items():
+ if self._device.fanmode == fan_mode:
+ return hass_fan_mode
+
+ if self._device.state_on:
+ return FAN_ON
+
+ return FAN_OFF
+
+ @property
+ def fan_modes(self) -> list:
+ """Return the list of available fan operation modes."""
+ return list(FAN_MODES)
+
+ async def async_set_fan_mode(self, fan_mode: str) -> None:
+ """Set new target fan mode."""
+ if fan_mode not in FAN_MODES:
+ raise ValueError(f"Unsupported fan mode {fan_mode}")
+
+ data = {"fanmode": FAN_MODES[fan_mode]}
+
+ await self._device.async_set_config(data)
+
+ # HVAC control
@property
def hvac_mode(self):
@@ -72,7 +169,7 @@ class DeconzThermostat(DeconzDevice, ClimateEntity):
Need to be one of HVAC_MODE_*.
"""
- for hass_hvac_mode, device_mode in HVAC_MODES.items():
+ for hass_hvac_mode, device_mode in self._hvac_modes.items():
if self._device.mode == device_mode:
return hass_hvac_mode
@@ -84,16 +181,56 @@ class DeconzThermostat(DeconzDevice, ClimateEntity):
@property
def hvac_modes(self) -> list:
"""Return the list of available hvac operation modes."""
- return list(HVAC_MODES)
+ return list(self._hvac_modes)
+
+ async def async_set_hvac_mode(self, hvac_mode: str) -> None:
+ """Set new target hvac mode."""
+ if hvac_mode not in self._hvac_modes:
+ raise ValueError(f"Unsupported HVAC mode {hvac_mode}")
+
+ data = {"mode": self._hvac_modes[hvac_mode]}
+ if len(self._hvac_modes) == 2: # Only allow turn on and off thermostat
+ data = {"on": self._hvac_modes[hvac_mode]}
+
+ await self._device.async_set_config(data)
+
+ # Preset control
@property
- def current_temperature(self):
+ def preset_mode(self) -> Optional[str]:
+ """Return preset mode."""
+ for hass_preset_mode, preset_mode in PRESET_MODES.items():
+ if self._device.preset == preset_mode:
+ return hass_preset_mode
+
+ return None
+
+ @property
+ def preset_modes(self) -> list:
+ """Return the list of available preset modes."""
+ return list(PRESET_MODES)
+
+ async def async_set_preset_mode(self, preset_mode: str) -> None:
+ """Set new preset mode."""
+ if preset_mode not in PRESET_MODES:
+ raise ValueError(f"Unsupported preset mode {preset_mode}")
+
+ data = {"preset": PRESET_MODES[preset_mode]}
+
+ await self._device.async_set_config(data)
+
+ # Temperature control
+
+ @property
+ def current_temperature(self) -> float:
"""Return the current temperature."""
return self._device.temperature
@property
- def target_temperature(self):
+ def target_temperature(self) -> float:
"""Return the target temperature."""
+ if self._device.mode == "cool":
+ return self._device.coolsetpoint
return self._device.heatsetpoint
async def async_set_temperature(self, **kwargs):
@@ -102,15 +239,8 @@ class DeconzThermostat(DeconzDevice, ClimateEntity):
raise ValueError(f"Expected attribute {ATTR_TEMPERATURE}")
data = {"heatsetpoint": kwargs[ATTR_TEMPERATURE] * 100}
-
- await self._device.async_set_config(data)
-
- async def async_set_hvac_mode(self, hvac_mode):
- """Set new target hvac mode."""
- if hvac_mode not in HVAC_MODES:
- raise ValueError(f"Unsupported mode {hvac_mode}")
-
- data = {"mode": HVAC_MODES[hvac_mode]}
+ if self._device.mode == "cool":
+ data = {"coolsetpoint": kwargs[ATTR_TEMPERATURE] * 100}
await self._device.async_set_config(data)
diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py
index c43c1c95504..6c2df3ad614 100644
--- a/homeassistant/components/deconz/config_flow.py
+++ b/homeassistant/components/deconz/config_flow.py
@@ -172,9 +172,6 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
except asyncio.TimeoutError:
return self.async_abort(reason="no_bridges")
- if self.bridge_id == "0000000000000000":
- return self.async_abort(reason="no_hardware_available")
-
return self.async_create_entry(title=self.bridge_id, data=self.deconz_config)
async def async_step_ssdp(self, discovery_info):
diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py
index 5982aead14f..6e57d08302a 100644
--- a/homeassistant/components/deconz/cover.py
+++ b/homeassistant/components/deconz/cover.py
@@ -1,12 +1,17 @@
"""Support for deCONZ covers."""
from homeassistant.components.cover import (
ATTR_POSITION,
+ ATTR_TILT_POSITION,
DEVICE_CLASS_WINDOW,
DOMAIN,
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
@@ -18,15 +23,12 @@ from .gateway import get_gateway_from_config_entry
async def async_setup_entry(hass, config_entry, async_add_entities):
- """Set up covers for deCONZ component.
-
- Covers are based on the same device class as lights in deCONZ.
- """
+ """Set up covers for deCONZ component."""
gateway = get_gateway_from_config_entry(hass, config_entry)
gateway.entities[DOMAIN] = set()
@callback
- def async_add_cover(lights):
+ def async_add_cover(lights=gateway.api.lights.values()):
"""Add cover from deCONZ."""
entities = []
@@ -46,7 +48,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
)
)
- async_add_cover(gateway.api.lights.values())
+ async_add_cover()
class DeconzCover(DeconzDevice, CoverEntity):
@@ -63,15 +65,16 @@ class DeconzCover(DeconzDevice, CoverEntity):
self._features |= SUPPORT_STOP
self._features |= SUPPORT_SET_POSITION
- @property
- def current_cover_position(self):
- """Return the current position of the cover."""
- return 100 - int(self._device.brightness / 254 * 100)
+ if self._device.tilt is not None:
+ self._features |= SUPPORT_OPEN_TILT
+ self._features |= SUPPORT_CLOSE_TILT
+ self._features |= SUPPORT_STOP_TILT
+ self._features |= SUPPORT_SET_TILT_POSITION
@property
- def is_closed(self):
- """Return if the cover is closed."""
- return self._device.state
+ def supported_features(self):
+ """Flag supported features."""
+ return self._features
@property
def device_class(self):
@@ -82,32 +85,52 @@ class DeconzCover(DeconzDevice, CoverEntity):
return DEVICE_CLASS_WINDOW
@property
- def supported_features(self):
- """Flag supported features."""
- return self._features
+ def current_cover_position(self):
+ """Return the current position of the cover."""
+ return 100 - self._device.lift
+
+ @property
+ def is_closed(self):
+ """Return if the cover is closed."""
+ return not self._device.is_open
async def async_set_cover_position(self, **kwargs):
"""Move the cover to a specific position."""
- position = kwargs[ATTR_POSITION]
- data = {"on": False}
-
- if position < 100:
- data["on"] = True
- data["bri"] = 254 - int(position / 100 * 254)
-
- await self._device.async_set_state(data)
+ position = 100 - kwargs[ATTR_POSITION]
+ await self._device.set_position(lift=position)
async def async_open_cover(self, **kwargs):
"""Open cover."""
- data = {ATTR_POSITION: 100}
- await self.async_set_cover_position(**data)
+ await self._device.open()
async def async_close_cover(self, **kwargs):
"""Close cover."""
- data = {ATTR_POSITION: 0}
- await self.async_set_cover_position(**data)
+ await self._device.close()
async def async_stop_cover(self, **kwargs):
"""Stop cover."""
- data = {"bri_inc": 0}
- await self._device.async_set_state(data)
+ await self._device.stop()
+
+ @property
+ def current_cover_tilt_position(self):
+ """Return the current tilt position of the cover."""
+ if self._device.tilt is not None:
+ return 100 - self._device.tilt
+ return None
+
+ async def async_set_cover_tilt_position(self, **kwargs):
+ """Tilt the cover to a specific position."""
+ position = 100 - kwargs[ATTR_TILT_POSITION]
+ await self._device.set_position(tilt=position)
+
+ async def async_open_cover_tilt(self, **kwargs):
+ """Open cover tilt."""
+ await self._device.set_position(tilt=0)
+
+ async def async_close_cover_tilt(self, **kwargs):
+ """Close cover tilt."""
+ await self._device.set_position(tilt=100)
+
+ async def async_stop_cover_tilt(self, **kwargs):
+ """Stop cover tilt."""
+ await self._device.stop()
diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py
index 968ab3cee39..81d3aa94d31 100644
--- a/homeassistant/components/deconz/deconz_event.py
+++ b/homeassistant/components/deconz/deconz_event.py
@@ -1,7 +1,7 @@
"""Representation of a deCONZ remote."""
from pydeconz.sensor import Switch
-from homeassistant.const import CONF_EVENT, CONF_ID, CONF_UNIQUE_ID
+from homeassistant.const import CONF_DEVICE_ID, CONF_EVENT, CONF_ID, CONF_UNIQUE_ID
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util import slugify
@@ -16,7 +16,7 @@ async def async_setup_events(gateway) -> None:
"""Set up the deCONZ events."""
@callback
- def async_add_sensor(sensors):
+ def async_add_sensor(sensors=gateway.api.sensors.values()):
"""Create DeconzEvent."""
for sensor in sensors:
@@ -38,9 +38,7 @@ async def async_setup_events(gateway) -> None:
)
)
- async_add_sensor(
- [gateway.api.sensors[key] for key in sorted(gateway.api.sensors, key=int)]
- )
+ async_add_sensor()
@callback
@@ -94,6 +92,9 @@ class DeconzEvent(DeconzBase):
CONF_EVENT: self._device.state,
}
+ if self.device_id:
+ data[CONF_DEVICE_ID] = self.device_id
+
if self._device.gesture is not None:
data[CONF_GESTURE] = self._device.gesture
diff --git a/homeassistant/components/deconz/fan.py b/homeassistant/components/deconz/fan.py
index 7936a8fead5..d92addff5bd 100644
--- a/homeassistant/components/deconz/fan.py
+++ b/homeassistant/components/deconz/fan.py
@@ -32,15 +32,12 @@ def convert_speed(speed: int) -> str:
async def async_setup_entry(hass, config_entry, async_add_entities) -> None:
- """Set up fans for deCONZ component.
-
- Fans are based on the same device class as lights in deCONZ.
- """
+ """Set up fans for deCONZ component."""
gateway = get_gateway_from_config_entry(hass, config_entry)
gateway.entities[DOMAIN] = set()
@callback
- def async_add_fan(lights) -> None:
+ def async_add_fan(lights=gateway.api.lights.values()) -> None:
"""Add fan from deCONZ."""
entities = []
@@ -58,7 +55,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities) -> None:
)
)
- async_add_fan(gateway.api.lights.values())
+ async_add_fan()
class DeconzFan(DeconzDevice, FanEntity):
@@ -108,9 +105,7 @@ class DeconzFan(DeconzDevice, FanEntity):
if speed not in SPEEDS:
raise ValueError(f"Unsupported speed {speed}")
- data = {"speed": SPEEDS[speed]}
-
- await self._device.async_set_state(data)
+ await self._device.set_speed(SPEEDS[speed])
async def async_turn_on(self, speed: str = None, **kwargs) -> None:
"""Turn on fan."""
diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py
index 475b3c48525..881ea883c4c 100644
--- a/homeassistant/components/deconz/gateway.py
+++ b/homeassistant/components/deconz/gateway.py
@@ -55,9 +55,6 @@ class DeconzGateway:
self.events = []
self.listeners = []
- self._current_option_allow_clip_sensor = self.option_allow_clip_sensor
- self._current_option_allow_deconz_groups = self.option_allow_deconz_groups
-
@property
def bridgeid(self) -> str:
"""Return the unique identifier of the gateway."""
@@ -124,16 +121,22 @@ class DeconzGateway:
async_dispatcher_send(self.hass, self.signal_reachable, True)
@callback
- def async_add_device_callback(self, device_type, device) -> None:
+ def async_add_device_callback(
+ self, device_type, device=None, force: bool = False
+ ) -> None:
"""Handle event of new device creation in deCONZ."""
- if not self.option_allow_new_devices:
+ if not force and not self.option_allow_new_devices:
return
- if not isinstance(device, list):
- device = [device]
+ args = []
+
+ if device is not None and not isinstance(device, list):
+ args.append([device])
async_dispatcher_send(
- self.hass, self.async_signal_new_device(device_type), device
+ self.hass,
+ self.async_signal_new_device(device_type),
+ *args, # Don't send device if None, it would override default value in listeners
)
async def async_update_device_registry(self) -> None:
@@ -171,7 +174,7 @@ class DeconzGateway:
raise ConfigEntryNotReady from err
except Exception as err: # pylint: disable=broad-except
- LOGGER.error("Error connecting with deCONZ gateway: %s", err)
+ LOGGER.error("Error connecting with deCONZ gateway: %s", err, exc_info=True)
return False
for component in SUPPORTED_PLATFORMS:
@@ -181,7 +184,7 @@ class DeconzGateway:
)
)
- self.hass.async_create_task(async_setup_events(self))
+ await async_setup_events(self)
self.api.start()
@@ -210,29 +213,21 @@ class DeconzGateway:
"""Manage entities affected by config entry options."""
deconz_ids = []
- if self._current_option_allow_clip_sensor != self.option_allow_clip_sensor:
- self._current_option_allow_clip_sensor = self.option_allow_clip_sensor
+ if self.option_allow_clip_sensor:
+ self.async_add_device_callback(NEW_SENSOR)
- sensors = [
- sensor
+ else:
+ deconz_ids += [
+ sensor.deconz_id
for sensor in self.api.sensors.values()
if sensor.type.startswith("CLIP")
]
- if self.option_allow_clip_sensor:
- self.async_add_device_callback(NEW_SENSOR, sensors)
- else:
- deconz_ids += [sensor.deconz_id for sensor in sensors]
+ if self.option_allow_deconz_groups:
+ self.async_add_device_callback(NEW_GROUP)
- if self._current_option_allow_deconz_groups != self.option_allow_deconz_groups:
- self._current_option_allow_deconz_groups = self.option_allow_deconz_groups
-
- groups = list(self.api.groups.values())
-
- if self.option_allow_deconz_groups:
- self.async_add_device_callback(NEW_GROUP, groups)
- else:
- deconz_ids += [group.deconz_id for group in groups]
+ else:
+ deconz_ids += [group.deconz_id for group in self.api.groups.values()]
entity_registry = await self.hass.helpers.entity_registry.async_get_registry()
diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py
index 3bdf2c67caa..6d759ccaf48 100644
--- a/homeassistant/components/deconz/light.py
+++ b/homeassistant/components/deconz/light.py
@@ -34,20 +34,24 @@ from .const import (
from .deconz_device import DeconzDevice
from .gateway import get_gateway_from_config_entry
+CONTROLLER = ["Configuration tool"]
+
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the deCONZ lights and groups from a config entry."""
gateway = get_gateway_from_config_entry(hass, config_entry)
gateway.entities[DOMAIN] = set()
+ other_light_resource_types = CONTROLLER + COVER_TYPES + LOCK_TYPES + SWITCH_TYPES
+
@callback
- def async_add_light(lights):
+ def async_add_light(lights=gateway.api.lights.values()):
"""Add light from deCONZ."""
entities = []
for light in lights:
if (
- light.type not in COVER_TYPES + LOCK_TYPES + SWITCH_TYPES
+ light.type not in other_light_resource_types
and light.uniqueid not in gateway.entities[DOMAIN]
):
entities.append(DeconzLight(light, gateway))
@@ -62,7 +66,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
)
@callback
- def async_add_group(groups):
+ def async_add_group(groups=gateway.api.groups.values()):
"""Add group from deCONZ."""
if not gateway.option_allow_deconz_groups:
return
@@ -87,8 +91,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
)
)
- async_add_light(gateway.api.lights.values())
- async_add_group(gateway.api.groups.values())
+ async_add_light()
+ async_add_group()
class DeconzBaseLight(DeconzDevice, LightEntity):
@@ -110,7 +114,9 @@ class DeconzBaseLight(DeconzDevice, LightEntity):
if self._device.ct is not None:
self._features |= SUPPORT_COLOR_TEMP
- if self._device.xy is not None:
+ if self._device.xy is not None or (
+ self._device.hue is not None and self._device.sat is not None
+ ):
self._features |= SUPPORT_COLOR
if self._device.effect is not None:
@@ -137,8 +143,10 @@ class DeconzBaseLight(DeconzDevice, LightEntity):
@property
def hs_color(self):
"""Return the hs color value."""
- if self._device.colormode in ("xy", "hs") and self._device.xy:
- return color_util.color_xy_to_hs(*self._device.xy)
+ if self._device.colormode in ("xy", "hs"):
+ if self._device.xy:
+ return color_util.color_xy_to_hs(*self._device.xy)
+ return (self._device.hue / 65535 * 360, self._device.sat / 255 * 100)
return None
@property
@@ -159,7 +167,11 @@ class DeconzBaseLight(DeconzDevice, LightEntity):
data["ct"] = kwargs[ATTR_COLOR_TEMP]
if ATTR_HS_COLOR in kwargs:
- data["xy"] = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR])
+ if self._device.xy is not None:
+ data["xy"] = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR])
+ else:
+ data["hue"] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535)
+ data["sat"] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255)
if ATTR_BRIGHTNESS in kwargs:
data["bri"] = kwargs[ATTR_BRIGHTNESS]
diff --git a/homeassistant/components/deconz/lock.py b/homeassistant/components/deconz/lock.py
index 175d422ea1b..4d428af3673 100644
--- a/homeassistant/components/deconz/lock.py
+++ b/homeassistant/components/deconz/lock.py
@@ -9,15 +9,12 @@ from .gateway import get_gateway_from_config_entry
async def async_setup_entry(hass, config_entry, async_add_entities):
- """Set up locks for deCONZ component.
-
- Locks are based on the same device class as lights in deCONZ.
- """
+ """Set up locks for deCONZ component."""
gateway = get_gateway_from_config_entry(hass, config_entry)
gateway.entities[DOMAIN] = set()
@callback
- def async_add_lock(lights):
+ def async_add_lock(lights=gateway.api.lights.values()):
"""Add lock from deCONZ."""
entities = []
@@ -35,7 +32,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
)
)
- async_add_lock(gateway.api.lights.values())
+ async_add_lock()
class DeconzLock(DeconzDevice, LockEntity):
@@ -46,14 +43,12 @@ class DeconzLock(DeconzDevice, LockEntity):
@property
def is_locked(self):
"""Return true if lock is on."""
- return self._device.state
+ return self._device.is_locked
async def async_lock(self, **kwargs):
"""Lock the lock."""
- data = {"on": True}
- await self._device.async_set_state(data)
+ await self._device.lock()
async def async_unlock(self, **kwargs):
"""Unlock the lock."""
- data = {"on": False}
- await self._device.async_set_state(data)
+ await self._device.unlock()
diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json
index 6a47864375e..c2846f8c57f 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==73"],
+ "requirements": ["pydeconz==76"],
"ssdp": [
{
"manufacturer": "Royal Philips Electronics"
diff --git a/homeassistant/components/deconz/scene.py b/homeassistant/components/deconz/scene.py
index 9ca7f39f034..4fbc1bfe453 100644
--- a/homeassistant/components/deconz/scene.py
+++ b/homeassistant/components/deconz/scene.py
@@ -14,7 +14,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
gateway = get_gateway_from_config_entry(hass, config_entry)
@callback
- def async_add_scene(scenes):
+ def async_add_scene(scenes=gateway.api.scenes.values()):
"""Add scene from deCONZ."""
entities = [DeconzScene(scene, gateway) for scene in scenes]
@@ -27,7 +27,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
)
)
- async_add_scene(gateway.api.scenes.values())
+ async_add_scene()
class DeconzScene(Scene):
diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py
index 7b55dbbf823..9d71fd0a9f9 100644
--- a/homeassistant/components/deconz/sensor.py
+++ b/homeassistant/components/deconz/sensor.py
@@ -76,7 +76,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
battery_handler = DeconzBatteryHandler(gateway)
@callback
- def async_add_sensor(sensors):
+ def async_add_sensor(sensors=gateway.api.sensors.values()):
"""Add sensors from deCONZ.
Create DeconzBattery if sensor has a battery attribute.
diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py
index a90f770eb9b..d524354ff0b 100644
--- a/homeassistant/components/deconz/services.py
+++ b/homeassistant/components/deconz/services.py
@@ -1,4 +1,7 @@
"""deCONZ services."""
+
+import asyncio
+
from pydeconz.utils import normalize_bridge_id
import voluptuous as vol
@@ -143,10 +146,10 @@ async def async_refresh_devices_service(hass, data):
await gateway.api.refresh_state()
gateway.ignore_state_updates = False
- gateway.async_add_device_callback(NEW_GROUP, list(gateway.api.groups.values()))
- gateway.async_add_device_callback(NEW_LIGHT, list(gateway.api.lights.values()))
- gateway.async_add_device_callback(NEW_SCENE, list(gateway.api.scenes.values()))
- gateway.async_add_device_callback(NEW_SENSOR, list(gateway.api.sensors.values()))
+ gateway.async_add_device_callback(NEW_GROUP, force=True)
+ gateway.async_add_device_callback(NEW_LIGHT, force=True)
+ gateway.async_add_device_callback(NEW_SCENE, force=True)
+ gateway.async_add_device_callback(NEW_SENSOR, force=True)
async def async_remove_orphaned_entries_service(hass, data):
@@ -155,8 +158,10 @@ async def async_remove_orphaned_entries_service(hass, data):
if CONF_BRIDGE_ID in data:
gateway = hass.data[DOMAIN][normalize_bridge_id(data[CONF_BRIDGE_ID])]
- entity_registry = await hass.helpers.entity_registry.async_get_registry()
- device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_registry, entity_registry = await asyncio.gather(
+ hass.helpers.device_registry.async_get_registry(),
+ hass.helpers.entity_registry.async_get_registry(),
+ )
entity_entries = async_entries_for_config_entry(
entity_registry, gateway.config_entry.entry_id
@@ -207,5 +212,12 @@ async def async_remove_orphaned_entries_service(hass, data):
# Remove devices that don't belong to any entity
for device_id in devices_to_be_removed:
- if len(async_entries_for_device(entity_registry, device_id)) == 0:
+ if (
+ len(
+ async_entries_for_device(
+ entity_registry, device_id, include_disabled_entities=True
+ )
+ )
+ == 0
+ ):
device_registry.async_remove_device(device_id)
diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py
index 509282e45f2..f497e06c7af 100644
--- a/homeassistant/components/deconz/switch.py
+++ b/homeassistant/components/deconz/switch.py
@@ -17,7 +17,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
gateway.entities[DOMAIN] = set()
@callback
- def async_add_switch(lights):
+ def async_add_switch(lights=gateway.api.lights.values()):
"""Add switch from deCONZ."""
entities = []
@@ -43,7 +43,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
)
)
- async_add_switch(gateway.api.lights.values())
+ async_add_switch()
class DeconzPowerPlug(DeconzDevice, SwitchEntity):
@@ -75,14 +75,12 @@ class DeconzSiren(DeconzDevice, SwitchEntity):
@property
def is_on(self):
"""Return true if switch is on."""
- return self._device.alert == "lselect"
+ return self._device.is_on
async def async_turn_on(self, **kwargs):
"""Turn on switch."""
- data = {"alert": "lselect"}
- await self._device.async_set_state(data)
+ await self._device.turn_on()
async def async_turn_off(self, **kwargs):
"""Turn off switch."""
- data = {"alert": "none"}
- await self._device.async_set_state(data)
+ await self._device.turn_off()
diff --git a/homeassistant/components/deconz/translations/ka.json b/homeassistant/components/deconz/translations/ka.json
new file mode 100644
index 00000000000..932e521a8ad
--- /dev/null
+++ b/homeassistant/components/deconz/translations/ka.json
@@ -0,0 +1,7 @@
+{
+ "device_automation": {
+ "trigger_type": {
+ "remote_button_rotated_fast": "\u10e6\u10d8\u10da\u10d0\u10d9\u10d8 \u10e1\u10ec\u10e0\u10d0\u10e4\u10d0\u10d3 \u10d1\u10e0\u10e3\u10dc\u10d0\u10d5\u10e1 \" {subtype} \""
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/deconz/translations/lb.json b/homeassistant/components/deconz/translations/lb.json
index 25adae28583..bb556842194 100644
--- a/homeassistant/components/deconz/translations/lb.json
+++ b/homeassistant/components/deconz/translations/lb.json
@@ -66,6 +66,7 @@
"remote_button_quadruple_press": "\"{subtype}\" Kn\u00e4ppche v\u00e9ier mol gedr\u00e9ckt",
"remote_button_quintuple_press": "\"{subtype}\" Kn\u00e4ppche f\u00ebnnef mol gedr\u00e9ckt",
"remote_button_rotated": "Kn\u00e4ppche gedr\u00e9int \"{subtype}\"",
+ "remote_button_rotated_fast": "Kn\u00e4ppche schnell gedr\u00e9int \"{subtype}\"",
"remote_button_rotation_stopped": "Kn\u00e4ppchen Rotatioun \"{subtype}\" gestoppt",
"remote_button_short_press": "\"{subtype}\" Kn\u00e4ppche gedr\u00e9ckt",
"remote_button_short_release": "\"{subtype}\" Kn\u00e4ppche lassgelooss",
diff --git a/homeassistant/components/deconz/translations/pl.json b/homeassistant/components/deconz/translations/pl.json
index e6c89d53b4a..24a3ba61706 100644
--- a/homeassistant/components/deconz/translations/pl.json
+++ b/homeassistant/components/deconz/translations/pl.json
@@ -14,11 +14,11 @@
"flow_title": "Bramka deCONZ Zigbee ({host})",
"step": {
"hassio_confirm": {
- "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}?",
+ "description": "Czy chcesz skonfigurowa\u0107 Home Assistanta, aby po\u0142\u0105czy\u0142 si\u0119 z bramk\u0105 deCONZ dostarczon\u0105 przez dodatek Hass.io {addon}?",
"title": "Bramka deCONZ Zigbee przez dodatek Hass.io"
},
"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\"",
+ "description": "Odblokuj bramk\u0119 deCONZ, aby zarejestrowa\u0107 j\u0105 w Home Assistantem. \n\n 1. Przejd\u017a do ustawienia deCONZ > bramka > Zaawansowane\n 2. Naci\u015bnij przycisk \"Uwierzytelnij aplikacj\u0119\"",
"title": "Po\u0142\u0105czenie z deCONZ"
},
"manual_input": {
diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json
index 8c6a3dde6cf..9a533092b8b 100644
--- a/homeassistant/components/default_config/manifest.json
+++ b/homeassistant/components/default_config/manifest.json
@@ -5,8 +5,14 @@
"dependencies": [
"automation",
"cloud",
+ "counter",
"frontend",
"history",
+ "input_boolean",
+ "input_datetime",
+ "input_number",
+ "input_select",
+ "input_text",
"logbook",
"map",
"media_source",
@@ -18,16 +24,11 @@
"sun",
"system_health",
"tag",
+ "timer",
"updater",
+ "webhook",
"zeroconf",
- "zone",
- "input_boolean",
- "input_datetime",
- "input_text",
- "input_number",
- "input_select",
- "counter",
- "timer"
+ "zone"
],
"codeowners": []
}
diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py
index eea613cc401..09c3d27a1bc 100644
--- a/homeassistant/components/demo/__init__.py
+++ b/homeassistant/components/demo/__init__.py
@@ -19,6 +19,7 @@ COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [
"light",
"lock",
"media_player",
+ "number",
"sensor",
"switch",
"vacuum",
diff --git a/homeassistant/components/demo/number.py b/homeassistant/components/demo/number.py
new file mode 100644
index 00000000000..a8b9cb0ac4d
--- /dev/null
+++ b/homeassistant/components/demo/number.py
@@ -0,0 +1,130 @@
+"""Demo platform that offers a fake Number entity."""
+import voluptuous as vol
+
+from homeassistant.components.number import NumberEntity
+from homeassistant.const import DEVICE_DEFAULT_NAME
+
+from . import DOMAIN
+
+
+async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
+ """Set up the demo Number entity."""
+ async_add_entities(
+ [
+ DemoNumber(
+ "volume1",
+ "volume",
+ 42.0,
+ "mdi:volume-high",
+ False,
+ ),
+ DemoNumber(
+ "pwm1",
+ "PWM 1",
+ 42.0,
+ "mdi:square-wave",
+ False,
+ 0.0,
+ 1.0,
+ 0.01,
+ ),
+ ]
+ )
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up the Demo config entry."""
+ await async_setup_platform(hass, {}, async_add_entities)
+
+
+class DemoNumber(NumberEntity):
+ """Representation of a demo Number entity."""
+
+ def __init__(
+ self,
+ unique_id,
+ name,
+ state,
+ icon,
+ assumed,
+ min_value=None,
+ max_value=None,
+ step=None,
+ ):
+ """Initialize the Demo Number entity."""
+ self._unique_id = unique_id
+ self._name = name or DEVICE_DEFAULT_NAME
+ self._state = state
+ self._icon = icon
+ self._assumed = assumed
+ self._min_value = min_value
+ self._max_value = max_value
+ self._step = step
+
+ @property
+ def device_info(self):
+ """Return device info."""
+ return {
+ "identifiers": {
+ # Serial numbers are unique identifiers within a specific domain
+ (DOMAIN, self.unique_id)
+ },
+ "name": self.name,
+ }
+
+ @property
+ def unique_id(self):
+ """Return the unique id."""
+ return self._unique_id
+
+ @property
+ def should_poll(self):
+ """No polling needed for a demo Number entity."""
+ return False
+
+ @property
+ def name(self):
+ """Return the name of the device if any."""
+ return self._name
+
+ @property
+ def icon(self):
+ """Return the icon to use for device if any."""
+ return self._icon
+
+ @property
+ def assumed_state(self):
+ """Return if the state is based on assumptions."""
+ return self._assumed
+
+ @property
+ def state(self):
+ """Return the current value."""
+ return self._state
+
+ @property
+ def min_value(self):
+ """Return the minimum value."""
+ return self._min_value or super().min_value
+
+ @property
+ def max_value(self):
+ """Return the maximum value."""
+ return self._max_value or super().max_value
+
+ @property
+ def step(self):
+ """Return the value step."""
+ return self._step or super().step
+
+ async def async_set_value(self, value):
+ """Update the current value."""
+ num_value = float(value)
+
+ if num_value < self.min_value or num_value > self.max_value:
+ raise vol.Invalid(
+ f"Invalid value for {self.entity_id}: {value} (range {self.min_value} - {self.max_value})"
+ )
+
+ self._state = num_value
+ self.async_write_ha_state()
diff --git a/homeassistant/components/demo/translations/it.json b/homeassistant/components/demo/translations/it.json
index 50939df5631..dc3e218895b 100644
--- a/homeassistant/components/demo/translations/it.json
+++ b/homeassistant/components/demo/translations/it.json
@@ -1,12 +1,6 @@
{
"options": {
"step": {
- "init": {
- "data": {
- "one": "uno",
- "other": "altri"
- }
- },
"options_1": {
"data": {
"bool": "Valore booleano facoltativo",
diff --git a/homeassistant/components/demo/weather.py b/homeassistant/components/demo/weather.py
index 3c87cd1c27c..9792a45bbdf 100644
--- a/homeassistant/components/demo/weather.py
+++ b/homeassistant/components/demo/weather.py
@@ -2,6 +2,20 @@
from datetime import timedelta
from homeassistant.components.weather import (
+ ATTR_CONDITION_CLOUDY,
+ ATTR_CONDITION_EXCEPTIONAL,
+ ATTR_CONDITION_FOG,
+ ATTR_CONDITION_HAIL,
+ ATTR_CONDITION_LIGHTNING,
+ ATTR_CONDITION_LIGHTNING_RAINY,
+ ATTR_CONDITION_PARTLYCLOUDY,
+ ATTR_CONDITION_POURING,
+ ATTR_CONDITION_RAINY,
+ ATTR_CONDITION_SNOWY,
+ ATTR_CONDITION_SNOWY_RAINY,
+ ATTR_CONDITION_SUNNY,
+ ATTR_CONDITION_WINDY,
+ ATTR_CONDITION_WINDY_VARIANT,
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_PRECIPITATION,
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
@@ -14,20 +28,20 @@ from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
import homeassistant.util.dt as dt_util
CONDITION_CLASSES = {
- "cloudy": [],
- "fog": [],
- "hail": [],
- "lightning": [],
- "lightning-rainy": [],
- "partlycloudy": [],
- "pouring": [],
- "rainy": ["shower rain"],
- "snowy": [],
- "snowy-rainy": [],
- "sunny": ["sunshine"],
- "windy": [],
- "windy-variant": [],
- "exceptional": [],
+ ATTR_CONDITION_CLOUDY: [],
+ ATTR_CONDITION_FOG: [],
+ ATTR_CONDITION_HAIL: [],
+ ATTR_CONDITION_LIGHTNING: [],
+ ATTR_CONDITION_LIGHTNING_RAINY: [],
+ ATTR_CONDITION_PARTLYCLOUDY: [],
+ ATTR_CONDITION_POURING: [],
+ ATTR_CONDITION_RAINY: ["shower rain"],
+ ATTR_CONDITION_SNOWY: [],
+ ATTR_CONDITION_SNOWY_RAINY: [],
+ ATTR_CONDITION_SUNNY: ["sunshine"],
+ ATTR_CONDITION_WINDY: [],
+ ATTR_CONDITION_WINDY_VARIANT: [],
+ ATTR_CONDITION_EXCEPTIONAL: [],
}
@@ -49,13 +63,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
0.5,
TEMP_CELSIUS,
[
- ["rainy", 1, 22, 15, 60],
- ["rainy", 5, 19, 8, 30],
- ["cloudy", 0, 15, 9, 10],
- ["sunny", 0, 12, 6, 0],
- ["partlycloudy", 2, 14, 7, 20],
- ["rainy", 15, 18, 7, 0],
- ["fog", 0.2, 21, 12, 100],
+ [ATTR_CONDITION_RAINY, 1, 22, 15, 60],
+ [ATTR_CONDITION_RAINY, 5, 19, 8, 30],
+ [ATTR_CONDITION_CLOUDY, 0, 15, 9, 10],
+ [ATTR_CONDITION_SUNNY, 0, 12, 6, 0],
+ [ATTR_CONDITION_PARTLYCLOUDY, 2, 14, 7, 20],
+ [ATTR_CONDITION_RAINY, 15, 18, 7, 0],
+ [ATTR_CONDITION_FOG, 0.2, 21, 12, 100],
],
),
DemoWeather(
@@ -67,13 +81,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
4.8,
TEMP_FAHRENHEIT,
[
- ["snowy", 2, -10, -15, 60],
- ["partlycloudy", 1, -13, -14, 25],
- ["sunny", 0, -18, -22, 70],
- ["sunny", 0.1, -23, -23, 90],
- ["snowy", 4, -19, -20, 40],
- ["sunny", 0.3, -14, -19, 0],
- ["sunny", 0, -9, -12, 0],
+ [ATTR_CONDITION_SNOWY, 2, -10, -15, 60],
+ [ATTR_CONDITION_PARTLYCLOUDY, 1, -13, -14, 25],
+ [ATTR_CONDITION_SUNNY, 0, -18, -22, 70],
+ [ATTR_CONDITION_SUNNY, 0.1, -23, -23, 90],
+ [ATTR_CONDITION_SNOWY, 4, -19, -20, 40],
+ [ATTR_CONDITION_SUNNY, 0.3, -14, -19, 0],
+ [ATTR_CONDITION_SUNNY, 0, -9, -12, 0],
],
),
]
diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json
index 86bee686764..c8341a3ec2c 100644
--- a/homeassistant/components/denonavr/manifest.json
+++ b/homeassistant/components/denonavr/manifest.json
@@ -3,7 +3,7 @@
"name": "Denon AVR Network Receivers",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/denonavr",
- "requirements": ["denonavr==0.9.5", "getmac==0.8.2"],
+ "requirements": ["denonavr==0.9.7", "getmac==0.8.2"],
"codeowners": ["@scarface-4711", "@starkillerOG"],
"ssdp": [
{
diff --git a/homeassistant/components/denonavr/translations/et.json b/homeassistant/components/denonavr/translations/et.json
index 9f793f03023..45869680bda 100644
--- a/homeassistant/components/denonavr/translations/et.json
+++ b/homeassistant/components/denonavr/translations/et.json
@@ -10,7 +10,7 @@
"error": {
"discovery_error": "Denon AVR Network Receiver'i avastamine nurjus"
},
- "flow_title": "Denon AVR v\u00f5rguvastuv\u00f5tja: {nimi}",
+ "flow_title": "Denon AVR v\u00f5rguvastuv\u00f5tja: {name}",
"step": {
"confirm": {
"description": "Palun kinnita vastuv\u00f5tja lisamine",
diff --git a/homeassistant/components/denonavr/translations/hu.json b/homeassistant/components/denonavr/translations/hu.json
index 3b2d79a34a7..aa56cb47741 100644
--- a/homeassistant/components/denonavr/translations/hu.json
+++ b/homeassistant/components/denonavr/translations/hu.json
@@ -1,7 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "cannot_connect": "Nem siker\u00fclt csatlakozni, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja \u00fajra. A h\u00e1l\u00f3zati \u00e9s Ethernet k\u00e1belek kih\u00faz\u00e1sa \u00e9s \u00fajracsatlakoztat\u00e1sa seg\u00edthet"
+ },
+ "error": {
+ "discovery_error": "Nem siker\u00fclt megtal\u00e1lni a Denon AVR h\u00e1l\u00f3zati er\u0151s\u00edt\u0151t"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/denonavr/translations/ka.json b/homeassistant/components/denonavr/translations/ka.json
new file mode 100644
index 00000000000..fbb3a50cb06
--- /dev/null
+++ b/homeassistant/components/denonavr/translations/ka.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "\u10d3\u10d0\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d4\u10d1\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0, \u10d2\u10d7\u10ee\u10dd\u10d5\u10d7 \u10e1\u10ea\u10d0\u10d3\u10dd\u10d7 \u10ee\u10d4\u10da\u10d0\u10ee\u10da\u10d0, \u10e8\u10d4\u10e1\u10d0\u10eb\u10da\u10dd\u10d0 \u10d9\u10d5\u10d4\u10d1\u10d8\u10e1 \u10d3\u10d0 \u10e5\u10e1\u10d4\u10da\u10d8\u10e1 \u10d9\u10d0\u10d1\u10d4\u10da\u10d4\u10d1\u10d8\u10e1 \u10d2\u10d0\u10d7\u10d8\u10e8\u10d5\u10d0\u10db \u10d3\u10d0\u10d2\u10d4\u10ee\u10db\u10d0\u10e0\u10dd\u10d7"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/denonavr/translations/nl.json b/homeassistant/components/denonavr/translations/nl.json
index eac5783809c..9f79aebeb60 100644
--- a/homeassistant/components/denonavr/translations/nl.json
+++ b/homeassistant/components/denonavr/translations/nl.json
@@ -3,7 +3,7 @@
"abort": {
"already_configured": "Apparaat is al geconfigureerd"
},
- "flow_title": "Denon AVR Network Receiver: {naam}",
+ "flow_title": "Denon AVR Network Receiver: {name}",
"step": {
"confirm": {
"title": "Denon AVR Network Receivers"
diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py
index 6d8e2307145..d785ee826e8 100644
--- a/homeassistant/components/device_tracker/__init__.py
+++ b/homeassistant/components/device_tracker/__init__.py
@@ -1,24 +1,18 @@
"""Provide functionality to keep track of devices."""
-import asyncio
-
-import voluptuous as vol
-
-from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME
-from homeassistant.helpers import discovery
-import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.event import async_track_utc_time_change
-from homeassistant.helpers.typing import ConfigType, GPSType, HomeAssistantType
+from homeassistant.const import ( # noqa: F401 pylint: disable=unused-import
+ ATTR_GPS_ACCURACY,
+ STATE_HOME,
+)
+from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from homeassistant.loader import bind_hass
-from . import legacy, setup
from .config_entry import ( # noqa: F401 pylint: disable=unused-import
async_setup_entry,
async_unload_entry,
)
-from .const import (
+from .const import ( # noqa: F401 pylint: disable=unused-import
ATTR_ATTRIBUTES,
ATTR_BATTERY,
- ATTR_CONSIDER_HOME,
ATTR_DEV_ID,
ATTR_GPS,
ATTR_HOST_NAME,
@@ -29,60 +23,21 @@ from .const import (
CONF_NEW_DEVICE_DEFAULTS,
CONF_SCAN_INTERVAL,
CONF_TRACK_NEW,
- DEFAULT_CONSIDER_HOME,
- DEFAULT_TRACK_NEW,
DOMAIN,
- PLATFORM_TYPE_LEGACY,
SOURCE_TYPE_BLUETOOTH,
SOURCE_TYPE_BLUETOOTH_LE,
SOURCE_TYPE_GPS,
SOURCE_TYPE_ROUTER,
)
-from .legacy import DeviceScanner # noqa: F401 pylint: disable=unused-import
-
-SERVICE_SEE = "see"
-
-SOURCE_TYPES = (
- SOURCE_TYPE_GPS,
- SOURCE_TYPE_ROUTER,
- SOURCE_TYPE_BLUETOOTH,
- SOURCE_TYPE_BLUETOOTH_LE,
-)
-
-NEW_DEVICE_DEFAULTS_SCHEMA = vol.Any(
- None,
- vol.Schema({vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean}),
-)
-PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(
- {
- vol.Optional(CONF_SCAN_INTERVAL): cv.time_period,
- vol.Optional(CONF_TRACK_NEW): cv.boolean,
- vol.Optional(CONF_CONSIDER_HOME, default=DEFAULT_CONSIDER_HOME): vol.All(
- cv.time_period, cv.positive_timedelta
- ),
- vol.Optional(CONF_NEW_DEVICE_DEFAULTS, default={}): NEW_DEVICE_DEFAULTS_SCHEMA,
- }
-)
-PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE.extend(PLATFORM_SCHEMA.schema)
-SERVICE_SEE_PAYLOAD_SCHEMA = vol.Schema(
- vol.All(
- cv.has_at_least_one_key(ATTR_MAC, ATTR_DEV_ID),
- {
- ATTR_MAC: cv.string,
- ATTR_DEV_ID: cv.string,
- ATTR_HOST_NAME: cv.string,
- ATTR_LOCATION_NAME: cv.string,
- ATTR_GPS: cv.gps,
- ATTR_GPS_ACCURACY: cv.positive_int,
- ATTR_BATTERY: cv.positive_int,
- ATTR_ATTRIBUTES: dict,
- ATTR_SOURCE_TYPE: vol.In(SOURCE_TYPES),
- ATTR_CONSIDER_HOME: cv.time_period,
- # Temp workaround for iOS app introduced in 0.65
- vol.Optional("battery_status"): str,
- vol.Optional("hostname"): str,
- },
- )
+from .legacy import ( # noqa: F401 pylint: disable=unused-import
+ PLATFORM_SCHEMA,
+ PLATFORM_SCHEMA_BASE,
+ SERVICE_SEE,
+ SERVICE_SEE_PAYLOAD_SCHEMA,
+ SOURCE_TYPES,
+ DeviceScanner,
+ async_setup_integration as async_setup_legacy_integration,
+ see,
)
@@ -92,78 +47,8 @@ def is_on(hass: HomeAssistantType, entity_id: str):
return hass.states.is_state(entity_id, STATE_HOME)
-def see(
- hass: HomeAssistantType,
- mac: str = None,
- dev_id: str = None,
- host_name: str = None,
- location_name: str = None,
- gps: GPSType = None,
- gps_accuracy=None,
- battery: int = None,
- attributes: dict = None,
-):
- """Call service to notify you see device."""
- data = {
- key: value
- for key, value in (
- (ATTR_MAC, mac),
- (ATTR_DEV_ID, dev_id),
- (ATTR_HOST_NAME, host_name),
- (ATTR_LOCATION_NAME, location_name),
- (ATTR_GPS, gps),
- (ATTR_GPS_ACCURACY, gps_accuracy),
- (ATTR_BATTERY, battery),
- )
- if value is not None
- }
- if attributes:
- data[ATTR_ATTRIBUTES] = attributes
- hass.services.call(DOMAIN, SERVICE_SEE, data)
-
-
async def async_setup(hass: HomeAssistantType, config: ConfigType):
"""Set up the device tracker."""
- tracker = await legacy.get_tracker(hass, config)
+ await async_setup_legacy_integration(hass, config)
- legacy_platforms = await setup.async_extract_config(hass, config)
-
- setup_tasks = [
- legacy_platform.async_setup_legacy(hass, tracker)
- for legacy_platform in legacy_platforms
- ]
-
- if setup_tasks:
- await asyncio.wait(setup_tasks)
-
- async def async_platform_discovered(p_type, info):
- """Load a platform."""
- platform = await setup.async_create_platform_type(hass, config, p_type, {})
-
- if platform is None or platform.type != PLATFORM_TYPE_LEGACY:
- return
-
- await platform.async_setup_legacy(hass, tracker, info)
-
- discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered)
-
- # Clean up stale devices
- async_track_utc_time_change(
- hass, tracker.async_update_stale, second=range(0, 60, 5)
- )
-
- async def async_see_service(call):
- """Service to see a device."""
- # Temp workaround for iOS, introduced in 0.65
- data = dict(call.data)
- data.pop("hostname", None)
- data.pop("battery_status", None)
- await tracker.async_see(**data)
-
- hass.services.async_register(
- DOMAIN, SERVICE_SEE, async_see_service, SERVICE_SEE_PAYLOAD_SCHEMA
- )
-
- # restore
- await tracker.async_setup_tracked_device()
return True
diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py
index 038fad06680..5f60d84f406 100644
--- a/homeassistant/components/device_tracker/legacy.py
+++ b/homeassistant/components/device_tracker/legacy.py
@@ -2,8 +2,10 @@
import asyncio
from datetime import timedelta
import hashlib
-from typing import Any, List, Sequence
+from types import ModuleType
+from typing import Any, Callable, Dict, List, Optional, Sequence
+import attr
import voluptuous as vol
from homeassistant import util
@@ -25,32 +27,343 @@ from homeassistant.const import (
)
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
+from homeassistant.helpers import config_per_platform, discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_registry import async_get_registry
+from homeassistant.helpers.event import (
+ async_track_time_interval,
+ async_track_utc_time_change,
+)
from homeassistant.helpers.restore_state import RestoreEntity
-from homeassistant.helpers.typing import GPSType, HomeAssistantType
-import homeassistant.util.dt as dt_util
+from homeassistant.helpers.typing import ConfigType, GPSType, HomeAssistantType
+from homeassistant.setup import async_prepare_setup_platform
+from homeassistant.util import dt as dt_util
from homeassistant.util.yaml import dump
from .const import (
+ ATTR_ATTRIBUTES,
ATTR_BATTERY,
+ ATTR_CONSIDER_HOME,
+ ATTR_DEV_ID,
+ ATTR_GPS,
ATTR_HOST_NAME,
+ ATTR_LOCATION_NAME,
ATTR_MAC,
ATTR_SOURCE_TYPE,
CONF_CONSIDER_HOME,
CONF_NEW_DEVICE_DEFAULTS,
+ CONF_SCAN_INTERVAL,
CONF_TRACK_NEW,
DEFAULT_CONSIDER_HOME,
DEFAULT_TRACK_NEW,
DOMAIN,
LOGGER,
+ PLATFORM_TYPE_LEGACY,
+ SCAN_INTERVAL,
+ SOURCE_TYPE_BLUETOOTH,
+ SOURCE_TYPE_BLUETOOTH_LE,
SOURCE_TYPE_GPS,
+ SOURCE_TYPE_ROUTER,
+)
+
+SERVICE_SEE = "see"
+
+SOURCE_TYPES = (
+ SOURCE_TYPE_GPS,
+ SOURCE_TYPE_ROUTER,
+ SOURCE_TYPE_BLUETOOTH,
+ SOURCE_TYPE_BLUETOOTH_LE,
+)
+
+NEW_DEVICE_DEFAULTS_SCHEMA = vol.Any(
+ None,
+ vol.Schema({vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean}),
+)
+PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(
+ {
+ vol.Optional(CONF_SCAN_INTERVAL): cv.time_period,
+ vol.Optional(CONF_TRACK_NEW): cv.boolean,
+ vol.Optional(CONF_CONSIDER_HOME, default=DEFAULT_CONSIDER_HOME): vol.All(
+ cv.time_period, cv.positive_timedelta
+ ),
+ vol.Optional(CONF_NEW_DEVICE_DEFAULTS, default={}): NEW_DEVICE_DEFAULTS_SCHEMA,
+ }
+)
+PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE.extend(PLATFORM_SCHEMA.schema)
+
+SERVICE_SEE_PAYLOAD_SCHEMA = vol.Schema(
+ vol.All(
+ cv.has_at_least_one_key(ATTR_MAC, ATTR_DEV_ID),
+ {
+ ATTR_MAC: cv.string,
+ ATTR_DEV_ID: cv.string,
+ ATTR_HOST_NAME: cv.string,
+ ATTR_LOCATION_NAME: cv.string,
+ ATTR_GPS: cv.gps,
+ ATTR_GPS_ACCURACY: cv.positive_int,
+ ATTR_BATTERY: cv.positive_int,
+ ATTR_ATTRIBUTES: dict,
+ ATTR_SOURCE_TYPE: vol.In(SOURCE_TYPES),
+ ATTR_CONSIDER_HOME: cv.time_period,
+ # Temp workaround for iOS app introduced in 0.65
+ vol.Optional("battery_status"): str,
+ vol.Optional("hostname"): str,
+ },
+ )
)
YAML_DEVICES = "known_devices.yaml"
EVENT_NEW_DEVICE = "device_tracker_new_device"
+def see(
+ hass: HomeAssistantType,
+ mac: str = None,
+ dev_id: str = None,
+ host_name: str = None,
+ location_name: str = None,
+ gps: GPSType = None,
+ gps_accuracy=None,
+ battery: int = None,
+ attributes: dict = None,
+):
+ """Call service to notify you see device."""
+ data = {
+ key: value
+ for key, value in (
+ (ATTR_MAC, mac),
+ (ATTR_DEV_ID, dev_id),
+ (ATTR_HOST_NAME, host_name),
+ (ATTR_LOCATION_NAME, location_name),
+ (ATTR_GPS, gps),
+ (ATTR_GPS_ACCURACY, gps_accuracy),
+ (ATTR_BATTERY, battery),
+ )
+ if value is not None
+ }
+ if attributes:
+ data[ATTR_ATTRIBUTES] = attributes
+ hass.services.call(DOMAIN, SERVICE_SEE, data)
+
+
+async def async_setup_integration(hass: HomeAssistantType, config: ConfigType) -> None:
+ """Set up the legacy integration."""
+ tracker = await get_tracker(hass, config)
+
+ legacy_platforms = await async_extract_config(hass, config)
+
+ setup_tasks = [
+ legacy_platform.async_setup_legacy(hass, tracker)
+ for legacy_platform in legacy_platforms
+ ]
+
+ if setup_tasks:
+ await asyncio.wait(setup_tasks)
+
+ async def async_platform_discovered(p_type, info):
+ """Load a platform."""
+ platform = await async_create_platform_type(hass, config, p_type, {})
+
+ if platform is None or platform.type != PLATFORM_TYPE_LEGACY:
+ return
+
+ await platform.async_setup_legacy(hass, tracker, info)
+
+ discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered)
+
+ # Clean up stale devices
+ async_track_utc_time_change(
+ hass, tracker.async_update_stale, second=range(0, 60, 5)
+ )
+
+ async def async_see_service(call):
+ """Service to see a device."""
+ # Temp workaround for iOS, introduced in 0.65
+ data = dict(call.data)
+ data.pop("hostname", None)
+ data.pop("battery_status", None)
+ await tracker.async_see(**data)
+
+ hass.services.async_register(
+ DOMAIN, SERVICE_SEE, async_see_service, SERVICE_SEE_PAYLOAD_SCHEMA
+ )
+
+ # restore
+ await tracker.async_setup_tracked_device()
+
+
+@attr.s
+class DeviceTrackerPlatform:
+ """Class to hold platform information."""
+
+ LEGACY_SETUP = (
+ "async_get_scanner",
+ "get_scanner",
+ "async_setup_scanner",
+ "setup_scanner",
+ )
+
+ name: str = attr.ib()
+ platform: ModuleType = attr.ib()
+ config: Dict = attr.ib()
+
+ @property
+ def type(self):
+ """Return platform type."""
+ for methods, platform_type in ((self.LEGACY_SETUP, PLATFORM_TYPE_LEGACY),):
+ for meth in methods:
+ if hasattr(self.platform, meth):
+ return platform_type
+
+ return None
+
+ async def async_setup_legacy(self, hass, tracker, discovery_info=None):
+ """Set up a legacy platform."""
+ LOGGER.info("Setting up %s.%s", DOMAIN, self.type)
+ try:
+ scanner = None
+ setup = None
+ if hasattr(self.platform, "async_get_scanner"):
+ scanner = await self.platform.async_get_scanner(
+ hass, {DOMAIN: self.config}
+ )
+ elif hasattr(self.platform, "get_scanner"):
+ scanner = await hass.async_add_executor_job(
+ self.platform.get_scanner, hass, {DOMAIN: self.config}
+ )
+ elif hasattr(self.platform, "async_setup_scanner"):
+ setup = await self.platform.async_setup_scanner(
+ hass, self.config, tracker.async_see, discovery_info
+ )
+ elif hasattr(self.platform, "setup_scanner"):
+ setup = await hass.async_add_executor_job(
+ self.platform.setup_scanner,
+ hass,
+ self.config,
+ tracker.see,
+ discovery_info,
+ )
+ else:
+ raise HomeAssistantError("Invalid legacy device_tracker platform.")
+
+ if scanner:
+ async_setup_scanner_platform(
+ hass, self.config, scanner, tracker.async_see, self.type
+ )
+ return
+
+ if not setup:
+ LOGGER.error("Error setting up platform %s", self.type)
+ return
+
+ except Exception: # pylint: disable=broad-except
+ LOGGER.exception("Error setting up platform %s", self.type)
+
+
+async def async_extract_config(hass, config):
+ """Extract device tracker config and split between legacy and modern."""
+ legacy = []
+
+ for platform in await asyncio.gather(
+ *(
+ async_create_platform_type(hass, config, p_type, p_config)
+ for p_type, p_config in config_per_platform(config, DOMAIN)
+ )
+ ):
+ if platform is None:
+ continue
+
+ if platform.type == PLATFORM_TYPE_LEGACY:
+ legacy.append(platform)
+ else:
+ raise ValueError(
+ f"Unable to determine type for {platform.name}: {platform.type}"
+ )
+
+ return legacy
+
+
+async def async_create_platform_type(
+ hass, config, p_type, p_config
+) -> Optional[DeviceTrackerPlatform]:
+ """Determine type of platform."""
+ platform = await async_prepare_setup_platform(hass, config, DOMAIN, p_type)
+
+ if platform is None:
+ return None
+
+ return DeviceTrackerPlatform(p_type, platform, p_config)
+
+
+@callback
+def async_setup_scanner_platform(
+ hass: HomeAssistantType,
+ config: ConfigType,
+ scanner: Any,
+ async_see_device: Callable,
+ platform: str,
+):
+ """Set up the connect scanner-based platform to device tracker.
+
+ This method must be run in the event loop.
+ """
+ interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
+ update_lock = asyncio.Lock()
+ scanner.hass = hass
+
+ # Initial scan of each mac we also tell about host name for config
+ seen: Any = set()
+
+ async def async_device_tracker_scan(now: dt_util.dt.datetime):
+ """Handle interval matches."""
+ if update_lock.locked():
+ LOGGER.warning(
+ "Updating device list from %s took longer than the scheduled "
+ "scan interval %s",
+ platform,
+ interval,
+ )
+ return
+
+ async with update_lock:
+ found_devices = await scanner.async_scan_devices()
+
+ for mac in found_devices:
+ if mac in seen:
+ host_name = None
+ else:
+ host_name = await scanner.async_get_device_name(mac)
+ seen.add(mac)
+
+ try:
+ extra_attributes = await scanner.async_get_extra_attributes(mac)
+ except NotImplementedError:
+ extra_attributes = {}
+
+ kwargs = {
+ "mac": mac,
+ "host_name": host_name,
+ "source_type": SOURCE_TYPE_ROUTER,
+ "attributes": {
+ "scanner": scanner.__class__.__name__,
+ **extra_attributes,
+ },
+ }
+
+ zone_home = hass.states.get(hass.components.zone.ENTITY_ID_HOME)
+ if zone_home:
+ kwargs["gps"] = [
+ zone_home.attributes[ATTR_LATITUDE],
+ zone_home.attributes[ATTR_LONGITUDE],
+ ]
+ kwargs["gps_accuracy"] = 0
+
+ hass.async_create_task(async_see_device(**kwargs))
+
+ async_track_time_interval(hass, async_device_tracker_scan, interval)
+ hass.async_create_task(async_device_tracker_scan(None))
+
+
async def get_tracker(hass, config):
"""Create a tracker."""
yaml_path = hass.config.path(YAML_DEVICES)
@@ -349,17 +662,17 @@ class Device(RestoreEntity):
@property
def state_attributes(self):
"""Return the device state attributes."""
- attr = {ATTR_SOURCE_TYPE: self.source_type}
+ attributes = {ATTR_SOURCE_TYPE: self.source_type}
if self.gps:
- attr[ATTR_LATITUDE] = self.gps[0]
- attr[ATTR_LONGITUDE] = self.gps[1]
- attr[ATTR_GPS_ACCURACY] = self.gps_accuracy
+ attributes[ATTR_LATITUDE] = self.gps[0]
+ attributes[ATTR_LONGITUDE] = self.gps[1]
+ attributes[ATTR_GPS_ACCURACY] = self.gps_accuracy
if self.battery:
- attr[ATTR_BATTERY] = self.battery
+ attributes[ATTR_BATTERY] = self.battery
- return attr
+ return attributes
@property
def device_state_attributes(self):
@@ -453,13 +766,13 @@ class Device(RestoreEntity):
self.last_update_home = state.state == STATE_HOME
self.last_seen = dt_util.utcnow()
- for attr, var in (
+ for attribute, var in (
(ATTR_SOURCE_TYPE, "source_type"),
(ATTR_GPS_ACCURACY, "gps_accuracy"),
(ATTR_BATTERY, "battery"),
):
- if attr in state.attributes:
- setattr(self, var, state.attributes[attr])
+ if attribute in state.attributes:
+ setattr(self, var, state.attributes[attribute])
if ATTR_LONGITUDE in state.attributes:
self.gps = (
diff --git a/homeassistant/components/device_tracker/setup.py b/homeassistant/components/device_tracker/setup.py
deleted file mode 100644
index 133ea4eb414..00000000000
--- a/homeassistant/components/device_tracker/setup.py
+++ /dev/null
@@ -1,196 +0,0 @@
-"""Device tracker helpers."""
-import asyncio
-from types import ModuleType
-from typing import Any, Callable, Dict, Optional
-
-import attr
-
-from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE
-from homeassistant.core import callback
-from homeassistant.exceptions import HomeAssistantError
-from homeassistant.helpers import config_per_platform
-from homeassistant.helpers.event import async_track_time_interval
-from homeassistant.helpers.typing import ConfigType, HomeAssistantType
-from homeassistant.setup import async_prepare_setup_platform
-from homeassistant.util import dt as dt_util
-
-from .const import (
- CONF_SCAN_INTERVAL,
- DOMAIN,
- LOGGER,
- PLATFORM_TYPE_LEGACY,
- SCAN_INTERVAL,
- SOURCE_TYPE_ROUTER,
-)
-
-
-@attr.s
-class DeviceTrackerPlatform:
- """Class to hold platform information."""
-
- LEGACY_SETUP = (
- "async_get_scanner",
- "get_scanner",
- "async_setup_scanner",
- "setup_scanner",
- )
-
- name: str = attr.ib()
- platform: ModuleType = attr.ib()
- config: Dict = attr.ib()
-
- @property
- def type(self):
- """Return platform type."""
- for methods, platform_type in ((self.LEGACY_SETUP, PLATFORM_TYPE_LEGACY),):
- for meth in methods:
- if hasattr(self.platform, meth):
- return platform_type
-
- return None
-
- async def async_setup_legacy(self, hass, tracker, discovery_info=None):
- """Set up a legacy platform."""
- LOGGER.info("Setting up %s.%s", DOMAIN, self.type)
- try:
- scanner = None
- setup = None
- if hasattr(self.platform, "async_get_scanner"):
- scanner = await self.platform.async_get_scanner(
- hass, {DOMAIN: self.config}
- )
- elif hasattr(self.platform, "get_scanner"):
- scanner = await hass.async_add_executor_job(
- self.platform.get_scanner, hass, {DOMAIN: self.config}
- )
- elif hasattr(self.platform, "async_setup_scanner"):
- setup = await self.platform.async_setup_scanner(
- hass, self.config, tracker.async_see, discovery_info
- )
- elif hasattr(self.platform, "setup_scanner"):
- setup = await hass.async_add_executor_job(
- self.platform.setup_scanner,
- hass,
- self.config,
- tracker.see,
- discovery_info,
- )
- else:
- raise HomeAssistantError("Invalid legacy device_tracker platform.")
-
- if scanner:
- async_setup_scanner_platform(
- hass, self.config, scanner, tracker.async_see, self.type
- )
- return
-
- if not setup:
- LOGGER.error("Error setting up platform %s", self.type)
- return
-
- except Exception: # pylint: disable=broad-except
- LOGGER.exception("Error setting up platform %s", self.type)
-
-
-async def async_extract_config(hass, config):
- """Extract device tracker config and split between legacy and modern."""
- legacy = []
-
- for platform in await asyncio.gather(
- *(
- async_create_platform_type(hass, config, p_type, p_config)
- for p_type, p_config in config_per_platform(config, DOMAIN)
- )
- ):
- if platform is None:
- continue
-
- if platform.type == PLATFORM_TYPE_LEGACY:
- legacy.append(platform)
- else:
- raise ValueError(
- f"Unable to determine type for {platform.name}: {platform.type}"
- )
-
- return legacy
-
-
-async def async_create_platform_type(
- hass, config, p_type, p_config
-) -> Optional[DeviceTrackerPlatform]:
- """Determine type of platform."""
- platform = await async_prepare_setup_platform(hass, config, DOMAIN, p_type)
-
- if platform is None:
- return None
-
- return DeviceTrackerPlatform(p_type, platform, p_config)
-
-
-@callback
-def async_setup_scanner_platform(
- hass: HomeAssistantType,
- config: ConfigType,
- scanner: Any,
- async_see_device: Callable,
- platform: str,
-):
- """Set up the connect scanner-based platform to device tracker.
-
- This method must be run in the event loop.
- """
- interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
- update_lock = asyncio.Lock()
- scanner.hass = hass
-
- # Initial scan of each mac we also tell about host name for config
- seen: Any = set()
-
- async def async_device_tracker_scan(now: dt_util.dt.datetime):
- """Handle interval matches."""
- if update_lock.locked():
- LOGGER.warning(
- "Updating device list from %s took longer than the scheduled "
- "scan interval %s",
- platform,
- interval,
- )
- return
-
- async with update_lock:
- found_devices = await scanner.async_scan_devices()
-
- for mac in found_devices:
- if mac in seen:
- host_name = None
- else:
- host_name = await scanner.async_get_device_name(mac)
- seen.add(mac)
-
- try:
- extra_attributes = await scanner.async_get_extra_attributes(mac)
- except NotImplementedError:
- extra_attributes = {}
-
- kwargs = {
- "mac": mac,
- "host_name": host_name,
- "source_type": SOURCE_TYPE_ROUTER,
- "attributes": {
- "scanner": scanner.__class__.__name__,
- **extra_attributes,
- },
- }
-
- zone_home = hass.states.get(hass.components.zone.ENTITY_ID_HOME)
- if zone_home:
- kwargs["gps"] = [
- zone_home.attributes[ATTR_LATITUDE],
- zone_home.attributes[ATTR_LONGITUDE],
- ]
- kwargs["gps_accuracy"] = 0
-
- hass.async_create_task(async_see_device(**kwargs))
-
- async_track_time_interval(hass, async_device_tracker_scan, interval)
- hass.async_create_task(async_device_tracker_scan(None))
diff --git a/homeassistant/components/device_tracker/translations/hu.json b/homeassistant/components/device_tracker/translations/hu.json
index 2954376e314..11c81f5e5ec 100644
--- a/homeassistant/components/device_tracker/translations/hu.json
+++ b/homeassistant/components/device_tracker/translations/hu.json
@@ -3,6 +3,10 @@
"condition_type": {
"is_home": "{entity_name} otthon van",
"is_not_home": "{entity_name} nincs otthon"
+ },
+ "trigger_type": {
+ "enters": "{entity_name} bel\u00e9pett a z\u00f3n\u00e1ba",
+ "leaves": "{entity_name} elhagyta a z\u00f3n\u00e1t"
}
},
"state": {
diff --git a/homeassistant/components/device_tracker/translations/ka.json b/homeassistant/components/device_tracker/translations/ka.json
new file mode 100644
index 00000000000..8d01dff16e7
--- /dev/null
+++ b/homeassistant/components/device_tracker/translations/ka.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "trigger_type": {
+ "enters": "{entity_name} \u10e8\u10d4\u10d3\u10d8\u10e1 \u10d6\u10dd\u10dc\u10d0\u10e8\u10d8",
+ "leaves": "{entity_name} \u10e2\u10dd\u10d5\u10d4\u10d1\u10e1 \u10d6\u10dd\u10dc\u10d0\u10e1"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/device_tracker/translations/lb.json b/homeassistant/components/device_tracker/translations/lb.json
index 88d1b40b7ba..6b38b297b94 100644
--- a/homeassistant/components/device_tracker/translations/lb.json
+++ b/homeassistant/components/device_tracker/translations/lb.json
@@ -3,6 +3,10 @@
"condition_type": {
"is_home": "{entity_name} ass doheem",
"is_not_home": "{entity_name} ass net doheem"
+ },
+ "trigger_type": {
+ "enters": "{entity_name} k\u00ebnnt an eng Zone",
+ "leaves": "{entity_name} verl\u00e9isst eng Zone"
}
},
"state": {
diff --git a/homeassistant/components/device_tracker/translations/zh-Hans.json b/homeassistant/components/device_tracker/translations/zh-Hans.json
index 28adcdbdd1a..c019a3dcda8 100644
--- a/homeassistant/components/device_tracker/translations/zh-Hans.json
+++ b/homeassistant/components/device_tracker/translations/zh-Hans.json
@@ -3,6 +3,10 @@
"condition_type": {
"is_home": "{entity_name} \u5728\u5bb6",
"is_not_home": "{entity_name} \u4e0d\u5728\u5bb6"
+ },
+ "trigger_type": {
+ "enters": "{entity_name} \u8fdb\u5165\u533a\u57df",
+ "leaves": "{entity_name} \u79bb\u5f00\u533a\u57df"
}
},
"state": {
diff --git a/homeassistant/components/dexcom/translations/hu.json b/homeassistant/components/dexcom/translations/hu.json
new file mode 100644
index 00000000000..7a67a978ae1
--- /dev/null
+++ b/homeassistant/components/dexcom/translations/hu.json
@@ -0,0 +1,10 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/dexcom/translations/ka.json b/homeassistant/components/dexcom/translations/ka.json
new file mode 100644
index 00000000000..834e00eaa97
--- /dev/null
+++ b/homeassistant/components/dexcom/translations/ka.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u10d0\u10dc\u10d2\u10d0\u10e0\u10d8\u10e8\u10d8 \u10e3\u10d9\u10d5\u10d4 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/dexcom/translations/lb.json b/homeassistant/components/dexcom/translations/lb.json
index 86aced8ee11..7bc32ef7e5e 100644
--- a/homeassistant/components/dexcom/translations/lb.json
+++ b/homeassistant/components/dexcom/translations/lb.json
@@ -15,6 +15,7 @@
"server": "Server",
"username": "Benotzernumm"
},
+ "description": "F\u00ebll deng Desxcom Share Umeldungs Informatiounen aus",
"title": "Dexcom Integration ariichten"
}
}
diff --git a/homeassistant/components/dialogflow/translations/hu.json b/homeassistant/components/dialogflow/translations/hu.json
index 638adb4ae12..04427a1efed 100644
--- a/homeassistant/components/dialogflow/translations/hu.json
+++ b/homeassistant/components/dialogflow/translations/hu.json
@@ -1,5 +1,8 @@
{
"config": {
+ "create_entry": {
+ "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a [Dialogflow webhook integr\u00e1ci\u00f3j\u00e1t] ( {dialogflow_url} ). \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: \" {webhook_url} \"\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/json \n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1sd a [dokument\u00e1ci\u00f3t] ( {docs_url} )."
+ },
"step": {
"user": {
"description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani az Dialogflowt?",
diff --git a/homeassistant/components/dialogflow/translations/ka.json b/homeassistant/components/dialogflow/translations/ka.json
new file mode 100644
index 00000000000..75c4f0a922c
--- /dev/null
+++ b/homeassistant/components/dialogflow/translations/ka.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0. \u10e8\u10d4\u10e1\u10d0\u10eb\u10da\u10d4\u10d1\u10d4\u10da\u10d8\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0.",
+ "webhook_not_internet_accessible": "\u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 Home Assistant \u10d4\u10d9\u10d6\u10d4\u10db\u10de\u10da\u10d0\u10e0\u10d8 \u10e1\u10d0\u10ed\u10d8\u10e0\u10dd\u10e0\u10d4\u10d1\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10dc\u10d4\u10e2\u10d7\u10d0\u10dc \u10ec\u10d5\u10d3\u10dd\u10db\u10d0\u10e1 webhook \u10e8\u10d4\u10e2\u10e7\u10dd\u10d1\u10d8\u10dc\u10d4\u10d1\u10d4\u10d1\u10d8\u10e1 \u10db\u10d8\u10e1\u10d0\u10e6\u10d4\u10d1\u10d0\u10d3."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/dialogflow/translations/lb.json b/homeassistant/components/dialogflow/translations/lb.json
index 85d5d30128b..15730baccb6 100644
--- a/homeassistant/components/dialogflow/translations/lb.json
+++ b/homeassistant/components/dialogflow/translations/lb.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech."
+ "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech.",
+ "webhook_not_internet_accessible": "Deng Home Assistant Instanz muss iwwert Internet accessibel si fir Webhook Noriichten z'empf\u00e4nken."
},
"create_entry": {
"default": "Fir Evenementer un Home Assistant ze sch\u00e9cken, muss [Webhook Integratioun mat Dialogflow]({dialogflow_url}) ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\nLiest [Dokumentatioun]({docs_url}) fir w\u00e9ider D\u00e9tailer."
diff --git a/homeassistant/components/dialogflow/translations/pl.json b/homeassistant/components/dialogflow/translations/pl.json
index 031c69c6eca..c90ed20af74 100644
--- a/homeassistant/components/dialogflow/translations/pl.json
+++ b/homeassistant/components/dialogflow/translations/pl.json
@@ -5,7 +5,7 @@
"webhook_not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty webhook"
},
"create_entry": {
- "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant, 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/__init__.py b/homeassistant/components/directv/__init__.py
index 59682178d40..22a97b9e82e 100644
--- a/homeassistant/components/directv/__init__.py
+++ b/homeassistant/components/directv/__init__.py
@@ -22,7 +22,7 @@ from .const import (
DOMAIN,
)
-CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.120")
+CONFIG_SCHEMA = cv.deprecated(DOMAIN)
PLATFORMS = ["media_player", "remote"]
SCAN_INTERVAL = timedelta(seconds=30)
diff --git a/homeassistant/components/directv/manifest.json b/homeassistant/components/directv/manifest.json
index e4be9cc3e25..91685553596 100644
--- a/homeassistant/components/directv/manifest.json
+++ b/homeassistant/components/directv/manifest.json
@@ -2,7 +2,7 @@
"domain": "directv",
"name": "DirecTV",
"documentation": "https://www.home-assistant.io/integrations/directv",
- "requirements": ["directv==0.3.0"],
+ "requirements": ["directv==0.4.0"],
"codeowners": ["@ctalkington"],
"quality_scale": "gold",
"config_flow": true,
diff --git a/homeassistant/components/directv/translations/it.json b/homeassistant/components/directv/translations/it.json
index cdb2f8b418d..2b6e25d63ef 100644
--- a/homeassistant/components/directv/translations/it.json
+++ b/homeassistant/components/directv/translations/it.json
@@ -10,10 +10,6 @@
"flow_title": "DirecTV: {name}",
"step": {
"ssdp_confirm": {
- "data": {
- "one": "uno",
- "other": "altri"
- },
"description": "Vuoi impostare {name} ?"
},
"user": {
diff --git a/homeassistant/components/discord/manifest.json b/homeassistant/components/discord/manifest.json
index ccaa595f126..88bebe509b7 100644
--- a/homeassistant/components/discord/manifest.json
+++ b/homeassistant/components/discord/manifest.json
@@ -2,6 +2,6 @@
"domain": "discord",
"name": "Discord",
"documentation": "https://www.home-assistant.io/integrations/discord",
- "requirements": ["discord.py==1.4.1"],
+ "requirements": ["discord.py==1.5.1"],
"codeowners": []
}
diff --git a/homeassistant/components/doorbird/logbook.py b/homeassistant/components/doorbird/logbook.py
index c7ed802b7ea..a4889360d81 100644
--- a/homeassistant/components/doorbird/logbook.py
+++ b/homeassistant/components/doorbird/logbook.py
@@ -13,7 +13,7 @@ def async_describe_events(hass, async_describe_event):
@callback
def async_describe_logbook_event(event):
"""Describe a logbook event."""
- _, doorbird_event = event.event_type.split("_", 1)
+ doorbird_event = event.event_type.split("_", 1)[1]
return {
"name": "Doorbird",
diff --git a/homeassistant/components/doorbird/switch.py b/homeassistant/components/doorbird/switch.py
index 358f4172346..f1f146aebb9 100644
--- a/homeassistant/components/doorbird/switch.py
+++ b/homeassistant/components/doorbird/switch.py
@@ -2,6 +2,8 @@
import datetime
from homeassistant.components.switch import SwitchEntity
+from homeassistant.core import callback
+from homeassistant.helpers.event import async_track_point_in_utc_time
import homeassistant.util.dt as dt_util
from .const import DOMAIN, DOOR_STATION, DOOR_STATION_INFO
@@ -37,13 +39,13 @@ class DoorBirdSwitch(DoorBirdEntity, SwitchEntity):
self._doorstation = doorstation
self._relay = relay
self._state = False
- self._assume_off = datetime.datetime.min
if relay == IR_RELAY:
self._time = datetime.timedelta(minutes=5)
else:
self._time = datetime.timedelta(seconds=5)
self._unique_id = f"{self._mac_addr}_{self._relay}"
+ self._reset_sub = None
@property
def unique_id(self):
@@ -63,27 +65,41 @@ class DoorBirdSwitch(DoorBirdEntity, SwitchEntity):
"""Return the icon to display."""
return "mdi:lightbulb" if self._relay == IR_RELAY else "mdi:dip-switch"
+ @property
+ def should_poll(self):
+ """No need to poll."""
+ return False
+
@property
def is_on(self):
"""Get the assumed state of the relay."""
return self._state
- def turn_on(self, **kwargs):
+ async def async_turn_on(self, **kwargs):
+ """Power the relay."""
+ if self._reset_sub is not None:
+ self._reset_sub()
+ self._reset_sub = None
+ self._reset_sub = async_track_point_in_utc_time(
+ self.hass, self._async_turn_off, dt_util.utcnow() + self._time
+ )
+ await self.hass.async_add_executor_job(self._turn_on)
+ self.async_write_ha_state()
+
+ def _turn_on(self):
"""Power the relay."""
if self._relay == IR_RELAY:
self._state = self._doorstation.device.turn_light_on()
else:
self._state = self._doorstation.device.energize_relay(self._relay)
- now = dt_util.utcnow()
- self._assume_off = now + self._time
-
- def turn_off(self, **kwargs):
+ async def async_turn_off(self, **kwargs):
"""Turn off the relays is not needed. They are time-based."""
raise NotImplementedError("DoorBird relays cannot be manually turned off.")
- async def async_update(self):
+ @callback
+ def _async_turn_off(self, *_):
"""Wait for the correct amount of assumed time to pass."""
- if self._state and self._assume_off <= dt_util.utcnow():
- self._state = False
- self._assume_off = datetime.datetime.min
+ self._state = False
+ self._reset_sub = None
+ self.async_write_ha_state()
diff --git a/homeassistant/components/dsmr/manifest.json b/homeassistant/components/dsmr/manifest.json
index 964c68ab182..fdbba4212f6 100644
--- a/homeassistant/components/dsmr/manifest.json
+++ b/homeassistant/components/dsmr/manifest.json
@@ -2,7 +2,7 @@
"domain": "dsmr",
"name": "DSMR Slimme Meter",
"documentation": "https://www.home-assistant.io/integrations/dsmr",
- "requirements": ["dsmr_parser==0.18"],
+ "requirements": ["dsmr_parser==0.23"],
"codeowners": ["@Robbie1221"],
"config_flow": false
}
diff --git a/homeassistant/components/dsmr/translations/hu.json b/homeassistant/components/dsmr/translations/hu.json
index 3b2d79a34a7..930b739fb18 100644
--- a/homeassistant/components/dsmr/translations/hu.json
+++ b/homeassistant/components/dsmr/translations/hu.json
@@ -3,5 +3,15 @@
"abort": {
"already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "time_between_update": "Minim\u00e1lis id\u0151 az entit\u00e1sfriss\u00edt\u00e9sek k\u00f6z\u00f6tt [mp]"
+ },
+ "title": "DSMR opci\u00f3k"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/dsmr/translations/it.json b/homeassistant/components/dsmr/translations/it.json
index b295fb60747..75cbb713056 100644
--- a/homeassistant/components/dsmr/translations/it.json
+++ b/homeassistant/components/dsmr/translations/it.json
@@ -2,14 +2,16 @@
"config": {
"abort": {
"already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
- },
- "error": {
- "one": "uno",
- "other": "altri"
- },
+ }
+ },
+ "options": {
"step": {
- "one": "uno",
- "other": "altri"
+ "init": {
+ "data": {
+ "time_between_update": "Tempo minimo tra gli aggiornamenti dell'entit\u00e0 [s]."
+ },
+ "title": "Opzioni DSMR"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/dsmr/translations/ka.json b/homeassistant/components/dsmr/translations/ka.json
new file mode 100644
index 00000000000..df4b7b039f9
--- /dev/null
+++ b/homeassistant/components/dsmr/translations/ka.json
@@ -0,0 +1,12 @@
+{
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "time_between_update": "\u10dd\u10d1\u10d8\u10d4\u10e5\u10e2\u10d4\u10d1\u10d8\u10e1 \u10d2\u10d0\u10dc\u10d0\u10ee\u10da\u10d4\u10d1\u10d4\u10d1\u10e1 \u10e8\u10dd\u10e0\u10d8\u10e1 \u10db\u10d8\u10dc\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8 \u10d3\u10e0\u10dd"
+ },
+ "title": "DSMR \u10de\u10d0\u10e0\u10d0\u10db\u10d4\u10e2\u10e0\u10d4\u10d1\u10d8"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/dsmr/translations/lb.json b/homeassistant/components/dsmr/translations/lb.json
index 6469543442e..b742d16b36d 100644
--- a/homeassistant/components/dsmr/translations/lb.json
+++ b/homeassistant/components/dsmr/translations/lb.json
@@ -3,5 +3,15 @@
"abort": {
"already_configured": "Apparat ass scho konfigur\u00e9iert"
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "time_between_update": "Minimum Z\u00e4it zw\u00ebschen Entit\u00e9it's Aktualis\u00e9ierungen [s]"
+ },
+ "title": "DSMR Optiounen"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/dsmr/translations/zh-Hant.json b/homeassistant/components/dsmr/translations/zh-Hant.json
index 1ab3e1f720f..e35c96a7bf7 100644
--- a/homeassistant/components/dsmr/translations/zh-Hant.json
+++ b/homeassistant/components/dsmr/translations/zh-Hant.json
@@ -3,5 +3,15 @@
"abort": {
"already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "time_between_update": "\u5be6\u9ad4\u66f4\u65b0\u9593\u9694\u6700\u5c0f\u6642\u9593"
+ },
+ "title": "DSMR \u9078\u9805"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py
index 5fda67e65a3..309f0d297ec 100644
--- a/homeassistant/components/dsmr_reader/definitions.py
+++ b/homeassistant/components/dsmr_reader/definitions.py
@@ -2,8 +2,14 @@
from homeassistant.const import (
CURRENCY_EURO,
+ DEVICE_CLASS_CURRENT,
+ DEVICE_CLASS_ENERGY,
+ DEVICE_CLASS_POWER,
+ DEVICE_CLASS_TIMESTAMP,
+ DEVICE_CLASS_VOLTAGE,
ELECTRICAL_CURRENT_AMPERE,
ENERGY_KILO_WATT_HOUR,
+ POWER_KILO_WATT,
VOLT,
VOLUME_CUBIC_METERS,
)
@@ -26,243 +32,297 @@ def tariff_transform(value):
DEFINITIONS = {
"dsmr/reading/electricity_delivered_1": {
"name": "Low tariff usage",
- "icon": "mdi:flash",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_ENERGY,
"unit": ENERGY_KILO_WATT_HOUR,
},
"dsmr/reading/electricity_returned_1": {
"name": "Low tariff returned",
- "icon": "mdi:flash-outline",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_ENERGY,
"unit": ENERGY_KILO_WATT_HOUR,
},
"dsmr/reading/electricity_delivered_2": {
"name": "High tariff usage",
- "icon": "mdi:flash",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_ENERGY,
"unit": ENERGY_KILO_WATT_HOUR,
},
"dsmr/reading/electricity_returned_2": {
"name": "High tariff returned",
- "icon": "mdi:flash-outline",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_ENERGY,
"unit": ENERGY_KILO_WATT_HOUR,
},
"dsmr/reading/electricity_currently_delivered": {
"name": "Current power usage",
- "icon": "mdi:flash",
- "unit": "kW",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_POWER,
+ "unit": POWER_KILO_WATT,
},
"dsmr/reading/electricity_currently_returned": {
"name": "Current power return",
- "icon": "mdi:flash-outline",
- "unit": "kW",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_POWER,
+ "unit": POWER_KILO_WATT,
},
"dsmr/reading/phase_currently_delivered_l1": {
"name": "Current power usage L1",
- "icon": "mdi:flash",
- "unit": "kW",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_POWER,
+ "unit": POWER_KILO_WATT,
},
"dsmr/reading/phase_currently_delivered_l2": {
"name": "Current power usage L2",
- "icon": "mdi:flash",
- "unit": "kW",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_POWER,
+ "unit": POWER_KILO_WATT,
},
"dsmr/reading/phase_currently_delivered_l3": {
"name": "Current power usage L3",
- "icon": "mdi:flash",
- "unit": "kW",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_POWER,
+ "unit": POWER_KILO_WATT,
},
"dsmr/reading/phase_currently_returned_l1": {
"name": "Current power return L1",
- "icon": "mdi:flash-outline",
- "unit": "kW",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_POWER,
+ "unit": POWER_KILO_WATT,
},
"dsmr/reading/phase_currently_returned_l2": {
"name": "Current power return L2",
- "icon": "mdi:flash-outline",
- "unit": "kW",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_POWER,
+ "unit": POWER_KILO_WATT,
},
"dsmr/reading/phase_currently_returned_l3": {
"name": "Current power return L3",
- "icon": "mdi:flash-outline",
- "unit": "kW",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_POWER,
+ "unit": POWER_KILO_WATT,
},
"dsmr/reading/extra_device_delivered": {
"name": "Gas meter usage",
+ "enable_default": True,
"icon": "mdi:fire",
"unit": VOLUME_CUBIC_METERS,
},
"dsmr/reading/phase_voltage_l1": {
"name": "Current voltage L1",
- "icon": "mdi:flash",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_VOLTAGE,
"unit": VOLT,
},
"dsmr/reading/phase_voltage_l2": {
"name": "Current voltage L2",
- "icon": "mdi:flash",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_VOLTAGE,
"unit": VOLT,
},
"dsmr/reading/phase_voltage_l3": {
"name": "Current voltage L3",
- "icon": "mdi:flash",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_VOLTAGE,
"unit": VOLT,
},
"dsmr/reading/phase_power_current_l1": {
"name": "Phase power current L1",
- "icon": "mdi:flash",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_CURRENT,
"unit": ELECTRICAL_CURRENT_AMPERE,
},
"dsmr/reading/phase_power_current_l2": {
"name": "Phase power current L2",
- "icon": "mdi:flash",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_CURRENT,
"unit": ELECTRICAL_CURRENT_AMPERE,
},
"dsmr/reading/phase_power_current_l3": {
"name": "Phase power current L3",
- "icon": "mdi:flash",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_CURRENT,
"unit": ELECTRICAL_CURRENT_AMPERE,
},
+ "dsmr/reading/timestamp": {
+ "name": "Telegram timestamp",
+ "enable_default": False,
+ "device_class": DEVICE_CLASS_TIMESTAMP,
+ },
"dsmr/consumption/gas/delivered": {
"name": "Gas usage",
+ "enable_default": True,
"icon": "mdi:fire",
"unit": VOLUME_CUBIC_METERS,
},
"dsmr/consumption/gas/currently_delivered": {
"name": "Current gas usage",
+ "enable_default": True,
"icon": "mdi:fire",
"unit": VOLUME_CUBIC_METERS,
},
"dsmr/consumption/gas/read_at": {
"name": "Gas meter read",
- "icon": "mdi:clock",
- "unit": "",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_TIMESTAMP,
},
"dsmr/day-consumption/electricity1": {
"name": "Low tariff usage",
- "icon": "mdi:counter",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_ENERGY,
"unit": ENERGY_KILO_WATT_HOUR,
},
"dsmr/day-consumption/electricity2": {
"name": "High tariff usage",
- "icon": "mdi:counter",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_ENERGY,
"unit": ENERGY_KILO_WATT_HOUR,
},
"dsmr/day-consumption/electricity1_returned": {
"name": "Low tariff return",
- "icon": "mdi:counter",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_ENERGY,
"unit": ENERGY_KILO_WATT_HOUR,
},
"dsmr/day-consumption/electricity2_returned": {
"name": "High tariff return",
- "icon": "mdi:counter",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_ENERGY,
"unit": ENERGY_KILO_WATT_HOUR,
},
"dsmr/day-consumption/electricity_merged": {
"name": "Power usage total",
- "icon": "mdi:counter",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_ENERGY,
"unit": ENERGY_KILO_WATT_HOUR,
},
"dsmr/day-consumption/electricity_returned_merged": {
"name": "Power return total",
- "icon": "mdi:counter",
+ "enable_default": True,
+ "device_class": DEVICE_CLASS_ENERGY,
"unit": ENERGY_KILO_WATT_HOUR,
},
"dsmr/day-consumption/electricity1_cost": {
"name": "Low tariff cost",
+ "enable_default": True,
"icon": "mdi:currency-eur",
"unit": CURRENCY_EURO,
},
"dsmr/day-consumption/electricity2_cost": {
"name": "High tariff cost",
+ "enable_default": True,
"icon": "mdi:currency-eur",
"unit": CURRENCY_EURO,
},
"dsmr/day-consumption/electricity_cost_merged": {
"name": "Power total cost",
+ "enable_default": True,
"icon": "mdi:currency-eur",
"unit": CURRENCY_EURO,
},
"dsmr/day-consumption/gas": {
"name": "Gas usage",
+ "enable_default": True,
"icon": "mdi:counter",
"unit": VOLUME_CUBIC_METERS,
},
"dsmr/day-consumption/gas_cost": {
"name": "Gas cost",
+ "enable_default": True,
"icon": "mdi:currency-eur",
"unit": CURRENCY_EURO,
},
"dsmr/day-consumption/total_cost": {
"name": "Total cost",
+ "enable_default": True,
"icon": "mdi:currency-eur",
"unit": CURRENCY_EURO,
},
"dsmr/day-consumption/energy_supplier_price_electricity_delivered_1": {
"name": "Low tariff delivered price",
+ "enable_default": True,
"icon": "mdi:currency-eur",
"unit": CURRENCY_EURO,
},
"dsmr/day-consumption/energy_supplier_price_electricity_delivered_2": {
"name": "High tariff delivered price",
+ "enable_default": True,
"icon": "mdi:currency-eur",
"unit": CURRENCY_EURO,
},
"dsmr/day-consumption/energy_supplier_price_electricity_returned_1": {
"name": "Low tariff returned price",
+ "enable_default": True,
"icon": "mdi:currency-eur",
"unit": CURRENCY_EURO,
},
"dsmr/day-consumption/energy_supplier_price_electricity_returned_2": {
"name": "High tariff returned price",
+ "enable_default": True,
"icon": "mdi:currency-eur",
"unit": CURRENCY_EURO,
},
"dsmr/day-consumption/energy_supplier_price_gas": {
"name": "Gas price",
+ "enable_default": True,
"icon": "mdi:currency-eur",
"unit": CURRENCY_EURO,
},
"dsmr/meter-stats/dsmr_version": {
"name": "DSMR version",
+ "enable_default": True,
"icon": "mdi:alert-circle",
"transform": dsmr_transform,
},
"dsmr/meter-stats/electricity_tariff": {
"name": "Electricity tariff",
+ "enable_default": True,
"icon": "mdi:flash",
"transform": tariff_transform,
},
"dsmr/meter-stats/power_failure_count": {
"name": "Power failure count",
+ "enable_default": True,
"icon": "mdi:flash",
},
"dsmr/meter-stats/long_power_failure_count": {
"name": "Long power failure count",
+ "enable_default": True,
"icon": "mdi:flash",
},
"dsmr/meter-stats/voltage_sag_count_l1": {
"name": "Voltage sag L1",
+ "enable_default": True,
"icon": "mdi:flash",
},
"dsmr/meter-stats/voltage_sag_count_l2": {
"name": "Voltage sag L2",
+ "enable_default": True,
"icon": "mdi:flash",
},
"dsmr/meter-stats/voltage_sag_count_l3": {
"name": "Voltage sag L3",
+ "enable_default": True,
"icon": "mdi:flash",
},
"dsmr/meter-stats/voltage_swell_count_l1": {
"name": "Voltage swell L1",
+ "enable_default": True,
"icon": "mdi:flash",
},
"dsmr/meter-stats/voltage_swell_count_l2": {
"name": "Voltage swell L2",
+ "enable_default": True,
"icon": "mdi:flash",
},
"dsmr/meter-stats/voltage_swell_count_l3": {
"name": "Voltage swell L3",
+ "enable_default": True,
"icon": "mdi:flash",
},
"dsmr/meter-stats/rejected_telegrams": {
"name": "Rejected telegrams",
+ "enable_default": True,
"icon": "mdi:flash",
},
}
diff --git a/homeassistant/components/dsmr_reader/sensor.py b/homeassistant/components/dsmr_reader/sensor.py
index 341451522d4..14234b49dbe 100644
--- a/homeassistant/components/dsmr_reader/sensor.py
+++ b/homeassistant/components/dsmr_reader/sensor.py
@@ -31,6 +31,8 @@ class DSMRSensor(Entity):
self._topic = topic
self._name = self._definition.get("name", topic.split("/")[-1])
+ self._device_class = self._definition.get("device_class")
+ self._enable_default = self._definition.get("enable_default")
self._unit_of_measurement = self._definition.get("unit")
self._icon = self._definition.get("icon")
self._transform = self._definition.get("transform")
@@ -67,11 +69,21 @@ class DSMRSensor(Entity):
"""Return the current state of the entity."""
return self._state
+ @property
+ def device_class(self):
+ """Return the device_class of this sensor."""
+ return self._device_class
+
@property
def unit_of_measurement(self):
"""Return the unit_of_measurement of this sensor."""
return self._unit_of_measurement
+ @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 icon(self):
"""Return the icon of this sensor."""
diff --git a/homeassistant/components/dunehd/translations/nl.json b/homeassistant/components/dunehd/translations/nl.json
index 91325588b13..c8e16770db2 100644
--- a/homeassistant/components/dunehd/translations/nl.json
+++ b/homeassistant/components/dunehd/translations/nl.json
@@ -5,7 +5,8 @@
},
"error": {
"already_configured": "Apparaat is al geconfigureerd",
- "cannot_connect": "Kon niet verbinden"
+ "cannot_connect": "Kon niet verbinden",
+ "invalid_host": "ongeldige host of IP adres"
},
"step": {
"user": {
diff --git a/homeassistant/components/dunehd/translations/sl.json b/homeassistant/components/dunehd/translations/sl.json
new file mode 100644
index 00000000000..ccab4d05b4c
--- /dev/null
+++ b/homeassistant/components/dunehd/translations/sl.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "invalid_host": "Neveljavno ime gostitelja ali neveljaven IP naslov"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/dyson/manifest.json b/homeassistant/components/dyson/manifest.json
index 94a29d1615d..4678b1ad598 100644
--- a/homeassistant/components/dyson/manifest.json
+++ b/homeassistant/components/dyson/manifest.json
@@ -2,7 +2,7 @@
"domain": "dyson",
"name": "Dyson",
"documentation": "https://www.home-assistant.io/integrations/dyson",
- "requirements": ["libpurecool==0.6.3"],
+ "requirements": ["libpurecool==0.6.4"],
"after_dependencies": ["zeroconf"],
- "codeowners": ["@etheralm"]
+ "codeowners": []
}
diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py
index ccfddca4b03..94396bbf883 100644
--- a/homeassistant/components/ecobee/climate.py
+++ b/homeassistant/components/ecobee/climate.py
@@ -24,6 +24,7 @@ from homeassistant.components.climate.const import (
SUPPORT_AUX_HEAT,
SUPPORT_FAN_MODE,
SUPPORT_PRESET_MODE,
+ SUPPORT_TARGET_HUMIDITY,
SUPPORT_TARGET_TEMPERATURE,
SUPPORT_TARGET_TEMPERATURE_RANGE,
)
@@ -161,6 +162,7 @@ SUPPORT_FLAGS = (
| SUPPORT_AUX_HEAT
| SUPPORT_TARGET_TEMPERATURE_RANGE
| SUPPORT_FAN_MODE
+ | SUPPORT_TARGET_HUMIDITY
)
@@ -651,7 +653,7 @@ class Thermostat(ClimateEntity):
def set_humidity(self, humidity):
"""Set the humidity level."""
- self.data.ecobee.set_humidity(self.thermostat_index, humidity)
+ self.data.ecobee.set_humidity(self.thermostat_index, int(humidity))
def set_hvac_mode(self, hvac_mode):
"""Set HVAC mode (auto, auxHeatOnly, cool, heat, off)."""
diff --git a/homeassistant/components/ecobee/const.py b/homeassistant/components/ecobee/const.py
index f380c9bbef3..5ec3a0fcf96 100644
--- a/homeassistant/components/ecobee/const.py
+++ b/homeassistant/components/ecobee/const.py
@@ -1,6 +1,20 @@
"""Constants for the ecobee integration."""
import logging
+from homeassistant.components.weather import (
+ ATTR_CONDITION_CLOUDY,
+ ATTR_CONDITION_FOG,
+ ATTR_CONDITION_HAIL,
+ ATTR_CONDITION_LIGHTNING_RAINY,
+ ATTR_CONDITION_PARTLYCLOUDY,
+ ATTR_CONDITION_POURING,
+ ATTR_CONDITION_RAINY,
+ ATTR_CONDITION_SNOWY,
+ ATTR_CONDITION_SNOWY_RAINY,
+ ATTR_CONDITION_SUNNY,
+ ATTR_CONDITION_WINDY,
+)
+
_LOGGER = logging.getLogger(__package__)
DOMAIN = "ecobee"
@@ -30,25 +44,25 @@ MANUFACTURER = "ecobee"
# Translates ecobee API weatherSymbol to Home Assistant usable names
# https://www.ecobee.com/home/developer/api/documentation/v1/objects/WeatherForecast.shtml
ECOBEE_WEATHER_SYMBOL_TO_HASS = {
- 0: "sunny",
- 1: "partlycloudy",
- 2: "partlycloudy",
- 3: "cloudy",
- 4: "cloudy",
- 5: "cloudy",
- 6: "rainy",
- 7: "snowy-rainy",
- 8: "pouring",
- 9: "hail",
- 10: "snowy",
- 11: "snowy",
- 12: "snowy-rainy",
+ 0: ATTR_CONDITION_SUNNY,
+ 1: ATTR_CONDITION_PARTLYCLOUDY,
+ 2: ATTR_CONDITION_PARTLYCLOUDY,
+ 3: ATTR_CONDITION_CLOUDY,
+ 4: ATTR_CONDITION_CLOUDY,
+ 5: ATTR_CONDITION_CLOUDY,
+ 6: ATTR_CONDITION_RAINY,
+ 7: ATTR_CONDITION_SNOWY_RAINY,
+ 8: ATTR_CONDITION_POURING,
+ 9: ATTR_CONDITION_HAIL,
+ 10: ATTR_CONDITION_SNOWY,
+ 11: ATTR_CONDITION_SNOWY,
+ 12: ATTR_CONDITION_SNOWY_RAINY,
13: "snowy-heavy",
- 14: "hail",
- 15: "lightning-rainy",
- 16: "windy",
+ 14: ATTR_CONDITION_HAIL,
+ 15: ATTR_CONDITION_LIGHTNING_RAINY,
+ 16: ATTR_CONDITION_WINDY,
17: "tornado",
- 18: "fog",
+ 18: ATTR_CONDITION_FOG,
19: "hazy",
20: "hazy",
21: "hazy",
diff --git a/homeassistant/components/eddystone_temperature/manifest.json b/homeassistant/components/eddystone_temperature/manifest.json
index c59cb6a9c7f..e6ff0a17ea3 100644
--- a/homeassistant/components/eddystone_temperature/manifest.json
+++ b/homeassistant/components/eddystone_temperature/manifest.json
@@ -2,6 +2,6 @@
"domain": "eddystone_temperature",
"name": "Eddystone",
"documentation": "https://www.home-assistant.io/integrations/eddystone_temperature",
- "requirements": ["beacontools[scan]==1.2.3", "construct==2.9.45"],
+ "requirements": ["beacontools[scan]==1.2.3", "construct==2.10.56"],
"codeowners": []
}
diff --git a/homeassistant/components/edl21/manifest.json b/homeassistant/components/edl21/manifest.json
index 3e469e44601..c3b65c3b352 100644
--- a/homeassistant/components/edl21/manifest.json
+++ b/homeassistant/components/edl21/manifest.json
@@ -2,6 +2,6 @@
"domain": "edl21",
"name": "EDL21",
"documentation": "https://www.home-assistant.io/integrations/edl21",
- "requirements": ["pysml==0.0.2"],
+ "requirements": ["pysml==0.0.3"],
"codeowners": ["@mtdcr"]
}
diff --git a/homeassistant/components/edl21/sensor.py b/homeassistant/components/edl21/sensor.py
index 5eb7542fed9..dc0f51abe61 100644
--- a/homeassistant/components/edl21/sensor.py
+++ b/homeassistant/components/edl21/sensor.py
@@ -77,20 +77,36 @@ class EDL21:
# D=7: Instantaneous value
# E=0: Total
"1-0:16.7.0*255": "Sum active instantaneous power",
+ # C=31: Active amperage L1
+ # D=7: Instantaneous value
+ # E=0: Total
+ "1-0:31.7.0*255": "L1 active instantaneous amperage",
# C=36: Active power L1
# D=7: Instantaneous value
# E=0: Total
"1-0:36.7.0*255": "L1 active instantaneous power",
- # C=56: Active power L1
+ # C=51: Active amperage L2
+ # D=7: Instantaneous value
+ # E=0: Total
+ "1-0:51.7.0*255": "L2 active instantaneous amperage",
+ # C=56: Active power L2
# D=7: Instantaneous value
# E=0: Total
"1-0:56.7.0*255": "L2 active instantaneous power",
- # C=76: Active power L1
+ # C=71: Active amperage L3
+ # D=7: Instantaneous value
+ # E=0: Total
+ "1-0:71.7.0*255": "L3 active instantaneous amperage",
+ # C=76: Active power L3
# D=7: Instantaneous value
# E=0: Total
"1-0:76.7.0*255": "L3 active instantaneous power",
+ # C=96: Electricity-related service entries
+ "1-0:96.1.0*255": "Metering point ID 1",
}
_OBIS_BLACKLIST = {
+ # C=96: Electricity-related service entries
+ "1-0:96.50.1*1", # Manufacturer specific
# A=129: Manufacturer specific
"129-129:199.130.3*255", # Iskraemeco: Manufacturer
"129-129:199.130.5*255", # Iskraemeco: Public Key
@@ -115,7 +131,7 @@ class EDL21:
electricity_id = None
for telegram in message_body.get("valList", []):
- if telegram.get("objName") == "1-0:0.0.9*255":
+ if telegram.get("objName") in ("1-0:0.0.9*255", "1-0:96.1.0*255"):
electricity_id = telegram.get("value")
break
diff --git a/homeassistant/components/elgato/manifest.json b/homeassistant/components/elgato/manifest.json
index f1a92ec727f..1da98a41211 100644
--- a/homeassistant/components/elgato/manifest.json
+++ b/homeassistant/components/elgato/manifest.json
@@ -3,7 +3,7 @@
"name": "Elgato Key Light",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/elgato",
- "requirements": ["elgato==0.2.0"],
+ "requirements": ["elgato==1.0.0"],
"zeroconf": ["_elg._tcp.local."],
"codeowners": ["@frenck"],
"quality_scale": "platinum"
diff --git a/homeassistant/components/elgato/translations/hu.json b/homeassistant/components/elgato/translations/hu.json
index dcfcb155d12..3c69fd4562a 100644
--- a/homeassistant/components/elgato/translations/hu.json
+++ b/homeassistant/components/elgato/translations/hu.json
@@ -1,7 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Ez az Elgato Key Light eszk\u00f6z m\u00e1r konfigur\u00e1lva van."
+ "already_configured": "Ez az Elgato Key Light eszk\u00f6z m\u00e1r konfigur\u00e1lva van.",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
"step": {
"user": {
diff --git a/homeassistant/components/elgato/translations/pl.json b/homeassistant/components/elgato/translations/pl.json
index 8d36d06f501..37a6b94a5b1 100644
--- a/homeassistant/components/elgato/translations/pl.json
+++ b/homeassistant/components/elgato/translations/pl.json
@@ -14,10 +14,10 @@
"host": "Nazwa hosta lub adres IP",
"port": "Port"
},
- "description": "Konfiguracja Elgato Key Light w celu integracji z Home Assistant."
+ "description": "Konfiguracja Elgato Key Light w celu integracji z Home Assistantem."
},
"zeroconf_confirm": {
- "description": "Czy chcesz doda\u0107 urz\u0105dzenie Elgato Key Light o numerze seryjnym `{serial_number}` do Home Assistant?",
+ "description": "Czy chcesz doda\u0107 urz\u0105dzenie Elgato Key Light o numerze seryjnym `{serial_number}` do Home Assistanta?",
"title": "Wykryto urz\u0105dzenie Elgato Key Light"
}
}
diff --git a/homeassistant/components/emulated_roku/translations/ka.json b/homeassistant/components/emulated_roku/translations/ka.json
new file mode 100644
index 00000000000..fb52156e9cf
--- /dev/null
+++ b/homeassistant/components/emulated_roku/translations/ka.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json
index 183a53284a0..e6ab8dbf6a9 100644
--- a/homeassistant/components/enphase_envoy/manifest.json
+++ b/homeassistant/components/enphase_envoy/manifest.json
@@ -2,6 +2,8 @@
"domain": "enphase_envoy",
"name": "Enphase Envoy",
"documentation": "https://www.home-assistant.io/integrations/enphase_envoy",
- "requirements": ["envoy_reader==0.16.2"],
- "codeowners": []
+ "requirements": ["envoy_reader==0.17.0"],
+ "codeowners": [
+ "@gtdiehl"
+ ]
}
diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py
index 3c3dbfe6ce6..f4fa96b52d6 100644
--- a/homeassistant/components/environment_canada/weather.py
+++ b/homeassistant/components/environment_canada/weather.py
@@ -6,6 +6,18 @@ from env_canada import ECData # pylint: disable=import-error
import voluptuous as vol
from homeassistant.components.weather import (
+ ATTR_CONDITION_CLEAR_NIGHT,
+ ATTR_CONDITION_CLOUDY,
+ ATTR_CONDITION_FOG,
+ ATTR_CONDITION_HAIL,
+ ATTR_CONDITION_LIGHTNING_RAINY,
+ ATTR_CONDITION_PARTLYCLOUDY,
+ ATTR_CONDITION_POURING,
+ ATTR_CONDITION_RAINY,
+ ATTR_CONDITION_SNOWY,
+ ATTR_CONDITION_SNOWY_RAINY,
+ ATTR_CONDITION_SUNNY,
+ ATTR_CONDITION_WINDY,
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_FORECAST_TEMP,
@@ -45,18 +57,18 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
# Icon codes from http://dd.weatheroffice.ec.gc.ca/citypage_weather/
# docs/current_conditions_icon_code_descriptions_e.csv
ICON_CONDITION_MAP = {
- "sunny": [0, 1],
- "clear-night": [30, 31],
- "partlycloudy": [2, 3, 4, 5, 22, 32, 33, 34, 35],
- "cloudy": [10],
- "rainy": [6, 9, 11, 12, 28, 36],
- "lightning-rainy": [19, 39, 46, 47],
- "pouring": [13],
- "snowy-rainy": [7, 14, 15, 27, 37],
- "snowy": [8, 16, 17, 18, 25, 26, 38, 40],
- "windy": [43],
- "fog": [20, 21, 23, 24, 44],
- "hail": [26, 27],
+ ATTR_CONDITION_SUNNY: [0, 1],
+ ATTR_CONDITION_CLEAR_NIGHT: [30, 31],
+ ATTR_CONDITION_PARTLYCLOUDY: [2, 3, 4, 5, 22, 32, 33, 34, 35],
+ ATTR_CONDITION_CLOUDY: [10],
+ ATTR_CONDITION_RAINY: [6, 9, 11, 12, 28, 36],
+ ATTR_CONDITION_LIGHTNING_RAINY: [19, 39, 46, 47],
+ ATTR_CONDITION_POURING: [13],
+ ATTR_CONDITION_SNOWY_RAINY: [7, 14, 15, 27, 37],
+ ATTR_CONDITION_SNOWY: [8, 16, 17, 18, 25, 26, 38, 40],
+ ATTR_CONDITION_WINDY: [43],
+ ATTR_CONDITION_FOG: [20, 21, 23, 24, 44],
+ ATTR_CONDITION_HAIL: [26, 27],
}
diff --git a/homeassistant/components/epson/translations/hu.json b/homeassistant/components/epson/translations/hu.json
new file mode 100644
index 00000000000..5ff60755bfd
--- /dev/null
+++ b/homeassistant/components/epson/translations/hu.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Gazdag\u00e9p",
+ "name": "N\u00e9v",
+ "port": "Port"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/epson/translations/ka.json b/homeassistant/components/epson/translations/ka.json
new file mode 100644
index 00000000000..b339899ea5f
--- /dev/null
+++ b/homeassistant/components/epson/translations/ka.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u10d3\u10d0\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d4\u10d1\u10d8\u10e1 \u10e8\u10d4\u10ea\u10d3\u10dd\u10db\u10d0"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u10f0\u10dd\u10e1\u10e2\u10d8",
+ "name": "\u10e1\u10d0\u10ee\u10d4\u10da\u10d8",
+ "port": "\u10de\u10dd\u10e0\u10e2\u10d8"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/epson/translations/lb.json b/homeassistant/components/epson/translations/lb.json
new file mode 100644
index 00000000000..e8d9f52998f
--- /dev/null
+++ b/homeassistant/components/epson/translations/lb.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Feeler beim verbannen"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host",
+ "name": "Numm",
+ "port": "Port"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json
index e15fd8d384b..5f5fefe25ea 100644
--- a/homeassistant/components/eq3btsmart/manifest.json
+++ b/homeassistant/components/eq3btsmart/manifest.json
@@ -2,6 +2,6 @@
"domain": "eq3btsmart",
"name": "EQ3 Bluetooth Smart Thermostats",
"documentation": "https://www.home-assistant.io/integrations/eq3btsmart",
- "requirements": ["construct==2.9.45", "python-eq3bt==0.1.11"],
+ "requirements": ["construct==2.10.56", "python-eq3bt==0.1.11"],
"codeowners": ["@rytilahti"]
}
diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py
index c9d07a22ec6..a12754a87f4 100644
--- a/homeassistant/components/esphome/__init__.py
+++ b/homeassistant/components/esphome/__init__.py
@@ -489,58 +489,38 @@ def esphome_map_enum(func: Callable[[], Dict[int, str]]):
return EsphomeEnumMapper(func)
-class EsphomeEntity(Entity):
- """Define a generic esphome entity."""
+class EsphomeBaseEntity(Entity):
+ """Define a base esphome entity."""
def __init__(self, entry_id: str, component_key: str, key: int):
"""Initialize."""
self._entry_id = entry_id
self._component_key = component_key
self._key = key
- self._remove_callbacks: List[Callable[[], None]] = []
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
- kwargs = {
- "entry_id": self._entry_id,
- "component_key": self._component_key,
- "key": self._key,
- }
- self._remove_callbacks.append(
+ self.async_on_remove(
async_dispatcher_connect(
self.hass,
(
- f"esphome_{kwargs.get('entry_id')}"
- f"_update_{kwargs.get('component_key')}_{kwargs.get('key')}"
- ),
- self._on_state_update,
- )
- )
-
- self._remove_callbacks.append(
- async_dispatcher_connect(
- self.hass,
- (
- f"esphome_{kwargs.get('entry_id')}_remove_"
- f"{kwargs.get('component_key')}_{kwargs.get('key')}"
+ f"esphome_{self._entry_id}_remove_"
+ f"{self._component_key}_{self._key}"
),
self.async_remove,
)
)
- self._remove_callbacks.append(
+ self.async_on_remove(
async_dispatcher_connect(
self.hass,
- f"esphome_{kwargs.get('entry_id')}_on_device_update",
+ f"esphome_{self._entry_id}_on_device_update",
self._on_device_update,
)
)
- async def _on_state_update(self) -> None:
- """Update the entity state when state or static info changed."""
- self.async_write_ha_state()
-
- async def _on_device_update(self) -> None:
+ @callback
+ def _on_device_update(self) -> None:
"""Update the entity state when device info has changed."""
if self._entry_data.available:
# Don't update the HA state yet when the device comes online.
@@ -549,12 +529,6 @@ class EsphomeEntity(Entity):
return
self.async_write_ha_state()
- async def async_will_remove_from_hass(self) -> None:
- """Unregister callbacks."""
- for remove_callback in self._remove_callbacks:
- remove_callback()
- self._remove_callbacks = []
-
@property
def _entry_data(self) -> RuntimeEntryData:
return self.hass.data[DATA_KEY][self._entry_id]
@@ -619,3 +593,23 @@ class EsphomeEntity(Entity):
def should_poll(self) -> bool:
"""Disable polling."""
return False
+
+
+class EsphomeEntity(EsphomeBaseEntity):
+ """Define a generic esphome entity."""
+
+ async def async_added_to_hass(self) -> None:
+ """Register callbacks."""
+
+ await super().async_added_to_hass()
+
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass,
+ (
+ f"esphome_{self._entry_id}"
+ f"_update_{self._component_key}_{self._key}"
+ ),
+ self.async_write_ha_state,
+ )
+ )
diff --git a/homeassistant/components/esphome/camera.py b/homeassistant/components/esphome/camera.py
index 1678281c3de..5b8f4f0d7e6 100644
--- a/homeassistant/components/esphome/camera.py
+++ b/homeassistant/components/esphome/camera.py
@@ -7,9 +7,10 @@ from aioesphomeapi import CameraInfo, CameraState
from homeassistant.components import camera
from homeassistant.components.camera import Camera
from homeassistant.config_entries import ConfigEntry
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.typing import HomeAssistantType
-from . import EsphomeEntity, platform_async_setup_entry
+from . import EsphomeBaseEntity, platform_async_setup_entry
async def async_setup_entry(
@@ -27,13 +28,13 @@ async def async_setup_entry(
)
-class EsphomeCamera(Camera, EsphomeEntity):
+class EsphomeCamera(Camera, EsphomeBaseEntity):
"""A camera implementation for ESPHome."""
def __init__(self, entry_id: str, component_key: str, key: int):
"""Initialize."""
Camera.__init__(self)
- EsphomeEntity.__init__(self, entry_id, component_key, key)
+ EsphomeBaseEntity.__init__(self, entry_id, component_key, key)
self._image_cond = asyncio.Condition()
@property
@@ -44,9 +45,25 @@ class EsphomeCamera(Camera, EsphomeEntity):
def _state(self) -> Optional[CameraState]:
return super()._state
+ async def async_added_to_hass(self) -> None:
+ """Register callbacks."""
+
+ await super().async_added_to_hass()
+
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass,
+ (
+ f"esphome_{self._entry_id}"
+ f"_update_{self._component_key}_{self._key}"
+ ),
+ self._on_state_update,
+ )
+ )
+
async def _on_state_update(self) -> None:
"""Notify listeners of new image when update arrives."""
- await super()._on_state_update()
+ self.async_write_ha_state()
async with self._image_cond:
self._image_cond.notify_all()
diff --git a/homeassistant/components/esphome/translations/ka.json b/homeassistant/components/esphome/translations/ka.json
new file mode 100644
index 00000000000..d0a8e7c5666
--- /dev/null
+++ b/homeassistant/components/esphome/translations/ka.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "invalid_auth": "\u10db\u10ea\u10d3\u10d0\u10e0\u10d8 \u10d0\u10d5\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/esphome/translations/pl.json b/homeassistant/components/esphome/translations/pl.json
index 1866b372eb4..34b8d9bd0e1 100644
--- a/homeassistant/components/esphome/translations/pl.json
+++ b/homeassistant/components/esphome/translations/pl.json
@@ -18,7 +18,7 @@
"description": "Wprowad\u017a has\u0142o ustawione w konfiguracji dla {name}."
},
"discovery_confirm": {
- "description": "Czy chcesz doda\u0107 w\u0119ze\u0142 ESPHome `{name}` do Home Assistant?",
+ "description": "Czy chcesz doda\u0107 w\u0119ze\u0142 ESPHome `{name}` do Home Assistanta?",
"title": "Znaleziono w\u0119ze\u0142 ESPHome"
},
"user": {
diff --git a/homeassistant/components/essent/manifest.json b/homeassistant/components/essent/manifest.json
index a46d37ccdc8..c90ce5ba664 100644
--- a/homeassistant/components/essent/manifest.json
+++ b/homeassistant/components/essent/manifest.json
@@ -2,6 +2,6 @@
"domain": "essent",
"name": "Essent",
"documentation": "https://www.home-assistant.io/integrations/essent",
- "requirements": ["PyEssent==0.13"],
+ "requirements": ["PyEssent==0.14"],
"codeowners": ["@TheLastProject"]
}
diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py
index 958750c4dcb..268e7709af3 100644
--- a/homeassistant/components/evohome/__init__.py
+++ b/homeassistant/components/evohome/__init__.py
@@ -34,7 +34,7 @@ from homeassistant.helpers.service import verify_domain_control
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
import homeassistant.util.dt as dt_util
-from .const import DOMAIN, EVO_FOLLOW, GWS, STORAGE_KEY, STORAGE_VER, TCS, UTC_OFFSET
+from .const import DOMAIN, GWS, STORAGE_KEY, STORAGE_VER, TCS, UTC_OFFSET
_LOGGER = logging.getLogger(__name__)
@@ -145,7 +145,6 @@ def _handle_exception(err) -> bool:
"Message is: %s",
err,
)
- return False
except aiohttp.ClientConnectionError:
# this appears to be a common occurrence with the vendor's servers
@@ -155,7 +154,6 @@ def _handle_exception(err) -> bool:
"Message is: %s",
err,
)
- return False
except aiohttp.ClientResponseError:
if err.status == HTTP_SERVICE_UNAVAILABLE:
@@ -163,17 +161,16 @@ def _handle_exception(err) -> bool:
"The vendor says their server is currently unavailable. "
"Check the vendor's service status page"
)
- return False
- if err.status == HTTP_TOO_MANY_REQUESTS:
+ elif err.status == HTTP_TOO_MANY_REQUESTS:
_LOGGER.warning(
"The vendor's API rate limit has been exceeded. "
"If this message persists, consider increasing the %s",
CONF_SCAN_INTERVAL,
)
- return False
- raise # we don't expect/handle any other Exceptions
+ else:
+ raise # we don't expect/handle any other Exceptions
async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
@@ -427,8 +424,8 @@ class EvoBroker:
try:
result = await api_function
except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err:
- if not _handle_exception(err):
- return
+ _handle_exception(err)
+ return
if update_state: # wait a moment for system to quiesce before updating state
self.hass.helpers.event.async_call_later(1, self._update_v2_api_state)
@@ -631,11 +628,11 @@ class EvoChild(EvoDevice):
dt_aware = dt_naive.replace(tzinfo=dt_util.UTC) - utc_offset
return dt_util.as_local(dt_aware)
- if not self._schedule["DailySchedules"]:
- return {} # no schedule {'DailySchedules': []}, so no scheduled setpoints
+ if not self._schedule or not self._schedule.get("DailySchedules"):
+ return {} # no scheduled setpoints when {'DailySchedules': []}
day_time = dt_util.now()
- day_of_week = int(day_time.strftime("%w")) # 0 is Sunday
+ day_of_week = day_time.weekday() # for evohome, 0 is Monday
time_of_day = day_time.strftime("%H:%M:%S")
try:
@@ -682,10 +679,6 @@ class EvoChild(EvoDevice):
async def _update_schedule(self) -> None:
"""Get the latest schedule, if any."""
- if "DailySchedules" in self._schedule and not self._schedule["DailySchedules"]:
- if not self._evo_device.setpointStatus["setpointMode"] == EVO_FOLLOW:
- return # avoid unnecessary I/O - there's nothing to update
-
self._schedule = await self._evo_broker.call_client_api(
self._evo_device.schedule(), update_state=False
)
diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py
index 701af451496..b9b2463314b 100644
--- a/homeassistant/components/ezviz/camera.py
+++ b/homeassistant/components/ezviz/camera.py
@@ -217,7 +217,7 @@ class HassEzvizCamera(Camera):
async def async_camera_image(self):
"""Return a frame from the camera stream."""
- ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop)
+ ffmpeg = ImageFrame(self._ffmpeg.binary)
image = await asyncio.shield(
ffmpeg.get_image(self._rtsp_stream, output_format=IMAGE_JPEG)
diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py
index 789299bd1ac..90a3d030703 100644
--- a/homeassistant/components/fan/__init__.py
+++ b/homeassistant/components/fan/__init__.py
@@ -144,6 +144,7 @@ class FanEntity(ToggleEntity):
def oscillate(self, oscillating: bool) -> None:
"""Oscillate the fan."""
+ raise NotImplementedError()
async def async_oscillate(self, oscillating: bool):
"""Oscillate the fan."""
diff --git a/homeassistant/components/ffmpeg/__init__.py b/homeassistant/components/ffmpeg/__init__.py
index bf44828cdb0..4b67f06ba23 100644
--- a/homeassistant/components/ffmpeg/__init__.py
+++ b/homeassistant/components/ffmpeg/__init__.py
@@ -97,7 +97,7 @@ async def async_get_image(
):
"""Get an image from a frame of an RTSP stream."""
manager = hass.data[DATA_FFMPEG]
- ffmpeg = ImageFrame(manager.binary, loop=hass.loop)
+ ffmpeg = ImageFrame(manager.binary)
image = await asyncio.shield(
ffmpeg.get_image(input_source, output_format=output_format, extra_cmd=extra_cmd)
)
@@ -123,7 +123,7 @@ class FFmpegManager:
async def async_get_version(self):
"""Return ffmpeg version."""
- ffversion = FFVersion(self._bin, self.hass.loop)
+ ffversion = FFVersion(self._bin)
self._version = await ffversion.get_version()
self._major_version = None
diff --git a/homeassistant/components/ffmpeg/camera.py b/homeassistant/components/ffmpeg/camera.py
index 6aea28d6509..4cd8b0d1453 100644
--- a/homeassistant/components/ffmpeg/camera.py
+++ b/homeassistant/components/ffmpeg/camera.py
@@ -61,7 +61,7 @@ class FFmpegCamera(Camera):
async def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from the camera."""
- stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop)
+ stream = CameraMjpeg(self._manager.binary)
await stream.open_camera(self._input, extra_cmd=self._extra_arguments)
try:
diff --git a/homeassistant/components/ffmpeg/manifest.json b/homeassistant/components/ffmpeg/manifest.json
index aee0b85d056..4e160687d8d 100644
--- a/homeassistant/components/ffmpeg/manifest.json
+++ b/homeassistant/components/ffmpeg/manifest.json
@@ -2,6 +2,6 @@
"domain": "ffmpeg",
"name": "FFmpeg",
"documentation": "https://www.home-assistant.io/integrations/ffmpeg",
- "requirements": ["ha-ffmpeg==2.0"],
+ "requirements": ["ha-ffmpeg==3.0.2"],
"codeowners": []
}
diff --git a/homeassistant/components/ffmpeg_motion/binary_sensor.py b/homeassistant/components/ffmpeg_motion/binary_sensor.py
index 9dbe08ec649..314fbbd2210 100644
--- a/homeassistant/components/ffmpeg_motion/binary_sensor.py
+++ b/homeassistant/components/ffmpeg_motion/binary_sensor.py
@@ -90,9 +90,7 @@ class FFmpegMotion(FFmpegBinarySensor):
"""Initialize FFmpeg motion binary sensor."""
super().__init__(config)
- self.ffmpeg = ffmpeg_sensor.SensorMotion(
- manager.binary, hass.loop, self._async_callback
- )
+ self.ffmpeg = ffmpeg_sensor.SensorMotion(manager.binary, self._async_callback)
async def _async_start_ffmpeg(self, entity_ids):
"""Start a FFmpeg instance.
diff --git a/homeassistant/components/ffmpeg_noise/binary_sensor.py b/homeassistant/components/ffmpeg_noise/binary_sensor.py
index 8425ed173b4..6c84c5973f1 100644
--- a/homeassistant/components/ffmpeg_noise/binary_sensor.py
+++ b/homeassistant/components/ffmpeg_noise/binary_sensor.py
@@ -53,9 +53,7 @@ class FFmpegNoise(FFmpegBinarySensor):
"""Initialize FFmpeg noise binary sensor."""
super().__init__(config)
- self.ffmpeg = ffmpeg_sensor.SensorNoise(
- manager.binary, hass.loop, self._async_callback
- )
+ self.ffmpeg = ffmpeg_sensor.SensorNoise(manager.binary, self._async_callback)
async def _async_start_ffmpeg(self, entity_ids):
"""Start a FFmpeg instance.
diff --git a/homeassistant/components/fireservicerota/__init__.py b/homeassistant/components/fireservicerota/__init__.py
new file mode 100644
index 00000000000..bf5f3f6beea
--- /dev/null
+++ b/homeassistant/components/fireservicerota/__init__.py
@@ -0,0 +1,265 @@
+"""The FireServiceRota integration."""
+import asyncio
+from datetime import timedelta
+import logging
+
+from pyfireservicerota import (
+ ExpiredTokenError,
+ FireServiceRota,
+ FireServiceRotaIncidents,
+ InvalidAuthError,
+ InvalidTokenError,
+)
+
+from homeassistant.components.binary_sensor import DOMAIN as BINARYSENSOR_DOMAIN
+from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
+from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
+from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
+from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_USERNAME
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.dispatcher import dispatcher_send
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
+
+from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN, WSS_BWRURL
+
+MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
+
+_LOGGER = logging.getLogger(__name__)
+
+SUPPORTED_PLATFORMS = {SENSOR_DOMAIN, BINARYSENSOR_DOMAIN, SWITCH_DOMAIN}
+
+
+async def async_setup(hass: HomeAssistant, config: dict) -> bool:
+ """Set up the FireServiceRota component."""
+
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+ """Set up FireServiceRota from a config entry."""
+
+ hass.data.setdefault(DOMAIN, {})
+
+ client = FireServiceRotaClient(hass, entry)
+ await client.setup()
+
+ if client.token_refresh_failure:
+ return False
+
+ async def async_update_data():
+ return await client.async_update()
+
+ coordinator = DataUpdateCoordinator(
+ hass,
+ _LOGGER,
+ name="duty binary sensor",
+ update_method=async_update_data,
+ update_interval=MIN_TIME_BETWEEN_UPDATES,
+ )
+
+ await coordinator.async_refresh()
+
+ hass.data[DOMAIN][entry.entry_id] = {
+ DATA_CLIENT: client,
+ DATA_COORDINATOR: coordinator,
+ }
+
+ for platform in SUPPORTED_PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, platform)
+ )
+
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+ """Unload FireServiceRota config entry."""
+
+ await hass.async_add_executor_job(
+ hass.data[DOMAIN][entry.entry_id].websocket.stop_listener
+ )
+
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, platform)
+ for platform in SUPPORTED_PLATFORMS
+ ]
+ )
+ )
+
+ if unload_ok:
+ del hass.data[DOMAIN][entry.entry_id]
+
+ return unload_ok
+
+
+class FireServiceRotaOauth:
+ """Handle authentication tokens."""
+
+ def __init__(self, hass, entry, fsr):
+ """Initialize the oauth object."""
+ self._hass = hass
+ self._entry = entry
+
+ self._url = entry.data[CONF_URL]
+ self._username = entry.data[CONF_USERNAME]
+ self._fsr = fsr
+
+ async def async_refresh_tokens(self) -> bool:
+ """Refresh tokens and update config entry."""
+ _LOGGER.debug("Refreshing authentication tokens after expiration")
+
+ try:
+ token_info = await self._hass.async_add_executor_job(
+ self._fsr.refresh_tokens
+ )
+
+ except (InvalidAuthError, InvalidTokenError):
+ _LOGGER.error("Error refreshing tokens, triggered reauth workflow")
+ self._hass.async_create_task(
+ self._hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_REAUTH},
+ data={
+ **self._entry.data,
+ },
+ )
+ )
+
+ return False
+
+ _LOGGER.debug("Saving new tokens in config entry")
+ self._hass.config_entries.async_update_entry(
+ self._entry,
+ data={
+ "auth_implementation": DOMAIN,
+ CONF_URL: self._url,
+ CONF_USERNAME: self._username,
+ CONF_TOKEN: token_info,
+ },
+ )
+
+ return True
+
+
+class FireServiceRotaWebSocket:
+ """Define a FireServiceRota websocket manager object."""
+
+ def __init__(self, hass, entry):
+ """Initialize the websocket object."""
+ self._hass = hass
+ self._entry = entry
+
+ self._fsr_incidents = FireServiceRotaIncidents(on_incident=self._on_incident)
+ self.incident_data = None
+
+ def _construct_url(self) -> str:
+ """Return URL with latest access token."""
+ return WSS_BWRURL.format(
+ self._entry.data[CONF_URL], self._entry.data[CONF_TOKEN]["access_token"]
+ )
+
+ def _on_incident(self, data) -> None:
+ """Received new incident, update data."""
+ _LOGGER.debug("Received new incident via websocket: %s", data)
+ self.incident_data = data
+ dispatcher_send(self._hass, f"{DOMAIN}_{self._entry.entry_id}_update")
+
+ def start_listener(self) -> None:
+ """Start the websocket listener."""
+ _LOGGER.debug("Starting incidents listener")
+ self._fsr_incidents.start(self._construct_url())
+
+ def stop_listener(self) -> None:
+ """Stop the websocket listener."""
+ _LOGGER.debug("Stopping incidents listener")
+ self._fsr_incidents.stop()
+
+
+class FireServiceRotaClient:
+ """Getting the latest data from fireservicerota."""
+
+ def __init__(self, hass, entry):
+ """Initialize the data object."""
+ self._hass = hass
+ self._entry = entry
+
+ self._url = entry.data[CONF_URL]
+ self._tokens = entry.data[CONF_TOKEN]
+
+ self.entry_id = entry.entry_id
+ self.unique_id = entry.unique_id
+
+ self.token_refresh_failure = False
+ self.incident_id = None
+ self.on_duty = False
+
+ self.fsr = FireServiceRota(base_url=self._url, token_info=self._tokens)
+
+ self.oauth = FireServiceRotaOauth(
+ self._hass,
+ self._entry,
+ self.fsr,
+ )
+
+ self.websocket = FireServiceRotaWebSocket(self._hass, self._entry)
+
+ async def setup(self) -> None:
+ """Set up the data client."""
+ await self._hass.async_add_executor_job(self.websocket.start_listener)
+
+ async def update_call(self, func, *args):
+ """Perform update call and return data."""
+ if self.token_refresh_failure:
+ return
+
+ try:
+ return await self._hass.async_add_executor_job(func, *args)
+ except (ExpiredTokenError, InvalidTokenError):
+ await self._hass.async_add_executor_job(self.websocket.stop_listener)
+ self.token_refresh_failure = True
+
+ if await self.oauth.async_refresh_tokens():
+ self.token_refresh_failure = False
+ await self._hass.async_add_executor_job(self.websocket.start_listener)
+
+ return await self._hass.async_add_executor_job(func, *args)
+
+ async def async_update(self) -> object:
+ """Get the latest availability data."""
+ data = await self.update_call(
+ self.fsr.get_availability, str(self._hass.config.time_zone)
+ )
+
+ if not data:
+ return
+
+ self.on_duty = bool(data.get("available"))
+
+ _LOGGER.debug("Updated availability data: %s", data)
+ return data
+
+ async def async_response_update(self) -> object:
+ """Get the latest incident response data."""
+
+ if not self.incident_id:
+ return
+
+ _LOGGER.debug("Updating response data for incident id %s", self.incident_id)
+
+ return await self.update_call(self.fsr.get_incident_response, self.incident_id)
+
+ async def async_set_response(self, value) -> None:
+ """Set incident response status."""
+
+ if not self.incident_id:
+ return
+
+ _LOGGER.debug(
+ "Setting incident response for incident id '%s' to state '%s'",
+ self.incident_id,
+ value,
+ )
+
+ await self.update_call(self.fsr.set_incident_response, self.incident_id, value)
diff --git a/homeassistant/components/fireservicerota/binary_sensor.py b/homeassistant/components/fireservicerota/binary_sensor.py
new file mode 100644
index 00000000000..fc06e605cbd
--- /dev/null
+++ b/homeassistant/components/fireservicerota/binary_sensor.py
@@ -0,0 +1,91 @@
+"""Binary Sensor platform for FireServiceRota integration."""
+import logging
+
+from homeassistant.components.binary_sensor import BinarySensorEntity
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.helpers.update_coordinator import (
+ CoordinatorEntity,
+ DataUpdateCoordinator,
+)
+
+from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN as FIRESERVICEROTA_DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(
+ hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
+) -> None:
+ """Set up FireServiceRota binary sensor based on a config entry."""
+
+ client = hass.data[FIRESERVICEROTA_DOMAIN][entry.entry_id][DATA_CLIENT]
+
+ coordinator: DataUpdateCoordinator = hass.data[FIRESERVICEROTA_DOMAIN][
+ entry.entry_id
+ ][DATA_COORDINATOR]
+
+ async_add_entities([ResponseBinarySensor(coordinator, client, entry)])
+
+
+class ResponseBinarySensor(CoordinatorEntity, BinarySensorEntity):
+ """Representation of an FireServiceRota sensor."""
+
+ def __init__(self, coordinator: DataUpdateCoordinator, client, entry):
+ """Initialize."""
+ super().__init__(coordinator)
+ self._client = client
+ self._unique_id = f"{entry.unique_id}_Duty"
+
+ self._state = None
+
+ @property
+ def name(self) -> str:
+ """Return the name of the sensor."""
+ return "Duty"
+
+ @property
+ def icon(self) -> str:
+ """Return the icon to use in the frontend."""
+ if self._state:
+ return "mdi:calendar-check"
+
+ return "mdi:calendar-remove"
+
+ @property
+ def unique_id(self) -> str:
+ """Return the unique ID for this binary sensor."""
+ return self._unique_id
+
+ @property
+ def is_on(self) -> bool:
+ """Return the state of the binary sensor."""
+
+ self._state = self._client.on_duty
+
+ return self._state
+
+ @property
+ def device_state_attributes(self):
+ """Return available attributes for binary sensor."""
+ attr = {}
+ if not self.coordinator.data:
+ return attr
+
+ data = self.coordinator.data
+ attr = {
+ key: data[key]
+ for key in (
+ "start_time",
+ "end_time",
+ "available",
+ "active",
+ "assigned_function_ids",
+ "skill_ids",
+ "type",
+ "assigned_function",
+ )
+ if key in data
+ }
+
+ return attr
diff --git a/homeassistant/components/fireservicerota/config_flow.py b/homeassistant/components/fireservicerota/config_flow.py
new file mode 100644
index 00000000000..be986744d6c
--- /dev/null
+++ b/homeassistant/components/fireservicerota/config_flow.py
@@ -0,0 +1,129 @@
+"""Config flow for FireServiceRota."""
+from pyfireservicerota import FireServiceRota, InvalidAuthError
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_URL, CONF_USERNAME
+
+from .const import DOMAIN, URL_LIST
+
+DATA_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_URL, default="www.brandweerrooster.nl"): vol.In(URL_LIST),
+ vol.Required(CONF_USERNAME): str,
+ vol.Required(CONF_PASSWORD): str,
+ }
+)
+
+
+class FireServiceRotaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a FireServiceRota config flow."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
+ def __init__(self):
+ """Initialize config flow."""
+ self.api = None
+ self._base_url = None
+ self._username = None
+ self._password = None
+ self._existing_entry = None
+ self._description_placeholders = None
+
+ async def async_step_user(self, user_input=None):
+ """Handle a flow initiated by the user."""
+ errors = {}
+
+ if user_input is None:
+ return self._show_setup_form(user_input, errors)
+
+ return await self._validate_and_create_entry(user_input, "user")
+
+ async def _validate_and_create_entry(self, user_input, step_id):
+ """Check if config is valid and create entry if so."""
+ self._password = user_input[CONF_PASSWORD]
+
+ extra_inputs = user_input
+
+ if self._existing_entry:
+ extra_inputs = self._existing_entry
+
+ self._username = extra_inputs[CONF_USERNAME]
+ self._base_url = extra_inputs[CONF_URL]
+
+ if self.unique_id is None:
+ await self.async_set_unique_id(self._username)
+ self._abort_if_unique_id_configured()
+
+ self.api = FireServiceRota(
+ base_url=self._base_url,
+ username=self._username,
+ password=self._password,
+ )
+
+ try:
+ token_info = await self.hass.async_add_executor_job(self.api.request_tokens)
+ except InvalidAuthError:
+ self.api = None
+ return self.async_show_form(
+ step_id=step_id,
+ data_schema=DATA_SCHEMA,
+ errors={"base": "invalid_auth"},
+ )
+
+ data = {
+ "auth_implementation": DOMAIN,
+ CONF_URL: self._base_url,
+ CONF_USERNAME: self._username,
+ CONF_TOKEN: token_info,
+ }
+
+ if step_id == "user":
+ return self.async_create_entry(title=self._username, data=data)
+
+ for entry in self.hass.config_entries.async_entries(DOMAIN):
+ if entry.unique_id == self.unique_id:
+ self.hass.config_entries.async_update_entry(entry, data=data)
+ await self.hass.config_entries.async_reload(entry.entry_id)
+ return self.async_abort(reason="reauth_successful")
+
+ def _show_setup_form(self, user_input=None, errors=None, step_id="user"):
+ """Show the setup form to the user."""
+
+ if user_input is None:
+ user_input = {}
+
+ if step_id == "user":
+ schema = {
+ vol.Required(CONF_URL, default="www.brandweerrooster.nl"): vol.In(
+ URL_LIST
+ ),
+ vol.Required(CONF_USERNAME): str,
+ vol.Required(CONF_PASSWORD): str,
+ }
+ else:
+ schema = {vol.Required(CONF_PASSWORD): str}
+
+ return self.async_show_form(
+ step_id=step_id,
+ data_schema=vol.Schema(schema),
+ errors=errors or {},
+ description_placeholders=self._description_placeholders,
+ )
+
+ async def async_step_reauth(self, user_input=None):
+ """Get new tokens for a config entry that can't authenticate."""
+
+ if not self._existing_entry:
+ await self.async_set_unique_id(user_input[CONF_USERNAME])
+ self._existing_entry = user_input.copy()
+ self._description_placeholders = {"username": user_input[CONF_USERNAME]}
+ user_input = None
+
+ if user_input is None:
+ return self._show_setup_form(step_id=config_entries.SOURCE_REAUTH)
+
+ return await self._validate_and_create_entry(
+ user_input, config_entries.SOURCE_REAUTH
+ )
diff --git a/homeassistant/components/fireservicerota/const.py b/homeassistant/components/fireservicerota/const.py
new file mode 100644
index 00000000000..9be0bfdc0ca
--- /dev/null
+++ b/homeassistant/components/fireservicerota/const.py
@@ -0,0 +1,12 @@
+"""Constants for the FireServiceRota integration."""
+
+DOMAIN = "fireservicerota"
+
+URL_LIST = {
+ "www.brandweerrooster.nl": "BrandweerRooster",
+ "www.fireservicerota.co.uk": "FireServiceRota",
+}
+WSS_BWRURL = "wss://{0}/cable?access_token={1}"
+
+DATA_CLIENT = "client"
+DATA_COORDINATOR = "coordinator"
diff --git a/homeassistant/components/fireservicerota/manifest.json b/homeassistant/components/fireservicerota/manifest.json
new file mode 100644
index 00000000000..6485d155f50
--- /dev/null
+++ b/homeassistant/components/fireservicerota/manifest.json
@@ -0,0 +1,8 @@
+{
+ "domain": "fireservicerota",
+ "name": "FireServiceRota",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integrations/fireservicerota",
+ "requirements": ["pyfireservicerota==0.0.40"],
+ "codeowners": ["@cyberjunky"]
+}
diff --git a/homeassistant/components/fireservicerota/sensor.py b/homeassistant/components/fireservicerota/sensor.py
new file mode 100644
index 00000000000..83272eff926
--- /dev/null
+++ b/homeassistant/components/fireservicerota/sensor.py
@@ -0,0 +1,133 @@
+"""Sensor platform for FireServiceRota integration."""
+import logging
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.restore_state import RestoreEntity
+from homeassistant.helpers.typing import HomeAssistantType
+
+from .const import DATA_CLIENT, DOMAIN as FIRESERVICEROTA_DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(
+ hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
+) -> None:
+ """Set up FireServiceRota sensor based on a config entry."""
+ client = hass.data[FIRESERVICEROTA_DOMAIN][entry.entry_id][DATA_CLIENT]
+
+ async_add_entities([IncidentsSensor(client)])
+
+
+class IncidentsSensor(RestoreEntity):
+ """Representation of FireServiceRota incidents sensor."""
+
+ def __init__(self, client):
+ """Initialize."""
+ self._client = client
+ self._entry_id = self._client.entry_id
+ self._unique_id = f"{self._client.unique_id}_Incidents"
+ self._state = None
+ self._state_attributes = {}
+
+ @property
+ def name(self) -> str:
+ """Return the name of the sensor."""
+ return "Incidents"
+
+ @property
+ def icon(self) -> str:
+ """Return the icon to use in the frontend."""
+ if (
+ "prio" in self._state_attributes
+ and self._state_attributes["prio"][0] == "a"
+ ):
+ return "mdi:ambulance"
+
+ return "mdi:fire-truck"
+
+ @property
+ def state(self) -> str:
+ """Return the state of the sensor."""
+ return self._state
+
+ @property
+ def unique_id(self) -> str:
+ """Return the unique ID of the sensor."""
+ return self._unique_id
+
+ @property
+ def should_poll(self) -> bool:
+ """No polling needed."""
+ return False
+
+ @property
+ def device_state_attributes(self) -> object:
+ """Return available attributes for sensor."""
+ attr = {}
+ data = self._state_attributes
+
+ if not data:
+ return attr
+
+ for value in (
+ "id",
+ "trigger",
+ "created_at",
+ "message_to_speech_url",
+ "prio",
+ "type",
+ "responder_mode",
+ "can_respond_until",
+ ):
+ if data.get(value):
+ attr[value] = data[value]
+
+ if "address" not in data:
+ continue
+
+ for address_value in (
+ "latitude",
+ "longitude",
+ "address_type",
+ "formatted_address",
+ ):
+ if address_value in data["address"]:
+ attr[address_value] = data["address"][address_value]
+
+ return attr
+
+ async def async_added_to_hass(self) -> None:
+ """Run when about to be added to hass."""
+ await super().async_added_to_hass()
+
+ state = await self.async_get_last_state()
+ if state:
+ self._state = state.state
+ self._state_attributes = state.attributes
+ if "id" in self._state_attributes:
+ self._client.incident_id = self._state_attributes["id"]
+ _LOGGER.debug("Restored entity 'Incidents' to: %s", self._state)
+
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass,
+ f"{FIRESERVICEROTA_DOMAIN}_{self._entry_id}_update",
+ self.client_update,
+ )
+ )
+
+ @callback
+ def client_update(self) -> None:
+ """Handle updated data from the data client."""
+ data = self._client.websocket.incident_data
+ if not data or "body" not in data:
+ return
+
+ self._state = data["body"]
+ self._state_attributes = data
+ if "id" in self._state_attributes:
+ self._client.incident_id = self._state_attributes["id"]
+ self.async_write_ha_state()
diff --git a/homeassistant/components/fireservicerota/strings.json b/homeassistant/components/fireservicerota/strings.json
new file mode 100644
index 00000000000..c44673d6c2c
--- /dev/null
+++ b/homeassistant/components/fireservicerota/strings.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "password": "[%key:common::config_flow::data::password%]",
+ "username": "[%key:common::config_flow::data::username%]",
+ "url": "Website"
+ }
+ },
+ "reauth": {
+ "description": "Authentication tokens baceame invalid, login to recreate them.",
+ "data": {
+ "password": "[%key:common::config_flow::data::password%]"
+ }
+ }
+ },
+ "error": {
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
+ },
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
+ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
+ },
+ "create_entry": {
+ "default": "[%key:common::config_flow::create_entry::authenticated%]"
+ }
+ }
+}
diff --git a/homeassistant/components/fireservicerota/switch.py b/homeassistant/components/fireservicerota/switch.py
new file mode 100644
index 00000000000..7519270ca5c
--- /dev/null
+++ b/homeassistant/components/fireservicerota/switch.py
@@ -0,0 +1,149 @@
+"""Switch platform for FireServiceRota integration."""
+import logging
+
+from homeassistant.components.switch import SwitchEntity
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.typing import HomeAssistantType
+
+from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN as FIRESERVICEROTA_DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(
+ hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
+) -> None:
+ """Set up FireServiceRota switch based on a config entry."""
+ client = hass.data[FIRESERVICEROTA_DOMAIN][entry.entry_id][DATA_CLIENT]
+
+ coordinator = hass.data[FIRESERVICEROTA_DOMAIN][entry.entry_id][DATA_COORDINATOR]
+
+ async_add_entities([ResponseSwitch(coordinator, client, entry)])
+
+
+class ResponseSwitch(SwitchEntity):
+ """Representation of an FireServiceRota switch."""
+
+ def __init__(self, coordinator, client, entry):
+ """Initialize."""
+ self._coordinator = coordinator
+ self._client = client
+ self._unique_id = f"{entry.unique_id}_Response"
+ self._entry_id = entry.entry_id
+
+ self._state = None
+ self._state_attributes = {}
+ self._state_icon = None
+
+ @property
+ def name(self) -> str:
+ """Return the name of the switch."""
+ return "Incident Response"
+
+ @property
+ def icon(self) -> str:
+ """Return the icon to use in the frontend."""
+ if self._state_icon == "acknowledged":
+ return "mdi:run-fast"
+ if self._state_icon == "rejected":
+ return "mdi:account-off-outline"
+
+ return "mdi:forum"
+
+ @property
+ def is_on(self) -> bool:
+ """Get the assumed state of the switch."""
+ return self._state
+
+ @property
+ def unique_id(self) -> str:
+ """Return the unique ID for this switch."""
+ return self._unique_id
+
+ @property
+ def should_poll(self) -> bool:
+ """No polling needed."""
+ return False
+
+ @property
+ def available(self):
+ """Return if switch is available."""
+ return self._client.on_duty
+
+ @property
+ def device_state_attributes(self) -> object:
+ """Return available attributes for switch."""
+ attr = {}
+ if not self._state_attributes:
+ return attr
+
+ data = self._state_attributes
+ attr = {
+ key: data[key]
+ for key in (
+ "user_name",
+ "assigned_skill_ids",
+ "responded_at",
+ "start_time",
+ "status",
+ "reported_status",
+ "arrived_at_station",
+ "available_at_incident_creation",
+ "active_duty_function_ids",
+ )
+ if key in data
+ }
+
+ return attr
+
+ async def async_turn_on(self, **kwargs) -> None:
+ """Send Acknowlegde response status."""
+ await self.async_set_response(True)
+
+ async def async_turn_off(self, **kwargs) -> None:
+ """Send Reject response status."""
+ await self.async_set_response(False)
+
+ async def async_set_response(self, value) -> None:
+ """Send response status."""
+ if not self._client.on_duty:
+ _LOGGER.debug(
+ "Cannot send incident response when not on duty",
+ )
+ return
+
+ await self._client.async_set_response(value)
+ self.client_update()
+
+ async def async_added_to_hass(self) -> None:
+ """Register update callback."""
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass,
+ f"{FIRESERVICEROTA_DOMAIN}_{self._entry_id}_update",
+ self.client_update,
+ )
+ )
+ self.async_on_remove(
+ self._coordinator.async_add_listener(self.async_write_ha_state)
+ )
+
+ @callback
+ def client_update(self) -> None:
+ """Handle updated incident data from the client."""
+ self.async_schedule_update_ha_state(True)
+
+ async def async_update(self) -> bool:
+ """Update FireServiceRota response data."""
+ data = await self._client.async_response_update()
+
+ if not data or "status" not in data:
+ return
+
+ self._state = data["status"] == "acknowledged"
+ self._state_attributes = data
+ self._state_icon = data["status"]
+
+ _LOGGER.debug("Set state of entity 'Response Switch' to '%s'", self._state)
diff --git a/homeassistant/components/fireservicerota/translations/ca.json b/homeassistant/components/fireservicerota/translations/ca.json
new file mode 100644
index 00000000000..287bb81e51e
--- /dev/null
+++ b/homeassistant/components/fireservicerota/translations/ca.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El compte ja ha estat configurat",
+ "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament"
+ },
+ "create_entry": {
+ "default": "Autenticaci\u00f3 exitosa"
+ },
+ "error": {
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "Contrasenya"
+ },
+ "description": "Els tokens d'autenticaci\u00f3 ja no s\u00f3n v\u00e0lids, inicia sessi\u00f3 per tornar-los a generar."
+ },
+ "user": {
+ "data": {
+ "password": "Contrasenya",
+ "url": "Lloc web",
+ "username": "Nom d'usuari"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fireservicerota/translations/cs.json b/homeassistant/components/fireservicerota/translations/cs.json
new file mode 100644
index 00000000000..7ae758dab52
--- /dev/null
+++ b/homeassistant/components/fireservicerota/translations/cs.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u00da\u010det je ji\u017e nastaven",
+ "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9"
+ },
+ "create_entry": {
+ "default": "\u00dasp\u011b\u0161n\u011b ov\u011b\u0159eno"
+ },
+ "error": {
+ "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "Heslo"
+ },
+ "description": "Ov\u011b\u0159ovac\u00ed tokeny jsou neplatn\u00e9. Chcete-li je znovu vytvo\u0159it, p\u0159ihlaste se."
+ },
+ "user": {
+ "data": {
+ "password": "Heslo",
+ "url": "Webov\u00e1 str\u00e1nka",
+ "username": "U\u017eivatelsk\u00e9 jm\u00e9no"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fireservicerota/translations/en.json b/homeassistant/components/fireservicerota/translations/en.json
new file mode 100644
index 00000000000..288b89c31b8
--- /dev/null
+++ b/homeassistant/components/fireservicerota/translations/en.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Account is already configured",
+ "reauth_successful": "Re-authentication was successful"
+ },
+ "create_entry": {
+ "default": "Successfully authenticated"
+ },
+ "error": {
+ "invalid_auth": "Invalid authentication"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "Password"
+ },
+ "description": "Authentication tokens baceame invalid, login to recreate them."
+ },
+ "user": {
+ "data": {
+ "password": "Password",
+ "url": "Website",
+ "username": "Username"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fireservicerota/translations/es.json b/homeassistant/components/fireservicerota/translations/es.json
new file mode 100644
index 00000000000..9f27181dfe5
--- /dev/null
+++ b/homeassistant/components/fireservicerota/translations/es.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "La cuenta ya ha sido configurada",
+ "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente"
+ },
+ "create_entry": {
+ "default": "Autenticado correctamente"
+ },
+ "error": {
+ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "Contrase\u00f1a"
+ },
+ "description": "Los tokens de autenticaci\u00f3n ya no son v\u00e1lidos, inicia sesi\u00f3n para recrearlos."
+ },
+ "user": {
+ "data": {
+ "password": "Contrase\u00f1a",
+ "url": "Sitio web",
+ "username": "Usuario"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fireservicerota/translations/et.json b/homeassistant/components/fireservicerota/translations/et.json
new file mode 100644
index 00000000000..dedd74e8701
--- /dev/null
+++ b/homeassistant/components/fireservicerota/translations/et.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Kasutaja on juba seadistatud",
+ "reauth_successful": "Taastuvastamine \u00f5nnestus"
+ },
+ "create_entry": {
+ "default": "Tuvastamine \u00f5nnestus"
+ },
+ "error": {
+ "invalid_auth": "Vigane autentimine"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "Salas\u00f5na"
+ },
+ "description": "Tuvastusstring aegus, taasloomiseks logi sisse."
+ },
+ "user": {
+ "data": {
+ "password": "Salas\u00f5na",
+ "url": "Veebisait",
+ "username": "Kasutajanimi"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fireservicerota/translations/it.json b/homeassistant/components/fireservicerota/translations/it.json
new file mode 100644
index 00000000000..8fc43f294ec
--- /dev/null
+++ b/homeassistant/components/fireservicerota/translations/it.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'account \u00e8 gi\u00e0 configurato",
+ "reauth_successful": "La riautenticazione ha avuto successo"
+ },
+ "create_entry": {
+ "default": "Autenticazione riuscita"
+ },
+ "error": {
+ "invalid_auth": "Autenticazione non valida"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "Password"
+ },
+ "description": "I token di autenticazione non sono validi, effettua il login per ricrearli."
+ },
+ "user": {
+ "data": {
+ "password": "Password",
+ "url": "Sito web",
+ "username": "Nome utente"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fireservicerota/translations/ka.json b/homeassistant/components/fireservicerota/translations/ka.json
new file mode 100644
index 00000000000..422f3137d5c
--- /dev/null
+++ b/homeassistant/components/fireservicerota/translations/ka.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u10d0\u10dc\u10d2\u10d0\u10e0\u10d8\u10e8\u10d8 \u10e3\u10d9\u10d5\u10d4 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0",
+ "reauth_successful": "\u10e0\u10d4-\u10d0\u10d5\u10d7\u10d4\u10dc\u10e2\u10d8\u10e4\u10d8\u10ea\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0 \u10ec\u10d0\u10e0\u10db\u10d0\u10e2\u10d4\u10d1\u10d8\u10d7"
+ },
+ "create_entry": {
+ "default": "\u10d0\u10d5\u10d7\u10d4\u10dc\u10e2\u10d8\u10e4\u10d8\u10ea\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0 \u10ec\u10d0\u10e0\u10db\u10d0\u10e2\u10d4\u10d1\u10d8\u10d7"
+ },
+ "error": {
+ "invalid_auth": "\u10db\u10ea\u10d3\u10d0\u10e0\u10d8 \u10d0\u10d5\u10d7\u10d4\u10dc\u10e2\u10d8\u10e4\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "\u10de\u10d0\u10e0\u10dd\u10da\u10d8"
+ },
+ "description": "\u10d0\u10d5\u10d7\u10d4\u10dc\u10e2\u10d8\u10e4\u10d8\u10d9\u10d0\u10ea\u10d8\u10d8\u10e1 \u10e2\u10dd\u10d9\u10d4\u10dc\u10d8 \u10db\u10ea\u10d3\u10d0\u10e0\u10d8\u10d0, \u10e8\u10d4\u10d3\u10d8\u10d7 \u10e1\u10d8\u10e1\u10e2\u10d4\u10db\u10d0\u10e8\u10d8 \u10ee\u10d4\u10da\u10d0\u10ee\u10da\u10d0 \u10e8\u10d4\u10e1\u10d0\u10e5\u10db\u10dc\u10d4\u10da\u10d0\u10d3."
+ },
+ "user": {
+ "data": {
+ "password": "\u10de\u10d0\u10e0\u10dd\u10da\u10d8",
+ "url": "\u10d5\u10d4\u10d1\u10e1\u10d0\u10d8\u10e2\u10d8",
+ "username": "\u10db\u10dd\u10db\u10ee\u10db\u10d0\u10e0\u10d4\u10d1\u10d4\u10da\u10d8"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fireservicerota/translations/lb.json b/homeassistant/components/fireservicerota/translations/lb.json
new file mode 100644
index 00000000000..9f852c8fdfb
--- /dev/null
+++ b/homeassistant/components/fireservicerota/translations/lb.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Kont ass scho konfigur\u00e9iert",
+ "reauth_successful": "Re-authentifikatioun war erfollegr\u00e4ich"
+ },
+ "create_entry": {
+ "default": "Erfollegr\u00e4ich authentifiz\u00e9iert"
+ },
+ "error": {
+ "invalid_auth": "Ong\u00eblteg Authentifikatioun"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "Passwuert"
+ },
+ "description": "Acc\u00e8s Jetons sin ong\u00eblteg, verbann dech fir se n\u00e9i z'erstellen"
+ },
+ "user": {
+ "data": {
+ "password": "Passwuert",
+ "url": "Webs\u00e4it",
+ "username": "Benotzernumm"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fireservicerota/translations/no.json b/homeassistant/components/fireservicerota/translations/no.json
new file mode 100644
index 00000000000..5a4635e1ed8
--- /dev/null
+++ b/homeassistant/components/fireservicerota/translations/no.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Kontoen er allerede konfigurert",
+ "reauth_successful": "Reautentisering var vellykket"
+ },
+ "create_entry": {
+ "default": "Vellykket godkjenning"
+ },
+ "error": {
+ "invalid_auth": "Ugyldig godkjenning"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "Passord"
+ },
+ "description": "Autentiseringstokener for baceame er ugyldige, logg inn for \u00e5 gjenskape dem."
+ },
+ "user": {
+ "data": {
+ "password": "Passord",
+ "url": "Nettsted",
+ "username": "Brukernavn"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fireservicerota/translations/pl.json b/homeassistant/components/fireservicerota/translations/pl.json
new file mode 100644
index 00000000000..2e5e480fcc1
--- /dev/null
+++ b/homeassistant/components/fireservicerota/translations/pl.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Konto jest ju\u017c skonfigurowane",
+ "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119"
+ },
+ "create_entry": {
+ "default": "Pomy\u015blnie uwierzytelniono"
+ },
+ "error": {
+ "invalid_auth": "Niepoprawne uwierzytelnienie"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "Has\u0142o"
+ },
+ "description": "Tokeny uwierzytelniaj\u0105ce straci\u0142y wa\u017cno\u015b\u0107. Zaloguj si\u0119, aby je odtworzy\u0107."
+ },
+ "user": {
+ "data": {
+ "password": "Has\u0142o",
+ "url": "Strona internetowa",
+ "username": "Nazwa u\u017cytkownika"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fireservicerota/translations/ru.json b/homeassistant/components/fireservicerota/translations/ru.json
new file mode 100644
index 00000000000..2c90bd53ca9
--- /dev/null
+++ b/homeassistant/components/fireservicerota/translations/ru.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.",
+ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e."
+ },
+ "create_entry": {
+ "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e."
+ },
+ "error": {
+ "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f."
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c"
+ },
+ "description": "\u0422\u043e\u043a\u0435\u043d\u044b \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b, \u0432\u043e\u0439\u0434\u0438\u0442\u0435, \u0447\u0442\u043e\u0431\u044b \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0438\u0445 \u0437\u0430\u043d\u043e\u0432\u043e."
+ },
+ "user": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "url": "\u0412\u0435\u0431-\u0441\u0430\u0439\u0442",
+ "username": "\u041b\u043e\u0433\u0438\u043d"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fireservicerota/translations/tr.json b/homeassistant/components/fireservicerota/translations/tr.json
new file mode 100644
index 00000000000..a2d2cab3b74
--- /dev/null
+++ b/homeassistant/components/fireservicerota/translations/tr.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "\u015eifre"
+ },
+ "description": "Kimlik do\u011frulama jetonlar\u0131 ge\u00e7ersiz, yeniden olu\u015fturmak i\u00e7in oturum a\u00e7\u0131n."
+ },
+ "user": {
+ "data": {
+ "password": "\u015eifre",
+ "url": "Web sitesi",
+ "username": "Kullan\u0131c\u0131 ad\u0131"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/fireservicerota/translations/zh-Hant.json b/homeassistant/components/fireservicerota/translations/zh-Hant.json
new file mode 100644
index 00000000000..af3cba40dc6
--- /dev/null
+++ b/homeassistant/components/fireservicerota/translations/zh-Hant.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f"
+ },
+ "create_entry": {
+ "default": "\u5df2\u6210\u529f\u8a8d\u8b49"
+ },
+ "error": {
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "\u5bc6\u78bc"
+ },
+ "description": "\u8a8d\u8b49\u5bc6\u9470\u5df2\u7d93\u5931\u6548\uff0c\u8acb\u767b\u5165\u91cd\u65b0\u65b0\u589e\u3002"
+ },
+ "user": {
+ "data": {
+ "password": "\u5bc6\u78bc",
+ "url": "\u7db2\u7ad9",
+ "username": "\u4f7f\u7528\u8005\u540d\u7a31"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/firmata/translations/it.json b/homeassistant/components/firmata/translations/it.json
index 6c2460ab0b2..a4f6f9e7222 100644
--- a/homeassistant/components/firmata/translations/it.json
+++ b/homeassistant/components/firmata/translations/it.json
@@ -2,10 +2,6 @@
"config": {
"abort": {
"cannot_connect": "Impossibile connettersi"
- },
- "step": {
- "one": "uno",
- "other": "altri"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py
index f6e3fd90fe5..387eb78448c 100644
--- a/homeassistant/components/fitbit/sensor.py
+++ b/homeassistant/components/fitbit/sensor.py
@@ -472,7 +472,13 @@ class FitbitSensor(Entity):
def update(self):
"""Get the latest data from the Fitbit API and update the states."""
if self.resource_type == "devices/battery" and self.extra:
+ registered_devs = self.client.get_devices()
+ device_id = self.extra.get("id")
+ self.extra = list(
+ filter(lambda device: device.get("id") == device_id, registered_devs)
+ )[0]
self._state = self.extra.get("battery")
+
else:
container = self.resource_type.replace("/", "-")
response = self.client.time_series(self.resource_type, period="7d")
diff --git a/homeassistant/components/flick_electric/translations/no.json b/homeassistant/components/flick_electric/translations/no.json
index 5cc6357058d..21f0b0fabc8 100644
--- a/homeassistant/components/flick_electric/translations/no.json
+++ b/homeassistant/components/flick_electric/translations/no.json
@@ -11,7 +11,7 @@
"step": {
"user": {
"data": {
- "client_id": "Klient-ID (valgfritt)",
+ "client_id": "Klient ID (valgfritt)",
"client_secret": "Klienthemmelighet (valgfritt)",
"password": "Passord",
"username": "Brukernavn"
diff --git a/homeassistant/components/flick_electric/translations/sl.json b/homeassistant/components/flick_electric/translations/sl.json
new file mode 100644
index 00000000000..562dfd70bc6
--- /dev/null
+++ b/homeassistant/components/flick_electric/translations/sl.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "error": {
+ "unknown": "Nepri\u010dakovana napaka"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Geslo"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/flo/config_flow.py b/homeassistant/components/flo/config_flow.py
index b509c894068..54c6ae94ee2 100644
--- a/homeassistant/components/flo/config_flow.py
+++ b/homeassistant/components/flo/config_flow.py
@@ -1,6 +1,4 @@
"""Config flow for flo integration."""
-import logging
-
from aioflo import async_get_api
from aioflo.errors import RequestError
import voluptuous as vol
@@ -9,9 +7,7 @@ from homeassistant import config_entries, core, exceptions
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from .const import DOMAIN # pylint:disable=unused-import
-
-_LOGGER = logging.getLogger(__name__)
+from .const import DOMAIN, LOGGER # pylint:disable=unused-import
DATA_SCHEMA = vol.Schema({"username": str, "password": str})
@@ -28,7 +24,7 @@ async def validate_input(hass: core.HomeAssistant, data):
data[CONF_USERNAME], data[CONF_PASSWORD], session=session
)
except RequestError as request_error:
- _LOGGER.error("Error connecting to the Flo API: %s", request_error)
+ LOGGER.error("Error connecting to the Flo API: %s", request_error)
raise CannotConnect from request_error
user_info = await api.user.get_info()
diff --git a/homeassistant/components/flo/const.py b/homeassistant/components/flo/const.py
index 94c1b8d4579..907561b5b9c 100644
--- a/homeassistant/components/flo/const.py
+++ b/homeassistant/components/flo/const.py
@@ -1,4 +1,8 @@
"""Constants for the flo integration."""
+import logging
+
+LOGGER = logging.getLogger(__package__)
+
CLIENT = "client"
DOMAIN = "flo"
FLO_HOME = "home"
diff --git a/homeassistant/components/flo/device.py b/homeassistant/components/flo/device.py
index 824d62a9519..af36034026d 100644
--- a/homeassistant/components/flo/device.py
+++ b/homeassistant/components/flo/device.py
@@ -1,7 +1,6 @@
"""Flo device object."""
import asyncio
from datetime import datetime, timedelta
-import logging
from typing import Any, Dict, Optional
from aioflo.api import API
@@ -12,9 +11,7 @@ from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
import homeassistant.util.dt as dt_util
-from .const import DOMAIN as FLO_DOMAIN
-
-_LOGGER = logging.getLogger(__name__)
+from .const import DOMAIN as FLO_DOMAIN, LOGGER
class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator):
@@ -33,7 +30,7 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator):
self._water_usage: Optional[Dict[str, Any]] = None
super().__init__(
hass,
- _LOGGER,
+ LOGGER,
name=f"{FLO_DOMAIN}-{device_id}",
update_interval=timedelta(seconds=60),
)
@@ -195,7 +192,7 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator):
self._device_information = await self.api_client.device.get_info(
self._flo_device_id
)
- _LOGGER.debug("Flo device data: %s", self._device_information)
+ LOGGER.debug("Flo device data: %s", self._device_information)
async def _update_consumption_data(self, *_) -> None:
"""Update water consumption data from the API."""
@@ -205,4 +202,4 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator):
self._water_usage = await self.api_client.water.get_consumption_info(
self._flo_location_id, start_date, end_date
)
- _LOGGER.debug("Updated Flo consumption data: %s", self._water_usage)
+ LOGGER.debug("Updated Flo consumption data: %s", self._water_usage)
diff --git a/homeassistant/components/flume/translations/no.json b/homeassistant/components/flume/translations/no.json
index 78dd7e40217..5f473bfdfef 100644
--- a/homeassistant/components/flume/translations/no.json
+++ b/homeassistant/components/flume/translations/no.json
@@ -11,7 +11,7 @@
"step": {
"user": {
"data": {
- "client_id": "Klient-ID",
+ "client_id": "Klient ID",
"client_secret": "Klienthemmelighet",
"password": "Passord",
"username": "Brukernavn"
diff --git a/homeassistant/components/flunearyou/__init__.py b/homeassistant/components/flunearyou/__init__.py
index 7399dd3847d..46442f112b6 100644
--- a/homeassistant/components/flunearyou/__init__.py
+++ b/homeassistant/components/flunearyou/__init__.py
@@ -20,7 +20,7 @@ from .const import (
DEFAULT_UPDATE_INTERVAL = timedelta(minutes=30)
-CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.119")
+CONFIG_SCHEMA = cv.deprecated(DOMAIN)
PLATFORMS = ["sensor"]
diff --git a/homeassistant/components/foobot/manifest.json b/homeassistant/components/foobot/manifest.json
index f4a4315055d..09458a18d91 100644
--- a/homeassistant/components/foobot/manifest.json
+++ b/homeassistant/components/foobot/manifest.json
@@ -2,6 +2,6 @@
"domain": "foobot",
"name": "Foobot",
"documentation": "https://www.home-assistant.io/integrations/foobot",
- "requirements": ["foobot_async==0.3.2"],
+ "requirements": ["foobot_async==1.0.0"],
"codeowners": []
}
diff --git a/homeassistant/components/forked_daapd/translations/hu.json b/homeassistant/components/forked_daapd/translations/hu.json
new file mode 100644
index 00000000000..a9c13f1ee68
--- /dev/null
+++ b/homeassistant/components/forked_daapd/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "forbidden": "Nem tud csatlakozni. K\u00e9rj\u00fck, ellen\u0151rizze a forked-daapd h\u00e1l\u00f3zati enged\u00e9lyeket."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/forked_daapd/translations/ka.json b/homeassistant/components/forked_daapd/translations/ka.json
new file mode 100644
index 00000000000..b02d04823e5
--- /dev/null
+++ b/homeassistant/components/forked_daapd/translations/ka.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "forbidden": "\u1c95\u10d4\u10e0 \u10d5\u10e3\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d3\u10d4\u10d1\u10d8. \u10d2\u10d7\u10ee\u10dd\u10d5\u10d7, \u10e8\u10d4\u10d0\u10db\u10dd\u10ec\u10db\u10dd\u10d7 \u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 forked-daapd \u10e5\u10e1\u10d4\u10da\u10d8\u10e1 \u10e3\u10e4\u10da\u10d4\u10d1\u10d4\u10d1\u10d8."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/forked_daapd/translations/sl.json b/homeassistant/components/forked_daapd/translations/sl.json
new file mode 100644
index 00000000000..1c59e4bc9c7
--- /dev/null
+++ b/homeassistant/components/forked_daapd/translations/sl.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "unknown_error": "Nepri\u010dakovana napaka"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/foscam/camera.py b/homeassistant/components/foscam/camera.py
index 1c4c6bb9c8c..bc28e160b25 100644
--- a/homeassistant/components/foscam/camera.py
+++ b/homeassistant/components/foscam/camera.py
@@ -13,14 +13,9 @@ from homeassistant.const import (
CONF_PORT,
CONF_USERNAME,
)
-from homeassistant.helpers import config_validation as cv
-from homeassistant.helpers.service import async_extract_entity_ids
+from homeassistant.helpers import config_validation as cv, entity_platform
-from .const import (
- DATA as FOSCAM_DATA,
- DOMAIN as FOSCAM_DOMAIN,
- ENTITIES as FOSCAM_ENTITIES,
-)
+from .const import DATA as FOSCAM_DATA, ENTITIES as FOSCAM_ENTITIES
_LOGGER = logging.getLogger(__name__)
@@ -90,28 +85,26 @@ SERVICE_PTZ_SCHEMA = vol.Schema(
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up a Foscam IP Camera."""
-
- async def async_handle_ptz(service):
- """Handle PTZ service call."""
- movement = service.data[ATTR_MOVEMENT]
- travel_time = service.data[ATTR_TRAVELTIME]
- entity_ids = await async_extract_entity_ids(hass, service)
-
- if not entity_ids:
- return
-
- _LOGGER.debug("Moving '%s' camera(s): %s", movement, entity_ids)
-
- all_cameras = hass.data[FOSCAM_DATA][FOSCAM_ENTITIES]
- target_cameras = [
- camera for camera in all_cameras if camera.entity_id in entity_ids
- ]
-
- for camera in target_cameras:
- await camera.async_perform_ptz(movement, travel_time)
-
- hass.services.async_register(
- FOSCAM_DOMAIN, SERVICE_PTZ, async_handle_ptz, schema=SERVICE_PTZ_SCHEMA
+ platform = entity_platform.current_platform.get()
+ assert platform is not None
+ platform.async_register_entity_service(
+ "ptz",
+ {
+ vol.Required(ATTR_MOVEMENT): vol.In(
+ [
+ DIR_UP,
+ DIR_DOWN,
+ DIR_LEFT,
+ DIR_RIGHT,
+ DIR_TOPLEFT,
+ DIR_TOPRIGHT,
+ DIR_BOTTOMLEFT,
+ DIR_BOTTOMRIGHT,
+ ]
+ ),
+ vol.Optional(ATTR_TRAVELTIME, default=DEFAULT_TRAVELTIME): cv.small_float,
+ },
+ "async_perform_ptz",
)
camera = FoscamCamera(
diff --git a/homeassistant/components/freebox/translations/hu.json b/homeassistant/components/freebox/translations/hu.json
index 937f441845c..d13f5fa17c8 100644
--- a/homeassistant/components/freebox/translations/hu.json
+++ b/homeassistant/components/freebox/translations/hu.json
@@ -1,5 +1,8 @@
{
"config": {
+ "error": {
+ "register_failed": "Regisztr\u00e1ci\u00f3 nem siker\u00fclt, k\u00e9rem pr\u00f3b\u00e1lja \u00fajra"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/fritzbox/translations/ka.json b/homeassistant/components/fritzbox/translations/ka.json
new file mode 100644
index 00000000000..6d43d983836
--- /dev/null
+++ b/homeassistant/components/fritzbox/translations/ka.json
@@ -0,0 +1,10 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "\u10e5\u10e1\u10d4\u10da\u10d8\u10e8 \u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0 \u10d0\u10e0 \u10db\u10dd\u10d8\u10eb\u10d4\u10d1\u10dc\u10d0"
+ },
+ "error": {
+ "invalid_auth": "\u10db\u10ea\u10d3\u10d0\u10e0\u10d8 \u10d0\u10d5\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json
index aea1717ce6f..caf309e6718 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==20201111.2"],
+ "requirements": ["home-assistant-frontend==20201212.0"],
"dependencies": [
"api",
"auth",
diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py
index 4072c43bc27..175ee8f1d5b 100644
--- a/homeassistant/components/generic_thermostat/climate.py
+++ b/homeassistant/components/generic_thermostat/climate.py
@@ -33,7 +33,7 @@ from homeassistant.const import (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
-from homeassistant.core import DOMAIN as HA_DOMAIN, callback
+from homeassistant.core import DOMAIN as HA_DOMAIN, CoreState, callback
from homeassistant.helpers import condition
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import (
@@ -207,7 +207,7 @@ class GenericThermostat(ClimateEntity, RestoreEntity):
)
@callback
- def _async_startup(event):
+ def _async_startup(*_):
"""Init on startup."""
sensor_state = self.hass.states.get(self.sensor_entity_id)
if sensor_state and sensor_state.state not in (
@@ -215,8 +215,12 @@ class GenericThermostat(ClimateEntity, RestoreEntity):
STATE_UNKNOWN,
):
self._async_update_temp(sensor_state)
+ self.async_write_ha_state()
- self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_startup)
+ if self.hass.state == CoreState.running:
+ _async_startup()
+ else:
+ self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_startup)
# Check If we have an old state
old_state = await self.async_get_last_state()
diff --git a/homeassistant/components/geofency/translations/ka.json b/homeassistant/components/geofency/translations/ka.json
new file mode 100644
index 00000000000..75c4f0a922c
--- /dev/null
+++ b/homeassistant/components/geofency/translations/ka.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0. \u10e8\u10d4\u10e1\u10d0\u10eb\u10da\u10d4\u10d1\u10d4\u10da\u10d8\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0.",
+ "webhook_not_internet_accessible": "\u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 Home Assistant \u10d4\u10d9\u10d6\u10d4\u10db\u10de\u10da\u10d0\u10e0\u10d8 \u10e1\u10d0\u10ed\u10d8\u10e0\u10dd\u10e0\u10d4\u10d1\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10dc\u10d4\u10e2\u10d7\u10d0\u10dc \u10ec\u10d5\u10d3\u10dd\u10db\u10d0\u10e1 webhook \u10e8\u10d4\u10e2\u10e7\u10dd\u10d1\u10d8\u10dc\u10d4\u10d1\u10d4\u10d1\u10d8\u10e1 \u10db\u10d8\u10e1\u10d0\u10e6\u10d4\u10d1\u10d0\u10d3."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/geofency/translations/lb.json b/homeassistant/components/geofency/translations/lb.json
index 1e7b20d8423..b8b6da7707b 100644
--- a/homeassistant/components/geofency/translations/lb.json
+++ b/homeassistant/components/geofency/translations/lb.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech."
+ "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech.",
+ "webhook_not_internet_accessible": "Deng Home Assistant Instanz muss iwwert Internet accessibel si fir Webhook Noriichten z'empf\u00e4nken."
},
"create_entry": {
"default": "Fir Evenementer un Home Assistant ze sch\u00e9cken, muss den Webhook Feature am Geofency ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nLiest [Dokumentatioun]({docs_url}) fir w\u00e9ider D\u00e9tailer."
diff --git a/homeassistant/components/geofency/translations/pl.json b/homeassistant/components/geofency/translations/pl.json
index b43dbabb806..c504e31051a 100644
--- a/homeassistant/components/geofency/translations/pl.json
+++ b/homeassistant/components/geofency/translations/pl.json
@@ -5,7 +5,7 @@
"webhook_not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty webhook"
},
"create_entry": {
- "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant, 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_volcano/translations/ka.json b/homeassistant/components/geonetnz_volcano/translations/ka.json
new file mode 100644
index 00000000000..2354e938d34
--- /dev/null
+++ b/homeassistant/components/geonetnz_volcano/translations/ka.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u10d0\u10d3\u10d2\u10d8\u10da\u10db\u10d3\u10d4\u10d1\u10d0\u10e0\u10d4\u10dd\u10d1\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/goalzero/translations/sl.json b/homeassistant/components/goalzero/translations/sl.json
new file mode 100644
index 00000000000..7dcc7b43684
--- /dev/null
+++ b/homeassistant/components/goalzero/translations/sl.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "error": {
+ "unknown": "Nepri\u010dakovana napaka"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "name": "Ime"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gogogate2/translations/sl.json b/homeassistant/components/gogogate2/translations/sl.json
new file mode 100644
index 00000000000..78e46cfb9e3
--- /dev/null
+++ b/homeassistant/components/gogogate2/translations/sl.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "ip_address": "IP naslov"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py
index 0afd66c10aa..8f4ee3b51c4 100644
--- a/homeassistant/components/google_assistant/__init__.py
+++ b/homeassistant/components/google_assistant/__init__.py
@@ -11,8 +11,6 @@ from homeassistant.helpers import config_validation as cv
from .const import (
CONF_ALIASES,
- CONF_ALLOW_UNLOCK,
- CONF_API_KEY,
CONF_CLIENT_EMAIL,
CONF_ENTITY_CONFIG,
CONF_EXPOSE,
@@ -36,6 +34,9 @@ from .const import EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED # noqa: F401, is
_LOGGER = logging.getLogger(__name__)
+CONF_ALLOW_UNLOCK = "allow_unlock"
+CONF_API_KEY = "api_key"
+
ENTITY_SCHEMA = vol.Schema(
{
vol.Optional(CONF_NAME): cv.string,
@@ -61,8 +62,6 @@ def _check_report_state(data):
GOOGLE_ASSISTANT_SCHEMA = vol.All(
- cv.deprecated(CONF_ALLOW_UNLOCK, invalidation_version="0.95"),
- cv.deprecated(CONF_API_KEY, invalidation_version="0.105"),
vol.Schema(
{
vol.Required(CONF_PROJECT_ID): cv.string,
@@ -72,13 +71,14 @@ GOOGLE_ASSISTANT_SCHEMA = vol.All(
vol.Optional(
CONF_EXPOSED_DOMAINS, default=DEFAULT_EXPOSED_DOMAINS
): cv.ensure_list,
- vol.Optional(CONF_API_KEY): cv.string,
vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ENTITY_SCHEMA},
- vol.Optional(CONF_ALLOW_UNLOCK): cv.boolean,
# str on purpose, makes sure it is configured correctly.
vol.Optional(CONF_SECURE_DEVICES_PIN): str,
vol.Optional(CONF_REPORT_STATE, default=False): cv.boolean,
vol.Optional(CONF_SERVICE_ACCOUNT): GOOGLE_SERVICE_ACCOUNT,
+ # deprecated configuration options
+ vol.Remove(CONF_ALLOW_UNLOCK): cv.boolean,
+ vol.Remove(CONF_API_KEY): cv.string,
},
extra=vol.PREVENT_EXTRA,
),
@@ -113,7 +113,7 @@ async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]):
await google_config.async_sync_entities(agent_user_id)
# Register service only if key is provided
- if CONF_API_KEY in config or CONF_SERVICE_ACCOUNT in config:
+ if CONF_SERVICE_ACCOUNT in config:
hass.services.async_register(
DOMAIN, SERVICE_REQUEST_SYNC, request_sync_service_handler
)
diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py
index be69f020190..d6badf2e7ba 100644
--- a/homeassistant/components/google_assistant/const.py
+++ b/homeassistant/components/google_assistant/const.py
@@ -30,9 +30,7 @@ CONF_EXPOSE_BY_DEFAULT = "expose_by_default"
CONF_EXPOSED_DOMAINS = "exposed_domains"
CONF_PROJECT_ID = "project_id"
CONF_ALIASES = "aliases"
-CONF_API_KEY = "api_key"
CONF_ROOM_HINT = "room"
-CONF_ALLOW_UNLOCK = "allow_unlock"
CONF_SECURE_DEVICES_PIN = "secure_devices_pin"
CONF_REPORT_STATE = "report_state"
CONF_SERVICE_ACCOUNT = "service_account"
@@ -104,6 +102,7 @@ ERR_UNSUPPORTED_INPUT = "unsupportedInput"
ERR_ALREADY_DISARMED = "alreadyDisarmed"
ERR_ALREADY_ARMED = "alreadyArmed"
+ERR_ALREADY_STOPPED = "alreadyStopped"
ERR_CHALLENGE_NEEDED = "challengeNeeded"
ERR_CHALLENGE_NOT_SETUP = "challengeFailedNotSetup"
diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py
index 4bf0df8b933..5cf1cb14379 100644
--- a/homeassistant/components/google_assistant/http.py
+++ b/homeassistant/components/google_assistant/http.py
@@ -19,7 +19,6 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util import dt as dt_util
from .const import (
- CONF_API_KEY,
CONF_CLIENT_EMAIL,
CONF_ENTITY_CONFIG,
CONF_EXPOSE,
@@ -135,11 +134,7 @@ class GoogleConfig(AbstractConfig):
return True
async def _async_request_sync_devices(self, agent_user_id: str):
- if CONF_API_KEY in self._config:
- await self.async_call_homegraph_api_key(
- REQUEST_SYNC_BASE_URL, {"agentUserId": agent_user_id}
- )
- elif CONF_SERVICE_ACCOUNT in self._config:
+ if CONF_SERVICE_ACCOUNT in self._config:
await self.async_call_homegraph_api(
REQUEST_SYNC_BASE_URL, {"agentUserId": agent_user_id}
)
@@ -164,25 +159,6 @@ class GoogleConfig(AbstractConfig):
self._access_token = token["access_token"]
self._access_token_renew = now + timedelta(seconds=token["expires_in"])
- async def async_call_homegraph_api_key(self, url, data):
- """Call a homegraph api with api key authentication."""
- websession = async_get_clientsession(self.hass)
- try:
- res = await websession.post(
- url, params={"key": self._config.get(CONF_API_KEY)}, json=data
- )
- _LOGGER.debug(
- "Response on %s with data %s was %s", url, data, await res.text()
- )
- res.raise_for_status()
- return res.status
- except ClientResponseError as error:
- _LOGGER.error("Request for %s failed: %d", url, error.status)
- return error.status
- except (asyncio.TimeoutError, ClientError):
- _LOGGER.error("Could not contact %s", url)
- return HTTP_INTERNAL_SERVER_ERROR
-
async def async_call_homegraph_api(self, url, data):
"""Call a homegraph api with authentication."""
session = async_get_clientsession(self.hass)
diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py
index 653324758e0..8790c3c7402 100644
--- a/homeassistant/components/google_assistant/trait.py
+++ b/homeassistant/components/google_assistant/trait.py
@@ -67,8 +67,8 @@ from .const import (
CHALLENGE_PIN_NEEDED,
ERR_ALREADY_ARMED,
ERR_ALREADY_DISARMED,
+ ERR_ALREADY_STOPPED,
ERR_CHALLENGE_NOT_SETUP,
- ERR_FUNCTION_NOT_SUPPORTED,
ERR_NOT_SUPPORTED,
ERR_UNSUPPORTED_INPUT,
ERR_VALUE_OUT_OF_RANGE,
@@ -120,6 +120,7 @@ COMMAND_INPUT = f"{PREFIX_COMMANDS}SetInput"
COMMAND_NEXT_INPUT = f"{PREFIX_COMMANDS}NextInput"
COMMAND_PREVIOUS_INPUT = f"{PREFIX_COMMANDS}PreviousInput"
COMMAND_OPENCLOSE = f"{PREFIX_COMMANDS}OpenClose"
+COMMAND_OPENCLOSE_RELATIVE = f"{PREFIX_COMMANDS}OpenCloseRelative"
COMMAND_SET_VOLUME = f"{PREFIX_COMMANDS}setVolume"
COMMAND_VOLUME_RELATIVE = f"{PREFIX_COMMANDS}volumeRelative"
COMMAND_MUTE = f"{PREFIX_COMMANDS}mute"
@@ -564,24 +565,49 @@ class StartStopTrait(_Trait):
@staticmethod
def supported(domain, features, device_class):
"""Test if state is supported."""
- return domain == vacuum.DOMAIN
+ if domain == vacuum.DOMAIN:
+ return True
+
+ if domain == cover.DOMAIN and features & cover.SUPPORT_STOP:
+ return True
+
+ return False
def sync_attributes(self):
"""Return StartStop attributes for a sync request."""
- return {
- "pausable": self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
- & vacuum.SUPPORT_PAUSE
- != 0
- }
+ domain = self.state.domain
+ if domain == vacuum.DOMAIN:
+ return {
+ "pausable": self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
+ & vacuum.SUPPORT_PAUSE
+ != 0
+ }
+ if domain == cover.DOMAIN:
+ return {}
def query_attributes(self):
"""Return StartStop query attributes."""
- return {
- "isRunning": self.state.state == vacuum.STATE_CLEANING,
- "isPaused": self.state.state == vacuum.STATE_PAUSED,
- }
+ domain = self.state.domain
+ state = self.state.state
+
+ if domain == vacuum.DOMAIN:
+ return {
+ "isRunning": state == vacuum.STATE_CLEANING,
+ "isPaused": state == vacuum.STATE_PAUSED,
+ }
+
+ if domain == cover.DOMAIN:
+ return {"isRunning": state in (cover.STATE_CLOSING, cover.STATE_OPENING)}
async def execute(self, command, data, params, challenge):
+ """Execute a StartStop command."""
+ domain = self.state.domain
+ if domain == vacuum.DOMAIN:
+ return await self._execute_vacuum(command, data, params, challenge)
+ if domain == cover.DOMAIN:
+ return await self._execute_cover(command, data, params, challenge)
+
+ async def _execute_vacuum(self, command, data, params, challenge):
"""Execute a StartStop command."""
if command == COMMAND_STARTSTOP:
if params["start"]:
@@ -618,6 +644,31 @@ class StartStopTrait(_Trait):
context=data.context,
)
+ async def _execute_cover(self, command, data, params, challenge):
+ """Execute a StartStop command."""
+ if command == COMMAND_STARTSTOP:
+ if params["start"] is False:
+ if self.state.state in (cover.STATE_CLOSING, cover.STATE_OPENING):
+ await self.hass.services.async_call(
+ self.state.domain,
+ cover.SERVICE_STOP_COVER,
+ {ATTR_ENTITY_ID: self.state.entity_id},
+ blocking=True,
+ context=data.context,
+ )
+ else:
+ raise SmartHomeError(
+ ERR_ALREADY_STOPPED, "Cover is already stopped"
+ )
+ else:
+ raise SmartHomeError(
+ ERR_NOT_SUPPORTED, "Starting a cover is not supported"
+ )
+ else:
+ raise SmartHomeError(
+ ERR_NOT_SUPPORTED, f"Command {command} is not supported"
+ )
+
@register_trait
class TemperatureSettingTrait(_Trait):
@@ -1519,9 +1570,7 @@ class OpenCloseTrait(_Trait):
)
name = TRAIT_OPENCLOSE
- commands = [COMMAND_OPENCLOSE]
-
- override_position = None
+ commands = [COMMAND_OPENCLOSE, COMMAND_OPENCLOSE_RELATIVE]
@staticmethod
def supported(domain, features, device_class):
@@ -1545,9 +1594,24 @@ class OpenCloseTrait(_Trait):
def sync_attributes(self):
"""Return opening direction."""
response = {}
+ features = self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
+
if self.state.domain == binary_sensor.DOMAIN:
response["queryOnlyOpenClose"] = True
response["discreteOnlyOpenClose"] = True
+ elif self.state.domain == cover.DOMAIN:
+ if features & cover.SUPPORT_SET_POSITION == 0:
+ response["discreteOnlyOpenClose"] = True
+
+ if (
+ features & cover.SUPPORT_OPEN == 0
+ and features & cover.SUPPORT_CLOSE == 0
+ ):
+ response["queryOnlyOpenClose"] = True
+
+ if self.state.attributes.get(ATTR_ASSUMED_STATE):
+ response["commandOnlyOpenClose"] = True
+
return response
def query_attributes(self):
@@ -1555,25 +1619,20 @@ class OpenCloseTrait(_Trait):
domain = self.state.domain
response = {}
- if self.override_position is not None:
- response["openPercent"] = self.override_position
-
- elif domain == cover.DOMAIN:
- # When it's an assumed state, we will return that querying state
- # is not supported.
- if self.state.attributes.get(ATTR_ASSUMED_STATE):
- raise SmartHomeError(
- ERR_NOT_SUPPORTED, "Querying state is not supported"
- )
+ # When it's an assumed state, we will return empty state
+ # This shouldn't happen because we set `commandOnlyOpenClose`
+ # but Google still queries. Erroring here will cause device
+ # to show up offline.
+ if self.state.attributes.get(ATTR_ASSUMED_STATE):
+ return response
+ if domain == cover.DOMAIN:
if self.state.state == STATE_UNKNOWN:
raise SmartHomeError(
ERR_NOT_SUPPORTED, "Querying state is not supported"
)
- position = self.override_position or self.state.attributes.get(
- cover.ATTR_CURRENT_POSITION
- )
+ position = self.state.attributes.get(cover.ATTR_CURRENT_POSITION)
if position is not None:
response["openPercent"] = position
@@ -1593,26 +1652,36 @@ class OpenCloseTrait(_Trait):
async def execute(self, command, data, params, challenge):
"""Execute an Open, close, Set position command."""
domain = self.state.domain
+ features = self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if domain == cover.DOMAIN:
svc_params = {ATTR_ENTITY_ID: self.state.entity_id}
+ should_verify = False
+ if command == COMMAND_OPENCLOSE_RELATIVE:
+ position = self.state.attributes.get(cover.ATTR_CURRENT_POSITION)
+ if position is None:
+ raise SmartHomeError(
+ ERR_NOT_SUPPORTED,
+ "Current position not know for relative command",
+ )
+ position = max(0, min(100, position + params["openRelativePercent"]))
+ else:
+ position = params["openPercent"]
- if params["openPercent"] == 0:
+ if features & cover.SUPPORT_SET_POSITION:
+ service = cover.SERVICE_SET_COVER_POSITION
+ if position > 0:
+ should_verify = True
+ svc_params[cover.ATTR_POSITION] = position
+ elif position == 0:
service = cover.SERVICE_CLOSE_COVER
should_verify = False
- elif params["openPercent"] == 100:
+ elif position == 100:
service = cover.SERVICE_OPEN_COVER
should_verify = True
- elif (
- self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
- & cover.SUPPORT_SET_POSITION
- ):
- service = cover.SERVICE_SET_COVER_POSITION
- should_verify = True
- svc_params[cover.ATTR_POSITION] = params["openPercent"]
else:
raise SmartHomeError(
- ERR_FUNCTION_NOT_SUPPORTED, "Setting a position is not supported"
+ ERR_NOT_SUPPORTED, "No support for partial open close"
)
if (
@@ -1626,12 +1695,6 @@ class OpenCloseTrait(_Trait):
cover.DOMAIN, service, svc_params, blocking=True, context=data.context
)
- if (
- self.state.attributes.get(ATTR_ASSUMED_STATE)
- or self.state.state == STATE_UNKNOWN
- ):
- self.override_position = params["openPercent"]
-
@register_trait
class VolumeTrait(_Trait):
diff --git a/homeassistant/components/gpslogger/translations/ka.json b/homeassistant/components/gpslogger/translations/ka.json
new file mode 100644
index 00000000000..75c4f0a922c
--- /dev/null
+++ b/homeassistant/components/gpslogger/translations/ka.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0. \u10e8\u10d4\u10e1\u10d0\u10eb\u10da\u10d4\u10d1\u10d4\u10da\u10d8\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0.",
+ "webhook_not_internet_accessible": "\u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 Home Assistant \u10d4\u10d9\u10d6\u10d4\u10db\u10de\u10da\u10d0\u10e0\u10d8 \u10e1\u10d0\u10ed\u10d8\u10e0\u10dd\u10e0\u10d4\u10d1\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10dc\u10d4\u10e2\u10d7\u10d0\u10dc \u10ec\u10d5\u10d3\u10dd\u10db\u10d0\u10e1 webhook \u10e8\u10d4\u10e2\u10e7\u10dd\u10d1\u10d8\u10dc\u10d4\u10d1\u10d4\u10d1\u10d8\u10e1 \u10db\u10d8\u10e1\u10d0\u10e6\u10d4\u10d1\u10d0\u10d3."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/gpslogger/translations/lb.json b/homeassistant/components/gpslogger/translations/lb.json
index b3b137544f3..25f073ebd8c 100644
--- a/homeassistant/components/gpslogger/translations/lb.json
+++ b/homeassistant/components/gpslogger/translations/lb.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech."
+ "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech.",
+ "webhook_not_internet_accessible": "Deng Home Assistant Instanz muss iwwert Internet accessibel si fir Webhook Noriichten z'empf\u00e4nken."
},
"create_entry": {
"default": "Fir Evenementer un Home Assistant ze sch\u00e9cken, muss den Webhook Feature am GPSLogger ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nLiest [Dokumentatioun]({docs_url}) fir w\u00e9ider D\u00e9tailer."
diff --git a/homeassistant/components/gpslogger/translations/pl.json b/homeassistant/components/gpslogger/translations/pl.json
index a7f40dbeb9f..f868f770c5d 100644
--- a/homeassistant/components/gpslogger/translations/pl.json
+++ b/homeassistant/components/gpslogger/translations/pl.json
@@ -5,7 +5,7 @@
"webhook_not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty webhook"
},
"create_entry": {
- "default": "Aby wysy\u0142a\u0107 lokalizacje do Home Assistant, 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/gree/translations/ka.json b/homeassistant/components/gree/translations/ka.json
new file mode 100644
index 00000000000..2dff3849b85
--- /dev/null
+++ b/homeassistant/components/gree/translations/ka.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "\u10e5\u10e1\u10d4\u10da\u10d8\u10e1 \u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10d8\u10eb\u10d4\u10d1\u10dc\u10d0",
+ "single_instance_allowed": "\u10e3\u10d9\u10d5\u10d4 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0. \u10e8\u10d4\u10e1\u10d0\u10eb\u10da\u10d4\u10d1\u10d4\u10da\u10d8\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0."
+ },
+ "step": {
+ "confirm": {
+ "description": "\u10d2\u10dc\u10d4\u10d1\u10d0\u10d5\u10d7 \u10d3\u10d0\u10d8\u10ec\u10e7\u10dd\u10d7 \u10d3\u10d0\u10e7\u10d4\u10dc\u10d4\u10d1\u10d0?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py
index 4a0050868d9..32a9bd41014 100644
--- a/homeassistant/components/group/__init__.py
+++ b/homeassistant/components/group/__init__.py
@@ -1,4 +1,5 @@
"""Provide the functionality to group entities."""
+from abc import abstractmethod
import asyncio
from contextvars import ContextVar
import logging
@@ -398,7 +399,8 @@ class GroupEntity(Entity):
assert self.hass is not None
async def _update_at_start(_):
- await self.async_update_ha_state(True)
+ await self.async_update()
+ self.async_write_ha_state()
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _update_at_start)
@@ -409,7 +411,12 @@ class GroupEntity(Entity):
if self.hass.state != CoreState.running:
return
- await self.async_update_ha_state(True)
+ await self.async_update()
+ self.async_write_ha_state()
+
+ @abstractmethod
+ async def async_update(self) -> None:
+ """Abstract method to update the entity."""
class Group(Entity):
diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py
index 25a8665db9f..b52546c48d7 100644
--- a/homeassistant/components/group/cover.py
+++ b/homeassistant/components/group/cover.py
@@ -150,6 +150,8 @@ class CoverGroup(GroupEntity, CoverEntity):
"""Register listeners."""
for entity_id in self._entities:
new_state = self.hass.states.get(entity_id)
+ if new_state is None:
+ continue
await self.async_update_supported_features(
entity_id, new_state, update_state=False
)
@@ -307,6 +309,8 @@ class CoverGroup(GroupEntity, CoverEntity):
self._cover_position = 0 if self.is_closed else 100
for entity_id in self._covers[KEY_POSITION]:
state = self.hass.states.get(entity_id)
+ if state is None:
+ continue
pos = state.attributes.get(ATTR_CURRENT_POSITION)
if position == -1:
position = pos
@@ -323,6 +327,8 @@ class CoverGroup(GroupEntity, CoverEntity):
self._tilt_position = 100
for entity_id in self._tilts[KEY_POSITION]:
state = self.hass.states.get(entity_id)
+ if state is None:
+ continue
pos = state.attributes.get(ATTR_CURRENT_TILT_POSITION)
if position == -1:
position = pos
@@ -351,6 +357,8 @@ class CoverGroup(GroupEntity, CoverEntity):
if not self._assumed_state:
for entity_id in self._entities:
state = self.hass.states.get(entity_id)
+ if state is None:
+ continue
if state and state.attributes.get(ATTR_ASSUMED_STATE):
self._assumed_state = True
break
diff --git a/homeassistant/components/guardian/translations/hu.json b/homeassistant/components/guardian/translations/hu.json
new file mode 100644
index 00000000000..563ede56155
--- /dev/null
+++ b/homeassistant/components/guardian/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/guardian/translations/sl.json b/homeassistant/components/guardian/translations/sl.json
new file mode 100644
index 00000000000..f3542bb5899
--- /dev/null
+++ b/homeassistant/components/guardian/translations/sl.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "ip_address": "IP naslov",
+ "port": "Vrata"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py
index 6484c85b95e..e8b874b2334 100644
--- a/homeassistant/components/hassio/__init__.py
+++ b/homeassistant/components/hassio/__init__.py
@@ -2,6 +2,7 @@
from datetime import timedelta
import logging
import os
+from typing import Optional
import voluptuous as vol
@@ -23,6 +24,7 @@ from homeassistant.util.dt import utcnow
from .addon_panel import async_setup_addon_panel
from .auth import async_setup_auth_view
+from .const import ATTR_DISCOVERY
from .discovery import async_setup_discovery_view
from .handler import HassIO, HassioAPIError, api_data
from .http import HassIOView
@@ -159,7 +161,7 @@ async def async_uninstall_addon(hass: HomeAssistantType, slug: str) -> dict:
"""
hassio = hass.data[DOMAIN]
command = f"/addons/{slug}/uninstall"
- return await hassio.send_command(command)
+ return await hassio.send_command(command, timeout=60)
@bind_hass
@@ -183,7 +185,7 @@ async def async_stop_addon(hass: HomeAssistantType, slug: str) -> dict:
"""
hassio = hass.data[DOMAIN]
command = f"/addons/{slug}/stop"
- return await hassio.send_command(command)
+ return await hassio.send_command(command, timeout=60)
@bind_hass
@@ -200,6 +202,17 @@ async def async_set_addon_options(
return await hassio.send_command(command, payload=options)
+@bind_hass
+async def async_get_addon_discovery_info(
+ hass: HomeAssistantType, slug: str
+) -> Optional[dict]:
+ """Return discovery data for an add-on."""
+ hassio = hass.data[DOMAIN]
+ data = await hassio.retrieve_discovery_messages()
+ discovered_addons = data[ATTR_DISCOVERY]
+ return next((addon for addon in discovered_addons if addon["addon"] == slug), None)
+
+
@callback
@bind_hass
def get_info(hass):
diff --git a/homeassistant/components/hassio/translations/cs.json b/homeassistant/components/hassio/translations/cs.json
index 729dc069d7d..cf1a28c3cc2 100644
--- a/homeassistant/components/hassio/translations/cs.json
+++ b/homeassistant/components/hassio/translations/cs.json
@@ -3,7 +3,7 @@
"info": {
"board": "Deska",
"disk_total": "Kapacita disku",
- "disk_used": "Obsazen\u00fd disk",
+ "disk_used": "Obsazeno na disku",
"docker_version": "Verze Dockeru",
"healthy": "V po\u0159\u00e1dku",
"host_os": "Hostitelsk\u00fd opera\u010dn\u00ed syst\u00e9m",
diff --git a/homeassistant/components/hassio/translations/es.json b/homeassistant/components/hassio/translations/es.json
index 4c8223f606b..5faf32e515f 100644
--- a/homeassistant/components/hassio/translations/es.json
+++ b/homeassistant/components/hassio/translations/es.json
@@ -5,12 +5,13 @@
"disk_total": "Disco total",
"disk_used": "Disco usado",
"docker_version": "Versi\u00f3n de Docker",
- "host_os": "Sistema operativo host",
+ "healthy": "Saludable",
+ "host_os": "Sistema operativo del Host",
"installed_addons": "Complementos instalados",
"supervisor_api": "API del Supervisor",
"supervisor_version": "Versi\u00f3n del Supervisor",
"supported": "Soportado",
- "update_channel": "Actualizar canal",
+ "update_channel": "Canal de actualizaci\u00f3n",
"version_api": "Versi\u00f3n del API"
}
},
diff --git a/homeassistant/components/hassio/translations/hu.json b/homeassistant/components/hassio/translations/hu.json
index 981cb51c83a..4119802eb77 100644
--- a/homeassistant/components/hassio/translations/hu.json
+++ b/homeassistant/components/hassio/translations/hu.json
@@ -1,3 +1,17 @@
{
+ "system_health": {
+ "info": {
+ "disk_total": "\u00d6sszes hely",
+ "disk_used": "Felhaszn\u00e1lt hely",
+ "docker_version": "Docker verzi\u00f3",
+ "host_os": "Gazdag\u00e9p oper\u00e1ci\u00f3s rendszer",
+ "installed_addons": "Telep\u00edtett kieg\u00e9sz\u00edt\u0151k",
+ "supervisor_api": "Adminisztr\u00e1tor API",
+ "supervisor_version": "Adminisztr\u00e1tor verzi\u00f3",
+ "supported": "T\u00e1mogatott",
+ "update_channel": "Friss\u00edt\u00e9si csatorna",
+ "version_api": "API verzi\u00f3"
+ }
+ },
"title": "Hass.io"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/it.json b/homeassistant/components/hassio/translations/it.json
index 981cb51c83a..937b6099bd9 100644
--- a/homeassistant/components/hassio/translations/it.json
+++ b/homeassistant/components/hassio/translations/it.json
@@ -1,3 +1,19 @@
{
+ "system_health": {
+ "info": {
+ "board": "Scheda di base",
+ "disk_total": "Disco totale",
+ "disk_used": "Disco utilizzato",
+ "docker_version": "Versione Docker",
+ "healthy": "Sano",
+ "host_os": "Sistema Operativo Host",
+ "installed_addons": "Componenti aggiuntivi installati",
+ "supervisor_api": "API Supervisore",
+ "supervisor_version": "Versione Supervisore",
+ "supported": "Supportato",
+ "update_channel": "Canale di aggiornamento",
+ "version_api": "Versione API"
+ }
+ },
"title": "Hass.io"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/ka.json b/homeassistant/components/hassio/translations/ka.json
new file mode 100644
index 00000000000..7a224350df7
--- /dev/null
+++ b/homeassistant/components/hassio/translations/ka.json
@@ -0,0 +1,18 @@
+{
+ "system_health": {
+ "info": {
+ "board": "\u10d3\u10d0\u10e4\u10d0",
+ "disk_total": "\u10d3\u10d8\u10e1\u10d9\u10d6\u10d4 \u10e1\u10e3\u10da",
+ "disk_used": "\u10d3\u10d8\u10e1\u10d9\u10d6\u10d4 \u10d2\u10d0\u10db\u10dd\u10e7\u10d4\u10dc\u10d4\u10d1\u10e3\u10da\u10d8",
+ "docker_version": "Docker-\u10d8\u10e1 \u10d5\u10d4\u10e0\u10e1\u10d8\u10d0",
+ "healthy": "\u10d2\u10d0\u10db\u10d0\u10e0\u10d7\u10e3\u10da\u10d8\u10d0",
+ "host_os": "\u10f0\u10dd\u10e1\u10e2 \u10dd\u10de\u10d4\u10e0\u10d0\u10ea\u10d8\u10e3\u10da\u10d8 \u10e1\u10d8\u10e1\u10e2\u10d4\u10db\u10d0",
+ "installed_addons": "\u10d3\u10d0\u10d8\u10dc\u10e1\u10e2\u10d0\u10da\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8 \u10d3\u10d0\u10db\u10d0\u10e2\u10d4\u10d1\u10d4\u10d1\u10d8",
+ "supervisor_api": "Supervisor API",
+ "supervisor_version": "Supervisor \u10d5\u10d4\u10e0\u10e1\u10d8\u10d0",
+ "supported": "\u10db\u10ee\u10d0\u10e0\u10d3\u10d0\u10ed\u10d4\u10e0\u10d0",
+ "update_channel": "\u10d2\u10d0\u10dc\u10d0\u10ee\u10da\u10d4\u10d1\u10d4\u10d1\u10d8\u10e1 \u10d0\u10e0\u10ee\u10d8",
+ "version_api": "API-\u10e1 \u10d5\u10d4\u10e0\u10e1\u10d8\u10d0"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/lb.json b/homeassistant/components/hassio/translations/lb.json
index 981cb51c83a..54aae5a2c59 100644
--- a/homeassistant/components/hassio/translations/lb.json
+++ b/homeassistant/components/hassio/translations/lb.json
@@ -1,3 +1,19 @@
{
+ "system_health": {
+ "info": {
+ "board": "Board",
+ "disk_total": "Disk Toal",
+ "disk_used": "Disk benotzt",
+ "docker_version": "Docker Versioun",
+ "healthy": "Gesond",
+ "host_os": "Host Betribssystem",
+ "installed_addons": "Install\u00e9iert Add-ons",
+ "supervisor_api": "Supervisor API",
+ "supervisor_version": "Supervisor Versioun",
+ "supported": "\u00cbnnerst\u00ebtzt",
+ "update_channel": "Aktualis\u00e9ierungs Kanal",
+ "version_api": "API Versioun"
+ }
+ },
"title": "Hass.io"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/no.json b/homeassistant/components/hassio/translations/no.json
index 2fb04f5156e..e652f76a12c 100644
--- a/homeassistant/components/hassio/translations/no.json
+++ b/homeassistant/components/hassio/translations/no.json
@@ -8,7 +8,7 @@
"healthy": "Sunn",
"host_os": "Vertsoperativsystem",
"installed_addons": "Installerte tillegg",
- "supervisor_api": "API for Supervisor",
+ "supervisor_api": "Supervisor API",
"supervisor_version": "Supervisor versjon",
"supported": "St\u00f8ttet",
"update_channel": "Oppdater kanal",
diff --git a/homeassistant/components/hassio/translations/pt.json b/homeassistant/components/hassio/translations/pt.json
index 981cb51c83a..973601e744a 100644
--- a/homeassistant/components/hassio/translations/pt.json
+++ b/homeassistant/components/hassio/translations/pt.json
@@ -1,3 +1,14 @@
{
+ "system_health": {
+ "info": {
+ "disk_total": "Disco Total",
+ "disk_used": "Disco Usado",
+ "docker_version": "Vers\u00e3o Docker",
+ "host_os": "Sistema operativo anfitri\u00e3o",
+ "supervisor_api": "API do Supervisor",
+ "supervisor_version": "Vers\u00e3o do Supervisor",
+ "supported": "Suportado"
+ }
+ },
"title": "Hass.io"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/ru.json b/homeassistant/components/hassio/translations/ru.json
index 052e1ec21dd..4f9b16621fc 100644
--- a/homeassistant/components/hassio/translations/ru.json
+++ b/homeassistant/components/hassio/translations/ru.json
@@ -2,13 +2,16 @@
"system_health": {
"info": {
"board": "\u041f\u043b\u0430\u0442\u0430",
+ "disk_total": "\u041f\u0430\u043c\u044f\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e",
+ "disk_used": "\u041f\u0430\u043c\u044f\u0442\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u043e",
"docker_version": "\u0412\u0435\u0440\u0441\u0438\u044f Docker",
+ "healthy": "\u0418\u0441\u043f\u0440\u0430\u0432\u043d\u043e",
"host_os": "\u041e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u0445\u043e\u0441\u0442\u0430",
"installed_addons": "\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044b\u0435 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f",
"supervisor_api": "Supervisor API",
"supervisor_version": "\u0412\u0435\u0440\u0441\u0438\u044f Supervisor",
- "supported": "\u041f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f",
- "update_channel": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u043a\u0430\u043d\u0430\u043b",
+ "supported": "\u041f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0430",
+ "update_channel": "\u041a\u0430\u043d\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0439",
"version_api": "\u0412\u0435\u0440\u0441\u0438\u044f API"
}
},
diff --git a/homeassistant/components/hassio/translations/sl.json b/homeassistant/components/hassio/translations/sl.json
index 981cb51c83a..eb2f5f7ca8b 100644
--- a/homeassistant/components/hassio/translations/sl.json
+++ b/homeassistant/components/hassio/translations/sl.json
@@ -1,3 +1,18 @@
{
+ "system_health": {
+ "info": {
+ "disk_total": "Skupaj na disku",
+ "disk_used": "Uporabljen disk",
+ "docker_version": "Razli\u010dica Dockerja",
+ "healthy": "Zdravo",
+ "host_os": "Gostiteljski operacijski sistem",
+ "installed_addons": "Name\u0161\u010deni dodatki",
+ "supervisor_api": "API nadzornika",
+ "supervisor_version": "Razli\u010dica nadzornika",
+ "supported": "Podprto",
+ "update_channel": "Posodobi kanal",
+ "version_api": "API razli\u010dica"
+ }
+ },
"title": "Hass.io"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/zh-Hans.json b/homeassistant/components/hassio/translations/zh-Hans.json
index 981cb51c83a..95b4a8a8a61 100644
--- a/homeassistant/components/hassio/translations/zh-Hans.json
+++ b/homeassistant/components/hassio/translations/zh-Hans.json
@@ -1,3 +1,19 @@
{
+ "system_health": {
+ "info": {
+ "board": "\u677f\u5b50",
+ "disk_total": "\u78c1\u76d8\u5927\u5c0f",
+ "disk_used": "\u78c1\u76d8\u5df2\u7528",
+ "docker_version": "Docker \u7248\u672c",
+ "healthy": "\u5065\u5eb7",
+ "host_os": "\u5bbf\u4e3b\u64cd\u4f5c\u7cfb\u7edf",
+ "installed_addons": "\u5df2\u5b89\u88c5\u7684\u52a0\u8f7d\u9879",
+ "supervisor_api": "Supervisor API",
+ "supervisor_version": "Supervisor \u7248\u672c",
+ "supported": "\u53d7\u652f\u6301",
+ "update_channel": "\u66f4\u65b0\u901a\u9053",
+ "version_api": "API\u7248\u672c"
+ }
+ },
"title": "Hass.io"
}
\ No newline at end of file
diff --git a/homeassistant/components/hassio/translations/zh-Hant.json b/homeassistant/components/hassio/translations/zh-Hant.json
index 981cb51c83a..574b82358d6 100644
--- a/homeassistant/components/hassio/translations/zh-Hant.json
+++ b/homeassistant/components/hassio/translations/zh-Hant.json
@@ -1,3 +1,19 @@
{
+ "system_health": {
+ "info": {
+ "board": "\u677f",
+ "disk_total": "\u7e3d\u78c1\u789f\u7a7a\u9593",
+ "disk_used": "\u5df2\u4f7f\u7528\u7a7a\u9593",
+ "docker_version": "Docker \u7248\u672c",
+ "healthy": "\u5065\u5eb7\u5ea6",
+ "host_os": "\u4e3b\u6a5f\u4f5c\u696d\u7cfb\u7d71",
+ "installed_addons": "\u5df2\u5b89\u88dd Add-on",
+ "supervisor_api": "Supervisor API",
+ "supervisor_version": "Supervisor \u7248\u672c",
+ "supported": "\u652f\u63f4",
+ "update_channel": "\u66f4\u65b0\u983b\u9053",
+ "version_api": "\u7248\u672c API"
+ }
+ },
"title": "Hass.io"
}
\ No newline at end of file
diff --git a/homeassistant/components/heos/manifest.json b/homeassistant/components/heos/manifest.json
index a6da3623da7..6505a564560 100644
--- a/homeassistant/components/heos/manifest.json
+++ b/homeassistant/components/heos/manifest.json
@@ -3,7 +3,7 @@
"name": "Denon HEOS",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/heos",
- "requirements": ["pyheos==0.6.0"],
+ "requirements": ["pyheos==0.7.2"],
"ssdp": [
{
"st": "urn:schemas-denon-com:device:ACT-Denon:1"
diff --git a/homeassistant/components/heos/translations/hu.json b/homeassistant/components/heos/translations/hu.json
index cbf055e2fba..cf688d6fdeb 100644
--- a/homeassistant/components/heos/translations/hu.json
+++ b/homeassistant/components/heos/translations/hu.json
@@ -1,5 +1,8 @@
{
"config": {
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/heos/translations/ka.json b/homeassistant/components/heos/translations/ka.json
new file mode 100644
index 00000000000..a73d02d47eb
--- /dev/null
+++ b/homeassistant/components/heos/translations/ka.json
@@ -0,0 +1,10 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u10e3\u10d9\u10d5\u10d4 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0. \u10e8\u10d4\u10e1\u10d0\u10eb\u10da\u10d4\u10d1\u10d4\u10da\u10d8\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0."
+ },
+ "error": {
+ "cannot_connect": "\u10db\u10d0\u10e0\u10ea\u10ee\u10d8 \u10e8\u10d4\u10d4\u10e0\u10d7\u10d4\u10d1\u10d8\u10e1\u10d0\u10e1"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py
index 8f4a89e3e37..6778e893f6f 100644
--- a/homeassistant/components/history_stats/sensor.py
+++ b/homeassistant/components/history_stats/sensor.py
@@ -62,7 +62,7 @@ PLATFORM_SCHEMA = vol.All(
PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_ENTITY_ID): cv.entity_id,
- vol.Required(CONF_STATE): cv.string,
+ vol.Required(CONF_STATE): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_START): cv.template,
vol.Optional(CONF_END): cv.template,
vol.Optional(CONF_DURATION): cv.time_period,
@@ -77,11 +77,10 @@ PLATFORM_SCHEMA = vol.All(
# noinspection PyUnusedLocal
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the History Stats sensor."""
-
setup_reload_service(hass, DOMAIN, PLATFORMS)
entity_id = config.get(CONF_ENTITY_ID)
- entity_state = config.get(CONF_STATE)
+ entity_states = config.get(CONF_STATE)
start = config.get(CONF_START)
end = config.get(CONF_END)
duration = config.get(CONF_DURATION)
@@ -95,7 +94,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(
[
HistoryStatsSensor(
- hass, entity_id, entity_state, start, end, duration, sensor_type, name
+ hass, entity_id, entity_states, start, end, duration, sensor_type, name
)
]
)
@@ -107,11 +106,11 @@ class HistoryStatsSensor(Entity):
"""Representation of a HistoryStats sensor."""
def __init__(
- self, hass, entity_id, entity_state, start, end, duration, sensor_type, name
+ self, hass, entity_id, entity_states, start, end, duration, sensor_type, name
):
"""Initialize the HistoryStats sensor."""
self._entity_id = entity_id
- self._entity_state = entity_state
+ self._entity_states = entity_states
self._duration = duration
self._start = start
self._end = end
@@ -230,14 +229,14 @@ class HistoryStatsSensor(Entity):
# Get the first state
last_state = history.get_state(self.hass, start, self._entity_id)
- last_state = last_state is not None and last_state == self._entity_state
+ last_state = last_state is not None and last_state in self._entity_states
last_time = start_timestamp
elapsed = 0
count = 0
# Make calculations
for item in history_list.get(self._entity_id):
- current_state = item.state == self._entity_state
+ current_state = item.state in self._entity_states
current_time = item.last_changed.timestamp()
if last_state:
diff --git a/homeassistant/components/home_connect/translations/hu.json b/homeassistant/components/home_connect/translations/hu.json
index 31804cfc421..f02fb97b9df 100644
--- a/homeassistant/components/home_connect/translations/hu.json
+++ b/homeassistant/components/home_connect/translations/hu.json
@@ -1,5 +1,8 @@
{
"config": {
+ "create_entry": {
+ "default": "Sikeres autentik\u00e1ci\u00f3"
+ },
"step": {
"pick_implementation": {
"title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert"
diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py
index f1b8d4e87d6..c2ee40b7d43 100644
--- a/homeassistant/components/homeassistant/__init__.py
+++ b/homeassistant/components/homeassistant/__init__.py
@@ -21,7 +21,7 @@ from homeassistant.const import (
import homeassistant.core as ha
from homeassistant.exceptions import HomeAssistantError, Unauthorized, UnknownUser
from homeassistant.helpers import config_validation as cv
-from homeassistant.helpers.service import async_extract_entity_ids
+from homeassistant.helpers.service import async_extract_referenced_entity_ids
_LOGGER = logging.getLogger(__name__)
DOMAIN = ha.DOMAIN
@@ -37,39 +37,37 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> bool:
async def async_handle_turn_service(service):
"""Handle calls to homeassistant.turn_on/off."""
- entity_ids = await async_extract_entity_ids(hass, service)
+ referenced = await async_extract_referenced_entity_ids(hass, service)
+ all_referenced = referenced.referenced | referenced.indirectly_referenced
# Generic turn on/off method requires entity id
- if not entity_ids:
+ if not all_referenced:
_LOGGER.error(
- "homeassistant/%s cannot be called without entity_id", service.service
+ "homeassistant.%s cannot be called without a target", service.service
)
return
# Group entity_ids by domain. groupby requires sorted data.
by_domain = it.groupby(
- sorted(entity_ids), lambda item: ha.split_entity_id(item)[0]
+ sorted(all_referenced), lambda item: ha.split_entity_id(item)[0]
)
tasks = []
+ unsupported_entities = set()
for domain, ent_ids in by_domain:
# This leads to endless loop.
if domain == DOMAIN:
_LOGGER.warning(
- "Called service homeassistant.%s with invalid entity IDs %s",
+ "Called service homeassistant.%s with invalid entities %s",
service.service,
", ".join(ent_ids),
)
continue
- # We want to block for all calls and only return when all calls
- # have been processed. If a service does not exist it causes a 10
- # second delay while we're blocking waiting for a response.
- # But services can be registered on other HA instances that are
- # listening to the bus too. So as an in between solution, we'll
- # block only if the service is defined in the current HA instance.
- blocking = hass.services.has_service(domain, service.service)
+ if not hass.services.has_service(domain, service.service):
+ unsupported_entities.update(set(ent_ids) & referenced.referenced)
+ continue
# Create a new dict for this call
data = dict(service.data)
@@ -79,10 +77,21 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> bool:
tasks.append(
hass.services.async_call(
- domain, service.service, data, blocking, context=service.context
+ domain,
+ service.service,
+ data,
+ blocking=True,
+ context=service.context,
)
)
+ if unsupported_entities:
+ _LOGGER.warning(
+ "The service homeassistant.%s does not support entities %s",
+ service.service,
+ ", ".join(sorted(unsupported_entities)),
+ )
+
if tasks:
await asyncio.gather(*tasks)
diff --git a/homeassistant/components/homeassistant/translations/ca.json b/homeassistant/components/homeassistant/translations/ca.json
index f02939b201e..8f9931c6820 100644
--- a/homeassistant/components/homeassistant/translations/ca.json
+++ b/homeassistant/components/homeassistant/translations/ca.json
@@ -6,6 +6,7 @@
"dev": "Desenvolupament",
"docker": "Docker",
"docker_version": "Docker",
+ "hassio": "Supervisor",
"host_os": "Home Assistant OS",
"installation_type": "Tipus d'instal\u00b7laci\u00f3",
"os_name": "Fam\u00edlia del sistema operatiu",
diff --git a/homeassistant/components/homeassistant/translations/cs.json b/homeassistant/components/homeassistant/translations/cs.json
index 46bf2c56b4b..3b6414b58ad 100644
--- a/homeassistant/components/homeassistant/translations/cs.json
+++ b/homeassistant/components/homeassistant/translations/cs.json
@@ -6,6 +6,7 @@
"dev": "V\u00fdvoj",
"docker": "Docker",
"docker_version": "Docker",
+ "hassio": "Supervisor",
"host_os": "Home Assistant OS",
"installation_type": "Typ instalace",
"os_name": "Rodina opera\u010dn\u00edch syst\u00e9m\u016f",
diff --git a/homeassistant/components/homeassistant/translations/en.json b/homeassistant/components/homeassistant/translations/en.json
index ee029772d21..22538ad6536 100644
--- a/homeassistant/components/homeassistant/translations/en.json
+++ b/homeassistant/components/homeassistant/translations/en.json
@@ -18,4 +18,4 @@
"virtualenv": "Virtual Environment"
}
}
-}
+}
\ No newline at end of file
diff --git a/homeassistant/components/homeassistant/translations/et.json b/homeassistant/components/homeassistant/translations/et.json
index 7bdecd0178f..22e3ab1e00d 100644
--- a/homeassistant/components/homeassistant/translations/et.json
+++ b/homeassistant/components/homeassistant/translations/et.json
@@ -6,6 +6,7 @@
"dev": "Arendus",
"docker": "Docker",
"docker_version": "Docker",
+ "hassio": "Haldur",
"host_os": "Home Assistant OS",
"installation_type": "Paigalduse t\u00fc\u00fcp",
"os_name": "Operatsioonis\u00fcsteemi j\u00e4rk",
diff --git a/homeassistant/components/homeassistant/translations/hu.json b/homeassistant/components/homeassistant/translations/hu.json
new file mode 100644
index 00000000000..e202a747ac7
--- /dev/null
+++ b/homeassistant/components/homeassistant/translations/hu.json
@@ -0,0 +1,21 @@
+{
+ "system_health": {
+ "info": {
+ "arch": "Processzor architekt\u00fara",
+ "chassis": "Kivitel",
+ "dev": "Fejleszt\u00e9s",
+ "docker": "Docker",
+ "docker_version": "Docker",
+ "hassio": "Adminisztr\u00e1tor",
+ "host_os": "Home Assistant OS",
+ "installation_type": "Telep\u00edt\u00e9s t\u00edpusa",
+ "os_name": "Oper\u00e1ci\u00f3s rendszer csal\u00e1d",
+ "os_version": "Oper\u00e1ci\u00f3s rendszer verzi\u00f3ja",
+ "python_version": "Python verzi\u00f3",
+ "supervisor": "Adminisztr\u00e1tor",
+ "timezone": "Id\u0151z\u00f3na",
+ "version": "Verzi\u00f3",
+ "virtualenv": "Virtu\u00e1lis k\u00f6rnyezet"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/homeassistant/translations/it.json b/homeassistant/components/homeassistant/translations/it.json
index 80f7b39d210..f3168807715 100644
--- a/homeassistant/components/homeassistant/translations/it.json
+++ b/homeassistant/components/homeassistant/translations/it.json
@@ -6,9 +6,10 @@
"dev": "Sviluppo",
"docker": "Docker",
"docker_version": "Docker",
+ "hassio": "Supervisore",
"host_os": "Sistema Operativo di Home Assistant",
"installation_type": "Tipo di installazione",
- "os_name": "Nome del Sistema Operativo",
+ "os_name": "Famiglia del Sistema Operativo",
"os_version": "Versione del Sistema Operativo",
"python_version": "Versione Python",
"supervisor": "Supervisore",
diff --git a/homeassistant/components/homeassistant/translations/ka.json b/homeassistant/components/homeassistant/translations/ka.json
new file mode 100644
index 00000000000..4b5dec2fd30
--- /dev/null
+++ b/homeassistant/components/homeassistant/translations/ka.json
@@ -0,0 +1,21 @@
+{
+ "system_health": {
+ "info": {
+ "arch": "\u10de\u10e0\u10dd\u10ea\u10d4\u10e1\u10dd\u10e0\u10d8\u10e1 \u10d0\u10e0\u10e5\u10d8\u10e2\u10d4\u10e5\u10e2\u10e3\u10e0\u10d0",
+ "chassis": "\u10e8\u10d0\u10e1\u10d8",
+ "dev": "\u10e8\u10d4\u10db\u10e3\u10e8\u10d0\u10d5\u10d4\u10d1\u10d0",
+ "docker": "Docker",
+ "docker_version": "Docker",
+ "hassio": "Supervisor",
+ "host_os": "Home Assistant \u10dd\u10de\u10d4\u10e0\u10d0\u10ea\u10d8\u10e3\u10da\u10d8",
+ "installation_type": "\u10d8\u10dc\u10e1\u10e2\u10d0\u10da\u10d0\u10ea\u10d8\u10d8\u10e1 \u10e2\u10d8\u10de\u10d8",
+ "os_name": "\u10dd\u10de\u10d4\u10e0\u10d0\u10ea\u10d8\u10e3\u10da\u10d8 \u10e1\u10d8\u10e1\u10e2\u10d4\u10db\u10d8\u10e1 \u10dd\u10ef\u10d0\u10ee\u10d8",
+ "os_version": "\u10dd\u10de\u10d4\u10e0\u10d0\u10ea\u10d8\u10e3\u10da\u10d8 \u10e1\u10d8\u10e1\u10e2\u10d4\u10db\u10d8\u10e1 \u10d5\u10d4\u10e0\u10e1\u10d8\u10d0",
+ "python_version": "Python-\u10d8\u10e1 \u10d5\u10d4\u10e0\u10e1\u10d8\u10d0",
+ "supervisor": "Supervisor",
+ "timezone": "\u1c93\u10e0\u10dd\u10d8\u10e1 \u10e1\u10d0\u10e0\u10e2\u10e7\u10d4\u10da\u10d8",
+ "version": "\u10d5\u10d4\u10e0\u10e1\u10d8\u10d0",
+ "virtualenv": "\u10d5\u10d8\u10e0\u10e2\u10e3\u10d0\u10da\u10e3\u10e0\u10d8 \u10d2\u10d0\u10e0\u10d4\u10db\u10dd"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/homeassistant/translations/lb.json b/homeassistant/components/homeassistant/translations/lb.json
new file mode 100644
index 00000000000..07cfe8c4c83
--- /dev/null
+++ b/homeassistant/components/homeassistant/translations/lb.json
@@ -0,0 +1,21 @@
+{
+ "system_health": {
+ "info": {
+ "arch": "CPU Architektur",
+ "chassis": "Chassis",
+ "dev": "Entw\u00e9cklung",
+ "docker": "Docker",
+ "docker_version": "Docker",
+ "hassio": "Supervisor API",
+ "host_os": "Home Assistant OS",
+ "installation_type": "Typ vun Installatioun",
+ "os_name": "Betribssystem Famille",
+ "os_version": "Betribssystem Versioun",
+ "python_version": "Python Versioun",
+ "supervisor": "Supervisor",
+ "timezone": "Z\u00e4itzon",
+ "version": "Versioun",
+ "virtualenv": "Virtuellen Environnement"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/homeassistant/translations/no.json b/homeassistant/components/homeassistant/translations/no.json
index 72bba59116c..3cf39e2cf7d 100644
--- a/homeassistant/components/homeassistant/translations/no.json
+++ b/homeassistant/components/homeassistant/translations/no.json
@@ -6,6 +6,7 @@
"dev": "Utvikling",
"docker": "",
"docker_version": "",
+ "hassio": "Supervisor",
"host_os": "",
"installation_type": "Installasjonstype",
"os_name": "Familie for operativsystem",
diff --git a/homeassistant/components/homeassistant/translations/pl.json b/homeassistant/components/homeassistant/translations/pl.json
index e65756cfc83..44d35cb582c 100644
--- a/homeassistant/components/homeassistant/translations/pl.json
+++ b/homeassistant/components/homeassistant/translations/pl.json
@@ -3,9 +3,10 @@
"info": {
"arch": "Architektura procesora",
"chassis": "Wersja komputera",
- "dev": "Wersja rozwojowa",
+ "dev": "Wersja deweloperska",
"docker": "Docker",
"docker_version": "Wersja Dockera",
+ "hassio": "Supervisor",
"host_os": "System operacyjny HA",
"installation_type": "Typ instalacji",
"os_name": "Rodzina systemu operacyjnego",
diff --git a/homeassistant/components/homeassistant/translations/pt.json b/homeassistant/components/homeassistant/translations/pt.json
index 1b30839faff..7bf340567f5 100644
--- a/homeassistant/components/homeassistant/translations/pt.json
+++ b/homeassistant/components/homeassistant/translations/pt.json
@@ -5,6 +5,7 @@
"dev": "Desenvolvimento",
"docker": "Docker",
"docker_version": "Docker",
+ "hassio": "Supervisor",
"host_os": "Sistema Operativo do Home Assistant",
"installation_type": "Tipo de Instala\u00e7\u00e3o",
"os_name": "Nome do Sistema Operativo",
diff --git a/homeassistant/components/homeassistant/translations/ru.json b/homeassistant/components/homeassistant/translations/ru.json
index 76af5da7d13..651400c5fe5 100644
--- a/homeassistant/components/homeassistant/translations/ru.json
+++ b/homeassistant/components/homeassistant/translations/ru.json
@@ -6,6 +6,7 @@
"dev": "\u0421\u0440\u0435\u0434\u0430 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438",
"docker": "Docker",
"docker_version": "Docker",
+ "hassio": "Supervisor",
"host_os": "Home Assistant OS",
"installation_type": "\u0422\u0438\u043f \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438",
"os_name": "\u0421\u0435\u043c\u0435\u0439\u0441\u0442\u0432\u043e \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0445 \u0441\u0438\u0441\u0442\u0435\u043c",
diff --git a/homeassistant/components/homeassistant/translations/sl.json b/homeassistant/components/homeassistant/translations/sl.json
new file mode 100644
index 00000000000..64e972f5a44
--- /dev/null
+++ b/homeassistant/components/homeassistant/translations/sl.json
@@ -0,0 +1,16 @@
+{
+ "system_health": {
+ "info": {
+ "arch": "Arhitektura CPU",
+ "dev": "Razvoj",
+ "docker": "Docker",
+ "docker_version": "Docker",
+ "hassio": "Nadzornik",
+ "installation_type": "Vrsta namestitve",
+ "os_version": "Razli\u010dica operacijskega sistema",
+ "python_version": "Razli\u010dica Pythona",
+ "timezone": "\u010casovni pas",
+ "version": "Razli\u010dica"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/homeassistant/translations/zh-Hans.json b/homeassistant/components/homeassistant/translations/zh-Hans.json
new file mode 100644
index 00000000000..6d6e1f2eed8
--- /dev/null
+++ b/homeassistant/components/homeassistant/translations/zh-Hans.json
@@ -0,0 +1,21 @@
+{
+ "system_health": {
+ "info": {
+ "arch": "CPU \u67b6\u6784",
+ "chassis": "\u673a\u7bb1",
+ "dev": "\u5f00\u53d1\u7248",
+ "docker": "Docker",
+ "docker_version": "Docker",
+ "hassio": "Supervisor",
+ "host_os": "Home Assistant OS",
+ "installation_type": "\u5b89\u88c5\u7c7b\u578b",
+ "os_name": "\u64cd\u4f5c\u7cfb\u7edf\u7cfb\u5217",
+ "os_version": "\u64cd\u4f5c\u7cfb\u7edf\u7248\u672c",
+ "python_version": "Python \u7248\u672c",
+ "supervisor": "Supervisor",
+ "timezone": "\u65f6\u533a",
+ "version": "\u7248\u672c",
+ "virtualenv": "\u865a\u62df\u73af\u5883"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/homeassistant/translations/zh-Hant.json b/homeassistant/components/homeassistant/translations/zh-Hant.json
index 2bc13851390..eba7a8034db 100644
--- a/homeassistant/components/homeassistant/translations/zh-Hant.json
+++ b/homeassistant/components/homeassistant/translations/zh-Hant.json
@@ -6,9 +6,10 @@
"dev": "\u958b\u767c\u7248",
"docker": "Docker",
"docker_version": "Docker",
+ "hassio": "Supervisor",
"host_os": "Home Assistant OS",
"installation_type": "\u5b89\u88dd\u985e\u578b",
- "os_name": "\u4f5c\u696d\u7cfb\u7d71\u540d\u7a31",
+ "os_name": "\u4f5c\u696d\u7cfb\u7d71\u5bb6\u65cf",
"os_version": "\u4f5c\u696d\u7cfb\u7d71\u7248\u672c",
"python_version": "Python \u7248\u672c",
"supervisor": "Supervisor",
diff --git a/homeassistant/components/homeassistant/triggers/event.py b/homeassistant/components/homeassistant/triggers/event.py
index c5baf6ca4b2..b7ab081d266 100644
--- a/homeassistant/components/homeassistant/triggers/event.py
+++ b/homeassistant/components/homeassistant/triggers/event.py
@@ -14,7 +14,7 @@ CONF_EVENT_CONTEXT = "context"
TRIGGER_SCHEMA = vol.Schema(
{
vol.Required(CONF_PLATFORM): "event",
- vol.Required(CONF_EVENT_TYPE): cv.string,
+ vol.Required(CONF_EVENT_TYPE): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_EVENT_DATA): dict,
vol.Optional(CONF_EVENT_CONTEXT): dict,
}
@@ -32,7 +32,8 @@ async def async_attach_trigger(
hass, config, action, automation_info, *, platform_type="event"
):
"""Listen for events based on configuration."""
- event_type = config.get(CONF_EVENT_TYPE)
+ event_types = config.get(CONF_EVENT_TYPE)
+ removes = []
event_data_schema = None
if config.get(CONF_EVENT_DATA):
@@ -82,4 +83,14 @@ async def async_attach_trigger(
event.context,
)
- return hass.bus.async_listen(event_type, handle_event)
+ removes = [
+ hass.bus.async_listen(event_type, handle_event) for event_type in event_types
+ ]
+
+ @callback
+ def remove_listen_events():
+ """Remove event listeners."""
+ for remove in removes:
+ remove()
+
+ return remove_listen_events
diff --git a/homeassistant/components/homeassistant/triggers/numeric_state.py b/homeassistant/components/homeassistant/triggers/numeric_state.py
index a6e3b33ae97..25b9a4417dc 100644
--- a/homeassistant/components/homeassistant/triggers/numeric_state.py
+++ b/homeassistant/components/homeassistant/triggers/numeric_state.py
@@ -63,7 +63,7 @@ async def async_attach_trigger(
hass, config, action, automation_info, *, platform_type="numeric_state"
) -> CALLBACK_TYPE:
"""Listen for state changes based on configuration."""
- entity_id = config.get(CONF_ENTITY_ID)
+ entity_ids = config.get(CONF_ENTITY_ID)
below = config.get(CONF_BELOW)
above = config.get(CONF_ABOVE)
time_delta = config.get(CONF_FOR)
@@ -78,29 +78,32 @@ async def async_attach_trigger(
if value_template is not None:
value_template.hass = hass
- @callback
- def check_numeric_state(entity, from_s, to_s):
- """Return True if criteria are now met."""
- if to_s is None:
- return False
-
- variables = {
+ def variables(entity_id):
+ """Return a dict with trigger variables."""
+ return {
"trigger": {
"platform": "numeric_state",
- "entity_id": entity,
+ "entity_id": entity_id,
"below": below,
"above": above,
"attribute": attribute,
}
}
+
+ @callback
+ def check_numeric_state(entity_id, from_s, to_s):
+ """Return True if criteria are now met."""
+ if to_s is None:
+ return False
+
return condition.async_numeric_state(
- hass, to_s, below, above, value_template, variables, attribute
+ hass, to_s, below, above, value_template, variables(entity_id), attribute
)
@callback
def state_automation_listener(event):
"""Listen for state changes and calls action."""
- entity = event.data.get("entity_id")
+ entity_id = event.data.get("entity_id")
from_s = event.data.get("old_state")
to_s = event.data.get("new_state")
@@ -112,38 +115,29 @@ async def async_attach_trigger(
{
"trigger": {
"platform": platform_type,
- "entity_id": entity,
+ "entity_id": entity_id,
"below": below,
"above": above,
"from_state": from_s,
"to_state": to_s,
- "for": time_delta if not time_delta else period[entity],
- "description": f"numeric state of {entity}",
+ "for": time_delta if not time_delta else period[entity_id],
+ "description": f"numeric state of {entity_id}",
}
},
to_s.context,
)
- matching = check_numeric_state(entity, from_s, to_s)
+ matching = check_numeric_state(entity_id, from_s, to_s)
if not matching:
- entities_triggered.discard(entity)
- elif entity not in entities_triggered:
- entities_triggered.add(entity)
+ entities_triggered.discard(entity_id)
+ elif entity_id not in entities_triggered:
+ entities_triggered.add(entity_id)
if time_delta:
- variables = {
- "trigger": {
- "platform": "numeric_state",
- "entity_id": entity,
- "below": below,
- "above": above,
- }
- }
-
try:
- period[entity] = cv.positive_time_period(
- template.render_complex(time_delta, variables)
+ period[entity_id] = cv.positive_time_period(
+ template.render_complex(time_delta, variables(entity_id))
)
except (exceptions.TemplateError, vol.Invalid) as ex:
_LOGGER.error(
@@ -151,20 +145,20 @@ async def async_attach_trigger(
automation_info["name"],
ex,
)
- entities_triggered.discard(entity)
+ entities_triggered.discard(entity_id)
return
- unsub_track_same[entity] = async_track_same_state(
+ unsub_track_same[entity_id] = async_track_same_state(
hass,
- period[entity],
+ period[entity_id],
call_action,
- entity_ids=entity,
+ entity_ids=entity_id,
async_check_same_func=check_numeric_state,
)
else:
call_action()
- unsub = async_track_state_change_event(hass, entity_id, state_automation_listener)
+ unsub = async_track_state_change_event(hass, entity_ids, state_automation_listener)
@callback
def async_remove():
diff --git a/homeassistant/components/homekit/translations/hu.json b/homeassistant/components/homekit/translations/hu.json
new file mode 100644
index 00000000000..4cf1f44c439
--- /dev/null
+++ b/homeassistant/components/homekit/translations/hu.json
@@ -0,0 +1,17 @@
+{
+ "options": {
+ "step": {
+ "include_exclude": {
+ "data": {
+ "entities": "Entit\u00e1sok",
+ "mode": "M\u00f3d"
+ }
+ },
+ "init": {
+ "data": {
+ "mode": "M\u00f3d"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/homekit/translations/ka.json b/homeassistant/components/homekit/translations/ka.json
new file mode 100644
index 00000000000..97787f722fc
--- /dev/null
+++ b/homeassistant/components/homekit/translations/ka.json
@@ -0,0 +1,19 @@
+{
+ "options": {
+ "step": {
+ "include_exclude": {
+ "data": {
+ "entities": "\u10dd\u10d1\u10d8\u10d4\u10e5\u10e2\u10d4\u10d1\u10d8",
+ "mode": "\u10e0\u10d4\u10df\u10db\u10d8"
+ },
+ "description": "\u10e8\u10d4\u10d0\u10e0\u10e9\u10d8\u10d4\u10d7 \u10d2\u10d0\u10db\u10dd\u10e1\u10d0\u10d5\u10da\u10d4\u10dc\u10d8 \u10dd\u10d1\u10d8\u10d4\u10e5\u10e2\u10d4\u10d1\u10d8. \u10d0\u10e5\u10e1\u10d4\u10e1\u10e3\u10d0\u10e0\u10d4\u10d1\u10d8\u10e1 \u10e0\u10d4\u10df\u10d8\u10db\u10e8\u10d8 \u10d2\u10d0\u10db\u10dd\u10d5\u10da\u10d4\u10dc\u10d0\u10e1 \u10d4\u10e5\u10d5\u10d4\u10db\u10d3\u10d4\u10d1\u10d0\u10e0\u10d4\u10d1\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10dd\u10d1\u10d8\u10d4\u10e5\u10e2\u10d8. \u10d1\u10e0\u10d8\u10ef\u10d8\u10e1 \u10db\u10dd\u10ea\u10e3\u10da\u10dd\u10d1\u10d8\u10e1 \u10e0\u10d4\u10df\u10d8\u10db\u10e8\u10d8 \u10d7\u10e3 \u10e1\u10de\u10d4\u10ea\u10d8\u10e4\u10d8\u10d9\u10e3\u10e0\u10d8 \u10dd\u10d1\u10d8\u10d4\u10e5\u10e2\u10d4\u10d1\u10d8 \u10d0\u10e0 \u10d0\u10e0\u10d8\u10e1 \u10e8\u10d4\u10e0\u10e9\u10d4\u10e3\u10da\u10d8, \u10db\u10d8\u10e1\u10d0\u10ec\u10d5\u10d3\u10dd\u10db\u10d8 \u10d8\u10e5\u10dc\u10d4\u10d1\u10d0 \u10d3\u10dd\u10db\u10d4\u10dc\u10d8\u10e1 \u10e7\u10d5\u10d4\u10da\u10d0 \u10dd\u10d1\u10d8\u10d4\u10e5\u10e2\u10d8. \u10ee\u10d8\u10d3\u10d8\u10e1 \u10d2\u10d0\u10db\u10dd\u10e0\u10d8\u10ea\u10ee\u10d5\u10d8\u10e1 \u10e0\u10d4\u10df\u10d8\u10db\u10e8\u10d8, \u10d3\u10dd\u10db\u10d4\u10dc\u10d8\u10e1 \u10e7\u10d5\u10d4\u10da\u10d0 \u10dd\u10d1\u10d8\u10d4\u10e5\u10e2\u10d8 \u10d8\u10e5\u10dc\u10d4\u10d1\u10d0 \u10dc\u10d8\u10e1\u10d0\u10ec\u10d5\u10d3\u10dd\u10db\u10d8 \u10d2\u10d0\u10db\u10dd\u10e0\u10d8\u10ea\u10ee\u10e3\u10da \u10dd\u10d1\u10d8\u10d4\u10e5\u10e2\u10d4\u10d1\u10d8\u10e1 \u10d2\u10d0\u10e0\u10d3\u10d0.",
+ "title": "\u10d0\u10d8\u10e0\u10e9\u10d8\u10d4\u10d7 \u10d2\u10d0\u10db\u10dd\u10e1\u10d0\u10d5\u10da\u10d4\u10dc\u10d8 \u10dd\u10d1\u10d8\u10d4\u10e5\u10e2\u10d4\u10d1\u10d8"
+ },
+ "init": {
+ "data": {
+ "mode": "\u10e0\u10d4\u10df\u10d8\u10db\u10d8"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/homekit/translations/ko.json b/homeassistant/components/homekit/translations/ko.json
index 7020cb163e2..1b7276d4171 100644
--- a/homeassistant/components/homekit/translations/ko.json
+++ b/homeassistant/components/homekit/translations/ko.json
@@ -32,7 +32,7 @@
"data": {
"camera_copy": "\ub124\uc774\ud2f0\ube0c H.264 \uc2a4\ud2b8\ub9bc\uc744 \uc9c0\uc6d0\ud558\ub294 \uce74\uba54\ub77c"
},
- "description": "\ub124\uc774\ud2f0\ube0c H.264 \uc2a4\ud2b8\ub9bc\uc744 \uc9c0\uc6d0\ud558\ub294 \ubaa8\ub4e0 \uce74\uba54\ub77c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694. \uce74\uba54\ub77c\uac00 H.264 \uc2a4\ud2b8\ub9bc\uc744 \ucd9c\ub825\ud558\uc9c0 \uc54a\uc73c\uba74 \uc2dc\uc2a4\ud15c\uc740 \ube44\ub514\uc624\ub97c HomeKit \uc6a9 H.264 \ud3ec\ub9f7\uc73c\ub85c \ubcc0\ud658\uc2dc\ud0b5\ub2c8\ub2e4. \ud2b8\ub79c\uc2a4\ucf54\ub529 \ubcc0\ud658\uc5d0\ub294 \ub192\uc740 CPU \uc131\ub2a5\uc774 \ud544\uc694\ud558\uba70 \ub77c\uc988\ubca0\ub9ac\ud30c\uc774\uc640 \uac19\uc740 \ub2e8\uc77c \ubcf4\ub4dc \ucef4\ud4e8\ud130\uc5d0\uc11c\ub294 \uc791\ub3d9\ud558\uc9c0 \uc54a\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
+ "description": "\ub124\uc774\ud2f0\ube0c H.264 \uc2a4\ud2b8\ub9bc\uc744 \uc9c0\uc6d0\ud558\ub294 \ubaa8\ub4e0 \uce74\uba54\ub77c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694. \uce74\uba54\ub77c\uac00 H.264 \uc2a4\ud2b8\ub9bc\uc744 \ucd9c\ub825\ud558\uc9c0 \uc54a\uc73c\uba74 \uc2dc\uc2a4\ud15c\uc740 \ube44\ub514\uc624\ub97c HomeKit \uc6a9 H.264 \ud3ec\ub9f7\uc73c\ub85c \ubcc0\ud658\uc2dc\ud0b5\ub2c8\ub2e4. \ud2b8\ub79c\uc2a4\ucf54\ub529 \ubcc0\ud658\uc5d0\ub294 \ub192\uc740 CPU \uc131\ub2a5\uc774 \ud544\uc694\ud558\uba70 Raspberry Pi \uc640 \uac19\uc740 \ub2e8\uc77c \ubcf4\ub4dc \ucef4\ud4e8\ud130\uc5d0\uc11c\ub294 \uc791\ub3d9\ud558\uc9c0 \uc54a\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
"title": "\uce74\uba54\ub77c \ube44\ub514\uc624 \ucf54\ub371 \uc120\ud0dd\ud558\uae30"
},
"init": {
diff --git a/homeassistant/components/homekit/translations/nl.json b/homeassistant/components/homekit/translations/nl.json
index 487544cd5b5..ca41ff6758c 100644
--- a/homeassistant/components/homekit/translations/nl.json
+++ b/homeassistant/components/homekit/translations/nl.json
@@ -37,7 +37,8 @@
},
"init": {
"data": {
- "include_domains": "Op te nemen domeinen"
+ "include_domains": "Op te nemen domeinen",
+ "mode": "modus"
},
"description": "HomeKit kan worden geconfigureerd om een brug of een enkel accessoire te tonen. In de accessoiremodus kan slechts \u00e9\u00e9n entiteit worden gebruikt. De accessoiremodus is vereist om mediaspelers met de tv-apparaatklasse correct te laten werken. Entiteiten in de \"Op te nemen domeinen\" zullen worden blootgesteld aan HomeKit. U kunt op het volgende scherm selecteren welke entiteiten u wilt opnemen of uitsluiten van deze lijst.",
"title": "Selecteer domeinen om zichtbaar te maken."
diff --git a/homeassistant/components/homekit/translations/sl.json b/homeassistant/components/homekit/translations/sl.json
index af2823da6d4..caeba3a9b6c 100644
--- a/homeassistant/components/homekit/translations/sl.json
+++ b/homeassistant/components/homekit/translations/sl.json
@@ -6,7 +6,7 @@
"step": {
"pairing": {
"description": "Takoj, ko je most {name} pripravljen, bo zdru\u017eevanje na voljo v \"Obvestilih\" kot \"Nastavitev HomeKit mostu\".",
- "title": "Upari HomeKit Most"
+ "title": "Seznani HomeKit"
},
"user": {
"data": {
@@ -36,7 +36,8 @@
},
"include_exclude": {
"data": {
- "entities": "Entitete"
+ "entities": "Entitete",
+ "mode": "Na\u010din"
},
"title": "Izberite entitete, ki jih \u017eelite izpostaviti"
},
diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py
index fc9d20d61aa..b61a2c57612 100644
--- a/homeassistant/components/homekit/type_cameras.py
+++ b/homeassistant/components/homekit/type_cameras.py
@@ -370,7 +370,7 @@ class Camera(HomeAccessory, PyhapCamera):
if self.config[CONF_SUPPORT_AUDIO]:
output = output + " " + AUDIO_OUTPUT.format(**output_vars)
_LOGGER.debug("FFmpeg output settings: %s", output)
- stream = HAFFmpeg(self._ffmpeg.binary, loop=self.driver.loop)
+ stream = HAFFmpeg(self._ffmpeg.binary)
opened = await stream.open(
cmd=[], input_source=input_source, output=output, stdout_pipe=False
)
diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py
index b7139741be2..ef0fb531b1b 100644
--- a/homeassistant/components/homekit_controller/__init__.py
+++ b/homeassistant/components/homekit_controller/__init__.py
@@ -38,6 +38,8 @@ class HomeKitEntity(Entity):
self._signals = []
+ super().__init__()
+
@property
def accessory(self) -> Accessory:
"""Return an Accessory model that this entity is attached to."""
@@ -171,6 +173,16 @@ class HomeKitEntity(Entity):
raise NotImplementedError
+class AccessoryEntity(HomeKitEntity):
+ """A HomeKit entity that is related to an entire accessory rather than a specific service or characteristic."""
+
+ @property
+ def unique_id(self) -> str:
+ """Return the ID of this device."""
+ serial = self.accessory_info.value(CharacteristicsTypes.SERIAL_NUMBER)
+ return f"homekit-{serial}-aid:{self._aid}"
+
+
async def async_setup_entry(hass, entry):
"""Set up a HomeKit connection on a config entry."""
conn = HKDevice(hass, entry, entry.data)
diff --git a/homeassistant/components/homekit_controller/air_quality.py b/homeassistant/components/homekit_controller/air_quality.py
index 999980ad60c..896034a2ca0 100644
--- a/homeassistant/components/homekit_controller/air_quality.py
+++ b/homeassistant/components/homekit_controller/air_quality.py
@@ -1,5 +1,6 @@
"""Support for HomeKit Controller air quality sensors."""
from aiohomekit.model.characteristics import CharacteristicsTypes
+from aiohomekit.model.services import ServicesTypes
from homeassistant.components.air_quality import AirQualityEntity
from homeassistant.core import callback
@@ -85,10 +86,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
conn = hass.data[KNOWN_DEVICES][hkid]
@callback
- def async_add_service(aid, service):
- if service["stype"] != "air-quality":
+ def async_add_service(service):
+ if service.short_type != ServicesTypes.AIR_QUALITY_SENSOR:
return False
- info = {"aid": aid, "iid": service["iid"]}
+ info = {"aid": service.accessory.aid, "iid": service.iid}
async_add_entities([HomeAirQualitySensor(conn, info)], True)
return True
diff --git a/homeassistant/components/homekit_controller/alarm_control_panel.py b/homeassistant/components/homekit_controller/alarm_control_panel.py
index bf3a5f27142..621fb01ff74 100644
--- a/homeassistant/components/homekit_controller/alarm_control_panel.py
+++ b/homeassistant/components/homekit_controller/alarm_control_panel.py
@@ -1,5 +1,6 @@
"""Support for Homekit Alarm Control Panel."""
from aiohomekit.model.characteristics import CharacteristicsTypes
+from aiohomekit.model.services import ServicesTypes
from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity
from homeassistant.components.alarm_control_panel.const import (
@@ -43,10 +44,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
conn = hass.data[KNOWN_DEVICES][hkid]
@callback
- def async_add_service(aid, service):
- if service["stype"] != "security-system":
+ def async_add_service(service):
+ if service.short_type != ServicesTypes.SECURITY_SYSTEM:
return False
- info = {"aid": aid, "iid": service["iid"]}
+ info = {"aid": service.accessory.aid, "iid": service.iid}
async_add_entities([HomeKitAlarmControlPanelEntity(conn, info)], True)
return True
diff --git a/homeassistant/components/homekit_controller/binary_sensor.py b/homeassistant/components/homekit_controller/binary_sensor.py
index c718f7dc11a..537e9c2a698 100644
--- a/homeassistant/components/homekit_controller/binary_sensor.py
+++ b/homeassistant/components/homekit_controller/binary_sensor.py
@@ -1,5 +1,6 @@
"""Support for Homekit motion sensors."""
from aiohomekit.model.characteristics import CharacteristicsTypes
+from aiohomekit.model.services import ServicesTypes
from homeassistant.components.binary_sensor import (
DEVICE_CLASS_GAS,
@@ -124,12 +125,12 @@ class HomeKitLeakSensor(HomeKitEntity, BinarySensorEntity):
ENTITY_TYPES = {
- "motion": HomeKitMotionSensor,
- "contact": HomeKitContactSensor,
- "smoke": HomeKitSmokeSensor,
- "carbon-monoxide": HomeKitCarbonMonoxideSensor,
- "occupancy": HomeKitOccupancySensor,
- "leak": HomeKitLeakSensor,
+ ServicesTypes.MOTION_SENSOR: HomeKitMotionSensor,
+ ServicesTypes.CONTACT_SENSOR: HomeKitContactSensor,
+ ServicesTypes.SMOKE_SENSOR: HomeKitSmokeSensor,
+ ServicesTypes.CARBON_MONOXIDE_SENSOR: HomeKitCarbonMonoxideSensor,
+ ServicesTypes.OCCUPANCY_SENSOR: HomeKitOccupancySensor,
+ ServicesTypes.LEAK_SENSOR: HomeKitLeakSensor,
}
@@ -139,11 +140,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
conn = hass.data[KNOWN_DEVICES][hkid]
@callback
- def async_add_service(aid, service):
- entity_class = ENTITY_TYPES.get(service["stype"])
+ def async_add_service(service):
+ entity_class = ENTITY_TYPES.get(service.short_type)
if not entity_class:
return False
- info = {"aid": aid, "iid": service["iid"]}
+ info = {"aid": service.accessory.aid, "iid": service.iid}
async_add_entities([entity_class(conn, info)], True)
return True
diff --git a/homeassistant/components/homekit_controller/camera.py b/homeassistant/components/homekit_controller/camera.py
new file mode 100644
index 00000000000..fc6a5bb4522
--- /dev/null
+++ b/homeassistant/components/homekit_controller/camera.py
@@ -0,0 +1,50 @@
+"""Support for Homekit cameras."""
+from aiohomekit.model.services import ServicesTypes
+
+from homeassistant.components.camera import Camera
+from homeassistant.core import callback
+
+from . import KNOWN_DEVICES, AccessoryEntity
+
+
+class HomeKitCamera(AccessoryEntity, Camera):
+ """Representation of a Homekit camera."""
+
+ # content_type = "image/jpeg"
+
+ def get_characteristic_types(self):
+ """Define the homekit characteristics the entity is tracking."""
+ return []
+
+ @property
+ def state(self):
+ """Return the current state of the camera."""
+ return "idle"
+
+ async def async_camera_image(self):
+ """Return a jpeg with the current camera snapshot."""
+ return await self._accessory.pairing.image(
+ self._aid,
+ 640,
+ 480,
+ )
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up Homekit sensors."""
+ hkid = config_entry.data["AccessoryPairingID"]
+ conn = hass.data[KNOWN_DEVICES][hkid]
+
+ @callback
+ def async_add_accessory(accessory):
+ stream_mgmt = accessory.services.first(
+ service_type=ServicesTypes.CAMERA_RTP_STREAM_MANAGEMENT
+ )
+ if not stream_mgmt:
+ return
+
+ info = {"aid": accessory.aid, "iid": stream_mgmt.iid}
+ async_add_entities([HomeKitCamera(conn, info)], True)
+ return True
+
+ conn.add_accessory_factory(async_add_accessory)
diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py
index 546e46d67cb..ed2d3c74d7d 100644
--- a/homeassistant/components/homekit_controller/climate.py
+++ b/homeassistant/components/homekit_controller/climate.py
@@ -10,6 +10,7 @@ from aiohomekit.model.characteristics import (
SwingModeValues,
TargetHeaterCoolerStateValues,
)
+from aiohomekit.model.services import ServicesTypes
from aiohomekit.utils import clamp_enum_to_char
from homeassistant.components.climate import (
@@ -87,11 +88,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
conn = hass.data[KNOWN_DEVICES][hkid]
@callback
- def async_add_service(aid, service):
- entity_class = ENTITY_TYPES.get(service["stype"])
+ def async_add_service(service):
+ entity_class = ENTITY_TYPES.get(service.short_type)
if not entity_class:
return False
- info = {"aid": aid, "iid": service["iid"]}
+ info = {"aid": service.accessory.aid, "iid": service.iid}
async_add_entities([entity_class(conn, info)], True)
return True
@@ -454,6 +455,6 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity):
ENTITY_TYPES = {
- "heater-cooler": HomeKitHeaterCoolerEntity,
- "thermostat": HomeKitClimateEntity,
+ ServicesTypes.HEATER_COOLER: HomeKitHeaterCoolerEntity,
+ ServicesTypes.THERMOSTAT: HomeKitClimateEntity,
}
diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py
index 9881ef15dcb..e046a131a6b 100644
--- a/homeassistant/components/homekit_controller/config_flow.py
+++ b/homeassistant/components/homekit_controller/config_flow.py
@@ -21,6 +21,15 @@ HOMEKIT_BRIDGE_DOMAIN = "homekit"
HOMEKIT_BRIDGE_SERIAL_NUMBER = "homekit.bridge"
HOMEKIT_BRIDGE_MODEL = "Home Assistant HomeKit Bridge"
+HOMEKIT_IGNORE = [
+ # eufy Indoor Cam 2K and 2K Pan & Tilt
+ # https://github.com/home-assistant/core/issues/42307
+ "T8400",
+ "T8410",
+ # Hive Hub - vendor does not give user a pairing code
+ "HHKBridge1,1",
+]
+
PAIRING_FILE = "pairing.json"
MDNS_SUFFIX = "._hap._tcp.local."
@@ -255,6 +264,10 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow):
# Devices in HOMEKIT_IGNORE have native local integrations - users
# should be encouraged to use native integration and not confused
# by alternative HK API.
+ if model in HOMEKIT_IGNORE:
+ return self.async_abort(reason="ignored_model")
+
+ # If this is a HomeKit bridge exported by *this* HA instance ignore it.
if await self._hkid_is_homekit_bridge(hkid):
return self.async_abort(reason="ignored_model")
diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py
index ed2aaaa4656..2b37d3e3d20 100644
--- a/homeassistant/components/homekit_controller/connection.py
+++ b/homeassistant/components/homekit_controller/connection.py
@@ -76,6 +76,9 @@ class HKDevice:
self.entity_map = Accessories()
+ # A list of callbacks that turn HK accessories into entities
+ self.accessory_factories = []
+
# A list of callbacks that turn HK service metadata into entities
self.listeners = []
@@ -289,29 +292,42 @@ class HKDevice:
return True
+ def add_accessory_factory(self, add_entities_cb):
+ """Add a callback to run when discovering new entities for accessories."""
+ self.accessory_factories.append(add_entities_cb)
+ self._add_new_entities_for_accessory([add_entities_cb])
+
+ def _add_new_entities_for_accessory(self, handlers):
+ for accessory in self.entity_map.accessories:
+ for handler in handlers:
+ if (accessory.aid, None) in self.entities:
+ continue
+ if handler(accessory):
+ self.entities.append((accessory.aid, None))
+ break
+
def add_listener(self, add_entities_cb):
- """Add a callback to run when discovering new entities."""
+ """Add a callback to run when discovering new entities for services."""
self.listeners.append(add_entities_cb)
self._add_new_entities([add_entities_cb])
def add_entities(self):
"""Process the entity map and create HA entities."""
self._add_new_entities(self.listeners)
+ self._add_new_entities_for_accessory(self.accessory_factories)
def _add_new_entities(self, callbacks):
- for accessory in self.accessories:
- aid = accessory["aid"]
- for service in accessory["services"]:
- iid = service["iid"]
- stype = ServicesTypes.get_short(service["type"].upper())
- service["stype"] = stype
+ for accessory in self.entity_map.accessories:
+ aid = accessory.aid
+ for service in accessory.services:
+ iid = service.iid
if (aid, iid) in self.entities:
# Don't add the same entity again
continue
for listener in callbacks:
- if listener(aid, service):
+ if listener(service):
self.entities.append((aid, iid))
break
diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py
index b1e32417137..c3af1033148 100644
--- a/homeassistant/components/homekit_controller/const.py
+++ b/homeassistant/components/homekit_controller/const.py
@@ -25,6 +25,7 @@ HOMEKIT_ACCESSORY_DISPATCH = {
"motion": "binary_sensor",
"carbon-dioxide": "sensor",
"humidity": "sensor",
+ "humidifier-dehumidifier": "humidifier",
"light": "sensor",
"temperature": "sensor",
"battery": "sensor",
@@ -37,4 +38,5 @@ HOMEKIT_ACCESSORY_DISPATCH = {
"occupancy": "binary_sensor",
"television": "media_player",
"valve": "switch",
+ "camera-rtp-stream-management": "camera",
}
diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py
index a79ef5d1ee7..fdf48ebba5d 100644
--- a/homeassistant/components/homekit_controller/cover.py
+++ b/homeassistant/components/homekit_controller/cover.py
@@ -1,5 +1,6 @@
"""Support for Homekit covers."""
from aiohomekit.model.characteristics import CharacteristicsTypes
+from aiohomekit.model.services import ServicesTypes
from homeassistant.components.cover import (
ATTR_POSITION,
@@ -39,17 +40,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
conn = hass.data[KNOWN_DEVICES][hkid]
@callback
- def async_add_service(aid, service):
- info = {"aid": aid, "iid": service["iid"]}
- if service["stype"] == "garage-door-opener":
- async_add_entities([HomeKitGarageDoorCover(conn, info)], True)
- return True
-
- if service["stype"] in ("window-covering", "window"):
- async_add_entities([HomeKitWindowCover(conn, info)], True)
- return True
-
- return False
+ def async_add_service(service):
+ entity_class = ENTITY_TYPES.get(service.short_type)
+ if not entity_class:
+ return False
+ info = {"aid": service.accessory.aid, "iid": service.iid}
+ async_add_entities([entity_class(conn, info)], True)
+ return True
conn.add_listener(async_add_service)
@@ -246,3 +243,9 @@ class HomeKitWindowCover(HomeKitEntity, CoverEntity):
if not obstruction_detected:
return {}
return {"obstruction-detected": obstruction_detected}
+
+
+ENTITY_TYPES = {
+ ServicesTypes.GARAGE_DOOR_OPENER: HomeKitGarageDoorCover,
+ ServicesTypes.WINDOW_COVERING: HomeKitWindowCover,
+}
diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py
index 76b82eec597..b2e668915d7 100644
--- a/homeassistant/components/homekit_controller/device_trigger.py
+++ b/homeassistant/components/homekit_controller/device_trigger.py
@@ -174,9 +174,9 @@ def enumerate_doorbell(service):
TRIGGER_FINDERS = {
- "service-label": enumerate_stateless_switch_group,
- "stateless-programmable-switch": enumerate_stateless_switch,
- "doorbell": enumerate_doorbell,
+ ServicesTypes.SERVICE_LABEL: enumerate_stateless_switch_group,
+ ServicesTypes.STATELESS_PROGRAMMABLE_SWITCH: enumerate_stateless_switch,
+ ServicesTypes.DOORBELL: enumerate_doorbell,
}
@@ -186,8 +186,9 @@ async def async_setup_triggers_for_entry(hass: HomeAssistant, config_entry):
conn = hass.data[KNOWN_DEVICES][hkid]
@callback
- def async_add_service(aid, service_dict):
- service_type = service_dict["stype"]
+ def async_add_service(service):
+ aid = service.accessory.aid
+ service_type = service.short_type
# If not a known service type then we can't handle any stateless events for it
if service_type not in TRIGGER_FINDERS:
@@ -201,11 +202,6 @@ async def async_setup_triggers_for_entry(hass: HomeAssistant, config_entry):
if device_id in hass.data[TRIGGERS]:
return False
- # At the moment add_listener calls us with the raw service dict, rather than
- # a service model. So turn it into a service ourselves.
- accessory = conn.entity_map.aid(aid)
- service = accessory.services.iid(service_dict["iid"])
-
# Just because we recognise the service type doesn't mean we can actually
# extract any triggers - so only proceed if we can
triggers = TRIGGER_FINDERS[service_type](service)
diff --git a/homeassistant/components/homekit_controller/fan.py b/homeassistant/components/homekit_controller/fan.py
index 5fe119f65b7..476d0f2c8e5 100644
--- a/homeassistant/components/homekit_controller/fan.py
+++ b/homeassistant/components/homekit_controller/fan.py
@@ -1,5 +1,6 @@
"""Support for Homekit fans."""
from aiohomekit.model.characteristics import CharacteristicsTypes
+from aiohomekit.model.services import ServicesTypes
from homeassistant.components.fan import (
DIRECTION_FORWARD,
@@ -161,8 +162,8 @@ class HomeKitFanV2(BaseHomeKitFan):
ENTITY_TYPES = {
- "fan": HomeKitFanV1,
- "fanv2": HomeKitFanV2,
+ ServicesTypes.FAN: HomeKitFanV1,
+ ServicesTypes.FAN_V2: HomeKitFanV2,
}
@@ -172,11 +173,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
conn = hass.data[KNOWN_DEVICES][hkid]
@callback
- def async_add_service(aid, service):
- entity_class = ENTITY_TYPES.get(service["stype"])
+ def async_add_service(service):
+ entity_class = ENTITY_TYPES.get(service.short_type)
if not entity_class:
return False
- info = {"aid": aid, "iid": service["iid"]}
+ info = {"aid": service.accessory.aid, "iid": service.iid}
async_add_entities([entity_class(conn, info)], True)
return True
diff --git a/homeassistant/components/homekit_controller/humidifier.py b/homeassistant/components/homekit_controller/humidifier.py
new file mode 100644
index 00000000000..e4bed25d618
--- /dev/null
+++ b/homeassistant/components/homekit_controller/humidifier.py
@@ -0,0 +1,276 @@
+"""Support for HomeKit Controller humidifier."""
+from typing import List, Optional
+
+from aiohomekit.model.characteristics import CharacteristicsTypes
+from aiohomekit.model.services import ServicesTypes
+
+from homeassistant.components.humidifier import HumidifierEntity
+from homeassistant.components.humidifier.const import (
+ DEVICE_CLASS_DEHUMIDIFIER,
+ DEVICE_CLASS_HUMIDIFIER,
+ MODE_AUTO,
+ MODE_NORMAL,
+ SUPPORT_MODES,
+)
+from homeassistant.core import callback
+
+from . import KNOWN_DEVICES, HomeKitEntity
+
+SUPPORT_FLAGS = 0
+
+HK_MODE_TO_HA = {
+ 0: "off",
+ 1: MODE_AUTO,
+ 2: "humidifying",
+ 3: "dehumidifying",
+}
+
+HA_MODE_TO_HK = {
+ MODE_AUTO: 0,
+ "humidifying": 1,
+ "dehumidifying": 2,
+}
+
+
+class HomeKitHumidifier(HomeKitEntity, HumidifierEntity):
+ """Representation of a HomeKit Controller Humidifier."""
+
+ def get_characteristic_types(self):
+ """Define the homekit characteristics the entity cares about."""
+ return [
+ CharacteristicsTypes.ACTIVE,
+ CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT,
+ CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE,
+ CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE,
+ CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD,
+ ]
+
+ @property
+ def device_class(self) -> str:
+ """Return the device class of the device."""
+ return DEVICE_CLASS_HUMIDIFIER
+
+ @property
+ def supported_features(self):
+ """Return the list of supported features."""
+ return SUPPORT_FLAGS | SUPPORT_MODES
+
+ @property
+ def is_on(self):
+ """Return true if device is on."""
+ return self.service.value(CharacteristicsTypes.ACTIVE)
+
+ async def async_turn_on(self, **kwargs):
+ """Turn the specified valve on."""
+ await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: True})
+
+ async def async_turn_off(self, **kwargs):
+ """Turn the specified valve off."""
+ await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: False})
+
+ @property
+ def target_humidity(self) -> Optional[int]:
+ """Return the humidity we try to reach."""
+ return self.service.value(
+ CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD
+ )
+
+ @property
+ def mode(self) -> Optional[str]:
+ """Return the current mode, e.g., home, auto, baby.
+
+ Requires SUPPORT_MODES.
+ """
+ mode = self.service.value(
+ CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE
+ )
+ return MODE_AUTO if mode == 1 else MODE_NORMAL
+
+ @property
+ def available_modes(self) -> Optional[List[str]]:
+ """Return a list of available modes.
+
+ Requires SUPPORT_MODES.
+ """
+ available_modes = [
+ MODE_NORMAL,
+ MODE_AUTO,
+ ]
+
+ return available_modes
+
+ async def async_set_humidity(self, humidity: int) -> None:
+ """Set new target humidity."""
+ await self.async_put_characteristics(
+ {CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD: humidity}
+ )
+
+ async def async_set_mode(self, mode: str) -> None:
+ """Set new mode."""
+ if mode == MODE_AUTO:
+ await self.async_put_characteristics(
+ {
+ CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE: 0,
+ CharacteristicsTypes.ACTIVE: True,
+ }
+ )
+ else:
+ await self.async_put_characteristics(
+ {
+ CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE: 1,
+ CharacteristicsTypes.ACTIVE: True,
+ }
+ )
+
+ @property
+ def min_humidity(self) -> int:
+ """Return the minimum humidity."""
+ return self.service[
+ CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD
+ ].minValue
+
+ @property
+ def max_humidity(self) -> int:
+ """Return the maximum humidity."""
+ return self.service[
+ CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD
+ ].maxValue
+
+
+class HomeKitDehumidifier(HomeKitEntity, HumidifierEntity):
+ """Representation of a HomeKit Controller Humidifier."""
+
+ def get_characteristic_types(self):
+ """Define the homekit characteristics the entity cares about."""
+ return [
+ CharacteristicsTypes.ACTIVE,
+ CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT,
+ CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE,
+ CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE,
+ CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD,
+ CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD,
+ ]
+
+ @property
+ def device_class(self) -> str:
+ """Return the device class of the device."""
+ return DEVICE_CLASS_DEHUMIDIFIER
+
+ @property
+ def supported_features(self):
+ """Return the list of supported features."""
+ return SUPPORT_FLAGS | SUPPORT_MODES
+
+ @property
+ def is_on(self):
+ """Return true if device is on."""
+ return self.service.value(CharacteristicsTypes.ACTIVE)
+
+ async def async_turn_on(self, **kwargs):
+ """Turn the specified valve on."""
+ await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: True})
+
+ async def async_turn_off(self, **kwargs):
+ """Turn the specified valve off."""
+ await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: False})
+
+ @property
+ def target_humidity(self) -> Optional[int]:
+ """Return the humidity we try to reach."""
+ return self.service.value(
+ CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD
+ )
+
+ @property
+ def mode(self) -> Optional[str]:
+ """Return the current mode, e.g., home, auto, baby.
+
+ Requires SUPPORT_MODES.
+ """
+ mode = self.service.value(
+ CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE
+ )
+ return MODE_AUTO if mode == 1 else MODE_NORMAL
+
+ @property
+ def available_modes(self) -> Optional[List[str]]:
+ """Return a list of available modes.
+
+ Requires SUPPORT_MODES.
+ """
+ available_modes = [
+ MODE_NORMAL,
+ MODE_AUTO,
+ ]
+
+ return available_modes
+
+ async def async_set_humidity(self, humidity: int) -> None:
+ """Set new target humidity."""
+ await self.async_put_characteristics(
+ {CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD: humidity}
+ )
+
+ async def async_set_mode(self, mode: str) -> None:
+ """Set new mode."""
+ if mode == MODE_AUTO:
+ await self.async_put_characteristics(
+ {
+ CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE: 0,
+ CharacteristicsTypes.ACTIVE: True,
+ }
+ )
+ else:
+ await self.async_put_characteristics(
+ {
+ CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE: 2,
+ CharacteristicsTypes.ACTIVE: True,
+ }
+ )
+
+ @property
+ def min_humidity(self) -> int:
+ """Return the minimum humidity."""
+ return self.service[
+ CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD
+ ].minValue
+
+ @property
+ def max_humidity(self) -> int:
+ """Return the maximum humidity."""
+ return self.service[
+ CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD
+ ].maxValue
+
+ @property
+ def unique_id(self) -> str:
+ """Return the ID of this device."""
+ serial = self.accessory_info.value(CharacteristicsTypes.SERIAL_NUMBER)
+ return f"homekit-{serial}-{self._iid}-{self.device_class}"
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up Homekit humidifer."""
+ hkid = config_entry.data["AccessoryPairingID"]
+ conn = hass.data[KNOWN_DEVICES][hkid]
+
+ @callback
+ def async_add_service(service):
+ if service.short_type != ServicesTypes.HUMIDIFIER_DEHUMIDIFIER:
+ return False
+
+ info = {"aid": service.accessory.aid, "iid": service.iid}
+
+ entities = []
+
+ if service.has(CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD):
+ entities.append(HomeKitHumidifier(conn, info))
+
+ if service.has(CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD):
+ entities.append(HomeKitDehumidifier(conn, info))
+
+ async_add_entities(entities, True)
+
+ return True
+
+ conn.add_listener(async_add_service)
diff --git a/homeassistant/components/homekit_controller/light.py b/homeassistant/components/homekit_controller/light.py
index 3f9f6f3de29..497131327b6 100644
--- a/homeassistant/components/homekit_controller/light.py
+++ b/homeassistant/components/homekit_controller/light.py
@@ -1,5 +1,6 @@
"""Support for Homekit lights."""
from aiohomekit.model.characteristics import CharacteristicsTypes
+from aiohomekit.model.services import ServicesTypes
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
@@ -21,10 +22,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
conn = hass.data[KNOWN_DEVICES][hkid]
@callback
- def async_add_service(aid, service):
- if service["stype"] != "lightbulb":
+ def async_add_service(service):
+ if service.short_type != ServicesTypes.LIGHTBULB:
return False
- info = {"aid": aid, "iid": service["iid"]}
+ info = {"aid": service.accessory.aid, "iid": service.iid}
async_add_entities([HomeKitLight(conn, info)], True)
return True
diff --git a/homeassistant/components/homekit_controller/lock.py b/homeassistant/components/homekit_controller/lock.py
index 52e9947d986..8ac7fd608fd 100644
--- a/homeassistant/components/homekit_controller/lock.py
+++ b/homeassistant/components/homekit_controller/lock.py
@@ -1,5 +1,6 @@
"""Support for HomeKit Controller locks."""
from aiohomekit.model.characteristics import CharacteristicsTypes
+from aiohomekit.model.services import ServicesTypes
from homeassistant.components.lock import LockEntity
from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_LOCKED, STATE_UNLOCKED
@@ -20,10 +21,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
conn = hass.data[KNOWN_DEVICES][hkid]
@callback
- def async_add_service(aid, service):
- if service["stype"] != "lock-mechanism":
+ def async_add_service(service):
+ if service.short_type != ServicesTypes.LOCK_MECHANISM:
return False
- info = {"aid": aid, "iid": service["iid"]}
+ info = {"aid": service.accessory.aid, "iid": service.iid}
async_add_entities([HomeKitLock(conn, info)], True)
return True
diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json
index efe842bad0f..9580a7ee50d 100644
--- a/homeassistant/components/homekit_controller/manifest.json
+++ b/homeassistant/components/homekit_controller/manifest.json
@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"requirements": [
- "aiohomekit==0.2.54"
+ "aiohomekit==0.2.60"
],
"zeroconf": [
"_hap._tcp.local."
diff --git a/homeassistant/components/homekit_controller/media_player.py b/homeassistant/components/homekit_controller/media_player.py
index 2ffc794409b..6dfa8720ee5 100644
--- a/homeassistant/components/homekit_controller/media_player.py
+++ b/homeassistant/components/homekit_controller/media_player.py
@@ -44,10 +44,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
conn = hass.data[KNOWN_DEVICES][hkid]
@callback
- def async_add_service(aid, service):
- if service["stype"] != "television":
+ def async_add_service(service):
+ if service.short_type != ServicesTypes.TELEVISION:
return False
- info = {"aid": aid, "iid": service["iid"]}
+ info = {"aid": service.accessory.aid, "iid": service.iid}
async_add_entities([HomeKitTelevision(conn, info)], True)
return True
diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py
index 2075eb9dcc3..677f1bc67f1 100644
--- a/homeassistant/components/homekit_controller/sensor.py
+++ b/homeassistant/components/homekit_controller/sensor.py
@@ -1,5 +1,6 @@
"""Support for Homekit sensors."""
from aiohomekit.model.characteristics import CharacteristicsTypes
+from aiohomekit.model.services import ServicesTypes
from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
@@ -216,11 +217,11 @@ class HomeKitBatterySensor(HomeKitEntity):
ENTITY_TYPES = {
- "humidity": HomeKitHumiditySensor,
- "temperature": HomeKitTemperatureSensor,
- "light": HomeKitLightSensor,
- "carbon-dioxide": HomeKitCarbonDioxideSensor,
- "battery": HomeKitBatterySensor,
+ ServicesTypes.HUMIDITY_SENSOR: HomeKitHumiditySensor,
+ ServicesTypes.TEMPERATURE_SENSOR: HomeKitTemperatureSensor,
+ ServicesTypes.LIGHT_SENSOR: HomeKitLightSensor,
+ ServicesTypes.CARBON_DIOXIDE_SENSOR: HomeKitCarbonDioxideSensor,
+ ServicesTypes.BATTERY_SERVICE: HomeKitBatterySensor,
}
@@ -230,11 +231,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
conn = hass.data[KNOWN_DEVICES][hkid]
@callback
- def async_add_service(aid, service):
- entity_class = ENTITY_TYPES.get(service["stype"])
+ def async_add_service(service):
+ entity_class = ENTITY_TYPES.get(service.short_type)
if not entity_class:
return False
- info = {"aid": aid, "iid": service["iid"]}
+ info = {"aid": service.accessory.aid, "iid": service.iid}
async_add_entities([entity_class(conn, info)], True)
return True
diff --git a/homeassistant/components/homekit_controller/switch.py b/homeassistant/components/homekit_controller/switch.py
index 3408d036b58..b9d0b273cb1 100644
--- a/homeassistant/components/homekit_controller/switch.py
+++ b/homeassistant/components/homekit_controller/switch.py
@@ -4,6 +4,7 @@ from aiohomekit.model.characteristics import (
InUseValues,
IsConfiguredValues,
)
+from aiohomekit.model.services import ServicesTypes
from homeassistant.components.switch import SwitchEntity
from homeassistant.core import callback
@@ -96,9 +97,9 @@ class HomeKitValve(HomeKitEntity, SwitchEntity):
ENTITY_TYPES = {
- "switch": HomeKitSwitch,
- "outlet": HomeKitSwitch,
- "valve": HomeKitValve,
+ ServicesTypes.SWITCH: HomeKitSwitch,
+ ServicesTypes.OUTLET: HomeKitSwitch,
+ ServicesTypes.VALVE: HomeKitValve,
}
@@ -108,11 +109,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
conn = hass.data[KNOWN_DEVICES][hkid]
@callback
- def async_add_service(aid, service):
- entity_class = ENTITY_TYPES.get(service["stype"])
+ def async_add_service(service):
+ entity_class = ENTITY_TYPES.get(service.short_type)
if not entity_class:
return False
- info = {"aid": aid, "iid": service["iid"]}
+ info = {"aid": service.accessory.aid, "iid": service.iid}
async_add_entities([entity_class(conn, info)], True)
return True
diff --git a/homeassistant/components/homematicip_cloud/translations/no.json b/homeassistant/components/homematicip_cloud/translations/no.json
index ef171c7b52e..d28fe17a691 100644
--- a/homeassistant/components/homematicip_cloud/translations/no.json
+++ b/homeassistant/components/homematicip_cloud/translations/no.json
@@ -14,7 +14,7 @@
"step": {
"init": {
"data": {
- "hapid": "Tilgangspunkt-ID (SGTIN)",
+ "hapid": "Tilgangspunkt ID (SGTIN)",
"name": "Navn (valgfritt, brukes som navneprefiks for alle enheter)",
"pin": "PIN-kode"
},
diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py
index b880cfdf1cc..bdfd505a317 100644
--- a/homeassistant/components/homematicip_cloud/weather.py
+++ b/homeassistant/components/homematicip_cloud/weather.py
@@ -6,7 +6,19 @@ from homematicip.aio.device import (
)
from homematicip.base.enums import WeatherCondition
-from homeassistant.components.weather import WeatherEntity
+from homeassistant.components.weather import (
+ ATTR_CONDITION_CLOUDY,
+ ATTR_CONDITION_FOG,
+ ATTR_CONDITION_LIGHTNING,
+ ATTR_CONDITION_LIGHTNING_RAINY,
+ ATTR_CONDITION_PARTLYCLOUDY,
+ ATTR_CONDITION_RAINY,
+ ATTR_CONDITION_SNOWY,
+ ATTR_CONDITION_SNOWY_RAINY,
+ ATTR_CONDITION_SUNNY,
+ ATTR_CONDITION_WINDY,
+ WeatherEntity,
+)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import TEMP_CELSIUS
from homeassistant.helpers.typing import HomeAssistantType
@@ -15,20 +27,20 @@ from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity
from .hap import HomematicipHAP
HOME_WEATHER_CONDITION = {
- WeatherCondition.CLEAR: "sunny",
- WeatherCondition.LIGHT_CLOUDY: "partlycloudy",
- WeatherCondition.CLOUDY: "cloudy",
- WeatherCondition.CLOUDY_WITH_RAIN: "rainy",
- WeatherCondition.CLOUDY_WITH_SNOW_RAIN: "snowy-rainy",
- WeatherCondition.HEAVILY_CLOUDY: "cloudy",
- WeatherCondition.HEAVILY_CLOUDY_WITH_RAIN: "rainy",
- WeatherCondition.HEAVILY_CLOUDY_WITH_STRONG_RAIN: "snowy-rainy",
- WeatherCondition.HEAVILY_CLOUDY_WITH_SNOW: "snowy",
- WeatherCondition.HEAVILY_CLOUDY_WITH_SNOW_RAIN: "snowy-rainy",
- WeatherCondition.HEAVILY_CLOUDY_WITH_THUNDER: "lightning",
- WeatherCondition.HEAVILY_CLOUDY_WITH_RAIN_AND_THUNDER: "lightning-rainy",
- WeatherCondition.FOGGY: "fog",
- WeatherCondition.STRONG_WIND: "windy",
+ WeatherCondition.CLEAR: ATTR_CONDITION_SUNNY,
+ WeatherCondition.LIGHT_CLOUDY: ATTR_CONDITION_PARTLYCLOUDY,
+ WeatherCondition.CLOUDY: ATTR_CONDITION_CLOUDY,
+ WeatherCondition.CLOUDY_WITH_RAIN: ATTR_CONDITION_RAINY,
+ WeatherCondition.CLOUDY_WITH_SNOW_RAIN: ATTR_CONDITION_SNOWY_RAINY,
+ WeatherCondition.HEAVILY_CLOUDY: ATTR_CONDITION_CLOUDY,
+ WeatherCondition.HEAVILY_CLOUDY_WITH_RAIN: ATTR_CONDITION_RAINY,
+ WeatherCondition.HEAVILY_CLOUDY_WITH_STRONG_RAIN: ATTR_CONDITION_SNOWY_RAINY,
+ WeatherCondition.HEAVILY_CLOUDY_WITH_SNOW: ATTR_CONDITION_SNOWY,
+ WeatherCondition.HEAVILY_CLOUDY_WITH_SNOW_RAIN: ATTR_CONDITION_SNOWY_RAINY,
+ WeatherCondition.HEAVILY_CLOUDY_WITH_THUNDER: ATTR_CONDITION_LIGHTNING,
+ WeatherCondition.HEAVILY_CLOUDY_WITH_RAIN_AND_THUNDER: ATTR_CONDITION_LIGHTNING_RAINY,
+ WeatherCondition.FOGGY: ATTR_CONDITION_FOG,
+ WeatherCondition.STRONG_WIND: ATTR_CONDITION_WINDY,
WeatherCondition.UNKNOWN: "",
}
@@ -92,11 +104,11 @@ class HomematicipWeatherSensor(HomematicipGenericEntity, WeatherEntity):
def condition(self) -> str:
"""Return the current condition."""
if getattr(self._device, "raining", None):
- return "rainy"
+ return ATTR_CONDITION_RAINY
if self._device.storm:
- return "windy"
+ return ATTR_CONDITION_WINDY
if self._device.sunshine:
- return "sunny"
+ return ATTR_CONDITION_SUNNY
return ""
diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py
index 421b17e47cc..9e47dd29a23 100644
--- a/homeassistant/components/http/__init__.py
+++ b/homeassistant/components/http/__init__.py
@@ -433,6 +433,8 @@ class HomeAssistantHTTP:
"Failed to create HTTP server at port %d: %s", self.server_port, error
)
+ _LOGGER.info("Now listening on port %d", self.server_port)
+
async def stop(self):
"""Stop the aiohttp server."""
await self.site.stop()
diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py
index 2c02cc7391f..14d81a1eb6e 100644
--- a/homeassistant/components/http/ban.py
+++ b/homeassistant/components/http/ban.py
@@ -15,7 +15,7 @@ from homeassistant.const import HTTP_BAD_REQUEST
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
-from homeassistant.util.yaml import dump
+from homeassistant.util import dt as dt_util, yaml
# mypy: allow-untyped-defs, no-check-untyped-defs
@@ -179,7 +179,7 @@ class IpBan:
def __init__(self, ip_ban: str, banned_at: Optional[datetime] = None) -> None:
"""Initialize IP Ban object."""
self.ip_address = ip_address(ip_ban)
- self.banned_at = banned_at or datetime.utcnow()
+ self.banned_at = banned_at or dt_util.utcnow()
async def async_load_ip_bans_config(hass: HomeAssistant, path: str) -> List[IpBan]:
@@ -208,10 +208,6 @@ async def async_load_ip_bans_config(hass: HomeAssistant, path: str) -> List[IpBa
def update_ip_bans_config(path: str, ip_ban: IpBan) -> None:
"""Update config file with new banned IP address."""
with open(path, "a") as out:
- ip_ = {
- str(ip_ban.ip_address): {
- ATTR_BANNED_AT: ip_ban.banned_at.strftime("%Y-%m-%dT%H:%M:%S")
- }
- }
+ ip_ = {str(ip_ban.ip_address): {ATTR_BANNED_AT: ip_ban.banned_at.isoformat()}}
out.write("\n")
- out.write(dump(ip_))
+ out.write(yaml.dump(ip_))
diff --git a/homeassistant/components/huawei_lte/translations/pl.json b/homeassistant/components/huawei_lte/translations/pl.json
index 0cc6269c05d..0720182b697 100644
--- a/homeassistant/components/huawei_lte/translations/pl.json
+++ b/homeassistant/components/huawei_lte/translations/pl.json
@@ -23,7 +23,7 @@
"url": "URL",
"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 zewn\u0105trz Home Assistant, 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/hyperion/__init__.py b/homeassistant/components/hyperion/__init__.py
index 60a0a2d3210..13dd977b7dc 100644
--- a/homeassistant/components/hyperion/__init__.py
+++ b/homeassistant/components/hyperion/__init__.py
@@ -1 +1,174 @@
"""The Hyperion component."""
+
+import asyncio
+import logging
+from typing import Any, Optional
+
+from hyperion import client, const as hyperion_const
+from pkg_resources import parse_version
+
+from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN
+from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers.dispatcher import async_dispatcher_send
+from homeassistant.helpers.typing import ConfigType, HomeAssistantType
+
+from .const import (
+ CONF_ON_UNLOAD,
+ CONF_ROOT_CLIENT,
+ DOMAIN,
+ HYPERION_RELEASES_URL,
+ HYPERION_VERSION_WARN_CUTOFF,
+ SIGNAL_INSTANCES_UPDATED,
+)
+
+PLATFORMS = [LIGHT_DOMAIN]
+
+_LOGGER = logging.getLogger(__name__)
+
+# Unique ID
+# =========
+# A config entry represents a connection to a single Hyperion server. The config entry
+# unique_id is the server id returned from the Hyperion instance (a unique ID per
+# server).
+#
+# Each server connection may create multiple entities. The unique_id for each entity is
+# __, where will be the unique_id on the
+# relevant config entry (as above), will be the server instance # and
+# will be a unique identifying type name for each entity associated with this
+# server/instance (e.g. "hyperion_light").
+#
+# The get_hyperion_unique_id method will create a per-entity unique id when given the
+# server id, an instance number and a name.
+
+# hass.data format
+# ================
+#
+# hass.data[DOMAIN] = {
+# : {
+# "ROOT_CLIENT": ,
+# "ON_UNLOAD": [, ...],
+# }
+# }
+
+
+def get_hyperion_unique_id(server_id: str, instance: int, name: str) -> str:
+ """Get a unique_id for a Hyperion instance."""
+ return f"{server_id}_{instance}_{name}"
+
+
+def create_hyperion_client(
+ *args: Any,
+ **kwargs: Any,
+) -> client.HyperionClient:
+ """Create a Hyperion Client."""
+ return client.HyperionClient(*args, **kwargs)
+
+
+async def async_create_connect_hyperion_client(
+ *args: Any,
+ **kwargs: Any,
+) -> Optional[client.HyperionClient]:
+ """Create and connect a Hyperion Client."""
+ hyperion_client = create_hyperion_client(*args, **kwargs)
+
+ if not await hyperion_client.async_client_connect():
+ return None
+ return hyperion_client
+
+
+async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
+ """Set up Hyperion component."""
+ hass.data[DOMAIN] = {}
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
+ """Set up Hyperion from a config entry."""
+ host = config_entry.data[CONF_HOST]
+ port = config_entry.data[CONF_PORT]
+ token = config_entry.data.get(CONF_TOKEN)
+
+ hyperion_client = await async_create_connect_hyperion_client(
+ host, port, token=token
+ )
+ if not hyperion_client:
+ raise ConfigEntryNotReady
+ version = await hyperion_client.async_sysinfo_version()
+ if version is not None:
+ try:
+ if parse_version(version) < parse_version(HYPERION_VERSION_WARN_CUTOFF):
+ _LOGGER.warning(
+ "Using a Hyperion server version < %s is not recommended -- "
+ "some features may be unavailable or may not function correctly. "
+ "Please consider upgrading: %s",
+ HYPERION_VERSION_WARN_CUTOFF,
+ HYPERION_RELEASES_URL,
+ )
+ except ValueError:
+ pass
+
+ hyperion_client.set_callbacks(
+ {
+ f"{hyperion_const.KEY_INSTANCE}-{hyperion_const.KEY_UPDATE}": lambda json: (
+ async_dispatcher_send(
+ hass,
+ SIGNAL_INSTANCES_UPDATED.format(config_entry.entry_id),
+ json,
+ )
+ )
+ }
+ )
+
+ hass.data[DOMAIN][config_entry.entry_id] = {
+ CONF_ROOT_CLIENT: hyperion_client,
+ CONF_ON_UNLOAD: [],
+ }
+
+ # Must only listen for option updates after the setup is complete, as otherwise
+ # the YAML->ConfigEntry migration code triggers an options update, which causes a
+ # reload -- which clashes with the initial load (causing entity_id / unique_id
+ # clashes).
+ async def setup_then_listen() -> None:
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_setup(config_entry, component)
+ for component in PLATFORMS
+ ]
+ )
+ hass.data[DOMAIN][config_entry.entry_id][CONF_ON_UNLOAD].append(
+ config_entry.add_update_listener(_async_options_updated)
+ )
+
+ hass.async_create_task(setup_then_listen())
+ return True
+
+
+async def _async_options_updated(
+ hass: HomeAssistantType, config_entry: ConfigEntry
+) -> None:
+ """Handle options update."""
+ await hass.config_entries.async_reload(config_entry.entry_id)
+
+
+async def async_unload_entry(
+ hass: HomeAssistantType, config_entry: ConfigEntry
+) -> bool:
+ """Unload a config entry."""
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(config_entry, component)
+ for component in PLATFORMS
+ ]
+ )
+ )
+ if unload_ok and config_entry.entry_id in hass.data[DOMAIN]:
+ config_data = hass.data[DOMAIN].pop(config_entry.entry_id)
+ for func in config_data[CONF_ON_UNLOAD]:
+ func()
+ root_client = config_data[CONF_ROOT_CLIENT]
+ await root_client.async_client_disconnect()
+ return unload_ok
diff --git a/homeassistant/components/hyperion/config_flow.py b/homeassistant/components/hyperion/config_flow.py
new file mode 100644
index 00000000000..aef74e530b1
--- /dev/null
+++ b/homeassistant/components/hyperion/config_flow.py
@@ -0,0 +1,445 @@
+"""Hyperion config flow."""
+from __future__ import annotations
+
+import asyncio
+import logging
+from typing import Any, Dict, Optional
+from urllib.parse import urlparse
+
+from hyperion import client, const
+import voluptuous as vol
+
+from homeassistant.components.ssdp import ATTR_SSDP_LOCATION, ATTR_UPNP_SERIAL
+from homeassistant.config_entries import (
+ CONN_CLASS_LOCAL_PUSH,
+ ConfigEntry,
+ ConfigFlow,
+ OptionsFlow,
+)
+from homeassistant.const import CONF_BASE, CONF_HOST, CONF_ID, CONF_PORT, CONF_TOKEN
+from homeassistant.core import callback
+from homeassistant.helpers.typing import ConfigType
+
+from . import create_hyperion_client
+
+# pylint: disable=unused-import
+from .const import (
+ CONF_AUTH_ID,
+ CONF_CREATE_TOKEN,
+ CONF_PRIORITY,
+ DEFAULT_ORIGIN,
+ DEFAULT_PRIORITY,
+ DOMAIN,
+)
+
+_LOGGER = logging.getLogger(__name__)
+_LOGGER.setLevel(logging.DEBUG)
+
+# +------------------+ +------------------+ +--------------------+
+# |Step: SSDP | |Step: user | |Step: import |
+# | | | | | |
+# |Input: | |Input: | |Input: |
+# +------------------+ +------------------+ +--------------------+
+# v v v
+# +----------------------+-----------------------+
+# Auth not | Auth |
+# required? | required? |
+# | v
+# | +------------+
+# | |Step: auth |
+# | | |
+# | |Input: token|
+# | +------------+
+# | Static |
+# v token |
+# <------------------+
+# | |
+# | | New token
+# | v
+# | +------------------+
+# | |Step: create_token|
+# | +------------------+
+# | |
+# | v
+# | +---------------------------+ +--------------------------------+
+# | |Step: create_token_external|-->|Step: create_token_external_fail|
+# | +---------------------------+ +--------------------------------+
+# | |
+# | v
+# | +-----------------------------------+
+# | |Step: create_token_external_success|
+# | +-----------------------------------+
+# | |
+# v<------------------+
+# |
+# v
+# +-------------+ Confirm not required?
+# |Step: Confirm|---------------------->+
+# +-------------+ |
+# | |
+# v SSDP: Explicit confirm |
+# +------------------------------>+
+# |
+# v
+# +----------------+
+# | Create! |
+# +----------------+
+
+# A note on choice of discovery mechanisms: Hyperion supports both Zeroconf and SSDP out
+# of the box. This config flow needs two port numbers from the Hyperion instance, the
+# JSON port (for the API) and the UI port (for the user to approve dynamically created
+# auth tokens). With Zeroconf the port numbers for both are in different Zeroconf
+# entries, and as Home Assistant only passes a single entry into the config flow, we can
+# only conveniently 'see' one port or the other (which means we need to guess one port
+# number). With SSDP, we get the combined block including both port numbers, so SSDP is
+# the favored discovery implementation.
+
+
+class HyperionConfigFlow(ConfigFlow, domain=DOMAIN):
+ """Handle a Hyperion config flow."""
+
+ VERSION = 1
+ CONNECTION_CLASS = CONN_CLASS_LOCAL_PUSH
+
+ def __init__(self) -> None:
+ """Instantiate config flow."""
+ self._data: Dict[str, Any] = {}
+ self._request_token_task: Optional[asyncio.Task] = None
+ self._auth_id: Optional[str] = None
+ self._require_confirm: bool = False
+ self._port_ui: int = const.DEFAULT_PORT_UI
+
+ def _create_client(self, raw_connection: bool = False) -> client.HyperionClient:
+ """Create and connect a client instance."""
+ return create_hyperion_client(
+ self._data[CONF_HOST],
+ self._data[CONF_PORT],
+ token=self._data.get(CONF_TOKEN),
+ raw_connection=raw_connection,
+ )
+
+ async def _advance_to_auth_step_if_necessary(
+ self, hyperion_client: client.HyperionClient
+ ) -> Dict[str, Any]:
+ """Determine if auth is required."""
+ auth_resp = await hyperion_client.async_is_auth_required()
+
+ # Could not determine if auth is required.
+ if not auth_resp or not client.ResponseOK(auth_resp):
+ return self.async_abort(reason="auth_required_error")
+ auth_required = auth_resp.get(const.KEY_INFO, {}).get(const.KEY_REQUIRED, False)
+ if auth_required:
+ return await self.async_step_auth()
+ return await self.async_step_confirm()
+
+ async def async_step_import(self, import_data: ConfigType) -> Dict[str, Any]:
+ """Handle a flow initiated by a YAML config import."""
+ self._data.update(import_data)
+ async with self._create_client(raw_connection=True) as hyperion_client:
+ if not hyperion_client:
+ return self.async_abort(reason="cannot_connect")
+ return await self._advance_to_auth_step_if_necessary(hyperion_client)
+
+ async def async_step_ssdp( # type: ignore[override]
+ self, discovery_info: Dict[str, Any]
+ ) -> Dict[str, Any]:
+ """Handle a flow initiated by SSDP."""
+ # Sample data provided by SSDP: {
+ # 'ssdp_location': 'http://192.168.0.1:8090/description.xml',
+ # 'ssdp_st': 'upnp:rootdevice',
+ # 'deviceType': 'urn:schemas-upnp-org:device:Basic:1',
+ # 'friendlyName': 'Hyperion (192.168.0.1)',
+ # 'manufacturer': 'Hyperion Open Source Ambient Lighting',
+ # 'manufacturerURL': 'https://www.hyperion-project.org',
+ # 'modelDescription': 'Hyperion Open Source Ambient Light',
+ # 'modelName': 'Hyperion',
+ # 'modelNumber': '2.0.0-alpha.8',
+ # 'modelURL': 'https://www.hyperion-project.org',
+ # 'serialNumber': 'f9aab089-f85a-55cf-b7c1-222a72faebe9',
+ # 'UDN': 'uuid:f9aab089-f85a-55cf-b7c1-222a72faebe9',
+ # 'ports': {
+ # 'jsonServer': '19444',
+ # 'sslServer': '8092',
+ # 'protoBuffer': '19445',
+ # 'flatBuffer': '19400'
+ # },
+ # 'presentationURL': 'index.html',
+ # 'iconList': {
+ # 'icon': {
+ # 'mimetype': 'image/png',
+ # 'height': '100',
+ # 'width': '100',
+ # 'depth': '32',
+ # 'url': 'img/hyperion/ssdp_icon.png'
+ # }
+ # },
+ # 'ssdp_usn': 'uuid:f9aab089-f85a-55cf-b7c1-222a72faebe9',
+ # 'ssdp_ext': '',
+ # 'ssdp_server': 'Raspbian GNU/Linux 10 (buster)/10 UPnP/1.0 Hyperion/2.0.0-alpha.8'}
+
+ # SSDP requires user confirmation.
+ self._require_confirm = True
+ self._data[CONF_HOST] = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname
+ try:
+ self._port_ui = urlparse(discovery_info[ATTR_SSDP_LOCATION]).port
+ except ValueError:
+ self._port_ui = const.DEFAULT_PORT_UI
+
+ try:
+ self._data[CONF_PORT] = int(
+ discovery_info.get("ports", {}).get(
+ "jsonServer", const.DEFAULT_PORT_JSON
+ )
+ )
+ except ValueError:
+ self._data[CONF_PORT] = const.DEFAULT_PORT_JSON
+
+ hyperion_id = discovery_info.get(ATTR_UPNP_SERIAL)
+ if not hyperion_id:
+ return self.async_abort(reason="no_id")
+
+ # For discovery mechanisms, we set the unique_id as early as possible to
+ # avoid discovery popping up a duplicate on the screen. The unique_id is set
+ # authoritatively later in the flow by asking the server to confirm its id
+ # (which should theoretically be the same as specified here)
+ await self.async_set_unique_id(hyperion_id)
+ self._abort_if_unique_id_configured()
+
+ async with self._create_client(raw_connection=True) as hyperion_client:
+ if not hyperion_client:
+ return self.async_abort(reason="cannot_connect")
+ return await self._advance_to_auth_step_if_necessary(hyperion_client)
+
+ # pylint: disable=arguments-differ
+ async def async_step_user(
+ self,
+ user_input: Optional[ConfigType] = None,
+ ) -> Dict[str, Any]:
+ """Handle a flow initiated by the user."""
+ errors = {}
+ if user_input:
+ self._data.update(user_input)
+
+ async with self._create_client(raw_connection=True) as hyperion_client:
+ if hyperion_client:
+ return await self._advance_to_auth_step_if_necessary(
+ hyperion_client
+ )
+ errors[CONF_BASE] = "cannot_connect"
+
+ return self.async_show_form(
+ step_id="user",
+ data_schema=vol.Schema(
+ {
+ vol.Required(CONF_HOST): str,
+ vol.Optional(CONF_PORT, default=const.DEFAULT_PORT_JSON): int,
+ }
+ ),
+ errors=errors,
+ )
+
+ async def _cancel_request_token_task(self) -> None:
+ """Cancel the request token task if it exists."""
+ if self._request_token_task is not None:
+ if not self._request_token_task.done():
+ self._request_token_task.cancel()
+
+ try:
+ await self._request_token_task
+ except asyncio.CancelledError:
+ pass
+ self._request_token_task = None
+
+ async def _request_token_task_func(self, auth_id: str) -> None:
+ """Send an async_request_token request."""
+ auth_resp: Optional[Dict[str, Any]] = None
+ async with self._create_client(raw_connection=True) as hyperion_client:
+ if hyperion_client:
+ # The Hyperion-py client has a default timeout of 3 minutes on this request.
+ auth_resp = await hyperion_client.async_request_token(
+ comment=DEFAULT_ORIGIN, id=auth_id
+ )
+ assert self.hass
+ await self.hass.config_entries.flow.async_configure(
+ flow_id=self.flow_id, user_input=auth_resp
+ )
+
+ def _get_hyperion_url(self) -> str:
+ """Return the URL of the Hyperion UI."""
+ # If this flow was kicked off by SSDP, this will be the correct frontend URL. If
+ # this is a manual flow instantiation, then it will be a best guess (as this
+ # flow does not have that information available to it). This is only used for
+ # approving new dynamically created tokens, so the complexity of asking the user
+ # manually for this information is likely not worth it (when it would only be
+ # used to open a URL, that the user already knows the address of).
+ return f"http://{self._data[CONF_HOST]}:{self._port_ui}"
+
+ async def _can_login(self) -> Optional[bool]:
+ """Verify login details."""
+ async with self._create_client(raw_connection=True) as hyperion_client:
+ if not hyperion_client:
+ return None
+ return bool(
+ client.LoginResponseOK(
+ await hyperion_client.async_login(token=self._data[CONF_TOKEN])
+ )
+ )
+
+ async def async_step_auth(
+ self,
+ user_input: Optional[ConfigType] = None,
+ ) -> Dict[str, Any]:
+ """Handle the auth step of a flow."""
+ errors = {}
+ if user_input:
+ if user_input.get(CONF_CREATE_TOKEN):
+ return await self.async_step_create_token()
+
+ # Using a static token.
+ self._data[CONF_TOKEN] = user_input.get(CONF_TOKEN)
+ login_ok = await self._can_login()
+ if login_ok is None:
+ return self.async_abort(reason="cannot_connect")
+ if login_ok:
+ return await self.async_step_confirm()
+ errors[CONF_BASE] = "invalid_access_token"
+
+ return self.async_show_form(
+ step_id="auth",
+ data_schema=vol.Schema(
+ {
+ vol.Required(CONF_CREATE_TOKEN): bool,
+ vol.Optional(CONF_TOKEN): str,
+ }
+ ),
+ errors=errors,
+ )
+
+ async def async_step_create_token(
+ self, user_input: Optional[ConfigType] = None
+ ) -> Dict[str, Any]:
+ """Send a request for a new token."""
+ if user_input is None:
+ self._auth_id = client.generate_random_auth_id()
+ return self.async_show_form(
+ step_id="create_token",
+ description_placeholders={
+ CONF_AUTH_ID: self._auth_id,
+ },
+ )
+
+ # Cancel the request token task if it's already running, then re-create it.
+ await self._cancel_request_token_task()
+ # Start a task in the background requesting a new token. The next step will
+ # wait on the response (which includes the user needing to visit the Hyperion
+ # UI to approve the request for a new token).
+ assert self.hass
+ assert self._auth_id is not None
+ self._request_token_task = self.hass.async_create_task(
+ self._request_token_task_func(self._auth_id)
+ )
+ return self.async_external_step(
+ step_id="create_token_external", url=self._get_hyperion_url()
+ )
+
+ async def async_step_create_token_external(
+ self, auth_resp: Optional[ConfigType] = None
+ ) -> Dict[str, Any]:
+ """Handle completion of the request for a new token."""
+ if auth_resp is not None and client.ResponseOK(auth_resp):
+ token = auth_resp.get(const.KEY_INFO, {}).get(const.KEY_TOKEN)
+ if token:
+ self._data[CONF_TOKEN] = token
+ return self.async_external_step_done(
+ next_step_id="create_token_success"
+ )
+ return self.async_external_step_done(next_step_id="create_token_fail")
+
+ async def async_step_create_token_success(
+ self, _: Optional[ConfigType] = None
+ ) -> Dict[str, Any]:
+ """Create an entry after successful token creation."""
+ # Clean-up the request task.
+ await self._cancel_request_token_task()
+
+ # Test the token.
+ login_ok = await self._can_login()
+
+ if login_ok is None:
+ return self.async_abort(reason="cannot_connect")
+ if not login_ok:
+ return self.async_abort(reason="auth_new_token_not_work_error")
+ return await self.async_step_confirm()
+
+ async def async_step_create_token_fail(
+ self, _: Optional[ConfigType] = None
+ ) -> Dict[str, Any]:
+ """Show an error on the auth form."""
+ # Clean-up the request task.
+ await self._cancel_request_token_task()
+ return self.async_abort(reason="auth_new_token_not_granted_error")
+
+ async def async_step_confirm(
+ self, user_input: Optional[ConfigType] = None
+ ) -> Dict[str, Any]:
+ """Get final confirmation before entry creation."""
+ if user_input is None and self._require_confirm:
+ return self.async_show_form(
+ step_id="confirm",
+ description_placeholders={
+ CONF_HOST: self._data[CONF_HOST],
+ CONF_PORT: self._data[CONF_PORT],
+ CONF_ID: self.unique_id,
+ },
+ )
+
+ async with self._create_client() as hyperion_client:
+ if not hyperion_client:
+ return self.async_abort(reason="cannot_connect")
+ hyperion_id = await hyperion_client.async_sysinfo_id()
+
+ if not hyperion_id:
+ return self.async_abort(reason="no_id")
+
+ await self.async_set_unique_id(hyperion_id, raise_on_progress=False)
+ self._abort_if_unique_id_configured()
+
+ # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
+ return self.async_create_entry(
+ title=f"{self._data[CONF_HOST]}:{self._data[CONF_PORT]}", data=self._data
+ )
+
+ @staticmethod
+ @callback
+ def async_get_options_flow(config_entry: ConfigEntry) -> HyperionOptionsFlow:
+ """Get the Hyperion Options flow."""
+ return HyperionOptionsFlow(config_entry)
+
+
+class HyperionOptionsFlow(OptionsFlow):
+ """Hyperion options flow."""
+
+ def __init__(self, config_entry: ConfigEntry):
+ """Initialize a Hyperion options flow."""
+ self._config_entry = config_entry
+
+ async def async_step_init(
+ self, user_input: Optional[Dict[str, Any]] = None
+ ) -> Dict[str, Any]:
+ """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_PRIORITY,
+ default=self._config_entry.options.get(
+ CONF_PRIORITY, DEFAULT_PRIORITY
+ ),
+ ): vol.All(vol.Coerce(int), vol.Range(min=0, max=255)),
+ }
+ ),
+ )
diff --git a/homeassistant/components/hyperion/const.py b/homeassistant/components/hyperion/const.py
new file mode 100644
index 00000000000..9875f3bd918
--- /dev/null
+++ b/homeassistant/components/hyperion/const.py
@@ -0,0 +1,24 @@
+"""Constants for Hyperion integration."""
+DOMAIN = "hyperion"
+
+DEFAULT_NAME = "Hyperion"
+DEFAULT_ORIGIN = "Home Assistant"
+DEFAULT_PRIORITY = 128
+
+CONF_AUTH_ID = "auth_id"
+CONF_CREATE_TOKEN = "create_token"
+CONF_INSTANCE = "instance"
+CONF_PRIORITY = "priority"
+
+CONF_ROOT_CLIENT = "ROOT_CLIENT"
+CONF_ON_UNLOAD = "ON_UNLOAD"
+
+SIGNAL_INSTANCES_UPDATED = f"{DOMAIN}_instances_updated_signal." "{}"
+SIGNAL_INSTANCE_REMOVED = f"{DOMAIN}_instance_removed_signal." "{}"
+
+SOURCE_IMPORT = "import"
+
+HYPERION_VERSION_WARN_CUTOFF = "2.0.0-alpha.9"
+HYPERION_RELEASES_URL = "https://github.com/hyperion-project/hyperion.ng/releases"
+
+TYPE_HYPERION_LIGHT = "hyperion_light"
diff --git a/homeassistant/components/hyperion/light.py b/homeassistant/components/hyperion/light.py
index e68ad8e31ee..5aa087c0515 100644
--- a/homeassistant/components/hyperion/light.py
+++ b/homeassistant/components/hyperion/light.py
@@ -1,28 +1,59 @@
"""Support for Hyperion-NG remotes."""
+from __future__ import annotations
+
import logging
+import re
+from types import MappingProxyType
+from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Tuple, cast
from hyperion import client, const
import voluptuous as vol
+from homeassistant import data_entry_flow
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_EFFECT,
ATTR_HS_COLOR,
+ DOMAIN as LIGHT_DOMAIN,
PLATFORM_SCHEMA,
SUPPORT_BRIGHTNESS,
SUPPORT_COLOR,
SUPPORT_EFFECT,
LightEntity,
)
-from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_TOKEN
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.dispatcher import (
+ async_dispatcher_connect,
+ async_dispatcher_send,
+)
+from homeassistant.helpers.entity_registry import async_get_registry
+from homeassistant.helpers.typing import (
+ ConfigType,
+ DiscoveryInfoType,
+ HomeAssistantType,
+)
import homeassistant.util.color as color_util
+from . import async_create_connect_hyperion_client, get_hyperion_unique_id
+from .const import (
+ CONF_ON_UNLOAD,
+ CONF_PRIORITY,
+ CONF_ROOT_CLIENT,
+ DEFAULT_ORIGIN,
+ DEFAULT_PRIORITY,
+ DOMAIN,
+ SIGNAL_INSTANCE_REMOVED,
+ SIGNAL_INSTANCES_UPDATED,
+ SOURCE_IMPORT,
+ TYPE_HYPERION_LIGHT,
+)
+
_LOGGER = logging.getLogger(__name__)
CONF_DEFAULT_COLOR = "default_color"
-CONF_PRIORITY = "priority"
CONF_HDMI_PRIORITY = "hdmi_priority"
CONF_EFFECT_LIST = "effect_list"
@@ -35,22 +66,27 @@ CONF_EFFECT_LIST = "effect_list"
# showing a solid color. This is the same method used by WLED.
KEY_EFFECT_SOLID = "Solid"
+KEY_ENTRY_ID_YAML = "YAML"
+
DEFAULT_COLOR = [255, 255, 255]
DEFAULT_BRIGHTNESS = 255
DEFAULT_EFFECT = KEY_EFFECT_SOLID
DEFAULT_NAME = "Hyperion"
-DEFAULT_ORIGIN = "Home Assistant"
-DEFAULT_PORT = 19444
-DEFAULT_PRIORITY = 128
+DEFAULT_PORT = const.DEFAULT_PORT_JSON
DEFAULT_HDMI_PRIORITY = 880
-DEFAULT_EFFECT_LIST = []
+DEFAULT_EFFECT_LIST: List[str] = []
SUPPORT_HYPERION = SUPPORT_COLOR | SUPPORT_BRIGHTNESS | SUPPORT_EFFECT
+# Usage of YAML for configuration of the Hyperion component is deprecated.
PLATFORM_SCHEMA = vol.All(
- cv.deprecated(CONF_HDMI_PRIORITY, invalidation_version="0.118"),
- cv.deprecated(CONF_DEFAULT_COLOR, invalidation_version="0.118"),
- cv.deprecated(CONF_EFFECT_LIST, invalidation_version="0.118"),
+ cv.deprecated(CONF_HDMI_PRIORITY),
+ cv.deprecated(CONF_HOST),
+ cv.deprecated(CONF_PORT),
+ cv.deprecated(CONF_DEFAULT_COLOR),
+ cv.deprecated(CONF_NAME),
+ cv.deprecated(CONF_PRIORITY),
+ cv.deprecated(CONF_EFFECT_LIST),
PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_HOST): cv.string,
@@ -77,96 +113,277 @@ ICON_EFFECT = "mdi:lava-lamp"
ICON_EXTERNAL_SOURCE = "mdi:television-ambient-light"
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Set up a Hyperion server remote."""
- name = config[CONF_NAME]
+async def async_setup_platform(
+ hass: HomeAssistantType,
+ config: ConfigType,
+ async_add_entities: Callable,
+ discovery_info: Optional[DiscoveryInfoType] = None,
+) -> None:
+ """Set up Hyperion platform.."""
+
+ # This is the entrypoint for the old YAML-style Hyperion integration. The goal here
+ # is to auto-convert the YAML configuration into a config entry, with no human
+ # interaction, preserving the entity_id. This should be possible, as the YAML
+ # configuration did not support any of the things that should otherwise require
+ # human interaction in the config flow (e.g. it did not support auth).
+
host = config[CONF_HOST]
port = config[CONF_PORT]
- priority = config[CONF_PRIORITY]
+ instance = 0 # YAML only supports a single instance.
- hyperion_client = client.HyperionClient(host, port)
-
- if not await hyperion_client.async_client_connect():
+ # First, connect to the server and get the server id (which will be unique_id on a config_entry
+ # if there is one).
+ hyperion_client = await async_create_connect_hyperion_client(host, port)
+ if not hyperion_client:
+ raise PlatformNotReady
+ hyperion_id = await hyperion_client.async_sysinfo_id()
+ if not hyperion_id:
raise PlatformNotReady
- async_add_entities([Hyperion(name, priority, hyperion_client)])
+ future_unique_id = get_hyperion_unique_id(
+ hyperion_id, instance, TYPE_HYPERION_LIGHT
+ )
+
+ # Possibility 1: Already converted.
+ # There is already a config entry with the unique id reporting by the
+ # server. Nothing to do here.
+ for entry in hass.config_entries.async_entries(domain=DOMAIN):
+ if entry.unique_id == hyperion_id:
+ return
+
+ # Possibility 2: Upgraded to the new Hyperion component pre-config-flow.
+ # No config entry for this unique_id, but have an entity_registry entry
+ # with an old-style unique_id:
+ # :- (instance will always be 0, as YAML
+ # configuration does not support multiple
+ # instances)
+ # The unique_id needs to be updated, then the config_flow should do the rest.
+ registry = await async_get_registry(hass)
+ for entity_id, entity in registry.entities.items():
+ if entity.config_entry_id is not None or entity.platform != DOMAIN:
+ continue
+ result = re.search(rf"([^:]+):(\d+)-{instance}", entity.unique_id)
+ if result and result.group(1) == host and int(result.group(2)) == port:
+ registry.async_update_entity(entity_id, new_unique_id=future_unique_id)
+ break
+ else:
+ # Possibility 3: This is the first upgrade to the new Hyperion component.
+ # No config entry and no entity_registry entry, in which case the CONF_NAME
+ # variable will be used as the preferred name. Rather than pollute the config
+ # entry with a "suggested name" type variable, instead create an entry in the
+ # registry that will subsequently be used when the entity is created with this
+ # unique_id.
+
+ # This also covers the case that should not occur in the wild (no config entry,
+ # but new style unique_id).
+ registry.async_get_or_create(
+ domain=LIGHT_DOMAIN,
+ platform=DOMAIN,
+ unique_id=future_unique_id,
+ suggested_object_id=config[CONF_NAME],
+ )
+
+ async def migrate_yaml_to_config_entry_and_options(
+ host: str, port: int, priority: int
+ ) -> None:
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_IMPORT},
+ data={
+ CONF_HOST: host,
+ CONF_PORT: port,
+ },
+ )
+ if (
+ result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ or result.get("result") is None
+ ):
+ _LOGGER.warning(
+ "Could not automatically migrate Hyperion YAML to a config entry."
+ )
+ return
+ config_entry = result.get("result")
+ options = {**config_entry.options, CONF_PRIORITY: config[CONF_PRIORITY]}
+ hass.config_entries.async_update_entry(config_entry, options=options)
+
+ _LOGGER.info(
+ "Successfully migrated Hyperion YAML configuration to a config entry."
+ )
+
+ # Kick off a config flow to create the config entry.
+ hass.async_create_task(
+ migrate_yaml_to_config_entry_and_options(host, port, config[CONF_PRIORITY])
+ )
-class Hyperion(LightEntity):
+async def async_setup_entry(
+ hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable
+) -> bool:
+ """Set up a Hyperion platform from config entry."""
+ host = config_entry.data[CONF_HOST]
+ port = config_entry.data[CONF_PORT]
+ token = config_entry.data.get(CONF_TOKEN)
+
+ async def async_instances_to_entities(response: Dict[str, Any]) -> None:
+ if not response or const.KEY_DATA not in response:
+ return
+ await async_instances_to_entities_raw(response[const.KEY_DATA])
+
+ async def async_instances_to_entities_raw(instances: List[Dict[str, Any]]) -> None:
+ registry = await async_get_registry(hass)
+ entities_to_add: List[HyperionLight] = []
+ desired_unique_ids: Set[str] = set()
+ server_id = cast(str, config_entry.unique_id)
+
+ # In practice, an instance can be in 3 states as seen by this function:
+ #
+ # * Exists, and is running: Add it to hass.
+ # * Exists, but is not running: Cannot add yet, but should not delete it either.
+ # It will show up as "unavailable".
+ # * No longer exists: Delete it from hass.
+
+ # Add instances that are missing.
+ for instance in instances:
+ instance_id = instance.get(const.KEY_INSTANCE)
+ if instance_id is None or not instance.get(const.KEY_RUNNING, False):
+ continue
+ unique_id = get_hyperion_unique_id(
+ server_id, instance_id, TYPE_HYPERION_LIGHT
+ )
+ desired_unique_ids.add(unique_id)
+ if unique_id in current_entities:
+ continue
+ hyperion_client = await async_create_connect_hyperion_client(
+ host, port, instance=instance_id, token=token
+ )
+ if not hyperion_client:
+ continue
+ current_entities.add(unique_id)
+ entities_to_add.append(
+ HyperionLight(
+ unique_id,
+ instance.get(const.KEY_FRIENDLY_NAME, DEFAULT_NAME),
+ config_entry.options,
+ hyperion_client,
+ )
+ )
+
+ # Delete instances that are no longer present on this server.
+ for unique_id in current_entities - desired_unique_ids:
+ current_entities.remove(unique_id)
+ async_dispatcher_send(hass, SIGNAL_INSTANCE_REMOVED.format(unique_id))
+ entity_id = registry.async_get_entity_id(LIGHT_DOMAIN, DOMAIN, unique_id)
+ if entity_id:
+ registry.async_remove(entity_id)
+
+ async_add_entities(entities_to_add)
+
+ # Readability note: This variable is kept alive in the context of the callback to
+ # async_instances_to_entities below.
+ current_entities: Set[str] = set()
+
+ await async_instances_to_entities_raw(
+ hass.data[DOMAIN][config_entry.entry_id][CONF_ROOT_CLIENT].instances,
+ )
+ hass.data[DOMAIN][config_entry.entry_id][CONF_ON_UNLOAD].append(
+ async_dispatcher_connect(
+ hass,
+ SIGNAL_INSTANCES_UPDATED.format(config_entry.entry_id),
+ async_instances_to_entities,
+ )
+ )
+ return True
+
+
+class HyperionLight(LightEntity):
"""Representation of a Hyperion remote."""
- def __init__(self, name, priority, hyperion_client):
+ def __init__(
+ self,
+ unique_id: str,
+ name: str,
+ options: MappingProxyType[str, Any],
+ hyperion_client: client.HyperionClient,
+ ) -> None:
"""Initialize the light."""
+ self._unique_id = unique_id
self._name = name
- self._priority = priority
+ self._options = options
self._client = hyperion_client
# Active state representing the Hyperion instance.
- self._set_internal_state(
- brightness=255, rgb_color=DEFAULT_COLOR, effect=KEY_EFFECT_SOLID
- )
- self._effect_list = []
+ self._brightness: int = 255
+ self._rgb_color: Sequence[int] = DEFAULT_COLOR
+ self._effect: str = KEY_EFFECT_SOLID
+ self._icon: str = ICON_LIGHTBULB
+
+ self._effect_list: List[str] = []
@property
- def should_poll(self):
+ def should_poll(self) -> bool:
"""Return whether or not this entity should be polled."""
return False
@property
- def name(self):
+ def name(self) -> str:
"""Return the name of the light."""
return self._name
@property
- def brightness(self):
+ def brightness(self) -> int:
"""Return the brightness of this light between 0..255."""
return self._brightness
@property
- def hs_color(self):
+ def hs_color(self) -> Tuple[float, float]:
"""Return last color value set."""
return color_util.color_RGB_to_hs(*self._rgb_color)
@property
- def is_on(self):
+ def is_on(self) -> bool:
"""Return true if not black."""
- return self._client.is_on()
+ return bool(self._client.is_on()) and self._client.visible_priority is not None
@property
- def icon(self):
+ def icon(self) -> str:
"""Return state specific icon."""
return self._icon
@property
- def effect(self):
+ def effect(self) -> str:
"""Return the current effect."""
return self._effect
@property
- def effect_list(self):
+ def effect_list(self) -> List[str]:
"""Return the list of supported effects."""
return (
self._effect_list
- + const.KEY_COMPONENTID_EXTERNAL_SOURCES
+ + list(const.KEY_COMPONENTID_EXTERNAL_SOURCES)
+ [KEY_EFFECT_SOLID]
)
@property
- def supported_features(self):
+ def supported_features(self) -> int:
"""Flag supported features."""
return SUPPORT_HYPERION
@property
- def available(self):
+ def available(self) -> bool:
"""Return server availability."""
- return self._client.has_loaded_state
+ return bool(self._client.has_loaded_state)
@property
- def unique_id(self):
+ def unique_id(self) -> str:
"""Return a unique id for this instance."""
- return self._client.id
+ return self._unique_id
- async def async_turn_on(self, **kwargs):
+ def _get_option(self, key: str) -> Any:
+ """Get a value from the provided options."""
+ defaults = {CONF_PRIORITY: DEFAULT_PRIORITY}
+ return self._options.get(key, defaults[key])
+
+ async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the lights on."""
# == Turn device on ==
# Turn on both ALL (Hyperion itself) and LEDDEVICE. It would be
@@ -196,7 +413,11 @@ class Hyperion(LightEntity):
# == Get key parameters ==
brightness = kwargs.get(ATTR_BRIGHTNESS, self._brightness)
- effect = kwargs.get(ATTR_EFFECT, self._effect)
+ if ATTR_EFFECT not in kwargs and ATTR_HS_COLOR in kwargs:
+ effect = KEY_EFFECT_SOLID
+ else:
+ effect = kwargs.get(ATTR_EFFECT, self._effect)
+ rgb_color: Sequence[int]
if ATTR_HS_COLOR in kwargs:
rgb_color = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR])
else:
@@ -220,7 +441,7 @@ class Hyperion(LightEntity):
# Clear any color/effect.
if not await self._client.async_send_clear(
- **{const.KEY_PRIORITY: self._priority}
+ **{const.KEY_PRIORITY: self._get_option(CONF_PRIORITY)}
):
return
@@ -241,13 +462,13 @@ class Hyperion(LightEntity):
# This call should not be necessary, but without it there is no priorities-update issued:
# https://github.com/hyperion-project/hyperion.ng/issues/992
if not await self._client.async_send_clear(
- **{const.KEY_PRIORITY: self._priority}
+ **{const.KEY_PRIORITY: self._get_option(CONF_PRIORITY)}
):
return
if not await self._client.async_send_set_effect(
**{
- const.KEY_PRIORITY: self._priority,
+ const.KEY_PRIORITY: self._get_option(CONF_PRIORITY),
const.KEY_EFFECT: {const.KEY_NAME: effect},
const.KEY_ORIGIN: DEFAULT_ORIGIN,
}
@@ -257,14 +478,14 @@ class Hyperion(LightEntity):
else:
if not await self._client.async_send_set_color(
**{
- const.KEY_PRIORITY: self._priority,
+ const.KEY_PRIORITY: self._get_option(CONF_PRIORITY),
const.KEY_COLOR: rgb_color,
const.KEY_ORIGIN: DEFAULT_ORIGIN,
}
):
return
- async def async_turn_off(self, **kwargs):
+ async def async_turn_off(self, **kwargs: Any) -> None:
"""Disable the LED output component."""
if not await self._client.async_send_set_component(
**{
@@ -276,7 +497,12 @@ class Hyperion(LightEntity):
):
return
- def _set_internal_state(self, brightness=None, rgb_color=None, effect=None):
+ def _set_internal_state(
+ self,
+ brightness: Optional[int] = None,
+ rgb_color: Optional[Sequence[int]] = None,
+ effect: Optional[str] = None,
+ ) -> None:
"""Set the internal state."""
if brightness is not None:
self._brightness = brightness
@@ -291,11 +517,11 @@ class Hyperion(LightEntity):
else:
self._icon = ICON_EFFECT
- def _update_components(self, _=None):
+ def _update_components(self, _: Optional[Dict[str, Any]] = None) -> None:
"""Update Hyperion components."""
self.async_write_ha_state()
- def _update_adjustment(self, _=None):
+ def _update_adjustment(self, _: Optional[Dict[str, Any]] = None) -> None:
"""Update Hyperion adjustments."""
if self._client.adjustment:
brightness_pct = self._client.adjustment[0].get(
@@ -308,7 +534,7 @@ class Hyperion(LightEntity):
)
self.async_write_ha_state()
- def _update_priorities(self, _=None):
+ def _update_priorities(self, _: Optional[Dict[str, Any]] = None) -> None:
"""Update Hyperion priorities."""
visible_priority = self._client.visible_priority
if visible_priority:
@@ -326,13 +552,13 @@ class Hyperion(LightEntity):
rgb_color=visible_priority[const.KEY_VALUE][const.KEY_RGB],
effect=KEY_EFFECT_SOLID,
)
- self.async_write_ha_state()
+ self.async_write_ha_state()
- def _update_effect_list(self, _=None):
+ def _update_effect_list(self, _: Optional[Dict[str, Any]] = None) -> None:
"""Update Hyperion effects."""
if not self._client.effects:
return
- effect_list = []
+ effect_list: List[str] = []
for effect in self._client.effects or []:
if const.KEY_NAME in effect:
effect_list.append(effect[const.KEY_NAME])
@@ -340,7 +566,7 @@ class Hyperion(LightEntity):
self._effect_list = effect_list
self.async_write_ha_state()
- def _update_full_state(self):
+ def _update_full_state(self) -> None:
"""Update full Hyperion state."""
self._update_adjustment()
self._update_priorities()
@@ -356,12 +582,21 @@ class Hyperion(LightEntity):
self._rgb_color,
)
- def _update_client(self, json):
+ def _update_client(self, _: Optional[Dict[str, Any]] = None) -> None:
"""Update client connection state."""
self.async_write_ha_state()
- async def async_added_to_hass(self):
+ async def async_added_to_hass(self) -> None:
"""Register callbacks when entity added to hass."""
+ assert self.hass
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass,
+ SIGNAL_INSTANCE_REMOVED.format(self._unique_id),
+ self.async_remove,
+ )
+ )
+
self._client.set_callbacks(
{
f"{const.KEY_ADJUSTMENT}-{const.KEY_UPDATE}": self._update_adjustment,
@@ -374,4 +609,7 @@ class Hyperion(LightEntity):
# Load initial state.
self._update_full_state()
- return True
+
+ async def async_will_remove_from_hass(self) -> None:
+ """Disconnect from server."""
+ await self._client.async_client_disconnect()
diff --git a/homeassistant/components/hyperion/manifest.json b/homeassistant/components/hyperion/manifest.json
index 4a9bf2ada8c..d8c6a2c352e 100644
--- a/homeassistant/components/hyperion/manifest.json
+++ b/homeassistant/components/hyperion/manifest.json
@@ -1,7 +1,18 @@
{
+ "codeowners": [
+ "@dermotduffy"
+ ],
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integrations/hyperion",
"domain": "hyperion",
"name": "Hyperion",
- "documentation": "https://www.home-assistant.io/integrations/hyperion",
- "requirements": ["hyperion-py==0.3.0"],
- "codeowners": ["@dermotduffy"]
-}
+ "requirements": [
+ "hyperion-py==0.6.0"
+ ],
+ "ssdp": [
+ {
+ "manufacturer": "Hyperion Open Source Ambient Lighting",
+ "st": "urn:hyperion-project.org:device:basic:1"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/homeassistant/components/hyperion/strings.json b/homeassistant/components/hyperion/strings.json
new file mode 100644
index 00000000000..180f266f1af
--- /dev/null
+++ b/homeassistant/components/hyperion/strings.json
@@ -0,0 +1,52 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "host": "[%key:common::config_flow::data::host%]",
+ "port": "[%key:common::config_flow::data::port%]"
+ }
+ },
+ "auth": {
+ "description": "Configure authorization to your Hyperion Ambilight server",
+ "data": {
+ "create_token": "Automatically create new token",
+ "token": "Or provide pre-existing token"
+ }
+ },
+ "create_token": {
+ "description": "Choose **Submit** below to request a new authentication token. You will be redirected to the Hyperion UI to approve the request. Please verify the shown id is \"{auth_id}\"",
+ "title": "Automatically create new authentication token"
+ },
+ "create_token_external": {
+ "title": "Accept new token in Hyperion UI"
+ },
+ "confirm": {
+ "description": "Do you want to add this Hyperion Ambilight to Home Assistant?\n\n**Host:** {host}\n**Port:** {port}\n**ID**: {id}",
+ "title": "Confirm addition of Hyperion Ambilight service"
+ }
+ },
+ "error": {
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]"
+ },
+ "abort": {
+ "auth_required_error": "Failed to determine if authorization is required",
+ "already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
+ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "auth_new_token_not_granted_error": "Newly created token was not approved on Hyperion UI",
+ "auth_new_token_not_work_error": "Failed to authenticate using newly created token",
+ "no_id": "The Hyperion Ambilight instance did not report its id"
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "priority": "Hyperion priority to use for colors and effects"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hyperion/translations/cs.json b/homeassistant/components/hyperion/translations/cs.json
new file mode 100644
index 00000000000..c5358988bac
--- /dev/null
+++ b/homeassistant/components/hyperion/translations/cs.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Slu\u017eba je ji\u017e nastavena",
+ "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1",
+ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit"
+ },
+ "error": {
+ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit",
+ "invalid_access_token": "Neplatn\u00fd p\u0159\u00edstupov\u00fd token"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Hostitel",
+ "port": "Port"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hyperion/translations/en.json b/homeassistant/components/hyperion/translations/en.json
new file mode 100644
index 00000000000..c4c4f512d6f
--- /dev/null
+++ b/homeassistant/components/hyperion/translations/en.json
@@ -0,0 +1,52 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Service is already configured",
+ "already_in_progress": "Configuration flow is already in progress",
+ "auth_new_token_not_granted_error": "Newly created token was not approved on Hyperion UI",
+ "auth_new_token_not_work_error": "Failed to authenticate using newly created token",
+ "auth_required_error": "Failed to determine if authorization is required",
+ "cannot_connect": "Failed to connect",
+ "no_id": "The Hyperion Ambilight instance did not report its id"
+ },
+ "error": {
+ "cannot_connect": "Failed to connect",
+ "invalid_access_token": "Invalid access token"
+ },
+ "step": {
+ "auth": {
+ "data": {
+ "create_token": "Automatically create new token",
+ "token": "Or provide pre-existing token"
+ },
+ "description": "Configure authorization to your Hyperion Ambilight server"
+ },
+ "confirm": {
+ "description": "Do you want to add this Hyperion Ambilight to Home Assistant?\n\n**Host:** {host}\n**Port:** {port}\n**ID**: {id}",
+ "title": "Confirm addition of Hyperion Ambilight service"
+ },
+ "create_token": {
+ "description": "Choose **Submit** below to request a new authentication token. You will be redirected to the Hyperion UI to approve the request. Please verify the shown id is \"{auth_id}\"",
+ "title": "Automatically create new authentication token"
+ },
+ "create_token_external": {
+ "title": "Accept new token in Hyperion UI"
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Port"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "priority": "Hyperion priority to use for colors and effects"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hyperion/translations/es.json b/homeassistant/components/hyperion/translations/es.json
new file mode 100644
index 00000000000..bb1ef3e2c03
--- /dev/null
+++ b/homeassistant/components/hyperion/translations/es.json
@@ -0,0 +1,52 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El servicio ya est\u00e1 configurado",
+ "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso",
+ "auth_new_token_not_granted_error": "El token reci\u00e9n creado no se aprob\u00f3 en la interfaz de usuario de Hyperion",
+ "auth_new_token_not_work_error": "Error al autenticarse con el token reci\u00e9n creado",
+ "auth_required_error": "No se pudo determinar si se requiere autorizaci\u00f3n",
+ "cannot_connect": "No se pudo conectar",
+ "no_id": "La instancia de Hyperion Ambilight no inform\u00f3 su identificaci\u00f3n"
+ },
+ "error": {
+ "cannot_connect": "No se pudo conectar",
+ "invalid_access_token": "Token de acceso no v\u00e1lido"
+ },
+ "step": {
+ "auth": {
+ "data": {
+ "create_token": "Crea un nuevo token autom\u00e1ticamente",
+ "token": "O proporcionar un token preexistente"
+ },
+ "description": "Configurar autorizaci\u00f3n a tu servidor Hyperion Ambilight"
+ },
+ "confirm": {
+ "description": "\u00bfQuieres a\u00f1adir este Hyperion Ambilight a Home Assistant?\n\n**Host:** {host}\n**Puerto:** {port}\n**Identificaci\u00f3n**: {id}",
+ "title": "Confirmar la adici\u00f3n del servicio Hyperion Ambilight"
+ },
+ "create_token": {
+ "description": "Elige ** Enviar ** a continuaci\u00f3n para solicitar un nuevo token de autenticaci\u00f3n. Se te redirigir\u00e1 a la interfaz de usuario de Hyperion para aprobar la solicitud. Verifica que la identificaci\u00f3n que se muestra sea \"{auth_id}\"",
+ "title": "Crear autom\u00e1ticamente un nuevo token de autenticaci\u00f3n"
+ },
+ "create_token_external": {
+ "title": "Aceptar nuevo token en la interfaz de usuario de Hyperion"
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Puerto"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "priority": "Prioridad de Hyperion a usar para colores y efectos"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hyperion/translations/et.json b/homeassistant/components/hyperion/translations/et.json
new file mode 100644
index 00000000000..e8d6232236b
--- /dev/null
+++ b/homeassistant/components/hyperion/translations/et.json
@@ -0,0 +1,52 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Teenus on juba seadistatud",
+ "already_in_progress": "Seadistamine on juba k\u00e4imas",
+ "auth_new_token_not_granted_error": "\u00c4sja loodud juurdep\u00e4\u00e4sut\u00f5end ei ole Hyperioni oma",
+ "auth_new_token_not_work_error": "Loodud juurdep\u00e4\u00e4sut\u00f5endiga autentimine nurjus",
+ "auth_required_error": "Autoriseerimise vajalikkuse tuvastamine nurjus",
+ "cannot_connect": "\u00dchendamine nurjus",
+ "no_id": "Hyperion Ambilighti eksemplar ei teatanud oma ID-d"
+ },
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus",
+ "invalid_access_token": "Vigane juurdep\u00e4\u00e4sut\u00f5end"
+ },
+ "step": {
+ "auth": {
+ "data": {
+ "create_token": "Loo automaatselt uus juurdep\u00e4\u00e4sut\u00f5end",
+ "token": "V\u00f5i kasuta juba olemasolevat juurdep\u00e4\u00e4sut\u00f5endit"
+ },
+ "description": "Seadista oma Hyperion Ambilighti serveri tuvastamine"
+ },
+ "confirm": {
+ "description": "Kas soovid selle Hyperion Ambilighti lisada Home Assistanti?\n\n**Host:** {host}\n**Port:** {port}\n**ID**: {id}",
+ "title": "Kinnita Hyperion Ambilight teenuse lisamine"
+ },
+ "create_token": {
+ "description": "Uue juurdep\u00e4\u00e4sut\u00f5endi taotlemiseks vali allpool ** Esita **. Taotluse kinnitamiseks suunatakse Hyperioni kasutajaliidesesse. Palun kontrolli kas kuvatud ID on \" {auth_id} \"",
+ "title": "Loo automaatselt uus juurdep\u00e4\u00e4sut\u00f5end"
+ },
+ "create_token_external": {
+ "title": "N\u00f5ustu uue juurdep\u00e4\u00e4sut\u00f5endiga Hyperion UI-s"
+ },
+ "user": {
+ "data": {
+ "host": "",
+ "port": ""
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "priority": "V\u00e4rvide ja efektide puhul on kasutatavad Hyperioni eelistused"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hyperion/translations/lb.json b/homeassistant/components/hyperion/translations/lb.json
new file mode 100644
index 00000000000..6d86d67564c
--- /dev/null
+++ b/homeassistant/components/hyperion/translations/lb.json
@@ -0,0 +1,52 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Service ass scho konfigur\u00e9iert",
+ "already_in_progress": "Konfiguratioun's Oflaf ass schon am gaang",
+ "auth_new_token_not_granted_error": "Nei erstallte Jeton ass net an der Hyperion UI accord\u00e9iert",
+ "auth_new_token_not_work_error": "Feeler bei der Authentifikatioun mam nei erstallte Jeton",
+ "auth_required_error": "Feeler beim best\u00ebmmen ob Autorisatioun erfuerderlech ass",
+ "cannot_connect": "Feeler beim verbannen",
+ "no_id": "D\u00ebs Hyperion Ambilight Instanz huet seng ID net gemellt."
+ },
+ "error": {
+ "cannot_connect": "Feeler beim verbannen",
+ "invalid_access_token": "Ong\u00ebltegen Acc\u00e8s Jeton"
+ },
+ "step": {
+ "auth": {
+ "data": {
+ "create_token": "Neie Jeton automatesch erstellen",
+ "token": "oder scho bestehenden Jeton uginn"
+ },
+ "description": "Autorisatioun mat dengem Hyperion Ambilight Server konfigur\u00e9ieren"
+ },
+ "confirm": {
+ "description": "Soll d\u00ebsen Hyperion Ambilight am Home Assistant dob\u00e4i gesaat ginn?\n\n**Host:** {host}\n**Port:** {port}\n**ID**: {id}",
+ "title": "Dob\u00e4isetzen vum Hyperion Ambilight Service best\u00e4tegen"
+ },
+ "create_token": {
+ "description": "Klick **Ofsch\u00e9cken** fir een neien Authentifikatioun's Jeton unzefroen. Du g\u00ebss dann an Hyperion UI weidergeleet fir d'Ufro z'accord\u00e9ieren. Iwwerpr\u00e9if dass d\u00e9i ugewisen id \"{auth_id}\" ass.",
+ "title": "Neien Authentifikatioun's Jeton automatesch erstellen"
+ },
+ "create_token_external": {
+ "title": "Neie Jeton an der Hyperion UI accord\u00e9ieren"
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "port": "Port"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "priority": "Hyperion Priorit\u00e9it fir Faarwen an Effekter"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hyperion/translations/no.json b/homeassistant/components/hyperion/translations/no.json
new file mode 100644
index 00000000000..79c90379f18
--- /dev/null
+++ b/homeassistant/components/hyperion/translations/no.json
@@ -0,0 +1,52 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Tjenesten er allerede konfigurert",
+ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede",
+ "auth_new_token_not_granted_error": "Nyopprettet token ble ikke godkjent p\u00e5 Hyperion UI",
+ "auth_new_token_not_work_error": "Kunne ikke godkjenne ved hjelp av nylig opprettet token",
+ "auth_required_error": "Kan ikke fastsl\u00e5 om autorisasjon er n\u00f8dvendig",
+ "cannot_connect": "Tilkobling mislyktes",
+ "no_id": "Hyperion Ambilight-forekomsten rapporterte ikke ID-en"
+ },
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes",
+ "invalid_access_token": "Ugyldig tilgangstoken"
+ },
+ "step": {
+ "auth": {
+ "data": {
+ "create_token": "Opprett nytt token automatisk",
+ "token": "Eller oppgi eksisterende token"
+ },
+ "description": "Konfigurer autorisasjon til Hyperion Ambilight-serveren"
+ },
+ "confirm": {
+ "description": "Vil du legge til denne Hyperion Ambilight i Home Assistant? \n\n ** Vert: ** {host}\n ** Port: ** {port}\n ** ID **: {id}",
+ "title": "Bekreft tillegg av Hyperion Ambilight-tjenesten"
+ },
+ "create_token": {
+ "description": "Velg **Send** nedenfor for \u00e5 be om et nytt godkjenningstoken. Du vil bli omdirigert til Hyperion UI for \u00e5 godkjenne foresp\u00f8rselen. Kontroller at den viste IDen er {auth_id}.",
+ "title": "Opprett nytt godkjenningstoken automatisk"
+ },
+ "create_token_external": {
+ "title": "Godta nytt token i Hyperion UI"
+ },
+ "user": {
+ "data": {
+ "host": "Vert",
+ "port": "Port"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "priority": "Hyperion-prioritet for bruke til farger og effekter"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hyperion/translations/pl.json b/homeassistant/components/hyperion/translations/pl.json
new file mode 100644
index 00000000000..e705d115c8d
--- /dev/null
+++ b/homeassistant/components/hyperion/translations/pl.json
@@ -0,0 +1,52 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana",
+ "already_in_progress": "Konfiguracja jest ju\u017c w toku",
+ "auth_new_token_not_granted_error": "Nowo utworzony token nie zosta\u0142 zatwierdzony w interfejsie u\u017cytkownika Hyperion",
+ "auth_new_token_not_work_error": "Nie uda\u0142o si\u0119 uwierzytelni\u0107 przy u\u017cyciu nowo utworzonego tokena",
+ "auth_required_error": "Nie uda\u0142o si\u0119 okre\u015bli\u0107, czy wymagana jest autoryzacja",
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "no_id": "Instancja Hyperion Ambilight nie zg\u0142osi\u0142a swojego identyfikatora"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_access_token": "Niepoprawny token dost\u0119pu"
+ },
+ "step": {
+ "auth": {
+ "data": {
+ "create_token": "Automatycznie utw\u00f3rz nowy token",
+ "token": "Lub podaj istniej\u0105cy token"
+ },
+ "description": "Skonfiguruj autoryzacj\u0119 do serwera Hyperion Ambilight"
+ },
+ "confirm": {
+ "description": "Czy chcesz doda\u0107 ten Hyperion Ambilight do Home Assistanta? \n\n **Host:** {host}\n **Port:** {port}\n **ID**: {id}",
+ "title": "Potwierdzanie dodania us\u0142ugi Hyperion Ambilight"
+ },
+ "create_token": {
+ "description": "Naci\u015bnij **Prze\u015blij** poni\u017cej, aby za\u017c\u0105da\u0107 nowego tokena uwierzytelniania. Nast\u0105pi przekierowanie do interfejsu u\u017cytkownika Hyperion, aby zatwierdzi\u0107 \u017c\u0105danie. Sprawd\u017a, czy wy\u015bwietlany identyfikator to \u201e {auth_id} \u201d",
+ "title": "Automatyczne tworzenie nowego tokena uwierzytelniaj\u0105cego"
+ },
+ "create_token_external": {
+ "title": "Akceptowanie nowego tokena w interfejsie u\u017cytkownika Hyperion"
+ },
+ "user": {
+ "data": {
+ "host": "Nazwa hosta lub adres IP",
+ "port": "Port"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "priority": "Hyperion ma pierwsze\u0144stwo w u\u017cyciu dla kolor\u00f3w i efekt\u00f3w"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hyperion/translations/ru.json b/homeassistant/components/hyperion/translations/ru.json
new file mode 100644
index 00000000000..fda9ef4bb5b
--- /dev/null
+++ b/homeassistant/components/hyperion/translations/ru.json
@@ -0,0 +1,52 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.",
+ "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.",
+ "auth_new_token_not_granted_error": "\u0421\u043e\u0437\u0434\u0430\u043d\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u043d\u0435 \u0431\u044b\u043b \u043e\u0434\u043e\u0431\u0440\u0435\u043d \u0432 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u043c \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435 Hyperion.",
+ "auth_new_token_not_work_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u0440\u043e\u0439\u0442\u0438 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u043e\u0433\u043e \u0442\u043e\u043a\u0435\u043d\u0430.",
+ "auth_required_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c, \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043b\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "no_id": "Hyperion Ambilight \u043d\u0435 \u0441\u043e\u043e\u0431\u0449\u0438\u043b \u0441\u0432\u043e\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "invalid_access_token": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430."
+ },
+ "step": {
+ "auth": {
+ "data": {
+ "create_token": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0439 \u0442\u043e\u043a\u0435\u043d",
+ "token": "\u0418\u043b\u0438 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u0442\u043e\u043a\u0435\u043d"
+ },
+ "description": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0435 Hyperion Ambilight."
+ },
+ "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 Hyperion Ambilight?\n\n**\u0425\u043e\u0441\u0442:** {host}\n**\u041f\u043e\u0440\u0442:** {port}\n**ID**: {id}",
+ "title": "\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0441\u043b\u0443\u0436\u0431\u044b Hyperion Ambilight"
+ },
+ "create_token": {
+ "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 **\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c** \u043d\u0438\u0436\u0435, \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u043f\u0440\u043e\u0441\u0438\u0442\u044c \u043d\u043e\u0432\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u0412\u044b \u0431\u0443\u0434\u0435\u0442\u0435 \u043f\u0435\u0440\u0435\u043d\u0430\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u044b \u0432 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 Hyperion \u0434\u043b\u044f \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f \u0437\u0430\u043f\u0440\u043e\u0441\u0430. \u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 - \"{auth_id}\"",
+ "title": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438"
+ },
+ "create_token_external": {
+ "title": "\u041f\u0440\u0438\u043d\u044f\u0442\u044c \u043d\u043e\u0432\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0432 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u043c \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435 Hyperion"
+ },
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "port": "\u041f\u043e\u0440\u0442"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "priority": "\u041f\u0440\u0438\u043e\u0440\u0438\u0442\u0435\u0442 Hyperion \u0434\u043b\u044f \u0446\u0432\u0435\u0442\u043e\u0432 \u0438 \u044d\u0444\u0444\u0435\u043a\u0442\u043e\u0432"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hyperion/translations/zh-Hant.json b/homeassistant/components/hyperion/translations/zh-Hant.json
new file mode 100644
index 00000000000..fb9cbe3b7a8
--- /dev/null
+++ b/homeassistant/components/hyperion/translations/zh-Hant.json
@@ -0,0 +1,52 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d",
+ "auth_new_token_not_granted_error": "\u65b0\u5275\u5bc6\u9470\u672a\u7372\u5f97 Hyperion UI \u6838\u51c6",
+ "auth_new_token_not_work_error": "\u4f7f\u7528\u65b0\u5275\u5bc6\u9470\u8a8d\u8b49\u5931\u6557",
+ "auth_required_error": "\u7121\u6cd5\u5224\u5b9a\u662f\u5426\u9700\u8981\u9a57\u8b49",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "no_id": "Hyperion Ambilight \u5be6\u9ad4\u672a\u56de\u5831\u5176 ID"
+ },
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "invalid_access_token": "\u5b58\u53d6\u5bc6\u9470\u7121\u6548"
+ },
+ "step": {
+ "auth": {
+ "data": {
+ "create_token": "\u81ea\u52d5\u65b0\u5275\u5bc6\u9470",
+ "token": "\u6216\u63d0\u4f9b\u73fe\u6709\u5bc6\u9470"
+ },
+ "description": "\u8a2d\u5b9a Hyperion Ambilight \u4f3a\u670d\u5668\u8a8d\u8b49"
+ },
+ "confirm": {
+ "description": "\u662f\u5426\u8981\u5c07 Hyperion Ambilight \u65b0\u589e\u81f3 Home Assistant\uff1f\n\n**\u4e3b\u6a5f\u7aef\uff1a** {host}\n**\u901a\u8a0a\u57e0\uff1a** {port}\n**ID**\uff1a {id}",
+ "title": "\u78ba\u8a8d\u9644\u52a0 Hyperion Ambilight \u670d\u52d9"
+ },
+ "create_token": {
+ "description": "\u9ede\u9078\u4e0b\u65b9 **\u50b3\u9001** \u4ee5\u8acb\u6c42\u65b0\u8a8d\u8b49\u5bc6\u9470\u3002\u5c07\u6703\u91cd\u65b0\u5c0e\u5411\u81f3 Hyperion UI \u4ee5\u6838\u51c6\u8981\u6c42\u3002\u8acb\u78ba\u8a8d\u986f\u793a ID \u70ba \"{auth_id}\"",
+ "title": "\u81ea\u52d5\u65b0\u5275\u8a8d\u8b49\u5bc6\u9470"
+ },
+ "create_token_external": {
+ "title": "\u63a5\u53d7 Hyperion UI \u4e2d\u7684\u65b0\u5bc6\u9470"
+ },
+ "user": {
+ "data": {
+ "host": "\u4e3b\u6a5f\u7aef",
+ "port": "\u901a\u8a0a\u57e0"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "priority": "Hyperion \u512a\u5148\u4f7f\u7528\u4e4b\u8272\u6eab\u8207\u7279\u6548"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/iaqualink/translations/hu.json b/homeassistant/components/iaqualink/translations/hu.json
index dee4ed9ee0f..149fee90583 100644
--- a/homeassistant/components/iaqualink/translations/hu.json
+++ b/homeassistant/components/iaqualink/translations/hu.json
@@ -1,5 +1,8 @@
{
"config": {
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/iaqualink/translations/ka.json b/homeassistant/components/iaqualink/translations/ka.json
new file mode 100644
index 00000000000..552111158b9
--- /dev/null
+++ b/homeassistant/components/iaqualink/translations/ka.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u10e3\u10d9\u10d5\u10d4 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0. \u10e8\u10d4\u10e1\u10d0\u10eb\u10da\u10d4\u10d1\u10d4\u10da\u10d8\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/icloud/translations/hu.json b/homeassistant/components/icloud/translations/hu.json
index e2664659d05..2e820418e94 100644
--- a/homeassistant/components/icloud/translations/hu.json
+++ b/homeassistant/components/icloud/translations/hu.json
@@ -5,6 +5,11 @@
"validate_verification_code": "Nem siker\u00fclt ellen\u0151rizni az ellen\u0151rz\u0151 k\u00f3dot, ki kell v\u00e1lasztania egy megb\u00edzhat\u00f3s\u00e1gi eszk\u00f6zt, \u00e9s \u00fajra kell ind\u00edtania az ellen\u0151rz\u00e9st"
},
"step": {
+ "reauth": {
+ "data": {
+ "password": "Jelsz\u00f3"
+ }
+ },
"trusted_device": {
"data": {
"trusted_device": "Megb\u00edzhat\u00f3 eszk\u00f6z"
diff --git a/homeassistant/components/icloud/translations/ka.json b/homeassistant/components/icloud/translations/ka.json
new file mode 100644
index 00000000000..950b4e2f327
--- /dev/null
+++ b/homeassistant/components/icloud/translations/ka.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "reauth_successful": "\u10e0\u10d0-\u10d0\u10d5\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0 \u10d8\u10e7\u10dd \u10ec\u10d0\u10e0\u10db\u10d0\u10e2\u10d4\u10d1\u10e3\u10da\u10d8"
+ },
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "\u10de\u10d0\u10e0\u10dd\u10da\u10d8"
+ },
+ "description": "\u10e8\u10d4\u10dc\u10d8 \u10d0\u10d3\u10e0\u10d4 \u10e8\u10d4\u10e7\u10d5\u10d0\u10dc\u10d8\u10da\u10d8 \u10de\u10d0\u10e0\u10dd\u10da\u10d8 {username} \u10d0\u10e6\u10d0\u10e0 \u10db\u10e3\u10e8\u10d0\u10dd\u10d1\u10e1. \u10d2\u10d0\u10dc\u10d0\u10d0\u10ee\u10da\u10d4\u10d7 \u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 \u10de\u10d0\u10e0\u10dd\u10da\u10d8, \u10e0\u10dd\u10db \u10d2\u10d0\u10dc\u10d0\u10d2\u10e0\u10eb\u10dd\u10d7 \u10d8\u10dc\u10e2\u10d4\u10d2\u10e0\u10d0\u10ea\u10d8\u10d7 \u10e1\u10d0\u10e0\u10d2\u10d4\u10d1\u10da\u10dd\u10d1\u10d0.",
+ "title": "\u10e0\u10d4\u10d0\u10d5\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d8\u10e1 \u10d8\u10dc\u10e2\u10d4\u10d2\u10e0\u10d0\u10ea\u10d8\u10d0"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ifttt/translations/hu.json b/homeassistant/components/ifttt/translations/hu.json
index a64800ab7db..c3e7007b1a0 100644
--- a/homeassistant/components/ifttt/translations/hu.json
+++ b/homeassistant/components/ifttt/translations/hu.json
@@ -1,5 +1,8 @@
{
"config": {
+ "create_entry": {
+ "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, akkor az \u201eIFTTT Webhook kisalkalmaz\u00e1s\u201d ( {applet_url} ) \"Webk\u00e9r\u00e9s k\u00e9sz\u00edt\u00e9se\" m\u0171velet\u00e9t kell haszn\u00e1lnia. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: \" {webhook_url} \"\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/json \n\n L\u00e1sd [a dokument\u00e1ci\u00f3] ( {docs_url} ), hogyan konfigur\u00e1lhatja az automatiz\u00e1l\u00e1sokat a bej\u00f6v\u0151 adatok kezel\u00e9s\u00e9re."
+ },
"step": {
"user": {
"description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani az IFTTT-t?",
diff --git a/homeassistant/components/ifttt/translations/ka.json b/homeassistant/components/ifttt/translations/ka.json
new file mode 100644
index 00000000000..75c4f0a922c
--- /dev/null
+++ b/homeassistant/components/ifttt/translations/ka.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0. \u10e8\u10d4\u10e1\u10d0\u10eb\u10da\u10d4\u10d1\u10d4\u10da\u10d8\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0.",
+ "webhook_not_internet_accessible": "\u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 Home Assistant \u10d4\u10d9\u10d6\u10d4\u10db\u10de\u10da\u10d0\u10e0\u10d8 \u10e1\u10d0\u10ed\u10d8\u10e0\u10dd\u10e0\u10d4\u10d1\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10dc\u10d4\u10e2\u10d7\u10d0\u10dc \u10ec\u10d5\u10d3\u10dd\u10db\u10d0\u10e1 webhook \u10e8\u10d4\u10e2\u10e7\u10dd\u10d1\u10d8\u10dc\u10d4\u10d1\u10d4\u10d1\u10d8\u10e1 \u10db\u10d8\u10e1\u10d0\u10e6\u10d4\u10d1\u10d0\u10d3."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ifttt/translations/lb.json b/homeassistant/components/ifttt/translations/lb.json
index 56b3ba9ad81..64ef529ebc9 100644
--- a/homeassistant/components/ifttt/translations/lb.json
+++ b/homeassistant/components/ifttt/translations/lb.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech."
+ "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech.",
+ "webhook_not_internet_accessible": "Deng Home Assistant Instanz muss iwwert Internet accessibel si fir Webhook Noriichten z'empf\u00e4nken."
},
"create_entry": {
"default": "Fir Evenementer un Home Assistant ze sch\u00e9ckemusst dir d'Aktioun \"Make a web request\" vum [IFTTT Webhook applet] ({applet_url}) benotzen.\n\nGitt folgend Informatiounen un:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nKuckt iech [Dokumentatioun]({docs_url}) w\u00e9i een Automatisatioune mat empfaangene Donn\u00e9e konfigur\u00e9iert."
diff --git a/homeassistant/components/ifttt/translations/pl.json b/homeassistant/components/ifttt/translations/pl.json
index 7c0dfbfab66..d8d7bff1585 100644
--- a/homeassistant/components/ifttt/translations/pl.json
+++ b/homeassistant/components/ifttt/translations/pl.json
@@ -5,7 +5,7 @@
"webhook_not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty webhook"
},
"create_entry": {
- "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant, musisz u\u017cy\u0107 akcji \"Make a web request\" z [apletu IFTTT Webhook]({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 [apletu IFTTT Webhook]({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_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py
index e95287d2cbe..aa1f0b8814a 100644
--- a/homeassistant/components/input_datetime/__init__.py
+++ b/homeassistant/components/input_datetime/__init__.py
@@ -1,5 +1,5 @@
"""Support to select a date and/or a time."""
-import datetime
+import datetime as py_datetime
import logging
import typing
@@ -33,12 +33,16 @@ CONF_HAS_TIME = "has_time"
CONF_INITIAL = "initial"
DEFAULT_VALUE = "1970-01-01 00:00:00"
-DEFAULT_DATE = datetime.date(1970, 1, 1)
-DEFAULT_TIME = datetime.time(0, 0, 0)
+DEFAULT_DATE = py_datetime.date(1970, 1, 1)
+DEFAULT_TIME = py_datetime.time(0, 0, 0)
ATTR_DATETIME = "datetime"
ATTR_TIMESTAMP = "timestamp"
+FMT_DATE = "%Y-%m-%d"
+FMT_TIME = "%H:%M:%S"
+FMT_DATETIME = f"{FMT_DATE} {FMT_TIME}"
+
def validate_set_datetime_attrs(config):
"""Validate set_datetime service attributes."""
@@ -51,20 +55,6 @@ def validate_set_datetime_attrs(config):
return config
-SERVICE_SET_DATETIME = "set_datetime"
-SERVICE_SET_DATETIME_SCHEMA = vol.All(
- vol.Schema(
- {
- vol.Optional(ATTR_DATE): cv.date,
- vol.Optional(ATTR_TIME): cv.time,
- vol.Optional(ATTR_DATETIME): cv.datetime,
- vol.Optional(ATTR_TIMESTAMP): vol.Coerce(float),
- },
- extra=vol.ALLOW_EXTRA,
- ),
- cv.has_at_least_one_key(ATTR_DATE, ATTR_TIME, ATTR_DATETIME, ATTR_TIMESTAMP),
- validate_set_datetime_attrs,
-)
STORAGE_KEY = DOMAIN
STORAGE_VERSION = 1
@@ -162,31 +152,24 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
schema=RELOAD_SERVICE_SCHEMA,
)
- async def async_set_datetime_service(entity, call):
- """Handle a call to the input datetime 'set datetime' service."""
- date = call.data.get(ATTR_DATE)
- time = call.data.get(ATTR_TIME)
- dttm = call.data.get(ATTR_DATETIME)
- tmsp = call.data.get(ATTR_TIMESTAMP)
-
- if tmsp:
- dttm = dt_util.as_local(dt_util.utc_from_timestamp(tmsp)).replace(
- tzinfo=None
- )
- if dttm:
- date = dttm.date()
- time = dttm.time()
- if not entity.has_date:
- date = None
- if not entity.has_time:
- time = None
- if not date and not time:
- raise vol.Invalid("Nothing to set")
-
- entity.async_set_datetime(date, time)
-
component.async_register_entity_service(
- SERVICE_SET_DATETIME, SERVICE_SET_DATETIME_SCHEMA, async_set_datetime_service
+ "set_datetime",
+ vol.All(
+ vol.Schema(
+ {
+ vol.Optional(ATTR_DATE): cv.date,
+ vol.Optional(ATTR_TIME): cv.time,
+ vol.Optional(ATTR_DATETIME): cv.datetime,
+ vol.Optional(ATTR_TIMESTAMP): vol.Coerce(float),
+ },
+ extra=vol.ALLOW_EXTRA,
+ ),
+ cv.has_at_least_one_key(
+ ATTR_DATE, ATTR_TIME, ATTR_DATETIME, ATTR_TIMESTAMP
+ ),
+ validate_set_datetime_attrs,
+ ),
+ "async_set_datetime",
)
return True
@@ -221,16 +204,31 @@ class InputDatetime(RestoreEntity):
self._config = config
self.editable = True
self._current_datetime = None
+
initial = config.get(CONF_INITIAL)
- if initial:
- if self.has_date and self.has_time:
- self._current_datetime = dt_util.parse_datetime(initial)
- elif self.has_date:
- date = dt_util.parse_date(initial)
- self._current_datetime = datetime.datetime.combine(date, DEFAULT_TIME)
- else:
- time = dt_util.parse_time(initial)
- self._current_datetime = datetime.datetime.combine(DEFAULT_DATE, time)
+ if not initial:
+ return
+
+ if self.has_date and self.has_time:
+ current_datetime = dt_util.parse_datetime(initial)
+
+ elif self.has_date:
+ date = dt_util.parse_date(initial)
+ current_datetime = py_datetime.datetime.combine(date, DEFAULT_TIME)
+
+ else:
+ time = dt_util.parse_time(initial)
+ current_datetime = py_datetime.datetime.combine(DEFAULT_DATE, time)
+
+ # If the user passed in an initial value with a timezone, convert it to right tz
+ if current_datetime.tzinfo is not None:
+ self._current_datetime = current_datetime.astimezone(
+ dt_util.DEFAULT_TIME_ZONE
+ )
+ else:
+ self._current_datetime = dt_util.DEFAULT_TIME_ZONE.localize(
+ current_datetime
+ )
@classmethod
def from_yaml(cls, config: typing.Dict) -> "InputDatetime":
@@ -257,21 +255,27 @@ class InputDatetime(RestoreEntity):
if self.has_date and self.has_time:
date_time = dt_util.parse_datetime(old_state.state)
if date_time is None:
- self._current_datetime = dt_util.parse_datetime(DEFAULT_VALUE)
- return
- self._current_datetime = date_time
+ current_datetime = dt_util.parse_datetime(DEFAULT_VALUE)
+ else:
+ current_datetime = date_time
+
elif self.has_date:
date = dt_util.parse_date(old_state.state)
if date is None:
- self._current_datetime = dt_util.parse_datetime(DEFAULT_VALUE)
- return
- self._current_datetime = datetime.datetime.combine(date, DEFAULT_TIME)
+ current_datetime = dt_util.parse_datetime(DEFAULT_VALUE)
+ else:
+ current_datetime = py_datetime.datetime.combine(date, DEFAULT_TIME)
+
else:
time = dt_util.parse_time(old_state.state)
if time is None:
- self._current_datetime = dt_util.parse_datetime(DEFAULT_VALUE)
- return
- self._current_datetime = datetime.datetime.combine(DEFAULT_DATE, time)
+ current_datetime = dt_util.parse_datetime(DEFAULT_VALUE)
+ else:
+ current_datetime = py_datetime.datetime.combine(DEFAULT_DATE, time)
+
+ self._current_datetime = current_datetime.replace(
+ tzinfo=dt_util.DEFAULT_TIME_ZONE
+ )
@property
def should_poll(self):
@@ -305,10 +309,12 @@ class InputDatetime(RestoreEntity):
return None
if self.has_date and self.has_time:
- return self._current_datetime
+ return self._current_datetime.strftime(FMT_DATETIME)
+
if self.has_date:
- return self._current_datetime.date()
- return self._current_datetime.time()
+ return self._current_datetime.strftime(FMT_DATE)
+
+ return self._current_datetime.strftime(FMT_TIME)
@property
def state_attributes(self):
@@ -338,11 +344,13 @@ class InputDatetime(RestoreEntity):
+ self._current_datetime.minute * 60
+ self._current_datetime.second
)
+
elif not self.has_time:
- extended = datetime.datetime.combine(
- self._current_datetime, datetime.time(0, 0)
+ extended = py_datetime.datetime.combine(
+ self._current_datetime, py_datetime.time(0, 0)
)
attrs["timestamp"] = extended.timestamp()
+
else:
attrs["timestamp"] = self._current_datetime.timestamp()
@@ -354,13 +362,35 @@ class InputDatetime(RestoreEntity):
return self._config[CONF_ID]
@callback
- def async_set_datetime(self, date_val, time_val):
+ def async_set_datetime(self, date=None, time=None, datetime=None, timestamp=None):
"""Set a new date / time."""
- if not date_val:
- date_val = self._current_datetime.date()
- if not time_val:
- time_val = self._current_datetime.time()
- self._current_datetime = datetime.datetime.combine(date_val, time_val)
+ if timestamp:
+ datetime = dt_util.as_local(dt_util.utc_from_timestamp(timestamp)).replace(
+ tzinfo=None
+ )
+
+ if datetime:
+ date = datetime.date()
+ time = datetime.time()
+
+ if not self.has_date:
+ date = None
+
+ if not self.has_time:
+ time = None
+
+ if not date and not time:
+ raise vol.Invalid("Nothing to set")
+
+ if not date:
+ date = self._current_datetime.date()
+
+ if not time:
+ time = self._current_datetime.time()
+
+ self._current_datetime = py_datetime.datetime.combine(date, time).replace(
+ tzinfo=dt_util.DEFAULT_TIME_ZONE
+ )
self.async_write_ha_state()
async def async_update_config(self, config: typing.Dict) -> None:
diff --git a/homeassistant/components/input_datetime/reproduce_state.py b/homeassistant/components/input_datetime/reproduce_state.py
index b9eb9800adf..cc906ac50b3 100644
--- a/homeassistant/components/input_datetime/reproduce_state.py
+++ b/homeassistant/components/input_datetime/reproduce_state.py
@@ -8,15 +8,7 @@ from homeassistant.core import Context, State
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util import dt as dt_util
-from . import (
- ATTR_DATE,
- ATTR_DATETIME,
- ATTR_TIME,
- CONF_HAS_DATE,
- CONF_HAS_TIME,
- DOMAIN,
- SERVICE_SET_DATETIME,
-)
+from . import ATTR_DATE, ATTR_DATETIME, ATTR_TIME, CONF_HAS_DATE, CONF_HAS_TIME, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -53,22 +45,13 @@ async def _async_reproduce_state(
_LOGGER.warning("Unable to find entity %s", state.entity_id)
return
+ has_time = cur_state.attributes.get(CONF_HAS_TIME)
+ has_date = cur_state.attributes.get(CONF_HAS_DATE)
+
if not (
- (
- is_valid_datetime(state.state)
- and cur_state.attributes.get(CONF_HAS_DATE)
- and cur_state.attributes.get(CONF_HAS_TIME)
- )
- or (
- is_valid_date(state.state)
- and cur_state.attributes.get(CONF_HAS_DATE)
- and not cur_state.attributes.get(CONF_HAS_TIME)
- )
- or (
- is_valid_time(state.state)
- and cur_state.attributes.get(CONF_HAS_TIME)
- and not cur_state.attributes.get(CONF_HAS_DATE)
- )
+ (is_valid_datetime(state.state) and has_date and has_time)
+ or (is_valid_date(state.state) and has_date and not has_time)
+ or (is_valid_time(state.state) and has_time and not has_date)
):
_LOGGER.warning(
"Invalid state specified for %s: %s", state.entity_id, state.state
@@ -79,24 +62,17 @@ async def _async_reproduce_state(
if cur_state.state == state.state:
return
- service = SERVICE_SET_DATETIME
service_data = {ATTR_ENTITY_ID: state.entity_id}
- has_time = cur_state.attributes.get(CONF_HAS_TIME)
- has_date = cur_state.attributes.get(CONF_HAS_DATE)
-
if has_time and has_date:
service_data[ATTR_DATETIME] = state.state
elif has_time:
service_data[ATTR_TIME] = state.state
elif has_date:
service_data[ATTR_DATE] = state.state
- else:
- _LOGGER.warning("input_datetime needs either has_date or has_time or both")
- return
await hass.services.async_call(
- DOMAIN, service, service_data, context=context, blocking=True
+ DOMAIN, "set_datetime", service_data, context=context, blocking=True
)
diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py
index 8ec26ea3956..1f979cad7a9 100644
--- a/homeassistant/components/input_number/__init__.py
+++ b/homeassistant/components/input_number/__init__.py
@@ -7,11 +7,11 @@ import voluptuous as vol
from homeassistant.const import (
ATTR_EDITABLE,
ATTR_MODE,
- ATTR_UNIT_OF_MEASUREMENT,
CONF_ICON,
CONF_ID,
CONF_MODE,
CONF_NAME,
+ CONF_UNIT_OF_MEASUREMENT,
SERVICE_RELOAD,
)
from homeassistant.core import callback
@@ -67,7 +67,7 @@ CREATE_FIELDS = {
vol.Optional(CONF_INITIAL): vol.Coerce(float),
vol.Optional(CONF_STEP, default=1): vol.All(vol.Coerce(float), vol.Range(min=1e-3)),
vol.Optional(CONF_ICON): cv.icon,
- vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string,
+ vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_MODE, default=MODE_SLIDER): vol.In([MODE_BOX, MODE_SLIDER]),
}
@@ -78,7 +78,7 @@ UPDATE_FIELDS = {
vol.Optional(CONF_INITIAL): vol.Coerce(float),
vol.Optional(CONF_STEP): vol.All(vol.Coerce(float), vol.Range(min=1e-3)),
vol.Optional(CONF_ICON): cv.icon,
- vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string,
+ vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_MODE): vol.In([MODE_BOX, MODE_SLIDER]),
}
@@ -95,7 +95,7 @@ CONFIG_SCHEMA = vol.Schema(
vol.Coerce(float), vol.Range(min=1e-3)
),
vol.Optional(CONF_ICON): cv.icon,
- vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string,
+ vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_MODE, default=MODE_SLIDER): vol.In(
[MODE_BOX, MODE_SLIDER]
),
@@ -250,7 +250,7 @@ class InputNumber(RestoreEntity):
@property
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
- return self._config.get(ATTR_UNIT_OF_MEASUREMENT)
+ return self._config.get(CONF_UNIT_OF_MEASUREMENT)
@property
def unique_id(self) -> typing.Optional[str]:
diff --git a/homeassistant/components/insteon/translations/pl.json b/homeassistant/components/insteon/translations/pl.json
index 28c4414b9da..c4a58e0e09a 100644
--- a/homeassistant/components/insteon/translations/pl.json
+++ b/homeassistant/components/insteon/translations/pl.json
@@ -76,7 +76,7 @@
"port": "Port",
"username": "Nazwa u\u017cytkownika"
},
- "description": "Zmie\u0144 informacje o po\u0142\u0105czeniu Huba Insteon. Po wprowadzeniu tej zmiany musisz ponownie uruchomi\u0107 Home Assistant. Nie zmienia to konfiguracji samego Huba. Aby zmieni\u0107 jego konfiguracj\u0119, u\u017cyj aplikacji Hub.",
+ "description": "Zmie\u0144 informacje o po\u0142\u0105czeniu Huba Insteon. Po wprowadzeniu tej zmiany musisz ponownie uruchomi\u0107 Home Assistanta. Nie zmienia to konfiguracji samego Huba. Aby zmieni\u0107 jego konfiguracj\u0119, u\u017cyj aplikacji Hub.",
"title": "Insteon"
},
"init": {
diff --git a/homeassistant/components/intesishome/climate.py b/homeassistant/components/intesishome/climate.py
index 57912d7d24d..a41161c7a6e 100644
--- a/homeassistant/components/intesishome/climate.py
+++ b/homeassistant/components/intesishome/climate.py
@@ -374,9 +374,11 @@ class IntesisAC(ClimateEntity):
reconnect_minutes,
)
# Schedule reconnection
- async_call_later(
- self.hass, reconnect_minutes * 60, self._controller.connect()
- )
+
+ async def try_connect(_now):
+ await self._controller.connect()
+
+ async_call_later(self.hass, reconnect_minutes * 60, try_connect)
if self._controller.is_connected and not self._connected:
# Connection has been restored
diff --git a/homeassistant/components/ios/__init__.py b/homeassistant/components/ios/__init__.py
index bc5c5ec1c4e..2feafba949d 100644
--- a/homeassistant/components/ios/__init__.py
+++ b/homeassistant/components/ios/__init__.py
@@ -9,6 +9,7 @@ from homeassistant.const import HTTP_BAD_REQUEST, HTTP_INTERNAL_SERVER_ERROR
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, discovery
+from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.util.json import load_json, save_json
from .const import (
@@ -346,9 +347,11 @@ class iOSIdentifyDeviceView(HomeAssistantView):
data[ATTR_LAST_SEEN_AT] = datetime.datetime.now().isoformat()
- name = data.get(ATTR_DEVICE_ID)
+ device_id = data[ATTR_DEVICE_ID]
- hass.data[DOMAIN][ATTR_DEVICES][name] = data
+ hass.data[DOMAIN][ATTR_DEVICES][device_id] = data
+
+ async_dispatcher_send(hass, f"{DOMAIN}.{device_id}", data)
try:
save_json(self._config_path, hass.data[DOMAIN])
diff --git a/homeassistant/components/ios/sensor.py b/homeassistant/components/ios/sensor.py
index 01520063ccb..ccbc118a681 100644
--- a/homeassistant/components/ios/sensor.py
+++ b/homeassistant/components/ios/sensor.py
@@ -1,9 +1,13 @@
"""Support for Home Assistant iOS app sensors."""
from homeassistant.components import ios
from homeassistant.const import PERCENTAGE
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.icon import icon_for_battery_level
+from .const import DOMAIN
+
SENSOR_TYPES = {
"level": ["Battery Level", PERCENTAGE],
"state": ["Battery State", None],
@@ -73,6 +77,11 @@ class IOSSensor(Entity):
device_id = self._device[ios.ATTR_DEVICE_ID]
return f"{self.type}_{device_id}"
+ @property
+ def should_poll(self):
+ """No polling needed."""
+ return False
+
@property
def unit_of_measurement(self):
"""Return the unit of measurement this sensor expresses itself in."""
@@ -114,7 +123,17 @@ class IOSSensor(Entity):
return icon_state
return icon_for_battery_level(battery_level=battery_level, charging=charging)
- async def async_update(self):
+ @callback
+ def _update(self, device):
"""Get the latest state of the sensor."""
- self._device = ios.devices(self.hass).get(self._device_name)
+ self._device = device
self._state = self._device[ios.ATTR_BATTERY][self.type]
+ self.async_write_ha_state()
+
+ async def async_added_to_hass(self) -> None:
+ """Added to hass so need to register to dispatch."""
+ self._state = self._device[ios.ATTR_BATTERY][self.type]
+ device_id = self._device[ios.ATTR_DEVICE_ID]
+ self.async_on_remove(
+ async_dispatcher_connect(self.hass, f"{DOMAIN}.{device_id}", self._update)
+ )
diff --git a/homeassistant/components/ipma/strings.json b/homeassistant/components/ipma/strings.json
index bf0cf7a9ff8..24f142938d0 100644
--- a/homeassistant/components/ipma/strings.json
+++ b/homeassistant/components/ipma/strings.json
@@ -13,5 +13,10 @@
}
},
"error": { "name_exists": "Name already exists" }
+ },
+ "system_health": {
+ "info": {
+ "api_endpoint_reachable": "IPMA API endpoint reachable"
+ }
}
}
diff --git a/homeassistant/components/ipma/system_health.py b/homeassistant/components/ipma/system_health.py
new file mode 100644
index 00000000000..cd783490c35
--- /dev/null
+++ b/homeassistant/components/ipma/system_health.py
@@ -0,0 +1,22 @@
+"""Provide info to system health."""
+from homeassistant.components import system_health
+from homeassistant.core import HomeAssistant, callback
+
+IPMA_API_URL = "http://api.ipma.pt"
+
+
+@callback
+def async_register(
+ hass: HomeAssistant, register: system_health.SystemHealthRegistration
+) -> None:
+ """Register system health callbacks."""
+ register.async_register_info(system_health_info)
+
+
+async def system_health_info(hass):
+ """Get info for the info page."""
+ return {
+ "api_endpoint_reachable": system_health.async_check_can_reach_url(
+ hass, IPMA_API_URL
+ )
+ }
diff --git a/homeassistant/components/ipma/translations/en.json b/homeassistant/components/ipma/translations/en.json
index 823bf9ab93b..67b38267a1c 100644
--- a/homeassistant/components/ipma/translations/en.json
+++ b/homeassistant/components/ipma/translations/en.json
@@ -15,5 +15,10 @@
"title": "Location"
}
}
+ },
+ "system_health": {
+ "info": {
+ "api_endpoint_reachable": "IPMA API endpoint reachable"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/ipma/translations/es.json b/homeassistant/components/ipma/translations/es.json
index a3f83c150e7..d942608ad87 100644
--- a/homeassistant/components/ipma/translations/es.json
+++ b/homeassistant/components/ipma/translations/es.json
@@ -15,5 +15,10 @@
"title": "Ubicaci\u00f3n"
}
}
+ },
+ "system_health": {
+ "info": {
+ "api_endpoint_reachable": "Se puede acceder al punto de conexi\u00f3n de la API IPMA"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/ipma/translations/et.json b/homeassistant/components/ipma/translations/et.json
index a45685c96e2..b5b1464c7e7 100644
--- a/homeassistant/components/ipma/translations/et.json
+++ b/homeassistant/components/ipma/translations/et.json
@@ -15,5 +15,10 @@
"title": "Asukoht"
}
}
+ },
+ "system_health": {
+ "info": {
+ "api_endpoint_reachable": "IPMA API l\u00f5pp-punkt on k\u00e4ttesaadav"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/ipma/translations/no.json b/homeassistant/components/ipma/translations/no.json
index 4e4cdebeec6..1108b347110 100644
--- a/homeassistant/components/ipma/translations/no.json
+++ b/homeassistant/components/ipma/translations/no.json
@@ -15,5 +15,10 @@
"title": "Plassering"
}
}
+ },
+ "system_health": {
+ "info": {
+ "api_endpoint_reachable": "IPMA API-endepunkt n\u00e5s"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/ipma/translations/pl.json b/homeassistant/components/ipma/translations/pl.json
index 405b9fa81c6..9a10d541f1d 100644
--- a/homeassistant/components/ipma/translations/pl.json
+++ b/homeassistant/components/ipma/translations/pl.json
@@ -15,5 +15,10 @@
"title": "Lokalizacja"
}
}
+ },
+ "system_health": {
+ "info": {
+ "api_endpoint_reachable": "Dost\u0119pno\u015b\u0107 punktu ko\u0144cowego API IPMA"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/ipma/translations/ru.json b/homeassistant/components/ipma/translations/ru.json
index b9e98886e63..08e1673eef7 100644
--- a/homeassistant/components/ipma/translations/ru.json
+++ b/homeassistant/components/ipma/translations/ru.json
@@ -15,5 +15,10 @@
"title": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435"
}
}
+ },
+ "system_health": {
+ "info": {
+ "api_endpoint_reachable": "\u0414\u043e\u0441\u0442\u0443\u043f \u043a API IPMA"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/ipma/translations/zh-Hant.json b/homeassistant/components/ipma/translations/zh-Hant.json
index a3d084ddaa3..fc329a756b4 100644
--- a/homeassistant/components/ipma/translations/zh-Hant.json
+++ b/homeassistant/components/ipma/translations/zh-Hant.json
@@ -15,5 +15,10 @@
"title": "\u5ea7\u6a19"
}
}
+ },
+ "system_health": {
+ "info": {
+ "api_endpoint_reachable": "IPMA API \u53ef\u9054\u7aef\u9ede"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py
index d8ac0c039da..32a5967f8b4 100644
--- a/homeassistant/components/ipma/weather.py
+++ b/homeassistant/components/ipma/weather.py
@@ -8,6 +8,20 @@ from pyipma.location import Location
import voluptuous as vol
from homeassistant.components.weather import (
+ ATTR_CONDITION_CLOUDY,
+ ATTR_CONDITION_EXCEPTIONAL,
+ ATTR_CONDITION_FOG,
+ ATTR_CONDITION_HAIL,
+ ATTR_CONDITION_LIGHTNING,
+ ATTR_CONDITION_LIGHTNING_RAINY,
+ ATTR_CONDITION_PARTLYCLOUDY,
+ ATTR_CONDITION_POURING,
+ ATTR_CONDITION_RAINY,
+ ATTR_CONDITION_SNOWY,
+ ATTR_CONDITION_SNOWY_RAINY,
+ ATTR_CONDITION_SUNNY,
+ ATTR_CONDITION_WINDY,
+ ATTR_CONDITION_WINDY_VARIANT,
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_FORECAST_TEMP,
@@ -38,20 +52,20 @@ ATTRIBUTION = "Instituto Português do Mar e Atmosfera"
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30)
CONDITION_CLASSES = {
- "cloudy": [4, 5, 24, 25, 27],
- "fog": [16, 17, 26],
- "hail": [21, 22],
- "lightning": [19],
- "lightning-rainy": [20, 23],
- "partlycloudy": [2, 3],
- "pouring": [8, 11],
- "rainy": [6, 7, 9, 10, 12, 13, 14, 15],
- "snowy": [18],
- "snowy-rainy": [],
- "sunny": [1],
- "windy": [],
- "windy-variant": [],
- "exceptional": [],
+ ATTR_CONDITION_CLOUDY: [4, 5, 24, 25, 27],
+ ATTR_CONDITION_FOG: [16, 17, 26],
+ ATTR_CONDITION_HAIL: [21, 22],
+ ATTR_CONDITION_LIGHTNING: [19],
+ ATTR_CONDITION_LIGHTNING_RAINY: [20, 23],
+ ATTR_CONDITION_PARTLYCLOUDY: [2, 3],
+ ATTR_CONDITION_POURING: [8, 11],
+ ATTR_CONDITION_RAINY: [6, 7, 9, 10, 12, 13, 14, 15],
+ ATTR_CONDITION_SNOWY: [18],
+ ATTR_CONDITION_SNOWY_RAINY: [],
+ ATTR_CONDITION_SUNNY: [1],
+ ATTR_CONDITION_WINDY: [],
+ ATTR_CONDITION_WINDY_VARIANT: [],
+ ATTR_CONDITION_EXCEPTIONAL: [],
}
FORECAST_MODE = ["hourly", "daily"]
diff --git a/homeassistant/components/ipp/translations/hu.json b/homeassistant/components/ipp/translations/hu.json
index f4bcbbf586e..396992156c0 100644
--- a/homeassistant/components/ipp/translations/hu.json
+++ b/homeassistant/components/ipp/translations/hu.json
@@ -1,7 +1,13 @@
{
"config": {
"abort": {
- "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "connection_upgrade": "Nem siker\u00fclt csatlakozni a nyomtat\u00f3hoz, mert a kapcsolat friss\u00edt\u00e9se sz\u00fcks\u00e9ges."
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "connection_upgrade": "Nem siker\u00fclt csatlakozni a nyomtat\u00f3hoz. K\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra az SSL/TLS opci\u00f3 bejel\u00f6l\u00e9s\u00e9vel."
},
"flow_title": "Nyomtat\u00f3: {name}",
"step": {
diff --git a/homeassistant/components/ipp/translations/ka.json b/homeassistant/components/ipp/translations/ka.json
new file mode 100644
index 00000000000..44cf9de1c37
--- /dev/null
+++ b/homeassistant/components/ipp/translations/ka.json
@@ -0,0 +1,10 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "\u10db\u10d0\u10e0\u10ea\u10ee\u10d8 \u10e8\u10d4\u10d4\u10e0\u10d7\u10d4\u10d1\u10d8\u10e1\u10d0\u10e1"
+ },
+ "error": {
+ "cannot_connect": "\u10db\u10d0\u10e0\u10ea\u10ee\u10d8 \u10e8\u10d4\u10d4\u10e0\u10d7\u10d4\u10d1\u10d8\u10e1\u10d0\u10e1"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ipp/translations/pl.json b/homeassistant/components/ipp/translations/pl.json
index fa8e9a3ca0b..8a058c9b9d6 100644
--- a/homeassistant/components/ipp/translations/pl.json
+++ b/homeassistant/components/ipp/translations/pl.json
@@ -23,11 +23,11 @@
"ssl": "Certyfikat SSL",
"verify_ssl": "Weryfikacja certyfikatu SSL"
},
- "description": "Skonfiguruj drukark\u0119 za pomoc\u0105 protoko\u0142u IPP (Internet Printing Protocol) w celu integracji z Home Assistant.",
+ "description": "Skonfiguruj drukark\u0119 za pomoc\u0105 protoko\u0142u IPP (Internet Printing Protocol) w celu integracji z Home Assistantem.",
"title": "Po\u0142\u0105cz swoj\u0105 drukark\u0119"
},
"zeroconf_confirm": {
- "description": "Czy chcesz doda\u0107 drukark\u0119 o nazwie `{name}` do Home Assistant?",
+ "description": "Czy chcesz doda\u0107 drukark\u0119 o nazwie `{name}` do Home Assistanta?",
"title": "Wykryto drukark\u0119"
}
}
diff --git a/homeassistant/components/islamic_prayer_times/translations/ka.json b/homeassistant/components/islamic_prayer_times/translations/ka.json
new file mode 100644
index 00000000000..503f471efb3
--- /dev/null
+++ b/homeassistant/components/islamic_prayer_times/translations/ka.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0. \u10e8\u10d4\u10e1\u10d0\u10eb\u10da\u10d4\u10d1\u10d4\u10da\u10d8\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/isy994/translations/sl.json b/homeassistant/components/isy994/translations/sl.json
new file mode 100644
index 00000000000..d241fbeb59c
--- /dev/null
+++ b/homeassistant/components/isy994/translations/sl.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "error": {
+ "unknown": "Nepri\u010dakovana napaka"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "URL"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/juicenet/translations/sl.json b/homeassistant/components/juicenet/translations/sl.json
new file mode 100644
index 00000000000..8a0996ed92e
--- /dev/null
+++ b/homeassistant/components/juicenet/translations/sl.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "unknown": "Nepri\u010dakovana napaka"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py
index 87ade4955a0..1d547e895bf 100644
--- a/homeassistant/components/knx/__init__.py
+++ b/homeassistant/components/knx/__init__.py
@@ -1,4 +1,5 @@
"""Support KNX devices."""
+import asyncio
import logging
import voluptuous as vol
@@ -19,6 +20,7 @@ from homeassistant.const import (
CONF_HOST,
CONF_PORT,
EVENT_HOMEASSISTANT_STOP,
+ SERVICE_RELOAD,
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
@@ -27,7 +29,11 @@ from homeassistant.const import (
from homeassistant.core import callback
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity_platform import async_get_platforms
from homeassistant.helpers.event import async_track_state_change_event
+from homeassistant.helpers.reload import async_integration_yaml_config
+from homeassistant.helpers.service import async_register_admin_service
+from homeassistant.helpers.typing import ServiceCallType
from .const import DOMAIN, SupportedPlatforms
from .factory import create_knx_device
@@ -172,6 +178,28 @@ async def async_setup(hass, config):
schema=SERVICE_KNX_SEND_SCHEMA,
)
+ async def reload_service_handler(service_call: ServiceCallType) -> None:
+ """Remove all KNX components and load new ones from config."""
+
+ # First check for config file. If for some reason it is no longer there
+ # or knx is no longer mentioned, stop the reload.
+ config = await async_integration_yaml_config(hass, DOMAIN)
+
+ if not config or DOMAIN not in config:
+ return
+
+ await hass.data[DOMAIN].xknx.stop()
+
+ await asyncio.gather(
+ *[platform.async_reset() for platform in async_get_platforms(hass, DOMAIN)]
+ )
+
+ await async_setup(hass, config)
+
+ async_register_admin_service(
+ hass, DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=vol.Schema({})
+ )
+
return True
@@ -298,8 +326,6 @@ class KNXModule:
"knx_event",
{"address": str(telegram.group_address), "data": telegram.payload.value},
)
- # False signals XKNX to proceed with processing telegrams.
- return False
async def service_send_to_knx_bus(self, call):
"""Service for sending an arbitrary KNX message to the KNX bus."""
diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py
index c677b12c0ee..b88b1cfe86a 100644
--- a/homeassistant/components/knx/cover.py
+++ b/homeassistant/components/knx/cover.py
@@ -5,6 +5,7 @@ from homeassistant.components.cover import (
ATTR_POSITION,
ATTR_TILT_POSITION,
DEVICE_CLASS_BLIND,
+ DEVICE_CLASSES,
SUPPORT_CLOSE,
SUPPORT_OPEN,
SUPPORT_SET_POSITION,
@@ -47,6 +48,8 @@ class KNXCover(KnxEntity, CoverEntity):
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
+ if self._device.device_class in DEVICE_CLASSES:
+ return self._device.device_class
if self._device.supports_angle:
return DEVICE_CLASS_BLIND
return None
diff --git a/homeassistant/components/knx/factory.py b/homeassistant/components/knx/factory.py
index 6da2b73cd6a..385b7c009ed 100644
--- a/homeassistant/components/knx/factory.py
+++ b/homeassistant/components/knx/factory.py
@@ -82,6 +82,7 @@ def _create_cover(knx_module: XKNX, config: ConfigType) -> XknxCover:
travel_time_up=config[CoverSchema.CONF_TRAVELLING_TIME_UP],
invert_position=config[CoverSchema.CONF_INVERT_POSITION],
invert_angle=config[CoverSchema.CONF_INVERT_ANGLE],
+ device_class=config.get(CONF_DEVICE_CLASS),
)
diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json
index 4055048fd2d..631a6329c8c 100644
--- a/homeassistant/components/knx/manifest.json
+++ b/homeassistant/components/knx/manifest.json
@@ -2,7 +2,7 @@
"domain": "knx",
"name": "KNX",
"documentation": "https://www.home-assistant.io/integrations/knx",
- "requirements": ["xknx==0.15.3"],
+ "requirements": ["xknx==0.15.6"],
"codeowners": ["@Julius2342", "@farmio", "@marvin-w"],
"quality_scale": "silver"
}
diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py
index cbf06925163..c17667cbed2 100644
--- a/homeassistant/components/knx/schema.py
+++ b/homeassistant/components/knx/schema.py
@@ -77,6 +77,7 @@ class CoverSchema:
): cv.positive_int,
vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean,
vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean,
+ vol.Optional(CONF_DEVICE_CLASS): cv.string,
}
)
diff --git a/homeassistant/components/kodi/__init__.py b/homeassistant/components/kodi/__init__.py
index f3cb15a17eb..4dcb25b3ea9 100644
--- a/homeassistant/components/kodi/__init__.py
+++ b/homeassistant/components/kodi/__init__.py
@@ -15,7 +15,6 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.core import HomeAssistant
-from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import (
@@ -23,7 +22,6 @@ from .const import (
DATA_CONNECTION,
DATA_KODI,
DATA_REMOVE_LISTENER,
- DATA_VERSION,
DOMAIN,
)
@@ -48,13 +46,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
entry.data[CONF_SSL],
session=async_get_clientsession(hass),
)
+
+ kodi = Kodi(conn)
+
try:
await conn.connect()
- kodi = Kodi(conn)
- await kodi.ping()
- raw_version = (await kodi.get_application_properties(["version"]))["version"]
- except CannotConnectError as error:
- raise ConfigEntryNotReady from error
+ except CannotConnectError:
+ pass
except InvalidAuthError as error:
_LOGGER.error(
"Login to %s failed: [%s]",
@@ -68,12 +66,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
remove_stop_listener = hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _close)
- version = f"{raw_version['major']}.{raw_version['minor']}"
hass.data[DOMAIN][entry.entry_id] = {
DATA_CONNECTION: conn,
DATA_KODI: kodi,
DATA_REMOVE_LISTENER: remove_stop_listener,
- DATA_VERSION: version,
}
for component in PLATFORMS:
diff --git a/homeassistant/components/kodi/const.py b/homeassistant/components/kodi/const.py
index 26677f99e5e..8f0ae5de737 100644
--- a/homeassistant/components/kodi/const.py
+++ b/homeassistant/components/kodi/const.py
@@ -6,7 +6,6 @@ CONF_WS_PORT = "ws_port"
DATA_CONNECTION = "connection"
DATA_KODI = "kodi"
DATA_REMOVE_LISTENER = "remove_listener"
-DATA_VERSION = "version"
DEFAULT_PORT = 8080
DEFAULT_SSL = False
diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py
index 68809559cbf..dfe3af4b11e 100644
--- a/homeassistant/components/kodi/media_player.py
+++ b/homeassistant/components/kodi/media_player.py
@@ -48,13 +48,18 @@ from homeassistant.const import (
CONF_SSL,
CONF_TIMEOUT,
CONF_USERNAME,
+ EVENT_HOMEASSISTANT_STARTED,
STATE_IDLE,
STATE_OFF,
STATE_PAUSED,
STATE_PLAYING,
)
-from homeassistant.core import callback
-from homeassistant.helpers import config_validation as cv, entity_platform
+from homeassistant.core import CoreState, callback
+from homeassistant.helpers import (
+ config_validation as cv,
+ device_registry,
+ entity_platform,
+)
from homeassistant.helpers.event import async_track_time_interval
import homeassistant.util.dt as dt_util
@@ -63,7 +68,6 @@ from .const import (
CONF_WS_PORT,
DATA_CONNECTION,
DATA_KODI,
- DATA_VERSION,
DEFAULT_PORT,
DEFAULT_SSL,
DEFAULT_TIMEOUT,
@@ -91,7 +95,7 @@ DEPRECATED_TURN_OFF_ACTIONS = {
"shutdown": "System.Shutdown",
}
-WEBSOCKET_WATCHDOG_INTERVAL = timedelta(minutes=3)
+WEBSOCKET_WATCHDOG_INTERVAL = timedelta(seconds=10)
# https://github.com/xbmc/xbmc/blob/master/xbmc/media/MediaType.h
MEDIA_TYPES = {
@@ -229,14 +233,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
data = hass.data[DOMAIN][config_entry.entry_id]
connection = data[DATA_CONNECTION]
- version = data[DATA_VERSION]
kodi = data[DATA_KODI]
name = config_entry.data[CONF_NAME]
uid = config_entry.unique_id
if uid is None:
uid = config_entry.entry_id
- entity = KodiEntity(connection, kodi, name, uid, version)
+ entity = KodiEntity(connection, kodi, name, uid)
async_add_entities([entity])
@@ -264,13 +267,12 @@ def cmd(func):
class KodiEntity(MediaPlayerEntity):
"""Representation of a XBMC/Kodi device."""
- def __init__(self, connection, kodi, name, uid, version):
+ def __init__(self, connection, kodi, name, uid):
"""Initialize the Kodi entity."""
self._connection = connection
self._kodi = kodi
self._name = name
self._unique_id = uid
- self._version = version
self._players = None
self._properties = {}
self._item = {}
@@ -347,7 +349,6 @@ class KodiEntity(MediaPlayerEntity):
"identifiers": {(DOMAIN, self.unique_id)},
"name": self.name,
"manufacturer": "Kodi",
- "sw_version": self._version,
}
@property
@@ -370,27 +371,43 @@ class KodiEntity(MediaPlayerEntity):
return
if self._connection.connected:
- self._on_ws_connected()
+ await self._on_ws_connected()
- self.async_on_remove(
- async_track_time_interval(
- self.hass,
- self._async_connect_websocket_if_disconnected,
- WEBSOCKET_WATCHDOG_INTERVAL,
+ async def start_watchdog(event=None):
+ """Start websocket watchdog."""
+ await self._async_connect_websocket_if_disconnected()
+ self.async_on_remove(
+ async_track_time_interval(
+ self.hass,
+ self._async_connect_websocket_if_disconnected,
+ WEBSOCKET_WATCHDOG_INTERVAL,
+ )
)
- )
- @callback
- def _on_ws_connected(self):
+ # If Home Assistant is already in a running state, start the watchdog
+ # immediately, else trigger it after Home Assistant has finished starting.
+ if self.hass.state == CoreState.running:
+ await start_watchdog()
+ else:
+ self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, start_watchdog)
+
+ async def _on_ws_connected(self):
"""Call after ws is connected."""
self._register_ws_callbacks()
+
+ version = (await self._kodi.get_application_properties(["version"]))["version"]
+ sw_version = f"{version['major']}.{version['minor']}"
+ dev_reg = await device_registry.async_get_registry(self.hass)
+ device = dev_reg.async_get_device({(DOMAIN, self.unique_id)}, [])
+ dev_reg.async_update_device(device.id, sw_version=sw_version)
+
self.async_schedule_update_ha_state(True)
async def _async_ws_connect(self):
"""Connect to Kodi via websocket protocol."""
try:
await self._connection.connect()
- self._on_ws_connected()
+ await self._on_ws_connected()
except (jsonrpc_base.jsonrpc.TransportError, CannotConnectError):
_LOGGER.debug("Unable to connect to Kodi via websocket", exc_info=True)
await self._clear_connection(False)
@@ -426,6 +443,7 @@ class KodiEntity(MediaPlayerEntity):
self._connection.server.System.OnRestart = self.async_on_quit
self._connection.server.System.OnSleep = self.async_on_quit
+ @cmd
async def async_update(self):
"""Retrieve latest state."""
if not self._connection.connected:
diff --git a/homeassistant/components/kodi/translations/ca.json b/homeassistant/components/kodi/translations/ca.json
index 8dd03fe9fff..fbeaa3d4c71 100644
--- a/homeassistant/components/kodi/translations/ca.json
+++ b/homeassistant/components/kodi/translations/ca.json
@@ -4,6 +4,7 @@
"already_configured": "El dispositiu ja est\u00e0 configurat",
"cannot_connect": "Ha fallat la connexi\u00f3",
"invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
+ "no_uuid": "La inst\u00e0ncia de Kodi no t\u00e9 un identificador \u00fanic. Probablement, aix\u00f2 es deu a una versi\u00f3 antiga de Kodi (17.x o inferior). Pots configurar la integraci\u00f3 manualment o actualitzar Kodi a una versi\u00f3 m\u00e9s recent.",
"unknown": "Error inesperat"
},
"error": {
diff --git a/homeassistant/components/kodi/translations/cs.json b/homeassistant/components/kodi/translations/cs.json
index e21c08b0758..ccfb08328fb 100644
--- a/homeassistant/components/kodi/translations/cs.json
+++ b/homeassistant/components/kodi/translations/cs.json
@@ -4,6 +4,7 @@
"already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno",
"cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit",
"invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed",
+ "no_uuid": "Instance Kodi nem\u00e1 jedine\u010dn\u00e9 ID. To je pravd\u011bpodobn\u011b zp\u016fsobeno starou verz\u00ed Kodi (17.x nebo ni\u017e\u0161\u00ed). Integraci m\u016f\u017eete nastavit ru\u010dn\u011b nebo aktualizovat na nov\u011bj\u0161\u00ed verzi Kodi.",
"unknown": "Neo\u010dek\u00e1van\u00e1 chyba"
},
"error": {
diff --git a/homeassistant/components/kodi/translations/en.json b/homeassistant/components/kodi/translations/en.json
index be87d312146..be6af6a7f91 100644
--- a/homeassistant/components/kodi/translations/en.json
+++ b/homeassistant/components/kodi/translations/en.json
@@ -4,6 +4,7 @@
"already_configured": "Device is already configured",
"cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication",
+ "no_uuid": "Kodi instance does not have a unique id. This is most likely due to an old Kodi version (17.x or below). You can configure the integration manually or upgrade to a more recent Kodi version.",
"unknown": "Unexpected error"
},
"error": {
diff --git a/homeassistant/components/kodi/translations/es.json b/homeassistant/components/kodi/translations/es.json
index ddea2a65a2f..b19d09a92d0 100644
--- a/homeassistant/components/kodi/translations/es.json
+++ b/homeassistant/components/kodi/translations/es.json
@@ -4,6 +4,7 @@
"already_configured": "El dispositivo ya est\u00e1 configurado",
"cannot_connect": "No se pudo conectar",
"invalid_auth": "Autentificacion invalida",
+ "no_uuid": "La instancia de Kodi no tiene un identificador \u00fanico. Esto probablemente es debido a una versi\u00f3n antigua Kodi (17.x o inferior). Puedes configurar la integraci\u00f3n manualmente o actualizar a una versi\u00f3n m\u00e1s reciente de Kodi.",
"unknown": "Error inesperado"
},
"error": {
diff --git a/homeassistant/components/kodi/translations/et.json b/homeassistant/components/kodi/translations/et.json
index a569c470e1d..f12665dfe8a 100644
--- a/homeassistant/components/kodi/translations/et.json
+++ b/homeassistant/components/kodi/translations/et.json
@@ -4,6 +4,7 @@
"already_configured": "Seade on juba h\u00e4\u00e4lestatud",
"cannot_connect": "\u00dchendamine nurjus",
"invalid_auth": "Tuvastamine nurjus",
+ "no_uuid": "Kodi eksemplaril puudub ID. See on k\u00f5ige t\u00f5en\u00e4olisemalt tingitud vananenud Kodi versiooni t\u00f5ttu (17.x v\u00f5i vanem). Sidumist saad seadistada k\u00e4sitsi v\u00f5i t\u00e4ienda Kodi uuemale versioonile.",
"unknown": "Tundmatu viga"
},
"error": {
diff --git a/homeassistant/components/kodi/translations/it.json b/homeassistant/components/kodi/translations/it.json
index ad783a26a6b..cadec0d387e 100644
--- a/homeassistant/components/kodi/translations/it.json
+++ b/homeassistant/components/kodi/translations/it.json
@@ -4,6 +4,7 @@
"already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
"cannot_connect": "Impossibile connettersi",
"invalid_auth": "Autenticazione non valida",
+ "no_uuid": "L'istanza di Kodi non ha un ID univoco. Ci\u00f2 \u00e8 molto probabilmente dovuto a una vecchia versione di Kodi (17.xo inferiore). Puoi configurare l'integrazione manualmente o eseguire l'aggiornamento a una versione di Kodi pi\u00f9 recente.",
"unknown": "Errore imprevisto"
},
"error": {
diff --git a/homeassistant/components/kodi/translations/ka.json b/homeassistant/components/kodi/translations/ka.json
new file mode 100644
index 00000000000..16977c0ec0c
--- /dev/null
+++ b/homeassistant/components/kodi/translations/ka.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "no_uuid": "\u10d9\u10dd\u10d3\u10d8\u10e1 \u10d8\u10dc\u10e1\u10e2\u10d0\u10dc\u10e1\u10e1 \u10d0\u10e0 \u10d0\u10e5\u10d5\u10e1 \u10e3\u10dc\u10d8\u10d9\u10d0\u10da\u10e3\u10e0\u10d8 ID. \u10e1\u10d0\u10d5\u10d0\u10e0\u10d0\u10e3\u10d3\u10dd\u10d3 \u10d4\u10e1 \u10d2\u10d0\u10db\u10dd\u10ec\u10d5\u10d4\u10e3\u10da\u10d8\u10d0 \u10eb\u10d5\u10d4\u10da\u10d8 Kodi \u10d5\u10d4\u10e0\u10e1\u10d8\u10d8\u10d7 (17.x \u10d0\u10dc \u10e5\u10d5\u10d4\u10db\u10dd\u10d7). \u10d7\u10e5\u10d5\u10d4\u10dc \u10e8\u10d4\u10d2\u10d8\u10eb\u10da\u10d8\u10d0\u10d7 \u10ee\u10d4\u10da\u10d8\u10d7 \u10db\u10dd\u10d0\u10ee\u10d3\u10d8\u10dc\u10dd\u10d7 \u10d8\u10dc\u10e2\u10d4\u10d2\u10e0\u10d0\u10ea\u10d8\u10d8\u10e1 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10d0 \u10d0\u10dc \u10d2\u10d0\u10d3\u10d0\u10ee\u10d5\u10d8\u10d3\u10d4\u10d7 Kodi- \u10e1 \u10e3\u10d0\u10ee\u10da\u10d4\u10e1 \u10d5\u10d4\u10e0\u10e1\u10d8\u10d0\u10d6\u10d4."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/kodi/translations/lb.json b/homeassistant/components/kodi/translations/lb.json
index c00f0e127bb..55dd0fac319 100644
--- a/homeassistant/components/kodi/translations/lb.json
+++ b/homeassistant/components/kodi/translations/lb.json
@@ -4,6 +4,7 @@
"already_configured": "Apparat ass scho konfigur\u00e9iert",
"cannot_connect": "Feeler beim verbannen",
"invalid_auth": "Ong\u00eblteg Authentifikatioun",
+ "no_uuid": "Kodi Instanz huet keng eenzegarteg ID. D\u00ebst ass wahrscheinlech duerch eng al Kodi versioun (17.x oder dr\u00ebnner). Du kanns d'Integratioun manuell konfigur\u00e9ieren oder op m\u00e9i eng rezent Kodi Versioun aktualis\u00e9ieren.",
"unknown": "Onerwaarte Feeler"
},
"error": {
diff --git a/homeassistant/components/kodi/translations/no.json b/homeassistant/components/kodi/translations/no.json
index 594daf99e66..b7815c3aa3a 100644
--- a/homeassistant/components/kodi/translations/no.json
+++ b/homeassistant/components/kodi/translations/no.json
@@ -4,6 +4,7 @@
"already_configured": "Enheten er allerede konfigurert",
"cannot_connect": "Tilkobling mislyktes",
"invalid_auth": "Ugyldig godkjenning",
+ "no_uuid": "Kodi-forekomsten har ikke en unik id. Dette skyldes mest sannsynlig en gammel Kodi-versjon (17.x eller under). Du kan konfigurere integreringen manuelt eller oppgradere til en nyere Kodi-versjon.",
"unknown": "Uventet feil"
},
"error": {
diff --git a/homeassistant/components/kodi/translations/pl.json b/homeassistant/components/kodi/translations/pl.json
index 78fdf95a7a8..2194c480c03 100644
--- a/homeassistant/components/kodi/translations/pl.json
+++ b/homeassistant/components/kodi/translations/pl.json
@@ -4,6 +4,7 @@
"already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"invalid_auth": "Niepoprawne uwierzytelnienie",
+ "no_uuid": "Kodi nie ma unikalnego identyfikatora. Najprawdopodobniej jest to spowodowane star\u0105 wersj\u0105 Kodi (17.x lub starsz\u0105). Mo\u017cesz skonfigurowa\u0107 integracj\u0119 r\u0119cznie lub zaktualizowa\u0107 Kodi do nowszej wersji.",
"unknown": "Nieoczekiwany b\u0142\u0105d"
},
"error": {
@@ -21,7 +22,7 @@
"description": "Wprowad\u017a nazw\u0119 u\u017cytkownika i has\u0142o Kodi. Mo\u017cna je znale\u017a\u0107 w System/Ustawienia/Sie\u0107/Us\u0142ugi."
},
"discovery_confirm": {
- "description": "Czy chcesz doda\u0107 Kodi (\"{name}\") do Home Assistant?",
+ "description": "Czy chcesz doda\u0107 Kodi (\"{name}\") do Home Assistanta?",
"title": "Wykryte urz\u0105dzenia Kodi"
},
"user": {
diff --git a/homeassistant/components/kodi/translations/ru.json b/homeassistant/components/kodi/translations/ru.json
index a6a982dfdee..312008c9b62 100644
--- a/homeassistant/components/kodi/translations/ru.json
+++ b/homeassistant/components/kodi/translations/ru.json
@@ -4,6 +4,7 @@
"already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.",
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
"invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "no_uuid": "\u0423 \u044d\u0442\u043e\u0433\u043e \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440\u0430 Kodi \u043d\u0435\u0442 \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0430. \u0421\u043a\u043e\u0440\u0435\u0435 \u0432\u0441\u0435\u0433\u043e, \u044d\u0442\u043e \u0441\u0432\u044f\u0437\u0430\u043d\u043e \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u0441\u0442\u0430\u0440\u043e\u0439 \u0432\u0435\u0440\u0441\u0438\u0435\u0439 Kodi (17.x \u0438\u043b\u0438 \u043d\u0438\u0436\u0435). \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u0432\u0440\u0443\u0447\u043d\u0443\u044e \u0438\u043b\u0438 \u043f\u0435\u0440\u0435\u0439\u0442\u0438 \u043d\u0430 \u0431\u043e\u043b\u0435\u0435 \u043d\u043e\u0432\u0443\u044e \u0432\u0435\u0440\u0441\u0438\u044e Kodi.",
"unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
},
"error": {
@@ -22,7 +23,7 @@
},
"discovery_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 Kodi (`{name}`)?",
- "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u044b\u0439 Kodi"
+ "title": "Kodi"
},
"user": {
"data": {
diff --git a/homeassistant/components/kodi/translations/zh-Hant.json b/homeassistant/components/kodi/translations/zh-Hant.json
index 3e9065c140f..a4aaf909344 100644
--- a/homeassistant/components/kodi/translations/zh-Hant.json
+++ b/homeassistant/components/kodi/translations/zh-Hant.json
@@ -4,6 +4,7 @@
"already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
"cannot_connect": "\u9023\u7dda\u5931\u6557",
"invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
+ "no_uuid": "Kodi \u5be6\u4f8b\u6c92\u6709\u552f\u4e00 ID\u3002\u901a\u5e38\u662f\u56e0\u70ba Kodi \u7248\u672c\u904e\u820a\uff08\u4f4e\u65bc 17.x\uff09\u3002\u53ef\u4ee5\u624b\u52d5\u8a2d\u5b9a\u6574\u5408\u6216\u66f4\u65b0\u81f3\u6700\u65b0\u7248\u672c Kodi\u3002",
"unknown": "\u672a\u9810\u671f\u932f\u8aa4"
},
"error": {
diff --git a/homeassistant/components/konnected/translations/it.json b/homeassistant/components/konnected/translations/it.json
index 35cc0f1a4c2..b618ee04b48 100644
--- a/homeassistant/components/konnected/translations/it.json
+++ b/homeassistant/components/konnected/translations/it.json
@@ -32,9 +32,7 @@
"not_konn_panel": "Non \u00e8 un dispositivo Konnected.io riconosciuto"
},
"error": {
- "bad_host": "URL dell'host API di sostituzione non valido",
- "one": "uno",
- "other": "altri"
+ "bad_host": "URL dell'host API di sostituzione non valido"
},
"step": {
"options_binary": {
diff --git a/homeassistant/components/konnected/translations/pl.json b/homeassistant/components/konnected/translations/pl.json
index a6a2a715237..ee6c10cbdd8 100644
--- a/homeassistant/components/konnected/translations/pl.json
+++ b/homeassistant/components/konnected/translations/pl.json
@@ -90,7 +90,7 @@
"api_host": "Zast\u0119powanie adresu URL hosta API (opcjonalnie)",
"blink": "Miganie diody LED panelu podczas wysy\u0142ania zmiany stanu",
"discovery": "Odpowiadaj na \u017c\u0105dania wykrywania w Twojej sieci",
- "override_api_host": "Zast\u0105p domy\u015blny adres URL API Home Assistant"
+ "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/kulersky/__init__.py b/homeassistant/components/kulersky/__init__.py
new file mode 100644
index 00000000000..ff984e2c0d3
--- /dev/null
+++ b/homeassistant/components/kulersky/__init__.py
@@ -0,0 +1,44 @@
+"""Kuler Sky lights integration."""
+import asyncio
+
+import voluptuous as vol
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+
+from .const import DOMAIN
+
+CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA)
+
+PLATFORMS = ["light"]
+
+
+async def async_setup(hass: HomeAssistant, config: dict):
+ """Set up the Kuler Sky component."""
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Set up Kuler Sky from a config entry."""
+ for component in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, component)
+ )
+
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Unload a config entry."""
+ unload_ok = all(
+ await asyncio.gather(
+ *[
+ hass.config_entries.async_forward_entry_unload(entry, component)
+ for component in PLATFORMS
+ ]
+ )
+ )
+ if unload_ok:
+ hass.data[DOMAIN].pop(entry.entry_id)
+
+ return unload_ok
diff --git a/homeassistant/components/kulersky/config_flow.py b/homeassistant/components/kulersky/config_flow.py
new file mode 100644
index 00000000000..04f7719b8e6
--- /dev/null
+++ b/homeassistant/components/kulersky/config_flow.py
@@ -0,0 +1,29 @@
+"""Config flow for Kuler Sky."""
+import logging
+
+import pykulersky
+
+from homeassistant import config_entries
+from homeassistant.helpers import config_entry_flow
+
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def _async_has_devices(hass) -> bool:
+ """Return if there are devices that can be discovered."""
+ # Check if there are any devices that can be discovered in the network.
+ try:
+ devices = await hass.async_add_executor_job(
+ pykulersky.discover_bluetooth_devices
+ )
+ except pykulersky.PykulerskyException as exc:
+ _LOGGER.error("Unable to discover nearby Kuler Sky devices: %s", exc)
+ return False
+ return len(devices) > 0
+
+
+config_entry_flow.register_discovery_flow(
+ DOMAIN, "Kuler Sky", _async_has_devices, config_entries.CONN_CLASS_LOCAL_POLL
+)
diff --git a/homeassistant/components/kulersky/const.py b/homeassistant/components/kulersky/const.py
new file mode 100644
index 00000000000..ae1e7a435dc
--- /dev/null
+++ b/homeassistant/components/kulersky/const.py
@@ -0,0 +1,2 @@
+"""Constants for the Kuler Sky integration."""
+DOMAIN = "kulersky"
diff --git a/homeassistant/components/kulersky/light.py b/homeassistant/components/kulersky/light.py
new file mode 100644
index 00000000000..71dd4a158ca
--- /dev/null
+++ b/homeassistant/components/kulersky/light.py
@@ -0,0 +1,215 @@
+"""Kuler Sky light platform."""
+import asyncio
+from datetime import timedelta
+import logging
+from typing import Callable, List
+
+import pykulersky
+
+from homeassistant.components.light import (
+ ATTR_BRIGHTNESS,
+ ATTR_HS_COLOR,
+ ATTR_WHITE_VALUE,
+ SUPPORT_BRIGHTNESS,
+ SUPPORT_COLOR,
+ SUPPORT_WHITE_VALUE,
+ LightEntity,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import EVENT_HOMEASSISTANT_STOP
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.event import async_track_time_interval
+from homeassistant.helpers.typing import HomeAssistantType
+import homeassistant.util.color as color_util
+
+from .const import DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+SUPPORT_KULERSKY = SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_WHITE_VALUE
+
+DISCOVERY_INTERVAL = timedelta(seconds=60)
+
+PARALLEL_UPDATES = 0
+
+
+def check_light(light: pykulersky.Light):
+ """Attempt to connect to this light and read the color."""
+ light.connect()
+ light.get_color()
+
+
+async def async_setup_entry(
+ hass: HomeAssistantType,
+ config_entry: ConfigEntry,
+ async_add_entities: Callable[[List[Entity], bool], None],
+) -> None:
+ """Set up Kuler sky light devices."""
+ if DOMAIN not in hass.data:
+ hass.data[DOMAIN] = {}
+ if "devices" not in hass.data[DOMAIN]:
+ hass.data[DOMAIN]["devices"] = set()
+ if "discovery" not in hass.data[DOMAIN]:
+ hass.data[DOMAIN]["discovery"] = asyncio.Lock()
+
+ async def discover(*args):
+ """Attempt to discover new lights."""
+ # Since discovery needs to connect to all discovered bluetooth devices, and
+ # only rules out devices after a timeout, it can potentially take a long
+ # time. If there's already a discovery running, just skip this poll.
+ if hass.data[DOMAIN]["discovery"].locked():
+ return
+
+ async with hass.data[DOMAIN]["discovery"]:
+ bluetooth_devices = await hass.async_add_executor_job(
+ pykulersky.discover_bluetooth_devices
+ )
+
+ # Filter out already connected lights
+ new_devices = [
+ device
+ for device in bluetooth_devices
+ if device["address"] not in hass.data[DOMAIN]["devices"]
+ ]
+
+ for device in new_devices:
+ light = pykulersky.Light(device["address"], device["name"])
+ try:
+ # If the connection fails, either this is not a Kuler Sky
+ # light, or it's bluetooth connection is currently locked
+ # by another device. If the vendor's app is connected to
+ # the light when home assistant tries to connect, this
+ # connection will fail.
+ await hass.async_add_executor_job(check_light, light)
+ except pykulersky.PykulerskyException:
+ continue
+ # The light has successfully connected
+ hass.data[DOMAIN]["devices"].add(device["address"])
+ async_add_entities([KulerskyLight(light)], update_before_add=True)
+
+ # Start initial discovery
+ hass.async_create_task(discover())
+
+ # Perform recurring discovery of new devices
+ async_track_time_interval(hass, discover, DISCOVERY_INTERVAL)
+
+
+class KulerskyLight(LightEntity):
+ """Representation of an Kuler Sky Light."""
+
+ def __init__(self, light: pykulersky.Light):
+ """Initialize a Kuler Sky light."""
+ self._light = light
+ self._hs_color = None
+ self._brightness = None
+ self._white_value = None
+ self._available = True
+
+ async def async_added_to_hass(self) -> None:
+ """Run when entity about to be added to hass."""
+ self.async_on_remove(
+ self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.disconnect)
+ )
+
+ async def async_will_remove_from_hass(self) -> None:
+ """Run when entity will be removed from hass."""
+ await self.hass.async_add_executor_job(self.disconnect)
+
+ def disconnect(self, *args) -> None:
+ """Disconnect the underlying device."""
+ self._light.disconnect()
+
+ @property
+ def name(self):
+ """Return the display name of this light."""
+ return self._light.name
+
+ @property
+ def unique_id(self):
+ """Return the ID of this light."""
+ return self._light.address
+
+ @property
+ def device_info(self):
+ """Device info for this light."""
+ return {
+ "identifiers": {(DOMAIN, self.unique_id)},
+ "name": self.name,
+ "manufacturer": "Brightech",
+ }
+
+ @property
+ def supported_features(self):
+ """Flag supported features."""
+ return SUPPORT_KULERSKY
+
+ @property
+ def brightness(self):
+ """Return the brightness of the light."""
+ return self._brightness
+
+ @property
+ def hs_color(self):
+ """Return the hs color."""
+ return self._hs_color
+
+ @property
+ def white_value(self):
+ """Return the white value of this light between 0..255."""
+ return self._white_value
+
+ @property
+ def is_on(self):
+ """Return true if light is on."""
+ return self._brightness > 0 or self._white_value > 0
+
+ @property
+ def available(self) -> bool:
+ """Return True if entity is available."""
+ return self._available
+
+ def turn_on(self, **kwargs):
+ """Instruct the light to turn on."""
+ default_hs = (0, 0) if self._hs_color is None else self._hs_color
+ hue_sat = kwargs.get(ATTR_HS_COLOR, default_hs)
+
+ default_brightness = 0 if self._brightness is None else self._brightness
+ brightness = kwargs.get(ATTR_BRIGHTNESS, default_brightness)
+
+ default_white_value = 255 if self._white_value is None else self._white_value
+ white_value = kwargs.get(ATTR_WHITE_VALUE, default_white_value)
+
+ if brightness == 0 and white_value == 0 and not kwargs:
+ # If the light would be off, and no additional parameters were
+ # passed, just turn the light on full brightness.
+ brightness = 255
+ white_value = 255
+
+ rgb = color_util.color_hsv_to_RGB(*hue_sat, brightness / 255 * 100)
+
+ self._light.set_color(*rgb, white_value)
+
+ def turn_off(self, **kwargs):
+ """Instruct the light to turn off."""
+ self._light.set_color(0, 0, 0, 0)
+
+ def update(self):
+ """Fetch new state data for this light."""
+ try:
+ if not self._light.connected:
+ self._light.connect()
+ # pylint: disable=invalid-name
+ r, g, b, w = self._light.get_color()
+ except pykulersky.PykulerskyException as exc:
+ if self._available:
+ _LOGGER.warning("Unable to connect to %s: %s", self._light.address, exc)
+ self._available = False
+ return
+ if not self._available:
+ _LOGGER.info("Reconnected to %s", self.entity_id)
+ self._available = True
+
+ hsv = color_util.color_RGB_to_hsv(r, g, b)
+ self._hs_color = hsv[:2]
+ self._brightness = int(round((hsv[2] / 100) * 255))
+ self._white_value = w
diff --git a/homeassistant/components/kulersky/manifest.json b/homeassistant/components/kulersky/manifest.json
new file mode 100644
index 00000000000..4f445e4fc18
--- /dev/null
+++ b/homeassistant/components/kulersky/manifest.json
@@ -0,0 +1,12 @@
+{
+ "domain": "kulersky",
+ "name": "Kuler Sky",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integrations/kulersky",
+ "requirements": [
+ "pykulersky==0.4.0"
+ ],
+ "codeowners": [
+ "@emlove"
+ ]
+}
diff --git a/homeassistant/components/kulersky/strings.json b/homeassistant/components/kulersky/strings.json
new file mode 100644
index 00000000000..ad8f0f41ae7
--- /dev/null
+++ b/homeassistant/components/kulersky/strings.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "step": {
+ "confirm": {
+ "description": "[%key:common::config_flow::description::confirm_setup%]"
+ }
+ },
+ "abort": {
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
+ "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
+ }
+ }
+}
diff --git a/homeassistant/components/kulersky/translations/en.json b/homeassistant/components/kulersky/translations/en.json
new file mode 100644
index 00000000000..f05becffed3
--- /dev/null
+++ b/homeassistant/components/kulersky/translations/en.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "No devices found on the network",
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
+ },
+ "step": {
+ "confirm": {
+ "description": "Do you want to start set up?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lastfm/manifest.json b/homeassistant/components/lastfm/manifest.json
index 1bc38cb0359..adcd7e8d3d0 100644
--- a/homeassistant/components/lastfm/manifest.json
+++ b/homeassistant/components/lastfm/manifest.json
@@ -2,6 +2,6 @@
"domain": "lastfm",
"name": "Last.fm",
"documentation": "https://www.home-assistant.io/integrations/lastfm",
- "requirements": ["pylast==3.3.0"],
+ "requirements": ["pylast==4.0.0"],
"codeowners": []
}
diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py
index 9cf91695d56..faba23a52b9 100644
--- a/homeassistant/components/lcn/__init__.py
+++ b/homeassistant/components/lcn/__init__.py
@@ -233,7 +233,6 @@ async def async_setup(hass, config):
}
connection = pypck.connection.PchkConnectionManager(
- hass.loop,
conf_connection[CONF_HOST],
conf_connection[CONF_PORT],
conf_connection[CONF_USERNAME],
diff --git a/homeassistant/components/lcn/climate.py b/homeassistant/components/lcn/climate.py
index 9634dcf8fb3..8b0f4951bf9 100644
--- a/homeassistant/components/lcn/climate.py
+++ b/homeassistant/components/lcn/climate.py
@@ -17,6 +17,8 @@ from .const import (
)
from .helpers import get_connection
+PARALLEL_UPDATES = 0
+
async def async_setup_platform(
hass, hass_config, async_add_entities, discovery_info=None
@@ -118,14 +120,20 @@ class LcnClimate(LcnDevice, ClimateEntity):
async def async_set_hvac_mode(self, hvac_mode):
"""Set new target hvac mode."""
if hvac_mode == const.HVAC_MODE_HEAT:
+ if not await self.address_connection.lock_regulator(
+ self.regulator_id, False
+ ):
+ return
self._is_on = True
- self.address_connection.lock_regulator(self.regulator_id, False)
+ self.async_write_ha_state()
elif hvac_mode == const.HVAC_MODE_OFF:
+ if not await self.address_connection.lock_regulator(
+ self.regulator_id, True
+ ):
+ return
self._is_on = False
- self.address_connection.lock_regulator(self.regulator_id, True)
self._target_temperature = None
-
- self.async_write_ha_state()
+ self.async_write_ha_state()
async def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
@@ -133,10 +141,11 @@ class LcnClimate(LcnDevice, ClimateEntity):
if temperature is None:
return
+ if not await self.address_connection.var_abs(
+ self.setpoint, temperature, self.unit
+ ):
+ return
self._target_temperature = temperature
- self.address_connection.var_abs(
- self.setpoint, self._target_temperature, self.unit
- )
self.async_write_ha_state()
def input_received(self, input_obj):
diff --git a/homeassistant/components/lcn/cover.py b/homeassistant/components/lcn/cover.py
index 512a7978c8c..ae88441f89e 100644
--- a/homeassistant/components/lcn/cover.py
+++ b/homeassistant/components/lcn/cover.py
@@ -8,6 +8,8 @@ from . import LcnDevice
from .const import CONF_CONNECTIONS, CONF_MOTOR, CONF_REVERSE_TIME, DATA_LCN
from .helpers import get_connection
+PARALLEL_UPDATES = 0
+
async def async_setup_platform(
hass, hass_config, async_add_entities, discovery_info=None
@@ -86,27 +88,34 @@ class LcnOutputsCover(LcnDevice, CoverEntity):
async def async_close_cover(self, **kwargs):
"""Close the cover."""
+ state = pypck.lcn_defs.MotorStateModifier.DOWN
+ if not await self.address_connection.control_motors_outputs(
+ state, self.reverse_time
+ ):
+ return
self._is_opening = False
self._is_closing = True
- state = pypck.lcn_defs.MotorStateModifier.DOWN
- self.address_connection.control_motors_outputs(state, self.reverse_time)
self.async_write_ha_state()
async def async_open_cover(self, **kwargs):
"""Open the cover."""
+ state = pypck.lcn_defs.MotorStateModifier.UP
+ if not await self.address_connection.control_motors_outputs(
+ state, self.reverse_time
+ ):
+ return
self._is_closed = False
self._is_opening = True
self._is_closing = False
- state = pypck.lcn_defs.MotorStateModifier.UP
- self.address_connection.control_motors_outputs(state, self.reverse_time)
self.async_write_ha_state()
async def async_stop_cover(self, **kwargs):
"""Stop the cover."""
+ state = pypck.lcn_defs.MotorStateModifier.STOP
+ if not await self.address_connection.control_motors_outputs(state):
+ return
self._is_closing = False
self._is_opening = False
- state = pypck.lcn_defs.MotorStateModifier.STOP
- self.address_connection.control_motors_outputs(state)
self.async_write_ha_state()
def input_received(self, input_obj):
@@ -176,30 +185,33 @@ class LcnRelayCover(LcnDevice, CoverEntity):
async def async_close_cover(self, **kwargs):
"""Close the cover."""
- 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)
+ if not await self.address_connection.control_motors_relays(states):
+ return
+ self._is_opening = False
+ self._is_closing = True
self.async_write_ha_state()
async def async_open_cover(self, **kwargs):
"""Open the cover."""
+ states = [pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4
+ states[self.motor.value] = pypck.lcn_defs.MotorStateModifier.UP
+ if not await self.address_connection.control_motors_relays(states):
+ return
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)
self.async_write_ha_state()
async def async_stop_cover(self, **kwargs):
"""Stop the cover."""
- 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)
+ if not await self.address_connection.control_motors_relays(states):
+ return
+ self._is_closing = False
+ self._is_opening = False
self.async_write_ha_state()
def input_received(self, input_obj):
diff --git a/homeassistant/components/lcn/light.py b/homeassistant/components/lcn/light.py
index 8fd24c43069..def025e0cf2 100644
--- a/homeassistant/components/lcn/light.py
+++ b/homeassistant/components/lcn/light.py
@@ -21,6 +21,8 @@ from .const import (
)
from .helpers import get_connection
+PARALLEL_UPDATES = 0
+
async def async_setup_platform(
hass, hass_config, async_add_entities, discovery_info=None
@@ -87,8 +89,6 @@ class LcnOutputLight(LcnDevice, LightEntity):
async def async_turn_on(self, **kwargs):
"""Turn the entity on."""
- self._is_on = True
- self._is_dimming_to_zero = False
if ATTR_BRIGHTNESS in kwargs:
percent = int(kwargs[ATTR_BRIGHTNESS] / 255.0 * 100)
else:
@@ -100,12 +100,16 @@ class LcnOutputLight(LcnDevice, LightEntity):
else:
transition = self._transition
- self.address_connection.dim_output(self.output.value, percent, transition)
+ if not await self.address_connection.dim_output(
+ self.output.value, percent, transition
+ ):
+ return
+ self._is_on = True
+ self._is_dimming_to_zero = False
self.async_write_ha_state()
async def async_turn_off(self, **kwargs):
"""Turn the entity off."""
- self._is_on = False
if ATTR_TRANSITION in kwargs:
transition = pypck.lcn_defs.time_to_ramp_value(
kwargs[ATTR_TRANSITION] * 1000
@@ -113,9 +117,12 @@ class LcnOutputLight(LcnDevice, LightEntity):
else:
transition = self._transition
+ if not await self.address_connection.dim_output(
+ self.output.value, 0, transition
+ ):
+ return
self._is_dimming_to_zero = bool(transition)
-
- self.address_connection.dim_output(self.output.value, 0, transition)
+ self._is_on = False
self.async_write_ha_state()
def input_received(self, input_obj):
@@ -157,22 +164,22 @@ class LcnRelayLight(LcnDevice, LightEntity):
async def async_turn_on(self, **kwargs):
"""Turn the entity on."""
- self._is_on = True
states = [pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8
states[self.output.value] = pypck.lcn_defs.RelayStateModifier.ON
- self.address_connection.control_relays(states)
-
+ if not await self.address_connection.control_relays(states):
+ return
+ self._is_on = True
self.async_write_ha_state()
async def async_turn_off(self, **kwargs):
"""Turn the entity off."""
- self._is_on = False
states = [pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8
states[self.output.value] = pypck.lcn_defs.RelayStateModifier.OFF
- self.address_connection.control_relays(states)
-
+ if not await self.address_connection.control_relays(states):
+ return
+ self._is_on = False
self.async_write_ha_state()
def input_received(self, input_obj):
diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json
index dca4436d5c2..f07c4d9c646 100644
--- a/homeassistant/components/lcn/manifest.json
+++ b/homeassistant/components/lcn/manifest.json
@@ -2,6 +2,6 @@
"domain": "lcn",
"name": "LCN",
"documentation": "https://www.home-assistant.io/integrations/lcn",
- "requirements": ["pypck==0.7.4"],
+ "requirements": ["pypck==0.7.7"],
"codeowners": ["@alengwenus"]
}
diff --git a/homeassistant/components/lcn/scene.py b/homeassistant/components/lcn/scene.py
index 0bad1b87efe..cac13ee1653 100644
--- a/homeassistant/components/lcn/scene.py
+++ b/homeassistant/components/lcn/scene.py
@@ -18,6 +18,8 @@ from .const import (
)
from .helpers import get_connection
+PARALLEL_UPDATES = 0
+
async def async_setup_platform(
hass, hass_config, async_add_entities, discovery_info=None
@@ -67,7 +69,7 @@ class LcnScene(LcnDevice, Scene):
async def async_activate(self, **kwargs: Any) -> None:
"""Activate scene."""
- self.address_connection.activate_scene(
+ await self.address_connection.activate_scene(
self.register_id,
self.scene_id,
self.output_ports,
diff --git a/homeassistant/components/lcn/switch.py b/homeassistant/components/lcn/switch.py
index e441ce40383..1d6f7cb6df4 100644
--- a/homeassistant/components/lcn/switch.py
+++ b/homeassistant/components/lcn/switch.py
@@ -8,6 +8,8 @@ from . import LcnDevice
from .const import CONF_CONNECTIONS, CONF_OUTPUT, DATA_LCN, OUTPUT_PORTS
from .helpers import get_connection
+PARALLEL_UPDATES = 0
+
async def async_setup_platform(
hass, hass_config, async_add_entities, discovery_info=None
@@ -57,14 +59,16 @@ class LcnOutputSwitch(LcnDevice, SwitchEntity):
async def async_turn_on(self, **kwargs):
"""Turn the entity on."""
+ if not await self.address_connection.dim_output(self.output.value, 100, 0):
+ return
self._is_on = True
- self.address_connection.dim_output(self.output.value, 100, 0)
self.async_write_ha_state()
async def async_turn_off(self, **kwargs):
"""Turn the entity off."""
+ if not await self.address_connection.dim_output(self.output.value, 0, 0):
+ return
self._is_on = False
- self.address_connection.dim_output(self.output.value, 0, 0)
self.async_write_ha_state()
def input_received(self, input_obj):
@@ -102,20 +106,21 @@ class LcnRelaySwitch(LcnDevice, SwitchEntity):
async def async_turn_on(self, **kwargs):
"""Turn the entity on."""
- self._is_on = True
-
states = [pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8
states[self.output.value] = pypck.lcn_defs.RelayStateModifier.ON
- self.address_connection.control_relays(states)
+ if not await self.address_connection.control_relays(states):
+ return
+ self._is_on = True
self.async_write_ha_state()
async def async_turn_off(self, **kwargs):
"""Turn the entity off."""
- self._is_on = False
states = [pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8
states[self.output.value] = pypck.lcn_defs.RelayStateModifier.OFF
- self.address_connection.control_relays(states)
+ if not await self.address_connection.control_relays(states):
+ return
+ self._is_on = False
self.async_write_ha_state()
def input_received(self, input_obj):
diff --git a/homeassistant/components/life360/translations/hu.json b/homeassistant/components/life360/translations/hu.json
index 327dd40e386..086e3ebf7d2 100644
--- a/homeassistant/components/life360/translations/hu.json
+++ b/homeassistant/components/life360/translations/hu.json
@@ -1,7 +1,15 @@
{
"config": {
+ "abort": {
+ "unknown": "V\u00e1ratlan hiba"
+ },
+ "create_entry": {
+ "default": "A speci\u00e1lis be\u00e1ll\u00edt\u00e1sok megad\u00e1s\u00e1hoz l\u00e1sd: [Life360 dokument\u00e1ci\u00f3] ( {docs_url} )."
+ },
"error": {
- "invalid_username": "\u00c9rv\u00e9nytelen felhaszn\u00e1l\u00f3n\u00e9v"
+ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van",
+ "invalid_username": "\u00c9rv\u00e9nytelen felhaszn\u00e1l\u00f3n\u00e9v",
+ "unknown": "V\u00e1ratlan hiba"
},
"step": {
"user": {
diff --git a/homeassistant/components/life360/translations/ka.json b/homeassistant/components/life360/translations/ka.json
new file mode 100644
index 00000000000..35a27bfc78f
--- /dev/null
+++ b/homeassistant/components/life360/translations/ka.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "invalid_auth": "\u10db\u10ea\u10d3\u10d0\u10e0\u10d8 \u10d0\u10e3\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0",
+ "unknown": "\u10d2\u10d0\u10e3\u10d7\u10d5\u10d0\u10da\u10d8\u10e1\u10ec\u10d8\u10dc\u10d4\u10d1\u10d4\u10da\u10d8 \u10e8\u10d4\u10ea\u10d3\u10dd\u10db\u10d0"
+ },
+ "error": {
+ "already_configured": "\u10d0\u10dc\u10d2\u10d0\u10e0\u10d8\u10e8\u10d8 \u10e3\u10d9\u10d5\u10d4 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0",
+ "invalid_auth": "\u10db\u10ea\u10d3\u10d0\u10e0\u10d8 \u10d0\u10e3\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0",
+ "unknown": "\u10d2\u10d0\u10e3\u10d7\u10d5\u10d0\u10da\u10d8\u10e1\u10ec\u10d8\u10dc\u10d4\u10d1\u10d4\u10da\u10d8 \u10e8\u10d4\u10ea\u10d3\u10dd\u10db\u10d0"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/local_ip/__init__.py b/homeassistant/components/local_ip/__init__.py
index f787c028762..637520aa30c 100644
--- a/homeassistant/components/local_ip/__init__.py
+++ b/homeassistant/components/local_ip/__init__.py
@@ -11,7 +11,7 @@ from .const import DOMAIN, PLATFORM
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.All(
- cv.deprecated(CONF_NAME, invalidation_version="0.110"),
+ cv.deprecated(CONF_NAME),
vol.Schema({vol.Optional(CONF_NAME, default=DOMAIN): cv.string}),
)
},
diff --git a/homeassistant/components/local_ip/strings.json b/homeassistant/components/local_ip/strings.json
index 1859223e657..7e214df2592 100644
--- a/homeassistant/components/local_ip/strings.json
+++ b/homeassistant/components/local_ip/strings.json
@@ -4,9 +4,7 @@
"step": {
"user": {
"title": "Local IP Address",
- "data": {
- "name": "Sensor Name"
- }
+ "description": "[%key:common::config_flow::description::confirm_setup%]"
}
},
"abort": {
diff --git a/homeassistant/components/local_ip/translations/ca.json b/homeassistant/components/local_ip/translations/ca.json
index 7be2a2d70d6..cfd425e3034 100644
--- a/homeassistant/components/local_ip/translations/ca.json
+++ b/homeassistant/components/local_ip/translations/ca.json
@@ -8,6 +8,7 @@
"data": {
"name": "Nom del sensor"
},
+ "description": "Vols comen\u00e7ar la configuraci\u00f3?",
"title": "Adre\u00e7a IP local"
}
}
diff --git a/homeassistant/components/local_ip/translations/cs.json b/homeassistant/components/local_ip/translations/cs.json
index 239d312b3f9..f7254ebaf44 100644
--- a/homeassistant/components/local_ip/translations/cs.json
+++ b/homeassistant/components/local_ip/translations/cs.json
@@ -8,6 +8,7 @@
"data": {
"name": "N\u00e1zev senzoru"
},
+ "description": "Chcete za\u010d\u00edt nastavovat?",
"title": "M\u00edstn\u00ed IP adresa"
}
}
diff --git a/homeassistant/components/local_ip/translations/en.json b/homeassistant/components/local_ip/translations/en.json
index 7f823968f9c..167989b7ba8 100644
--- a/homeassistant/components/local_ip/translations/en.json
+++ b/homeassistant/components/local_ip/translations/en.json
@@ -8,6 +8,7 @@
"data": {
"name": "Sensor Name"
},
+ "description": "Do you want to start set up?",
"title": "Local IP Address"
}
}
diff --git a/homeassistant/components/local_ip/translations/es.json b/homeassistant/components/local_ip/translations/es.json
index 570a566d033..fe9a0ad1414 100644
--- a/homeassistant/components/local_ip/translations/es.json
+++ b/homeassistant/components/local_ip/translations/es.json
@@ -8,6 +8,7 @@
"data": {
"name": "Nombre del sensor"
},
+ "description": "\u00bfQuieres empezar a configurar?",
"title": "Direcci\u00f3n IP local"
}
}
diff --git a/homeassistant/components/local_ip/translations/et.json b/homeassistant/components/local_ip/translations/et.json
index 7bd74eff99c..63e9e251d30 100644
--- a/homeassistant/components/local_ip/translations/et.json
+++ b/homeassistant/components/local_ip/translations/et.json
@@ -8,6 +8,7 @@
"data": {
"name": "Anduri nimi"
},
+ "description": "Kas soovid alustada seadistamist?",
"title": "Kohalik IP-aadress"
}
}
diff --git a/homeassistant/components/local_ip/translations/it.json b/homeassistant/components/local_ip/translations/it.json
index 9173584c9f8..db47d7b9f9f 100644
--- a/homeassistant/components/local_ip/translations/it.json
+++ b/homeassistant/components/local_ip/translations/it.json
@@ -8,6 +8,7 @@
"data": {
"name": "Nome del sensore"
},
+ "description": "Vuoi iniziare la configurazione?",
"title": "Indirizzo IP locale"
}
}
diff --git a/homeassistant/components/local_ip/translations/ka.json b/homeassistant/components/local_ip/translations/ka.json
new file mode 100644
index 00000000000..62dd468b5c1
--- /dev/null
+++ b/homeassistant/components/local_ip/translations/ka.json
@@ -0,0 +1,9 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "description": "\u10d2\u10dc\u10d4\u10d1\u10d0\u10d5\u10d7 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10d8\u10e1 \u10d3\u10d0\u10ec\u10e7\u10d4\u10d1\u10d0?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/local_ip/translations/lb.json b/homeassistant/components/local_ip/translations/lb.json
index 877f1781e04..c94685b1a2a 100644
--- a/homeassistant/components/local_ip/translations/lb.json
+++ b/homeassistant/components/local_ip/translations/lb.json
@@ -8,6 +8,7 @@
"data": {
"name": "Numm vum Sensor"
},
+ "description": "Soll den Ariichtungs Prozess gestart ginn?",
"title": "Lokal IP Adresse"
}
}
diff --git a/homeassistant/components/local_ip/translations/no.json b/homeassistant/components/local_ip/translations/no.json
index cf229e06877..f5686ee5236 100644
--- a/homeassistant/components/local_ip/translations/no.json
+++ b/homeassistant/components/local_ip/translations/no.json
@@ -8,6 +8,7 @@
"data": {
"name": "Sensornavn"
},
+ "description": "Vil du starte oppsettet?",
"title": "Lokal IP-adresse"
}
}
diff --git a/homeassistant/components/local_ip/translations/pl.json b/homeassistant/components/local_ip/translations/pl.json
index 2002b250883..eab29842291 100644
--- a/homeassistant/components/local_ip/translations/pl.json
+++ b/homeassistant/components/local_ip/translations/pl.json
@@ -8,6 +8,7 @@
"data": {
"name": "Nazwa sensora"
},
+ "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?",
"title": "Lokalny adres IP"
}
}
diff --git a/homeassistant/components/local_ip/translations/ru.json b/homeassistant/components/local_ip/translations/ru.json
index 15648bf3f7f..afa78a42778 100644
--- a/homeassistant/components/local_ip/translations/ru.json
+++ b/homeassistant/components/local_ip/translations/ru.json
@@ -8,6 +8,7 @@
"data": {
"name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435"
},
+ "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?",
"title": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441"
}
}
diff --git a/homeassistant/components/local_ip/translations/zh-Hant.json b/homeassistant/components/local_ip/translations/zh-Hant.json
index 3ab8f2b7592..d0238ff7436 100644
--- a/homeassistant/components/local_ip/translations/zh-Hant.json
+++ b/homeassistant/components/local_ip/translations/zh-Hant.json
@@ -8,6 +8,7 @@
"data": {
"name": "\u50b3\u611f\u5668\u540d\u7a31"
},
+ "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f",
"title": "\u672c\u5730 IP \u4f4d\u5740"
}
}
diff --git a/homeassistant/components/locative/translations/hu.json b/homeassistant/components/locative/translations/hu.json
index a8c8bc05539..983ffacfa7d 100644
--- a/homeassistant/components/locative/translations/hu.json
+++ b/homeassistant/components/locative/translations/hu.json
@@ -1,5 +1,8 @@
{
"config": {
+ "create_entry": {
+ "default": "Ha helyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a webhook funkci\u00f3t a Locative alkalmaz\u00e1sban. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: \" {webhook_url} \"\n - M\u00f3dszer: POST \n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1sd a [dokument\u00e1ci\u00f3t] ( {docs_url} )."
+ },
"step": {
"user": {
"description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani a Locative Webhook-ot?",
diff --git a/homeassistant/components/locative/translations/ka.json b/homeassistant/components/locative/translations/ka.json
new file mode 100644
index 00000000000..75c4f0a922c
--- /dev/null
+++ b/homeassistant/components/locative/translations/ka.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0. \u10e8\u10d4\u10e1\u10d0\u10eb\u10da\u10d4\u10d1\u10d4\u10da\u10d8\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0.",
+ "webhook_not_internet_accessible": "\u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 Home Assistant \u10d4\u10d9\u10d6\u10d4\u10db\u10de\u10da\u10d0\u10e0\u10d8 \u10e1\u10d0\u10ed\u10d8\u10e0\u10dd\u10e0\u10d4\u10d1\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10dc\u10d4\u10e2\u10d7\u10d0\u10dc \u10ec\u10d5\u10d3\u10dd\u10db\u10d0\u10e1 webhook \u10e8\u10d4\u10e2\u10e7\u10dd\u10d1\u10d8\u10dc\u10d4\u10d1\u10d4\u10d1\u10d8\u10e1 \u10db\u10d8\u10e1\u10d0\u10e6\u10d4\u10d1\u10d0\u10d3."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/locative/translations/lb.json b/homeassistant/components/locative/translations/lb.json
index bafc7d93459..58b87783d44 100644
--- a/homeassistant/components/locative/translations/lb.json
+++ b/homeassistant/components/locative/translations/lb.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech."
+ "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech.",
+ "webhook_not_internet_accessible": "Deng Home Assistant Instanz muss iwwert Internet accessibel si fir Webhook Noriichten z'empf\u00e4nken."
},
"create_entry": {
"default": "Fir Plazen un Home Assistant ze sch\u00e9cken, muss den Webhook Feature an der Locative App ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nLiest [Dokumentatioun]({docs_url}) fir w\u00e9ider D\u00e9tailer."
diff --git a/homeassistant/components/locative/translations/pl.json b/homeassistant/components/locative/translations/pl.json
index 018bca1f99b..f91afa32f74 100644
--- a/homeassistant/components/locative/translations/pl.json
+++ b/homeassistant/components/locative/translations/pl.json
@@ -5,7 +5,7 @@
"webhook_not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty webhook"
},
"create_entry": {
- "default": "Aby wysy\u0142a\u0107 lokalizacje do Home Assistant, 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/logi_circle/translations/et.json b/homeassistant/components/logi_circle/translations/et.json
index 3aff36b4061..c61fc76a5aa 100644
--- a/homeassistant/components/logi_circle/translations/et.json
+++ b/homeassistant/components/logi_circle/translations/et.json
@@ -8,7 +8,7 @@
},
"error": {
"authorize_url_timeout": "Tuvastamise URL'i loomise ajal\u00f5pp.",
- "follow_link": "Palun j\u00e4rgige linki ja tuvasta enne Submit vajutamist.",
+ "follow_link": "Palun j\u00e4rgi linki ja tuvasta enne Esita nupu vajutamist.",
"invalid_auth": "Tuvastamine eba\u00f5nnestus"
},
"step": {
diff --git a/homeassistant/components/lovelace/translations/cs.json b/homeassistant/components/lovelace/translations/cs.json
index 5c4dc738c6c..f946a859ea2 100644
--- a/homeassistant/components/lovelace/translations/cs.json
+++ b/homeassistant/components/lovelace/translations/cs.json
@@ -4,7 +4,7 @@
"dashboards": "Dashboardy",
"mode": "Re\u017eim",
"resources": "Zdroje",
- "views": "Zobrazen\u00ed"
+ "views": "Pohledy"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/lovelace/translations/hu.json b/homeassistant/components/lovelace/translations/hu.json
new file mode 100644
index 00000000000..aa4934bad82
--- /dev/null
+++ b/homeassistant/components/lovelace/translations/hu.json
@@ -0,0 +1,10 @@
+{
+ "system_health": {
+ "info": {
+ "dashboards": "Ir\u00e1ny\u00edt\u00f3pultok",
+ "mode": "M\u00f3d",
+ "resources": "Er\u0151forr\u00e1sok",
+ "views": "N\u00e9zetek"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lovelace/translations/it.json b/homeassistant/components/lovelace/translations/it.json
index 918057460c0..b82bf3bff6a 100644
--- a/homeassistant/components/lovelace/translations/it.json
+++ b/homeassistant/components/lovelace/translations/it.json
@@ -3,7 +3,8 @@
"info": {
"dashboards": "Plance",
"mode": "Modalit\u00e0",
- "resources": "Risorse"
+ "resources": "Risorse",
+ "views": "Viste"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/lovelace/translations/ka.json b/homeassistant/components/lovelace/translations/ka.json
new file mode 100644
index 00000000000..a10a1f66ac8
--- /dev/null
+++ b/homeassistant/components/lovelace/translations/ka.json
@@ -0,0 +1,10 @@
+{
+ "system_health": {
+ "info": {
+ "dashboards": "\u10d3\u10d4\u10e8\u10d1\u10dd\u10e0\u10d3\u10d8",
+ "mode": "\u10e0\u10d4\u10df\u10d8\u10db\u10d8",
+ "resources": "\u10e0\u10d4\u10e1\u10e3\u10e0\u10e1\u10d4\u10d1\u10d8",
+ "views": "\u10ee\u10d4\u10d3\u10d4\u10d1\u10d8"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lovelace/translations/lb.json b/homeassistant/components/lovelace/translations/lb.json
new file mode 100644
index 00000000000..deda8621adc
--- /dev/null
+++ b/homeassistant/components/lovelace/translations/lb.json
@@ -0,0 +1,10 @@
+{
+ "system_health": {
+ "info": {
+ "dashboards": "Tableau de Bord",
+ "mode": "Modus",
+ "resources": "Ressourcen",
+ "views": "Usiichten"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lovelace/translations/pt.json b/homeassistant/components/lovelace/translations/pt.json
index 920f9447577..dd8cc7cc32d 100644
--- a/homeassistant/components/lovelace/translations/pt.json
+++ b/homeassistant/components/lovelace/translations/pt.json
@@ -3,7 +3,8 @@
"info": {
"dashboards": "Dashboards",
"mode": "Modo",
- "resources": "Recursos"
+ "resources": "Recursos",
+ "views": "Visualiza\u00e7\u00f5es"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/lovelace/translations/sl.json b/homeassistant/components/lovelace/translations/sl.json
new file mode 100644
index 00000000000..4042b5a8d4c
--- /dev/null
+++ b/homeassistant/components/lovelace/translations/sl.json
@@ -0,0 +1,9 @@
+{
+ "system_health": {
+ "info": {
+ "dashboards": "Nadzorne plo\u0161\u010de",
+ "mode": "Na\u010din",
+ "resources": "Viri"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lovelace/translations/zh-Hans.json b/homeassistant/components/lovelace/translations/zh-Hans.json
new file mode 100644
index 00000000000..a30b7b2518b
--- /dev/null
+++ b/homeassistant/components/lovelace/translations/zh-Hans.json
@@ -0,0 +1,10 @@
+{
+ "system_health": {
+ "info": {
+ "dashboards": "\u4eea\u8868\u76d8",
+ "mode": "\u6a21\u5f0f",
+ "resources": "\u8d44\u6e90",
+ "views": "\u89c6\u56fe"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lovelace/translations/zh-Hant.json b/homeassistant/components/lovelace/translations/zh-Hant.json
index b1f4309bd42..0f8ec76e3a3 100644
--- a/homeassistant/components/lovelace/translations/zh-Hant.json
+++ b/homeassistant/components/lovelace/translations/zh-Hant.json
@@ -3,7 +3,8 @@
"info": {
"dashboards": "\u4e3b\u9762\u677f",
"mode": "\u6a21\u5f0f",
- "resources": "\u8cc7\u6e90"
+ "resources": "\u8cc7\u6e90",
+ "views": "\u9762\u677f"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py
index 91e9c96d429..ca1b9aed4ff 100644
--- a/homeassistant/components/luftdaten/__init__.py
+++ b/homeassistant/components/luftdaten/__init__.py
@@ -198,8 +198,9 @@ class LuftDatenData:
try:
await self.client.get_data()
- self.data[DATA_LUFTDATEN] = self.client.values
- self.data[DATA_LUFTDATEN].update(self.client.meta)
+ if self.client.values:
+ self.data[DATA_LUFTDATEN] = self.client.values
+ self.data[DATA_LUFTDATEN].update(self.client.meta)
except LuftdatenError:
_LOGGER.error("Unable to retrieve data from luftdaten.info")
diff --git a/homeassistant/components/luftdaten/sensor.py b/homeassistant/components/luftdaten/sensor.py
index 2d9a8fa85a4..515d8ad577f 100644
--- a/homeassistant/components/luftdaten/sensor.py
+++ b/homeassistant/components/luftdaten/sensor.py
@@ -69,7 +69,10 @@ class LuftdatenSensor(Entity):
def state(self):
"""Return the state of the device."""
if self._data is not None:
- return self._data[self.sensor_type]
+ try:
+ return self._data[self.sensor_type]
+ except KeyError:
+ return None
@property
def unit_of_measurement(self):
@@ -85,7 +88,10 @@ class LuftdatenSensor(Entity):
def unique_id(self) -> str:
"""Return a unique, friendly identifier for this entity."""
if self._data is not None:
- return f"{self._data['sensor_id']}_{self.sensor_type}"
+ try:
+ return f"{self._data['sensor_id']}_{self.sensor_type}"
+ except KeyError:
+ return None
@property
def device_state_attributes(self):
@@ -93,7 +99,10 @@ class LuftdatenSensor(Entity):
self._attrs[ATTR_ATTRIBUTION] = DEFAULT_ATTRIBUTION
if self._data is not None:
- self._attrs[ATTR_SENSOR_ID] = self._data["sensor_id"]
+ try:
+ self._attrs[ATTR_SENSOR_ID] = self._data["sensor_id"]
+ except KeyError:
+ return None
on_map = ATTR_LATITUDE, ATTR_LONGITUDE
no_map = "lat", "long"
diff --git a/homeassistant/components/luftdaten/translations/ka.json b/homeassistant/components/luftdaten/translations/ka.json
new file mode 100644
index 00000000000..c6fa9829942
--- /dev/null
+++ b/homeassistant/components/luftdaten/translations/ka.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "\u10e1\u10d4\u10e0\u10d5\u10d8\u10e1\u10d8 \u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0",
+ "cannot_connect": "\u10d3\u10d0\u10d9\u10d0\u10d5\u10e8\u10e0\u10d4\u10d1\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mailgun/translations/hu.json b/homeassistant/components/mailgun/translations/hu.json
index 2a3265e8c62..51bbe6ef04c 100644
--- a/homeassistant/components/mailgun/translations/hu.json
+++ b/homeassistant/components/mailgun/translations/hu.json
@@ -1,5 +1,8 @@
{
"config": {
+ "create_entry": {
+ "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant programnak, be kell \u00e1ll\u00edtania a [Webhooks Mailgun-al] ( {mailgun_url} ) alkalmaz\u00e1st. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: \" {webhook_url} \"\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/json \n\n L\u00e1sd [a dokument\u00e1ci\u00f3] ( {docs_url} ), hogyan konfigur\u00e1lhatja az automatiz\u00e1l\u00e1sokat a bej\u00f6v\u0151 adatok kezel\u00e9s\u00e9re."
+ },
"step": {
"user": {
"description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani a Mailgunt?",
diff --git a/homeassistant/components/mailgun/translations/ka.json b/homeassistant/components/mailgun/translations/ka.json
new file mode 100644
index 00000000000..75c4f0a922c
--- /dev/null
+++ b/homeassistant/components/mailgun/translations/ka.json
@@ -0,0 +1,8 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0. \u10e8\u10d4\u10e1\u10d0\u10eb\u10da\u10d4\u10d1\u10d4\u10da\u10d8\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0.",
+ "webhook_not_internet_accessible": "\u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 Home Assistant \u10d4\u10d9\u10d6\u10d4\u10db\u10de\u10da\u10d0\u10e0\u10d8 \u10e1\u10d0\u10ed\u10d8\u10e0\u10dd\u10e0\u10d4\u10d1\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10dc\u10d4\u10e2\u10d7\u10d0\u10dc \u10ec\u10d5\u10d3\u10dd\u10db\u10d0\u10e1 webhook \u10e8\u10d4\u10e2\u10e7\u10dd\u10d1\u10d8\u10dc\u10d4\u10d1\u10d4\u10d1\u10d8\u10e1 \u10db\u10d8\u10e1\u10d0\u10e6\u10d4\u10d1\u10d0\u10d3."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mailgun/translations/lb.json b/homeassistant/components/mailgun/translations/lb.json
index 43564297cd5..6147d20fe69 100644
--- a/homeassistant/components/mailgun/translations/lb.json
+++ b/homeassistant/components/mailgun/translations/lb.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech."
+ "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech.",
+ "webhook_not_internet_accessible": "Deng Home Assistant Instanz muss iwwert Internet accessibel si fir Webhook Noriichten z'empf\u00e4nken."
},
"create_entry": {
"default": "Fir Evenementer un Home Assistant ze sch\u00e9cken, mussen [Webhooks mat Mailgun]({mailgun_url}) ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nLiest [Dokumentatioun]({docs_url}) w\u00e9i een Automatiounen ariicht welch eingehend Donn\u00e9\u00eb trait\u00e9ieren."
diff --git a/homeassistant/components/mailgun/translations/pl.json b/homeassistant/components/mailgun/translations/pl.json
index fac09505ffa..931cd7a8167 100644
--- a/homeassistant/components/mailgun/translations/pl.json
+++ b/homeassistant/components/mailgun/translations/pl.json
@@ -5,7 +5,7 @@
"webhook_not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty webhook"
},
"create_entry": {
- "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant, 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/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json
index 7d6e92ed274..b834cbc0aab 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.11.01.1"],
+ "requirements": ["youtube_dl==2020.11.12"],
"dependencies": ["media_player"],
"codeowners": [],
"quality_scale": "internal"
diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py
index 71db60baa2e..d670acb7af9 100644
--- a/homeassistant/components/media_player/__init__.py
+++ b/homeassistant/components/media_player/__init__.py
@@ -7,7 +7,7 @@ import functools as ft
import hashlib
import logging
import secrets
-from typing import List, Optional
+from typing import List, Optional, Tuple
from urllib.parse import urlparse
from aiohttp import web
@@ -434,8 +434,11 @@ class MediaPlayerEntity(Entity):
return await self._async_fetch_image_from_cache(url)
async def async_get_browse_image(
- self, media_content_type, media_content_id, media_image_id=None
- ):
+ self,
+ media_content_type: str,
+ media_content_id: str,
+ media_image_id: Optional[str] = None,
+ ) -> Tuple[Optional[str], Optional[str]]:
"""
Optionally fetch internally accessible image for media browser.
@@ -906,8 +909,11 @@ class MediaPlayerEntity(Entity):
return content, content_type
def get_browse_image_url(
- self, media_content_type, media_content_id, media_image_id=None
- ):
+ self,
+ media_content_type: str,
+ media_content_id: str,
+ media_image_id: Optional[str] = None,
+ ) -> str:
"""Generate an url for a media browser image."""
url_path = (
f"/api/media_player_proxy/{self.entity_id}/browse_media"
diff --git a/homeassistant/components/meraki/device_tracker.py b/homeassistant/components/meraki/device_tracker.py
index 614c2943530..55186d63146 100644
--- a/homeassistant/components/meraki/device_tracker.py
+++ b/homeassistant/components/meraki/device_tracker.py
@@ -36,6 +36,7 @@ class MerakiView(HomeAssistantView):
url = URL
name = "api:meraki"
+ requires_auth = False
def __init__(self, config, async_see):
"""Initialize Meraki URL endpoints."""
diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py
index f505d1db1a3..c0c8c11c644 100644
--- a/homeassistant/components/met/weather.py
+++ b/homeassistant/components/met/weather.py
@@ -21,8 +21,10 @@ from homeassistant.const import (
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_NAME,
+ LENGTH_INCHES,
LENGTH_KILOMETERS,
LENGTH_MILES,
+ LENGTH_MILLIMETERS,
PRESSURE_HPA,
PRESSURE_INHG,
TEMP_CELSIUS,
@@ -32,7 +34,14 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util.distance import convert as convert_distance
from homeassistant.util.pressure import convert as convert_pressure
-from .const import ATTR_MAP, CONDITIONS_MAP, CONF_TRACK_HOME, DOMAIN, FORECAST_MAP
+from .const import (
+ ATTR_FORECAST_PRECIPITATION,
+ ATTR_MAP,
+ CONDITIONS_MAP,
+ CONF_TRACK_HOME,
+ DOMAIN,
+ FORECAST_MAP,
+)
_LOGGER = logging.getLogger(__name__)
@@ -217,8 +226,18 @@ class MetWeather(CoordinatorEntity, WeatherEntity):
if not set(met_item).issuperset(required_keys):
continue
ha_item = {
- k: met_item[v] for k, v in FORECAST_MAP.items() if met_item.get(v)
+ k: met_item[v]
+ for k, v in FORECAST_MAP.items()
+ if met_item.get(v) is not None
}
+ if not self._is_metric:
+ if ATTR_FORECAST_PRECIPITATION in ha_item:
+ precip_inches = convert_distance(
+ ha_item[ATTR_FORECAST_PRECIPITATION],
+ LENGTH_MILLIMETERS,
+ LENGTH_INCHES,
+ )
+ ha_item[ATTR_FORECAST_PRECIPITATION] = round(precip_inches, 2)
if ha_item.get(ATTR_FORECAST_CONDITION):
ha_item[ATTR_FORECAST_CONDITION] = format_condition(
ha_item[ATTR_FORECAST_CONDITION]
diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py
index 3ae24cbc71b..bfbaa828ea7 100644
--- a/homeassistant/components/meteo_france/const.py
+++ b/homeassistant/components/meteo_france/const.py
@@ -1,5 +1,22 @@
"""Meteo-France component constants."""
+from homeassistant.components.weather import (
+ ATTR_CONDITION_CLEAR_NIGHT,
+ ATTR_CONDITION_CLOUDY,
+ ATTR_CONDITION_EXCEPTIONAL,
+ ATTR_CONDITION_FOG,
+ ATTR_CONDITION_HAIL,
+ ATTR_CONDITION_LIGHTNING,
+ ATTR_CONDITION_LIGHTNING_RAINY,
+ ATTR_CONDITION_PARTLYCLOUDY,
+ ATTR_CONDITION_POURING,
+ ATTR_CONDITION_RAINY,
+ ATTR_CONDITION_SNOWY,
+ ATTR_CONDITION_SNOWY_RAINY,
+ ATTR_CONDITION_SUNNY,
+ ATTR_CONDITION_WINDY,
+ ATTR_CONDITION_WINDY_VARIANT,
+)
from homeassistant.const import (
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
@@ -126,27 +143,31 @@ SENSOR_TYPES = {
}
CONDITION_CLASSES = {
- "clear-night": ["Nuit Claire", "Nuit claire"],
- "cloudy": ["Très nuageux", "Couvert"],
- "fog": [
+ ATTR_CONDITION_CLEAR_NIGHT: ["Nuit Claire", "Nuit claire"],
+ ATTR_CONDITION_CLOUDY: ["Très nuageux", "Couvert"],
+ ATTR_CONDITION_FOG: [
"Brume ou bancs de brouillard",
"Brume",
"Brouillard",
"Brouillard givrant",
"Bancs de Brouillard",
],
- "hail": ["Risque de grêle", "Risque de grèle"],
- "lightning": ["Risque d'orages", "Orages"],
- "lightning-rainy": ["Pluie orageuses", "Pluies orageuses", "Averses orageuses"],
- "partlycloudy": [
+ ATTR_CONDITION_HAIL: ["Risque de grêle", "Risque de grèle"],
+ ATTR_CONDITION_LIGHTNING: ["Risque d'orages", "Orages"],
+ ATTR_CONDITION_LIGHTNING_RAINY: [
+ "Pluie orageuses",
+ "Pluies orageuses",
+ "Averses orageuses",
+ ],
+ ATTR_CONDITION_PARTLYCLOUDY: [
"Ciel voilé",
"Ciel voilé nuit",
"Éclaircies",
"Eclaircies",
"Peu nuageux",
],
- "pouring": ["Pluie forte"],
- "rainy": [
+ ATTR_CONDITION_POURING: ["Pluie forte"],
+ ATTR_CONDITION_RAINY: [
"Bruine / Pluie faible",
"Bruine",
"Pluie faible",
@@ -158,16 +179,16 @@ CONDITION_CLASSES = {
"Averses",
"Pluie",
],
- "snowy": [
+ ATTR_CONDITION_SNOWY: [
"Neige / Averses de neige",
"Neige",
"Averses de neige",
"Neige forte",
"Quelques flocons",
],
- "snowy-rainy": ["Pluie et neige", "Pluie verglaçante"],
- "sunny": ["Ensoleillé"],
- "windy": [],
- "windy-variant": [],
- "exceptional": [],
+ ATTR_CONDITION_SNOWY_RAINY: ["Pluie et neige", "Pluie verglaçante"],
+ ATTR_CONDITION_SUNNY: ["Ensoleillé"],
+ ATTR_CONDITION_WINDY: [],
+ ATTR_CONDITION_WINDY_VARIANT: [],
+ ATTR_CONDITION_EXCEPTIONAL: [],
}
diff --git a/homeassistant/components/meteo_france/translations/hu.json b/homeassistant/components/meteo_france/translations/hu.json
index 83333c60fe8..dc74eafa409 100644
--- a/homeassistant/components/meteo_france/translations/hu.json
+++ b/homeassistant/components/meteo_france/translations/hu.json
@@ -4,6 +4,9 @@
"already_configured": "A v\u00e1ros m\u00e1r konfigur\u00e1lva van",
"unknown": "Ismeretlen hiba: k\u00e9rj\u00fck, pr\u00f3b\u00e1lja \u00fajra k\u00e9s\u0151bb"
},
+ "error": {
+ "empty": "Nincs eredm\u00e9ny a v\u00e1roskeres\u00e9sben: ellen\u0151rizze a v\u00e1ros mez\u0151t"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/metoffice/const.py b/homeassistant/components/metoffice/const.py
index b088672b8a5..e710911ee59 100644
--- a/homeassistant/components/metoffice/const.py
+++ b/homeassistant/components/metoffice/const.py
@@ -1,6 +1,23 @@
"""Constants for Met Office Integration."""
from datetime import timedelta
+from homeassistant.components.weather import (
+ ATTR_CONDITION_CLOUDY,
+ ATTR_CONDITION_EXCEPTIONAL,
+ ATTR_CONDITION_FOG,
+ ATTR_CONDITION_HAIL,
+ ATTR_CONDITION_LIGHTNING,
+ ATTR_CONDITION_LIGHTNING_RAINY,
+ ATTR_CONDITION_PARTLYCLOUDY,
+ ATTR_CONDITION_POURING,
+ ATTR_CONDITION_RAINY,
+ ATTR_CONDITION_SNOWY,
+ ATTR_CONDITION_SNOWY_RAINY,
+ ATTR_CONDITION_SUNNY,
+ ATTR_CONDITION_WINDY,
+ ATTR_CONDITION_WINDY_VARIANT,
+)
+
DOMAIN = "metoffice"
DEFAULT_NAME = "Met Office"
@@ -16,20 +33,20 @@ METOFFICE_NAME = "metoffice_name"
MODE_3HOURLY = "3hourly"
CONDITION_CLASSES = {
- "cloudy": ["7", "8"],
- "fog": ["5", "6"],
- "hail": ["19", "20", "21"],
- "lightning": ["30"],
- "lightning-rainy": ["28", "29"],
- "partlycloudy": ["2", "3"],
- "pouring": ["13", "14", "15"],
- "rainy": ["9", "10", "11", "12"],
- "snowy": ["22", "23", "24", "25", "26", "27"],
- "snowy-rainy": ["16", "17", "18"],
- "sunny": ["0", "1"],
- "windy": [],
- "windy-variant": [],
- "exceptional": [],
+ ATTR_CONDITION_CLOUDY: ["7", "8"],
+ ATTR_CONDITION_FOG: ["5", "6"],
+ ATTR_CONDITION_HAIL: ["19", "20", "21"],
+ ATTR_CONDITION_LIGHTNING: ["30"],
+ ATTR_CONDITION_LIGHTNING_RAINY: ["28", "29"],
+ ATTR_CONDITION_PARTLYCLOUDY: ["2", "3"],
+ ATTR_CONDITION_POURING: ["13", "14", "15"],
+ ATTR_CONDITION_RAINY: ["9", "10", "11", "12"],
+ ATTR_CONDITION_SNOWY: ["22", "23", "24", "25", "26", "27"],
+ ATTR_CONDITION_SNOWY_RAINY: ["16", "17", "18"],
+ ATTR_CONDITION_SUNNY: ["0", "1"],
+ ATTR_CONDITION_WINDY: [],
+ ATTR_CONDITION_WINDY_VARIANT: [],
+ ATTR_CONDITION_EXCEPTIONAL: [],
}
VISIBILITY_CLASSES = {
diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py
index 67ed692691b..0bb94242d64 100644
--- a/homeassistant/components/mill/climate.py
+++ b/homeassistant/components/mill/climate.py
@@ -114,6 +114,8 @@ class MillHeater(ClimateEntity):
"heating": self._heater.is_heating,
"controlled_by_tibber": self._heater.tibber_control,
"heater_generation": 1 if self._heater.is_gen1 else 2,
+ "consumption_today": self._heater.day_consumption,
+ "consumption_total": self._heater.total_consumption,
}
if self._heater.room:
res["room"] = self._heater.room.name
diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json
index 684be0479bd..d0faa1e2ed5 100644
--- a/homeassistant/components/mill/manifest.json
+++ b/homeassistant/components/mill/manifest.json
@@ -2,7 +2,7 @@
"domain": "mill",
"name": "Mill",
"documentation": "https://www.home-assistant.io/integrations/mill",
- "requirements": ["millheater==0.3.4"],
+ "requirements": ["millheater==0.4.0"],
"codeowners": ["@danielhiversen"],
"config_flow": true
}
diff --git a/homeassistant/components/mill/translations/ka.json b/homeassistant/components/mill/translations/ka.json
new file mode 100644
index 00000000000..e965142dbe1
--- /dev/null
+++ b/homeassistant/components/mill/translations/ka.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u10d3\u10d0\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d4\u10d1\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/minecraft_server/translations/hu.json b/homeassistant/components/minecraft_server/translations/hu.json
index 78778568e51..7a8958bd7c6 100644
--- a/homeassistant/components/minecraft_server/translations/hu.json
+++ b/homeassistant/components/minecraft_server/translations/hu.json
@@ -3,6 +3,9 @@
"abort": {
"already_configured": "Kiszolg\u00e1l\u00f3 m\u00e1r konfigur\u00e1lva van."
},
+ "error": {
+ "cannot_connect": "Nem siker\u00fclt csatlakozni a szerverhez. K\u00e9rj\u00fck, ellen\u0151rizze a gazdag\u00e9pet \u00e9s a portot, majd pr\u00f3b\u00e1lkozzon \u00fajra. Gondoskodjon arr\u00f3l, hogy a szerveren legal\u00e1bb a Minecraft 1.7-es verzi\u00f3j\u00e1t futtassa."
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py
index 264017796aa..3bc95bf3e05 100644
--- a/homeassistant/components/mobile_app/__init__.py
+++ b/homeassistant/components/mobile_app/__init__.py
@@ -123,6 +123,7 @@ async def async_unload_entry(hass, entry):
webhook_unregister(hass, webhook_id)
del hass.data[DOMAIN][DATA_CONFIG_ENTRIES][webhook_id]
+ del hass.data[DOMAIN][DATA_DEVICES][webhook_id]
await hass_notify.async_reload(hass, DOMAIN)
return True
diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py
index 6174e34f57a..b35468a6fb3 100644
--- a/homeassistant/components/mobile_app/const.py
+++ b/homeassistant/components/mobile_app/const.py
@@ -15,6 +15,7 @@ DATA_DELETED_IDS = "deleted_ids"
DATA_DEVICES = "devices"
DATA_SENSOR = "sensor"
DATA_STORE = "store"
+DATA_NOTIFY = "notify"
ATTR_APP_DATA = "app_data"
ATTR_APP_ID = "app_id"
diff --git a/homeassistant/components/mobile_app/device_action.py b/homeassistant/components/mobile_app/device_action.py
new file mode 100644
index 00000000000..2592d4b486b
--- /dev/null
+++ b/homeassistant/components/mobile_app/device_action.py
@@ -0,0 +1,87 @@
+"""Provides device actions for Mobile App."""
+from typing import List, Optional
+
+import voluptuous as vol
+
+from homeassistant.components import notify
+from homeassistant.components.device_automation import InvalidDeviceAutomationConfig
+from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_TYPE
+from homeassistant.core import Context, HomeAssistant
+from homeassistant.helpers import config_validation as cv, template
+
+from .const import DOMAIN
+from .util import get_notify_service, supports_push, webhook_id_from_device_id
+
+ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
+ {
+ vol.Required(CONF_TYPE): "notify",
+ vol.Required(notify.ATTR_MESSAGE): cv.template,
+ vol.Optional(notify.ATTR_TITLE): cv.template,
+ vol.Optional(notify.ATTR_DATA): cv.template_complex,
+ }
+)
+
+
+async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]:
+ """List device actions for Mobile App devices."""
+ webhook_id = webhook_id_from_device_id(hass, device_id)
+
+ if webhook_id is None or not supports_push(hass, webhook_id):
+ return []
+
+ return [{CONF_DEVICE_ID: device_id, CONF_DOMAIN: DOMAIN, CONF_TYPE: "notify"}]
+
+
+async def async_call_action_from_config(
+ hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context]
+) -> None:
+ """Execute a device action."""
+ webhook_id = webhook_id_from_device_id(hass, config[CONF_DEVICE_ID])
+
+ if webhook_id is None:
+ raise InvalidDeviceAutomationConfig(
+ "Unable to resolve webhook ID from the device ID"
+ )
+
+ service_name = get_notify_service(hass, webhook_id)
+
+ if service_name is None:
+ raise InvalidDeviceAutomationConfig(
+ "Unable to find notify service for webhook ID"
+ )
+
+ service_data = {notify.ATTR_TARGET: webhook_id}
+
+ # Render it here because we have access to variables here.
+ for key in (notify.ATTR_MESSAGE, notify.ATTR_TITLE, notify.ATTR_DATA):
+ if key not in config:
+ continue
+
+ value_template = config[key]
+ template.attach(hass, value_template)
+
+ try:
+ service_data[key] = template.render_complex(value_template, variables)
+ except template.TemplateError as err:
+ raise InvalidDeviceAutomationConfig(
+ f"Error rendering {key}: {err}"
+ ) from err
+
+ await hass.services.async_call(
+ notify.DOMAIN, service_name, service_data, blocking=True, context=context
+ )
+
+
+async def async_get_action_capabilities(hass, config):
+ """List action capabilities."""
+ if config[CONF_TYPE] != "notify":
+ return {}
+
+ return {
+ "extra_fields": vol.Schema(
+ {
+ vol.Required(notify.ATTR_MESSAGE): str,
+ vol.Optional(notify.ATTR_TITLE): str,
+ }
+ )
+ }
diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py
index 04d308a5a05..b3482a70fb9 100644
--- a/homeassistant/components/mobile_app/notify.py
+++ b/homeassistant/components/mobile_app/notify.py
@@ -35,8 +35,10 @@ from .const import (
ATTR_PUSH_TOKEN,
ATTR_PUSH_URL,
DATA_CONFIG_ENTRIES,
+ DATA_NOTIFY,
DOMAIN,
)
+from .util import supports_push
_LOGGER = logging.getLogger(__name__)
@@ -44,15 +46,13 @@ _LOGGER = logging.getLogger(__name__)
def push_registrations(hass):
"""Return a dictionary of push enabled registrations."""
targets = {}
+
for webhook_id, entry in hass.data[DOMAIN][DATA_CONFIG_ENTRIES].items():
- data = entry.data
- app_data = data[ATTR_APP_DATA]
- if ATTR_PUSH_TOKEN in app_data and ATTR_PUSH_URL in app_data:
- device_name = data[ATTR_DEVICE_NAME]
- if device_name in targets:
- _LOGGER.warning("Found duplicate device name %s", device_name)
- continue
- targets[device_name] = webhook_id
+ if not supports_push(hass, webhook_id):
+ continue
+
+ targets[entry.data[ATTR_DEVICE_NAME]] = webhook_id
+
return targets
@@ -84,7 +84,8 @@ def log_rate_limits(hass, device_name, resp, level=logging.INFO):
async def async_get_service(hass, config, discovery_info=None):
"""Get the mobile_app notification service."""
session = async_get_clientsession(hass)
- return MobileAppNotificationService(session)
+ service = hass.data[DOMAIN][DATA_NOTIFY] = MobileAppNotificationService(session)
+ return service
class MobileAppNotificationService(BaseNotificationService):
diff --git a/homeassistant/components/mobile_app/strings.json b/homeassistant/components/mobile_app/strings.json
index fd1cddbcb5f..b18f3e7265c 100644
--- a/homeassistant/components/mobile_app/strings.json
+++ b/homeassistant/components/mobile_app/strings.json
@@ -8,5 +8,10 @@
"abort": {
"install_app": "Open the mobile app to set up the integration with Home Assistant. See [the docs]({apps_url}) for a list of compatible apps."
}
+ },
+ "device_automation": {
+ "action_type": {
+ "notify": "Send a notification"
+ }
}
}
diff --git a/homeassistant/components/mobile_app/translations/cs.json b/homeassistant/components/mobile_app/translations/cs.json
index 5b5a68db066..467536cc5ec 100644
--- a/homeassistant/components/mobile_app/translations/cs.json
+++ b/homeassistant/components/mobile_app/translations/cs.json
@@ -8,5 +8,10 @@
"description": "Chcete nastavit komponentu Mobiln\u00ed aplikace?"
}
}
+ },
+ "device_automation": {
+ "action_type": {
+ "notify": "Odeslat ozn\u00e1men\u00ed"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/mobile_app/translations/en.json b/homeassistant/components/mobile_app/translations/en.json
index 6def5e98582..34631f86afa 100644
--- a/homeassistant/components/mobile_app/translations/en.json
+++ b/homeassistant/components/mobile_app/translations/en.json
@@ -8,5 +8,10 @@
"description": "Do you want to set up the Mobile App component?"
}
}
+ },
+ "device_automation": {
+ "action_type": {
+ "notify": "Send a notification"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/mobile_app/translations/es.json b/homeassistant/components/mobile_app/translations/es.json
index c442cc88a26..43ba004ac72 100644
--- a/homeassistant/components/mobile_app/translations/es.json
+++ b/homeassistant/components/mobile_app/translations/es.json
@@ -8,5 +8,10 @@
"description": "\u00bfQuieres configurar el componente de la aplicaci\u00f3n para el m\u00f3vil?"
}
}
+ },
+ "device_automation": {
+ "action_type": {
+ "notify": "Enviar una notificaci\u00f3n"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/mobile_app/translations/et.json b/homeassistant/components/mobile_app/translations/et.json
index e5c01546976..41d2be9d455 100644
--- a/homeassistant/components/mobile_app/translations/et.json
+++ b/homeassistant/components/mobile_app/translations/et.json
@@ -8,5 +8,10 @@
"description": "Kas soovid seadistada mobiilirakenduse sidumist?"
}
}
+ },
+ "device_automation": {
+ "action_type": {
+ "notify": "Saada teavitus"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/mobile_app/translations/lb.json b/homeassistant/components/mobile_app/translations/lb.json
index 6e15dd34634..e3674292888 100644
--- a/homeassistant/components/mobile_app/translations/lb.json
+++ b/homeassistant/components/mobile_app/translations/lb.json
@@ -8,5 +8,10 @@
"description": "Soll d'Mobil App konfigur\u00e9iert ginn?"
}
}
+ },
+ "device_automation": {
+ "action_type": {
+ "notify": "Eng Notifikatioun sch\u00e9cken"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/mobile_app/translations/no.json b/homeassistant/components/mobile_app/translations/no.json
index 346a5eb6b6d..65d465723b1 100644
--- a/homeassistant/components/mobile_app/translations/no.json
+++ b/homeassistant/components/mobile_app/translations/no.json
@@ -8,5 +8,10 @@
"description": "Vil du sette opp mobilapp-komponenten?"
}
}
+ },
+ "device_automation": {
+ "action_type": {
+ "notify": "Send et varsel"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/mobile_app/translations/pl.json b/homeassistant/components/mobile_app/translations/pl.json
index 15574f6d757..cd083447634 100644
--- a/homeassistant/components/mobile_app/translations/pl.json
+++ b/homeassistant/components/mobile_app/translations/pl.json
@@ -1,12 +1,17 @@
{
"config": {
"abort": {
- "install_app": "Otw\u00f3rz aplikacj\u0119 mobiln\u0105, aby skonfigurowa\u0107 integracj\u0119 z Home Assistant. Zapoznaj si\u0119 z [dokumentacj\u0105]({apps_url}), by zobaczy\u0107 list\u0119 kompatybilnych aplikacji."
+ "install_app": "Otw\u00f3rz aplikacj\u0119 mobiln\u0105, aby skonfigurowa\u0107 integracj\u0119 z Home Assistantem. Zapoznaj si\u0119 z [dokumentacj\u0105]({apps_url}), by zobaczy\u0107 list\u0119 kompatybilnych aplikacji."
},
"step": {
"confirm": {
"description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?"
}
}
+ },
+ "device_automation": {
+ "action_type": {
+ "notify": "wy\u015blij powiadomienie"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/mobile_app/translations/ru.json b/homeassistant/components/mobile_app/translations/ru.json
index 7bb103b852e..fc4496ba1d8 100644
--- a/homeassistant/components/mobile_app/translations/ru.json
+++ b/homeassistant/components/mobile_app/translations/ru.json
@@ -8,5 +8,10 @@
"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?"
}
}
+ },
+ "device_automation": {
+ "action_type": {
+ "notify": "\u041e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0435"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/mobile_app/translations/zh-Hant.json b/homeassistant/components/mobile_app/translations/zh-Hant.json
index e09710e4235..d54afd94f54 100644
--- a/homeassistant/components/mobile_app/translations/zh-Hant.json
+++ b/homeassistant/components/mobile_app/translations/zh-Hant.json
@@ -8,5 +8,10 @@
"description": "\u662f\u5426\u8981\u8a2d\u5b9a\u624b\u6a5f App \u5143\u4ef6\uff1f"
}
}
+ },
+ "device_automation": {
+ "action_type": {
+ "notify": "\u50b3\u9001\u901a\u77e5"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/mobile_app/util.py b/homeassistant/components/mobile_app/util.py
new file mode 100644
index 00000000000..60dfe242e04
--- /dev/null
+++ b/homeassistant/components/mobile_app/util.py
@@ -0,0 +1,50 @@
+"""Mobile app utility functions."""
+from typing import TYPE_CHECKING, Optional
+
+from homeassistant.core import callback
+
+from .const import (
+ ATTR_APP_DATA,
+ ATTR_PUSH_TOKEN,
+ ATTR_PUSH_URL,
+ DATA_CONFIG_ENTRIES,
+ DATA_DEVICES,
+ DATA_NOTIFY,
+ DOMAIN,
+)
+
+if TYPE_CHECKING:
+ from .notify import MobileAppNotificationService
+
+
+@callback
+def webhook_id_from_device_id(hass, device_id: str) -> Optional[str]:
+ """Get webhook ID from device ID."""
+ if DOMAIN not in hass.data:
+ return None
+
+ for cur_webhook_id, cur_device in hass.data[DOMAIN][DATA_DEVICES].items():
+ if cur_device.id == device_id:
+ return cur_webhook_id
+
+ return None
+
+
+@callback
+def supports_push(hass, webhook_id: str) -> bool:
+ """Return if push notifications is supported."""
+ config_entry = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][webhook_id]
+ app_data = config_entry.data[ATTR_APP_DATA]
+ return ATTR_PUSH_TOKEN in app_data and ATTR_PUSH_URL in app_data
+
+
+@callback
+def get_notify_service(hass, webhook_id: str) -> Optional[str]:
+ """Return the notify service for this webhook ID."""
+ notify_service: "MobileAppNotificationService" = hass.data[DOMAIN][DATA_NOTIFY]
+
+ for target_service, target_webhook_id in notify_service.registered_targets.items():
+ if target_webhook_id == webhook_id:
+ return target_service
+
+ return None
diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py
index d57d0f761c6..b09a38f082e 100644
--- a/homeassistant/components/modbus/climate.py
+++ b/homeassistant/components/modbus/climate.py
@@ -255,7 +255,15 @@ class ModbusThermostat(ClimateEntity):
byte_string = b"".join(
[x.to_bytes(2, byteorder="big") for x in result.registers]
)
- val = struct.unpack(self._structure, byte_string)[0]
+ val = struct.unpack(self._structure, byte_string)
+ if len(val) != 1 or not isinstance(val[0], (float, int)):
+ _LOGGER.error(
+ "Unable to parse result as a single int or float value; adjust your configuration. Result: %s",
+ str(val),
+ )
+ return -1
+
+ val = val[0]
register_value = format(
(self._scale * val) + self._offset, f".{self._precision}f"
)
diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py
index bd6b5e349c3..656e5e2986d 100644
--- a/homeassistant/components/modbus/sensor.py
+++ b/homeassistant/components/modbus/sensor.py
@@ -250,16 +250,32 @@ class ModbusRegisterSensor(RestoreEntity):
registers.reverse()
byte_string = b"".join([x.to_bytes(2, byteorder="big") for x in registers])
- if self._data_type != DATA_TYPE_STRING:
- val = struct.unpack(self._structure, byte_string)[0]
- val = self._scale * val + self._offset
- if isinstance(val, int):
- self._value = str(val)
- if self._precision > 0:
- self._value += "." + "0" * self._precision
- else:
- self._value = f"{val:.{self._precision}f}"
- else:
+ if self._data_type == DATA_TYPE_STRING:
self._value = byte_string.decode()
+ else:
+ val = struct.unpack(self._structure, byte_string)
+
+ # Issue: https://github.com/home-assistant/core/issues/41944
+ # If unpack() returns a tuple greater than 1, don't try to process the value.
+ # Instead, return the values of unpack(...) separated by commas.
+ if len(val) > 1:
+ self._value = ",".join(map(str, val))
+ else:
+ val = val[0]
+
+ # Apply scale and precision to floats and ints
+ if isinstance(val, (float, int)):
+ val = self._scale * val + self._offset
+
+ # We could convert int to float, and the code would still work; however
+ # we lose some precision, and unit tests will fail. Therefore, we do
+ # the conversion only when it's absolutely necessary.
+ if isinstance(val, int) and self._precision == 0:
+ self._value = str(val)
+ else:
+ self._value = f"{float(val):.{self._precision}f}"
+ else:
+ # Don't process remaining datatypes (bytes and booleans)
+ self._value = str(val)
self._available = True
diff --git a/homeassistant/components/motion_blinds/__init__.py b/homeassistant/components/motion_blinds/__init__.py
new file mode 100644
index 00000000000..72929e1ecb7
--- /dev/null
+++ b/homeassistant/components/motion_blinds/__init__.py
@@ -0,0 +1,101 @@
+"""The motion_blinds component."""
+from asyncio import TimeoutError as AsyncioTimeoutError
+from datetime import timedelta
+import logging
+from socket import timeout
+
+from homeassistant import config_entries, core
+from homeassistant.const import CONF_API_KEY, CONF_HOST
+from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers import device_registry as dr
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
+
+from .const import DOMAIN, KEY_COORDINATOR, KEY_GATEWAY, MANUFACTURER
+from .gateway import ConnectMotionGateway
+
+_LOGGER = logging.getLogger(__name__)
+
+MOTION_PLATFORMS = ["cover", "sensor"]
+
+
+async def async_setup(hass: core.HomeAssistant, config: dict):
+ """Set up the Motion Blinds component."""
+ return True
+
+
+async def async_setup_entry(
+ hass: core.HomeAssistant, entry: config_entries.ConfigEntry
+):
+ """Set up the motion_blinds components from a config entry."""
+ hass.data.setdefault(DOMAIN, {})
+ host = entry.data[CONF_HOST]
+ key = entry.data[CONF_API_KEY]
+
+ # Connect to motion gateway
+ connect_gateway_class = ConnectMotionGateway(hass)
+ if not await connect_gateway_class.async_connect_gateway(host, key):
+ raise ConfigEntryNotReady
+ motion_gateway = connect_gateway_class.gateway_device
+
+ def update_gateway():
+ """Call all updates using one async_add_executor_job."""
+ motion_gateway.Update()
+ for blind in motion_gateway.device_list.values():
+ blind.Update()
+
+ async def async_update_data():
+ """Fetch data from the gateway and blinds."""
+ try:
+ await hass.async_add_executor_job(update_gateway)
+ except timeout as socket_timeout:
+ raise AsyncioTimeoutError from socket_timeout
+
+ coordinator = DataUpdateCoordinator(
+ hass,
+ _LOGGER,
+ # Name of the data. For logging purposes.
+ name=entry.title,
+ update_method=async_update_data,
+ # Polling interval. Will only be polled if there are subscribers.
+ update_interval=timedelta(seconds=10),
+ )
+
+ # Fetch initial data so we have data when entities subscribe
+ await coordinator.async_refresh()
+
+ hass.data[DOMAIN][entry.entry_id] = {
+ KEY_GATEWAY: motion_gateway,
+ KEY_COORDINATOR: coordinator,
+ }
+
+ device_registry = await dr.async_get_registry(hass)
+ device_registry.async_get_or_create(
+ config_entry_id=entry.entry_id,
+ connections={(dr.CONNECTION_NETWORK_MAC, motion_gateway.mac)},
+ identifiers={(DOMAIN, entry.unique_id)},
+ manufacturer=MANUFACTURER,
+ name=entry.title,
+ model="Wi-Fi bridge",
+ sw_version=motion_gateway.protocol,
+ )
+
+ for component in MOTION_PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, component)
+ )
+
+ return True
+
+
+async def async_unload_entry(
+ hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry
+):
+ """Unload a config entry."""
+ unload_ok = await hass.config_entries.async_forward_entry_unload(
+ config_entry, "cover"
+ )
+
+ if unload_ok:
+ hass.data[DOMAIN].pop(config_entry.entry_id)
+
+ return unload_ok
diff --git a/homeassistant/components/motion_blinds/config_flow.py b/homeassistant/components/motion_blinds/config_flow.py
new file mode 100644
index 00000000000..fbee7d1b439
--- /dev/null
+++ b/homeassistant/components/motion_blinds/config_flow.py
@@ -0,0 +1,64 @@
+"""Config flow to configure Motion Blinds using their WLAN API."""
+import logging
+
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_API_KEY, CONF_HOST
+
+# pylint: disable=unused-import
+from .const import DOMAIN
+from .gateway import ConnectMotionGateway
+
+_LOGGER = logging.getLogger(__name__)
+
+DEFAULT_GATEWAY_NAME = "Motion Gateway"
+
+CONFIG_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_HOST): str,
+ vol.Required(CONF_API_KEY): vol.All(str, vol.Length(min=16, max=16)),
+ }
+)
+
+
+class MotionBlindsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a Motion Blinds config flow."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
+
+ def __init__(self):
+ """Initialize the Motion Blinds flow."""
+ self.host = None
+ self.key = None
+
+ async def async_step_user(self, user_input=None):
+ """Handle a flow initialized by the user."""
+ errors = {}
+ if user_input is not None:
+ self.host = user_input[CONF_HOST]
+ self.key = user_input[CONF_API_KEY]
+ return await self.async_step_connect()
+
+ return self.async_show_form(
+ step_id="user", data_schema=CONFIG_SCHEMA, errors=errors
+ )
+
+ async def async_step_connect(self, user_input=None):
+ """Connect to the Motion Gateway."""
+
+ connect_gateway_class = ConnectMotionGateway(self.hass)
+ if not await connect_gateway_class.async_connect_gateway(self.host, self.key):
+ return self.async_abort(reason="connection_error")
+ motion_gateway = connect_gateway_class.gateway_device
+
+ mac_address = motion_gateway.mac
+
+ await self.async_set_unique_id(mac_address)
+ self._abort_if_unique_id_configured()
+
+ return self.async_create_entry(
+ title=DEFAULT_GATEWAY_NAME,
+ data={CONF_HOST: self.host, CONF_API_KEY: self.key},
+ )
diff --git a/homeassistant/components/motion_blinds/const.py b/homeassistant/components/motion_blinds/const.py
new file mode 100644
index 00000000000..c80c8f881cd
--- /dev/null
+++ b/homeassistant/components/motion_blinds/const.py
@@ -0,0 +1,6 @@
+"""Constants for the Motion Blinds component."""
+DOMAIN = "motion_blinds"
+MANUFACTURER = "Motion, Coulisse B.V."
+
+KEY_GATEWAY = "gateway"
+KEY_COORDINATOR = "coordinator"
diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py
new file mode 100644
index 00000000000..4273be3f435
--- /dev/null
+++ b/homeassistant/components/motion_blinds/cover.py
@@ -0,0 +1,256 @@
+"""Support for Motion Blinds using their WLAN API."""
+
+import logging
+
+from motionblinds import BlindType
+
+from homeassistant.components.cover import (
+ ATTR_POSITION,
+ ATTR_TILT_POSITION,
+ DEVICE_CLASS_AWNING,
+ DEVICE_CLASS_BLIND,
+ DEVICE_CLASS_CURTAIN,
+ DEVICE_CLASS_GATE,
+ DEVICE_CLASS_SHADE,
+ DEVICE_CLASS_SHUTTER,
+ CoverEntity,
+)
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from .const import DOMAIN, KEY_COORDINATOR, KEY_GATEWAY, MANUFACTURER
+
+_LOGGER = logging.getLogger(__name__)
+
+
+POSITION_DEVICE_MAP = {
+ BlindType.RollerBlind: DEVICE_CLASS_SHADE,
+ BlindType.RomanBlind: DEVICE_CLASS_SHADE,
+ BlindType.HoneycombBlind: DEVICE_CLASS_SHADE,
+ BlindType.DimmingBlind: DEVICE_CLASS_SHADE,
+ BlindType.DayNightBlind: DEVICE_CLASS_SHADE,
+ BlindType.RollerShutter: DEVICE_CLASS_SHUTTER,
+ BlindType.Switch: DEVICE_CLASS_SHUTTER,
+ BlindType.RollerGate: DEVICE_CLASS_GATE,
+ BlindType.Awning: DEVICE_CLASS_AWNING,
+ BlindType.Curtain: DEVICE_CLASS_CURTAIN,
+ BlindType.CurtainLeft: DEVICE_CLASS_CURTAIN,
+ BlindType.CurtainRight: DEVICE_CLASS_CURTAIN,
+}
+
+TILT_DEVICE_MAP = {
+ BlindType.VenetianBlind: DEVICE_CLASS_BLIND,
+ BlindType.ShangriLaBlind: DEVICE_CLASS_BLIND,
+ BlindType.DoubleRoller: DEVICE_CLASS_SHADE,
+}
+
+TDBU_DEVICE_MAP = {
+ BlindType.TopDownBottomUp: DEVICE_CLASS_SHADE,
+}
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up the Motion Blind from a config entry."""
+ entities = []
+ motion_gateway = hass.data[DOMAIN][config_entry.entry_id][KEY_GATEWAY]
+ coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR]
+
+ for blind in motion_gateway.device_list.values():
+ if blind.type in POSITION_DEVICE_MAP:
+ entities.append(
+ MotionPositionDevice(
+ coordinator, blind, POSITION_DEVICE_MAP[blind.type], config_entry
+ )
+ )
+
+ elif blind.type in TILT_DEVICE_MAP:
+ entities.append(
+ MotionTiltDevice(
+ coordinator, blind, TILT_DEVICE_MAP[blind.type], config_entry
+ )
+ )
+
+ elif blind.type in TDBU_DEVICE_MAP:
+ entities.append(
+ MotionTDBUDevice(
+ coordinator, blind, TDBU_DEVICE_MAP[blind.type], config_entry, "Top"
+ )
+ )
+ entities.append(
+ MotionTDBUDevice(
+ coordinator,
+ blind,
+ TDBU_DEVICE_MAP[blind.type],
+ config_entry,
+ "Bottom",
+ )
+ )
+
+ else:
+ _LOGGER.warning("Blind type '%s' not yet supported", blind.blind_type)
+
+ async_add_entities(entities)
+
+
+class MotionPositionDevice(CoordinatorEntity, CoverEntity):
+ """Representation of a Motion Blind Device."""
+
+ def __init__(self, coordinator, blind, device_class, config_entry):
+ """Initialize the blind."""
+ super().__init__(coordinator)
+
+ self._blind = blind
+ self._device_class = device_class
+ self._config_entry = config_entry
+
+ @property
+ def unique_id(self):
+ """Return the unique id of the blind."""
+ return self._blind.mac
+
+ @property
+ def device_info(self):
+ """Return the device info of the blind."""
+ device_info = {
+ "identifiers": {(DOMAIN, self._blind.mac)},
+ "manufacturer": MANUFACTURER,
+ "name": f"{self._blind.blind_type}-{self._blind.mac[12:]}",
+ "model": self._blind.blind_type,
+ "via_device": (DOMAIN, self._config_entry.unique_id),
+ }
+
+ return device_info
+
+ @property
+ def name(self):
+ """Return the name of the blind."""
+ return f"{self._blind.blind_type}-{self._blind.mac[12:]}"
+
+ @property
+ def current_cover_position(self):
+ """
+ Return current position of cover.
+
+ None is unknown, 0 is open, 100 is closed.
+ """
+ if self._blind.position is None:
+ return None
+ return 100 - self._blind.position
+
+ @property
+ def device_class(self):
+ """Return the device class."""
+ return self._device_class
+
+ @property
+ def is_closed(self):
+ """Return if the cover is closed or not."""
+ return self._blind.position == 100
+
+ def open_cover(self, **kwargs):
+ """Open the cover."""
+ self._blind.Open()
+
+ def close_cover(self, **kwargs):
+ """Close cover."""
+ self._blind.Close()
+
+ def set_cover_position(self, **kwargs):
+ """Move the cover to a specific position."""
+ position = kwargs[ATTR_POSITION]
+ self._blind.Set_position(100 - position)
+
+ def stop_cover(self, **kwargs):
+ """Stop the cover."""
+ self._blind.Stop()
+
+
+class MotionTiltDevice(MotionPositionDevice):
+ """Representation of a Motion Blind Device."""
+
+ @property
+ def current_cover_tilt_position(self):
+ """
+ Return current angle of cover.
+
+ None is unknown, 0 is closed/minimum tilt, 100 is fully open/maximum tilt.
+ """
+ if self._blind.angle is None:
+ return None
+ return self._blind.angle * 100 / 180
+
+ def open_cover_tilt(self, **kwargs):
+ """Open the cover tilt."""
+ self._blind.Set_angle(180)
+
+ def close_cover_tilt(self, **kwargs):
+ """Close the cover tilt."""
+ self._blind.Set_angle(0)
+
+ def set_cover_tilt_position(self, **kwargs):
+ """Move the cover tilt to a specific position."""
+ angle = kwargs[ATTR_TILT_POSITION] * 180 / 100
+ self._blind.Set_angle(angle)
+
+ def stop_cover_tilt(self, **kwargs):
+ """Stop the cover."""
+ self._blind.Stop()
+
+
+class MotionTDBUDevice(MotionPositionDevice):
+ """Representation of a Motion Top Down Bottom Up blind Device."""
+
+ def __init__(self, coordinator, blind, device_class, config_entry, motor):
+ """Initialize the blind."""
+ super().__init__(coordinator, blind, device_class, config_entry)
+ self._motor = motor
+ self._motor_key = motor[0]
+
+ if self._motor not in ["Bottom", "Top"]:
+ _LOGGER.error("Unknown motor '%s'", self._motor)
+
+ @property
+ def unique_id(self):
+ """Return the unique id of the blind."""
+ return f"{self._blind.mac}-{self._motor}"
+
+ @property
+ def name(self):
+ """Return the name of the blind."""
+ return f"{self._blind.blind_type}-{self._motor}-{self._blind.mac[12:]}"
+
+ @property
+ def current_cover_position(self):
+ """
+ Return current position of cover.
+
+ None is unknown, 0 is open, 100 is closed.
+ """
+ if self._blind.position is None:
+ return None
+
+ return 100 - self._blind.position[self._motor_key]
+
+ @property
+ def is_closed(self):
+ """Return if the cover is closed or not."""
+ if self._blind.position is None:
+ return None
+
+ return self._blind.position[self._motor_key] == 100
+
+ def open_cover(self, **kwargs):
+ """Open the cover."""
+ self._blind.Open(motor=self._motor_key)
+
+ def close_cover(self, **kwargs):
+ """Close cover."""
+ self._blind.Close(motor=self._motor_key)
+
+ def set_cover_position(self, **kwargs):
+ """Move the cover to a specific position."""
+ position = kwargs[ATTR_POSITION]
+ self._blind.Set_position(100 - position, motor=self._motor_key)
+
+ def stop_cover(self, **kwargs):
+ """Stop the cover."""
+ self._blind.Stop(motor=self._motor_key)
diff --git a/homeassistant/components/motion_blinds/gateway.py b/homeassistant/components/motion_blinds/gateway.py
new file mode 100644
index 00000000000..e7e665d65f9
--- /dev/null
+++ b/homeassistant/components/motion_blinds/gateway.py
@@ -0,0 +1,45 @@
+"""Code to handle a Motion Gateway."""
+import logging
+import socket
+
+from motionblinds import MotionGateway
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class ConnectMotionGateway:
+ """Class to async connect to a Motion Gateway."""
+
+ def __init__(self, hass):
+ """Initialize the entity."""
+ self._hass = hass
+ self._gateway_device = None
+
+ @property
+ def gateway_device(self):
+ """Return the class containing all connections to the gateway."""
+ return self._gateway_device
+
+ def update_gateway(self):
+ """Update all information of the gateway."""
+ self.gateway_device.GetDeviceList()
+ self.gateway_device.Update()
+
+ async def async_connect_gateway(self, host, key):
+ """Connect to the Motion Gateway."""
+ _LOGGER.debug("Initializing with host %s (key %s...)", host, key[:3])
+ self._gateway_device = MotionGateway(ip=host, key=key)
+ try:
+ # update device info and get the connected sub devices
+ await self._hass.async_add_executor_job(self.update_gateway)
+ except socket.timeout:
+ _LOGGER.error(
+ "Timeout trying to connect to Motion Gateway with host %s", host
+ )
+ return False
+ _LOGGER.debug(
+ "Motion gateway mac: %s, protocol: %s detected",
+ self.gateway_device.mac,
+ self.gateway_device.protocol,
+ )
+ return True
diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json
new file mode 100644
index 00000000000..84cf711ac97
--- /dev/null
+++ b/homeassistant/components/motion_blinds/manifest.json
@@ -0,0 +1,8 @@
+{
+ "domain": "motion_blinds",
+ "name": "Motion Blinds",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integrations/motion_blinds",
+ "requirements": ["motionblinds==0.1.6"],
+ "codeowners": ["@starkillerOG"]
+}
\ No newline at end of file
diff --git a/homeassistant/components/motion_blinds/sensor.py b/homeassistant/components/motion_blinds/sensor.py
new file mode 100644
index 00000000000..81d555806ed
--- /dev/null
+++ b/homeassistant/components/motion_blinds/sensor.py
@@ -0,0 +1,181 @@
+"""Support for Motion Blinds sensors."""
+import logging
+
+from motionblinds import BlindType
+
+from homeassistant.const import (
+ DEVICE_CLASS_BATTERY,
+ DEVICE_CLASS_SIGNAL_STRENGTH,
+ PERCENTAGE,
+ SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
+)
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from .const import DOMAIN, KEY_COORDINATOR, KEY_GATEWAY
+
+_LOGGER = logging.getLogger(__name__)
+
+ATTR_BATTERY_VOLTAGE = "battery_voltage"
+TYPE_BLIND = "blind"
+TYPE_GATEWAY = "gateway"
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Perform the setup for Motion Blinds."""
+ entities = []
+ motion_gateway = hass.data[DOMAIN][config_entry.entry_id][KEY_GATEWAY]
+ coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR]
+
+ for blind in motion_gateway.device_list.values():
+ entities.append(MotionSignalStrengthSensor(coordinator, blind, TYPE_BLIND))
+ if blind.type == BlindType.TopDownBottomUp:
+ entities.append(MotionTDBUBatterySensor(coordinator, blind, "Bottom"))
+ entities.append(MotionTDBUBatterySensor(coordinator, blind, "Top"))
+ elif blind.battery_voltage > 0:
+ # Only add battery powered blinds
+ entities.append(MotionBatterySensor(coordinator, blind))
+
+ entities.append(
+ MotionSignalStrengthSensor(coordinator, motion_gateway, TYPE_GATEWAY)
+ )
+
+ async_add_entities(entities)
+
+
+class MotionBatterySensor(CoordinatorEntity, Entity):
+ """
+ Representation of a Motion Battery Sensor.
+
+ Updates are done by the cover platform.
+ """
+
+ def __init__(self, coordinator, blind):
+ """Initialize the Motion Battery Sensor."""
+ super().__init__(coordinator)
+
+ self._blind = blind
+
+ @property
+ def unique_id(self):
+ """Return the unique id of the blind."""
+ return f"{self._blind.mac}-battery"
+
+ @property
+ def device_info(self):
+ """Return the device info of the blind."""
+ return {"identifiers": {(DOMAIN, self._blind.mac)}}
+
+ @property
+ def name(self):
+ """Return the name of the blind battery sensor."""
+ return f"{self._blind.blind_type}-battery-{self._blind.mac[12:]}"
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement of this entity, if any."""
+ return PERCENTAGE
+
+ @property
+ def device_class(self):
+ """Return the device class of this entity."""
+ return DEVICE_CLASS_BATTERY
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._blind.battery_level
+
+ @property
+ def device_state_attributes(self):
+ """Return device specific state attributes."""
+ return {ATTR_BATTERY_VOLTAGE: self._blind.battery_voltage}
+
+
+class MotionTDBUBatterySensor(MotionBatterySensor):
+ """
+ Representation of a Motion Battery Sensor for a Top Down Bottom Up blind.
+
+ Updates are done by the cover platform.
+ """
+
+ def __init__(self, coordinator, blind, motor):
+ """Initialize the Motion Battery Sensor."""
+ super().__init__(coordinator, blind)
+
+ self._motor = motor
+
+ @property
+ def unique_id(self):
+ """Return the unique id of the blind."""
+ return f"{self._blind.mac}-{self._motor}-battery"
+
+ @property
+ def name(self):
+ """Return the name of the blind battery sensor."""
+ return f"{self._blind.blind_type}-{self._motor}-battery-{self._blind.mac[12:]}"
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ if self._blind.battery_level is None:
+ return None
+ return self._blind.battery_level[self._motor[0]]
+
+ @property
+ def device_state_attributes(self):
+ """Return device specific state attributes."""
+ attributes = {}
+ if self._blind.battery_voltage is not None:
+ attributes[ATTR_BATTERY_VOLTAGE] = self._blind.battery_voltage[
+ self._motor[0]
+ ]
+ return attributes
+
+
+class MotionSignalStrengthSensor(CoordinatorEntity, Entity):
+ """Representation of a Motion Signal Strength Sensor."""
+
+ def __init__(self, coordinator, device, device_type):
+ """Initialize the Motion Signal Strength Sensor."""
+ super().__init__(coordinator)
+
+ self._device = device
+ self._device_type = device_type
+
+ @property
+ def unique_id(self):
+ """Return the unique id of the blind."""
+ return f"{self._device.mac}-RSSI"
+
+ @property
+ def device_info(self):
+ """Return the device info of the blind."""
+ return {"identifiers": {(DOMAIN, self._device.mac)}}
+
+ @property
+ def name(self):
+ """Return the name of the blind signal strength sensor."""
+ if self._device_type == TYPE_GATEWAY:
+ return "Motion gateway signal strength"
+ return f"{self._device.blind_type} signal strength - {self._device.mac[12:]}"
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement of this entity, if any."""
+ return SIGNAL_STRENGTH_DECIBELS_MILLIWATT
+
+ @property
+ def device_class(self):
+ """Return the device class of this entity."""
+ return DEVICE_CLASS_SIGNAL_STRENGTH
+
+ @property
+ def entity_registry_enabled_default(self):
+ """Return if the entity should be enabled when first added to the entity registry."""
+ return False
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._device.RSSI
diff --git a/homeassistant/components/motion_blinds/strings.json b/homeassistant/components/motion_blinds/strings.json
new file mode 100644
index 00000000000..d9c8a4099ac
--- /dev/null
+++ b/homeassistant/components/motion_blinds/strings.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "flow_title": "Motion Blinds",
+ "step": {
+ "user": {
+ "title": "Motion Blinds",
+ "description": "You will need the 16 character API Key, see https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instructions",
+ "data": {
+ "host": "[%key:common::config_flow::data::ip%]",
+ "api_key": "[%key:common::config_flow::data::api_key%]"
+ }
+ }
+ },
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
+ "connection_error": "[%key:common::config_flow::error::cannot_connect%]"
+ }
+ }
+}
diff --git a/homeassistant/components/motion_blinds/translations/ca.json b/homeassistant/components/motion_blinds/translations/ca.json
new file mode 100644
index 00000000000..a4bf96457e6
--- /dev/null
+++ b/homeassistant/components/motion_blinds/translations/ca.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
+ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs",
+ "connection_error": "Ha fallat la connexi\u00f3"
+ },
+ "flow_title": "Motion Blinds",
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Clau API",
+ "host": "Adre\u00e7a IP"
+ },
+ "description": "Necessitar\u00e0s el token d'API de 16 car\u00e0cters, consulta les instruccions a https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key.",
+ "title": "Motion Blinds"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/motion_blinds/translations/cs.json b/homeassistant/components/motion_blinds/translations/cs.json
new file mode 100644
index 00000000000..41b5db3c83e
--- /dev/null
+++ b/homeassistant/components/motion_blinds/translations/cs.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno",
+ "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1",
+ "connection_error": "Nepoda\u0159ilo se p\u0159ipojit"
+ },
+ "flow_title": "Motion Blinds",
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Kl\u00ed\u010d API",
+ "host": "IP adresa"
+ },
+ "description": "Budete pot\u0159ebovat 16m\u00edstn\u00fd API kl\u00ed\u010d, pokyny najdete na https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key",
+ "title": "Motion Blinds"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/motion_blinds/translations/en.json b/homeassistant/components/motion_blinds/translations/en.json
new file mode 100644
index 00000000000..b7830a255fc
--- /dev/null
+++ b/homeassistant/components/motion_blinds/translations/en.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Device is already configured",
+ "already_in_progress": "Configuration flow is already in progress",
+ "connection_error": "Failed to connect"
+ },
+ "flow_title": "Motion Blinds",
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API Key",
+ "host": "IP Address"
+ },
+ "description": "You will need the 16 character API Key, see https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instructions",
+ "title": "Motion Blinds"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/motion_blinds/translations/es.json b/homeassistant/components/motion_blinds/translations/es.json
new file mode 100644
index 00000000000..bac5ffddbd3
--- /dev/null
+++ b/homeassistant/components/motion_blinds/translations/es.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "El dispositivo ya est\u00e1 configurado",
+ "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso",
+ "connection_error": "No se pudo conectar"
+ },
+ "flow_title": "Motion Blinds",
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Clave API",
+ "host": "Direcci\u00f3n IP"
+ },
+ "description": "Necesitar\u00e1s la Clave API de 16 caracteres, consulta https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key para instrucciones",
+ "title": "Motion Blinds"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/motion_blinds/translations/et.json b/homeassistant/components/motion_blinds/translations/et.json
new file mode 100644
index 00000000000..b55640d8905
--- /dev/null
+++ b/homeassistant/components/motion_blinds/translations/et.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Seade on juba h\u00e4\u00e4lestatud",
+ "already_in_progress": "Seadistamine on juba k\u00e4imas",
+ "connection_error": "\u00dchendamine nurjus"
+ },
+ "flow_title": "",
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API v\u00f5ti",
+ "host": "IP-aadress"
+ },
+ "description": "Vaja on 16-kohalist API-v\u00f5tit. Juhiste saamiseks vt https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key",
+ "title": ""
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/motion_blinds/translations/it.json b/homeassistant/components/motion_blinds/translations/it.json
new file mode 100644
index 00000000000..ff56f184ac2
--- /dev/null
+++ b/homeassistant/components/motion_blinds/translations/it.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
+ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso",
+ "connection_error": "Impossibile connettersi"
+ },
+ "flow_title": "Tende Motion",
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Chiave API",
+ "host": "Indirizzo IP"
+ },
+ "description": "Avrai bisogno della chiave API di 16 caratteri, consulta https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key per le istruzioni",
+ "title": "Tende Motion"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/motion_blinds/translations/ka.json b/homeassistant/components/motion_blinds/translations/ka.json
new file mode 100644
index 00000000000..e8ea5e0deba
--- /dev/null
+++ b/homeassistant/components/motion_blinds/translations/ka.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u10db\u10dd\u10ec\u10e7\u10de\u10d1\u10d8\u10da\u10dd\u10d1\u10d0 \u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0",
+ "already_in_progress": "\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10d8\u10e1 \u10db\u10d8\u10db\u10d3\u10d4\u10d5\u10e0\u10d1\u10d0 \u10e3\u10d9\u10d5\u10d4 \u10db\u10d8\u10db\u10d3\u10d8\u10dc\u10d0\u10e0\u10d4\u10dd\u10d1\u10e1",
+ "connection_error": "\u10d3\u10d0\u10d9\u10d0\u10d5\u10e8\u10e0\u10d4\u10d1\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0"
+ },
+ "flow_title": "\u10db\u10dd\u10eb\u10e0\u10d0\u10d5\u10d8 \u10df\u10d0\u10da\u10e3\u10d6\u10d4\u10d1\u10d8",
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API Key",
+ "host": "IP \u10db\u10d8\u10e1\u10d0\u10db\u10d0\u10e0\u10d7\u10d8"
+ },
+ "description": "\u10d7\u10e5\u10d5\u10d4\u10dc \u10d3\u10d0\u10d2\u10ed\u10d8\u10e0\u10d3\u10d4\u10d1\u10d0\u10d7 16 \u10d0\u10e1\u10dd\u10d8\u10d0\u10dc\u10d8 API key, \u10d8\u10dc\u10e1\u10e2\u10e0\u10e3\u10e5\u10ea\u10d8\u10d8\u10e1\u10d7\u10d5\u10d8\u10e1 \u10d8\u10ee\u10d8\u10da\u10d4\u10d7 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key",
+ "title": "\u10db\u10dd\u10eb\u10e0\u10d0\u10d5\u10d8 \u10df\u10d0\u10da\u10e3\u10d6\u10d4\u10d1\u10d8"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/motion_blinds/translations/lb.json b/homeassistant/components/motion_blinds/translations/lb.json
new file mode 100644
index 00000000000..7a3dcfdbf07
--- /dev/null
+++ b/homeassistant/components/motion_blinds/translations/lb.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Apparat ass scho konfigur\u00e9iert",
+ "already_in_progress": "Konfiguratioun's Oflaf ass schon am gaang",
+ "connection_error": "Feeler beim verbannen"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API Schl\u00ebssel",
+ "host": "IP Adresse"
+ },
+ "title": "Steierbar Jalousien"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/motion_blinds/translations/no.json b/homeassistant/components/motion_blinds/translations/no.json
new file mode 100644
index 00000000000..9e406150691
--- /dev/null
+++ b/homeassistant/components/motion_blinds/translations/no.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Enheten er allerede konfigurert",
+ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede",
+ "connection_error": "Tilkobling mislyktes"
+ },
+ "flow_title": "Motion Blinds",
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API-n\u00f8kkel",
+ "host": "IP adresse"
+ },
+ "description": "Du trenger API-n\u00f8kkelen med 16 tegn, se https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instruksjoner",
+ "title": "Motion Blinds"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/motion_blinds/translations/pl.json b/homeassistant/components/motion_blinds/translations/pl.json
new file mode 100644
index 00000000000..8f73496fd1d
--- /dev/null
+++ b/homeassistant/components/motion_blinds/translations/pl.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "already_in_progress": "Konfiguracja jest ju\u017c w toku",
+ "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
+ },
+ "flow_title": "Motion Blinds",
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "Klucz API",
+ "host": "Adres IP"
+ },
+ "description": "B\u0119dziesz potrzebowa\u0142 16-znakowego klucza API, instrukcje znajdziesz na https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key",
+ "title": "Motion Blinds"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/motion_blinds/translations/ru.json b/homeassistant/components/motion_blinds/translations/ru.json
new file mode 100644
index 00000000000..1a249a4fab8
--- /dev/null
+++ b/homeassistant/components/motion_blinds/translations/ru.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.",
+ "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.",
+ "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f."
+ },
+ "flow_title": "Motion Blinds",
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "\u041a\u043b\u044e\u0447 API",
+ "host": "IP-\u0430\u0434\u0440\u0435\u0441"
+ },
+ "description": "\u041e \u0442\u043e\u043c, \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c 16-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0437\u043d\u0430\u0442\u044c \u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key.",
+ "title": "Motion Blinds"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/motion_blinds/translations/zh-Hant.json b/homeassistant/components/motion_blinds/translations/zh-Hant.json
new file mode 100644
index 00000000000..8c8d23b565b
--- /dev/null
+++ b/homeassistant/components/motion_blinds/translations/zh-Hant.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d",
+ "connection_error": "\u9023\u7dda\u5931\u6557"
+ },
+ "flow_title": "Motion Blinds",
+ "step": {
+ "user": {
+ "data": {
+ "api_key": "API \u5bc6\u9470",
+ "host": "IP \u4f4d\u5740"
+ },
+ "description": "\u5c07\u9700\u8981\u8f38\u5165 16 \u4f4d\u5b57\u5143 API \u5bc6\u9470\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key \u4ee5\u7372\u5f97\u7372\u53d6\u5bc6\u9470\u7684\u6559\u5b78\u3002",
+ "title": "Motion Blinds"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py
index 5898b1918a2..cced3670cca 100644
--- a/homeassistant/components/mqtt/__init__.py
+++ b/homeassistant/components/mqtt/__init__.py
@@ -145,6 +145,7 @@ PLATFORMS = [
"fan",
"light",
"lock",
+ "scene",
"sensor",
"switch",
"vacuum",
@@ -190,7 +191,7 @@ def embedded_broker_deprecated(value):
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.All(
- cv.deprecated(CONF_TLS_VERSION, invalidation_version="0.115"),
+ cv.deprecated(CONF_TLS_VERSION),
vol.Schema(
{
vol.Optional(CONF_CLIENT_ID): cv.string,
@@ -944,7 +945,9 @@ class MQTT:
)
birth_message = Message(**self.conf[CONF_BIRTH_MESSAGE])
- self.hass.loop.create_task(publish_birth_message(birth_message))
+ asyncio.run_coroutine_threadsafe(
+ publish_birth_message(birth_message), self.hass.loop
+ )
def _mqtt_on_message(self, _mqttc, _userdata, msg) -> None:
"""Message received callback."""
@@ -1248,7 +1251,7 @@ async def cleanup_device_registry(hass, device_id):
if (
device_id
and not hass.helpers.entity_registry.async_entries_for_device(
- entity_registry, device_id
+ entity_registry, device_id, include_disabled_entities=True
)
and not await device_trigger.async_get_triggers(hass, device_id)
and not tag.async_has_tags(hass, device_id)
@@ -1485,20 +1488,22 @@ def async_subscribe_connection_status(hass, connection_status_callback):
connection_status_callback_job = HassJob(connection_status_callback)
- @callback
- def connected():
- hass.async_add_hass_job(connection_status_callback_job, True)
+ async def connected():
+ task = hass.async_run_hass_job(connection_status_callback_job, True)
+ if task:
+ await task
- @callback
- def disconnected():
- _LOGGER.error("Calling connection_status_callback, False")
- hass.async_add_hass_job(connection_status_callback_job, False)
+ async def disconnected():
+ task = hass.async_run_hass_job(connection_status_callback_job, False)
+ if task:
+ await task
subscriptions = {
"connect": async_dispatcher_connect(hass, MQTT_CONNECTED, connected),
"disconnect": async_dispatcher_connect(hass, MQTT_DISCONNECTED, disconnected),
}
+ @callback
def unsubscribe():
subscriptions["connect"]()
subscriptions["disconnect"]()
diff --git a/homeassistant/components/mqtt/debug_info.py b/homeassistant/components/mqtt/debug_info.py
index 75e4b53a191..a3c56652253 100644
--- a/homeassistant/components/mqtt/debug_info.py
+++ b/homeassistant/components/mqtt/debug_info.py
@@ -123,7 +123,7 @@ async def info_for_device(hass, device_id):
entity_registry = await hass.helpers.entity_registry.async_get_registry()
entries = hass.helpers.entity_registry.async_entries_for_device(
- entity_registry, device_id
+ entity_registry, device_id, include_disabled_entities=True
)
mqtt_debug_info = hass.data.setdefault(
DATA_MQTT_DEBUG_INFO, {"entities": {}, "triggers": {}}
diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py
index 7f9a6730285..d1e64d44bbc 100644
--- a/homeassistant/components/mqtt/discovery.py
+++ b/homeassistant/components/mqtt/discovery.py
@@ -37,6 +37,7 @@ SUPPORTED_COMPONENTS = [
"fan",
"light",
"lock",
+ "scene",
"sensor",
"switch",
"tag",
diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py
new file mode 100644
index 00000000000..4f4380332fd
--- /dev/null
+++ b/homeassistant/components/mqtt/scene.py
@@ -0,0 +1,144 @@
+"""Support for MQTT scenes."""
+import logging
+
+import voluptuous as vol
+
+from homeassistant.components import mqtt, scene
+from homeassistant.components.scene import Scene
+from homeassistant.const import CONF_ICON, CONF_NAME, CONF_PAYLOAD_ON, CONF_UNIQUE_ID
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.reload import async_setup_reload_service
+from homeassistant.helpers.typing import ConfigType, HomeAssistantType
+
+from . import (
+ ATTR_DISCOVERY_HASH,
+ CONF_COMMAND_TOPIC,
+ CONF_QOS,
+ CONF_RETAIN,
+ DOMAIN,
+ PLATFORMS,
+ MqttAvailability,
+ MqttDiscoveryUpdate,
+)
+from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash
+
+_LOGGER = logging.getLogger(__name__)
+
+DEFAULT_NAME = "MQTT Scene"
+DEFAULT_RETAIN = False
+
+PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend(
+ {
+ vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
+ vol.Optional(CONF_ICON): cv.icon,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_PAYLOAD_ON): cv.string,
+ vol.Optional(CONF_UNIQUE_ID): cv.string,
+ vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
+ }
+).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
+
+
+async def async_setup_platform(
+ hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None
+):
+ """Set up MQTT scene through configuration.yaml."""
+ await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
+ await _async_setup_entity(config, async_add_entities)
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up MQTT scene dynamically through MQTT discovery."""
+
+ async def async_discover(discovery_payload):
+ """Discover and add a MQTT scene."""
+ discovery_data = discovery_payload.discovery_data
+ try:
+ config = PLATFORM_SCHEMA(discovery_payload)
+ await _async_setup_entity(
+ config, async_add_entities, config_entry, discovery_data
+ )
+ except Exception:
+ clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH])
+ raise
+
+ async_dispatcher_connect(
+ hass, MQTT_DISCOVERY_NEW.format(scene.DOMAIN, "mqtt"), async_discover
+ )
+
+
+async def _async_setup_entity(
+ config, async_add_entities, config_entry=None, discovery_data=None
+):
+ """Set up the MQTT scene."""
+ async_add_entities([MqttScene(config, config_entry, discovery_data)])
+
+
+class MqttScene(
+ MqttAvailability,
+ MqttDiscoveryUpdate,
+ Scene,
+):
+ """Representation of a scene that can be activated using MQTT."""
+
+ def __init__(self, config, config_entry, discovery_data):
+ """Initialize the MQTT scene."""
+ self._state = False
+ self._sub_state = None
+
+ self._unique_id = config.get(CONF_UNIQUE_ID)
+
+ # Load config
+ self._setup_from_config(config)
+
+ MqttAvailability.__init__(self, config)
+ MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update)
+
+ async def async_added_to_hass(self):
+ """Subscribe to MQTT events."""
+ await super().async_added_to_hass()
+
+ async def discovery_update(self, discovery_payload):
+ """Handle updated discovery message."""
+ config = PLATFORM_SCHEMA(discovery_payload)
+ self._setup_from_config(config)
+ await self.availability_discovery_update(config)
+ self.async_write_ha_state()
+
+ def _setup_from_config(self, config):
+ """(Re)Setup the entity."""
+ self._config = config
+
+ async def async_will_remove_from_hass(self):
+ """Unsubscribe when removed."""
+ await MqttAvailability.async_will_remove_from_hass(self)
+ await MqttDiscoveryUpdate.async_will_remove_from_hass(self)
+
+ @property
+ def name(self):
+ """Return the name of the scene."""
+ return self._config[CONF_NAME]
+
+ @property
+ def unique_id(self):
+ """Return a unique ID."""
+ return self._unique_id
+
+ @property
+ def icon(self):
+ """Return the icon."""
+ return self._config.get(CONF_ICON)
+
+ async def async_activate(self, **kwargs):
+ """Activate the scene.
+
+ This method is a coroutine.
+ """
+ mqtt.async_publish(
+ self.hass,
+ self._config[CONF_COMMAND_TOPIC],
+ self._config[CONF_PAYLOAD_ON],
+ self._config[CONF_QOS],
+ self._config[CONF_RETAIN],
+ )
diff --git a/homeassistant/components/mqtt/translations/pl.json b/homeassistant/components/mqtt/translations/pl.json
index 2c11afd52e9..ce41d059b24 100644
--- a/homeassistant/components/mqtt/translations/pl.json
+++ b/homeassistant/components/mqtt/translations/pl.json
@@ -21,7 +21,7 @@
"data": {
"discovery": "W\u0142\u0105cz wykrywanie"
},
- "description": "Czy chcesz skonfigurowa\u0107 Home Assistant, 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/myq/manifest.json b/homeassistant/components/myq/manifest.json
index 12afcb97d56..ee3471725b6 100644
--- a/homeassistant/components/myq/manifest.json
+++ b/homeassistant/components/myq/manifest.json
@@ -2,7 +2,7 @@
"domain": "myq",
"name": "MyQ",
"documentation": "https://www.home-assistant.io/integrations/myq",
- "requirements": ["pymyq==2.0.8"],
+ "requirements": ["pymyq==2.0.11"],
"codeowners": ["@bdraco"],
"config_flow": true,
"homekit": {
diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py
index 6a6e95ddd01..bab6bf3fc40 100644
--- a/homeassistant/components/mysensors/sensor.py
+++ b/homeassistant/components/mysensors/sensor.py
@@ -85,7 +85,7 @@ class MySensorsSensor(mysensors.device.MySensorsEntity):
@property
def icon(self):
"""Return the icon to use in the frontend, if any."""
- _, icon = self._get_sensor_type()
+ icon = self._get_sensor_type()[1]
return icon
@property
@@ -97,7 +97,7 @@ class MySensorsSensor(mysensors.device.MySensorsEntity):
and set_req.V_UNIT_PREFIX in self._values
):
return self._values[set_req.V_UNIT_PREFIX]
- unit, _ = self._get_sensor_type()
+ unit = self._get_sensor_type()[0]
return unit
def _get_sensor_type(self):
diff --git a/homeassistant/components/neato/const.py b/homeassistant/components/neato/const.py
index 144ea40b92a..53948e2b19d 100644
--- a/homeassistant/components/neato/const.py
+++ b/homeassistant/components/neato/const.py
@@ -11,8 +11,6 @@ NEATO_ROBOTS = "neato_robots"
SCAN_INTERVAL_MINUTES = 1
-SERVICE_NEATO_CUSTOM_CLEANING = "custom_cleaning"
-
VALID_VENDORS = ["neato", "vorwerk"]
MODE = {1: "Eco", 2: "Turbo"}
diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py
index 841b160ad30..677bed1565b 100644
--- a/homeassistant/components/neato/vacuum.py
+++ b/homeassistant/components/neato/vacuum.py
@@ -25,8 +25,7 @@ from homeassistant.components.vacuum import (
StateVacuumEntity,
)
from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE
-import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.service import extract_entity_ids
+from homeassistant.helpers import config_validation as cv, entity_platform
from .const import (
ACTION,
@@ -39,7 +38,6 @@ from .const import (
NEATO_PERSISTENT_MAPS,
NEATO_ROBOTS,
SCAN_INTERVAL_MINUTES,
- SERVICE_NEATO_CUSTOM_CLEANING,
)
_LOGGER = logging.getLogger(__name__)
@@ -73,16 +71,6 @@ ATTR_NAVIGATION = "navigation"
ATTR_CATEGORY = "category"
ATTR_ZONE = "zone"
-SERVICE_NEATO_CUSTOM_CLEANING_SCHEMA = vol.Schema(
- {
- vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
- vol.Optional(ATTR_MODE, default=2): cv.positive_int,
- vol.Optional(ATTR_NAVIGATION, default=1): cv.positive_int,
- vol.Optional(ATTR_CATEGORY, default=4): cv.positive_int,
- vol.Optional(ATTR_ZONE): cv.string,
- }
-)
-
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up Neato vacuum with config entry."""
@@ -99,30 +87,19 @@ async def async_setup_entry(hass, entry, async_add_entities):
_LOGGER.debug("Adding vacuums %s", dev)
async_add_entities(dev, True)
- def neato_custom_cleaning_service(call):
- """Zone cleaning service that allows user to change options."""
- for robot in service_to_entities(call):
- if call.service == SERVICE_NEATO_CUSTOM_CLEANING:
- mode = call.data.get(ATTR_MODE)
- navigation = call.data.get(ATTR_NAVIGATION)
- category = call.data.get(ATTR_CATEGORY)
- zone = call.data.get(ATTR_ZONE)
- try:
- robot.neato_custom_cleaning(mode, navigation, category, zone)
- except NeatoRobotException as ex:
- _LOGGER.error("Neato vacuum connection error: %s", ex)
+ platform = entity_platform.current_platform.get()
+ assert platform is not None
- def service_to_entities(call):
- """Return the known devices that a service call mentions."""
- entity_ids = extract_entity_ids(hass, call)
- entities = [entity for entity in dev if entity.entity_id in entity_ids]
- return entities
-
- hass.services.async_register(
- NEATO_DOMAIN,
- SERVICE_NEATO_CUSTOM_CLEANING,
- neato_custom_cleaning_service,
- schema=SERVICE_NEATO_CUSTOM_CLEANING_SCHEMA,
+ platform.async_register_entity_service(
+ "custom_cleaning",
+ {
+ vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
+ vol.Optional(ATTR_MODE, default=2): cv.positive_int,
+ vol.Optional(ATTR_NAVIGATION, default=1): cv.positive_int,
+ vol.Optional(ATTR_CATEGORY, default=4): cv.positive_int,
+ vol.Optional(ATTR_ZONE): cv.string,
+ },
+ "neato_custom_cleaning",
)
@@ -407,7 +384,7 @@ class NeatoConnectedVacuum(StateVacuumEntity):
"Neato vacuum connection error for '%s': %s", self.entity_id, ex
)
- def neato_custom_cleaning(self, mode, navigation, category, zone=None, **kwargs):
+ def neato_custom_cleaning(self, mode, navigation, category, zone=None):
"""Zone cleaning service call."""
boundary_id = None
if zone is not None:
diff --git a/homeassistant/components/nello/lock.py b/homeassistant/components/nello/lock.py
index dc761d61461..61241660847 100644
--- a/homeassistant/components/nello/lock.py
+++ b/homeassistant/components/nello/lock.py
@@ -5,7 +5,7 @@ import logging
from pynello.private import Nello
import voluptuous as vol
-from homeassistant.components.lock import PLATFORM_SCHEMA, LockEntity
+from homeassistant.components.lock import PLATFORM_SCHEMA, SUPPORT_OPEN, LockEntity
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
import homeassistant.helpers.config_validation as cv
@@ -85,3 +85,13 @@ class NelloLock(LockEntity):
"""Unlock the device."""
if not self._nello_lock.open_door():
_LOGGER.error("Failed to unlock")
+
+ def open(self, **kwargs):
+ """Unlock the device."""
+ if not self._nello_lock.open_door():
+ _LOGGER.error("Failed to open")
+
+ @property
+ def supported_features(self):
+ """Flag supported features."""
+ return SUPPORT_OPEN
diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py
index ea2bd549c4b..97c9da5794b 100644
--- a/homeassistant/components/nest/__init__.py
+++ b/homeassistant/components/nest/__init__.py
@@ -5,7 +5,8 @@ from datetime import datetime, timedelta
import logging
import threading
-from google_nest_sdm.event import EventCallback, EventMessage
+from google_nest_sdm.event import AsyncEventCallback, EventMessage
+from google_nest_sdm.exceptions import GoogleNestException
from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber
from nest import Nest
from nest.nest import APIError, AuthorizationError
@@ -25,6 +26,7 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.core import HomeAssistant, callback
+from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import (
aiohttp_client,
config_entry_oauth2_flow,
@@ -41,11 +43,13 @@ from . import api, config_flow, local_auth
from .const import (
API_URL,
DATA_SDM,
+ DATA_SUBSCRIBER,
DOMAIN,
OAUTH2_AUTHORIZE,
OAUTH2_TOKEN,
SIGNAL_NEST_UPDATE,
)
+from .events import EVENT_NAME_MAP, NEST_EVENT
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)
@@ -53,6 +57,7 @@ _LOGGER = logging.getLogger(__name__)
CONF_PROJECT_ID = "project_id"
CONF_SUBSCRIBER_ID = "subscriber_id"
+
# Configuration for the legacy nest API
SERVICE_CANCEL_ETA = "cancel_eta"
SERVICE_SET_ETA = "set_eta"
@@ -158,30 +163,45 @@ async def async_setup(hass: HomeAssistant, config: dict):
return True
-class SignalUpdateCallback(EventCallback):
+class SignalUpdateCallback(AsyncEventCallback):
"""An EventCallback invoked when new events arrive from subscriber."""
def __init__(self, hass: HomeAssistant):
"""Initialize EventCallback."""
self._hass = hass
- def handle_event(self, event_message: EventMessage):
+ async def async_handle_event(self, event_message: EventMessage):
"""Process an incoming EventMessage."""
- _LOGGER.debug("Update %s @ %s", event_message.event_id, event_message.timestamp)
+ if not event_message.resource_update_name:
+ _LOGGER.debug("Ignoring event with no device_id")
+ return
+ device_id = event_message.resource_update_name
+ _LOGGER.debug("Update for %s @ %s", device_id, event_message.timestamp)
traits = event_message.resource_update_traits
if traits:
_LOGGER.debug("Trait update %s", traits.keys())
+ # This event triggered an update to a device that changed some
+ # properties which the DeviceManager should already have received.
+ # Send a signal to refresh state of all listening devices.
+ async_dispatcher_send(self._hass, SIGNAL_NEST_UPDATE)
events = event_message.resource_update_events
- if events:
- _LOGGER.debug("Event Update %s", events.keys())
-
- if not event_message.resource_update_traits:
- # Note: Currently ignoring events like camera motion
+ if not events:
return
- # This event triggered an update to a device that changed some
- # properties which the DeviceManager should already have received.
- # Send a signal to refresh state of all listening devices.
- async_dispatcher_send(self._hass, SIGNAL_NEST_UPDATE)
+ _LOGGER.debug("Event Update %s", events.keys())
+ device_registry = await self._hass.helpers.device_registry.async_get_registry()
+ device_entry = device_registry.async_get_device({(DOMAIN, device_id)}, ())
+ if not device_entry:
+ _LOGGER.debug("Ignoring event for unregistered device '%s'", device_id)
+ return
+ for event in events:
+ event_type = EVENT_NAME_MAP.get(event)
+ if not event_type:
+ continue
+ message = {
+ "device_id": device_entry.id,
+ "type": event_type,
+ }
+ self._hass.bus.async_fire(NEST_EVENT, message)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
@@ -208,8 +228,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
auth, config[CONF_PROJECT_ID], config[CONF_SUBSCRIBER_ID]
)
subscriber.set_update_callback(SignalUpdateCallback(hass))
- asyncio.create_task(subscriber.start_async())
- hass.data[DOMAIN][entry.entry_id] = subscriber
+
+ try:
+ await subscriber.start_async()
+ except GoogleNestException as err:
+ _LOGGER.error("Subscriber error: %s", err)
+ subscriber.stop_async()
+ raise ConfigEntryNotReady from err
+
+ try:
+ await subscriber.async_get_device_manager()
+ except GoogleNestException as err:
+ _LOGGER.error("Device Manager error: %s", err)
+ subscriber.stop_async()
+ raise ConfigEntryNotReady from err
+
+ hass.data[DOMAIN][DATA_SUBSCRIBER] = subscriber
for component in PLATFORMS:
hass.async_create_task(
@@ -225,7 +259,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
# Legacy API
return True
- subscriber = hass.data[DOMAIN][entry.entry_id]
+ subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER]
subscriber.stop_async()
unload_ok = all(
await asyncio.gather(
@@ -236,7 +270,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
)
)
if unload_ok:
- hass.data[DOMAIN].pop(entry.entry_id)
+ hass.data[DOMAIN].pop(DATA_SUBSCRIBER)
return unload_ok
diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py
index a8c5c86c4e8..cec35eeca29 100644
--- a/homeassistant/components/nest/camera_sdm.py
+++ b/homeassistant/components/nest/camera_sdm.py
@@ -4,20 +4,21 @@ import datetime
import logging
from typing import Optional
-from aiohttp.client_exceptions import ClientError
from google_nest_sdm.camera_traits import CameraImageTrait, CameraLiveStreamTrait
from google_nest_sdm.device import Device
+from google_nest_sdm.exceptions import GoogleNestException
from haffmpeg.tools import IMAGE_JPEG
from homeassistant.components.camera import SUPPORT_STREAM, Camera
from homeassistant.components.ffmpeg import async_get_image
from homeassistant.config_entries import ConfigEntry
+from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util.dt import utcnow
-from .const import DOMAIN, SIGNAL_NEST_UPDATE
+from .const import DATA_SUBSCRIBER, DOMAIN, SIGNAL_NEST_UPDATE
from .device_info import DeviceInfo
_LOGGER = logging.getLogger(__name__)
@@ -31,8 +32,11 @@ async def async_setup_sdm_entry(
) -> None:
"""Set up the cameras."""
- subscriber = hass.data[DOMAIN][entry.entry_id]
- device_manager = await subscriber.async_get_device_manager()
+ subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER]
+ try:
+ device_manager = await subscriber.async_get_device_manager()
+ except GoogleNestException as err:
+ raise PlatformNotReady from err
# Fetch initial data so we have data when entities subscribe.
@@ -130,7 +134,7 @@ class NestCamera(Camera):
self._stream_refresh_unsub = None
try:
self._stream = await self._stream.extend_rtsp_stream()
- except ClientError as err:
+ except GoogleNestException as err:
_LOGGER.debug("Failed to extend stream: %s", err)
# Next attempt to catch a url will get a new one
self._stream = None
diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py
index 57598e36ec9..e56d35c1dff 100644
--- a/homeassistant/components/nest/climate_sdm.py
+++ b/homeassistant/components/nest/climate_sdm.py
@@ -4,6 +4,7 @@ from typing import Optional
from google_nest_sdm.device import Device
from google_nest_sdm.device_traits import FanTrait, TemperatureTrait
+from google_nest_sdm.exceptions import GoogleNestException
from google_nest_sdm.thermostat_traits import (
ThermostatEcoTrait,
ThermostatHvacTrait,
@@ -34,10 +35,11 @@ from homeassistant.components.climate.const import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
+from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.typing import HomeAssistantType
-from .const import DOMAIN, SIGNAL_NEST_UPDATE
+from .const import DATA_SUBSCRIBER, DOMAIN, SIGNAL_NEST_UPDATE
from .device_info import DeviceInfo
# Mapping for sdm.devices.traits.ThermostatMode mode field
@@ -79,8 +81,11 @@ async def async_setup_sdm_entry(
) -> None:
"""Set up the client entities."""
- subscriber = hass.data[DOMAIN][entry.entry_id]
- device_manager = await subscriber.async_get_device_manager()
+ subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER]
+ try:
+ device_manager = await subscriber.async_get_device_manager()
+ except GoogleNestException as err:
+ raise PlatformNotReady from err
entities = []
for device in device_manager.devices.values():
diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py
index e792b496da5..6aaa5bcc489 100644
--- a/homeassistant/components/nest/config_flow.py
+++ b/homeassistant/components/nest/config_flow.py
@@ -177,7 +177,7 @@ class NestFlowHandler(
return self.async_abort(reason="authorize_url_timeout")
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected error generating auth url")
- return self.async_abort(reason="authorize_url_fail")
+ return self.async_abort(reason="unknown_authorize_url_generation")
return self.async_show_form(
step_id="link",
diff --git a/homeassistant/components/nest/const.py b/homeassistant/components/nest/const.py
index b418df97bba..3aba9ef5a7e 100644
--- a/homeassistant/components/nest/const.py
+++ b/homeassistant/components/nest/const.py
@@ -2,6 +2,7 @@
DOMAIN = "nest"
DATA_SDM = "sdm"
+DATA_SUBSCRIBER = "subscriber"
SIGNAL_NEST_UPDATE = "nest_update"
diff --git a/homeassistant/components/nest/device_trigger.py b/homeassistant/components/nest/device_trigger.py
new file mode 100644
index 00000000000..199dcf425de
--- /dev/null
+++ b/homeassistant/components/nest/device_trigger.py
@@ -0,0 +1,101 @@
+"""Provides device automations for Nest."""
+import logging
+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.components.device_automation.exceptions import (
+ InvalidDeviceAutomationConfig,
+)
+from homeassistant.components.homeassistant.triggers import event as event_trigger
+from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE
+from homeassistant.core import CALLBACK_TYPE, HomeAssistant
+from homeassistant.helpers.typing import ConfigType
+
+from .const import DATA_SUBSCRIBER, DOMAIN
+from .events import DEVICE_TRAIT_TRIGGER_MAP, NEST_EVENT
+
+_LOGGER = logging.getLogger(__name__)
+
+DEVICE = "device"
+
+TRIGGER_TYPES = set(DEVICE_TRAIT_TRIGGER_MAP.values())
+
+TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
+ {
+ vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
+ }
+)
+
+
+async def async_get_nest_device_id(hass: HomeAssistant, device_id: str) -> str:
+ """Get the nest API device_id from the HomeAssistant device_id."""
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+ device = device_registry.async_get(device_id)
+ for (domain, unique_id) in device.identifiers:
+ if domain == DOMAIN:
+ return unique_id
+ return None
+
+
+async def async_get_device_trigger_types(
+ hass: HomeAssistant, nest_device_id: str
+) -> List[str]:
+ """List event triggers supported for a Nest device."""
+ # All devices should have already been loaded so any failures here are
+ # "shouldn't happen" cases
+ subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER]
+ device_manager = await subscriber.async_get_device_manager()
+ nest_device = device_manager.devices.get(nest_device_id)
+ if not nest_device:
+ raise InvalidDeviceAutomationConfig(f"Nest device not found {nest_device_id}")
+
+ # Determine the set of event types based on the supported device traits
+ trigger_types = []
+ for trait in nest_device.traits.keys():
+ trigger_type = DEVICE_TRAIT_TRIGGER_MAP.get(trait)
+ if trigger_type:
+ trigger_types.append(trigger_type)
+ return trigger_types
+
+
+async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
+ """List device triggers for a Nest device."""
+ nest_device_id = await async_get_nest_device_id(hass, device_id)
+ if not nest_device_id:
+ raise InvalidDeviceAutomationConfig(f"Device not found {device_id}")
+ trigger_types = await async_get_device_trigger_types(hass, nest_device_id)
+ return [
+ {
+ CONF_PLATFORM: DEVICE,
+ CONF_DEVICE_ID: device_id,
+ CONF_DOMAIN: DOMAIN,
+ CONF_TYPE: trigger_type,
+ }
+ for trigger_type in trigger_types
+ ]
+
+
+async def async_attach_trigger(
+ hass: HomeAssistant,
+ config: ConfigType,
+ action: AutomationActionType,
+ automation_info: dict,
+) -> CALLBACK_TYPE:
+ """Attach a trigger."""
+ config = TRIGGER_SCHEMA(config)
+ event_config = event_trigger.TRIGGER_SCHEMA(
+ {
+ event_trigger.CONF_PLATFORM: "event",
+ event_trigger.CONF_EVENT_TYPE: NEST_EVENT,
+ event_trigger.CONF_EVENT_DATA: {
+ CONF_DEVICE_ID: config[CONF_DEVICE_ID],
+ CONF_TYPE: config[CONF_TYPE],
+ },
+ }
+ )
+ return await event_trigger.async_attach_trigger(
+ hass, event_config, action, automation_info, platform_type="device"
+ )
diff --git a/homeassistant/components/nest/events.py b/homeassistant/components/nest/events.py
new file mode 100644
index 00000000000..abfe688c71c
--- /dev/null
+++ b/homeassistant/components/nest/events.py
@@ -0,0 +1,49 @@
+"""Library from Pub/sub messages, events and device triggers."""
+
+from google_nest_sdm.camera_traits import (
+ CameraMotionTrait,
+ CameraPersonTrait,
+ CameraSoundTrait,
+)
+from google_nest_sdm.doorbell_traits import DoorbellChimeTrait
+from google_nest_sdm.event import (
+ CameraMotionEvent,
+ CameraPersonEvent,
+ CameraSoundEvent,
+ DoorbellChimeEvent,
+)
+
+NEST_EVENT = "nest_event"
+# The nest_event namespace will fire events that are triggered from messages
+# received via the Pub/Sub subscriber.
+#
+# An example event data payload:
+# {
+# "device_id": "enterprises/some/device/identifier"
+# "event_type": "camera_motion"
+# }
+#
+# The following event types are fired:
+EVENT_DOORBELL_CHIME = "doorbell_chime"
+EVENT_CAMERA_MOTION = "camera_motion"
+EVENT_CAMERA_PERSON = "camera_person"
+EVENT_CAMERA_SOUND = "camera_sound"
+
+# Mapping of supported device traits to home assistant event types. Devices
+# that support these traits will generate Pub/Sub event messages in
+# the EVENT_NAME_MAP
+DEVICE_TRAIT_TRIGGER_MAP = {
+ DoorbellChimeTrait.NAME: EVENT_DOORBELL_CHIME,
+ CameraMotionTrait.NAME: EVENT_CAMERA_MOTION,
+ CameraPersonTrait.NAME: EVENT_CAMERA_PERSON,
+ CameraSoundTrait.NAME: EVENT_CAMERA_SOUND,
+}
+
+# Mapping of incoming SDM Pub/Sub event message types to the home assistant
+# event type to fire.
+EVENT_NAME_MAP = {
+ DoorbellChimeEvent.NAME: EVENT_DOORBELL_CHIME,
+ CameraMotionEvent.NAME: EVENT_CAMERA_MOTION,
+ CameraPersonEvent.NAME: EVENT_CAMERA_PERSON,
+ CameraSoundEvent.NAME: EVENT_CAMERA_SOUND,
+}
diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json
index 9e8a48fa95f..60293612cd3 100644
--- a/homeassistant/components/nest/manifest.json
+++ b/homeassistant/components/nest/manifest.json
@@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/nest",
"requirements": [
"python-nest==4.1.0",
- "google-nest-sdm==0.1.14"
+ "google-nest-sdm==0.2.0"
],
"codeowners": [
"@awarecan",
diff --git a/homeassistant/components/nest/sensor_sdm.py b/homeassistant/components/nest/sensor_sdm.py
index 68c33529831..9009414c5b4 100644
--- a/homeassistant/components/nest/sensor_sdm.py
+++ b/homeassistant/components/nest/sensor_sdm.py
@@ -1,9 +1,11 @@
"""Support for Google Nest SDM sensors."""
+import logging
from typing import Optional
from google_nest_sdm.device import Device
from google_nest_sdm.device_traits import HumidityTrait, TemperatureTrait
+from google_nest_sdm.exceptions import GoogleNestException
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -12,13 +14,17 @@ from homeassistant.const import (
PERCENTAGE,
TEMP_CELSIUS,
)
+from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import HomeAssistantType
-from .const import DOMAIN, SIGNAL_NEST_UPDATE
+from .const import DATA_SUBSCRIBER, DOMAIN, SIGNAL_NEST_UPDATE
from .device_info import DeviceInfo
+_LOGGER = logging.getLogger(__name__)
+
+
DEVICE_TYPE_MAP = {
"sdm.devices.types.CAMERA": "Camera",
"sdm.devices.types.DISPLAY": "Display",
@@ -32,10 +38,12 @@ async def async_setup_sdm_entry(
) -> None:
"""Set up the sensors."""
- subscriber = hass.data[DOMAIN][entry.entry_id]
- device_manager = await subscriber.async_get_device_manager()
-
- # Fetch initial data so we have data when entities subscribe.
+ subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER]
+ try:
+ device_manager = await subscriber.async_get_device_manager()
+ except GoogleNestException as err:
+ _LOGGER.warning("Failed to get devices: %s", err)
+ raise PlatformNotReady from err
entities = []
for device in device_manager.devices.values():
diff --git a/homeassistant/components/nest/strings.json b/homeassistant/components/nest/strings.json
index 6882e8f55e7..f945469e26f 100644
--- a/homeassistant/components/nest/strings.json
+++ b/homeassistant/components/nest/strings.json
@@ -7,12 +7,16 @@
"init": {
"title": "Authentication Provider",
"description": "[%key:common::config_flow::title::oauth2_pick_implementation%]",
- "data": { "flow_impl": "Provider" }
+ "data": {
+ "flow_impl": "Provider"
+ }
},
"link": {
"title": "Link Nest Account",
"description": "To link your Nest account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided pin code below.",
- "data": { "code": "[%key:common::config_flow::data::pin%]" }
+ "data": {
+ "code": "[%key:common::config_flow::data::pin%]"
+ }
}
},
"error": {
@@ -25,11 +29,19 @@
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
- "authorize_url_fail": "Unknown error generating an authorize url.",
+ "unknown_authorize_url_generation": "[%key:common::config_flow::abort::unknown_authorize_url_generation%]",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]"
},
"create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]"
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "camera_person": "Person detected",
+ "camera_motion": "Motion detected",
+ "camera_sound": "Sound detected",
+ "doorbell_chime": "Doorbell pressed"
+ }
}
}
diff --git a/homeassistant/components/nest/translations/ca.json b/homeassistant/components/nest/translations/ca.json
index e65e264e3f7..46558f2e89a 100644
--- a/homeassistant/components/nest/translations/ca.json
+++ b/homeassistant/components/nest/translations/ca.json
@@ -5,7 +5,8 @@
"authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.",
"missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.",
"no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})",
- "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.",
+ "unknown_authorize_url_generation": "S'ha produ\u00eft un error desconegut al generar URL d'autoritzaci\u00f3."
},
"create_entry": {
"default": "Autenticaci\u00f3 exitosa"
diff --git a/homeassistant/components/nest/translations/cs.json b/homeassistant/components/nest/translations/cs.json
index 28268d9bab8..9ab94c993eb 100644
--- a/homeassistant/components/nest/translations/cs.json
+++ b/homeassistant/components/nest/translations/cs.json
@@ -5,7 +5,8 @@
"authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el",
"missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace.",
"no_url_available": "Nen\u00ed k dispozici \u017e\u00e1dn\u00e1 adresa URL. Informace o t\u00e9to chyb\u011b naleznete [v sekci n\u00e1pov\u011bdy]({docs_url})",
- "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace."
+ "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace.",
+ "unknown_authorize_url_generation": "Nezn\u00e1m\u00e1 chyba p\u0159i generov\u00e1n\u00ed autoriza\u010dn\u00ed URL adresy."
},
"create_entry": {
"default": "\u00dasp\u011b\u0161n\u011b ov\u011b\u0159eno"
@@ -35,5 +36,10 @@
"title": "Vyberte metodu ov\u011b\u0159en\u00ed"
}
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "camera_motion": "Detekov\u00e1n pohyb"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/nest/translations/en.json b/homeassistant/components/nest/translations/en.json
index 8839f24e30a..739d77c8268 100644
--- a/homeassistant/components/nest/translations/en.json
+++ b/homeassistant/components/nest/translations/en.json
@@ -5,7 +5,8 @@
"authorize_url_timeout": "Timeout generating authorize URL.",
"missing_configuration": "The component is not configured. Please follow the documentation.",
"no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})",
- "single_instance_allowed": "Already configured. Only a single configuration possible."
+ "single_instance_allowed": "Already configured. Only a single configuration possible.",
+ "unknown_authorize_url_generation": "Unknown error generating an authorize url."
},
"create_entry": {
"default": "Successfully authenticated"
@@ -35,5 +36,13 @@
"title": "Pick Authentication Method"
}
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "camera_motion": "Motion detected",
+ "camera_person": "Person detected",
+ "camera_sound": "Sound detected",
+ "doorbell_chime": "Doorbell pressed"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/nest/translations/es.json b/homeassistant/components/nest/translations/es.json
index 0c902ff89be..da5d717cb37 100644
--- a/homeassistant/components/nest/translations/es.json
+++ b/homeassistant/components/nest/translations/es.json
@@ -5,7 +5,8 @@
"authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.",
"missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.",
"no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})",
- "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n."
+ "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.",
+ "unknown_authorize_url_generation": "Error desconocido al generar una URL de autorizaci\u00f3n."
},
"create_entry": {
"default": "Autenticado con \u00e9xito"
@@ -35,5 +36,13 @@
"title": "Elija el m\u00e9todo de autenticaci\u00f3n"
}
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "camera_motion": "Movimiento detectado",
+ "camera_person": "Persona detectada",
+ "camera_sound": "Sonido detectado",
+ "doorbell_chime": "Timbre pulsado"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/nest/translations/et.json b/homeassistant/components/nest/translations/et.json
index 37f7e26a885..2e58ddeeddf 100644
--- a/homeassistant/components/nest/translations/et.json
+++ b/homeassistant/components/nest/translations/et.json
@@ -5,7 +5,8 @@
"authorize_url_timeout": "Tuvastamise URL-i loomise ajal\u00f5pp.",
"missing_configuration": "Osis pole seadistatud. Vaata dokumentatsiooni.",
"no_url_available": "URL pole saadaval. Selle t\u00f5rke kohta teabe saamiseks vaata [spikrijaotis]({docs_url})",
- "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine."
+ "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine.",
+ "unknown_authorize_url_generation": "Tundmatu viga tuvastamise URL-i loomisel."
},
"create_entry": {
"default": "Tuvastamine \u00f5nnestus"
@@ -35,5 +36,13 @@
"title": "Vali tuvastusmeetod"
}
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "camera_motion": "Tuvastati liikumine",
+ "camera_person": "Isik tuvastatud",
+ "camera_sound": "Tuvastati heli",
+ "doorbell_chime": "Uksekell helises"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/nest/translations/hu.json b/homeassistant/components/nest/translations/hu.json
index 8bbf6ebc955..d9a216305e8 100644
--- a/homeassistant/components/nest/translations/hu.json
+++ b/homeassistant/components/nest/translations/hu.json
@@ -4,6 +4,9 @@
"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."
},
+ "create_entry": {
+ "default": "Sikeres autentik\u00e1ci\u00f3"
+ },
"error": {
"internal_error": "Bels\u0151 hiba t\u00f6rt\u00e9nt a k\u00f3d valid\u00e1l\u00e1s\u00e1n\u00e1l",
"timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a k\u00f3d \u00e9rv\u00e9nyes\u00edt\u00e9se sor\u00e1n.",
diff --git a/homeassistant/components/nest/translations/it.json b/homeassistant/components/nest/translations/it.json
index d504b30c4bb..00e949b652b 100644
--- a/homeassistant/components/nest/translations/it.json
+++ b/homeassistant/components/nest/translations/it.json
@@ -5,7 +5,8 @@
"authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.",
"missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.",
"no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})",
- "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.",
+ "unknown_authorize_url_generation": "Errore sconosciuto durante la generazione di un URL di autorizzazione."
},
"create_entry": {
"default": "Autenticazione riuscita"
diff --git a/homeassistant/components/nest/translations/ka.json b/homeassistant/components/nest/translations/ka.json
new file mode 100644
index 00000000000..133efd8944f
--- /dev/null
+++ b/homeassistant/components/nest/translations/ka.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "abort": {
+ "no_url_available": "URL \u10db\u10d8\u10e3\u10ec\u10d5\u10d3\u10dd\u10db\u10d4\u10da\u10d8\u10d0. \u10d8\u10dc\u10e4\u10dd\u10e0\u10db\u10d0\u10ea\u10d8\u10d0\u10e1 \u10d0\u10db \u10e8\u10d4\u10ea\u10d3\u10dd\u10db\u10d8\u10e1\u10d0\u10d7\u10d5\u10d8\u10e1 , [\u10e8\u10d4\u10d0\u10db\u10dd\u10ec\u10db\u10d4\u10d7 help \u10e1\u10d4\u10e5\u10ea\u10d8\u10d0] ({docs_url})",
+ "unknown_authorize_url_generation": "\u10d0\u10d5\u10e2\u10dd\u10e0\u10d8\u10d6\u10d0\u10ea\u10d8\u10d8 \u10d2\u10d4\u10dc\u10d4\u10e0\u10d8\u10e0\u10d4\u10d1\u10d8\u10e1 \u10e3\u10ea\u10dc\u10dd\u10d1\u10d8 \u10e8\u10d4\u10ea\u10d3\u10dd\u10db\u10d0."
+ },
+ "create_entry": {
+ "default": "\u10d0\u10e7\u10d7\u10d4\u10dc\u10d7\u10d8\u10e4\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0 \u10ec\u10d0\u10e0\u10db\u10d0\u10e2\u10d4\u10d1\u10e3\u10da\u10d8\u10d0"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "\u10d0\u10d8\u10e0\u10e9\u10d8\u10d4\u10d7 \u10d0\u10e3\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10e1 \u10db\u10d4\u10d7\u10dd\u10d3\u10d8"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nest/translations/lb.json b/homeassistant/components/nest/translations/lb.json
index 4c28b3becc6..1f0115a429b 100644
--- a/homeassistant/components/nest/translations/lb.json
+++ b/homeassistant/components/nest/translations/lb.json
@@ -31,5 +31,13 @@
"title": "Authentifikatioun's Method auswielen"
}
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "camera_motion": "Bewegung erkannt",
+ "camera_person": "Persoun detekt\u00e9iert",
+ "camera_sound": "Toun detekt\u00e9iert",
+ "doorbell_chime": "Schell gedr\u00e9ckt"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/nest/translations/no.json b/homeassistant/components/nest/translations/no.json
index 4708ddc5350..69d67c5b4f2 100644
--- a/homeassistant/components/nest/translations/no.json
+++ b/homeassistant/components/nest/translations/no.json
@@ -5,7 +5,8 @@
"authorize_url_timeout": "Tidsavbrudd som genererer autorer URL-adresse.",
"missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.",
"no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})",
- "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.",
+ "unknown_authorize_url_generation": "Ukjent feil ved generering av autoriseringsadresse."
},
"create_entry": {
"default": "Vellykket godkjenning"
@@ -35,5 +36,13 @@
"title": "Velg godkjenningsmetode"
}
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "camera_motion": "Bevegelse oppdaget",
+ "camera_person": "Person oppdaget",
+ "camera_sound": "Lyd oppdaget",
+ "doorbell_chime": "Ringeklokke trykket"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/nest/translations/pl.json b/homeassistant/components/nest/translations/pl.json
index 77c86ddfb07..63e45df12fa 100644
--- a/homeassistant/components/nest/translations/pl.json
+++ b/homeassistant/components/nest/translations/pl.json
@@ -1,11 +1,12 @@
{
"config": {
"abort": {
- "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji",
+ "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania URL autoryzacji",
"authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji",
"missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.",
"no_url_available": "Brak dost\u0119pnego adresu URL. Aby uzyska\u0107 informacje na temat tego b\u0142\u0119du, [sprawd\u017a sekcj\u0119 pomocy] ({docs_url})",
- "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja."
+ "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.",
+ "unknown_authorize_url_generation": "Nieznany b\u0142\u0105d podczas generowania URL autoryzacji"
},
"create_entry": {
"default": "Pomy\u015blnie uwierzytelniono"
@@ -35,5 +36,13 @@
"title": "Wybierz metod\u0119 uwierzytelniania"
}
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "camera_motion": "nast\u0105pi wykrycie ruchu",
+ "camera_person": "nast\u0105pi wykrycie osoby",
+ "camera_sound": "nast\u0105pi wykrycie d\u017awi\u0119ku",
+ "doorbell_chime": "dzwonek zostanie wci\u015bni\u0119ty"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/nest/translations/ru.json b/homeassistant/components/nest/translations/ru.json
index be8905156bd..4060808c268 100644
--- a/homeassistant/components/nest/translations/ru.json
+++ b/homeassistant/components/nest/translations/ru.json
@@ -5,7 +5,8 @@
"authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.",
"missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438.",
"no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435.",
- "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e."
+ "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.",
+ "unknown_authorize_url_generation": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438."
},
"create_entry": {
"default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e."
@@ -35,5 +36,13 @@
"title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438"
}
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "camera_motion": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435",
+ "camera_person": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435 \u0447\u0435\u043b\u043e\u0432\u0435\u043a\u0430",
+ "camera_sound": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d \u0437\u0432\u0443\u043a",
+ "doorbell_chime": "\u041d\u0430\u0436\u0430\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0430 \u0434\u0432\u0435\u0440\u043d\u043e\u0433\u043e \u0437\u0432\u043e\u043d\u043a\u0430"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/nest/translations/zh-Hant.json b/homeassistant/components/nest/translations/zh-Hant.json
index d584381bdcc..80d0f8ee66a 100644
--- a/homeassistant/components/nest/translations/zh-Hant.json
+++ b/homeassistant/components/nest/translations/zh-Hant.json
@@ -5,7 +5,8 @@
"authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002",
"missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002",
"no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})",
- "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002"
+ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002",
+ "unknown_authorize_url_generation": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002"
},
"create_entry": {
"default": "\u5df2\u6210\u529f\u8a8d\u8b49"
@@ -35,5 +36,13 @@
"title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f"
}
}
+ },
+ "device_automation": {
+ "trigger_type": {
+ "camera_motion": "\u5075\u6e2c\u5230\u52d5\u4f5c",
+ "camera_person": "\u5075\u6e2c\u5230\u4eba\u54e1",
+ "camera_sound": "\u5075\u6e2c\u5230\u8072\u97f3",
+ "doorbell_chime": "\u9580\u9234\u6309\u4e0b"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/nightscout/translations/pl.json b/homeassistant/components/nightscout/translations/pl.json
index a21e509eec7..a544fc887c0 100644
--- a/homeassistant/components/nightscout/translations/pl.json
+++ b/homeassistant/components/nightscout/translations/pl.json
@@ -15,7 +15,7 @@
"api_key": "Klucz API",
"url": "URL"
},
- "description": "- URL: adres url Twojej instancji Nightscout. Np: https://myhomeassistant.duckdns.org:5423\n- Klucz API (opcjonalny): u\u017cywaj tylko wtedy, gdy Twoja instancja jest chroniona (auth_default_roles! = readable).",
+ "description": "- URL: adres URL Twojej instancji Nightscout. Np: https://m\u00f3jhomeassistant.duckdns.org:5423\n- Klucz API (opcjonalny): u\u017cywaj tylko wtedy, gdy Twoja instancja jest chroniona (auth_default_roles! = readable).",
"title": "Wprowad\u017a informacje o serwerze Nightscout."
}
}
diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py
index 08219567ed6..426c28bccff 100644
--- a/homeassistant/components/notify/__init__.py
+++ b/homeassistant/components/notify/__init__.py
@@ -110,6 +110,8 @@ class BaseNotificationService:
"""An abstract class for notification services."""
hass: Optional[HomeAssistantType] = None
+ # Name => target
+ registered_targets: Dict[str, str]
def send_message(self, message, **kwargs):
"""Send a message.
@@ -135,8 +137,8 @@ class BaseNotificationService:
title.hass = self.hass
kwargs[ATTR_TITLE] = title.async_render(parse_result=False)
- if self._registered_targets.get(service.service) is not None:
- kwargs[ATTR_TARGET] = [self._registered_targets[service.service]]
+ if self.registered_targets.get(service.service) is not None:
+ kwargs[ATTR_TARGET] = [self.registered_targets[service.service]]
elif service.data.get(ATTR_TARGET) is not None:
kwargs[ATTR_TARGET] = service.data.get(ATTR_TARGET)
@@ -157,23 +159,23 @@ class BaseNotificationService:
self.hass = hass
self._service_name = service_name
self._target_service_name_prefix = target_service_name_prefix
- self._registered_targets: Dict = {}
+ self.registered_targets = {}
async def async_register_services(self) -> None:
"""Create or update the notify services."""
assert self.hass
if hasattr(self, "targets"):
- stale_targets = set(self._registered_targets)
+ stale_targets = set(self.registered_targets)
# pylint: disable=no-member
for name, target in self.targets.items(): # type: ignore
target_name = slugify(f"{self._target_service_name_prefix}_{name}")
if target_name in stale_targets:
stale_targets.remove(target_name)
- if target_name in self._registered_targets:
+ if target_name in self.registered_targets:
continue
- self._registered_targets[target_name] = target
+ self.registered_targets[target_name] = target
self.hass.services.async_register(
DOMAIN,
target_name,
@@ -182,7 +184,7 @@ class BaseNotificationService:
)
for stale_target_name in stale_targets:
- del self._registered_targets[stale_target_name]
+ del self.registered_targets[stale_target_name]
self.hass.services.async_remove(
DOMAIN,
stale_target_name,
@@ -202,10 +204,10 @@ class BaseNotificationService:
"""Unregister the notify services."""
assert self.hass
- if self._registered_targets:
- remove_targets = set(self._registered_targets)
+ if self.registered_targets:
+ remove_targets = set(self.registered_targets)
for remove_target_name in remove_targets:
- del self._registered_targets[remove_target_name]
+ del self.registered_targets[remove_target_name]
self.hass.services.async_remove(
DOMAIN,
remove_target_name,
diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py
index 007f18525fe..88da19f5ab2 100644
--- a/homeassistant/components/notion/__init__.py
+++ b/homeassistant/components/notion/__init__.py
@@ -1,7 +1,6 @@
"""Support for Notion."""
import asyncio
from datetime import timedelta
-import logging
from aionotion import async_get_client
from aionotion.errors import InvalidCredentialsError, NotionError
@@ -21,9 +20,7 @@ from homeassistant.helpers.update_coordinator import (
UpdateFailed,
)
-from .const import DATA_COORDINATOR, DOMAIN
-
-_LOGGER = logging.getLogger(__name__)
+from .const import DATA_COORDINATOR, DOMAIN, LOGGER
PLATFORMS = ["binary_sensor", "sensor"]
@@ -33,7 +30,7 @@ ATTR_SYSTEM_NAME = "system_name"
DEFAULT_ATTRIBUTION = "Data provided by Notion"
DEFAULT_SCAN_INTERVAL = timedelta(minutes=1)
-CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.119")
+CONFIG_SCHEMA = cv.deprecated(DOMAIN)
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
@@ -56,10 +53,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], session
)
except InvalidCredentialsError:
- _LOGGER.error("Invalid username and/or password")
+ LOGGER.error("Invalid username and/or password")
return False
except NotionError as err:
- _LOGGER.error("Config entry failed: %s", err)
+ LOGGER.error("Config entry failed: %s", err)
raise ConfigEntryNotReady from err
async def async_update():
@@ -94,7 +91,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.entry_id
] = DataUpdateCoordinator(
hass,
- _LOGGER,
+ LOGGER,
name=entry.data[CONF_USERNAME],
update_interval=DEFAULT_SCAN_INTERVAL,
update_method=async_update,
diff --git a/homeassistant/components/notion/const.py b/homeassistant/components/notion/const.py
index 6a6da180374..5541cfedc70 100644
--- a/homeassistant/components/notion/const.py
+++ b/homeassistant/components/notion/const.py
@@ -1,5 +1,8 @@
"""Define constants for the Notion integration."""
+import logging
+
DOMAIN = "notion"
+LOGGER = logging.getLogger(__package__)
DATA_COORDINATOR = "coordinator"
diff --git a/homeassistant/components/notion/sensor.py b/homeassistant/components/notion/sensor.py
index 8f7337d200f..978e0aac46a 100644
--- a/homeassistant/components/notion/sensor.py
+++ b/homeassistant/components/notion/sensor.py
@@ -1,5 +1,4 @@
"""Support for Notion sensors."""
-import logging
from typing import Callable
from homeassistant.config_entries import ConfigEntry
@@ -8,9 +7,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import NotionEntity
-from .const import DATA_COORDINATOR, DOMAIN, SENSOR_TEMPERATURE
-
-_LOGGER = logging.getLogger(__name__)
+from .const import DATA_COORDINATOR, DOMAIN, LOGGER, SENSOR_TEMPERATURE
SENSOR_TYPES = {SENSOR_TEMPERATURE: ("Temperature", "temperature", TEMP_CELSIUS)}
@@ -84,7 +81,7 @@ class NotionSensor(NotionEntity):
if task["task_type"] == SENSOR_TEMPERATURE:
self._state = round(float(task["status"]["value"]), 1)
else:
- _LOGGER.error(
+ LOGGER.error(
"Unknown task type: %s: %s",
self.coordinator.data["sensors"][self._sensor_id],
task["task_type"],
diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py
new file mode 100644
index 00000000000..2fd04943e4d
--- /dev/null
+++ b/homeassistant/components/number/__init__.py
@@ -0,0 +1,103 @@
+"""Component to allow numeric input for platforms."""
+from datetime import timedelta
+import logging
+from typing import Any, Dict
+
+import voluptuous as vol
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.helpers.config_validation import ( # noqa: F401
+ PLATFORM_SCHEMA,
+ PLATFORM_SCHEMA_BASE,
+)
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.entity_component import EntityComponent
+from homeassistant.helpers.typing import ConfigType, HomeAssistantType
+
+from .const import (
+ ATTR_MAX,
+ ATTR_MIN,
+ ATTR_STEP,
+ ATTR_VALUE,
+ DEFAULT_MAX_VALUE,
+ DEFAULT_MIN_VALUE,
+ DEFAULT_STEP,
+ DOMAIN,
+ SERVICE_SET_VALUE,
+)
+
+SCAN_INTERVAL = timedelta(seconds=30)
+
+ENTITY_ID_FORMAT = DOMAIN + ".{}"
+
+MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
+ """Set up Number entities."""
+ component = hass.data[DOMAIN] = EntityComponent(
+ _LOGGER, DOMAIN, hass, SCAN_INTERVAL
+ )
+ await component.async_setup(config)
+
+ component.async_register_entity_service(
+ SERVICE_SET_VALUE,
+ {vol.Required(ATTR_VALUE): vol.Coerce(float)},
+ "async_set_value",
+ )
+
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
+ """Set up a config entry."""
+ return await hass.data[DOMAIN].async_setup_entry(entry) # type: ignore
+
+
+async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
+ """Unload a config entry."""
+ return await hass.data[DOMAIN].async_unload_entry(entry) # type: ignore
+
+
+class NumberEntity(Entity):
+ """Representation of a Number entity."""
+
+ @property
+ def capability_attributes(self) -> Dict[str, Any]:
+ """Return capability attributes."""
+ return {
+ ATTR_MIN: self.min_value,
+ ATTR_MAX: self.max_value,
+ ATTR_STEP: self.step,
+ }
+
+ @property
+ def min_value(self) -> float:
+ """Return the minimum value."""
+ return DEFAULT_MIN_VALUE
+
+ @property
+ def max_value(self) -> float:
+ """Return the maximum value."""
+ return DEFAULT_MAX_VALUE
+
+ @property
+ def step(self) -> float:
+ """Return the increment/decrement step."""
+ step = DEFAULT_STEP
+ value_range = abs(self.max_value - self.min_value)
+ if value_range != 0:
+ while value_range <= step:
+ step /= 10.0
+ return step
+
+ def set_value(self, value: float) -> None:
+ """Set new value."""
+ raise NotImplementedError()
+
+ async def async_set_value(self, value: float) -> None:
+ """Set new value."""
+ assert self.hass is not None
+ await self.hass.async_add_executor_job(self.set_value, value)
diff --git a/homeassistant/components/number/const.py b/homeassistant/components/number/const.py
new file mode 100644
index 00000000000..2aa8075cba3
--- /dev/null
+++ b/homeassistant/components/number/const.py
@@ -0,0 +1,14 @@
+"""Provides the constants needed for the component."""
+
+ATTR_VALUE = "value"
+ATTR_MIN = "min"
+ATTR_MAX = "max"
+ATTR_STEP = "step"
+
+DEFAULT_MIN_VALUE = 0.0
+DEFAULT_MAX_VALUE = 100.0
+DEFAULT_STEP = 1.0
+
+DOMAIN = "number"
+
+SERVICE_SET_VALUE = "set_value"
diff --git a/homeassistant/components/number/manifest.json b/homeassistant/components/number/manifest.json
new file mode 100644
index 00000000000..549494fa3f5
--- /dev/null
+++ b/homeassistant/components/number/manifest.json
@@ -0,0 +1,7 @@
+{
+ "domain": "number",
+ "name": "Number",
+ "documentation": "https://www.home-assistant.io/integrations/number",
+ "codeowners": ["@home-assistant/core", "@Shulyaka"],
+ "quality_scale": "internal"
+}
diff --git a/homeassistant/components/number/services.yaml b/homeassistant/components/number/services.yaml
new file mode 100644
index 00000000000..d18416f9974
--- /dev/null
+++ b/homeassistant/components/number/services.yaml
@@ -0,0 +1,11 @@
+# Describes the format for available Number entity services
+
+set_value:
+ description: Set the value of a Number entity.
+ fields:
+ entity_id:
+ description: Entity ID of the Number to set the new value.
+ example: number.volume
+ value:
+ description: The target value the entity should be set to.
+ example: 42
diff --git a/homeassistant/components/nzbget/translations/sl.json b/homeassistant/components/nzbget/translations/sl.json
new file mode 100644
index 00000000000..d4e640e4069
--- /dev/null
+++ b/homeassistant/components/nzbget/translations/sl.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "name": "Ime"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/onewire/binary_sensor.py b/homeassistant/components/onewire/binary_sensor.py
index 031dd984702..86b584c998c 100644
--- a/homeassistant/components/onewire/binary_sensor.py
+++ b/homeassistant/components/onewire/binary_sensor.py
@@ -94,29 +94,28 @@ def get_entities(onewirehub: OneWireHub):
for device in onewirehub.devices:
family = device["family"]
device_type = device["type"]
- sensor_id = os.path.split(os.path.split(device["path"])[0])[1]
+ device_id = os.path.split(os.path.split(device["path"])[0])[1]
if family not in DEVICE_BINARY_SENSORS:
continue
device_info = {
- "identifiers": {(DOMAIN, sensor_id)},
+ "identifiers": {(DOMAIN, device_id)},
"manufacturer": "Maxim Integrated",
"model": device_type,
- "name": sensor_id,
+ "name": device_id,
}
- for device_sensor in DEVICE_BINARY_SENSORS[family]:
- device_file = os.path.join(
- os.path.split(device["path"])[0], device_sensor["path"]
+ for entity_specs in DEVICE_BINARY_SENSORS[family]:
+ entity_path = os.path.join(
+ os.path.split(device["path"])[0], entity_specs["path"]
)
entities.append(
OneWireProxyBinarySensor(
- sensor_id,
- device_file,
- device_sensor["type"],
- device_sensor["name"],
- device_info,
- device_sensor.get("default_disabled", False),
- onewirehub.owproxy,
+ device_id=device_id,
+ device_name=device_id,
+ device_info=device_info,
+ entity_path=entity_path,
+ entity_specs=entity_specs,
+ owproxy=onewirehub.owproxy,
)
)
diff --git a/homeassistant/components/onewire/onewire_entities.py b/homeassistant/components/onewire/onewire_entities.py
index d16a2e9b493..9238bb5d32c 100644
--- a/homeassistant/components/onewire/onewire_entities.py
+++ b/homeassistant/components/onewire/onewire_entities.py
@@ -28,6 +28,7 @@ class OneWireBaseEntity(Entity):
entity_name: str = None,
device_info=None,
default_disabled: bool = False,
+ unique_id: str = None,
):
"""Initialize the entity."""
self._name = f"{name} {entity_name or entity_type.capitalize()}"
@@ -39,6 +40,7 @@ class OneWireBaseEntity(Entity):
self._state = None
self._value_raw = None
self._default_disabled = default_disabled
+ self._unique_id = unique_id or device_file
@property
def name(self) -> Optional[str]:
@@ -63,7 +65,7 @@ class OneWireBaseEntity(Entity):
@property
def unique_id(self) -> Optional[str]:
"""Return a unique ID."""
- return self._device_file
+ return self._unique_id
@property
def device_info(self) -> Optional[Dict[str, Any]]:
@@ -81,17 +83,22 @@ class OneWireProxyEntity(OneWireBaseEntity):
def __init__(
self,
- name: str,
- device_file: str,
- entity_type: str,
- entity_name: str,
+ device_id: str,
+ device_name: str,
device_info: Dict[str, Any],
- default_disabled: bool,
+ entity_path: str,
+ entity_specs: Dict[str, Any],
owproxy: protocol._Proxy,
):
"""Initialize the sensor."""
super().__init__(
- name, device_file, entity_type, entity_name, device_info, default_disabled
+ name=device_name,
+ device_file=entity_path,
+ entity_type=entity_specs["type"],
+ entity_name=entity_specs["name"],
+ device_info=device_info,
+ default_disabled=entity_specs.get("default_disabled", False),
+ unique_id=f"/{device_id}/{entity_specs['path']}",
)
self._owproxy = owproxy
diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py
index f908c1ada2d..98090dc949f 100644
--- a/homeassistant/components/onewire/sensor.py
+++ b/homeassistant/components/onewire/sensor.py
@@ -244,7 +244,7 @@ def get_entities(onewirehub: OneWireHub, config):
for device in onewirehub.devices:
family = device["family"]
device_type = device["type"]
- sensor_id = os.path.split(os.path.split(device["path"])[0])[1]
+ device_id = os.path.split(os.path.split(device["path"])[0])[1]
dev_type = "std"
if "EF" in family:
dev_type = "HobbyBoard"
@@ -254,38 +254,37 @@ def get_entities(onewirehub: OneWireHub, config):
_LOGGER.warning(
"Ignoring unknown family (%s) of sensor found for device: %s",
family,
- sensor_id,
+ device_id,
)
continue
device_info = {
- "identifiers": {(DOMAIN, sensor_id)},
+ "identifiers": {(DOMAIN, device_id)},
"manufacturer": "Maxim Integrated",
"model": device_type,
- "name": sensor_id,
+ "name": device_id,
}
- for device_sensor in hb_info_from_type(dev_type)[family]:
- if device_sensor["type"] == SENSOR_TYPE_MOISTURE:
- s_id = device_sensor["path"].split(".")[1]
+ for entity_specs in hb_info_from_type(dev_type)[family]:
+ if entity_specs["type"] == SENSOR_TYPE_MOISTURE:
+ s_id = entity_specs["path"].split(".")[1]
is_leaf = int(
onewirehub.owproxy.read(
f"{device['path']}moisture/is_leaf.{s_id}"
).decode()
)
if is_leaf:
- device_sensor["type"] = SENSOR_TYPE_WETNESS
- device_sensor["name"] = f"Wetness {s_id}"
- device_file = os.path.join(
- os.path.split(device["path"])[0], device_sensor["path"]
+ entity_specs["type"] = SENSOR_TYPE_WETNESS
+ entity_specs["name"] = f"Wetness {s_id}"
+ entity_path = os.path.join(
+ os.path.split(device["path"])[0], entity_specs["path"]
)
entities.append(
OneWireProxySensor(
- device_names.get(sensor_id, sensor_id),
- device_file,
- device_sensor["type"],
- device_sensor["name"],
- device_info,
- device_sensor.get("default_disabled", False),
- onewirehub.owproxy,
+ device_id=device_id,
+ device_name=device_names.get(device_id, device_id),
+ device_info=device_info,
+ entity_path=entity_path,
+ entity_specs=entity_specs,
+ owproxy=onewirehub.owproxy,
)
)
diff --git a/homeassistant/components/onewire/switch.py b/homeassistant/components/onewire/switch.py
index f1b588690ae..da1ed01a980 100644
--- a/homeassistant/components/onewire/switch.py
+++ b/homeassistant/components/onewire/switch.py
@@ -157,30 +157,29 @@ def get_entities(onewirehub: OneWireHub):
for device in onewirehub.devices:
family = device["family"]
device_type = device["type"]
- sensor_id = os.path.split(os.path.split(device["path"])[0])[1]
+ device_id = os.path.split(os.path.split(device["path"])[0])[1]
if family not in DEVICE_SWITCHES:
continue
device_info = {
- "identifiers": {(DOMAIN, sensor_id)},
+ "identifiers": {(DOMAIN, device_id)},
"manufacturer": "Maxim Integrated",
"model": device_type,
- "name": sensor_id,
+ "name": device_id,
}
- for device_switch in DEVICE_SWITCHES[family]:
- device_file = os.path.join(
- os.path.split(device["path"])[0], device_switch["path"]
+ for entity_specs in DEVICE_SWITCHES[family]:
+ entity_path = os.path.join(
+ os.path.split(device["path"])[0], entity_specs["path"]
)
entities.append(
OneWireProxySwitch(
- sensor_id,
- device_file,
- device_switch["type"],
- device_switch["name"],
- device_info,
- device_switch.get("default_disabled", False),
- onewirehub.owproxy,
+ device_id=device_id,
+ device_name=device_id,
+ device_info=device_info,
+ entity_path=entity_path,
+ entity_specs=entity_specs,
+ owproxy=onewirehub.owproxy,
)
)
diff --git a/homeassistant/components/onewire/translations/hu.json b/homeassistant/components/onewire/translations/hu.json
new file mode 100644
index 00000000000..8ac8f6d3b03
--- /dev/null
+++ b/homeassistant/components/onewire/translations/hu.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_path": "A k\u00f6nyvt\u00e1r nem tal\u00e1lhat\u00f3."
+ },
+ "step": {
+ "owserver": {
+ "data": {
+ "host": "Gazdag\u00e9p",
+ "port": "Port"
+ }
+ },
+ "user": {
+ "data": {
+ "type": "Kapcsolat t\u00edpusa"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/onewire/translations/ka.json b/homeassistant/components/onewire/translations/ka.json
new file mode 100644
index 00000000000..1b3c7e8ef5c
--- /dev/null
+++ b/homeassistant/components/onewire/translations/ka.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0 \u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0"
+ },
+ "error": {
+ "cannot_connect": "\u10d3\u10d0\u10d9\u10d0\u10d5\u10e8\u10e0\u10d4\u10d1\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0",
+ "invalid_path": "\u10d3\u10d8\u10e0\u10d4\u10e5\u10e2\u10dd\u10e0\u10d8\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10d8\u10eb\u10d4\u10d1\u10dc\u10d0."
+ },
+ "step": {
+ "owserver": {
+ "data": {
+ "host": "\u10f0\u10dd\u10e1\u10e2\u10d8",
+ "port": "\u10de\u10dd\u10e0\u10e2\u10d8"
+ },
+ "title": "owserver \u10e1\u10d4\u10e0\u10d5\u10d4\u10e0\u10d8\u10e1 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10d0"
+ },
+ "user": {
+ "data": {
+ "type": "\u1c99\u10d0\u10d5\u10e8\u10d8\u10e0\u10d8\u10e1 \u10e2\u10d8\u10de\u10d8"
+ },
+ "title": "1-\u10db\u10d0\u10d5\u10d7\u10d8\u10da\u10d8\u10e1 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10d0"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/onewire/translations/sl.json b/homeassistant/components/onewire/translations/sl.json
new file mode 100644
index 00000000000..7011c57c099
--- /dev/null
+++ b/homeassistant/components/onewire/translations/sl.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "type": "Vrsta povezave"
+ },
+ "title": "Nastavite 1-Wire"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py
index d9378588b03..50390464df8 100644
--- a/homeassistant/components/onvif/camera.py
+++ b/homeassistant/components/onvif/camera.py
@@ -35,6 +35,7 @@ from .const import (
LOGGER,
RELATIVE_MOVE,
SERVICE_PTZ,
+ STOP_MOVE,
ZOOM_IN,
ZOOM_OUT,
)
@@ -54,7 +55,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
vol.Optional(ATTR_DISTANCE, default=0.1): cv.small_float,
vol.Optional(ATTR_SPEED, default=0.5): cv.small_float,
vol.Optional(ATTR_MOVE_MODE, default=RELATIVE_MOVE): vol.In(
- [CONTINUOUS_MOVE, RELATIVE_MOVE, ABSOLUTE_MOVE, GOTOPRESET_MOVE]
+ [
+ CONTINUOUS_MOVE,
+ RELATIVE_MOVE,
+ ABSOLUTE_MOVE,
+ GOTOPRESET_MOVE,
+ STOP_MOVE,
+ ]
),
vol.Optional(ATTR_CONTINUOUS_DURATION, default=0.5): cv.small_float,
vol.Optional(ATTR_PRESET, default="0"): cv.string,
@@ -129,7 +136,7 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera):
)
if image is None:
- ffmpeg = ImageFrame(self.hass.data[DATA_FFMPEG].binary, loop=self.hass.loop)
+ ffmpeg = ImageFrame(self.hass.data[DATA_FFMPEG].binary)
image = await asyncio.shield(
ffmpeg.get_image(
self._stream_uri,
@@ -147,7 +154,7 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera):
LOGGER.debug("Handling mjpeg stream from camera '%s'", self.device.name)
ffmpeg_manager = self.hass.data[DATA_FFMPEG]
- stream = CameraMjpeg(ffmpeg_manager.binary, loop=self.hass.loop)
+ stream = CameraMjpeg(ffmpeg_manager.binary)
await stream.open_camera(
self._stream_uri,
diff --git a/homeassistant/components/onvif/const.py b/homeassistant/components/onvif/const.py
index 2ac78622f05..dc0688c4b30 100644
--- a/homeassistant/components/onvif/const.py
+++ b/homeassistant/components/onvif/const.py
@@ -39,5 +39,6 @@ CONTINUOUS_MOVE = "ContinuousMove"
RELATIVE_MOVE = "RelativeMove"
ABSOLUTE_MOVE = "AbsoluteMove"
GOTOPRESET_MOVE = "GotoPreset"
+STOP_MOVE = "Stop"
SERVICE_PTZ = "ptz"
diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py
index 599e6084581..84761a4777f 100644
--- a/homeassistant/components/onvif/device.py
+++ b/homeassistant/components/onvif/device.py
@@ -28,6 +28,7 @@ from .const import (
LOGGER,
PAN_FACTOR,
RELATIVE_MOVE,
+ STOP_MOVE,
TILT_FACTOR,
ZOOM_FACTOR,
)
@@ -433,6 +434,8 @@ class ONVIFDevice:
"Zoom": {"x": speed_val},
}
await ptz_service.GotoPreset(req)
+ elif move_mode == STOP_MOVE:
+ await ptz_service.Stop(req)
except ONVIFError as err:
if "Bad Request" in err.reason:
LOGGER.warning("Device '%s' doesn't support PTZ.", self.name)
diff --git a/homeassistant/components/onvif/services.yaml b/homeassistant/components/onvif/services.yaml
index e5a8c9fce35..bed426e9924 100644
--- a/homeassistant/components/onvif/services.yaml
+++ b/homeassistant/components/onvif/services.yaml
@@ -29,6 +29,6 @@ ptz:
description: "PTZ preset profile token. Sets the preset profile token which is executed with GotoPreset"
example: "1"
move_mode:
- description: "PTZ moving mode. One of ContinuousMove, RelativeMove, AbsoluteMove or GotoPreset"
+ description: "PTZ moving mode. One of ContinuousMove, RelativeMove, AbsoluteMove, GotoPreset, or Stop"
default: "RelativeMove"
example: "ContinuousMove"
diff --git a/homeassistant/components/onvif/translations/hu.json b/homeassistant/components/onvif/translations/hu.json
index dfa0aec8765..c9c3f137984 100644
--- a/homeassistant/components/onvif/translations/hu.json
+++ b/homeassistant/components/onvif/translations/hu.json
@@ -1,5 +1,8 @@
{
"config": {
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
"step": {
"auth": {
"data": {
@@ -7,11 +10,21 @@
"username": "Felhaszn\u00e1l\u00f3n\u00e9v"
}
},
+ "configure_profile": {
+ "data": {
+ "include": "Hozzon l\u00e9tre kamera entit\u00e1st"
+ },
+ "description": "L\u00e9trehozza a(z) {profile} f\u00e9nyk\u00e9pez\u0151g\u00e9p entit\u00e1s\u00e1t {resolution} felbont\u00e1ssal?",
+ "title": "Profilok konfigur\u00e1l\u00e1sa"
+ },
"manual_input": {
"data": {
"host": "Hoszt",
"port": "Port"
}
+ },
+ "user": {
+ "description": "A k\u00fcld\u00e9s gombra kattintva olyan ONVIF-eszk\u00f6z\u00f6ket keres\u00fcnk a h\u00e1l\u00f3zat\u00e1ban, amelyek t\u00e1mogatj\u00e1k az S profilt.\n\nEgyes gy\u00e1rt\u00f3k alap\u00e9rtelmez\u00e9s szerint elkezdt\u00e9k letiltani az ONVIF-et. Ellen\u0151rizze, hogy az ONVIF enged\u00e9lyezve van-e a kamera konfigur\u00e1ci\u00f3j\u00e1ban."
}
}
}
diff --git a/homeassistant/components/opentherm_gw/translations/ka.json b/homeassistant/components/opentherm_gw/translations/ka.json
new file mode 100644
index 00000000000..01c8a2e4811
--- /dev/null
+++ b/homeassistant/components/opentherm_gw/translations/ka.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u10d3\u10d0\u10d9\u10d0\u10d5\u10e8\u10e0\u10d4\u10d1\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/opentherm_gw/translations/no.json b/homeassistant/components/opentherm_gw/translations/no.json
index 7789573a42e..76118924e0a 100644
--- a/homeassistant/components/opentherm_gw/translations/no.json
+++ b/homeassistant/components/opentherm_gw/translations/no.json
@@ -3,7 +3,7 @@
"error": {
"already_configured": "Enheten er allerede konfigurert",
"cannot_connect": "Tilkobling mislyktes",
- "id_exists": "Gateway-ID finnes allerede"
+ "id_exists": "Gateway ID finnes allerede"
},
"step": {
"init": {
diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py
index 5161011dd4c..ce75365771d 100644
--- a/homeassistant/components/openuv/__init__.py
+++ b/homeassistant/components/openuv/__init__.py
@@ -1,6 +1,5 @@
"""Support for UV data from openuv.io."""
import asyncio
-import logging
from pyopenuv import Client
from pyopenuv.errors import OpenUvError
@@ -24,14 +23,14 @@ from homeassistant.helpers.dispatcher import (
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.service import verify_domain_control
-from .const import DOMAIN
-
-_LOGGER = logging.getLogger(__name__)
-
-DATA_OPENUV_CLIENT = "data_client"
-DATA_OPENUV_LISTENER = "data_listener"
-DATA_PROTECTION_WINDOW = "protection_window"
-DATA_UV = "uv"
+from .const import (
+ DATA_CLIENT,
+ DATA_LISTENER,
+ DATA_PROTECTION_WINDOW,
+ DATA_UV,
+ DOMAIN,
+ LOGGER,
+)
DEFAULT_ATTRIBUTION = "Data provided by OpenUV"
@@ -40,24 +39,12 @@ NOTIFICATION_TITLE = "OpenUV Component Setup"
TOPIC_UPDATE = f"{DOMAIN}_data_update"
-TYPE_CURRENT_OZONE_LEVEL = "current_ozone_level"
-TYPE_CURRENT_UV_INDEX = "current_uv_index"
-TYPE_CURRENT_UV_LEVEL = "current_uv_level"
-TYPE_MAX_UV_INDEX = "max_uv_index"
-TYPE_PROTECTION_WINDOW = "uv_protection_window"
-TYPE_SAFE_EXPOSURE_TIME_1 = "safe_exposure_time_type_1"
-TYPE_SAFE_EXPOSURE_TIME_2 = "safe_exposure_time_type_2"
-TYPE_SAFE_EXPOSURE_TIME_3 = "safe_exposure_time_type_3"
-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"]
async def async_setup(hass, config):
"""Set up the OpenUV component."""
- hass.data[DOMAIN] = {DATA_OPENUV_CLIENT: {}, DATA_OPENUV_LISTENER: {}}
+ hass.data[DOMAIN] = {DATA_CLIENT: {}, DATA_LISTENER: {}}
return True
@@ -77,9 +64,9 @@ async def async_setup_entry(hass, config_entry):
)
)
await openuv.async_update()
- hass.data[DOMAIN][DATA_OPENUV_CLIENT][config_entry.entry_id] = openuv
+ hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = openuv
except OpenUvError as err:
- _LOGGER.error("Config entry failed: %s", err)
+ LOGGER.error("Config entry failed: %s", err)
raise ConfigEntryNotReady from err
for component in PLATFORMS:
@@ -90,21 +77,21 @@ async def async_setup_entry(hass, config_entry):
@_verify_domain_control
async def update_data(service):
"""Refresh all OpenUV data."""
- _LOGGER.debug("Refreshing all OpenUV data")
+ LOGGER.debug("Refreshing all OpenUV data")
await openuv.async_update()
async_dispatcher_send(hass, TOPIC_UPDATE)
@_verify_domain_control
async def update_uv_index_data(service):
"""Refresh OpenUV UV index data."""
- _LOGGER.debug("Refreshing OpenUV UV index data")
+ LOGGER.debug("Refreshing OpenUV UV index data")
await openuv.async_update_uv_index_data()
async_dispatcher_send(hass, TOPIC_UPDATE)
@_verify_domain_control
async def update_protection_data(service):
"""Refresh OpenUV protection window data."""
- _LOGGER.debug("Refreshing OpenUV protection window data")
+ LOGGER.debug("Refreshing OpenUV protection window data")
await openuv.async_update_protection_data()
async_dispatcher_send(hass, TOPIC_UPDATE)
@@ -129,7 +116,7 @@ async def async_unload_entry(hass, config_entry):
)
)
if unload_ok:
- hass.data[DOMAIN][DATA_OPENUV_CLIENT].pop(config_entry.entry_id)
+ hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id)
return unload_ok
@@ -139,7 +126,7 @@ async def async_migrate_entry(hass, config_entry):
version = config_entry.version
data = {**config_entry.data}
- _LOGGER.debug("Migrating from version %s", version)
+ LOGGER.debug("Migrating from version %s", version)
# 1 -> 2: Remove unused condition data:
if version == 1:
@@ -147,7 +134,7 @@ async def async_migrate_entry(hass, config_entry):
data.pop(CONF_SENSORS, None)
version = config_entry.version = 2
hass.config_entries.async_update_entry(config_entry, data=data)
- _LOGGER.debug("Migration to version %s successful", version)
+ LOGGER.debug("Migration to version %s successful", version)
return True
@@ -166,7 +153,7 @@ class OpenUV:
resp = await self.client.uv_protection_window()
self.data[DATA_PROTECTION_WINDOW] = resp["result"]
except OpenUvError as err:
- _LOGGER.error("Error during protection data update: %s", err)
+ LOGGER.error("Error during protection data update: %s", err)
self.data[DATA_PROTECTION_WINDOW] = {}
async def async_update_uv_index_data(self):
@@ -175,7 +162,7 @@ class OpenUV:
data = await self.client.uv_index()
self.data[DATA_UV] = data
except OpenUvError as err:
- _LOGGER.error("Error during uv index data update: %s", err)
+ LOGGER.error("Error during uv index data update: %s", err)
self.data[DATA_UV] = {}
async def async_update(self):
diff --git a/homeassistant/components/openuv/binary_sensor.py b/homeassistant/components/openuv/binary_sensor.py
index 2d514b33cf3..62a83cdb141 100644
--- a/homeassistant/components/openuv/binary_sensor.py
+++ b/homeassistant/components/openuv/binary_sensor.py
@@ -1,20 +1,17 @@
"""Support for OpenUV binary sensors."""
-import logging
-
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.core import callback
from homeassistant.util.dt import as_local, parse_datetime, utcnow
-from . import (
- DATA_OPENUV_CLIENT,
+from . import OpenUvEntity
+from .const import (
+ DATA_CLIENT,
DATA_PROTECTION_WINDOW,
DOMAIN,
+ LOGGER,
TYPE_PROTECTION_WINDOW,
- OpenUvEntity,
)
-_LOGGER = logging.getLogger(__name__)
-
ATTR_PROTECTION_WINDOW_ENDING_TIME = "end_time"
ATTR_PROTECTION_WINDOW_ENDING_UV = "end_uv"
ATTR_PROTECTION_WINDOW_STARTING_TIME = "start_time"
@@ -25,7 +22,7 @@ BINARY_SENSORS = {TYPE_PROTECTION_WINDOW: ("Protection Window", "mdi:sunglasses"
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up an OpenUV sensor based on a config entry."""
- openuv = hass.data[DOMAIN][DATA_OPENUV_CLIENT][entry.entry_id]
+ openuv = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id]
binary_sensors = []
for kind, attrs in BINARY_SENSORS.items():
@@ -86,7 +83,7 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorEntity):
for key in ("from_time", "to_time", "from_uv", "to_uv"):
if not data.get(key):
- _LOGGER.info("Skipping update due to missing data: %s", key)
+ LOGGER.info("Skipping update due to missing data: %s", key)
return
if self._sensor_type == TYPE_PROTECTION_WINDOW:
diff --git a/homeassistant/components/openuv/const.py b/homeassistant/components/openuv/const.py
index fb8641ed6ec..683e349eb50 100644
--- a/homeassistant/components/openuv/const.py
+++ b/homeassistant/components/openuv/const.py
@@ -1,2 +1,22 @@
"""Define constants for the OpenUV component."""
+import logging
+
DOMAIN = "openuv"
+LOGGER = logging.getLogger(__package__)
+
+DATA_CLIENT = "data_client"
+DATA_LISTENER = "data_listener"
+DATA_PROTECTION_WINDOW = "protection_window"
+DATA_UV = "uv"
+
+TYPE_CURRENT_OZONE_LEVEL = "current_ozone_level"
+TYPE_CURRENT_UV_INDEX = "current_uv_index"
+TYPE_CURRENT_UV_LEVEL = "current_uv_level"
+TYPE_MAX_UV_INDEX = "max_uv_index"
+TYPE_PROTECTION_WINDOW = "uv_protection_window"
+TYPE_SAFE_EXPOSURE_TIME_1 = "safe_exposure_time_type_1"
+TYPE_SAFE_EXPOSURE_TIME_2 = "safe_exposure_time_type_2"
+TYPE_SAFE_EXPOSURE_TIME_3 = "safe_exposure_time_type_3"
+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"
diff --git a/homeassistant/components/openuv/sensor.py b/homeassistant/components/openuv/sensor.py
index 781f40d75b1..b9c73023c11 100644
--- a/homeassistant/components/openuv/sensor.py
+++ b/homeassistant/components/openuv/sensor.py
@@ -1,12 +1,11 @@
"""Support for OpenUV sensors."""
-import logging
-
from homeassistant.const import TIME_MINUTES, UV_INDEX
from homeassistant.core import callback
from homeassistant.util.dt import as_local, parse_datetime
-from . import (
- DATA_OPENUV_CLIENT,
+from . import OpenUvEntity
+from .const import (
+ DATA_CLIENT,
DATA_UV,
DOMAIN,
TYPE_CURRENT_OZONE_LEVEL,
@@ -19,11 +18,8 @@ from . import (
TYPE_SAFE_EXPOSURE_TIME_4,
TYPE_SAFE_EXPOSURE_TIME_5,
TYPE_SAFE_EXPOSURE_TIME_6,
- OpenUvEntity,
)
-_LOGGER = logging.getLogger(__name__)
-
ATTR_MAX_UV_TIME = "time"
EXPOSURE_TYPE_MAP = {
@@ -80,8 +76,8 @@ SENSORS = {
async def async_setup_entry(hass, entry, async_add_entities):
- """Set up a Nest sensor based on a config entry."""
- openuv = hass.data[DOMAIN][DATA_OPENUV_CLIENT][entry.entry_id]
+ """Set up a OpenUV sensor based on a config entry."""
+ openuv = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id]
sensors = []
for kind, attrs in SENSORS.items():
diff --git a/homeassistant/components/openuv/translations/sl.json b/homeassistant/components/openuv/translations/sl.json
index cbd73cc9ddc..8a07b7a4a20 100644
--- a/homeassistant/components/openuv/translations/sl.json
+++ b/homeassistant/components/openuv/translations/sl.json
@@ -1,5 +1,8 @@
{
"config": {
+ "abort": {
+ "already_configured": "Lokacija je \u017ee nastavljena"
+ },
"error": {
"invalid_api_key": "Neveljaven API klju\u010d"
},
diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py
index 2c23c618c75..2eb23e23861 100644
--- a/homeassistant/components/openweathermap/const.py
+++ b/homeassistant/components/openweathermap/const.py
@@ -1,5 +1,19 @@
"""Consts for the OpenWeatherMap."""
from homeassistant.components.weather import (
+ ATTR_CONDITION_CLOUDY,
+ ATTR_CONDITION_EXCEPTIONAL,
+ ATTR_CONDITION_FOG,
+ ATTR_CONDITION_HAIL,
+ ATTR_CONDITION_LIGHTNING,
+ ATTR_CONDITION_LIGHTNING_RAINY,
+ ATTR_CONDITION_PARTLYCLOUDY,
+ ATTR_CONDITION_POURING,
+ ATTR_CONDITION_RAINY,
+ ATTR_CONDITION_SNOWY,
+ ATTR_CONDITION_SNOWY_RAINY,
+ ATTR_CONDITION_SUNNY,
+ ATTR_CONDITION_WINDY,
+ ATTR_CONDITION_WINDY_VARIANT,
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_PRECIPITATION,
ATTR_FORECAST_TEMP,
@@ -135,21 +149,35 @@ LANGUAGES = [
"zh_tw",
"zu",
]
+WEATHER_CODE_SUNNY_OR_CLEAR_NIGHT = 800
CONDITION_CLASSES = {
- "cloudy": [803, 804],
- "fog": [701, 741],
- "hail": [906],
- "lightning": [210, 211, 212, 221],
- "lightning-rainy": [200, 201, 202, 230, 231, 232],
- "partlycloudy": [801, 802],
- "pouring": [504, 314, 502, 503, 522],
- "rainy": [300, 301, 302, 310, 311, 312, 313, 500, 501, 520, 521],
- "snowy": [600, 601, 602, 611, 612, 620, 621, 622],
- "snowy-rainy": [511, 615, 616],
- "sunny": [800],
- "windy": [905, 951, 952, 953, 954, 955, 956, 957],
- "windy-variant": [958, 959, 960, 961],
- "exceptional": [711, 721, 731, 751, 761, 762, 771, 900, 901, 962, 903, 904],
+ ATTR_CONDITION_CLOUDY: [803, 804],
+ ATTR_CONDITION_FOG: [701, 741],
+ ATTR_CONDITION_HAIL: [906],
+ ATTR_CONDITION_LIGHTNING: [210, 211, 212, 221],
+ ATTR_CONDITION_LIGHTNING_RAINY: [200, 201, 202, 230, 231, 232],
+ ATTR_CONDITION_PARTLYCLOUDY: [801, 802],
+ ATTR_CONDITION_POURING: [504, 314, 502, 503, 522],
+ ATTR_CONDITION_RAINY: [300, 301, 302, 310, 311, 312, 313, 500, 501, 520, 521],
+ ATTR_CONDITION_SNOWY: [600, 601, 602, 611, 612, 620, 621, 622],
+ ATTR_CONDITION_SNOWY_RAINY: [511, 615, 616],
+ ATTR_CONDITION_SUNNY: [WEATHER_CODE_SUNNY_OR_CLEAR_NIGHT],
+ ATTR_CONDITION_WINDY: [905, 951, 952, 953, 954, 955, 956, 957],
+ ATTR_CONDITION_WINDY_VARIANT: [958, 959, 960, 961],
+ ATTR_CONDITION_EXCEPTIONAL: [
+ 711,
+ 721,
+ 731,
+ 751,
+ 761,
+ 762,
+ 771,
+ 900,
+ 901,
+ 962,
+ 903,
+ 904,
+ ],
}
WEATHER_SENSOR_TYPES = {
ATTR_API_WEATHER: {SENSOR_NAME: "Weather"},
diff --git a/homeassistant/components/openweathermap/translations/sl.json b/homeassistant/components/openweathermap/translations/sl.json
new file mode 100644
index 00000000000..76fcbca199d
--- /dev/null
+++ b/homeassistant/components/openweathermap/translations/sl.json
@@ -0,0 +1,20 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "mode": "Na\u010din"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "mode": "Na\u010din"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/openweathermap/weather_update_coordinator.py b/homeassistant/components/openweathermap/weather_update_coordinator.py
index 40dddc2e90d..b4ddb40c046 100644
--- a/homeassistant/components/openweathermap/weather_update_coordinator.py
+++ b/homeassistant/components/openweathermap/weather_update_coordinator.py
@@ -6,6 +6,8 @@ import async_timeout
from pyowm.commons.exceptions import APIRequestError, UnauthorizedError
from homeassistant.components.weather import (
+ ATTR_CONDITION_CLEAR_NIGHT,
+ ATTR_CONDITION_SUNNY,
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_PRECIPITATION,
ATTR_FORECAST_TEMP,
@@ -14,7 +16,9 @@ from homeassistant.components.weather import (
ATTR_FORECAST_WIND_BEARING,
ATTR_FORECAST_WIND_SPEED,
)
+from homeassistant.helpers import sun
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
+from homeassistant.util import dt
from .const import (
ATTR_API_CLOUDS,
@@ -35,6 +39,7 @@ from .const import (
FORECAST_MODE_HOURLY,
FORECAST_MODE_ONECALL_DAILY,
FORECAST_MODE_ONECALL_HOURLY,
+ WEATHER_CODE_SUNNY_OR_CLEAR_NIGHT,
)
_LOGGER = logging.getLogger(__name__)
@@ -139,7 +144,9 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
),
ATTR_FORECAST_WIND_SPEED: entry.wind().get("speed"),
ATTR_FORECAST_WIND_BEARING: entry.wind().get("deg"),
- ATTR_FORECAST_CONDITION: self._get_condition(entry.weather_code),
+ ATTR_FORECAST_CONDITION: self._get_condition(
+ entry.weather_code, entry.reference_time("unix")
+ ),
}
temperature_dict = entry.temperature("celsius")
@@ -186,9 +193,17 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
return None
return round(rain_value + snow_value, 1)
- @staticmethod
- def _get_condition(weather_code):
+ def _get_condition(self, weather_code, timestamp=None):
"""Get weather condition from weather data."""
+ if weather_code == WEATHER_CODE_SUNNY_OR_CLEAR_NIGHT:
+
+ if timestamp:
+ timestamp = dt.utc_from_timestamp(timestamp)
+
+ if sun.is_up(self.hass, timestamp):
+ return ATTR_CONDITION_SUNNY
+ return ATTR_CONDITION_CLEAR_NIGHT
+
return [k for k, v in CONDITION_CLASSES.items() if weather_code in v][0]
diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py
index 445ae733ec5..0130ba30c30 100644
--- a/homeassistant/components/ovo_energy/__init__.py
+++ b/homeassistant/components/ovo_energy/__init__.py
@@ -15,6 +15,7 @@ from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
+ UpdateFailed,
)
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
@@ -33,23 +34,38 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
client = OVOEnergy()
try:
- await client.authenticate(entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD])
+ authenticated = await client.authenticate(
+ entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD]
+ )
except aiohttp.ClientError as exception:
_LOGGER.warning(exception)
raise ConfigEntryNotReady from exception
+ if not authenticated:
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "reauth"}, data=entry.data
+ )
+ )
+ return False
+
async def async_update_data() -> OVODailyUsage:
"""Fetch data from OVO Energy."""
- now = datetime.utcnow()
async with async_timeout.timeout(10):
try:
- await client.authenticate(
+ authenticated = await client.authenticate(
entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD]
)
- return await client.get_daily_usage(now.strftime("%Y-%m"))
except aiohttp.ClientError as exception:
- _LOGGER.warning(exception)
- return None
+ raise UpdateFailed(exception) from exception
+ if not authenticated:
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "reauth"}, data=entry.data
+ )
+ )
+ raise UpdateFailed("Not authenticated with OVO Energy")
+ return await client.get_daily_usage(datetime.utcnow().strftime("%Y-%m"))
coordinator = DataUpdateCoordinator(
hass,
@@ -137,6 +153,6 @@ class OVOEnergyDeviceEntity(OVOEnergyEntity):
return {
"identifiers": {(DOMAIN, self._client.account_id)},
"manufacturer": "OVO Energy",
- "name": self._client.account_id,
+ "name": self._client.username,
"entry_type": "service",
}
diff --git a/homeassistant/components/ovo_energy/config_flow.py b/homeassistant/components/ovo_energy/config_flow.py
index dfedf780592..f395415d89e 100644
--- a/homeassistant/components/ovo_energy/config_flow.py
+++ b/homeassistant/components/ovo_energy/config_flow.py
@@ -7,8 +7,9 @@ from homeassistant import config_entries
from homeassistant.config_entries import ConfigFlow
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
-from .const import CONF_ACCOUNT_ID, DOMAIN # pylint: disable=unused-import
+from .const import DOMAIN # pylint: disable=unused-import
+REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str})
USER_SCHEMA = vol.Schema(
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
)
@@ -20,6 +21,10 @@ class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN):
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+ def __init__(self):
+ """Initialize the flow."""
+ self.username = None
+
async def async_step_user(self, user_input=None):
"""Handle a flow initiated by the user."""
errors = {}
@@ -37,11 +42,10 @@ class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN):
self._abort_if_unique_id_configured()
return self.async_create_entry(
- title=client.account_id,
+ title=client.username,
data={
CONF_USERNAME: user_input[CONF_USERNAME],
CONF_PASSWORD: user_input[CONF_PASSWORD],
- CONF_ACCOUNT_ID: client.account_id,
},
)
@@ -50,3 +54,42 @@ class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN):
return self.async_show_form(
step_id="user", data_schema=USER_SCHEMA, errors=errors
)
+
+ async def async_step_reauth(self, user_input):
+ """Handle configuration by re-auth."""
+ errors = {}
+
+ if user_input and user_input.get(CONF_USERNAME):
+ self.username = user_input[CONF_USERNAME]
+
+ # pylint: disable=no-member
+ self.context["title_placeholders"] = {CONF_USERNAME: self.username}
+
+ if user_input is not None and user_input.get(CONF_PASSWORD) is not None:
+ client = OVOEnergy()
+ try:
+ authenticated = await client.authenticate(
+ self.username, user_input[CONF_PASSWORD]
+ )
+ except aiohttp.ClientError:
+ errors["base"] = "connection_error"
+ else:
+ if authenticated:
+ await self.async_set_unique_id(self.username)
+
+ for entry in self._async_current_entries():
+ if entry.unique_id == self.unique_id:
+ self.hass.config_entries.async_update_entry(
+ entry,
+ data={
+ CONF_USERNAME: self.username,
+ CONF_PASSWORD: user_input[CONF_PASSWORD],
+ },
+ )
+ return self.async_abort(reason="reauth_successful")
+
+ errors["base"] = "authorization_error"
+
+ return self.async_show_form(
+ step_id="reauth", data_schema=REAUTH_SCHEMA, errors=errors
+ )
diff --git a/homeassistant/components/ovo_energy/const.py b/homeassistant/components/ovo_energy/const.py
index e836bb2ca8a..f691eb9bc49 100644
--- a/homeassistant/components/ovo_energy/const.py
+++ b/homeassistant/components/ovo_energy/const.py
@@ -3,5 +3,3 @@ DOMAIN = "ovo_energy"
DATA_CLIENT = "ovo_client"
DATA_COORDINATOR = "coordinator"
-
-CONF_ACCOUNT_ID = "account_id"
diff --git a/homeassistant/components/ovo_energy/manifest.json b/homeassistant/components/ovo_energy/manifest.json
index ba9579279a9..6ec03eb19a5 100644
--- a/homeassistant/components/ovo_energy/manifest.json
+++ b/homeassistant/components/ovo_energy/manifest.json
@@ -3,6 +3,6 @@
"name": "OVO Energy",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ovo_energy",
- "requirements": ["ovoenergy==1.1.7"],
+ "requirements": ["ovoenergy==1.1.11"],
"codeowners": ["@timmo001"]
}
diff --git a/homeassistant/components/ovo_energy/sensor.py b/homeassistant/components/ovo_energy/sensor.py
index 2a64fbe2d22..2f2e1b8dd50 100644
--- a/homeassistant/components/ovo_energy/sensor.py
+++ b/homeassistant/components/ovo_energy/sensor.py
@@ -50,10 +50,7 @@ async def async_setup_entry(
)
)
- async_add_entities(
- entities,
- True,
- )
+ async_add_entities(entities, True)
class OVOEnergySensor(OVOEnergyDeviceEntity):
diff --git a/homeassistant/components/ovo_energy/strings.json b/homeassistant/components/ovo_energy/strings.json
index fac7c97bcbe..df19d4898f2 100644
--- a/homeassistant/components/ovo_energy/strings.json
+++ b/homeassistant/components/ovo_energy/strings.json
@@ -1,5 +1,6 @@
{
"config": {
+ "flow_title": "OVO Energy: {username}",
"error": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
@@ -13,7 +14,14 @@
},
"description": "Set up an OVO Energy instance to access your energy usage.",
"title": "Add OVO Energy Account"
+ },
+ "reauth": {
+ "data": {
+ "password": "[%key:common::config_flow::data::password%]"
+ },
+ "description": "Authentication failed for OVO Energy. Please enter your current credentials.",
+ "title": "Reauthentication"
+ }
}
- }
}
}
diff --git a/homeassistant/components/ovo_energy/translations/ca.json b/homeassistant/components/ovo_energy/translations/ca.json
index 4f8525bfd85..3cc971434a4 100644
--- a/homeassistant/components/ovo_energy/translations/ca.json
+++ b/homeassistant/components/ovo_energy/translations/ca.json
@@ -5,7 +5,15 @@
"cannot_connect": "Ha fallat la connexi\u00f3",
"invalid_auth": "Autenticaci\u00f3 inv\u00e0lida"
},
+ "flow_title": "OVO Energy: {username}",
"step": {
+ "reauth": {
+ "data": {
+ "password": "Contrasenya"
+ },
+ "description": "L'autenticaci\u00f3 d'OVO Energy ha fallat. Introdueix les teves credencials actuals.",
+ "title": "Reautenticaci\u00f3"
+ },
"user": {
"data": {
"password": "Contrasenya",
diff --git a/homeassistant/components/ovo_energy/translations/cs.json b/homeassistant/components/ovo_energy/translations/cs.json
index d8276248ad4..34dd05dc8bf 100644
--- a/homeassistant/components/ovo_energy/translations/cs.json
+++ b/homeassistant/components/ovo_energy/translations/cs.json
@@ -5,7 +5,15 @@
"cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit",
"invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed"
},
+ "flow_title": "OVO Energy: {username}",
"step": {
+ "reauth": {
+ "data": {
+ "password": "Heslo"
+ },
+ "description": "Ov\u011b\u0159en\u00ed pro OVO Energy se nezda\u0159ilo. Zadejte sv\u00e9 aktu\u00e1ln\u00ed p\u0159ihla\u0161ovac\u00ed \u00fadaje.",
+ "title": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed"
+ },
"user": {
"data": {
"password": "Heslo",
diff --git a/homeassistant/components/ovo_energy/translations/en.json b/homeassistant/components/ovo_energy/translations/en.json
index 160f47ae23f..7b3160af97e 100644
--- a/homeassistant/components/ovo_energy/translations/en.json
+++ b/homeassistant/components/ovo_energy/translations/en.json
@@ -5,7 +5,15 @@
"cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication"
},
+ "flow_title": "OVO Energy: {username}",
"step": {
+ "reauth": {
+ "data": {
+ "password": "Password"
+ },
+ "description": "Authentication failed for OVO Energy. Please enter your current credentials.",
+ "title": "Reauthentication"
+ },
"user": {
"data": {
"password": "Password",
diff --git a/homeassistant/components/ovo_energy/translations/es.json b/homeassistant/components/ovo_energy/translations/es.json
index 43f2d7e4541..aa9708c60d0 100644
--- a/homeassistant/components/ovo_energy/translations/es.json
+++ b/homeassistant/components/ovo_energy/translations/es.json
@@ -5,7 +5,15 @@
"cannot_connect": "No se pudo conectar",
"invalid_auth": "Autenticaci\u00f3n no v\u00e1lida"
},
+ "flow_title": "OVO Energy: {username}",
"step": {
+ "reauth": {
+ "data": {
+ "password": "Contrase\u00f1a"
+ },
+ "description": "Error de autenticaci\u00f3n para OVO Energy. Ingrese sus credenciales actuales.",
+ "title": "Reautenticaci\u00f3n"
+ },
"user": {
"data": {
"password": "Contrase\u00f1a",
diff --git a/homeassistant/components/ovo_energy/translations/et.json b/homeassistant/components/ovo_energy/translations/et.json
index bc3df1eaf44..b91f360159a 100644
--- a/homeassistant/components/ovo_energy/translations/et.json
+++ b/homeassistant/components/ovo_energy/translations/et.json
@@ -5,7 +5,15 @@
"cannot_connect": "\u00dchendus nurjus",
"invalid_auth": "Tuvastamise viga"
},
+ "flow_title": "OVO Energy: {username}",
"step": {
+ "reauth": {
+ "data": {
+ "password": "Salas\u00f5na"
+ },
+ "description": "OVO Energy autentimine nurjus. Sisesta oma praegused volitused.",
+ "title": "Taastuvastamine"
+ },
"user": {
"data": {
"password": "Salas\u00f5na",
diff --git a/homeassistant/components/ovo_energy/translations/hu.json b/homeassistant/components/ovo_energy/translations/hu.json
index f5481afa94a..c4b70e90076 100644
--- a/homeassistant/components/ovo_energy/translations/hu.json
+++ b/homeassistant/components/ovo_energy/translations/hu.json
@@ -1,7 +1,8 @@
{
"config": {
"error": {
- "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van"
+ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/ovo_energy/translations/it.json b/homeassistant/components/ovo_energy/translations/it.json
index f0619e7d593..9f4c2c68935 100644
--- a/homeassistant/components/ovo_energy/translations/it.json
+++ b/homeassistant/components/ovo_energy/translations/it.json
@@ -5,7 +5,15 @@
"cannot_connect": "Impossibile connettersi",
"invalid_auth": "Autenticazione non valida"
},
+ "flow_title": "OVO Energy: {username}",
"step": {
+ "reauth": {
+ "data": {
+ "password": "Password"
+ },
+ "description": "Autenticazione non riuscita per OVO Energy. Immettere le credenziali correnti.",
+ "title": "Riautenticazione"
+ },
"user": {
"data": {
"password": "Password",
diff --git a/homeassistant/components/ovo_energy/translations/ka.json b/homeassistant/components/ovo_energy/translations/ka.json
new file mode 100644
index 00000000000..1da3b36b1ec
--- /dev/null
+++ b/homeassistant/components/ovo_energy/translations/ka.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u10db\u10d0\u10e0\u10ea\u10ee\u10d8 \u10e8\u10d4\u10d4\u10e0\u10d7\u10d4\u10d1\u10d8\u10e1\u10d0\u10e1",
+ "invalid_auth": "\u10db\u10ea\u10d3\u10d0\u10e0\u10d8 \u10d0\u10e3\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0"
+ },
+ "flow_title": "OVO Energy: {username}",
+ "step": {
+ "reauth": {
+ "data": {
+ "password": "\u10de\u10d0\u10e0\u10dd\u10da\u10d8"
+ },
+ "description": "\u10d0\u10d5\u10e2\u10dd\u10e0\u10d8\u10d6\u10d0\u10ea\u10d8\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0 OVOEnergy-\u10e1\u10d7\u10d5\u10d8\u10e1. \u10d2\u10d7\u10ee\u10dd\u10d5\u10d7, \u10e8\u10d4\u10d8\u10e7\u10d5\u10d0\u10dc\u10dd\u10d7 \u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 \u10db\u10d8\u10db\u10d3\u10d8\u10dc\u10d0\u10e0\u10d4 \u10e1\u10d4\u10e0\u10d7\u10d8\u10e4\u10d8\u10d9\u10d0\u10e2\u10d4\u10d1\u10d8.",
+ "title": "\u10ee\u10d4\u10da\u10d0\u10ee\u10d0\u10da\u10d8 \u10d0\u10e3\u10d7\u10d4\u10dc\u10d7\u10d8\u10e4\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ovo_energy/translations/no.json b/homeassistant/components/ovo_energy/translations/no.json
index 72b69e43b9b..5e0537b6633 100644
--- a/homeassistant/components/ovo_energy/translations/no.json
+++ b/homeassistant/components/ovo_energy/translations/no.json
@@ -5,7 +5,15 @@
"cannot_connect": "Tilkobling mislyktes",
"invalid_auth": "Ugyldig godkjenning"
},
+ "flow_title": "OVO Energy: {username}",
"step": {
+ "reauth": {
+ "data": {
+ "password": "Passord"
+ },
+ "description": "Autentisering mislyktes for OVO Energy. Vennligst skriv inn din n\u00e5v\u00e6rende legitimasjon.",
+ "title": "Reautorisasjon"
+ },
"user": {
"data": {
"password": "Passord",
diff --git a/homeassistant/components/ovo_energy/translations/pl.json b/homeassistant/components/ovo_energy/translations/pl.json
index 541df85dc28..5767f3f7cf2 100644
--- a/homeassistant/components/ovo_energy/translations/pl.json
+++ b/homeassistant/components/ovo_energy/translations/pl.json
@@ -5,7 +5,15 @@
"cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"invalid_auth": "Niepoprawne uwierzytelnienie"
},
+ "flow_title": "OVO Energy: {username}",
"step": {
+ "reauth": {
+ "data": {
+ "password": "Has\u0142o"
+ },
+ "description": "Uwierzytelnianie dla OVO Energy nie powiod\u0142o si\u0119. Wprowad\u017a aktualne dane uwierzytelniaj\u0105ce.",
+ "title": "Ponowne uwierzytelnianie"
+ },
"user": {
"data": {
"password": "Has\u0142o",
diff --git a/homeassistant/components/ovo_energy/translations/ru.json b/homeassistant/components/ovo_energy/translations/ru.json
index 08f2ce89b0d..dd422bac01f 100644
--- a/homeassistant/components/ovo_energy/translations/ru.json
+++ b/homeassistant/components/ovo_energy/translations/ru.json
@@ -5,7 +5,15 @@
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
"invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f."
},
+ "flow_title": "OVO Energy: {username}",
"step": {
+ "reauth": {
+ "data": {
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c"
+ },
+ "description": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0412\u0430\u0448\u0438 \u0442\u0435\u043a\u0443\u0449\u0438\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.",
+ "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f"
+ },
"user": {
"data": {
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
diff --git a/homeassistant/components/ovo_energy/translations/zh-Hant.json b/homeassistant/components/ovo_energy/translations/zh-Hant.json
index a6ba2f8b624..f557a83009c 100644
--- a/homeassistant/components/ovo_energy/translations/zh-Hant.json
+++ b/homeassistant/components/ovo_energy/translations/zh-Hant.json
@@ -5,7 +5,15 @@
"cannot_connect": "\u9023\u7dda\u5931\u6557",
"invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548"
},
+ "flow_title": "OVO Energy\uff1a{username}",
"step": {
+ "reauth": {
+ "data": {
+ "password": "\u5bc6\u78bc"
+ },
+ "description": "OVO Energy \u8a8d\u8b49\u5931\u6557\u3002\u8acb\u8f38\u5165\u76ee\u524d\u7684\u6191\u8b49\u3002",
+ "title": "\u91cd\u65b0\u8a8d\u8b49"
+ },
"user": {
"data": {
"password": "\u5bc6\u78bc",
diff --git a/homeassistant/components/owntracks/translations/ka.json b/homeassistant/components/owntracks/translations/ka.json
new file mode 100644
index 00000000000..503f471efb3
--- /dev/null
+++ b/homeassistant/components/owntracks/translations/ka.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0. \u10e8\u10d4\u10e1\u10d0\u10eb\u10da\u10d4\u10d1\u10d4\u10da\u10d8\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ozw/__init__.py b/homeassistant/components/ozw/__init__.py
index 5706b75efb8..1f46e7a17c6 100644
--- a/homeassistant/components/ozw/__init__.py
+++ b/homeassistant/components/ozw/__init__.py
@@ -17,22 +17,25 @@ from openzwavemqtt.const import (
)
from openzwavemqtt.models.node import OZWNode
from openzwavemqtt.models.value import OZWValue
+from openzwavemqtt.util.mqtt_client import MQTTClient
import voluptuous as vol
from homeassistant.components import mqtt
from homeassistant.components.hassio.handler import HassioAPIError
from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant, callback
+from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg
from homeassistant.helpers.dispatcher import async_dispatcher_send
from . import const
from .const import (
CONF_INTEGRATION_CREATED_ADDON,
+ CONF_USE_ADDON,
DATA_UNSUBSCRIBE,
DOMAIN,
MANAGER,
- OPTIONS,
PLATFORMS,
TOPIC_OPENZWAVE,
)
@@ -50,13 +53,11 @@ _LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA)
DATA_DEVICES = "zwave-mqtt-devices"
+DATA_STOP_MQTT_CLIENT = "ozw_stop_mqtt_client"
async def async_setup(hass: HomeAssistant, config: dict):
"""Initialize basic config of ozw component."""
- if "mqtt" not in hass.config.components:
- _LOGGER.error("MQTT integration is not set up")
- return False
hass.data[DOMAIN] = {}
return True
@@ -69,16 +70,46 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
data_nodes = {}
data_values = {}
removed_nodes = []
+ manager_options = {"topic_prefix": f"{TOPIC_OPENZWAVE}/"}
- @callback
- def send_message(topic, payload):
- mqtt.async_publish(hass, topic, json.dumps(payload))
+ if entry.unique_id is None:
+ hass.config_entries.async_update_entry(entry, unique_id=DOMAIN)
- options = OZWOptions(send_message=send_message, topic_prefix=f"{TOPIC_OPENZWAVE}/")
+ if entry.data.get(CONF_USE_ADDON):
+ # Do not use MQTT integration. Use own MQTT client.
+ # Retrieve discovery info from the OpenZWave add-on.
+ discovery_info = await hass.components.hassio.async_get_addon_discovery_info(
+ "core_zwave"
+ )
+
+ if not discovery_info:
+ _LOGGER.error("Failed to get add-on discovery info")
+ raise ConfigEntryNotReady
+
+ discovery_info_config = discovery_info["config"]
+
+ host = discovery_info_config["host"]
+ port = discovery_info_config["port"]
+ username = discovery_info_config["username"]
+ password = discovery_info_config["password"]
+ mqtt_client = MQTTClient(host, port, username=username, password=password)
+ manager_options["send_message"] = mqtt_client.send_message
+
+ else:
+ if "mqtt" not in hass.config.components:
+ _LOGGER.error("MQTT integration is not set up")
+ return False
+
+ @callback
+ def send_message(topic, payload):
+ mqtt.async_publish(hass, topic, json.dumps(payload))
+
+ manager_options["send_message"] = send_message
+
+ options = OZWOptions(**manager_options)
manager = OZWManager(options)
hass.data[DOMAIN][MANAGER] = manager
- hass.data[DOMAIN][OPTIONS] = options
@callback
def async_node_added(node):
@@ -234,11 +265,33 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
for component in PLATFORMS
]
)
- ozw_data[DATA_UNSUBSCRIBE].append(
- await mqtt.async_subscribe(
- hass, f"{TOPIC_OPENZWAVE}/#", async_receive_message
+ if entry.data.get(CONF_USE_ADDON):
+ mqtt_client_task = asyncio.create_task(mqtt_client.start_client(manager))
+
+ async def async_stop_mqtt_client(event=None):
+ """Stop the mqtt client.
+
+ Do not unsubscribe the manager topic.
+ """
+ mqtt_client_task.cancel()
+ try:
+ await mqtt_client_task
+ except asyncio.CancelledError:
+ pass
+
+ ozw_data[DATA_UNSUBSCRIBE].append(
+ hass.bus.async_listen_once(
+ EVENT_HOMEASSISTANT_STOP, async_stop_mqtt_client
+ )
+ )
+ ozw_data[DATA_STOP_MQTT_CLIENT] = async_stop_mqtt_client
+
+ else:
+ ozw_data[DATA_UNSUBSCRIBE].append(
+ await mqtt.async_subscribe(
+ hass, f"{manager.options.topic_prefix}#", async_receive_message
+ )
)
- )
hass.async_create_task(start_platforms())
@@ -262,6 +315,13 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
# unsubscribe all listeners
for unsubscribe_listener in hass.data[DOMAIN][entry.entry_id][DATA_UNSUBSCRIBE]:
unsubscribe_listener()
+
+ if entry.data.get(CONF_USE_ADDON):
+ async_stop_mqtt_client = hass.data[DOMAIN][entry.entry_id][
+ DATA_STOP_MQTT_CLIENT
+ ]
+ await async_stop_mqtt_client()
+
hass.data[DOMAIN].pop(entry.entry_id)
return True
@@ -332,6 +392,7 @@ async def async_handle_node_update(hass: HomeAssistant, node: OZWNode):
def async_handle_scene_activated(hass: HomeAssistant, scene_value: OZWValue):
"""Handle a (central) scene activation message."""
node_id = scene_value.node.id
+ ozw_instance_id = scene_value.ozw_instance.id
scene_id = scene_value.index
scene_label = scene_value.label
if scene_value.command_class == CommandClass.SCENE_ACTIVATION:
@@ -346,7 +407,8 @@ def async_handle_scene_activated(hass: HomeAssistant, scene_value: OZWValue):
scene_value_id = scene_value.value["Selected_id"]
_LOGGER.debug(
- "[SCENE_ACTIVATED] node_id: %s - scene_id: %s - scene_value_id: %s",
+ "[SCENE_ACTIVATED] ozw_instance: %s - node_id: %s - scene_id: %s - scene_value_id: %s",
+ ozw_instance_id,
node_id,
scene_id,
scene_value_id,
@@ -355,6 +417,7 @@ def async_handle_scene_activated(hass: HomeAssistant, scene_value: OZWValue):
hass.bus.async_fire(
const.EVENT_SCENE_ACTIVATED,
{
+ const.ATTR_INSTANCE_ID: ozw_instance_id,
const.ATTR_NODE_ID: node_id,
const.ATTR_SCENE_ID: scene_id,
const.ATTR_SCENE_LABEL: scene_label,
diff --git a/homeassistant/components/ozw/config_flow.py b/homeassistant/components/ozw/config_flow.py
index 1c0ccdefa70..7c7c6e65dfe 100644
--- a/homeassistant/components/ozw/config_flow.py
+++ b/homeassistant/components/ozw/config_flow.py
@@ -7,7 +7,7 @@ from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.data_entry_flow import AbortFlow
-from .const import CONF_INTEGRATION_CREATED_ADDON
+from .const import CONF_INTEGRATION_CREATED_ADDON, CONF_USE_ADDON
from .const import DOMAIN # pylint:disable=unused-import
_LOGGER = logging.getLogger(__name__)
@@ -16,7 +16,6 @@ CONF_ADDON_DEVICE = "device"
CONF_ADDON_NETWORK_KEY = "network_key"
CONF_NETWORK_KEY = "network_key"
CONF_USB_PATH = "usb_path"
-CONF_USE_ADDON = "use_addon"
TITLE = "OpenZWave"
ON_SUPERVISOR_SCHEMA = vol.Schema({vol.Optional(CONF_USE_ADDON, default=False): bool})
@@ -36,23 +35,40 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self.use_addon = False
# If we install the add-on we should uninstall it on entry remove.
self.integration_created_addon = False
+ self.install_task = None
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
- # Currently all flow results need the MQTT integration.
- # This will change when we have the direct MQTT client connection.
- # When that is implemented, move this check to _async_use_mqtt_integration.
- if "mqtt" not in self.hass.config.components:
- return self.async_abort(reason="mqtt_required")
+ # Set a unique_id to make sure discovery flow is aborted on progress.
+ await self.async_set_unique_id(DOMAIN, raise_on_progress=False)
if not self.hass.components.hassio.is_hassio():
return self._async_use_mqtt_integration()
return await self.async_step_on_supervisor()
+ async def async_step_hassio(self, discovery_info):
+ """Receive configuration from add-on discovery info.
+
+ This flow is triggered by the OpenZWave add-on.
+ """
+ await self.async_set_unique_id(DOMAIN)
+ self._abort_if_unique_id_configured()
+
+ return await self.async_step_hassio_confirm()
+
+ async def async_step_hassio_confirm(self, user_input=None):
+ """Confirm the add-on discovery."""
+ if user_input is not None:
+ return await self.async_step_on_supervisor(
+ user_input={CONF_USE_ADDON: True}
+ )
+
+ return self.async_show_form(step_id="hassio_confirm")
+
def _async_create_entry_from_vars(self):
"""Return a config entry for the flow."""
return self.async_create_entry(
@@ -72,6 +88,8 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
This is the entry point for the logic that is needed
when this integration will depend on the MQTT integration.
"""
+ if "mqtt" not in self.hass.config.components:
+ return self.async_abort(reason="mqtt_required")
return self._async_create_entry_from_vars()
async def async_step_on_supervisor(self, user_input=None):
@@ -86,6 +104,9 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self.use_addon = True
if await self._async_is_addon_running():
+ addon_config = await self._async_get_addon_config()
+ self.usb_path = addon_config[CONF_ADDON_DEVICE]
+ self.network_key = addon_config.get(CONF_ADDON_NETWORK_KEY, "")
return self._async_create_entry_from_vars()
if await self._async_is_addon_installed():
@@ -93,16 +114,27 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return await self.async_step_install_addon()
- async def async_step_install_addon(self):
+ async def async_step_install_addon(self, user_input=None):
"""Install OpenZWave add-on."""
+ if not self.install_task:
+ self.install_task = self.hass.async_create_task(self._async_install_addon())
+ return self.async_show_progress(
+ step_id="install_addon", progress_action="install_addon"
+ )
+
try:
- await self.hass.components.hassio.async_install_addon("core_zwave")
+ await self.install_task
except self.hass.components.hassio.HassioAPIError as err:
_LOGGER.error("Failed to install OpenZWave add-on: %s", err)
- return self.async_abort(reason="addon_install_failed")
+ return self.async_show_progress_done(next_step_id="install_failed")
+
self.integration_created_addon = True
- return await self.async_step_start_addon()
+ return self.async_show_progress_done(next_step_id="start_addon")
+
+ async def async_step_install_failed(self, user_input=None):
+ """Add-on installation failed."""
+ return self.async_abort(reason="addon_install_failed")
async def async_step_start_addon(self, user_input=None):
"""Ask for config and start OpenZWave add-on."""
@@ -115,9 +147,10 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self.network_key = user_input[CONF_NETWORK_KEY]
self.usb_path = user_input[CONF_USB_PATH]
- new_addon_config = {CONF_ADDON_DEVICE: self.usb_path}
- if self.network_key:
- new_addon_config[CONF_ADDON_NETWORK_KEY] = self.network_key
+ new_addon_config = {
+ CONF_ADDON_DEVICE: self.usb_path,
+ CONF_ADDON_NETWORK_KEY: self.network_key,
+ }
if new_addon_config != self.addon_config:
await self._async_set_addon_config(new_addon_config)
@@ -181,3 +214,13 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
except self.hass.components.hassio.HassioAPIError as err:
_LOGGER.error("Failed to set OpenZWave add-on config: %s", err)
raise AbortFlow("addon_set_config_failed") from err
+
+ async def _async_install_addon(self):
+ """Install the OpenZWave add-on."""
+ try:
+ await self.hass.components.hassio.async_install_addon("core_zwave")
+ finally:
+ # Continue the flow after show progress when the task is done.
+ self.hass.async_create_task(
+ self.hass.config_entries.flow.async_configure(flow_id=self.flow_id)
+ )
diff --git a/homeassistant/components/ozw/const.py b/homeassistant/components/ozw/const.py
index 160b251eeca..f8d5090aa84 100644
--- a/homeassistant/components/ozw/const.py
+++ b/homeassistant/components/ozw/const.py
@@ -12,6 +12,7 @@ DOMAIN = "ozw"
DATA_UNSUBSCRIBE = "unsubscribe"
CONF_INTEGRATION_CREATED_ADDON = "integration_created_addon"
+CONF_USE_ADDON = "use_addon"
PLATFORMS = [
BINARY_SENSOR_DOMAIN,
@@ -24,7 +25,6 @@ PLATFORMS = [
SWITCH_DOMAIN,
]
MANAGER = "manager"
-OPTIONS = "options"
# MQTT Topics
TOPIC_OPENZWAVE = "OpenZWave"
diff --git a/homeassistant/components/ozw/discovery.py b/homeassistant/components/ozw/discovery.py
index a83f763c810..67e3442cf5f 100644
--- a/homeassistant/components/ozw/discovery.py
+++ b/homeassistant/components/ozw/discovery.py
@@ -254,11 +254,13 @@ DISCOVERY_SCHEMAS = (
"min_kelvin": {
const.DISC_COMMAND_CLASS: (CommandClass.CONFIGURATION,),
const.DISC_INDEX: 81, # PR for upstream to add SWITCH_COLOR_CT_WARM
+ const.DISC_TYPE: ValueType.INT,
const.DISC_OPTIONAL: True,
},
"max_kelvin": {
const.DISC_COMMAND_CLASS: (CommandClass.CONFIGURATION,),
const.DISC_INDEX: 82, # PR for upstream to add SWITCH_COLOR_CT_COLD
+ const.DISC_TYPE: ValueType.INT,
const.DISC_OPTIONAL: True,
},
},
diff --git a/homeassistant/components/ozw/manifest.json b/homeassistant/components/ozw/manifest.json
index fa25f984076..a1409fd79a8 100644
--- a/homeassistant/components/ozw/manifest.json
+++ b/homeassistant/components/ozw/manifest.json
@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ozw",
"requirements": [
- "python-openzwave-mqtt==1.3.2"
+ "python-openzwave-mqtt[mqtt-client]==1.4.0"
],
"after_dependencies": [
"mqtt"
@@ -14,4 +14,4 @@
"@marcelveldt",
"@MartinHjelmare"
]
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/ozw/strings.json b/homeassistant/components/ozw/strings.json
index 52317b3d6a8..ed9816c57f2 100644
--- a/homeassistant/components/ozw/strings.json
+++ b/homeassistant/components/ozw/strings.json
@@ -4,14 +4,25 @@
"on_supervisor": {
"title": "Select connection method",
"description": "Do you want to use the OpenZWave Supervisor add-on?",
- "data": {"use_addon": "Use the OpenZWave Supervisor add-on"}
+ "data": { "use_addon": "Use the OpenZWave Supervisor add-on" }
+ },
+ "install_addon": {
+ "title": "The OpenZWave add-on installation has started"
},
"start_addon": {
"title": "Enter the OpenZWave add-on configuration",
- "data": {"usb_path": "[%key:common::config_flow::data::usb_path%]", "network_key": "Network Key"}
+ "data": {
+ "usb_path": "[%key:common::config_flow::data::usb_path%]",
+ "network_key": "Network Key"
+ }
+ },
+ "hassio_confirm": {
+ "title": "Set up OpenZWave integration with the OpenZWave add-on"
}
},
"abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"addon_info_failed": "Failed to get OpenZWave add-on info.",
"addon_install_failed": "Failed to install the OpenZWave add-on.",
"addon_set_config_failed": "Failed to set OpenZWave configuration.",
@@ -20,6 +31,9 @@
},
"error": {
"addon_start_failed": "Failed to start the OpenZWave add-on. Check the configuration."
+ },
+ "progress": {
+ "install_addon": "Please wait while the OpenZWave add-on installation finishes. This can take several minutes."
}
}
}
diff --git a/homeassistant/components/ozw/translations/en.json b/homeassistant/components/ozw/translations/en.json
index e028e1923ae..1c837e0bde5 100644
--- a/homeassistant/components/ozw/translations/en.json
+++ b/homeassistant/components/ozw/translations/en.json
@@ -4,13 +4,24 @@
"addon_info_failed": "Failed to get OpenZWave add-on info.",
"addon_install_failed": "Failed to install the OpenZWave add-on.",
"addon_set_config_failed": "Failed to set OpenZWave configuration.",
+ "already_configured": "Device is already configured",
+ "already_in_progress": "Configuration flow is already in progress",
"mqtt_required": "The MQTT integration is not set up",
"single_instance_allowed": "Already configured. Only a single configuration possible."
},
"error": {
"addon_start_failed": "Failed to start the OpenZWave add-on. Check the configuration."
},
+ "progress": {
+ "install_addon": "Please wait while the OpenZWave add-on installation finishes. This can take several minutes."
+ },
"step": {
+ "hassio_confirm": {
+ "title": "Set up OpenZWave integration with the OpenZWave add-on"
+ },
+ "install_addon": {
+ "title": "The OpenZWave add-on installation has started"
+ },
"on_supervisor": {
"data": {
"use_addon": "Use the OpenZWave Supervisor add-on"
diff --git a/homeassistant/components/ozw/translations/es.json b/homeassistant/components/ozw/translations/es.json
index 8d39e611601..60d64f9afbf 100644
--- a/homeassistant/components/ozw/translations/es.json
+++ b/homeassistant/components/ozw/translations/es.json
@@ -10,7 +10,13 @@
"error": {
"addon_start_failed": "No se pudo iniciar el complemento OpenZWave. Verifica la configuraci\u00f3n."
},
+ "progress": {
+ "install_addon": "Espera mientras finaliza la instalaci\u00f3n del complemento OpenZWave. Esto puede tardar varios minutos."
+ },
"step": {
+ "install_addon": {
+ "title": "La instalaci\u00f3n del complemento OpenZWave se ha iniciado"
+ },
"on_supervisor": {
"data": {
"use_addon": "Usar el complemento de supervisor de OpenZWave"
diff --git a/homeassistant/components/ozw/translations/et.json b/homeassistant/components/ozw/translations/et.json
index 180e2a51542..6ddd2e7ab96 100644
--- a/homeassistant/components/ozw/translations/et.json
+++ b/homeassistant/components/ozw/translations/et.json
@@ -4,13 +4,24 @@
"addon_info_failed": "OpenZWave'i lisandmooduli teabe hankimine nurjus.",
"addon_install_failed": "OpenZWave'i lisandmooduli paigaldamine nurjus.",
"addon_set_config_failed": "OpenZWave'i konfiguratsiooni seadistamine eba\u00f5nnestus.",
+ "already_configured": "Seade on juba h\u00e4\u00e4lestatud",
+ "already_in_progress": "Seadistamine on juba k\u00e4imas",
"mqtt_required": "MQTT sidumine pole seadistatud",
"single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine."
},
"error": {
"addon_start_failed": "OpenZWave'i lisandmooduli k\u00e4ivitamine nurjus. Kontrolli s\u00e4tteid."
},
+ "progress": {
+ "install_addon": "Palun oota kuni OpenZWave lisandmooduli paigaldus l\u00f5peb. See v\u00f5ib v\u00f5tta mitu minutit."
+ },
"step": {
+ "hassio_confirm": {
+ "title": "Seadista OpenZWave'i sidumine OpenZWave lisandmooduli abil"
+ },
+ "install_addon": {
+ "title": "OpenZWave lisandmooduli paigaldamine on alanud"
+ },
"on_supervisor": {
"data": {
"use_addon": "Kasuta OpenZWave Supervisori lisandmoodulit"
diff --git a/homeassistant/components/ozw/translations/hu.json b/homeassistant/components/ozw/translations/hu.json
new file mode 100644
index 00000000000..9729938035d
--- /dev/null
+++ b/homeassistant/components/ozw/translations/hu.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "addon_info_failed": "Nem siker\u00fclt bet\u00f6lteni az OpenZWave kieg\u00e9sz\u00edt\u0151 inform\u00e1ci\u00f3kat.",
+ "addon_install_failed": "Nem siker\u00fclt telep\u00edteni az OpenZWave b\u0151v\u00edtm\u00e9nyt.",
+ "addon_set_config_failed": "Nem siker\u00fclt be\u00e1ll\u00edtani az OpenZWave konfigur\u00e1ci\u00f3t."
+ },
+ "error": {
+ "addon_start_failed": "Nem siker\u00fclt elind\u00edtani az OpenZWave b\u0151v\u00edtm\u00e9nyt. Ellen\u0151rizze a konfigur\u00e1ci\u00f3t."
+ },
+ "step": {
+ "on_supervisor": {
+ "data": {
+ "use_addon": "Haszn\u00e1lja az OpenZWave adminisztr\u00e1tori b\u0151v\u00edtm\u00e9nyt"
+ },
+ "description": "Szeretn\u00e9 haszn\u00e1lni az OpenZWave adminisztr\u00e1tori b\u0151v\u00edtm\u00e9nyt?",
+ "title": "V\u00e1lassza ki a csatlakoz\u00e1si m\u00f3dot"
+ },
+ "start_addon": {
+ "data": {
+ "network_key": "H\u00e1l\u00f3zati kulcs"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ozw/translations/ka.json b/homeassistant/components/ozw/translations/ka.json
new file mode 100644
index 00000000000..da587087c92
--- /dev/null
+++ b/homeassistant/components/ozw/translations/ka.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "addon_info_failed": "\u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0 OpenZWave \u10d3\u10d0\u10dc\u10d0\u10db\u10d0\u10e2\u10d8\u10e1 \u10d8\u10dc\u10e4\u10dd\u10e1 \u10db\u10d8\u10e6\u10d4\u10d1\u10d0.",
+ "addon_install_failed": "\u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0 OpenZWave \u10d3\u10d0\u10dc\u10d0\u10db\u10d0\u10e2\u10d8\u10e1 \u10d8\u10dc\u10e1\u10e2\u10d0\u10da\u10d8\u10e0\u10d4\u10d1\u10d0.",
+ "addon_set_config_failed": "\u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0 OpenZWave \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d8\u10e1 \u10d3\u10d0\u10e7\u10d4\u10dc\u10d4\u10d1\u10d0.",
+ "single_instance_allowed": "\u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0. \u10e8\u10d4\u10e1\u10d0\u10eb\u10da\u10d4\u10d1\u10d4\u10da\u10d8\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0."
+ },
+ "error": {
+ "addon_start_failed": "OpenZWave \u10d3\u10d0\u10dc\u10d0\u10db\u10d0\u10e2\u10d8 \u10d0\u10e0 \u10d3\u10d0\u10d8\u10e1\u10e2\u10d0\u10e0\u10e2\u10d0. \u10e8\u10d4\u10d0\u10db\u10dd\u10ec\u10db\u10d4\u10d7 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0."
+ },
+ "step": {
+ "on_supervisor": {
+ "data": {
+ "use_addon": "\u10d2\u10d0\u10db\u10dd\u10d8\u10e7\u10d4\u10dc\u10d4\u10d7 OpenZWave Supervisor \u10d3\u10d0\u10dc\u10d0\u10db\u10d0\u10e2\u10d8"
+ },
+ "description": "\u10d2\u10e1\u10e3\u10e0\u10d7 \u10d2\u10d0\u10db\u10dd\u10d8\u10e7\u10d4\u10dc\u10dd\u10d7 OpenZWave Supervisor-\u10d8\u10e1 \u10d3\u10d0\u10dc\u10d0\u10db\u10d0\u10e2\u10d8?",
+ "title": "\u10d0\u10d8\u10e0\u10e9\u10d8\u10d4\u10d7 \u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d8\u10e1 \u10db\u10d4\u10d7\u10dd\u10d3\u10d8"
+ },
+ "start_addon": {
+ "data": {
+ "network_key": "\u10e5\u10e1\u10d4\u10da\u10d8\u10e1 \u10d2\u10d0\u10e1\u10d0\u10e6\u10d4\u10d1\u10d8",
+ "usb_path": "USB \u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10da\u10dd\u10d1\u10d8\u10e1 \u10d2\u10d6\u10d0"
+ },
+ "title": "\u10e8\u10d4\u10d8\u10e7\u10d5\u10d0\u10dc\u10d4\u10d7 OpenZWave \u10d3\u10d0\u10dc\u10d0\u10db\u10d0\u10e2\u10d8\u10e1 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ozw/translations/no.json b/homeassistant/components/ozw/translations/no.json
index 04c7cf2d50c..966e1a4065b 100644
--- a/homeassistant/components/ozw/translations/no.json
+++ b/homeassistant/components/ozw/translations/no.json
@@ -10,7 +10,13 @@
"error": {
"addon_start_failed": "Kunne ikke starte OpenZWave-tillegget. Sjekk konfigurasjonen."
},
+ "progress": {
+ "install_addon": "Vent mens OpenZWave-tilleggsinstallasjonen er ferdig. Dette kan ta flere minutter."
+ },
"step": {
+ "install_addon": {
+ "title": "Installasjonen av tilleggsprogrammet OpenZWave har startet"
+ },
"on_supervisor": {
"data": {
"use_addon": "Bruk OpenZWave Supervisor-tillegget"
diff --git a/homeassistant/components/ozw/translations/pl.json b/homeassistant/components/ozw/translations/pl.json
index f63c4dd10b6..a143163ca1b 100644
--- a/homeassistant/components/ozw/translations/pl.json
+++ b/homeassistant/components/ozw/translations/pl.json
@@ -10,7 +10,13 @@
"error": {
"addon_start_failed": "Nie uda\u0142o si\u0119 uruchomi\u0107 dodatku OpenZWave. Sprawd\u017a konfiguracj\u0119."
},
+ "progress": {
+ "install_addon": "Poczekaj, a\u017c zako\u0144czy si\u0119 instalacja dodatku OpenZWave. Mo\u017ce to potrwa\u0107 kilka minut."
+ },
"step": {
+ "install_addon": {
+ "title": "Rozpocz\u0119\u0142a si\u0119 instalacja dodatku OpenZWave"
+ },
"on_supervisor": {
"data": {
"use_addon": "U\u017cyj dodatku OpenZWave Supervisor"
diff --git a/homeassistant/components/ozw/translations/ru.json b/homeassistant/components/ozw/translations/ru.json
index b7a582faa08..b2f5ebd6e8e 100644
--- a/homeassistant/components/ozw/translations/ru.json
+++ b/homeassistant/components/ozw/translations/ru.json
@@ -10,7 +10,13 @@
"error": {
"addon_start_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c OpenZWave. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e."
},
+ "progress": {
+ "install_addon": "\u041f\u043e\u0434\u043e\u0436\u0434\u0438\u0442\u0435, \u043f\u043e\u043a\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0441\u044f \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f OpenZWave. \u042d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u0437\u0430\u043d\u044f\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043c\u0438\u043d\u0443\u0442."
+ },
"step": {
+ "install_addon": {
+ "title": "\u041d\u0430\u0447\u0430\u043b\u0430\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f OpenZWave"
+ },
"on_supervisor": {
"data": {
"use_addon": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Supervisor OpenZWave"
diff --git a/homeassistant/components/ozw/translations/sl.json b/homeassistant/components/ozw/translations/sl.json
index 03b6d975c2d..c4d67feb5bf 100644
--- a/homeassistant/components/ozw/translations/sl.json
+++ b/homeassistant/components/ozw/translations/sl.json
@@ -2,6 +2,11 @@
"config": {
"abort": {
"mqtt_required": "Integracija MQTT ni nastavljena"
+ },
+ "step": {
+ "on_supervisor": {
+ "title": "Izberite na\u010din povezave"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/ozw/translations/zh-Hant.json b/homeassistant/components/ozw/translations/zh-Hant.json
index 13fcadde01f..f4334e1d632 100644
--- a/homeassistant/components/ozw/translations/zh-Hant.json
+++ b/homeassistant/components/ozw/translations/zh-Hant.json
@@ -10,7 +10,13 @@
"error": {
"addon_start_failed": "OpenZWave add-on \u555f\u52d5\u5931\u6557\uff0c\u8acb\u6aa2\u67e5\u8a2d\u5b9a\u3002"
},
+ "progress": {
+ "install_addon": "\u8acb\u7a0d\u7b49 OpenZWave add-on \u5b89\u88dd\u5b8c\u6210\uff0c\u53ef\u80fd\u6703\u9700\u8981\u5e7e\u5206\u9418\u3002"
+ },
"step": {
+ "install_addon": {
+ "title": "OpenZWave add-on \u5b89\u88dd\u5df2\u555f\u52d5"
+ },
"on_supervisor": {
"data": {
"use_addon": "\u4f7f\u7528 OpenZWave Supervisor add-on"
diff --git a/homeassistant/components/ozw/websocket_api.py b/homeassistant/components/ozw/websocket_api.py
index 482d78bb878..3ee6e040743 100644
--- a/homeassistant/components/ozw/websocket_api.py
+++ b/homeassistant/components/ozw/websocket_api.py
@@ -15,12 +15,13 @@ from openzwavemqtt.util.node import (
set_config_parameter,
)
import voluptuous as vol
+import voluptuous_serialize
from homeassistant.components import websocket_api
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
-from .const import ATTR_CONFIG_PARAMETER, ATTR_CONFIG_VALUE, DOMAIN, MANAGER, OPTIONS
+from .const import ATTR_CONFIG_PARAMETER, ATTR_CONFIG_VALUE, DOMAIN, MANAGER
from .lock import ATTR_USERCODE
TYPE = "type"
@@ -29,6 +30,7 @@ OZW_INSTANCE = "ozw_instance"
NODE_ID = "node_id"
PARAMETER = ATTR_CONFIG_PARAMETER
VALUE = ATTR_CONFIG_VALUE
+SCHEMA = "schema"
ATTR_NODE_QUERY_STAGE = "node_query_stage"
ATTR_IS_ZWAVE_PLUS = "is_zwave_plus"
@@ -106,6 +108,59 @@ def _call_util_function(hass, connection, msg, send_result, function, *args):
connection.send_result(msg[ID])
+def _get_config_params(node, *args):
+ raw_values = get_config_parameters(node)
+ config_params = []
+
+ for param in raw_values:
+ schema = {}
+
+ if param["type"] in ["Byte", "Int", "Short"]:
+ schema = vol.Schema(
+ {
+ vol.Required(param["label"], default=param["value"]): vol.All(
+ vol.Coerce(int), vol.Range(min=param["min"], max=param["max"])
+ )
+ }
+ )
+ data = {param["label"]: param["value"]}
+
+ if param["type"] == "List":
+
+ for options in param["options"]:
+ if options["Label"] == param["value"]:
+ selected = options
+ break
+
+ schema = vol.Schema(
+ {
+ vol.Required(param["label"],): vol.In(
+ {
+ option["Value"]: option["Label"]
+ for option in param["options"]
+ }
+ )
+ }
+ )
+ data = {param["label"]: selected["Value"]}
+
+ config_params.append(
+ {
+ "type": param["type"],
+ "label": param["label"],
+ "parameter": param["parameter"],
+ "help": param["help"],
+ "value": param["value"],
+ "schema": voluptuous_serialize.convert(
+ schema, custom_serializer=cv.custom_serializer
+ ),
+ "data": data,
+ }
+ )
+
+ return config_params
+
+
@websocket_api.websocket_command({vol.Required(TYPE): "ozw/get_instances"})
def websocket_get_instances(hass, connection, msg):
"""Get a list of OZW instances."""
@@ -213,7 +268,7 @@ def websocket_get_code_slots(hass, connection, msg):
)
def websocket_get_config_parameters(hass, connection, msg):
"""Get a list of configuration parameters for an OZW node instance."""
- _call_util_function(hass, connection, msg, True, get_config_parameters)
+ _call_util_function(hass, connection, msg, True, _get_config_params)
@websocket_api.websocket_command(
@@ -245,7 +300,7 @@ def websocket_get_config_parameters(hass, connection, msg):
def websocket_set_config_parameter(hass, connection, msg):
"""Set a config parameter to a node."""
_call_util_function(
- hass, connection, msg, False, set_config_parameter, msg[PARAMETER], msg[VALUE]
+ hass, connection, msg, True, set_config_parameter, msg[PARAMETER], msg[VALUE]
)
@@ -406,7 +461,7 @@ def websocket_refresh_node_info(hass, connection, msg):
"""Tell OpenZWave to re-interview a node."""
manager = hass.data[DOMAIN][MANAGER]
- options = hass.data[DOMAIN][OPTIONS]
+ options = manager.options
@callback
def forward_node(node):
diff --git a/homeassistant/components/panasonic_viera/translations/hu.json b/homeassistant/components/panasonic_viera/translations/hu.json
index 2c3a9a820f9..fbf7f49be6a 100644
--- a/homeassistant/components/panasonic_viera/translations/hu.json
+++ b/homeassistant/components/panasonic_viera/translations/hu.json
@@ -1,5 +1,11 @@
{
"config": {
+ "abort": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
"step": {
"user": {
"data": {
diff --git a/homeassistant/components/person/translations/zh-Hans.json b/homeassistant/components/person/translations/zh-Hans.json
index 16213ba3079..fd3c48e0b4e 100644
--- a/homeassistant/components/person/translations/zh-Hans.json
+++ b/homeassistant/components/person/translations/zh-Hans.json
@@ -5,5 +5,5 @@
"not_home": "\u79bb\u5f00"
}
},
- "title": "\u4e2a\u4eba"
+ "title": "\u4eba\u5458"
}
\ No newline at end of file
diff --git a/homeassistant/components/pi_hole/translations/sl.json b/homeassistant/components/pi_hole/translations/sl.json
index cd46d19f38c..f8f2b1e05e4 100644
--- a/homeassistant/components/pi_hole/translations/sl.json
+++ b/homeassistant/components/pi_hole/translations/sl.json
@@ -3,7 +3,10 @@
"step": {
"user": {
"data": {
- "location": "Lokacija"
+ "location": "Lokacija",
+ "name": "Ime",
+ "ssl": "Uporablja SSL certifikat",
+ "verify_ssl": "Preverite SSL certifikat"
}
}
}
diff --git a/homeassistant/components/plaato/translations/hu.json b/homeassistant/components/plaato/translations/hu.json
new file mode 100644
index 00000000000..76229e86224
--- /dev/null
+++ b/homeassistant/components/plaato/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "create_entry": {
+ "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a webhook funkci\u00f3t a Plaato Airlock-ban. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: \" {webhook_url} \"\n - M\u00f3dszer: POST \n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1sd a [dokument\u00e1ci\u00f3t] ( {docs_url} )."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plaato/translations/ka.json b/homeassistant/components/plaato/translations/ka.json
new file mode 100644
index 00000000000..a284a55fbcf
--- /dev/null
+++ b/homeassistant/components/plaato/translations/ka.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "webhook_not_internet_accessible": "\u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 Home Assistant \u10d4\u10d9\u10d6\u10d4\u10db\u10de\u10da\u10d0\u10e0\u10d8 \u10e1\u10d0\u10ed\u10d8\u10e0\u10dd\u10e0\u10d4\u10d1\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10dc\u10d4\u10e2\u10d7\u10d0\u10dc \u10ec\u10d5\u10d3\u10dd\u10db\u10d0\u10e1 webhook \u10e8\u10d4\u10e2\u10e7\u10dd\u10d1\u10d8\u10dc\u10d4\u10d1\u10d4\u10d1\u10d8\u10e1 \u10db\u10d8\u10e1\u10d0\u10e6\u10d4\u10d1\u10d0\u10d3."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plaato/translations/pl.json b/homeassistant/components/plaato/translations/pl.json
index 0abb0df4142..1f7c8141aa5 100644
--- a/homeassistant/components/plaato/translations/pl.json
+++ b/homeassistant/components/plaato/translations/pl.json
@@ -5,7 +5,7 @@
"webhook_not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty webhook"
},
"create_entry": {
- "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant, 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/plex/__init__.py b/homeassistant/components/plex/__init__.py
index e4f4f80dcfa..de8b278f3cf 100644
--- a/homeassistant/components/plex/__init__.py
+++ b/homeassistant/components/plex/__init__.py
@@ -1,6 +1,5 @@
"""Support to embed Plex."""
import asyncio
-import functools
from functools import partial
import logging
@@ -35,10 +34,7 @@ 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.debounce import Debouncer
-from homeassistant.helpers.dispatcher import (
- async_dispatcher_connect,
- async_dispatcher_send,
-)
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import (
CONF_SERVER,
@@ -176,6 +172,7 @@ async def async_setup_entry(hass, entry):
if data == STATE_CONNECTED:
_LOGGER.debug("Websocket to %s successful", entry.data[CONF_SERVER])
+ hass.async_create_task(async_update_plex())
elif data == STATE_DISCONNECTED:
_LOGGER.debug(
"Websocket to %s disconnected, retrying", entry.data[CONF_SERVER]
@@ -190,7 +187,7 @@ async def async_setup_entry(hass, entry):
hass.async_create_task(hass.config_entries.async_reload(entry.entry_id))
elif signal == SIGNAL_DATA:
- async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id))
+ hass.async_create_task(plex_server.async_update_session(data))
session = async_get_clientsession(hass)
verify_ssl = server_config.get(CONF_VERIFY_SSL)
@@ -219,7 +216,7 @@ async def async_setup_entry(hass, entry):
task = hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, platform)
)
- task.add_done_callback(functools.partial(start_websocket_session, platform))
+ task.add_done_callback(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)
diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py
index f54c376c667..c13be439be7 100644
--- a/homeassistant/components/plex/const.py
+++ b/homeassistant/components/plex/const.py
@@ -24,6 +24,7 @@ WEBSOCKETS = "websockets"
PLEX_SERVER_CONFIG = "server_config"
PLEX_NEW_MP_SIGNAL = "plex_new_mp_signal.{}"
+PLEX_UPDATE_MEDIA_PLAYER_SESSION_SIGNAL = "plex_update_session_signal.{}"
PLEX_UPDATE_MEDIA_PLAYER_SIGNAL = "plex_update_mp_signal.{}"
PLEX_UPDATE_PLATFORMS_SIGNAL = "plex_update_platforms_signal.{}"
PLEX_UPDATE_SENSOR_SIGNAL = "plex_update_sensor_signal.{}"
diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py
index 56e6f68a968..cfc5a12d6c5 100644
--- a/homeassistant/components/plex/media_browser.py
+++ b/homeassistant/components/plex/media_browser.py
@@ -52,14 +52,69 @@ ITEM_TYPE_MEDIA_CLASS = {
_LOGGER = logging.getLogger(__name__)
-def browse_media(
- entity_id, plex_server, media_content_type=None, media_content_id=None
-):
+def browse_media(entity, is_internal, media_content_type=None, media_content_id=None):
"""Implement the websocket media browsing helper."""
+ def item_payload(item):
+ """Create response payload for a single media item."""
+ try:
+ media_class = ITEM_TYPE_MEDIA_CLASS[item.type]
+ except KeyError as err:
+ _LOGGER.debug("Unknown type received: %s", item.type)
+ raise UnknownMediaType from err
+ payload = {
+ "title": item.title,
+ "media_class": media_class,
+ "media_content_id": str(item.ratingKey),
+ "media_content_type": item.type,
+ "can_play": True,
+ "can_expand": item.type in EXPANDABLES,
+ }
+ if hasattr(item, "thumbUrl"):
+ entity.plex_server.thumbnail_cache.setdefault(
+ str(item.ratingKey), item.thumbUrl
+ )
+
+ if is_internal:
+ thumbnail = item.thumbUrl
+ else:
+ thumbnail = entity.get_browse_image_url(item.type, item.ratingKey)
+
+ payload["thumbnail"] = thumbnail
+
+ return BrowseMedia(**payload)
+
+ def library_payload(library_id):
+ """Create response payload to describe contents of a specific library."""
+ library = entity.plex_server.library.sectionByID(library_id)
+ library_info = library_section_payload(library)
+ library_info.children = []
+ library_info.children.append(special_library_payload(library_info, "On Deck"))
+ library_info.children.append(
+ special_library_payload(library_info, "Recently Added")
+ )
+ for item in library.all():
+ try:
+ library_info.children.append(item_payload(item))
+ except UnknownMediaType:
+ continue
+ return library_info
+
+ def playlists_payload():
+ """Create response payload for all available playlists."""
+ playlists_info = {**PLAYLISTS_BROWSE_PAYLOAD, "children": []}
+ for playlist in entity.plex_server.playlists():
+ try:
+ playlists_info["children"].append(item_payload(playlist))
+ except UnknownMediaType:
+ continue
+ response = BrowseMedia(**playlists_info)
+ response.children_media_class = MEDIA_CLASS_PLAYLIST
+ return response
+
def build_item_response(payload):
"""Create response payload for the provided media query."""
- media = plex_server.lookup_media(**payload)
+ media = entity.plex_server.lookup_media(**payload)
if media is None:
return None
@@ -85,19 +140,21 @@ def browse_media(
if (
media_content_type
and media_content_type == "server"
- and media_content_id != plex_server.machine_identifier
+ and media_content_id != entity.plex_server.machine_identifier
):
raise BrowseError(
- f"Plex server with ID '{media_content_id}' is not associated with {entity_id}"
+ f"Plex server with ID '{media_content_id}' is not associated with {entity.entity_id}"
)
if special_folder:
if media_content_type == "server":
- library_or_section = plex_server.library
+ library_or_section = entity.plex_server.library
children_media_class = MEDIA_CLASS_DIRECTORY
- title = plex_server.friendly_name
+ title = entity.plex_server.friendly_name
elif media_content_type == "library":
- library_or_section = plex_server.library.sectionByID(media_content_id)
+ library_or_section = entity.plex_server.library.sectionByID(
+ media_content_id
+ )
title = library_or_section.title
try:
children_media_class = ITEM_TYPE_MEDIA_CLASS[library_or_section.TYPE]
@@ -133,10 +190,10 @@ def browse_media(
try:
if media_content_type in ["server", None]:
- return server_payload(plex_server)
+ return server_payload(entity.plex_server)
if media_content_type == "library":
- return library_payload(plex_server, media_content_id)
+ return library_payload(media_content_id)
except UnknownMediaType as err:
raise BrowseError(
@@ -144,7 +201,7 @@ def browse_media(
) from err
if media_content_type == "playlists":
- return playlists_payload(plex_server)
+ return playlists_payload()
payload = {
"media_type": DOMAIN,
@@ -156,27 +213,6 @@ def browse_media(
return response
-def item_payload(item):
- """Create response payload for a single media item."""
- try:
- media_class = ITEM_TYPE_MEDIA_CLASS[item.type]
- except KeyError as err:
- _LOGGER.debug("Unknown type received: %s", item.type)
- raise UnknownMediaType from err
- payload = {
- "title": item.title,
- "media_class": media_class,
- "media_content_id": str(item.ratingKey),
- "media_content_type": item.type,
- "can_play": True,
- "can_expand": item.type in EXPANDABLES,
- }
- if hasattr(item, "thumbUrl"):
- payload["thumbnail"] = item.thumbUrl
-
- return BrowseMedia(**payload)
-
-
def library_section_payload(section):
"""Create response payload for a single library section."""
try:
@@ -229,33 +265,3 @@ def server_payload(plex_server):
server_info.children.append(library_section_payload(library))
server_info.children.append(BrowseMedia(**PLAYLISTS_BROWSE_PAYLOAD))
return server_info
-
-
-def library_payload(plex_server, library_id):
- """Create response payload to describe contents of a specific library."""
- library = plex_server.library.sectionByID(library_id)
- library_info = library_section_payload(library)
- library_info.children = []
- library_info.children.append(special_library_payload(library_info, "On Deck"))
- library_info.children.append(
- special_library_payload(library_info, "Recently Added")
- )
- for item in library.all():
- try:
- library_info.children.append(item_payload(item))
- except UnknownMediaType:
- continue
- return library_info
-
-
-def playlists_payload(plex_server):
- """Create response payload for all available playlists."""
- playlists_info = {**PLAYLISTS_BROWSE_PAYLOAD, "children": []}
- for playlist in plex_server.playlists():
- try:
- playlists_info["children"].append(item_payload(playlist))
- except UnknownMediaType:
- continue
- response = BrowseMedia(**playlists_info)
- response.children_media_class = MEDIA_CLASS_PLAYLIST
- return response
diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py
index 94bed1db7de..4d765cc0508 100644
--- a/homeassistant/components/plex/media_player.py
+++ b/homeassistant/components/plex/media_player.py
@@ -1,4 +1,5 @@
"""Support to interface with the Plex API."""
+from functools import wraps
import json
import logging
@@ -7,25 +8,26 @@ import requests.exceptions
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN, MediaPlayerEntity
from homeassistant.components.media_player.const import (
- MEDIA_TYPE_MOVIE,
MEDIA_TYPE_MUSIC,
- MEDIA_TYPE_TVSHOW,
- MEDIA_TYPE_VIDEO,
SUPPORT_BROWSE_MEDIA,
SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE,
SUPPORT_PLAY,
SUPPORT_PLAY_MEDIA,
SUPPORT_PREVIOUS_TRACK,
+ SUPPORT_SEEK,
SUPPORT_STOP,
SUPPORT_VOLUME_MUTE,
SUPPORT_VOLUME_SET,
)
-from homeassistant.const import STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING
+from homeassistant.const import STATE_IDLE, STATE_PAUSED, STATE_PLAYING
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_registry import async_get_registry
-from homeassistant.util import dt as dt_util
+from homeassistant.helpers.network import is_internal_request
from .const import (
COMMON_PLAYERS,
@@ -34,23 +36,28 @@ from .const import (
DOMAIN as PLEX_DOMAIN,
NAME_FORMAT,
PLEX_NEW_MP_SIGNAL,
+ PLEX_UPDATE_MEDIA_PLAYER_SESSION_SIGNAL,
PLEX_UPDATE_MEDIA_PLAYER_SIGNAL,
+ PLEX_UPDATE_SENSOR_SIGNAL,
SERVERS,
)
from .media_browser import browse_media
-LIVE_TV_SECTION = "-4"
-PLAYLISTS_BROWSE_PAYLOAD = {
- "title": "Playlists",
- "media_content_id": "all",
- "media_content_type": "playlists",
- "can_play": False,
- "can_expand": True,
-}
-
_LOGGER = logging.getLogger(__name__)
+def needs_session(func):
+ """Ensure session is available for certain attributes."""
+
+ @wraps(func)
+ def get_session_attribute(self, *args):
+ if self.session is None:
+ return None
+ return func(self, *args)
+
+ return get_session_attribute
+
+
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Plex media_player from a config entry."""
server_id = config_entry.data[CONF_SERVER_IDENTIFIER]
@@ -58,9 +65,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
@callback
def async_new_media_players(new_entities):
- _async_add_entities(
- hass, registry, config_entry, async_add_entities, server_id, new_entities
- )
+ _async_add_entities(hass, registry, async_add_entities, server_id, new_entities)
unsub = async_dispatcher_connect(
hass, PLEX_NEW_MP_SIGNAL.format(server_id), async_new_media_players
@@ -70,9 +75,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
@callback
-def _async_add_entities(
- hass, registry, config_entry, async_add_entities, server_id, new_entities
-):
+def _async_add_entities(hass, registry, async_add_entities, server_id, new_entities):
"""Set up Plex media_player entities."""
_LOGGER.debug("New entities: %s", new_entities)
entities = []
@@ -104,258 +107,113 @@ class PlexMediaPlayer(MediaPlayerEntity):
"""Initialize the Plex device."""
self.plex_server = plex_server
self.device = device
- self.session = session
self.player_source = player_source
- self._app_name = ""
+
+ self.device_make = None
+ self.device_platform = None
+ self.device_product = None
+ self.device_title = None
+ self.device_version = None
+ self.machine_identifier = device.machineIdentifier
+ self.session_device = None
+
self._available = False
self._device_protocol_capabilities = None
- self._is_player_active = False
- self._machine_identifier = device.machineIdentifier
- self._make = ""
- self._device_platform = None
- self._device_product = None
- self._device_title = None
- self._device_version = None
self._name = None
- self._player_state = "idle"
self._previous_volume_level = 1 # Used in fake muting
- self._session_type = None
- self._session_username = None
self._state = STATE_IDLE
self._volume_level = 1 # since we can't retrieve remotely
self._volume_muted = False # since we can't retrieve remotely
- # General
- self._media_content_id = None
- self._media_content_rating = None
- self._media_content_type = None
- self._media_duration = None
- self._media_image_url = None
- self._media_summary = None
- self._media_title = None
- self._media_position = None
- self._media_position_updated_at = None
- # Music
- self._media_album_artist = None
- self._media_album_name = None
- self._media_artist = None
- self._media_track = None
- # TV Show
- self._media_episode = None
- self._media_season = None
- self._media_series_title = None
+
+ # Initializes other attributes
+ self.session = session
async def async_added_to_hass(self):
"""Run when about to be added to hass."""
- server_id = self.plex_server.machine_identifier
-
_LOGGER.debug("Added %s [%s]", self.entity_id, self.unique_id)
- unsub = async_dispatcher_connect(
- self.hass,
- PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(self.unique_id),
- self.async_refresh_media_player,
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass,
+ PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(self.unique_id),
+ self.async_refresh_media_player,
+ )
+ )
+
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass,
+ PLEX_UPDATE_MEDIA_PLAYER_SESSION_SIGNAL.format(self.unique_id),
+ self.async_update_from_websocket,
+ )
)
- self.hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub)
@callback
- def async_refresh_media_player(self, device, session):
+ def async_refresh_media_player(self, device, session, source):
"""Set instance objects and trigger an entity state update."""
_LOGGER.debug("Refreshing %s [%s / %s]", self.entity_id, device, session)
self.device = device
self.session = session
+ if source:
+ self.player_source = source
self.async_schedule_update_ha_state(True)
- def _clear_media_details(self):
- """Set all Media Items to None."""
- # General
- self._media_content_id = None
- self._media_content_rating = None
- self._media_content_type = None
- self._media_duration = None
- self._media_image_url = None
- self._media_summary = None
- self._media_title = None
- # Music
- self._media_album_artist = None
- self._media_album_name = None
- self._media_artist = None
- self._media_track = None
- # TV Show
- self._media_episode = None
- self._media_season = None
- self._media_series_title = None
+ async_dispatcher_send(
+ self.hass,
+ PLEX_UPDATE_SENSOR_SIGNAL.format(self.plex_server.machine_identifier),
+ )
- # Clear library Name
- self._app_name = ""
+ @callback
+ def async_update_from_websocket(self, state):
+ """Update the entity based on new websocket data."""
+ self.update_state(state)
+ self.async_write_ha_state()
+
+ async_dispatcher_send(
+ self.hass,
+ PLEX_UPDATE_SENSOR_SIGNAL.format(self.plex_server.machine_identifier),
+ )
def update(self):
"""Refresh key device data."""
- self._clear_media_details()
-
- self._available = self.device or self.session
-
- if self.device:
- try:
- device_url = self.device.url("/")
- except plexapi.exceptions.BadRequest:
- device_url = "127.0.0.1"
- if "127.0.0.1" in device_url:
- self.device.proxyThroughServer()
- self._device_platform = self.device.platform
- self._device_product = self.device.product
- self._device_title = self.device.title
- self._device_version = self.device.version
- self._device_protocol_capabilities = self.device.protocolCapabilities
- self._player_state = self.device.state
-
if not self.session:
self.force_idle()
- else:
- session_device = next(
- (
- p
- for p in self.session.players
- if p.machineIdentifier == self.device.machineIdentifier
- ),
- None,
- )
- if session_device:
- self._make = session_device.device or ""
- self._player_state = session_device.state
- self._device_platform = self._device_platform or session_device.platform
- self._device_product = self._device_product or session_device.product
- self._device_title = self._device_title or session_device.title
- self._device_version = self._device_version or session_device.version
- else:
- _LOGGER.warning("No player associated with active session")
+ if not self.device:
+ self._available = False
+ return
- if self.session.usernames:
- self._session_username = self.session.usernames[0]
+ self._available = True
- # Calculate throttled position for proper progress display.
- position = int(self.session.viewOffset / 1000)
- now = dt_util.utcnow()
- if self._media_position is not None:
- pos_diff = position - self._media_position
- time_diff = now - self._media_position_updated_at
- if pos_diff != 0 and abs(time_diff.total_seconds() - pos_diff) > 5:
- self._media_position_updated_at = now
- self._media_position = position
- else:
- self._media_position_updated_at = now
- self._media_position = position
+ try:
+ device_url = self.device.url("/")
+ except plexapi.exceptions.BadRequest:
+ device_url = "127.0.0.1"
+ if "127.0.0.1" in device_url:
+ self.device.proxyThroughServer()
+ self._device_protocol_capabilities = self.device.protocolCapabilities
- self._media_content_id = self.session.ratingKey
- self._media_content_rating = getattr(self.session, "contentRating", None)
+ for device in filter(None, [self.device, self.session_device]):
+ self.device_make = self.device_make or device.device
+ self.device_platform = self.device_platform or device.platform
+ self.device_product = self.device_product or device.product
+ self.device_title = self.device_title or device.title
+ self.device_version = self.device_version or device.version
- name_parts = [self._device_product, self._device_title or self._device_platform]
- if (self._device_product in COMMON_PLAYERS) and self.make:
+ name_parts = [self.device_product, self.device_title or self.device_platform]
+ if (self.device_product in COMMON_PLAYERS) and self.device_make:
# Add more context in name for likely duplicates
- name_parts.append(self.make)
+ name_parts.append(self.device_make)
if self.username and self.username != self.plex_server.owner:
# Prepend username for shared/managed clients
name_parts.insert(0, self.username)
self._name = NAME_FORMAT.format(" - ".join(name_parts))
- self._set_player_state()
-
- if self._is_player_active and self.session is not None:
- self._session_type = self.session.type
- if self.session.duration:
- self._media_duration = int(self.session.duration / 1000)
- else:
- self._media_duration = None
- # title (movie name, tv episode name, music song name)
- self._media_summary = self.session.summary
- self._media_title = self.session.title
- # media type
- self._set_media_type()
- if self.session.librarySectionID == LIVE_TV_SECTION:
- self._app_name = "Live TV"
- else:
- self._app_name = (
- self.session.section().title
- if self.session.section() is not None
- else ""
- )
- self._set_media_image()
- else:
- self._session_type = None
-
- def _set_media_image(self):
- thumb_url = self.session.thumbUrl
- if (
- self.media_content_type is MEDIA_TYPE_TVSHOW
- and not self.plex_server.option_use_episode_art
- ):
- if self.session.librarySectionID == LIVE_TV_SECTION:
- thumb_url = self.session.grandparentThumb
- else:
- thumb_url = self.session.url(self.session.grandparentThumb)
-
- if thumb_url is None:
- _LOGGER.debug(
- "Using media art because media thumb was not found: %s", self.name
- )
- thumb_url = self.session.url(self.session.art)
-
- self._media_image_url = thumb_url
-
- def _set_player_state(self):
- if self._player_state == "playing":
- self._is_player_active = True
- self._state = STATE_PLAYING
- elif self._player_state == "paused":
- self._is_player_active = True
- self._state = STATE_PAUSED
- elif self.device:
- self._is_player_active = False
- self._state = STATE_IDLE
- else:
- self._is_player_active = False
- self._state = STATE_OFF
-
- def _set_media_type(self):
- if self._session_type == "episode":
- self._media_content_type = MEDIA_TYPE_TVSHOW
-
- # season number (00)
- self._media_season = self.session.seasonNumber
- # show name
- self._media_series_title = self.session.grandparentTitle
- # episode number (00)
- if self.session.index is not None:
- self._media_episode = self.session.index
-
- elif self._session_type == "movie":
- self._media_content_type = MEDIA_TYPE_MOVIE
- if self.session.year is not None and self._media_title is not None:
- self._media_title += f" ({self.session.year!s})"
-
- elif self._session_type == "track":
- self._media_content_type = MEDIA_TYPE_MUSIC
- self._media_album_name = self.session.parentTitle
- self._media_album_artist = self.session.grandparentTitle
- self._media_track = self.session.index
- self._media_artist = self.session.originalTitle
- # use album artist if track artist is missing
- if self._media_artist is None:
- _LOGGER.debug(
- "Using album artist because track artist was not found: %s",
- self.name,
- )
- self._media_artist = self._media_album_artist
-
- elif self._session_type == "clip":
- _LOGGER.debug(
- "Clip content type detected, compatibility may vary: %s", self.name
- )
- self._media_content_type = MEDIA_TYPE_VIDEO
def force_idle(self):
"""Force client to idle."""
- self._player_state = STATE_IDLE
self._state = STATE_IDLE
- self.session = None
- self._clear_media_details()
+ if self.player_source == "session":
+ self.device = None
+ self.session_device = None
+ self._available = False
@property
def should_poll(self):
@@ -365,12 +223,21 @@ class PlexMediaPlayer(MediaPlayerEntity):
@property
def unique_id(self):
"""Return the id of this plex client."""
- return f"{self.plex_server.machine_identifier}:{self._machine_identifier}"
+ return f"{self.plex_server.machine_identifier}:{self.machine_identifier}"
@property
- def machine_identifier(self):
- """Return the Plex-provided identifier of this plex client."""
- return self._machine_identifier
+ def session(self):
+ """Return the active session for this player."""
+ return self._session
+
+ @session.setter
+ def session(self, session):
+ self._session = session
+ if session:
+ self.session_device = self.session.player
+ self.update_state(self.session.state)
+ else:
+ self._state = STATE_IDLE
@property
def available(self):
@@ -383,20 +250,33 @@ class PlexMediaPlayer(MediaPlayerEntity):
return self._name
@property
+ @needs_session
def username(self):
"""Return the username of the client owner."""
- return self._session_username
-
- @property
- def app_name(self):
- """Return the library name of playing media."""
- return self._app_name
+ return self.session.username
@property
def state(self):
"""Return the state of the device."""
return self._state
+ def update_state(self, state):
+ """Set the state of the device, handle session termination."""
+ if state == "playing":
+ self._state = STATE_PLAYING
+ elif state == "paused":
+ self._state = STATE_PAUSED
+ elif state == "stopped":
+ self.session = None
+ self.force_idle()
+ else:
+ self._state = STATE_IDLE
+
+ @property
+ def _is_player_active(self):
+ """Report if the client is playing media."""
+ return self.state in [STATE_PLAYING, STATE_PAUSED]
+
@property
def _active_media_plexapi_type(self):
"""Get the active media type required by PlexAPI commands."""
@@ -406,84 +286,112 @@ class PlexMediaPlayer(MediaPlayerEntity):
return "video"
@property
+ @needs_session
+ def session_key(self):
+ """Return current session key."""
+ return self.session.sessionKey
+
+ @property
+ @needs_session
+ def media_library_title(self):
+ """Return the library name of playing media."""
+ return self.session.media_library_title
+
+ @property
+ @needs_session
def media_content_id(self):
"""Return the content ID of current playing media."""
- return self._media_content_id
+ return self.session.media_content_id
@property
+ @needs_session
def media_content_type(self):
"""Return the content type of current playing media."""
- return self._media_content_type
+ return self.session.media_content_type
@property
+ @needs_session
+ def media_content_rating(self):
+ """Return the content rating of current playing media."""
+ return self.session.media_content_rating
+
+ @property
+ @needs_session
def media_artist(self):
"""Return the artist of current playing media, music track only."""
- return self._media_artist
+ return self.session.media_artist
@property
+ @needs_session
def media_album_name(self):
"""Return the album name of current playing media, music track only."""
- return self._media_album_name
+ return self.session.media_album_name
@property
+ @needs_session
def media_album_artist(self):
"""Return the album artist of current playing media, music only."""
- return self._media_album_artist
+ return self.session.media_album_artist
@property
+ @needs_session
def media_track(self):
"""Return the track number of current playing media, music only."""
- return self._media_track
+ return self.session.media_track
@property
+ @needs_session
def media_duration(self):
"""Return the duration of current playing media in seconds."""
- return self._media_duration
+ return self.session.media_duration
@property
+ @needs_session
def media_position(self):
"""Return the duration of current playing media in seconds."""
- return self._media_position
+ return self.session.media_position
@property
+ @needs_session
def media_position_updated_at(self):
"""When was the position of the current playing media valid."""
- return self._media_position_updated_at
+ return self.session.media_position_updated_at
@property
+ @needs_session
def media_image_url(self):
"""Return the image URL of current playing media."""
- return self._media_image_url
+ return self.session.media_image_url
@property
+ @needs_session
def media_summary(self):
"""Return the summary of current playing media."""
- return self._media_summary
+ return self.session.media_summary
@property
+ @needs_session
def media_title(self):
"""Return the title of current playing media."""
- return self._media_title
+ return self.session.media_title
@property
+ @needs_session
def media_season(self):
"""Return the season of current playing media (TV Show only)."""
- return self._media_season
+ return self.session.media_season
@property
+ @needs_session
def media_series_title(self):
"""Return the title of the series of current playing media."""
- return self._media_series_title
+ return self.session.media_series_title
@property
+ @needs_session
def media_episode(self):
"""Return the episode of current playing media (TV Show only)."""
- return self._media_episode
-
- @property
- def make(self):
- """Return the make of the device (ex. SHIELD Android TV)."""
- return self._make
+ return self.session.media_episode
@property
def supported_features(self):
@@ -494,6 +402,7 @@ class PlexMediaPlayer(MediaPlayerEntity):
| SUPPORT_PREVIOUS_TRACK
| SUPPORT_NEXT_TRACK
| SUPPORT_STOP
+ | SUPPORT_SEEK
| SUPPORT_VOLUME_SET
| SUPPORT_PLAY
| SUPPORT_PLAY_MEDIA
@@ -518,12 +427,14 @@ class PlexMediaPlayer(MediaPlayerEntity):
and "playback" in self._device_protocol_capabilities
):
return self._volume_level
+ return None
@property
def is_volume_muted(self):
"""Return boolean if volume is currently muted."""
if self._is_player_active and self.device:
return self._volume_muted
+ return None
def mute_volume(self, mute):
"""Mute the volume.
@@ -557,6 +468,11 @@ class PlexMediaPlayer(MediaPlayerEntity):
if self.device and "playback" in self._device_protocol_capabilities:
self.device.stop(self._active_media_plexapi_type)
+ def media_seek(self, position):
+ """Send the seek command."""
+ if self.device and "playback" in self._device_protocol_capabilities:
+ self.device.seekTo(position * 1000, self._active_media_plexapi_type)
+
def media_next_track(self):
"""Send next track command."""
if self.device and "playback" in self._device_protocol_capabilities:
@@ -597,13 +513,19 @@ class PlexMediaPlayer(MediaPlayerEntity):
@property
def device_state_attributes(self):
"""Return the scene state attributes."""
- return {
- "media_content_rating": self._media_content_rating,
- "session_username": self.username,
- "media_library_name": self._app_name,
- "summary": self.media_summary,
- "player_source": self.player_source,
- }
+ attributes = {}
+ for attr in [
+ "media_content_rating",
+ "media_library_title",
+ "player_source",
+ "summary",
+ "username",
+ ]:
+ value = getattr(self, attr, None)
+ if value:
+ attributes[attr] = value
+
+ return attributes
@property
def device_info(self):
@@ -613,19 +535,31 @@ class PlexMediaPlayer(MediaPlayerEntity):
return {
"identifiers": {(PLEX_DOMAIN, self.machine_identifier)},
- "manufacturer": self._device_platform or "Plex",
- "model": self._device_product or self.make,
+ "manufacturer": self.device_platform or "Plex",
+ "model": self.device_product or self.device_make,
"name": self.name,
- "sw_version": self._device_version,
+ "sw_version": self.device_version,
"via_device": (PLEX_DOMAIN, self.plex_server.machine_identifier),
}
async def async_browse_media(self, media_content_type=None, media_content_id=None):
"""Implement the websocket media browsing helper."""
+ is_internal = is_internal_request(self.hass)
return await self.hass.async_add_executor_job(
browse_media,
- self.entity_id,
- self.plex_server,
+ self,
+ is_internal,
media_content_type,
media_content_id,
)
+
+ async def async_get_browse_image(
+ self, media_content_type, media_content_id, media_image_id=None
+ ):
+ """Get media image from Plex server."""
+ image_url = self.plex_server.thumbnail_cache.get(media_content_id)
+ if image_url:
+ result = await self._async_fetch_image(image_url)
+ return result
+
+ return (None, None)
diff --git a/homeassistant/components/plex/models.py b/homeassistant/components/plex/models.py
new file mode 100644
index 00000000000..7633c5deaa8
--- /dev/null
+++ b/homeassistant/components/plex/models.py
@@ -0,0 +1,126 @@
+"""Models to represent various Plex objects used in the integration."""
+from homeassistant.components.media_player.const import (
+ MEDIA_TYPE_MOVIE,
+ MEDIA_TYPE_MUSIC,
+ MEDIA_TYPE_TVSHOW,
+ MEDIA_TYPE_VIDEO,
+)
+from homeassistant.util import dt as dt_util
+
+LIVE_TV_SECTION = "-4"
+
+
+class PlexSession:
+ """Represents a Plex playback session."""
+
+ def __init__(self, plex_server, session):
+ """Initialize the object."""
+ self.plex_server = plex_server
+
+ # Available on both media and session objects
+ self.media_content_id = None
+ self.media_content_type = None
+ self.media_content_rating = None
+ self.media_duration = None
+ self.media_image_url = None
+ self.media_library_title = None
+ self.media_summary = None
+ self.media_title = None
+ # TV Shows
+ self.media_episode = None
+ self.media_season = None
+ self.media_series_title = None
+ # Music
+ self.media_album_name = None
+ self.media_album_artist = None
+ self.media_artist = None
+ self.media_track = None
+
+ # Only available on sessions
+ self.player = next(iter(session.players), None)
+ self.device_product = self.player.product
+ self.media_position = session.viewOffset
+ self.session_key = session.sessionKey
+ self.state = self.player.state
+ self.username = next(iter(session.usernames), None)
+
+ # Used by sensor entity
+ sensor_user_list = [self.username, self.device_product]
+ self.sensor_title = None
+ self.sensor_user = " - ".join(filter(None, sensor_user_list))
+
+ self.update_media(session)
+
+ def __repr__(self):
+ """Return representation of the session."""
+ return f"<{self.session_key}:{self.sensor_title}>"
+
+ def update_media(self, media):
+ """Update attributes from a media object."""
+ self.media_content_id = media.ratingKey
+ self.media_content_rating = getattr(media, "contentRating", None)
+ self.media_image_url = self.get_media_image_url(media)
+ self.media_summary = media.summary
+ self.media_title = media.title
+
+ if media.duration:
+ self.media_duration = int(media.duration / 1000)
+
+ if media.librarySectionID == LIVE_TV_SECTION:
+ self.media_library_title = "Live TV"
+ else:
+ self.media_library_title = (
+ media.section().title if media.section() is not None else ""
+ )
+
+ if media.type == "episode":
+ self.media_content_type = MEDIA_TYPE_TVSHOW
+ self.media_season = media.seasonNumber
+ self.media_series_title = media.grandparentTitle
+ if media.index is not None:
+ self.media_episode = media.index
+ self.sensor_title = f"{self.media_series_title} - {media.seasonEpisode} - {self.media_title}"
+ elif media.type == "movie":
+ self.media_content_type = MEDIA_TYPE_MOVIE
+ if media.year is not None and media.title is not None:
+ self.media_title += f" ({media.year!s})"
+ self.sensor_title = self.media_title
+ elif media.type == "track":
+ self.media_content_type = MEDIA_TYPE_MUSIC
+ self.media_album_name = media.parentTitle
+ self.media_album_artist = media.grandparentTitle
+ self.media_track = media.index
+ self.media_artist = media.originalTitle or self.media_album_artist
+ self.sensor_title = (
+ f"{self.media_artist} - {self.media_album_name} - {self.media_title}"
+ )
+ elif media.type == "clip":
+ self.media_content_type = MEDIA_TYPE_VIDEO
+ self.sensor_title = media.title
+ else:
+ self.sensor_title = "Unknown"
+
+ @property
+ def media_position(self):
+ """Return the current playback position."""
+ return self._media_position
+
+ @media_position.setter
+ def media_position(self, offset):
+ """Set the current playback position."""
+ self._media_position = int(offset / 1000)
+ self.media_position_updated_at = dt_util.utcnow()
+
+ def get_media_image_url(self, media):
+ """Get the image URL from a media object."""
+ thumb_url = media.thumbUrl
+ if media.type == "episode" and not self.plex_server.option_use_episode_art:
+ if media.librarySectionID == LIVE_TV_SECTION:
+ thumb_url = media.grandparentThumb
+ else:
+ thumb_url = media.url(media.grandparentThumb)
+
+ if thumb_url is None:
+ thumb_url = media.url(media.art)
+
+ return thumb_url
diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py
index a3b465dfdb0..8c3733a7450 100644
--- a/homeassistant/components/plex/sensor.py
+++ b/homeassistant/components/plex/sensor.py
@@ -1,20 +1,15 @@
"""Support for Plex media server monitoring."""
import logging
-from homeassistant.core import callback
-from homeassistant.helpers.dispatcher import (
- async_dispatcher_connect,
- async_dispatcher_send,
-)
+from homeassistant.helpers.debounce import Debouncer
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
-from homeassistant.helpers.event import async_call_later
from .const import (
CONF_SERVER_IDENTIFIER,
DISPATCHERS,
DOMAIN as PLEX_DOMAIN,
NAME_FORMAT,
- PLEX_UPDATE_PLATFORMS_SIGNAL,
PLEX_UPDATE_SENSOR_SIGNAL,
SERVERS,
)
@@ -26,21 +21,26 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up Plex sensor from a config entry."""
server_id = config_entry.data[CONF_SERVER_IDENTIFIER]
plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id]
- sensor = PlexSensor(plexserver)
+ sensor = PlexSensor(hass, plexserver)
async_add_entities([sensor])
class PlexSensor(Entity):
"""Representation of a Plex now playing sensor."""
- def __init__(self, plex_server):
+ def __init__(self, hass, plex_server):
"""Initialize the sensor."""
- self.sessions = []
self._state = None
- self._now_playing = []
self._server = plex_server
self._name = NAME_FORMAT.format(plex_server.friendly_name)
self._unique_id = f"sensor-{plex_server.machine_identifier}"
+ self.async_refresh_sensor = Debouncer(
+ hass,
+ _LOGGER,
+ cooldown=3,
+ immediate=False,
+ function=self._async_refresh_sensor,
+ ).async_call
async def async_added_to_hass(self):
"""Run when about to be added to hass."""
@@ -52,85 +52,12 @@ class PlexSensor(Entity):
)
self.hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub)
- async def async_refresh_sensor(self, sessions):
+ async def _async_refresh_sensor(self):
"""Set instance object and trigger an entity state update."""
_LOGGER.debug("Refreshing sensor [%s]", self.unique_id)
-
- self.sessions = sessions
- update_failed = False
-
- @callback
- def update_plex(_):
- async_dispatcher_send(
- self.hass,
- PLEX_UPDATE_PLATFORMS_SIGNAL.format(self._server.machine_identifier),
- )
-
- now_playing = []
- for sess in self.sessions:
- if sess.TYPE == "photo":
- _LOGGER.debug("Photo session detected, skipping: %s", sess)
- continue
- if not sess.usernames:
- _LOGGER.debug(
- "Session temporarily incomplete, will try again: %s", sess
- )
- update_failed = True
- continue
- user = sess.usernames[0]
- device = sess.players[0].title
- now_playing_user = f"{user} - {device}"
- now_playing_title = ""
-
- if sess.TYPE == "episode":
- # example:
- # "Supernatural (2005) - s01e13 - Route 666"
-
- def sync_io_attributes(session):
- year = None
- try:
- year = session.show().year
- except TypeError:
- pass
- return (year, session.seasonEpisode)
-
- year, season_episode = await self.hass.async_add_executor_job(
- sync_io_attributes, sess
- )
- season_title = sess.grandparentTitle
- if year is not None:
- season_title += f" ({year!s})"
- episode_title = sess.title
- now_playing_title = (
- f"{season_title} - {season_episode} - {episode_title}"
- )
- elif sess.TYPE == "track":
- # example:
- # "Billy Talent - Afraid of Heights - Afraid of Heights"
- track_artist = sess.grandparentTitle
- track_album = sess.parentTitle
- track_title = sess.title
- now_playing_title = f"{track_artist} - {track_album} - {track_title}"
- elif sess.TYPE == "movie":
- # example:
- # "picture_of_last_summer_camp (2015)"
- # "The Incredible Hulk (2008)"
- now_playing_title = sess.title
- year = await self.hass.async_add_executor_job(getattr, sess, "year")
- if year is not None:
- now_playing_title += f" ({year})"
- else:
- now_playing_title = sess.title
-
- now_playing.append((now_playing_user, now_playing_title))
- self._state = len(self.sessions)
- self._now_playing = now_playing
-
+ self._state = len(self._server.sensor_attributes)
self.async_write_ha_state()
- if update_failed:
- async_call_later(self.hass, 5, update_plex)
-
@property
def name(self):
"""Return the name of the sensor."""
@@ -164,7 +91,7 @@ class PlexSensor(Entity):
@property
def device_state_attributes(self):
"""Return the state attributes."""
- return {content[0]: content[1] for content in self._now_playing}
+ return self._server.sensor_attributes
@property
def device_info(self):
diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py
index baa41cae87f..3834833b740 100644
--- a/homeassistant/components/plex/server.py
+++ b/homeassistant/components/plex/server.py
@@ -37,6 +37,7 @@ from .const import (
GDM_SCANNER,
PLAYER_SOURCE,
PLEX_NEW_MP_SIGNAL,
+ PLEX_UPDATE_MEDIA_PLAYER_SESSION_SIGNAL,
PLEX_UPDATE_MEDIA_PLAYER_SIGNAL,
PLEX_UPDATE_SENSOR_SIGNAL,
PLEXTV_THROTTLE,
@@ -52,6 +53,7 @@ from .errors import (
ShouldUpdateConfigEntry,
)
from .media_search import lookup_movie, lookup_music, lookup_tv
+from .models import PlexSession
_LOGGER = logging.getLogger(__name__)
@@ -71,6 +73,7 @@ class PlexServer:
"""Initialize a Plex server instance."""
self.hass = hass
self.entry_id = entry_id
+ self.active_sessions = {}
self._plex_account = None
self._plex_server = None
self._created_clients = set()
@@ -97,6 +100,7 @@ class PlexServer:
immediate=True,
function=self._async_update_platforms,
).async_call
+ self.thumbnail_cache = {}
# Header conditionally added as it is not available in config entry v1
if CONF_CLIENT_ID in server_config:
@@ -232,7 +236,7 @@ class PlexServer:
raise ShouldUpdateConfigEntry
@callback
- def async_refresh_entity(self, machine_identifier, device, session):
+ def async_refresh_entity(self, machine_identifier, device, session, source):
"""Forward refresh dispatch to media_player."""
unique_id = f"{self.machine_identifier}:{machine_identifier}"
_LOGGER.debug("Refreshing %s", unique_id)
@@ -241,6 +245,64 @@ class PlexServer:
PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(unique_id),
device,
session,
+ source,
+ )
+
+ async def async_update_session(self, payload):
+ """Process a session payload received from a websocket callback."""
+ try:
+ session_payload = payload["PlaySessionStateNotification"][0]
+ except KeyError:
+ await self.async_update_platforms()
+ return
+
+ state = session_payload["state"]
+ if state == "buffering":
+ return
+
+ session_key = int(session_payload["sessionKey"])
+ offset = int(session_payload["viewOffset"])
+ rating_key = int(session_payload["ratingKey"])
+
+ unique_id, active_session = next(
+ (
+ (unique_id, session)
+ for unique_id, session in self.active_sessions.items()
+ if session.session_key == session_key
+ ),
+ (None, None),
+ )
+
+ if not active_session:
+ await self.async_update_platforms()
+ return
+
+ if state == "stopped":
+ self.active_sessions.pop(unique_id, None)
+ else:
+ active_session.state = state
+ active_session.media_position = offset
+
+ def update_with_new_media():
+ """Update an existing session with new media details."""
+ media = self.fetch_item(rating_key)
+ active_session.update_media(media)
+
+ if active_session.media_content_id != rating_key and state in [
+ "playing",
+ "paused",
+ ]:
+ await self.hass.async_add_executor_job(update_with_new_media)
+
+ async_dispatcher_send(
+ self.hass,
+ PLEX_UPDATE_MEDIA_PLAYER_SESSION_SIGNAL.format(unique_id),
+ state,
+ )
+
+ async_dispatcher_send(
+ self.hass,
+ PLEX_UPDATE_SENSOR_SIGNAL.format(self.machine_identifier),
)
def _fetch_platform_data(self):
@@ -321,9 +383,6 @@ class PlexServer:
device.machineIdentifier,
)
- for device in devices:
- process_device("PMS", device)
-
def connect_to_client(source, baseurl, machine_identifier, name="Unknown"):
"""Connect to a Plex client and return a PlexClient instance."""
try:
@@ -384,25 +443,46 @@ class PlexServer:
elif plextv_client.clientIdentifier not in available_clients:
connect_to_resource(plextv_client)
- await self.hass.async_add_executor_job(connect_new_clients)
+ def process_sessions():
+ live_session_keys = {x.sessionKey for x in sessions}
+ for unique_id, session in list(self.active_sessions.items()):
+ if session.session_key not in live_session_keys:
+ _LOGGER.debug("Purging unknown session: %s", session.session_key)
+ self.active_sessions.pop(unique_id)
- for session in sessions:
- if session.TYPE == "photo":
- _LOGGER.debug("Photo session detected, skipping: %s", session)
- continue
-
- session_username = session.usernames[0]
- for player in session.players:
- if session_username and session_username not in monitored_users:
- ignored_clients.add(player.machineIdentifier)
- _LOGGER.debug(
- "Ignoring %s client owned by '%s'",
- player.product,
- session_username,
- )
+ for session in sessions:
+ if session.TYPE == "photo":
+ _LOGGER.debug("Photo session detected, skipping: %s", session)
continue
- process_device("session", player)
- available_clients[player.machineIdentifier]["session"] = session
+
+ session_username = session.usernames[0]
+ for player in session.players:
+ unique_id = f"{self.machine_identifier}:{player.machineIdentifier}"
+ if unique_id not in self.active_sessions:
+ _LOGGER.debug("Creating new Plex session: %s", session)
+ self.active_sessions[unique_id] = PlexSession(self, session)
+ if session_username and session_username not in monitored_users:
+ ignored_clients.add(player.machineIdentifier)
+ _LOGGER.debug(
+ "Ignoring %s client owned by '%s'",
+ player.product,
+ session_username,
+ )
+ continue
+
+ process_device("session", player)
+ available_clients[player.machineIdentifier][
+ "session"
+ ] = self.active_sessions[unique_id]
+
+ for device in devices:
+ process_device("PMS", device)
+
+ def sync_tasks():
+ connect_new_clients()
+ process_sessions()
+
+ await self.hass.async_add_executor_job(sync_tasks)
new_entity_configs = []
for client_id, client_data in available_clients.items():
@@ -413,7 +493,10 @@ class PlexServer:
self._created_clients.add(client_id)
else:
self.async_refresh_entity(
- client_id, client_data["device"], client_data.get("session")
+ client_id,
+ client_data["device"],
+ client_data.get("session"),
+ client_data.get(PLAYER_SOURCE),
)
self._known_clients.update(new_clients | ignored_clients)
@@ -422,7 +505,7 @@ class PlexServer:
self._known_clients - self._known_idle - ignored_clients
).difference(available_clients)
for client_id in idle_clients:
- self.async_refresh_entity(client_id, None, None)
+ self.async_refresh_entity(client_id, None, None, None)
self._known_idle.add(client_id)
self._client_device_cache.pop(client_id, None)
@@ -436,7 +519,6 @@ class PlexServer:
async_dispatcher_send(
self.hass,
PLEX_UPDATE_SENSOR_SIGNAL.format(self.machine_identifier),
- sessions,
)
@property
@@ -571,3 +653,8 @@ class PlexServer:
except MediaNotFound as failed_item:
_LOGGER.error("%s not found in %s", failed_item, library_name)
return None
+
+ @property
+ def sensor_attributes(self):
+ """Return active session information for use in activity sensor."""
+ return {x.sensor_user: x.sensor_title for x in self.active_sessions.values()}
diff --git a/homeassistant/components/plex/translations/sl.json b/homeassistant/components/plex/translations/sl.json
index 5a7ae2db621..b1622219402 100644
--- a/homeassistant/components/plex/translations/sl.json
+++ b/homeassistant/components/plex/translations/sl.json
@@ -21,7 +21,7 @@
"port": "Vrata",
"ssl": "Uporaba SSL",
"token": "\u017deton (izbirno)",
- "verify_ssl": "Preverite SSL potrdilo"
+ "verify_ssl": "Preverite SSL certifikat"
},
"title": "Ro\u010dna konfiguracija Plex"
},
diff --git a/homeassistant/components/plugwise/__init__.py b/homeassistant/components/plugwise/__init__.py
index ee6c44bc558..47a9a1e7d9c 100644
--- a/homeassistant/components/plugwise/__init__.py
+++ b/homeassistant/components/plugwise/__init__.py
@@ -1,16 +1,10 @@
"""Plugwise platform for Home Assistant Core."""
-import asyncio
-
-import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
-from .const import ALL_PLATFORMS, DOMAIN, UNDO_UPDATE_LISTENER
-from .gateway import async_setup_entry_gw
-
-CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA)
+from .gateway import async_setup_entry_gw, async_unload_entry_gw
async def async_setup(hass: HomeAssistant, config: dict):
@@ -27,19 +21,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
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
- ]
- )
- )
-
- hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]()
-
- if unload_ok:
- hass.data[DOMAIN].pop(entry.entry_id)
-
- return unload_ok
+ """Unload the Plugwise components."""
+ if entry.data.get(CONF_HOST):
+ return await async_unload_entry_gw(hass, entry)
+ # PLACEHOLDER USB entry setup
+ return False
diff --git a/homeassistant/components/plugwise/binary_sensor.py b/homeassistant/components/plugwise/binary_sensor.py
index 2ba85326265..825d27d59bb 100644
--- a/homeassistant/components/plugwise/binary_sensor.py
+++ b/homeassistant/components/plugwise/binary_sensor.py
@@ -3,7 +3,6 @@
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 (
@@ -13,13 +12,16 @@ from .const import (
FLOW_OFF_ICON,
FLOW_ON_ICON,
IDLE_ICON,
+ NO_NOTIFICATION_ICON,
+ NOTIFICATION_ICON,
)
-from .sensor import SmileSensor
+from .gateway import SmileGateway
BINARY_SENSOR_MAP = {
"dhw_state": ["Domestic Hot Water State", None],
"slave_boiler_state": ["Secondary Heater Device State", None],
}
+SEVERITIES = ["other", "info", "warning", "error"]
_LOGGER = logging.getLogger(__name__)
@@ -30,12 +32,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR]
entities = []
+ is_thermostat = api.single_master_thermostat()
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 in BINARY_SENSOR_MAP:
if binary_sensor not in data:
continue
@@ -47,32 +50,65 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
device_properties["name"],
dev_id,
binary_sensor,
- device_properties["class"],
)
)
+ if device_properties["class"] == "gateway" and is_thermostat is not None:
+ entities.append(
+ PwNotifySensor(
+ api,
+ coordinator,
+ device_properties["name"],
+ dev_id,
+ "plugwise_notification",
+ )
+ )
+
async_add_entities(entities, True)
-class PwBinarySensor(SmileSensor, BinarySensorEntity):
- """Representation of a Plugwise binary_sensor."""
+class SmileBinarySensor(SmileGateway):
+ """Represent Smile Binary Sensors."""
- def __init__(self, api, coordinator, name, dev_id, binary_sensor, model):
- """Set up the Plugwise API."""
- super().__init__(api, coordinator, name, dev_id, binary_sensor)
+ def __init__(self, api, coordinator, name, dev_id, binary_sensor):
+ """Initialise the binary_sensor."""
+ super().__init__(api, coordinator, name, dev_id)
self._binary_sensor = binary_sensor
- self._is_on = False
self._icon = None
+ self._is_on = False
+
+ if dev_id == self._api.heater_id:
+ self._entity_name = "Auxiliary"
+
+ sensorname = binary_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}-{binary_sensor}"
+ @property
+ def icon(self):
+ """Return the icon of this entity."""
+ return self._icon
+
@property
def is_on(self):
"""Return true if the binary sensor is on."""
return self._is_on
+ @callback
+ def _async_process_data(self):
+ """Update the entity."""
+ raise NotImplementedError
+
+
+class PwBinarySensor(SmileBinarySensor, BinarySensorEntity):
+ """Representation of a Plugwise binary_sensor."""
+
@callback
def _async_process_data(self):
"""Update the entity."""
@@ -89,10 +125,48 @@ class PwBinarySensor(SmileSensor, BinarySensorEntity):
self._is_on = data[self._binary_sensor]
- self._state = STATE_ON if self._is_on else STATE_OFF
if self._binary_sensor == "dhw_state":
self._icon = FLOW_ON_ICON if self._is_on else FLOW_OFF_ICON
if self._binary_sensor == "slave_boiler_state":
self._icon = FLAME_ICON if self._is_on else IDLE_ICON
self.async_write_ha_state()
+
+
+class PwNotifySensor(SmileBinarySensor, BinarySensorEntity):
+ """Representation of a Plugwise Notification binary_sensor."""
+
+ def __init__(self, api, coordinator, name, dev_id, binary_sensor):
+ """Set up the Plugwise API."""
+ super().__init__(api, coordinator, name, dev_id, binary_sensor)
+
+ self._attributes = {}
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes."""
+ return self._attributes
+
+ @callback
+ def _async_process_data(self):
+ """Update the entity."""
+ notify = self._api.notifications
+
+ for severity in SEVERITIES:
+ self._attributes[f"{severity}_msg"] = []
+
+ self._is_on = False
+ self._icon = NO_NOTIFICATION_ICON
+
+ if notify:
+ self._is_on = True
+ self._icon = NOTIFICATION_ICON
+
+ for details in notify.values():
+ for msg_type, msg in details.items():
+ if msg_type not in SEVERITIES:
+ msg_type = "other"
+
+ self._attributes[f"{msg_type.lower()}_msg"].append(msg)
+
+ self.async_write_ha_state()
diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py
index 7981283f27d..c8a2191963e 100644
--- a/homeassistant/components/plugwise/climate.py
+++ b/homeassistant/components/plugwise/climate.py
@@ -2,7 +2,7 @@
import logging
-from Plugwise_Smile.Smile import Smile
+from plugwise.exceptions import PlugwiseException
from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import (
@@ -192,7 +192,7 @@ class PwThermostat(SmileGateway, ClimateEntity):
await self._api.set_temperature(self._loc_id, temperature)
self._setpoint = temperature
self.async_write_ha_state()
- except Smile.PlugwiseError:
+ except PlugwiseException:
_LOGGER.error("Error while communicating to device")
else:
_LOGGER.error("Invalid temperature requested")
@@ -205,7 +205,7 @@ class PwThermostat(SmileGateway, ClimateEntity):
try:
await self._api.set_temperature(self._loc_id, self._schedule_temp)
self._setpoint = self._schedule_temp
- except Smile.PlugwiseError:
+ except PlugwiseException:
_LOGGER.error("Error while communicating to device")
try:
await self._api.set_schedule_state(
@@ -213,7 +213,7 @@ class PwThermostat(SmileGateway, ClimateEntity):
)
self._hvac_mode = hvac_mode
self.async_write_ha_state()
- except Smile.PlugwiseError:
+ except PlugwiseException:
_LOGGER.error("Error while communicating to device")
async def async_set_preset_mode(self, preset_mode):
@@ -223,7 +223,7 @@ class PwThermostat(SmileGateway, ClimateEntity):
self._preset_mode = preset_mode
self._setpoint = self._presets.get(self._preset_mode, "none")[0]
self.async_write_ha_state()
- except Smile.PlugwiseError:
+ except PlugwiseException:
_LOGGER.error("Error while communicating to device")
@callback
diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py
index 6fd7cde44bc..e0d22627737 100644
--- a/homeassistant/components/plugwise/config_flow.py
+++ b/homeassistant/components/plugwise/config_flow.py
@@ -1,7 +1,8 @@
"""Config flow for Plugwise integration."""
import logging
-from Plugwise_Smile.Smile import Smile
+from plugwise.exceptions import InvalidAuthentication, PlugwiseException
+from plugwise.smile import Smile
import voluptuous as vol
from homeassistant import config_entries, core, exceptions
@@ -67,9 +68,9 @@ async def validate_gw_input(hass: core.HomeAssistant, data):
try:
await api.connect()
- except Smile.InvalidAuthentication as err:
+ except InvalidAuthentication as err:
raise InvalidAuth from err
- except Smile.PlugwiseError as err:
+ except PlugwiseException as err:
raise CannotConnect from err
return api
diff --git a/homeassistant/components/plugwise/const.py b/homeassistant/components/plugwise/const.py
index 5c0cf2b097a..c6ef43af602 100644
--- a/homeassistant/components/plugwise/const.py
+++ b/homeassistant/components/plugwise/const.py
@@ -2,7 +2,9 @@
DOMAIN = "plugwise"
SENSOR_PLATFORMS = ["sensor", "switch"]
-ALL_PLATFORMS = ["binary_sensor", "climate", "sensor", "switch"]
+PLATFORMS_GATEWAY = ["binary_sensor", "climate", "sensor", "switch"]
+PW_TYPE = "plugwise_type"
+GATEWAY = "gateway"
# Sensor mapping
SENSOR_MAP_DEVICE_CLASS = 2
@@ -42,6 +44,8 @@ FLOW_OFF_ICON = "mdi:water-pump-off"
FLOW_ON_ICON = "mdi:water-pump"
IDLE_ICON = "mdi:circle-off-outline"
SWITCH_ICON = "mdi:electric-switch"
+NO_NOTIFICATION_ICON = "mdi:mailbox-outline"
+NOTIFICATION_ICON = "mdi:mailbox-up-outline"
COORDINATOR = "coordinator"
UNDO_UPDATE_LISTENER = "undo_update_listener"
diff --git a/homeassistant/components/plugwise/gateway.py b/homeassistant/components/plugwise/gateway.py
index 5ba6eda2770..3b61bd3930d 100644
--- a/homeassistant/components/plugwise/gateway.py
+++ b/homeassistant/components/plugwise/gateway.py
@@ -5,8 +5,13 @@ from datetime import timedelta
import logging
from typing import Dict
-from Plugwise_Smile.Smile import Smile
import async_timeout
+from plugwise.exceptions import (
+ InvalidAuthentication,
+ PlugwiseException,
+ XMLDataMissingError,
+)
+from plugwise.smile import Smile
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
@@ -28,13 +33,15 @@ from homeassistant.helpers.update_coordinator import (
)
from .const import (
- ALL_PLATFORMS,
COORDINATOR,
DEFAULT_PORT,
DEFAULT_SCAN_INTERVAL,
DEFAULT_TIMEOUT,
DEFAULT_USERNAME,
DOMAIN,
+ GATEWAY,
+ PLATFORMS_GATEWAY,
+ PW_TYPE,
SENSOR_PLATFORMS,
UNDO_UPDATE_LISTENER,
)
@@ -64,11 +71,11 @@ async def async_setup_entry_gw(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.error("Unable to connect to Smile")
raise ConfigEntryNotReady
- except Smile.InvalidAuthentication:
+ except InvalidAuthentication:
_LOGGER.error("Invalid username or Smile ID")
return False
- except Smile.PlugwiseError as err:
+ except PlugwiseException as err:
_LOGGER.error("Error while communicating to device %s", api.smile_name)
raise ConfigEntryNotReady from err
@@ -88,7 +95,7 @@ async def async_setup_entry_gw(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async with async_timeout.timeout(DEFAULT_TIMEOUT):
await api.full_update_device()
return True
- except Smile.XMLDataMissingError as err:
+ except XMLDataMissingError as err:
raise UpdateFailed("Smile update failed") from err
coordinator = DataUpdateCoordinator(
@@ -115,6 +122,7 @@ async def async_setup_entry_gw(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
"api": api,
COORDINATOR: coordinator,
+ PW_TYPE: GATEWAY,
UNDO_UPDATE_LISTENER: undo_listener,
}
@@ -130,7 +138,7 @@ async def async_setup_entry_gw(hass: HomeAssistant, entry: ConfigEntry) -> bool:
single_master_thermostat = api.single_master_thermostat()
- platforms = ALL_PLATFORMS
+ platforms = PLATFORMS_GATEWAY
if single_master_thermostat is None:
platforms = SENSOR_PLATFORMS
@@ -150,13 +158,13 @@ async def _update_listener(hass: HomeAssistant, entry: ConfigEntry):
)
-async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
+async def async_unload_entry_gw(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
+ for component in PLATFORMS_GATEWAY
]
)
)
diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json
index f431ce9ee97..5a32341139c 100644
--- a/homeassistant/components/plugwise/manifest.json
+++ b/homeassistant/components/plugwise/manifest.json
@@ -2,8 +2,8 @@
"domain": "plugwise",
"name": "Plugwise",
"documentation": "https://www.home-assistant.io/integrations/plugwise",
- "requirements": ["Plugwise_Smile==1.6.0"],
- "codeowners": ["@CoMPaTech", "@bouwew"],
+ "requirements": ["plugwise==0.8.3"],
+ "codeowners": ["@CoMPaTech", "@bouwew", "@brefra"],
"zeroconf": ["_plugwise._tcp.local."],
"config_flow": true
}
diff --git a/homeassistant/components/plugwise/switch.py b/homeassistant/components/plugwise/switch.py
index 8221bc2cb57..ce3be04681a 100644
--- a/homeassistant/components/plugwise/switch.py
+++ b/homeassistant/components/plugwise/switch.py
@@ -2,7 +2,7 @@
import logging
-from Plugwise_Smile.Smile import Smile
+from plugwise.exceptions import PlugwiseException
from homeassistant.components.switch import SwitchEntity
from homeassistant.core import callback
@@ -14,6 +14,12 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up the Smile switches from a config entry."""
+ # PLACEHOLDER USB entry setup
+ return await async_setup_entry_gateway(hass, config_entry, async_add_entities)
+
+
+async def async_setup_entry_gateway(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]
@@ -37,7 +43,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
model = "Switch Group"
entities.append(
- PwSwitch(
+ GwSwitch(
api, coordinator, device_properties["name"], dev_id, members, model
)
)
@@ -45,7 +51,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities(entities, True)
-class PwSwitch(SmileGateway, SwitchEntity):
+class GwSwitch(SmileGateway, SwitchEntity):
"""Representation of a Plugwise plug."""
def __init__(self, api, coordinator, name, dev_id, members, model):
@@ -79,7 +85,7 @@ class PwSwitch(SmileGateway, SwitchEntity):
if state_on:
self._is_on = True
self.async_write_ha_state()
- except Smile.PlugwiseError:
+ except PlugwiseException:
_LOGGER.error("Error while communicating to device")
async def async_turn_off(self, **kwargs):
@@ -91,7 +97,7 @@ class PwSwitch(SmileGateway, SwitchEntity):
if state_off:
self._is_on = False
self.async_write_ha_state()
- except Smile.PlugwiseError:
+ except PlugwiseException:
_LOGGER.error("Error while communicating to device")
@callback
diff --git a/homeassistant/components/plugwise/translations/ka.json b/homeassistant/components/plugwise/translations/ka.json
new file mode 100644
index 00000000000..d4b446b309c
--- /dev/null
+++ b/homeassistant/components/plugwise/translations/ka.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "flow_type": "\u1c99\u10d0\u10d5\u10e8\u10d8\u10e0\u10d8\u10e1 \u10e2\u10d8\u10de\u10d8"
+ }
+ },
+ "user_gateway": {
+ "data": {
+ "host": "IP \u10db\u10d8\u10e1\u10d0\u10db\u10d0\u10e0\u10d7\u10d8",
+ "password": "Smile ID",
+ "port": "\u10de\u10dd\u10e0\u10e2\u10d8",
+ "username": "\u10e6\u10d8\u10db\u10d8\u10da\u10d8\u10d0\u10dc\u10d8 \u10db\u10dd\u10db\u10ee\u10db\u10d0\u10e0\u10d4\u10d1\u10da\u10d8\u10e1 \u10e1\u10d0\u10ee\u10d4\u10da\u10d8"
+ },
+ "description": "\u10d2\u10d7\u10ee\u10dd\u10d5\u10d7 \u10e8\u10d4\u10d8\u10e7\u10d5\u10d0\u10dc\u10dd\u10d7",
+ "title": "\u10d3\u10d0\u10e3\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d3\u10d8\u10d7 Smile-\u10e1"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/plugwise/translations/sl.json b/homeassistant/components/plugwise/translations/sl.json
new file mode 100644
index 00000000000..8a0996ed92e
--- /dev/null
+++ b/homeassistant/components/plugwise/translations/sl.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "unknown": "Nepri\u010dakovana napaka"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/point/config_flow.py b/homeassistant/components/point/config_flow.py
index 815b872d3e9..aaefc45bc9c 100644
--- a/homeassistant/components/point/config_flow.py
+++ b/homeassistant/components/point/config_flow.py
@@ -101,7 +101,7 @@ class PointFlowHandler(config_entries.ConfigFlow):
return self.async_abort(reason="authorize_url_timeout")
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected error generating auth url")
- return self.async_abort(reason="authorize_url_fail")
+ return self.async_abort(reason="unknown_authorize_url_generation")
return self.async_show_form(
step_id="auth",
description_placeholders={"authorization_url": url},
diff --git a/homeassistant/components/point/strings.json b/homeassistant/components/point/strings.json
index 194121e8e25..8a28e314b69 100644
--- a/homeassistant/components/point/strings.json
+++ b/homeassistant/components/point/strings.json
@@ -23,7 +23,7 @@
"external_setup": "Point successfully configured from another flow.",
"no_flows": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
- "authorize_url_fail": "Unknown error generating an authorize url."
+ "unknown_authorize_url_generation": "[%key:common::config_flow::abort::unknown_authorize_url_generation%]"
}
}
}
diff --git a/homeassistant/components/point/translations/ca.json b/homeassistant/components/point/translations/ca.json
index b0814e56615..158c6addade 100644
--- a/homeassistant/components/point/translations/ca.json
+++ b/homeassistant/components/point/translations/ca.json
@@ -5,7 +5,8 @@
"authorize_url_fail": "S'ha produ\u00eft un error desconegut al generar l'URL d'autoritzaci\u00f3.",
"authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.",
"external_setup": "Point s'ha configurat correctament des d'un altre flux de dades.",
- "no_flows": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3."
+ "no_flows": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.",
+ "unknown_authorize_url_generation": "S'ha produ\u00eft un error desconegut al generar URL d'autoritzaci\u00f3."
},
"create_entry": {
"default": "Autenticaci\u00f3 exitosa"
diff --git a/homeassistant/components/point/translations/cs.json b/homeassistant/components/point/translations/cs.json
index 39670ea6ed5..6dedba8af11 100644
--- a/homeassistant/components/point/translations/cs.json
+++ b/homeassistant/components/point/translations/cs.json
@@ -5,7 +5,8 @@
"authorize_url_fail": "Nezn\u00e1m\u00e1 chyba p\u0159i generov\u00e1n\u00ed autoriza\u010dn\u00ed URL.",
"authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el",
"external_setup": "Point \u00fasp\u011b\u0161n\u011b nastaveno jin\u00fdm zp\u016fsobem.",
- "no_flows": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace."
+ "no_flows": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace.",
+ "unknown_authorize_url_generation": "Nezn\u00e1m\u00e1 chyba p\u0159i generov\u00e1n\u00ed autoriza\u010dn\u00ed URL adresy."
},
"create_entry": {
"default": "\u00dasp\u011b\u0161n\u011b ov\u011b\u0159eno"
diff --git a/homeassistant/components/point/translations/en.json b/homeassistant/components/point/translations/en.json
index cd6731a4bb0..50e9e4f3ce0 100644
--- a/homeassistant/components/point/translations/en.json
+++ b/homeassistant/components/point/translations/en.json
@@ -5,7 +5,8 @@
"authorize_url_fail": "Unknown error generating an authorize url.",
"authorize_url_timeout": "Timeout generating authorize URL.",
"external_setup": "Point successfully configured from another flow.",
- "no_flows": "The component is not configured. Please follow the documentation."
+ "no_flows": "The component is not configured. Please follow the documentation.",
+ "unknown_authorize_url_generation": "Unknown error generating an authorize url."
},
"create_entry": {
"default": "Successfully authenticated"
diff --git a/homeassistant/components/point/translations/es.json b/homeassistant/components/point/translations/es.json
index 5374d2808d9..51c334e794f 100644
--- a/homeassistant/components/point/translations/es.json
+++ b/homeassistant/components/point/translations/es.json
@@ -5,7 +5,8 @@
"authorize_url_fail": "Error desconocido generando la url de autorizaci\u00f3n",
"authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.",
"external_setup": "Point se ha configurado correctamente a partir de otro flujo.",
- "no_flows": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n."
+ "no_flows": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.",
+ "unknown_authorize_url_generation": "Error desconocido al generar una URL de autorizaci\u00f3n."
},
"create_entry": {
"default": "Autenticado correctamente"
diff --git a/homeassistant/components/point/translations/et.json b/homeassistant/components/point/translations/et.json
index 7a26d227c8e..7317e2cd3e3 100644
--- a/homeassistant/components/point/translations/et.json
+++ b/homeassistant/components/point/translations/et.json
@@ -5,7 +5,8 @@
"authorize_url_fail": "Tundmatu viga tuvastamise URL-i loomisel.",
"authorize_url_timeout": "Tuvastamise URL'i loomise ajal\u00f5pp.",
"external_setup": "Point on teisest voost edukalt seadistatud.",
- "no_flows": "Osis pole seadistatud. Palun vaata dokumentatsiooni."
+ "no_flows": "Osis pole seadistatud. Palun vaata dokumentatsiooni.",
+ "unknown_authorize_url_generation": "Tundmatu viga tuvastamise URL-i loomisel."
},
"create_entry": {
"default": "Tuvastamine \u00f5nnestus"
diff --git a/homeassistant/components/point/translations/it.json b/homeassistant/components/point/translations/it.json
index 8f2b5f94c4b..49eb2a760a4 100644
--- a/homeassistant/components/point/translations/it.json
+++ b/homeassistant/components/point/translations/it.json
@@ -5,7 +5,8 @@
"authorize_url_fail": "Errore sconosciuto nel generare l'url di autorizzazione",
"authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.",
"external_setup": "Point configurato correttamente da un altro flusso.",
- "no_flows": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione."
+ "no_flows": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.",
+ "unknown_authorize_url_generation": "Errore sconosciuto durante la generazione di un URL di autorizzazione."
},
"create_entry": {
"default": "Autenticazione riuscita"
diff --git a/homeassistant/components/point/translations/ka.json b/homeassistant/components/point/translations/ka.json
new file mode 100644
index 00000000000..8e555221947
--- /dev/null
+++ b/homeassistant/components/point/translations/ka.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "unknown_authorize_url_generation": "\u10d0\u10d5\u10e2\u10dd\u10e0\u10d8\u10d6\u10d0\u10ea\u10d8\u10d8 \u10d2\u10d4\u10dc\u10d4\u10e0\u10d8\u10e0\u10d4\u10d1\u10d8\u10e1 \u10e3\u10ea\u10dc\u10dd\u10d1\u10d8 \u10e8\u10d4\u10ea\u10d3\u10dd\u10db\u10d0."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/point/translations/no.json b/homeassistant/components/point/translations/no.json
index 2b4f431189e..59dff606f8f 100644
--- a/homeassistant/components/point/translations/no.json
+++ b/homeassistant/components/point/translations/no.json
@@ -5,7 +5,8 @@
"authorize_url_fail": "Ukjent feil ved oppretting av godkjenningsadresse.",
"authorize_url_timeout": "Tidsavbrudd som genererer autorer URL-adresse.",
"external_setup": "Punktet er konfigurert fra en annen flyt.",
- "no_flows": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen."
+ "no_flows": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.",
+ "unknown_authorize_url_generation": "Ukjent feil ved generering av autoriseringsadresse."
},
"create_entry": {
"default": "Vellykket godkjenning"
diff --git a/homeassistant/components/point/translations/pl.json b/homeassistant/components/point/translations/pl.json
index e0624b5ff98..66b81d5675e 100644
--- a/homeassistant/components/point/translations/pl.json
+++ b/homeassistant/components/point/translations/pl.json
@@ -2,10 +2,11 @@
"config": {
"abort": {
"already_setup": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.",
- "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji",
+ "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania URL autoryzacji",
"authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji",
"external_setup": "Punkt pomy\u015blnie skonfigurowany",
- "no_flows": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105."
+ "no_flows": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.",
+ "unknown_authorize_url_generation": "Nieznany b\u0142\u0105d podczas generowania URL autoryzacji"
},
"create_entry": {
"default": "Pomy\u015blnie uwierzytelniono"
diff --git a/homeassistant/components/point/translations/ru.json b/homeassistant/components/point/translations/ru.json
index a8dbb47b400..abb90240871 100644
--- a/homeassistant/components/point/translations/ru.json
+++ b/homeassistant/components/point/translations/ru.json
@@ -5,7 +5,8 @@
"authorize_url_fail": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.",
"authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.",
"external_setup": "Point \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u0438\u0437 \u0434\u0440\u0443\u0433\u043e\u0433\u043e \u043f\u043e\u0442\u043e\u043a\u0430.",
- "no_flows": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438."
+ "no_flows": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438.",
+ "unknown_authorize_url_generation": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438."
},
"create_entry": {
"default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e."
diff --git a/homeassistant/components/point/translations/zh-Hant.json b/homeassistant/components/point/translations/zh-Hant.json
index c4a58a064c4..bbab02f959e 100644
--- a/homeassistant/components/point/translations/zh-Hant.json
+++ b/homeassistant/components/point/translations/zh-Hant.json
@@ -5,7 +5,8 @@
"authorize_url_fail": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4",
"authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002",
"external_setup": "\u5df2\u7531\u5176\u4ed6\u6d41\u7a0b\u6210\u529f\u8a2d\u5b9a Point\u3002",
- "no_flows": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002"
+ "no_flows": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002",
+ "unknown_authorize_url_generation": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002"
},
"create_entry": {
"default": "\u5df2\u6210\u529f\u8a8d\u8b49"
diff --git a/homeassistant/components/profiler/translations/hu.json b/homeassistant/components/profiler/translations/hu.json
new file mode 100644
index 00000000000..bbdd2e5b536
--- /dev/null
+++ b/homeassistant/components/profiler/translations/hu.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva. Csak egyetlen konfigur\u00e1ci\u00f3 lehets\u00e9ges."
+ },
+ "step": {
+ "user": {
+ "description": "El akarja kezdeni a be\u00e1ll\u00edt\u00e1st?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ps4/translations/hu.json b/homeassistant/components/ps4/translations/hu.json
index 7a8623b9030..c3bcabb0d3a 100644
--- a/homeassistant/components/ps4/translations/hu.json
+++ b/homeassistant/components/ps4/translations/hu.json
@@ -1,5 +1,9 @@
{
"config": {
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "login_failed": "Nem siker\u00fclt p\u00e1ros\u00edtani a PlayStation 4-gyel. Ellen\u0151rizze, hogy a helyes-e."
+ },
"step": {
"creds": {
"title": "PlayStation 4"
diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py
index 0bc2a6f6ca8..fb3446fb652 100644
--- a/homeassistant/components/pvoutput/sensor.py
+++ b/homeassistant/components/pvoutput/sensor.py
@@ -53,7 +53,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
verify_ssl = DEFAULT_VERIFY_SSL
headers = {"X-Pvoutput-Apikey": api_key, "X-Pvoutput-SystemId": system_id}
- rest = RestData(method, _ENDPOINT, auth, headers, payload, verify_ssl)
+ rest = RestData(method, _ENDPOINT, auth, headers, None, payload, verify_ssl)
await rest.async_update()
if rest.data is None:
diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py
index a520772ff77..41c56e38db6 100644
--- a/homeassistant/components/rainmachine/__init__.py
+++ b/homeassistant/components/rainmachine/__init__.py
@@ -74,7 +74,7 @@ SERVICE_STOP_PROGRAM_SCHEMA = vol.Schema(
SERVICE_STOP_ZONE_SCHEMA = vol.Schema({vol.Required(CONF_ZONE_ID): cv.positive_int})
-CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.119")
+CONFIG_SCHEMA = cv.deprecated(DOMAIN)
PLATFORMS = ["binary_sensor", "sensor", "switch"]
diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py
index 49eba95d047..80540491ee7 100644
--- a/homeassistant/components/rainmachine/config_flow.py
+++ b/homeassistant/components/rainmachine/config_flow.py
@@ -15,6 +15,14 @@ from .const import ( # pylint: disable=unused-import
DOMAIN,
)
+DATA_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_IP_ADDRESS): str,
+ vol.Required(CONF_PASSWORD): str,
+ vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
+ }
+)
+
class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a RainMachine config flow."""
@@ -22,24 +30,6 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
- def __init__(self):
- """Initialize the config flow."""
- self.data_schema = vol.Schema(
- {
- vol.Required(CONF_IP_ADDRESS): str,
- vol.Required(CONF_PASSWORD): str,
- vol.Optional(CONF_PORT, default=DEFAULT_PORT): 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 if errors else {},
- )
-
@staticmethod
@callback
def async_get_options_flow(config_entry):
@@ -49,7 +39,9 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
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()
+ return self.async_show_form(
+ step_id="user", data_schema=DATA_SCHEMA, errors={}
+ )
await self.async_set_unique_id(user_input[CONF_IP_ADDRESS])
self._abort_if_unique_id_configured()
@@ -65,7 +57,11 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
ssl=user_input.get(CONF_SSL, True),
)
except RainMachineError:
- return await self._show_form({CONF_PASSWORD: "invalid_auth"})
+ return self.async_show_form(
+ step_id="user",
+ data_schema=DATA_SCHEMA,
+ errors={CONF_PASSWORD: "invalid_auth"},
+ )
# Unfortunately, RainMachine doesn't provide a way to refresh the
# access token without using the IP address and password, so we have to
diff --git a/homeassistant/components/rainmachine/translations/hu.json b/homeassistant/components/rainmachine/translations/hu.json
index be68eed63e3..44e24519ca2 100644
--- a/homeassistant/components/rainmachine/translations/hu.json
+++ b/homeassistant/components/rainmachine/translations/hu.json
@@ -10,5 +10,14 @@
"title": "T\u00f6ltsd ki az adataid"
}
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "zone_run_time": "Alap\u00e9rtelmezett z\u00f3nafut\u00e1si id\u0151 (m\u00e1sodpercben)"
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/rainmachine/translations/ka.json b/homeassistant/components/rainmachine/translations/ka.json
new file mode 100644
index 00000000000..cb1c9a8336b
--- /dev/null
+++ b/homeassistant/components/rainmachine/translations/ka.json
@@ -0,0 +1,12 @@
+{
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "zone_run_time": "\u10dc\u10d0\u10d2\u10e3\u10da\u10d8\u10e1\u10ee\u10db\u10d4\u10d5\u10d8 \u10d6\u10dd\u10dc\u10d8\u10e1 \u10d2\u10d0\u10e8\u10d5\u10d4\u10d1\u10d8\u10e1 \u10d3\u10e0\u10dd (\u10ec\u10d0\u10db\u10d4\u10d1\u10e8\u10d8)"
+ },
+ "title": "RainMachine-\u10e1 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/recollect_waste/__init__.py b/homeassistant/components/recollect_waste/__init__.py
index 8ba2fc676f4..57bd346c91b 100644
--- a/homeassistant/components/recollect_waste/__init__.py
+++ b/homeassistant/components/recollect_waste/__init__.py
@@ -1 +1,83 @@
-"""The recollect_waste component."""
+"""The Recollect Waste integration."""
+import asyncio
+from datetime import date, timedelta
+from typing import List
+
+from aiorecollect.client import Client, PickupEvent
+from aiorecollect.errors import RecollectError
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import ConfigEntryNotReady
+from homeassistant.helpers import aiohttp_client
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
+
+from .const import CONF_PLACE_ID, CONF_SERVICE_ID, DATA_COORDINATOR, DOMAIN, LOGGER
+
+DEFAULT_NAME = "recollect_waste"
+DEFAULT_UPDATE_INTERVAL = timedelta(days=1)
+
+PLATFORMS = ["sensor"]
+
+
+async def async_setup(hass: HomeAssistant, config: dict) -> bool:
+ """Set up the RainMachine component."""
+ hass.data[DOMAIN] = {DATA_COORDINATOR: {}}
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+ """Set up RainMachine as config entry."""
+ session = aiohttp_client.async_get_clientsession(hass)
+ client = Client(
+ entry.data[CONF_PLACE_ID], entry.data[CONF_SERVICE_ID], session=session
+ )
+
+ async def async_get_pickup_events() -> List[PickupEvent]:
+ """Get the next pickup."""
+ try:
+ return await client.async_get_pickup_events(
+ start_date=date.today(), end_date=date.today() + timedelta(weeks=4)
+ )
+ except RecollectError as err:
+ raise UpdateFailed(
+ f"Error while requesting data from Recollect: {err}"
+ ) from err
+
+ coordinator = DataUpdateCoordinator(
+ hass,
+ LOGGER,
+ name=f"Place {entry.data[CONF_PLACE_ID]}, Service {entry.data[CONF_SERVICE_ID]}",
+ update_interval=DEFAULT_UPDATE_INTERVAL,
+ update_method=async_get_pickup_events,
+ )
+
+ await coordinator.async_refresh()
+
+ if not coordinator.last_update_success:
+ raise ConfigEntryNotReady
+
+ hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = coordinator
+
+ for component in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, component)
+ )
+
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
+ """Unload an RainMachine 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_COORDINATOR].pop(entry.entry_id)
+
+ return unload_ok
diff --git a/homeassistant/components/recollect_waste/config_flow.py b/homeassistant/components/recollect_waste/config_flow.py
new file mode 100644
index 00000000000..f0d1527a0fb
--- /dev/null
+++ b/homeassistant/components/recollect_waste/config_flow.py
@@ -0,0 +1,64 @@
+"""Config flow for Recollect Waste integration."""
+from aiorecollect.client import Client
+from aiorecollect.errors import RecollectError
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.helpers import aiohttp_client
+
+from .const import ( # pylint:disable=unused-import
+ CONF_PLACE_ID,
+ CONF_SERVICE_ID,
+ DOMAIN,
+ LOGGER,
+)
+
+DATA_SCHEMA = vol.Schema(
+ {vol.Required(CONF_PLACE_ID): str, vol.Required(CONF_SERVICE_ID): str}
+)
+
+
+class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle a config flow for Recollect Waste."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
+ async def async_step_import(self, import_config: dict = None) -> dict:
+ """Handle configuration via YAML import."""
+ return await self.async_step_user(import_config)
+
+ async def async_step_user(self, user_input: dict = None) -> dict:
+ """Handle configuration via the UI."""
+ if user_input is None:
+ return self.async_show_form(
+ step_id="user", data_schema=DATA_SCHEMA, errors={}
+ )
+
+ unique_id = f"{user_input[CONF_PLACE_ID]}, {user_input[CONF_SERVICE_ID]}"
+
+ await self.async_set_unique_id(unique_id)
+ self._abort_if_unique_id_configured()
+
+ session = aiohttp_client.async_get_clientsession(self.hass)
+ client = Client(
+ user_input[CONF_PLACE_ID], user_input[CONF_SERVICE_ID], session=session
+ )
+
+ try:
+ await client.async_get_next_pickup_event()
+ except RecollectError as err:
+ LOGGER.error("Error during setup of integration: %s", err)
+ return self.async_show_form(
+ step_id="user",
+ data_schema=DATA_SCHEMA,
+ errors={"base": "invalid_place_or_service_id"},
+ )
+
+ return self.async_create_entry(
+ title=unique_id,
+ data={
+ CONF_PLACE_ID: user_input[CONF_PLACE_ID],
+ CONF_SERVICE_ID: user_input[CONF_SERVICE_ID],
+ },
+ )
diff --git a/homeassistant/components/recollect_waste/const.py b/homeassistant/components/recollect_waste/const.py
new file mode 100644
index 00000000000..8012bdbb02b
--- /dev/null
+++ b/homeassistant/components/recollect_waste/const.py
@@ -0,0 +1,11 @@
+"""Define Recollect Waste constants."""
+import logging
+
+DOMAIN = "recollect_waste"
+
+LOGGER = logging.getLogger(__package__)
+
+CONF_PLACE_ID = "place_id"
+CONF_SERVICE_ID = "service_id"
+
+DATA_COORDINATOR = "coordinator"
diff --git a/homeassistant/components/recollect_waste/manifest.json b/homeassistant/components/recollect_waste/manifest.json
index bed07f919ef..4e6b71d59b7 100644
--- a/homeassistant/components/recollect_waste/manifest.json
+++ b/homeassistant/components/recollect_waste/manifest.json
@@ -1,7 +1,12 @@
{
"domain": "recollect_waste",
"name": "ReCollect Waste",
+ "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/recollect_waste",
- "requirements": ["aiorecollect==0.2.1"],
- "codeowners": []
+ "requirements": [
+ "aiorecollect==0.2.2"
+ ],
+ "codeowners": [
+ "@bachya"
+ ]
}
diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py
index d360ff8f301..7ce75b1e3fa 100644
--- a/homeassistant/components/recollect_waste/sensor.py
+++ b/homeassistant/components/recollect_waste/sensor.py
@@ -1,27 +1,30 @@
-"""Support for Recollect Waste curbside collection pickup."""
-from datetime import date, timedelta
-import logging
+"""Support for Recollect Waste sensors."""
+from typing import Callable
-from aiorecollect import Client
-from aiorecollect.errors import RecollectError
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import CONF_NAME
-from homeassistant.helpers import aiohttp_client, config_validation as cv
-from homeassistant.helpers.entity import Entity
+from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
+from homeassistant.const import ATTR_ATTRIBUTION
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers.update_coordinator import (
+ CoordinatorEntity,
+ DataUpdateCoordinator,
+)
+
+from .const import CONF_PLACE_ID, CONF_SERVICE_ID, DATA_COORDINATOR, DOMAIN, LOGGER
-_LOGGER = logging.getLogger(__name__)
ATTR_PICKUP_TYPES = "pickup_types"
ATTR_AREA_NAME = "area_name"
ATTR_NEXT_PICKUP_TYPES = "next_pickup_types"
ATTR_NEXT_PICKUP_DATE = "next_pickup_date"
-CONF_PLACE_ID = "place_id"
-CONF_SERVICE_ID = "service_id"
-DEFAULT_NAME = "recollect_waste"
-ICON = "mdi:trash-can-outline"
-SCAN_INTERVAL = timedelta(days=1)
+DEFAULT_ATTRIBUTION = "Pickup data provided by Recollect Waste"
+DEFAULT_NAME = "recollect_waste"
+DEFAULT_ICON = "mdi:trash-can-outline"
+
+CONF_NAME = "name"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
@@ -32,70 +35,88 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
)
-async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Set up the Recollect Waste platform."""
- session = aiohttp_client.async_get_clientsession(hass)
- client = Client(config[CONF_PLACE_ID], config[CONF_SERVICE_ID], session=session)
-
- # Ensure the client can connect to the API successfully
- # with given place_id and service_id.
- try:
- await client.async_get_next_pickup_event()
- except RecollectError as err:
- _LOGGER.error("Error setting up Recollect sensor platform: %s", err)
- return
-
- async_add_entities([RecollectWasteSensor(config.get(CONF_NAME), client)], True)
+async def async_setup_platform(
+ hass: HomeAssistant,
+ config: dict,
+ async_add_entities: Callable,
+ discovery_info: dict = None,
+):
+ """Import Awair configuration from YAML."""
+ LOGGER.warning(
+ "Loading Recollect Waste via platform setup is deprecated. "
+ "Please remove it from your configuration."
+ )
+ hass.async_create_task(
+ hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": SOURCE_IMPORT},
+ data=config,
+ )
+ )
-class RecollectWasteSensor(Entity):
+async def async_setup_entry(
+ hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable
+) -> None:
+ """Set up Recollect Waste sensors based on a config entry."""
+ coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id]
+ async_add_entities([RecollectWasteSensor(coordinator, entry)])
+
+
+class RecollectWasteSensor(CoordinatorEntity):
"""Recollect Waste Sensor."""
- def __init__(self, name, client):
+ def __init__(self, coordinator: DataUpdateCoordinator, entry: ConfigEntry) -> None:
"""Initialize the sensor."""
- self._attributes = {}
- self._name = name
+ super().__init__(coordinator)
+ self._attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
+ self._place_id = entry.data[CONF_PLACE_ID]
+ self._service_id = entry.data[CONF_SERVICE_ID]
self._state = None
- self.client = client
@property
- def name(self):
- """Return the name of the sensor."""
- return self._name
-
- @property
- def unique_id(self) -> str:
- """Return a unique ID."""
- return f"{self.client.place_id}{self.client.service_id}"
-
- @property
- def state(self):
- """Return the state of the sensor."""
- return self._state
-
- @property
- def device_state_attributes(self):
+ def device_state_attributes(self) -> dict:
"""Return the state attributes."""
return self._attributes
@property
- def icon(self):
+ def icon(self) -> str:
"""Icon to use in the frontend."""
- return ICON
+ return DEFAULT_ICON
- async def async_update(self):
- """Update device state."""
- try:
- pickup_event_array = await self.client.async_get_pickup_events(
- start_date=date.today(), end_date=date.today() + timedelta(weeks=4)
- )
- except RecollectError as err:
- _LOGGER.error("Error while requesting data from Recollect: %s", err)
- return
+ @property
+ def name(self) -> str:
+ """Return the name of the sensor."""
+ return DEFAULT_NAME
- pickup_event = pickup_event_array[0]
- next_pickup_event = pickup_event_array[1]
+ @property
+ def state(self) -> str:
+ """Return the state of the sensor."""
+ return self._state
+
+ @property
+ def unique_id(self) -> str:
+ """Return a unique ID."""
+ return f"{self._place_id}{self._service_id}"
+
+ @callback
+ def _handle_coordinator_update(self) -> None:
+ """Respond to a DataUpdateCoordinator update."""
+ self.update_from_latest_data()
+ self.async_write_ha_state()
+
+ async def async_added_to_hass(self) -> None:
+ """Handle entity which will be added."""
+ await super().async_added_to_hass()
+ self.update_from_latest_data()
+
+ @callback
+ def update_from_latest_data(self) -> None:
+ """Update the state."""
+ pickup_event = self.coordinator.data[0]
+ next_pickup_event = self.coordinator.data[1]
next_date = str(next_pickup_event.date)
+
self._state = pickup_event.date
self._attributes.update(
{
diff --git a/homeassistant/components/recollect_waste/strings.json b/homeassistant/components/recollect_waste/strings.json
new file mode 100644
index 00000000000..0cd251c737b
--- /dev/null
+++ b/homeassistant/components/recollect_waste/strings.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "place_id": "Place ID",
+ "service_id": "Service ID"
+ }
+ }
+ },
+ "error": {
+ "invalid_place_or_service_id": "Invalid Place or Service ID"
+ },
+ "abort": {
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
+ }
+ }
+}
diff --git a/homeassistant/components/recollect_waste/translations/en.json b/homeassistant/components/recollect_waste/translations/en.json
new file mode 100644
index 00000000000..28d73d189b8
--- /dev/null
+++ b/homeassistant/components/recollect_waste/translations/en.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Device is already configured"
+ },
+ "error": {
+ "invalid_place_or_service_id": "Invalid Place or Service ID"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "place_id": "Place ID",
+ "service_id": "Service ID"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/recollect_waste/translations/hu.json b/homeassistant/components/recollect_waste/translations/hu.json
new file mode 100644
index 00000000000..112c8cb8385
--- /dev/null
+++ b/homeassistant/components/recollect_waste/translations/hu.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ },
+ "error": {
+ "invalid_place_or_service_id": "\u00c9rv\u00e9nytelen hely vagy szolg\u00e1ltat\u00e1s azonos\u00edt\u00f3"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "place_id": "Hely azonos\u00edt\u00f3ja",
+ "service_id": "Szolg\u00e1ltat\u00e1s azonos\u00edt\u00f3ja"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/recollect_waste/translations/it.json b/homeassistant/components/recollect_waste/translations/it.json
new file mode 100644
index 00000000000..d52e7be1282
--- /dev/null
+++ b/homeassistant/components/recollect_waste/translations/it.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato"
+ },
+ "error": {
+ "invalid_place_or_service_id": "ID luogo o servizio non valido"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "place_id": "ID luogo",
+ "service_id": "ID servizio"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/recollect_waste/translations/ka.json b/homeassistant/components/recollect_waste/translations/ka.json
new file mode 100644
index 00000000000..796ec66cf7f
--- /dev/null
+++ b/homeassistant/components/recollect_waste/translations/ka.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0 \u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0"
+ },
+ "error": {
+ "invalid_place_or_service_id": "\u10d0\u10e0\u10d0\u10e1\u10ec\u10dd\u10e0\u10d8 \u10d0\u10d3\u10d2\u10d8\u10da\u10db\u10d3\u10d4\u10d1\u10d0\u10e0\u10d4\u10dd\u10d1\u10d8\u10e1 \u10d0\u10dc \u10e1\u10d4\u10e0\u10d5\u10d8\u10e1\u10d8\u10e1 ID"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "place_id": "\u10d0\u10d3\u10d2\u10d8\u10da\u10db\u10d3\u10d4\u10d1\u10d0\u10e0\u10d4\u10dd\u10d1\u10d8\u10e1 ID",
+ "service_id": "\u10e1\u10d4\u10e0\u10d5\u10d8\u10e1\u10d8\u10e1 ID"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/recollect_waste/translations/pt.json b/homeassistant/components/recollect_waste/translations/pt.json
new file mode 100644
index 00000000000..57e7ea502f5
--- /dev/null
+++ b/homeassistant/components/recollect_waste/translations/pt.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "service_id": "ID do servi\u00e7o"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/recollect_waste/translations/sl.json b/homeassistant/components/recollect_waste/translations/sl.json
new file mode 100644
index 00000000000..cae09d77621
--- /dev/null
+++ b/homeassistant/components/recollect_waste/translations/sl.json
@@ -0,0 +1,14 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Naprava je \u017ee konfigurirana"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "service_id": "ID storitve"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/recollect_waste/translations/zh-Hans.json b/homeassistant/components/recollect_waste/translations/zh-Hans.json
new file mode 100644
index 00000000000..f4ba15680e4
--- /dev/null
+++ b/homeassistant/components/recollect_waste/translations/zh-Hans.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "service_id": "\u670d\u52a1 ID"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/recollect_waste/translations/zh-Hant.json b/homeassistant/components/recollect_waste/translations/zh-Hant.json
new file mode 100644
index 00000000000..7ce887b05c2
--- /dev/null
+++ b/homeassistant/components/recollect_waste/translations/zh-Hant.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ },
+ "error": {
+ "invalid_place_or_service_id": "\u5730\u9ede\u6216\u670d\u52d9 ID \u7121\u6548"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "place_id": "\u5730\u9ede ID",
+ "service_id": "\u670d\u52d9 ID"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py
index 18d364315b7..0f8a5ae7f8f 100644
--- a/homeassistant/components/recorder/__init__.py
+++ b/homeassistant/components/recorder/__init__.py
@@ -514,6 +514,14 @@ class Recorder(threading.Thread):
self.event_session.expunge(dbstate)
self._pending_expunge = []
self.event_session.commit()
+ except exc.IntegrityError as err:
+ _LOGGER.error(
+ "Integrity error executing query (database likely deleted out from under us): %s",
+ err,
+ )
+ self.event_session.rollback()
+ self._old_states = {}
+ raise
except Exception as err:
_LOGGER.error("Error executing query: %s", err)
self.event_session.rollback()
diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py
index e88852e4a5a..c633c114b46 100644
--- a/homeassistant/components/recorder/migration.py
+++ b/homeassistant/components/recorder/migration.py
@@ -1,12 +1,13 @@
"""Schema migration helpers."""
import logging
-from sqlalchemy import Table, text
+from sqlalchemy import ForeignKeyConstraint, MetaData, Table, text
from sqlalchemy.engine import reflection
from sqlalchemy.exc import InternalError, OperationalError, SQLAlchemyError
+from sqlalchemy.schema import AddConstraint, DropConstraint
from .const import DOMAIN
-from .models import SCHEMA_VERSION, Base, SchemaChanges
+from .models import SCHEMA_VERSION, TABLE_STATES, Base, SchemaChanges
from .util import session_scope
_LOGGER = logging.getLogger(__name__)
@@ -205,6 +206,39 @@ def _add_columns(engine, table_name, columns_def):
)
+def _update_states_table_with_foreign_key_options(engine):
+ """Add the options to foreign key constraints."""
+ inspector = reflection.Inspector.from_engine(engine)
+ alters = []
+ for foreign_key in inspector.get_foreign_keys(TABLE_STATES):
+ if foreign_key["name"] and not foreign_key["options"]:
+ alters.append(
+ {
+ "old_fk": ForeignKeyConstraint((), (), name=foreign_key["name"]),
+ "columns": foreign_key["constrained_columns"],
+ }
+ )
+
+ if not alters:
+ return
+
+ states_key_constraints = Base.metadata.tables[TABLE_STATES].foreign_key_constraints
+ old_states_table = Table( # noqa: F841 pylint: disable=unused-variable
+ TABLE_STATES, MetaData(), *[alter["old_fk"] for alter in alters]
+ )
+
+ for alter in alters:
+ try:
+ engine.execute(DropConstraint(alter["old_fk"]))
+ for fkc in states_key_constraints:
+ if fkc.column_keys == alter["columns"]:
+ engine.execute(AddConstraint(fkc))
+ except (InternalError, OperationalError):
+ _LOGGER.exception(
+ "Could not update foreign options in %s table", TABLE_STATES
+ )
+
+
def _apply_update(engine, new_version, old_version):
"""Perform operations to bring schema up to date."""
if new_version == 1:
@@ -277,6 +311,8 @@ def _apply_update(engine, new_version, old_version):
_drop_index(engine, "states", "ix_states_entity_id")
_create_index(engine, "events", "ix_events_event_type_time_fired")
_drop_index(engine, "events", "ix_events_event_type")
+ elif new_version == 10:
+ _update_states_table_with_foreign_key_options(engine)
else:
raise ValueError(f"No schema migration defined for version {new_version}")
diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py
index 4756ac13ce3..5b37f7e3f9d 100644
--- a/homeassistant/components/recorder/models.py
+++ b/homeassistant/components/recorder/models.py
@@ -25,7 +25,7 @@ import homeassistant.util.dt as dt_util
# pylint: disable=invalid-name
Base = declarative_base()
-SCHEMA_VERSION = 9
+SCHEMA_VERSION = 10
_LOGGER = logging.getLogger(__name__)
@@ -36,12 +36,16 @@ TABLE_STATES = "states"
TABLE_RECORDER_RUNS = "recorder_runs"
TABLE_SCHEMA_CHANGES = "schema_changes"
-ALL_TABLES = [TABLE_EVENTS, TABLE_STATES, TABLE_RECORDER_RUNS, TABLE_SCHEMA_CHANGES]
+ALL_TABLES = [TABLE_STATES, TABLE_EVENTS, TABLE_RECORDER_RUNS, TABLE_SCHEMA_CHANGES]
class Events(Base): # type: ignore
"""Event history data."""
+ __table_args__ = {
+ "mysql_default_charset": "utf8mb4",
+ "mysql_collate": "utf8mb4_unicode_ci",
+ }
__tablename__ = TABLE_EVENTS
event_id = Column(Integer, primary_key=True)
event_type = Column(String(32))
@@ -96,17 +100,25 @@ class Events(Base): # type: ignore
class States(Base): # type: ignore
"""State change history."""
+ __table_args__ = {
+ "mysql_default_charset": "utf8mb4",
+ "mysql_collate": "utf8mb4_unicode_ci",
+ }
__tablename__ = TABLE_STATES
state_id = Column(Integer, primary_key=True)
domain = Column(String(64))
entity_id = Column(String(255))
state = Column(String(255))
attributes = Column(Text)
- event_id = Column(Integer, ForeignKey("events.event_id"), index=True)
+ event_id = Column(
+ Integer, ForeignKey("events.event_id", ondelete="CASCADE"), index=True
+ )
last_changed = Column(DateTime(timezone=True), default=dt_util.utcnow)
last_updated = Column(DateTime(timezone=True), default=dt_util.utcnow, index=True)
created = Column(DateTime(timezone=True), default=dt_util.utcnow)
- old_state_id = Column(Integer, ForeignKey("states.state_id"))
+ old_state_id = Column(
+ Integer, ForeignKey("states.state_id", ondelete="SET NULL"), index=True
+ )
event = relationship("Events", uselist=False)
old_state = relationship("States", remote_side=[state_id])
diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py
index fee4480e134..43e84785f7d 100644
--- a/homeassistant/components/recorder/purge.py
+++ b/homeassistant/components/recorder/purge.py
@@ -68,6 +68,7 @@ def purge_old_data(instance, purge_days: int, repack: bool) -> bool:
deleted_rows = (
session.query(RecorderRuns)
.filter(RecorderRuns.start < purge_before)
+ .filter(RecorderRuns.run_id != instance.run_info.run_id)
.delete(synchronize_session=False)
)
_LOGGER.debug("Deleted %s recorder_runs", deleted_rows)
diff --git a/homeassistant/components/rest/binary_sensor.py b/homeassistant/components/rest/binary_sensor.py
index c19bfe307d0..7f0f920b843 100644
--- a/homeassistant/components/rest/binary_sensor.py
+++ b/homeassistant/components/rest/binary_sensor.py
@@ -14,6 +14,7 @@ from homeassistant.const import (
CONF_HEADERS,
CONF_METHOD,
CONF_NAME,
+ CONF_PARAMS,
CONF_PASSWORD,
CONF_PAYLOAD,
CONF_RESOURCE,
@@ -45,6 +46,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
[HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]
),
vol.Optional(CONF_HEADERS): {cv.string: cv.string},
+ vol.Optional(CONF_PARAMS): {cv.string: cv.string},
vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(["POST", "GET"]),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
@@ -78,6 +80,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
headers = config.get(CONF_HEADERS)
+ params = config.get(CONF_PARAMS)
device_class = config.get(CONF_DEVICE_CLASS)
value_template = config.get(CONF_VALUE_TEMPLATE)
force_update = config.get(CONF_FORCE_UPDATE)
@@ -97,7 +100,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
else:
auth = None
- rest = RestData(method, resource, auth, headers, payload, verify_ssl, timeout)
+ rest = RestData(
+ method, resource, auth, headers, params, payload, verify_ssl, timeout
+ )
await rest.async_update()
if rest.data is None:
raise PlatformNotReady
diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py
index 9d9e802c2a0..bd35383e981 100644
--- a/homeassistant/components/rest/data.py
+++ b/homeassistant/components/rest/data.py
@@ -17,6 +17,7 @@ class RestData:
resource,
auth,
headers,
+ params,
data,
verify_ssl,
timeout=DEFAULT_TIMEOUT,
@@ -26,6 +27,7 @@ class RestData:
self._resource = resource
self._auth = auth
self._headers = headers
+ self._params = params
self._request_data = data
self._timeout = timeout
self._verify_ssl = verify_ssl
@@ -53,6 +55,7 @@ class RestData:
self._method,
self._resource,
headers=self._headers,
+ params=self._params,
auth=self._auth,
data=self._request_data,
timeout=self._timeout,
diff --git a/homeassistant/components/rest/notify.py b/homeassistant/components/rest/notify.py
index b8f81b19e92..3e4f97d5bc7 100644
--- a/homeassistant/components/rest/notify.py
+++ b/homeassistant/components/rest/notify.py
@@ -17,6 +17,7 @@ from homeassistant.const import (
CONF_HEADERS,
CONF_METHOD,
CONF_NAME,
+ CONF_PARAMS,
CONF_PASSWORD,
CONF_RESOURCE,
CONF_USERNAME,
@@ -51,6 +52,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
["POST", "GET", "POST_JSON"]
),
vol.Optional(CONF_HEADERS): vol.Schema({cv.string: cv.string}),
+ vol.Optional(CONF_PARAMS): vol.Schema({cv.string: cv.string}),
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_TARGET_PARAMETER_NAME): cv.string,
vol.Optional(CONF_TITLE_PARAMETER_NAME): cv.string,
@@ -75,6 +77,7 @@ def get_service(hass, config, discovery_info=None):
resource = config.get(CONF_RESOURCE)
method = config.get(CONF_METHOD)
headers = config.get(CONF_HEADERS)
+ params = config.get(CONF_PARAMS)
message_param_name = config.get(CONF_MESSAGE_PARAMETER_NAME)
title_param_name = config.get(CONF_TITLE_PARAMETER_NAME)
target_param_name = config.get(CONF_TARGET_PARAMETER_NAME)
@@ -97,6 +100,7 @@ def get_service(hass, config, discovery_info=None):
resource,
method,
headers,
+ params,
message_param_name,
title_param_name,
target_param_name,
@@ -116,6 +120,7 @@ class RestNotificationService(BaseNotificationService):
resource,
method,
headers,
+ params,
message_param_name,
title_param_name,
target_param_name,
@@ -129,6 +134,7 @@ class RestNotificationService(BaseNotificationService):
self._hass = hass
self._method = method.upper()
self._headers = headers
+ self._params = params
self._message_param_name = message_param_name
self._title_param_name = title_param_name
self._target_param_name = target_param_name
@@ -171,6 +177,7 @@ class RestNotificationService(BaseNotificationService):
response = requests.post(
self._resource,
headers=self._headers,
+ params=self._params,
data=data,
timeout=10,
auth=self._auth,
@@ -180,6 +187,7 @@ class RestNotificationService(BaseNotificationService):
response = requests.post(
self._resource,
headers=self._headers,
+ params=self._params,
json=data,
timeout=10,
auth=self._auth,
@@ -189,7 +197,7 @@ class RestNotificationService(BaseNotificationService):
response = requests.get(
self._resource,
headers=self._headers,
- params=data,
+ params=self._params.update(data),
timeout=10,
auth=self._auth,
verify=self._verify_ssl,
diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py
index 826160604ba..f048eaa3b47 100644
--- a/homeassistant/components/rest/sensor.py
+++ b/homeassistant/components/rest/sensor.py
@@ -16,6 +16,7 @@ from homeassistant.const import (
CONF_HEADERS,
CONF_METHOD,
CONF_NAME,
+ CONF_PARAMS,
CONF_PASSWORD,
CONF_PAYLOAD,
CONF_RESOURCE,
@@ -56,6 +57,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
[HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]
),
vol.Optional(CONF_HEADERS): vol.Schema({cv.string: cv.string}),
+ vol.Optional(CONF_PARAMS): vol.Schema({cv.string: cv.string}),
vol.Optional(CONF_JSON_ATTRS, default=[]): cv.ensure_list_csv,
vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(METHODS),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
@@ -90,6 +92,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
headers = config.get(CONF_HEADERS)
+ params = config.get(CONF_PARAMS)
unit = config.get(CONF_UNIT_OF_MEASUREMENT)
device_class = config.get(CONF_DEVICE_CLASS)
value_template = config.get(CONF_VALUE_TEMPLATE)
@@ -112,7 +115,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
auth = (username, password)
else:
auth = None
- rest = RestData(method, resource, auth, headers, payload, verify_ssl, timeout)
+ rest = RestData(
+ method, resource, auth, headers, params, payload, verify_ssl, timeout
+ )
await rest.async_update()
if rest.data is None:
diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py
index 1b980e12b75..b6bd759d0bf 100644
--- a/homeassistant/components/rest/switch.py
+++ b/homeassistant/components/rest/switch.py
@@ -11,6 +11,7 @@ from homeassistant.const import (
CONF_HEADERS,
CONF_METHOD,
CONF_NAME,
+ CONF_PARAMS,
CONF_PASSWORD,
CONF_RESOURCE,
CONF_TIMEOUT,
@@ -46,6 +47,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
vol.Required(CONF_RESOURCE): cv.url,
vol.Optional(CONF_STATE_RESOURCE): cv.url,
vol.Optional(CONF_HEADERS): {cv.string: cv.string},
+ vol.Optional(CONF_PARAMS): {cv.string: cv.string},
vol.Optional(CONF_BODY_OFF, default=DEFAULT_BODY_OFF): cv.template,
vol.Optional(CONF_BODY_ON, default=DEFAULT_BODY_ON): cv.template,
vol.Optional(CONF_IS_ON_TEMPLATE): cv.template,
@@ -71,6 +73,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
is_on_template = config.get(CONF_IS_ON_TEMPLATE)
method = config.get(CONF_METHOD)
headers = config.get(CONF_HEADERS)
+ params = config.get(CONF_PARAMS)
name = config.get(CONF_NAME)
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
@@ -97,6 +100,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
state_resource,
method,
headers,
+ params,
auth,
body_on,
body_off,
@@ -129,6 +133,7 @@ class RestSwitch(SwitchEntity):
state_resource,
method,
headers,
+ params,
auth,
body_on,
body_off,
@@ -143,6 +148,7 @@ class RestSwitch(SwitchEntity):
self._state_resource = state_resource
self._method = method
self._headers = headers
+ self._params = params
self._auth = auth
self._body_on = body_on
self._body_off = body_off
@@ -201,6 +207,7 @@ class RestSwitch(SwitchEntity):
auth=self._auth,
data=bytes(body, "utf-8"),
headers=self._headers,
+ params=self._params,
)
return req
@@ -219,7 +226,10 @@ class RestSwitch(SwitchEntity):
with async_timeout.timeout(self._timeout):
req = await websession.get(
- self._state_resource, auth=self._auth, headers=self._headers
+ self._state_resource,
+ auth=self._auth,
+ headers=self._headers,
+ params=self._params,
)
text = await req.text()
diff --git a/homeassistant/components/rflink/manifest.json b/homeassistant/components/rflink/manifest.json
index 266092581f0..cdcfe97c219 100644
--- a/homeassistant/components/rflink/manifest.json
+++ b/homeassistant/components/rflink/manifest.json
@@ -2,6 +2,6 @@
"domain": "rflink",
"name": "RFLink",
"documentation": "https://www.home-assistant.io/integrations/rflink",
- "requirements": ["rflink==0.0.54"],
+ "requirements": ["rflink==0.0.55"],
"codeowners": []
}
diff --git a/homeassistant/components/rfxtrx/translations/hu.json b/homeassistant/components/rfxtrx/translations/hu.json
index 3b2d79a34a7..964b143f1d5 100644
--- a/homeassistant/components/rfxtrx/translations/hu.json
+++ b/homeassistant/components/rfxtrx/translations/hu.json
@@ -3,5 +3,15 @@
"abort": {
"already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
}
+ },
+ "options": {
+ "step": {
+ "prompt_options": {
+ "data": {
+ "remove_device": "V\u00e1lassza ki a t\u00f6r\u00f6lni k\u00edv\u00e1nt eszk\u00f6zt"
+ },
+ "title": "Rfxtrx opci\u00f3k"
+ }
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/rfxtrx/translations/it.json b/homeassistant/components/rfxtrx/translations/it.json
index a97cfdd2ce4..ff705fdd0a2 100644
--- a/homeassistant/components/rfxtrx/translations/it.json
+++ b/homeassistant/components/rfxtrx/translations/it.json
@@ -5,13 +5,9 @@
"cannot_connect": "Impossibile connettersi"
},
"error": {
- "cannot_connect": "Impossibile connettersi",
- "one": "uno",
- "other": "altri"
+ "cannot_connect": "Impossibile connettersi"
},
"step": {
- "one": "uno",
- "other": "altri",
"setup_network": {
"data": {
"host": "Host",
@@ -39,7 +35,6 @@
}
}
},
- "one": "uno",
"options": {
"error": {
"already_configured_device": "Il dispositivo \u00e8 gi\u00e0 configurato",
@@ -74,6 +69,5 @@
"title": "Configurare le opzioni del dispositivo"
}
}
- },
- "other": "altri"
+ }
}
\ No newline at end of file
diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py
index a313bcf03ba..bd5950b81a9 100644
--- a/homeassistant/components/ring/camera.py
+++ b/homeassistant/components/ring/camera.py
@@ -103,7 +103,7 @@ class RingCam(RingEntityMixin, Camera):
async def async_camera_image(self):
"""Return a still image response from the camera."""
- ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop)
+ ffmpeg = ImageFrame(self._ffmpeg.binary)
if self._video_url is None:
return
@@ -121,7 +121,7 @@ class RingCam(RingEntityMixin, Camera):
if self._video_url is None:
return
- stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop)
+ stream = CameraMjpeg(self._ffmpeg.binary)
await stream.open_camera(self._video_url)
try:
diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json
index d46f12af511..550da4d38ec 100644
--- a/homeassistant/components/ring/manifest.json
+++ b/homeassistant/components/ring/manifest.json
@@ -2,7 +2,7 @@
"domain": "ring",
"name": "Ring",
"documentation": "https://www.home-assistant.io/integrations/ring",
- "requirements": ["ring_doorbell==0.6.0"],
+ "requirements": ["ring_doorbell==0.6.2"],
"dependencies": ["ffmpeg"],
"codeowners": ["@balloob"],
"config_flow": true
diff --git a/homeassistant/components/risco/translations/pl.json b/homeassistant/components/risco/translations/pl.json
index 92deb2da70a..ef7ed9f13e0 100644
--- a/homeassistant/components/risco/translations/pl.json
+++ b/homeassistant/components/risco/translations/pl.json
@@ -28,7 +28,7 @@
"armed_night": "Uzbrojony (noc)"
},
"description": "Wybierz stan, w kt\u00f3rym chcesz ustawi\u0107 alarm Risco podczas uzbrajania alarmu w Home Assistant",
- "title": "Mapuj stany Home Assistant do stan\u00f3w alarmu Risco"
+ "title": "Mapuj stany Home Assistanta do stan\u00f3w alarmu Risco"
},
"init": {
"data": {
@@ -47,8 +47,8 @@
"arm": "Uzbrojony (pod nieobecno\u015b\u0107)",
"partial_arm": "Uzbrojony (obecny)"
},
- "description": "Wybierz stan, kt\u00f3ry b\u0119dzie raportowa\u0142 alarm Home Assistant, dla ka\u017cdego stanu zg\u0142oszonego przez Risco",
- "title": "Mapuj stany Risco do stan\u00f3w Home Assistant"
+ "description": "Wybierz stan, kt\u00f3ry b\u0119dzie raportowa\u0142 alarm Home Assistanta, dla ka\u017cdego stanu zg\u0142oszonego przez Risco",
+ "title": "Mapuj stany Risco do stan\u00f3w Home Assistanta"
}
}
}
diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py
index 739e345a637..af2e0ee946f 100644
--- a/homeassistant/components/roku/__init__.py
+++ b/homeassistant/components/roku/__init__.py
@@ -30,7 +30,7 @@ from .const import (
DOMAIN,
)
-CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.120")
+CONFIG_SCHEMA = cv.deprecated(DOMAIN)
PLATFORMS = [MEDIA_PLAYER_DOMAIN, REMOTE_DOMAIN]
SCAN_INTERVAL = timedelta(seconds=15)
diff --git a/homeassistant/components/roku/translations/it.json b/homeassistant/components/roku/translations/it.json
index abbe29fb2a7..007be91d155 100644
--- a/homeassistant/components/roku/translations/it.json
+++ b/homeassistant/components/roku/translations/it.json
@@ -10,10 +10,6 @@
"flow_title": "Roku: {name}",
"step": {
"ssdp_confirm": {
- "data": {
- "one": "uno",
- "other": "altri"
- },
"description": "Vuoi impostare {name}?",
"title": "Roku"
},
diff --git a/homeassistant/components/roomba/__init__.py b/homeassistant/components/roomba/__init__.py
index 912b134d454..be85ec3619f 100644
--- a/homeassistant/components/roomba/__init__.py
+++ b/homeassistant/components/roomba/__init__.py
@@ -3,7 +3,7 @@ import asyncio
import logging
import async_timeout
-from roomba import Roomba, RoombaConnectionError
+from roombapy import Roomba, RoombaConnectionError
import voluptuous as vol
from homeassistant import config_entries, exceptions
diff --git a/homeassistant/components/roomba/config_flow.py b/homeassistant/components/roomba/config_flow.py
index b25c4ece440..166b5992d86 100644
--- a/homeassistant/components/roomba/config_flow.py
+++ b/homeassistant/components/roomba/config_flow.py
@@ -1,5 +1,5 @@
"""Config flow to configure roomba component."""
-from roomba import Roomba
+from roombapy import Roomba
import voluptuous as vol
from homeassistant import config_entries, core
diff --git a/homeassistant/components/roomba/irobot_base.py b/homeassistant/components/roomba/irobot_base.py
index 8bc1e22547f..7dd045a1137 100644
--- a/homeassistant/components/roomba/irobot_base.py
+++ b/homeassistant/components/roomba/irobot_base.py
@@ -51,6 +51,7 @@ SUPPORT_IROBOT = (
STATE_MAP = {
"": STATE_IDLE,
"charge": STATE_DOCKED,
+ "evac": STATE_RETURNING, # Emptying at cleanbase
"hmMidMsn": STATE_CLEANING, # Recharging at the middle of a cycle
"hmPostMsn": STATE_RETURNING, # Cycle finished
"hmUsrDock": STATE_RETURNING,
diff --git a/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json
index 9b0e3f21983..808c7eb9432 100644
--- a/homeassistant/components/roomba/manifest.json
+++ b/homeassistant/components/roomba/manifest.json
@@ -3,6 +3,6 @@
"name": "iRobot Roomba",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/roomba",
- "requirements": ["roombapy==1.6.1"],
+ "requirements": ["roombapy==1.6.2"],
"codeowners": ["@pschmitt", "@cyr-ius", "@shenxn"]
}
diff --git a/homeassistant/components/rpi_power/manifest.json b/homeassistant/components/rpi_power/manifest.json
index e0d2a6424e8..1b355711535 100644
--- a/homeassistant/components/rpi_power/manifest.json
+++ b/homeassistant/components/rpi_power/manifest.json
@@ -7,7 +7,7 @@
"@swetoast"
],
"requirements": [
- "rpi-bad-power==0.0.3"
+ "rpi-bad-power==0.1.0"
],
"config_flow": true
}
diff --git a/homeassistant/components/rpi_power/translations/ko.json b/homeassistant/components/rpi_power/translations/ko.json
index 02271833220..b9a9a1be643 100644
--- a/homeassistant/components/rpi_power/translations/ko.json
+++ b/homeassistant/components/rpi_power/translations/ko.json
@@ -10,5 +10,5 @@
}
}
},
- "title": "\ub77c\uc988\ubca0\ub9ac\ud30c\uc774 \uc804\uc6d0 \uacf5\uae09 \uc7a5\uce58 \uac80\uc0ac\uae30"
+ "title": "Raspberry Pi \uc804\uc6d0 \uacf5\uae09 \uac80\uc0ac"
}
\ No newline at end of file
diff --git a/homeassistant/components/ruckus_unleashed/translations/hu.json b/homeassistant/components/ruckus_unleashed/translations/hu.json
new file mode 100644
index 00000000000..c1a23478ac4
--- /dev/null
+++ b/homeassistant/components/ruckus_unleashed/translations/hu.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "unknown": "V\u00e1ratlan hiba"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Gazdag\u00e9p",
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ruckus_unleashed/translations/ka.json b/homeassistant/components/ruckus_unleashed/translations/ka.json
new file mode 100644
index 00000000000..980dc52ca82
--- /dev/null
+++ b/homeassistant/components/ruckus_unleashed/translations/ka.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0 \u10e3\u10d9\u10d5\u10d4 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0"
+ },
+ "error": {
+ "cannot_connect": "\u10db\u10d0\u10e0\u10ea\u10ee\u10d8 \u10e8\u10d4\u10d4\u10e0\u10d7\u10d4\u10d1\u10d8\u10e1\u10d0\u10e1",
+ "invalid_auth": "\u10db\u10ea\u10d3\u10d0\u10e0\u10d8 \u10d0\u10d5\u10e2\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0",
+ "unknown": "\u10db\u10dd\u10e3\u10da\u10dd\u10d3\u10dc\u10d4\u10da\u10d8 \u10e8\u10d4\u10ea\u10d3\u10dd\u10db\u10d0"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u10f0\u10dd\u10e1\u10e2\u10d8",
+ "password": "\u10de\u10d0\u10e0\u10dd\u10da\u10d8",
+ "username": "\u10db\u10dd\u10db\u10ee\u10db\u10d0\u10e0\u10d4\u10d1\u10d4\u10da\u10d8"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ruckus_unleashed/translations/sl.json b/homeassistant/components/ruckus_unleashed/translations/sl.json
new file mode 100644
index 00000000000..6e82b8dd9a3
--- /dev/null
+++ b/homeassistant/components/ruckus_unleashed/translations/sl.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "error": {
+ "invalid_auth": "Neveljavna avtentikacija",
+ "unknown": "Nepri\u010dakovana napaka"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Gostitelj",
+ "password": "Geslo",
+ "username": "Uporabni\u0161ko ime"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/salt/__init__.py b/homeassistant/components/salt/__init__.py
deleted file mode 100644
index 29c371ece52..00000000000
--- a/homeassistant/components/salt/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""The salt component."""
diff --git a/homeassistant/components/salt/device_tracker.py b/homeassistant/components/salt/device_tracker.py
deleted file mode 100644
index 7c03403622a..00000000000
--- a/homeassistant/components/salt/device_tracker.py
+++ /dev/null
@@ -1,71 +0,0 @@
-"""Support for Salt Fiber Box routers."""
-import logging
-
-from saltbox import RouterLoginException, RouterNotReachableException, SaltBox
-import voluptuous as vol
-
-from homeassistant.components.device_tracker import (
- DOMAIN,
- PLATFORM_SCHEMA,
- DeviceScanner,
-)
-from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
-import homeassistant.helpers.config_validation as cv
-
-_LOGGER = logging.getLogger(__name__)
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Required(CONF_HOST): cv.string,
- vol.Required(CONF_USERNAME): cv.string,
- vol.Required(CONF_PASSWORD): cv.string,
- }
-)
-
-
-def get_scanner(hass, config):
- """Return the Salt device scanner."""
- scanner = SaltDeviceScanner(config[DOMAIN])
-
- # Test whether the router is accessible.
- data = scanner.get_salt_data()
- return scanner if data is not None else None
-
-
-class SaltDeviceScanner(DeviceScanner):
- """This class queries a Salt Fiber Box router."""
-
- def __init__(self, config):
- """Initialize the scanner."""
- host = config[CONF_HOST]
- username = config[CONF_USERNAME]
- password = config[CONF_PASSWORD]
- self.saltbox = SaltBox(f"http://{host}", username, password)
- self.online_clients = []
-
- def scan_devices(self):
- """Scan for new devices and return a list with found device IDs."""
- self._update_info()
- return [client["mac"] for client in self.online_clients]
-
- def get_device_name(self, device):
- """Return the name of the given device or None if we don't know."""
- for client in self.online_clients:
- if client["mac"] == device:
- return client["name"]
- return None
-
- def get_salt_data(self):
- """Retrieve data from Salt router and return parsed result."""
- try:
- clients = self.saltbox.get_online_clients()
- return clients
- except (RouterLoginException, RouterNotReachableException) as error:
- _LOGGER.warning(error)
- return None
-
- def _update_info(self):
- """Pull the current information from the Salt router."""
- _LOGGER.debug("Loading data from Salt Fiber Box")
- data = self.get_salt_data()
- self.online_clients = data or []
diff --git a/homeassistant/components/salt/manifest.json b/homeassistant/components/salt/manifest.json
deleted file mode 100644
index cad9b6d3661..00000000000
--- a/homeassistant/components/salt/manifest.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "domain": "salt",
- "name": "Salt Fiber Box",
- "documentation": "https://www.home-assistant.io/integrations/salt",
- "requirements": ["saltbox==0.1.3"],
- "codeowners": ["@bjornorri"]
-}
diff --git a/homeassistant/components/samsungtv/translations/hu.json b/homeassistant/components/samsungtv/translations/hu.json
index 15eabd20d3d..ca42aff331a 100644
--- a/homeassistant/components/samsungtv/translations/hu.json
+++ b/homeassistant/components/samsungtv/translations/hu.json
@@ -4,6 +4,7 @@
"already_configured": "Ez a Samsung TV m\u00e1r konfigur\u00e1lva van.",
"already_in_progress": "A Samsung TV konfigur\u00e1l\u00e1sa m\u00e1r folyamatban van.",
"auth_missing": "A Home Assistant nem jogosult csatlakozni ehhez a Samsung TV-hez. Ellen\u0151rizd a TV be\u00e1ll\u00edt\u00e1sait a Home Assistant enged\u00e9lyez\u00e9s\u00e9hez.",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
"not_supported": "Ez a Samsung TV k\u00e9sz\u00fcl\u00e9k jelenleg nem t\u00e1mogatott."
},
"flow_title": "Samsung TV: {model}",
diff --git a/homeassistant/components/samsungtv/translations/pl.json b/homeassistant/components/samsungtv/translations/pl.json
index 0257827abed..07751797f85 100644
--- a/homeassistant/components/samsungtv/translations/pl.json
+++ b/homeassistant/components/samsungtv/translations/pl.json
@@ -10,7 +10,7 @@
"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, 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": {
@@ -18,7 +18,7 @@
"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."
+ "description": "Wprowad\u017a informacje o telewizorze Samsung. 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."
}
}
}
diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py
index 613151511ae..b76995fe39f 100644
--- a/homeassistant/components/scrape/sensor.py
+++ b/homeassistant/components/scrape/sensor.py
@@ -78,7 +78,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
auth = HTTPBasicAuth(username, password)
else:
auth = None
- rest = RestData(method, resource, auth, headers, payload, verify_ssl)
+ rest = RestData(method, resource, auth, headers, None, payload, verify_ssl)
await rest.async_update()
if rest.data is None:
diff --git a/homeassistant/components/scsgate/__init__.py b/homeassistant/components/scsgate/__init__.py
index f64106049de..6a190e64b7d 100644
--- a/homeassistant/components/scsgate/__init__.py
+++ b/homeassistant/components/scsgate/__init__.py
@@ -131,7 +131,7 @@ class SCSGate:
with self._devices_to_register_lock:
while self._devices_to_register:
- _, device = self._devices_to_register.popitem()
+ device = self._devices_to_register.popitem()[1]
self._devices[device.scs_id] = device
self._device_being_registered = device.scs_id
self._reactor.append_task(GetStatusTask(target=device.scs_id))
diff --git a/homeassistant/components/sensehat/sensor.py b/homeassistant/components/sensehat/sensor.py
index 3966e52f1a8..67beb021d89 100644
--- a/homeassistant/components/sensehat/sensor.py
+++ b/homeassistant/components/sensehat/sensor.py
@@ -1,7 +1,7 @@
"""Support for Sense HAT sensors."""
from datetime import timedelta
import logging
-import os
+from pathlib import Path
from sense_hat import SenseHat
import voluptuous as vol
@@ -43,9 +43,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def get_cpu_temp():
"""Get CPU temperature."""
- res = os.popen("vcgencmd measure_temp").readline()
- t_cpu = float(res.replace("temp=", "").replace("'C\n", ""))
- return t_cpu
+ t_cpu = Path("/sys/class/thermal/thermal_zone0/temp").read_text().strip()
+ return float(t_cpu) * 0.001
def get_average(temp_base):
diff --git a/homeassistant/components/sensor/translations/zh-Hans.json b/homeassistant/components/sensor/translations/zh-Hans.json
index 44f3b415d4d..33a375c000a 100644
--- a/homeassistant/components/sensor/translations/zh-Hans.json
+++ b/homeassistant/components/sensor/translations/zh-Hans.json
@@ -2,25 +2,33 @@
"device_automation": {
"condition_type": {
"is_battery_level": "{entity_name} \u5f53\u524d\u7684\u7535\u6c60\u7535\u91cf",
+ "is_current": "{entity_name} \u5f53\u524d\u7684\u7535\u6d41",
+ "is_energy": "{entity_name} \u5f53\u524d\u7528\u7535\u91cf",
"is_humidity": "{entity_name} \u5f53\u524d\u7684\u6e7f\u5ea6",
"is_illuminance": "{entity_name} \u5f53\u524d\u7684\u5149\u7167\u5f3a\u5ea6",
"is_power": "{entity_name} \u5f53\u524d\u7684\u529f\u7387",
+ "is_power_factor": "{entity_name} \u5f53\u524d\u7684\u529f\u7387\u56e0\u6570",
"is_pressure": "{entity_name} \u5f53\u524d\u7684\u538b\u529b",
"is_signal_strength": "{entity_name} \u5f53\u524d\u7684\u4fe1\u53f7\u5f3a\u5ea6",
"is_temperature": "{entity_name} \u5f53\u524d\u7684\u6e29\u5ea6",
"is_timestamp": "{entity_name} \u5f53\u524d\u7684\u65f6\u95f4\u6233",
- "is_value": "{entity_name} \u5f53\u524d\u7684\u503c"
+ "is_value": "{entity_name} \u5f53\u524d\u7684\u503c",
+ "is_voltage": "{entity_name} \u5f53\u524d\u7684\u7535\u538b"
},
"trigger_type": {
"battery_level": "{entity_name} \u7684\u7535\u6c60\u7535\u91cf\u53d8\u5316",
+ "current": "{entity_name} \u7684\u7535\u6d41\u53d8\u5316",
+ "energy": "{entity_name} \u7684\u7528\u7535\u91cf\u53d8\u5316",
"humidity": "{entity_name} \u7684\u6e7f\u5ea6\u53d8\u5316",
"illuminance": "{entity_name} \u7684\u5149\u7167\u5f3a\u5ea6\u53d8\u5316",
"power": "{entity_name} \u7684\u529f\u7387\u53d8\u5316",
+ "power_factor": "{entity_name} \u7684\u529f\u7387\u56e0\u6570\u53d8\u5316",
"pressure": "{entity_name} \u7684\u538b\u529b\u53d8\u5316",
"signal_strength": "{entity_name} \u7684\u4fe1\u53f7\u5f3a\u5ea6\u53d8\u5316",
"temperature": "{entity_name} \u7684\u6e29\u5ea6\u53d8\u5316",
"timestamp": "{entity_name} \u7684\u65f6\u95f4\u6233\u53d8\u5316",
- "value": "{entity_name} \u7684\u503c\u53d8\u5316"
+ "value": "{entity_name} \u7684\u503c\u53d8\u5316",
+ "voltage": "{entity_name} \u7684\u7535\u538b\u53d8\u5316"
}
},
"state": {
diff --git a/homeassistant/components/sentry/__init__.py b/homeassistant/components/sentry/__init__.py
index eecac0281e6..6be02b9ba5e 100644
--- a/homeassistant/components/sentry/__init__.py
+++ b/homeassistant/components/sentry/__init__.py
@@ -33,7 +33,7 @@ from .const import (
ENTITY_COMPONENTS,
)
-CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.117")
+CONFIG_SCHEMA = cv.deprecated(DOMAIN)
LOGGER_INFO_REGEX = re.compile(r"^(\w+)\.?(\w+)?\.?(\w+)?\.?(\w+)?(?:\..*)?$")
diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json
index 1257c60e75a..be07586cebd 100644
--- a/homeassistant/components/sentry/manifest.json
+++ b/homeassistant/components/sentry/manifest.json
@@ -3,6 +3,6 @@
"name": "Sentry",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sentry",
- "requirements": ["sentry-sdk==0.19.2"],
+ "requirements": ["sentry-sdk==0.19.4"],
"codeowners": ["@dcramer", "@frenck"]
}
diff --git a/homeassistant/components/seventeentrack/sensor.py b/homeassistant/components/seventeentrack/sensor.py
index 7abed897346..94efe9b98c7 100644
--- a/homeassistant/components/seventeentrack/sensor.py
+++ b/homeassistant/components/seventeentrack/sensor.py
@@ -152,6 +152,7 @@ class SeventeenTrackSummarySensor(Entity):
ATTR_FRIENDLY_NAME: package.friendly_name,
ATTR_INFO_TEXT: package.info_text,
ATTR_STATUS: package.status,
+ ATTR_LOCATION: package.location,
ATTR_TRACKING_NUMBER: package.tracking_number,
}
)
diff --git a/homeassistant/components/sharkiq/translations/ka.json b/homeassistant/components/sharkiq/translations/ka.json
new file mode 100644
index 00000000000..44a03e20bec
--- /dev/null
+++ b/homeassistant/components/sharkiq/translations/ka.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u10d0\u10dc\u10d2\u10d0\u10e0\u10d8\u10e8\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py
index 1f6ecdbd031..298c7e111b2 100644
--- a/homeassistant/components/shelly/__init__.py
+++ b/homeassistant/components/shelly/__init__.py
@@ -24,13 +24,18 @@ from homeassistant.helpers import (
)
from .const import (
+ COAP,
DATA_CONFIG_ENTRY,
DOMAIN,
+ INPUTS_EVENTS_DICT,
POLLING_TIMEOUT_MULTIPLIER,
+ REST,
+ REST_SENSORS_UPDATE_INTERVAL,
SETUP_ENTRY_TIMEOUT_SEC,
SLEEP_PERIOD_MULTIPLIER,
UPDATE_PERIOD_MULTIPLIER,
)
+from .utils import get_device_name
PLATFORMS = ["binary_sensor", "cover", "light", "sensor", "switch"]
_LOGGER = logging.getLogger(__name__)
@@ -82,10 +87,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
except (asyncio.TimeoutError, OSError) as err:
raise ConfigEntryNotReady from err
- wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][
- entry.entry_id
+ hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = {}
+ coap_wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][
+ COAP
] = ShellyDeviceWrapper(hass, entry, device)
- await wrapper.async_setup()
+ await coap_wrapper.async_setup()
+
+ hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][
+ REST
+ ] = ShellyDeviceRestWrapper(hass, device)
for component in PLATFORMS:
hass.async_create_task(
@@ -100,6 +110,7 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator):
def __init__(self, hass, entry, device: aioshelly.Device):
"""Initialize the Shelly device wrapper."""
+ self.device_id = None
sleep_mode = device.settings.get("sleep_mode")
if sleep_mode:
@@ -118,7 +129,7 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator):
super().__init__(
hass,
_LOGGER,
- name=device.settings["name"] or device.settings["device"]["hostname"],
+ name=get_device_name(device),
update_interval=timedelta(seconds=update_interval),
)
self.hass = hass
@@ -127,6 +138,50 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator):
self.device.subscribe_updates(self.async_set_updated_data)
+ self._async_remove_input_events_handler = self.async_add_listener(
+ self._async_input_events_handler
+ )
+ self._last_input_events_count = dict()
+
+ @callback
+ def _async_input_events_handler(self):
+ """Handle device input events."""
+ for block in self.device.blocks:
+ if (
+ "inputEvent" not in block.sensor_ids
+ or "inputEventCnt" not in block.sensor_ids
+ ):
+ continue
+
+ channel = int(block.channel or 0) + 1
+ event_type = block.inputEvent
+ last_event_count = self._last_input_events_count.get(channel)
+ self._last_input_events_count[channel] = block.inputEventCnt
+
+ if (
+ last_event_count is None
+ or last_event_count == block.inputEventCnt
+ or event_type == ""
+ ):
+ continue
+
+ if event_type in INPUTS_EVENTS_DICT:
+ self.hass.bus.async_fire(
+ "shelly.click",
+ {
+ "device_id": self.device_id,
+ "device": self.device.settings["device"]["hostname"],
+ "channel": channel,
+ "click_type": INPUTS_EVENTS_DICT[event_type],
+ },
+ )
+ else:
+ _LOGGER.warning(
+ "Shelly input event %s for device %s is not supported, please open issue",
+ event_type,
+ self.name,
+ )
+
async def _async_update_data(self):
"""Fetch data."""
_LOGGER.debug("Polling Shelly Device - %s", self.name)
@@ -153,7 +208,7 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator):
"""Set up the wrapper."""
dev_reg = await device_registry.async_get_registry(self.hass)
model_type = self.device.settings["device"]["type"]
- dev_reg.async_get_or_create(
+ entry = dev_reg.async_get_or_create(
config_entry_id=self.entry.entry_id,
name=self.name,
connections={(device_registry.CONNECTION_NETWORK_MAC, self.mac)},
@@ -163,10 +218,41 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator):
model=aioshelly.MODEL_NAMES.get(model_type, model_type),
sw_version=self.device.settings["fw"],
)
+ self.device_id = entry.id
def shutdown(self):
"""Shutdown the wrapper."""
self.device.shutdown()
+ self._async_remove_input_events_handler()
+
+
+class ShellyDeviceRestWrapper(update_coordinator.DataUpdateCoordinator):
+ """Rest Wrapper for a Shelly device with Home Assistant specific functions."""
+
+ def __init__(self, hass, device: aioshelly.Device):
+ """Initialize the Shelly device wrapper."""
+
+ super().__init__(
+ hass,
+ _LOGGER,
+ name=get_device_name(device),
+ update_interval=timedelta(seconds=REST_SENSORS_UPDATE_INTERVAL),
+ )
+ self.device = device
+
+ async def _async_update_data(self):
+ """Fetch data."""
+ try:
+ async with async_timeout.timeout(5):
+ _LOGGER.debug("REST update for %s", self.name)
+ return await self.device.update_status()
+ except OSError as err:
+ raise update_coordinator.UpdateFailed("Error fetching data") from err
+
+ @property
+ def mac(self):
+ """Mac address of the device."""
+ return self.device.settings["device"]["mac"]
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
@@ -180,6 +266,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
)
)
if unload_ok:
- hass.data[DOMAIN][DATA_CONFIG_ENTRY].pop(entry.entry_id).shutdown()
+ hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][COAP].shutdown()
+ hass.data[DOMAIN][DATA_CONFIG_ENTRY].pop(entry.entry_id)
return unload_ok
diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py
index 1460c62f153..53038352d4d 100644
--- a/homeassistant/components/shelly/binary_sensor.py
+++ b/homeassistant/components/shelly/binary_sensor.py
@@ -1,8 +1,10 @@
"""Binary sensor for Shelly."""
from homeassistant.components.binary_sensor import (
+ DEVICE_CLASS_CONNECTIVITY,
DEVICE_CLASS_GAS,
DEVICE_CLASS_MOISTURE,
DEVICE_CLASS_OPENING,
+ DEVICE_CLASS_POWER,
DEVICE_CLASS_PROBLEM,
DEVICE_CLASS_SMOKE,
DEVICE_CLASS_VIBRATION,
@@ -11,9 +13,13 @@ from homeassistant.components.binary_sensor import (
from .entity import (
BlockAttributeDescription,
+ RestAttributeDescription,
ShellyBlockAttributeEntity,
+ ShellyRestAttributeEntity,
async_setup_entry_attribute_entities,
+ async_setup_entry_rest,
)
+from .utils import is_momentary_input
SENSORS = {
("device", "overtemp"): BlockAttributeDescription(
@@ -46,6 +52,43 @@ SENSORS = {
("sensor", "vibration"): BlockAttributeDescription(
name="Vibration", device_class=DEVICE_CLASS_VIBRATION
),
+ ("input", "input"): BlockAttributeDescription(
+ name="Input",
+ device_class=DEVICE_CLASS_POWER,
+ default_enabled=False,
+ removal_condition=is_momentary_input,
+ ),
+ ("relay", "input"): BlockAttributeDescription(
+ name="Input",
+ device_class=DEVICE_CLASS_POWER,
+ default_enabled=False,
+ removal_condition=is_momentary_input,
+ ),
+ ("device", "input"): BlockAttributeDescription(
+ name="Input",
+ device_class=DEVICE_CLASS_POWER,
+ default_enabled=False,
+ removal_condition=is_momentary_input,
+ ),
+}
+
+REST_SENSORS = {
+ "cloud": RestAttributeDescription(
+ name="Cloud",
+ value=lambda status, _: status["cloud"]["connected"],
+ device_class=DEVICE_CLASS_CONNECTIVITY,
+ default_enabled=False,
+ ),
+ "fwupdate": RestAttributeDescription(
+ name="Firmware update",
+ icon="mdi:update",
+ value=lambda status, _: status["update"]["has_update"],
+ default_enabled=False,
+ device_state_attributes=lambda status: {
+ "latest_stable_version": status["update"]["new_version"],
+ "installed_version": status["update"]["old_version"],
+ },
+ ),
}
@@ -55,6 +98,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
hass, config_entry, async_add_entities, SENSORS, ShellyBinarySensor
)
+ await async_setup_entry_rest(
+ hass, config_entry, async_add_entities, REST_SENSORS, ShellyRestBinarySensor
+ )
+
class ShellyBinarySensor(ShellyBlockAttributeEntity, BinarySensorEntity):
"""Shelly binary sensor entity."""
@@ -63,3 +110,12 @@ class ShellyBinarySensor(ShellyBlockAttributeEntity, BinarySensorEntity):
def is_on(self):
"""Return true if sensor state is on."""
return bool(self.attribute_value)
+
+
+class ShellyRestBinarySensor(ShellyRestAttributeEntity, BinarySensorEntity):
+ """Shelly REST binary sensor entity."""
+
+ @property
+ def is_on(self):
+ """Return true if REST sensor state is on."""
+ return bool(self.attribute_value)
diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py
index 50af82c2b7d..cd747466973 100644
--- a/homeassistant/components/shelly/const.py
+++ b/homeassistant/components/shelly/const.py
@@ -1,11 +1,16 @@
"""Constants for the Shelly integration."""
+COAP = "coap"
DATA_CONFIG_ENTRY = "config_entry"
DOMAIN = "shelly"
+REST = "rest"
# Used to calculate the timeout in "_async_update_data" used for polling data from devices.
POLLING_TIMEOUT_MULTIPLIER = 1.2
+# Refresh interval for REST sensors
+REST_SENSORS_UPDATE_INTERVAL = 60
+
# Timeout used for initial entry setup in "async_setup_entry".
SETUP_ENTRY_TIMEOUT_SEC = 10
@@ -14,3 +19,16 @@ SLEEP_PERIOD_MULTIPLIER = 1.2
# Multiplier used to calculate the "update_interval" for non-sleeping devices.
UPDATE_PERIOD_MULTIPLIER = 2.2
+
+# Shelly Air - Maximum work hours before lamp replacement
+SHAIR_MAX_WORK_HOURS = 9000
+
+# Map Shelly input events
+INPUTS_EVENTS_DICT = {
+ "S": "single",
+ "SS": "double",
+ "SSS": "triple",
+ "L": "long",
+ "SL": "single_long",
+ "LS": "long_single",
+}
diff --git a/homeassistant/components/shelly/cover.py b/homeassistant/components/shelly/cover.py
index a65ab5a05af..6caa7d5132c 100644
--- a/homeassistant/components/shelly/cover.py
+++ b/homeassistant/components/shelly/cover.py
@@ -12,13 +12,13 @@ from homeassistant.components.cover import (
from homeassistant.core import callback
from . import ShellyDeviceWrapper
-from .const import DATA_CONFIG_ENTRY, DOMAIN
+from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN
from .entity import ShellyBlockEntity
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up cover for device."""
- wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id]
+ wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][COAP]
blocks = [block for block in wrapper.device.blocks if block.type == "roller"]
if not blocks:
diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py
index 314ee48cf7e..b4df2d486f8 100644
--- a/homeassistant/components/shelly/entity.py
+++ b/homeassistant/components/shelly/entity.py
@@ -5,11 +5,11 @@ from typing import Any, Callable, Optional, Union
import aioshelly
from homeassistant.core import callback
-from homeassistant.helpers import device_registry, entity
+from homeassistant.helpers import device_registry, entity, update_coordinator
-from . import ShellyDeviceWrapper
-from .const import DATA_CONFIG_ENTRY, DOMAIN
-from .utils import get_entity_name
+from . import ShellyDeviceRestWrapper, ShellyDeviceWrapper
+from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN, REST
+from .utils import async_remove_shelly_entity, get_entity_name
async def async_setup_entry_attribute_entities(
@@ -18,7 +18,7 @@ async def async_setup_entry_attribute_entities(
"""Set up entities for block attributes."""
wrapper: ShellyDeviceWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][
config_entry.entry_id
- ]
+ ][COAP]
blocks = []
for block in wrapper.device.blocks:
@@ -31,7 +31,17 @@ async def async_setup_entry_attribute_entities(
if getattr(block, sensor_id, None) in (-1, None):
continue
- blocks.append((block, sensor_id, description))
+ # Filter and remove entities that according to settings should not create an entity
+ if description.removal_condition and description.removal_condition(
+ wrapper.device.settings, block
+ ):
+ domain = sensor_class.__module__.split(".")[-1]
+ unique_id = sensor_class(
+ wrapper, block, sensor_id, description
+ ).unique_id
+ await async_remove_shelly_entity(hass, domain, unique_id)
+ else:
+ blocks.append((block, sensor_id, description))
if not blocks:
return
@@ -44,22 +54,64 @@ async def async_setup_entry_attribute_entities(
)
+async def async_setup_entry_rest(
+ hass, config_entry, async_add_entities, sensors, sensor_class
+):
+ """Set up entities for REST sensors."""
+ wrapper: ShellyDeviceRestWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][
+ config_entry.entry_id
+ ][REST]
+
+ entities = []
+ for sensor_id in sensors:
+ description = sensors.get(sensor_id)
+
+ if not wrapper.device.settings.get("sleep_mode"):
+ entities.append((sensor_id, description))
+
+ if not entities:
+ return
+
+ async_add_entities(
+ [
+ sensor_class(wrapper, sensor_id, description)
+ for sensor_id, description in entities
+ ]
+ )
+
+
@dataclass
class BlockAttributeDescription:
"""Class to describe a sensor."""
name: str
# Callable = lambda attr_info: unit
+ icon: Optional[str] = None
unit: Union[None, str, Callable[[dict], str]] = None
value: Callable[[Any], Any] = lambda val: val
device_class: Optional[str] = None
default_enabled: bool = True
available: Optional[Callable[[aioshelly.Block], bool]] = None
+ # Callable (settings, block), return true if entity should be removed
+ removal_condition: Optional[Callable[[dict, aioshelly.Block], bool]] = None
device_state_attributes: Optional[
Callable[[aioshelly.Block], Optional[dict]]
] = None
+@dataclass
+class RestAttributeDescription:
+ """Class to describe a REST sensor."""
+
+ name: str
+ icon: Optional[str] = None
+ unit: Optional[str] = None
+ value: Callable[[dict, Any], Any] = None
+ device_class: Optional[str] = None
+ default_enabled: bool = True
+ device_state_attributes: Optional[Callable[[dict], Optional[dict]]] = None
+
+
class ShellyBlockEntity(entity.Entity):
"""Helper class to represent a block."""
@@ -67,7 +119,7 @@ class ShellyBlockEntity(entity.Entity):
"""Initialize Shelly entity."""
self.wrapper = wrapper
self.block = block
- self._name = get_entity_name(wrapper, block)
+ self._name = get_entity_name(wrapper.device, block)
@property
def name(self):
@@ -133,7 +185,7 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity):
self._unit = unit
self._unique_id = f"{super().unique_id}-{self.attribute}"
- self._name = get_entity_name(wrapper, block, self.description.name)
+ self._name = get_entity_name(wrapper.device, block, self.description.name)
@property
def unique_id(self):
@@ -170,6 +222,11 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity):
"""Device class of sensor."""
return self.description.device_class
+ @property
+ def icon(self):
+ """Icon of sensor."""
+ return self.description.icon
+
@property
def available(self):
"""Available."""
@@ -187,3 +244,79 @@ class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity):
return None
return self.description.device_state_attributes(self.block)
+
+
+class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity):
+ """Class to load info from REST."""
+
+ def __init__(
+ self,
+ wrapper: ShellyDeviceWrapper,
+ attribute: str,
+ description: RestAttributeDescription,
+ ) -> None:
+ """Initialize sensor."""
+ super().__init__(wrapper)
+ self.wrapper = wrapper
+ self.attribute = attribute
+ self.description = description
+ self._name = get_entity_name(wrapper.device, None, self.description.name)
+ self._last_value = None
+
+ @property
+ def name(self):
+ """Name of sensor."""
+ return self._name
+
+ @property
+ def device_info(self):
+ """Device info."""
+ return {
+ "connections": {(device_registry.CONNECTION_NETWORK_MAC, self.wrapper.mac)}
+ }
+
+ @property
+ def entity_registry_enabled_default(self) -> bool:
+ """Return if it should be enabled by default."""
+ return self.description.default_enabled
+
+ @property
+ def available(self):
+ """Available."""
+ return self.wrapper.last_update_success
+
+ @property
+ def attribute_value(self):
+ """Value of sensor."""
+ self._last_value = self.description.value(
+ self.wrapper.device.status, self._last_value
+ )
+ return self._last_value
+
+ @property
+ def unit_of_measurement(self):
+ """Return unit of sensor."""
+ return self.description.unit
+
+ @property
+ def device_class(self):
+ """Device class of sensor."""
+ return self.description.device_class
+
+ @property
+ def icon(self):
+ """Icon of sensor."""
+ return self.description.icon
+
+ @property
+ def unique_id(self):
+ """Return unique ID of entity."""
+ return f"{self.wrapper.mac}-{self.attribute}"
+
+ @property
+ def device_state_attributes(self) -> dict:
+ """Return the state attributes."""
+ if self.description.device_state_attributes is None:
+ return None
+
+ return self.description.device_state_attributes(self.wrapper.device.status)
diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py
index 922d874a4cc..b3a6869d67d 100644
--- a/homeassistant/components/shelly/light.py
+++ b/homeassistant/components/shelly/light.py
@@ -17,14 +17,14 @@ from homeassistant.util.color import (
)
from . import ShellyDeviceWrapper
-from .const import DATA_CONFIG_ENTRY, DOMAIN
+from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN
from .entity import ShellyBlockEntity
-from .utils import async_remove_entity_by_domain
+from .utils import async_remove_shelly_entity
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up lights for device."""
- wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id]
+ wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][COAP]
blocks = []
for block in wrapper.device.blocks:
@@ -39,9 +39,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
unique_id = (
f'{wrapper.device.shelly["mac"]}-{block.type}_{block.channel}'
)
- await async_remove_entity_by_domain(
- hass, "switch", unique_id, config_entry.entry_id
- )
+ await async_remove_shelly_entity(hass, "switch", unique_id)
if not blocks:
return
diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py
index ddd61b6b613..b92b90c1b46 100644
--- a/homeassistant/components/shelly/sensor.py
+++ b/homeassistant/components/shelly/sensor.py
@@ -8,19 +8,27 @@ from homeassistant.const import (
LIGHT_LUX,
PERCENTAGE,
POWER_WATT,
+ SIGNAL_STRENGTH_DECIBELS,
VOLT,
)
+from .const import SHAIR_MAX_WORK_HOURS
from .entity import (
BlockAttributeDescription,
+ RestAttributeDescription,
ShellyBlockAttributeEntity,
+ ShellyRestAttributeEntity,
async_setup_entry_attribute_entities,
+ async_setup_entry_rest,
)
-from .utils import temperature_unit
+from .utils import get_device_uptime, temperature_unit
SENSORS = {
("device", "battery"): BlockAttributeDescription(
- name="Battery", unit=PERCENTAGE, device_class=sensor.DEVICE_CLASS_BATTERY
+ name="Battery",
+ unit=PERCENTAGE,
+ device_class=sensor.DEVICE_CLASS_BATTERY,
+ removal_condition=lambda settings, _: settings.get("external_power") == 1,
),
("device", "deviceTemp"): BlockAttributeDescription(
name="Device Temperature",
@@ -119,6 +127,7 @@ SENSORS = {
name="Gas Concentration",
unit=CONCENTRATION_PARTS_PER_MILLION,
value=lambda value: value,
+ icon="mdi:gauge",
# "sensorOp" is "normal" when the Shelly Gas is working properly and taking measurements.
available=lambda block: block.sensorOp == "normal",
),
@@ -139,7 +148,38 @@ SENSORS = {
unit=LIGHT_LUX,
device_class=sensor.DEVICE_CLASS_ILLUMINANCE,
),
- ("sensor", "tilt"): BlockAttributeDescription(name="tilt", unit=DEGREE),
+ ("sensor", "tilt"): BlockAttributeDescription(name="Tilt", unit=DEGREE),
+ ("relay", "totalWorkTime"): BlockAttributeDescription(
+ name="Lamp life",
+ unit=PERCENTAGE,
+ icon="mdi:progress-wrench",
+ value=lambda value: round(100 - (value / 3600 / SHAIR_MAX_WORK_HOURS), 1),
+ device_state_attributes=lambda block: {
+ "Operational hours": round(block.totalWorkTime / 3600, 1)
+ },
+ ),
+ ("adc", "adc"): BlockAttributeDescription(
+ name="ADC",
+ unit=VOLT,
+ value=lambda value: round(value, 1),
+ device_class=sensor.DEVICE_CLASS_VOLTAGE,
+ ),
+}
+
+REST_SENSORS = {
+ "rssi": RestAttributeDescription(
+ name="RSSI",
+ unit=SIGNAL_STRENGTH_DECIBELS,
+ value=lambda status, _: status["wifi_sta"]["rssi"],
+ device_class=sensor.DEVICE_CLASS_SIGNAL_STRENGTH,
+ default_enabled=False,
+ ),
+ "uptime": RestAttributeDescription(
+ name="Uptime",
+ value=get_device_uptime,
+ device_class=sensor.DEVICE_CLASS_TIMESTAMP,
+ default_enabled=False,
+ ),
}
@@ -148,6 +188,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
await async_setup_entry_attribute_entities(
hass, config_entry, async_add_entities, SENSORS, ShellySensor
)
+ await async_setup_entry_rest(
+ hass, config_entry, async_add_entities, REST_SENSORS, ShellyRestSensor
+ )
class ShellySensor(ShellyBlockAttributeEntity):
@@ -157,3 +200,12 @@ class ShellySensor(ShellyBlockAttributeEntity):
def state(self):
"""Return value of sensor."""
return self.attribute_value
+
+
+class ShellyRestSensor(ShellyRestAttributeEntity):
+ """Represent a shelly REST sensor."""
+
+ @property
+ def state(self):
+ """Return value of sensor."""
+ return self.attribute_value
diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py
index 1f14053929d..c86487072c6 100644
--- a/homeassistant/components/shelly/switch.py
+++ b/homeassistant/components/shelly/switch.py
@@ -5,14 +5,14 @@ from homeassistant.components.switch import SwitchEntity
from homeassistant.core import callback
from . import ShellyDeviceWrapper
-from .const import DATA_CONFIG_ENTRY, DOMAIN
+from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN
from .entity import ShellyBlockEntity
-from .utils import async_remove_entity_by_domain
+from .utils import async_remove_shelly_entity
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up switches for device."""
- wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id]
+ wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][COAP]
# In roller mode the relay blocks exist but do not contain required info
if (
@@ -32,11 +32,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
unique_id = (
f'{wrapper.device.shelly["mac"]}-{block.type}_{block.channel}'
)
- await async_remove_entity_by_domain(
+ await async_remove_shelly_entity(
hass,
"light",
unique_id,
- config_entry.entry_id,
)
if not relay_blocks:
diff --git a/homeassistant/components/shelly/translations/nl.json b/homeassistant/components/shelly/translations/nl.json
index ecdfa41e109..75a2d2771d6 100644
--- a/homeassistant/components/shelly/translations/nl.json
+++ b/homeassistant/components/shelly/translations/nl.json
@@ -11,7 +11,7 @@
"flow_title": "Shelly: {name}",
"step": {
"confirm_discovery": {
- "description": "Wilt u het {model} bij {host} instellen? Voordat het apparaat op batterijen kan worden ingesteld, moet het worden gewekt door op de knop op het apparaat te drukken."
+ "description": "Wilt u het {model} bij {host} instellen? Voordat apparaten op batterijen kunnen worden ingesteld, moet het worden gewekt door op de knop op het apparaat te drukken."
},
"credentials": {
"data": {
diff --git a/homeassistant/components/shelly/translations/sl.json b/homeassistant/components/shelly/translations/sl.json
new file mode 100644
index 00000000000..8d0b45b44e1
--- /dev/null
+++ b/homeassistant/components/shelly/translations/sl.json
@@ -0,0 +1,9 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "description": "Pred nastavitvijo je treba naprave, ki delujejo na baterije, prebuditi s pritiskom na gumb na napravi."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py
index 36ce48b5421..976afdd755b 100644
--- a/homeassistant/components/shelly/utils.py
+++ b/homeassistant/components/shelly/utils.py
@@ -1,28 +1,26 @@
"""Shelly helpers functions."""
+
+from datetime import timedelta
import logging
from typing import Optional
import aioshelly
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
-from homeassistant.helpers import entity_registry
+from homeassistant.util.dt import parse_datetime, utcnow
-from . import ShellyDeviceWrapper
+from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
-async def async_remove_entity_by_domain(hass, domain, unique_id, config_entry_id):
- """Remove entity by domain."""
-
+async def async_remove_shelly_entity(hass, domain, unique_id):
+ """Remove a Shelly entity."""
entity_reg = await hass.helpers.entity_registry.async_get_registry()
- for entry in entity_registry.async_entries_for_config_entry(
- entity_reg, config_entry_id
- ):
- if entry.domain == domain and entry.unique_id == unique_id:
- entity_reg.async_remove(entry.entity_id)
- _LOGGER.debug("Removed %s domain for %s", domain, entry.original_name)
- break
+ entity_id = entity_reg.async_get_entity_id(domain, DOMAIN, unique_id)
+ if entity_id:
+ _LOGGER.debug("Removing entity: %s", entity_id)
+ entity_reg.async_remove(entity_id)
def temperature_unit(block_info: dict) -> str:
@@ -32,44 +30,81 @@ def temperature_unit(block_info: dict) -> str:
return TEMP_CELSIUS
+def get_device_name(device: aioshelly.Device) -> str:
+ """Naming for device."""
+ return device.settings["name"] or device.settings["device"]["hostname"]
+
+
def get_entity_name(
- wrapper: ShellyDeviceWrapper,
+ device: aioshelly.Device,
block: aioshelly.Block,
description: Optional[str] = None,
-):
+) -> str:
"""Naming for switch and sensors."""
- entity_name = wrapper.name
+ entity_name = get_device_name(device)
- channels = None
- if block.type == "input":
- channels = wrapper.device.shelly.get("num_inputs")
- elif block.type == "emeter":
- channels = wrapper.device.shelly.get("num_emeters")
- elif block.type in ["relay", "light"]:
- channels = wrapper.device.shelly.get("num_outputs")
- elif block.type in ["roller", "device"]:
- channels = 1
-
- channels = channels or 1
-
- if channels > 1 and block.type != "device":
- entity_name = None
- mode = block.type + "s"
- if mode in wrapper.device.settings:
- entity_name = wrapper.device.settings[mode][int(block.channel)].get("name")
-
- if not entity_name:
- if wrapper.model == "SHEM-3":
- base = ord("A")
+ if block:
+ channels = None
+ if block.type == "input":
+ # Shelly Dimmer/1L has two input channels and missing "num_inputs"
+ if device.settings["device"]["type"] in ["SHDM-1", "SHDM-2", "SHSW-L"]:
+ channels = 2
else:
- base = ord("1")
- entity_name = f"{wrapper.name} channel {chr(int(block.channel)+base)}"
+ channels = device.shelly.get("num_inputs")
+ elif block.type == "emeter":
+ channels = device.shelly.get("num_emeters")
+ elif block.type in ["relay", "light"]:
+ channels = device.shelly.get("num_outputs")
+ elif block.type in ["roller", "device"]:
+ channels = 1
- # Shelly Dimmer has two input channels and missing "num_inputs"
- if wrapper.model in ["SHDM-1", "SHDM-2"] and block.type == "input":
- entity_name = f"{entity_name} channel {int(block.channel)+1}"
+ channels = channels or 1
+
+ if channels > 1 and block.type != "device":
+ entity_name = None
+ mode = block.type + "s"
+ if mode in device.settings:
+ entity_name = device.settings[mode][int(block.channel)].get("name")
+
+ if not entity_name:
+ if device.settings["device"]["type"] == "SHEM-3":
+ base = ord("A")
+ else:
+ base = ord("1")
+ entity_name = (
+ f"{get_device_name(device)} channel {chr(int(block.channel)+base)}"
+ )
if description:
entity_name = f"{entity_name} {description}"
return entity_name
+
+
+def is_momentary_input(settings: dict, block: aioshelly.Block) -> bool:
+ """Return true if input button settings is set to a momentary type."""
+ button = settings.get("relays") or settings.get("lights") or settings.get("inputs")
+
+ # Shelly 1L has two button settings in the first channel
+ if settings["device"]["type"] == "SHSW-L":
+ channel = int(block.channel or 0) + 1
+ button_type = button[0].get("btn" + str(channel) + "_type")
+ else:
+ # Some devices has only one channel in settings
+ channel = min(int(block.channel or 0), len(button) - 1)
+ button_type = button[channel].get("btn_type")
+
+ return button_type in ["momentary", "momentary_on_release"]
+
+
+def get_device_uptime(status: dict, last_uptime: str) -> str:
+ """Return device uptime string, tolerate up to 5 seconds deviation."""
+ uptime = utcnow() - timedelta(seconds=status["uptime"])
+
+ if not last_uptime:
+ return uptime.replace(microsecond=0).isoformat()
+
+ if abs((uptime - parse_datetime(last_uptime)).total_seconds()) > 5:
+ return uptime.replace(microsecond=0).isoformat()
+
+ return last_uptime
diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py
index 2430aad43cf..89f5c40b1ff 100644
--- a/homeassistant/components/simplisafe/__init__.py
+++ b/homeassistant/components/simplisafe/__init__.py
@@ -138,7 +138,7 @@ SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA = SERVICE_BASE_SCHEMA.extend(
}
)
-CONFIG_SCHEMA = cv.deprecated(DOMAIN, invalidation_version="0.119")
+CONFIG_SCHEMA = cv.deprecated(DOMAIN)
@callback
diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py
index c34255bc62a..f17a2ce2e4c 100644
--- a/homeassistant/components/simplisafe/config_flow.py
+++ b/homeassistant/components/simplisafe/config_flow.py
@@ -15,6 +15,15 @@ from homeassistant.helpers import aiohttp_client
from . import async_get_client_id
from .const import DOMAIN, LOGGER # pylint: disable=unused-import
+FULL_DATA_SCHEMA = vol.Schema(
+ {
+ vol.Required(CONF_USERNAME): str,
+ vol.Required(CONF_PASSWORD): str,
+ vol.Optional(CONF_CODE): str,
+ }
+)
+PASSWORD_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str})
+
class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a SimpliSafe config flow."""
@@ -24,15 +33,6 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self):
"""Initialize the config flow."""
- self.full_data_schema = vol.Schema(
- {
- vol.Required(CONF_USERNAME): str,
- vol.Required(CONF_PASSWORD): str,
- vol.Optional(CONF_CODE): str,
- }
- )
- self.password_data_schema = vol.Schema({vol.Required(CONF_PASSWORD): str})
-
self._code = None
self._password = None
self._username = None
@@ -125,21 +125,19 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle re-auth completion."""
if not user_input:
return self.async_show_form(
- step_id="reauth_confirm", data_schema=self.password_data_schema
+ step_id="reauth_confirm", data_schema=PASSWORD_DATA_SCHEMA
)
self._password = user_input[CONF_PASSWORD]
return await self._async_login_during_step(
- step_id="reauth_confirm", form_schema=self.password_data_schema
+ step_id="reauth_confirm", form_schema=PASSWORD_DATA_SCHEMA
)
async def async_step_user(self, user_input=None):
"""Handle the start of the config flow."""
if not user_input:
- return self.async_show_form(
- step_id="user", data_schema=self.full_data_schema
- )
+ return self.async_show_form(step_id="user", data_schema=FULL_DATA_SCHEMA)
await self.async_set_unique_id(user_input[CONF_USERNAME])
self._abort_if_unique_id_configured()
@@ -149,7 +147,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self._username = user_input[CONF_USERNAME]
return await self._async_login_during_step(
- step_id="user", form_schema=self.full_data_schema
+ step_id="user", form_schema=FULL_DATA_SCHEMA
)
@@ -171,7 +169,9 @@ class SimpliSafeOptionsFlowHandler(config_entries.OptionsFlow):
{
vol.Optional(
CONF_CODE,
- default=self.config_entry.options.get(CONF_CODE),
+ description={
+ "suggested_value": self.config_entry.options.get(CONF_CODE)
+ },
): str
}
),
diff --git a/homeassistant/components/simplisafe/lock.py b/homeassistant/components/simplisafe/lock.py
index 000b1cd9abb..08ffb82d24f 100644
--- a/homeassistant/components/simplisafe/lock.py
+++ b/homeassistant/components/simplisafe/lock.py
@@ -17,13 +17,17 @@ ATTR_PIN_PAD_LOW_BATTERY = "pin_pad_low_battery"
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up SimpliSafe locks based on a config entry."""
simplisafe = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id]
- async_add_entities(
- [
- SimpliSafeLock(simplisafe, system, lock)
- for system in simplisafe.systems.values()
- for lock in system.locks.values()
- ]
- )
+ locks = []
+
+ for system in simplisafe.systems.values():
+ if system.version == 2:
+ LOGGER.info("Skipping lock setup for V2 system: %s", system.system_id)
+ continue
+
+ for lock in system.locks.values():
+ locks.append(SimpliSafeLock(simplisafe, system, lock))
+
+ async_add_entities(locks)
class SimpliSafeLock(SimpliSafeEntity, LockEntity):
diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json
index 0f209b366e8..a502a7908f0 100644
--- a/homeassistant/components/simplisafe/manifest.json
+++ b/homeassistant/components/simplisafe/manifest.json
@@ -3,6 +3,6 @@
"name": "SimpliSafe",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/simplisafe",
- "requirements": ["simplisafe-python==9.6.0"],
+ "requirements": ["simplisafe-python==9.6.2"],
"codeowners": ["@bachya"]
}
diff --git a/homeassistant/components/simplisafe/translations/et.json b/homeassistant/components/simplisafe/translations/et.json
index f386228f404..b98a121046a 100644
--- a/homeassistant/components/simplisafe/translations/et.json
+++ b/homeassistant/components/simplisafe/translations/et.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "See SimpliSafe'i konto on juba kasutusel.",
- "reauth_successful": "Taasautentimine \u00f5nnestus"
+ "reauth_successful": "Taastuvastamine \u00f5nnestus"
},
"error": {
"identifier_exists": "Konto on juba registreeritud",
diff --git a/homeassistant/components/simplisafe/translations/ka.json b/homeassistant/components/simplisafe/translations/ka.json
new file mode 100644
index 00000000000..30cd7df1991
--- /dev/null
+++ b/homeassistant/components/simplisafe/translations/ka.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "invalid_auth": "\u10db\u10ea\u10d3\u10d0\u10e0\u10d8 \u10d0\u10e3\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/slack/manifest.json b/homeassistant/components/slack/manifest.json
index ad45abbe3c0..e183dd455f1 100644
--- a/homeassistant/components/slack/manifest.json
+++ b/homeassistant/components/slack/manifest.json
@@ -3,5 +3,5 @@
"name": "Slack",
"documentation": "https://www.home-assistant.io/integrations/slack",
"requirements": ["slackclient==2.5.0"],
- "codeowners": []
+ "codeowners": ["@bachya"]
}
diff --git a/homeassistant/components/slack/notify.py b/homeassistant/components/slack/notify.py
index 88317b31585..90caad62a58 100644
--- a/homeassistant/components/slack/notify.py
+++ b/homeassistant/components/slack/notify.py
@@ -198,17 +198,21 @@ class SlackNotificationService(BaseNotificationService):
_LOGGER.error("Error while uploading file message: %s", err)
async def _async_send_text_only_message(
- self, targets, message, title, blocks, username, icon
+ self,
+ targets,
+ message,
+ title,
+ *,
+ username=None,
+ icon=None,
+ blocks=None,
):
"""Send a text-only message."""
- message_dict = {
- "blocks": blocks,
- "link_names": True,
- "text": message,
- "username": username,
- }
+ message_dict = {"link_names": True, "text": message}
+
+ if username:
+ message_dict["username"] = username
- icon = icon or self._icon
if icon:
if icon.lower().startswith(("http://", "https://")):
icon_type = "url"
@@ -217,6 +221,9 @@ class SlackNotificationService(BaseNotificationService):
message_dict[f"icon_{icon_type}"] = icon
+ if blocks:
+ message_dict["blocks"] = blocks
+
tasks = {
target: self._client.chat_postMessage(**message_dict, channel=target)
for target in targets
@@ -256,15 +263,15 @@ class SlackNotificationService(BaseNotificationService):
elif ATTR_BLOCKS in data:
blocks = data[ATTR_BLOCKS]
else:
- blocks = {}
+ blocks = None
return await self._async_send_text_only_message(
targets,
message,
title,
- blocks,
username=data.get(ATTR_USERNAME, self._username),
icon=data.get(ATTR_ICON, self._icon),
+ blocks=blocks,
)
# Message Type 2: A message that uploads a remote file
diff --git a/homeassistant/components/smappee/translations/hu.json b/homeassistant/components/smappee/translations/hu.json
index 5bb10e0f851..4258cfb0912 100644
--- a/homeassistant/components/smappee/translations/hu.json
+++ b/homeassistant/components/smappee/translations/hu.json
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
- "already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ "already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/smappee/translations/pl.json b/homeassistant/components/smappee/translations/pl.json
index 56f20551195..ac3393e51e1 100644
--- a/homeassistant/components/smappee/translations/pl.json
+++ b/homeassistant/components/smappee/translations/pl.json
@@ -15,7 +15,7 @@
"data": {
"environment": "\u015arodowisko"
},
- "description": "Skonfiguruj Smappee, aby zintegrowa\u0107 go z Home Assistant."
+ "description": "Skonfiguruj Smappee, aby zintegrowa\u0107 go z Home Assistantem."
},
"local": {
"data": {
@@ -27,7 +27,7 @@
"title": "Wybierz metod\u0119 uwierzytelniania"
},
"zeroconf_confirm": {
- "description": "Czy chcesz doda\u0107 do Home Assistant urz\u0105dzenie Smappee o numerze seryjnym `{serialnumber}`?",
+ "description": "Czy chcesz doda\u0107 do Home Assistanta urz\u0105dzenie Smappee o numerze seryjnym `{serialnumber}`?",
"title": "Wykryto urz\u0105dzenie Smappee"
}
}
diff --git a/homeassistant/components/smarthab/translations/hu.json b/homeassistant/components/smarthab/translations/hu.json
new file mode 100644
index 00000000000..b40828cc764
--- /dev/null
+++ b/homeassistant/components/smarthab/translations/hu.json
@@ -0,0 +1,9 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "description": "Technikai okokb\u00f3l ne felejtsen el m\u00e1sodlagos fi\u00f3kot haszn\u00e1lni a Home Assistant be\u00e1ll\u00edt\u00e1s\u00e1hoz. A SmartHab alkalmaz\u00e1sb\u00f3l l\u00e9trehozhat egyet."
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/smarthab/translations/pl.json b/homeassistant/components/smarthab/translations/pl.json
index 7b2d65b85ab..14ca88f1c00 100644
--- a/homeassistant/components/smarthab/translations/pl.json
+++ b/homeassistant/components/smarthab/translations/pl.json
@@ -11,7 +11,7 @@
"email": "Adres e-mail",
"password": "Has\u0142o"
},
- "description": "Ze wzgl\u0119d\u00f3w technicznych, nale\u017cy u\u017cy\u0107 dodatkowego konta, specjalnie na u\u017cytek dla Home Assistant. Mo\u017cesz je utworzy\u0107 z poziomu aplikacji SmartHab.",
+ "description": "Ze wzgl\u0119d\u00f3w technicznych, nale\u017cy u\u017cy\u0107 dodatkowego konta, specjalnie na u\u017cytek dla Home Assistanta. Mo\u017cesz je utworzy\u0107 z poziomu aplikacji SmartHab.",
"title": "Konfiguracja SmartHab"
}
}
diff --git a/homeassistant/components/smartthings/manifest.json b/homeassistant/components/smartthings/manifest.json
index 30ef278d1d1..88ed85306db 100644
--- a/homeassistant/components/smartthings/manifest.json
+++ b/homeassistant/components/smartthings/manifest.json
@@ -3,7 +3,7 @@
"name": "SmartThings",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/smartthings",
- "requirements": ["pysmartapp==0.3.2", "pysmartthings==0.7.4"],
+ "requirements": ["pysmartapp==0.3.3", "pysmartthings==0.7.6"],
"dependencies": ["webhook"],
"after_dependencies": ["cloud"],
"codeowners": ["@andrewsayre"]
diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py
index f0240886913..835c4168f07 100644
--- a/homeassistant/components/smartthings/sensor.py
+++ b/homeassistant/components/smartthings/sensor.py
@@ -20,6 +20,7 @@ from homeassistant.const import (
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
VOLT,
+ VOLUME_CUBIC_METERS,
)
from . import SmartThingsEntity
@@ -116,6 +117,12 @@ CAPABILITY_TO_SENSORS = {
None,
)
],
+ Capability.gas_meter: [
+ Map(Attribute.gas_meter, "Gas Meter", ENERGY_KILO_WATT_HOUR, None),
+ Map(Attribute.gas_meter_calorific, "Gas Meter Calorific", None, None),
+ Map(Attribute.gas_meter_time, "Gas Meter Time", None, DEVICE_CLASS_TIMESTAMP),
+ Map(Attribute.gas_meter_volume, "Gas Meter Volume", VOLUME_CUBIC_METERS, None),
+ ],
Capability.illuminance_measurement: [
Map(Attribute.illuminance, "Illuminance", LIGHT_LUX, DEVICE_CLASS_ILLUMINANCE)
],
@@ -228,7 +235,10 @@ CAPABILITY_TO_SENSORS = {
)
],
Capability.three_axis: [],
- Capability.tv_channel: [Map(Attribute.tv_channel, "Tv Channel", None, None)],
+ Capability.tv_channel: [
+ Map(Attribute.tv_channel, "Tv Channel", None, None),
+ Map(Attribute.tv_channel_name, "Tv Channel Name", None, None),
+ ],
Capability.tvoc_measurement: [
Map(
Attribute.tvoc_level,
diff --git a/homeassistant/components/smartthings/translations/hu.json b/homeassistant/components/smartthings/translations/hu.json
index a148bfa04fb..5cbe1d086bc 100644
--- a/homeassistant/components/smartthings/translations/hu.json
+++ b/homeassistant/components/smartthings/translations/hu.json
@@ -11,7 +11,8 @@
"pat": {
"data": {
"access_token": "Hozz\u00e1f\u00e9r\u00e9si token"
- }
+ },
+ "description": "K\u00e9rj\u00fck, adjon meg egy SmartThings [Szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si tokent] ( {token_url} ), amelyet az [utas\u00edt\u00e1sok] ( {component_url} ) alapj\u00e1n hoztak l\u00e9tre. Ezt haszn\u00e1ljuk a Home Assistant integr\u00e1ci\u00f3j\u00e1nak l\u00e9trehoz\u00e1s\u00e1hoz a SmartThings-fi\u00f3kban."
},
"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.",
diff --git a/homeassistant/components/smartthings/translations/pl.json b/homeassistant/components/smartthings/translations/pl.json
index 6696235ab8f..74201e76fbd 100644
--- a/homeassistant/components/smartthings/translations/pl.json
+++ b/homeassistant/components/smartthings/translations/pl.json
@@ -1,7 +1,7 @@
{
"config": {
"abort": {
- "invalid_webhook_url": "Home Assistant nie jest poprawnie skonfigurowany do otrzymywania danych od SmartThings. Adres URL webhook jest nieprawid\u0142owy: \n > {webhook_url} \n\n Zaktualizuj konfiguracj\u0119 zgodnie z [instrukcj\u0105]({component_url}), uruchom ponownie Home Assistant i spr\u00f3buj ponownie.",
+ "invalid_webhook_url": "Home Assistant nie jest poprawnie skonfigurowany do otrzymywania danych od SmartThings. Adres URL webhook jest nieprawid\u0142owy: \n > {webhook_url} \n\n Zaktualizuj konfiguracj\u0119 zgodnie z [instrukcj\u0105]({component_url}), uruchom ponownie Home Assistanta i spr\u00f3buj ponownie.",
"no_available_locations": "Nie ma dost\u0119pnych lokalizacji SmartThings do skonfigurowania w Home Assistant"
},
"error": {
@@ -13,7 +13,7 @@
},
"step": {
"authorize": {
- "title": "Autoryzuj Home Assistant"
+ "title": "Autoryzuj Home Assistanta"
},
"pat": {
"data": {
@@ -26,11 +26,11 @@
"data": {
"location_id": "Lokalizacja"
},
- "description": "Wybierz lokalizacj\u0119 SmartThings, kt\u00f3r\u0105 chcesz doda\u0107 do Home Assistant. 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": {
- "description": "SmartThings zostanie skonfigurowany, by wysy\u0142a\u0107 aktualizacje push do Home Assistant na:\n> {webhook_url}\n\nJe\u015bli adres jest nieprawid\u0142owy, popraw swoj\u0105 konfiguracj\u0119, uruchom ponownie Home Assistant i spr\u00f3buj ponownie.",
+ "description": "SmartThings zostanie skonfigurowany, by wysy\u0142a\u0107 aktualizacje push do Home Assistanta na:\n> {webhook_url}\n\nJe\u015bli adres jest nieprawid\u0142owy, popraw swoj\u0105 konfiguracj\u0119, uruchom ponownie Home Assistant i spr\u00f3buj ponownie.",
"title": "Potwierd\u017a Callback URL"
}
}
diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py
index f09491bf611..c13982ee15d 100644
--- a/homeassistant/components/smhi/weather.py
+++ b/homeassistant/components/smhi/weather.py
@@ -10,6 +10,20 @@ from smhi import Smhi
from smhi.smhi_lib import SmhiForecastException
from homeassistant.components.weather import (
+ ATTR_CONDITION_CLOUDY,
+ ATTR_CONDITION_EXCEPTIONAL,
+ ATTR_CONDITION_FOG,
+ ATTR_CONDITION_HAIL,
+ ATTR_CONDITION_LIGHTNING,
+ ATTR_CONDITION_LIGHTNING_RAINY,
+ ATTR_CONDITION_PARTLYCLOUDY,
+ ATTR_CONDITION_POURING,
+ ATTR_CONDITION_RAINY,
+ ATTR_CONDITION_SNOWY,
+ ATTR_CONDITION_SNOWY_RAINY,
+ ATTR_CONDITION_SUNNY,
+ ATTR_CONDITION_WINDY,
+ ATTR_CONDITION_WINDY_VARIANT,
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_PRECIPITATION,
ATTR_FORECAST_TEMP,
@@ -29,20 +43,20 @@ _LOGGER = logging.getLogger(__name__)
# Used to map condition from API results
CONDITION_CLASSES = {
- "cloudy": [5, 6],
- "fog": [7],
- "hail": [],
- "lightning": [21],
- "lightning-rainy": [11],
- "partlycloudy": [3, 4],
- "pouring": [10, 20],
- "rainy": [8, 9, 18, 19],
- "snowy": [15, 16, 17, 25, 26, 27],
- "snowy-rainy": [12, 13, 14, 22, 23, 24],
- "sunny": [1, 2],
- "windy": [],
- "windy-variant": [],
- "exceptional": [],
+ ATTR_CONDITION_CLOUDY: [5, 6],
+ ATTR_CONDITION_FOG: [7],
+ ATTR_CONDITION_HAIL: [],
+ ATTR_CONDITION_LIGHTNING: [21],
+ ATTR_CONDITION_LIGHTNING_RAINY: [11],
+ ATTR_CONDITION_PARTLYCLOUDY: [3, 4],
+ ATTR_CONDITION_POURING: [10, 20],
+ ATTR_CONDITION_RAINY: [8, 9, 18, 19],
+ ATTR_CONDITION_SNOWY: [15, 16, 17, 25, 26, 27],
+ ATTR_CONDITION_SNOWY_RAINY: [12, 13, 14, 22, 23, 24],
+ ATTR_CONDITION_SUNNY: [1, 2],
+ ATTR_CONDITION_WINDY: [],
+ ATTR_CONDITION_WINDY_VARIANT: [],
+ ATTR_CONDITION_EXCEPTIONAL: [],
}
# 5 minutes between retrying connect to API again
diff --git a/homeassistant/components/solaredge/config_flow.py b/homeassistant/components/solaredge/config_flow.py
index 8a7f2af3a99..49c265b4221 100644
--- a/homeassistant/components/solaredge/config_flow.py
+++ b/homeassistant/components/solaredge/config_flow.py
@@ -41,15 +41,14 @@ class SolarEdgeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
api = solaredge.Solaredge(api_key)
try:
response = api.get_details(site_id)
- except (ConnectTimeout, HTTPError):
- self._errors[CONF_SITE_ID] = "could_not_connect"
- return False
- try:
if response["details"]["status"].lower() != "active":
self._errors[CONF_SITE_ID] = "site_not_active"
return False
+ except (ConnectTimeout, HTTPError):
+ self._errors[CONF_SITE_ID] = "could_not_connect"
+ return False
except KeyError:
- self._errors[CONF_SITE_ID] = "api_failure"
+ self._errors[CONF_SITE_ID] = "invalid_api_key"
return False
return True
@@ -59,7 +58,7 @@ class SolarEdgeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if user_input is not None:
name = slugify(user_input.get(CONF_NAME, DEFAULT_NAME))
if self._site_in_configuration_exists(user_input[CONF_SITE_ID]):
- self._errors[CONF_SITE_ID] = "site_exists"
+ self._errors[CONF_SITE_ID] = "already_configured"
else:
site = user_input[CONF_SITE_ID]
api = user_input[CONF_API_KEY]
@@ -94,5 +93,5 @@ class SolarEdgeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_import(self, user_input=None):
"""Import a config entry."""
if self._site_in_configuration_exists(user_input[CONF_SITE_ID]):
- return self.async_abort(reason="site_exists")
+ return self.async_abort(reason="already_configured")
return await self.async_step_user(user_input)
diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py
index 0cd498b4e3b..e3e59676bf5 100644
--- a/homeassistant/components/solaredge/sensor.py
+++ b/homeassistant/components/solaredge/sensor.py
@@ -425,22 +425,21 @@ class SolarEdgeEnergyDetailsService(SolarEdgeDataService):
self.data = {}
self.attributes = {}
self.unit = energy_details["unit"]
- meters = energy_details["meters"]
- for entity in meters:
- for key, data in entity.items():
- if key == "type" and data in [
- "Production",
- "SelfConsumption",
- "FeedIn",
- "Purchased",
- "Consumption",
- ]:
- energy_type = data
- if key == "values":
- for row in data:
- self.data[energy_type] = row["value"]
- self.attributes[energy_type] = {"date": row["date"]}
+ for meter in energy_details["meters"]:
+ if "type" not in meter or "values" not in meter:
+ continue
+ if meter["type"] not in [
+ "Production",
+ "SelfConsumption",
+ "FeedIn",
+ "Purchased",
+ "Consumption",
+ ]:
+ continue
+ if len(meter["values"][0]) == 2:
+ self.data[meter["type"]] = meter["values"][0]["value"]
+ self.attributes[meter["type"]] = {"date": meter["values"][0]["date"]}
_LOGGER.debug(
"Updated SolarEdge energy details: %s, %s", self.data, self.attributes
diff --git a/homeassistant/components/solaredge/strings.json b/homeassistant/components/solaredge/strings.json
index eb4c5cda1fd..b6f258b0dc8 100644
--- a/homeassistant/components/solaredge/strings.json
+++ b/homeassistant/components/solaredge/strings.json
@@ -11,10 +11,13 @@
}
},
"error": {
- "site_exists": "This site_id is already configured"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
+ "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
+ "site_not_active": "The site is not active",
+ "could_not_connect": "Could not connect to the solaredge API"
},
"abort": {
- "site_exists": "This site_id is already configured"
+ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/solaredge/translations/ca.json b/homeassistant/components/solaredge/translations/ca.json
index cce579cd7e5..6705e6d19e8 100644
--- a/homeassistant/components/solaredge/translations/ca.json
+++ b/homeassistant/components/solaredge/translations/ca.json
@@ -1,10 +1,15 @@
{
"config": {
"abort": {
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
"site_exists": "Aquest site_id ja est\u00e0 configurat"
},
"error": {
- "site_exists": "Aquest site_id ja est\u00e0 configurat"
+ "already_configured": "El dispositiu ja est\u00e0 configurat",
+ "could_not_connect": "No s'ha pogut connectar amb l'API de Solaredge",
+ "invalid_api_key": "Clau API inv\u00e0lida",
+ "site_exists": "Aquest site_id ja est\u00e0 configurat",
+ "site_not_active": "El lloc web no est\u00e0 actiu"
},
"step": {
"user": {
diff --git a/homeassistant/components/solaredge/translations/cs.json b/homeassistant/components/solaredge/translations/cs.json
index 501ff51a2bb..e985ed4f221 100644
--- a/homeassistant/components/solaredge/translations/cs.json
+++ b/homeassistant/components/solaredge/translations/cs.json
@@ -1,10 +1,15 @@
{
"config": {
"abort": {
+ "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno",
"site_exists": "Tento identifik\u00e1tor je ji\u017e nastaven"
},
"error": {
- "site_exists": "Tento identifik\u00e1tor je ji\u017e nastaven"
+ "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno",
+ "could_not_connect": "Nelze se p\u0159ipojit k API SolarEdge",
+ "invalid_api_key": "Neplatn\u00fd kl\u00ed\u010d API",
+ "site_exists": "Tento identifik\u00e1tor je ji\u017e nastaven",
+ "site_not_active": "Str\u00e1nka nen\u00ed aktivn\u00ed"
},
"step": {
"user": {
diff --git a/homeassistant/components/solaredge/translations/en.json b/homeassistant/components/solaredge/translations/en.json
index d3d1fb4e862..a9abfd5e013 100644
--- a/homeassistant/components/solaredge/translations/en.json
+++ b/homeassistant/components/solaredge/translations/en.json
@@ -1,10 +1,15 @@
{
"config": {
"abort": {
+ "already_configured": "Device is already configured",
"site_exists": "This site_id is already configured"
},
"error": {
- "site_exists": "This site_id is already configured"
+ "already_configured": "Device is already configured",
+ "could_not_connect": "Could not connect to the solaredge API",
+ "invalid_api_key": "Invalid API key",
+ "site_exists": "This site_id is already configured",
+ "site_not_active": "The site is not active"
},
"step": {
"user": {
diff --git a/homeassistant/components/solaredge/translations/es.json b/homeassistant/components/solaredge/translations/es.json
index 7a8b55fc649..0447184c309 100644
--- a/homeassistant/components/solaredge/translations/es.json
+++ b/homeassistant/components/solaredge/translations/es.json
@@ -1,10 +1,15 @@
{
"config": {
"abort": {
+ "already_configured": "El dispositivo ya est\u00e1 configurado",
"site_exists": "Este site_id ya est\u00e1 configurado"
},
"error": {
- "site_exists": "Este site_id ya est\u00e1 configurado"
+ "already_configured": "El dispositivo ya est\u00e1 configurado",
+ "could_not_connect": "No se pudo conectar con la API de solaredge",
+ "invalid_api_key": "Clave API no v\u00e1lida",
+ "site_exists": "Este site_id ya est\u00e1 configurado",
+ "site_not_active": "El sitio no est\u00e1 activo"
},
"step": {
"user": {
diff --git a/homeassistant/components/solaredge/translations/et.json b/homeassistant/components/solaredge/translations/et.json
index e3abcedc5c1..4497c536042 100644
--- a/homeassistant/components/solaredge/translations/et.json
+++ b/homeassistant/components/solaredge/translations/et.json
@@ -1,10 +1,15 @@
{
"config": {
"abort": {
+ "already_configured": "Seade on juba h\u00e4\u00e4lestatud",
"site_exists": "See site_id on juba konfigureeritud"
},
"error": {
- "site_exists": "See site_id on juba konfigureeritud"
+ "already_configured": "Seade on juba h\u00e4\u00e4lestatud",
+ "could_not_connect": "Ei saanud \u00fchendust Solaredge API-ga",
+ "invalid_api_key": "Vale API v\u00f5ti",
+ "site_exists": "See site_id on juba konfigureeritud",
+ "site_not_active": "Sait pole aktiivne"
},
"step": {
"user": {
diff --git a/homeassistant/components/solaredge/translations/it.json b/homeassistant/components/solaredge/translations/it.json
index 28b34bdbd3c..0d99250e77a 100644
--- a/homeassistant/components/solaredge/translations/it.json
+++ b/homeassistant/components/solaredge/translations/it.json
@@ -1,10 +1,15 @@
{
"config": {
"abort": {
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
"site_exists": "Questo site_id \u00e8 gi\u00e0 configurato"
},
"error": {
- "site_exists": "Questo site_id \u00e8 gi\u00e0 configurato"
+ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
+ "could_not_connect": "Impossibile connettersi all'API Solaredge",
+ "invalid_api_key": "Chiave API non valida",
+ "site_exists": "Questo site_id \u00e8 gi\u00e0 configurato",
+ "site_not_active": "Il sito non \u00e8 attivo"
},
"step": {
"user": {
diff --git a/homeassistant/components/solaredge/translations/ka.json b/homeassistant/components/solaredge/translations/ka.json
new file mode 100644
index 00000000000..d6982775db2
--- /dev/null
+++ b/homeassistant/components/solaredge/translations/ka.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "\u10db\u10dd\u10ec\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0 \u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0"
+ },
+ "error": {
+ "already_configured": "\u10db\u10dd\u10ec\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0 \u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0",
+ "could_not_connect": "Solaredge API- \u10e1\u10d7\u10d0\u10dc \u10d3\u10d0\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d4\u10d1\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0",
+ "invalid_api_key": "\u10db\u10ea\u10d3\u10d0\u10e0\u10d8 API key",
+ "site_not_active": "\u10e1\u10d0\u10d8\u10e2\u10d8 \u10d0\u10e0 \u10d0\u10e0\u10d8\u10e1 \u10d0\u10e5\u10e2\u10d8\u10e3\u10e0\u10d8"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/solaredge/translations/no.json b/homeassistant/components/solaredge/translations/no.json
index 5360bd6d810..7ff7dd8f144 100644
--- a/homeassistant/components/solaredge/translations/no.json
+++ b/homeassistant/components/solaredge/translations/no.json
@@ -1,17 +1,22 @@
{
"config": {
"abort": {
+ "already_configured": "Enheten er allerede konfigurert",
"site_exists": "Denne site_id er allerede konfigurert"
},
"error": {
- "site_exists": "Denne site_id er allerede konfigurert"
+ "already_configured": "Enheten er allerede konfigurert",
+ "could_not_connect": "Kunne ikke koble til solaredge API",
+ "invalid_api_key": "Ugyldig API-n\u00f8kkel",
+ "site_exists": "Denne site_id er allerede konfigurert",
+ "site_not_active": "Nettstedet er ikke aktivt"
},
"step": {
"user": {
"data": {
"api_key": "API-n\u00f8kkel",
"name": "Navnet p\u00e5 denne installasjonen",
- "site_id": "SolarEdge nettsted-id"
+ "site_id": "SolarEdge nettsted ID"
},
"title": "Definer API-parametrene for denne installasjonen"
}
diff --git a/homeassistant/components/solaredge/translations/pl.json b/homeassistant/components/solaredge/translations/pl.json
index 0c217a12510..2fa4af72cb3 100644
--- a/homeassistant/components/solaredge/translations/pl.json
+++ b/homeassistant/components/solaredge/translations/pl.json
@@ -1,10 +1,15 @@
{
"config": {
"abort": {
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
"site_exists": "Ten site_id jest ju\u017c skonfigurowany"
},
"error": {
- "site_exists": "Ten site_id jest ju\u017c skonfigurowany"
+ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "could_not_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z API solaredge",
+ "invalid_api_key": "Nieprawid\u0142owy klucz API",
+ "site_exists": "Ten site_id jest ju\u017c skonfigurowany",
+ "site_not_active": "Strona nie jest aktywna"
},
"step": {
"user": {
diff --git a/homeassistant/components/solaredge/translations/ru.json b/homeassistant/components/solaredge/translations/ru.json
index 1dadfbf94ee..fbda2e7eb18 100644
--- a/homeassistant/components/solaredge/translations/ru.json
+++ b/homeassistant/components/solaredge/translations/ru.json
@@ -1,10 +1,15 @@
{
"config": {
"abort": {
+ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.",
"site_exists": "\u042d\u0442\u043e\u0442 site_id \u0443\u0436\u0435 \u0441\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d."
},
"error": {
- "site_exists": "\u042d\u0442\u043e\u0442 site_id \u0443\u0436\u0435 \u0441\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d."
+ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.",
+ "could_not_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a API Solaredge.",
+ "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.",
+ "site_exists": "\u042d\u0442\u043e\u0442 site_id \u0443\u0436\u0435 \u0441\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d.",
+ "site_not_active": "\u0421\u0430\u0439\u0442 \u043d\u0435 \u0430\u043a\u0442\u0438\u0432\u0435\u043d."
},
"step": {
"user": {
diff --git a/homeassistant/components/solaredge/translations/zh-Hant.json b/homeassistant/components/solaredge/translations/zh-Hant.json
index ab134fff57d..01c1db919cb 100644
--- a/homeassistant/components/solaredge/translations/zh-Hant.json
+++ b/homeassistant/components/solaredge/translations/zh-Hant.json
@@ -1,10 +1,15 @@
{
"config": {
"abort": {
+ "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
"site_exists": "site_id \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
},
"error": {
- "site_exists": "site_id \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "could_not_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 solaredge API",
+ "invalid_api_key": "API \u5bc6\u9470\u7121\u6548",
+ "site_exists": "site_id \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "site_not_active": "\u7db2\u7ad9\u672a\u555f\u7528"
},
"step": {
"user": {
diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py
index ca9f2e3fc13..6073d12815b 100644
--- a/homeassistant/components/solarlog/sensor.py
+++ b/homeassistant/components/solarlog/sensor.py
@@ -4,39 +4,28 @@ from urllib.parse import ParseResult, urlparse
from requests.exceptions import HTTPError, Timeout
from sunwatcher.solarlog.solarlog import SolarLog
-import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.config_entries import SOURCE_IMPORT
-from homeassistant.const import CONF_HOST, CONF_NAME
-import homeassistant.helpers.config_validation as cv
+from homeassistant.const import CONF_HOST
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
-from .const import DEFAULT_HOST, DEFAULT_NAME, DOMAIN, SCAN_INTERVAL, SENSOR_TYPES
+from .const import DOMAIN, SCAN_INTERVAL, SENSOR_TYPES
_LOGGER = logging.getLogger(__name__)
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- }
-)
-
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
- """Import YAML configuration when available."""
- hass.async_create_task(
- hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_IMPORT}, data=dict(config)
- )
+ """Set up the solarlog platform."""
+ _LOGGER.warning(
+ "Configuration of the solarlog platform in configuration.yaml is deprecated "
+ "in Home Assistant 0.119. Please remove entry from your configuration"
)
async def async_setup_entry(hass, entry, async_add_entities):
"""Add solarlog entry."""
host_entry = entry.data[CONF_HOST]
+ device_name = entry.title
url = urlparse(host_entry, "http")
netloc = url.netloc or url.path
@@ -44,8 +33,6 @@ async def async_setup_entry(hass, entry, async_add_entities):
url = ParseResult("http", netloc, path, *url[3:])
host = url.geturl()
- platform_name = entry.title
-
try:
api = await hass.async_add_executor_job(SolarLog, host)
_LOGGER.debug("Connected to Solar-Log device, setting up entries")
@@ -61,7 +48,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
# Create a new sensor for each sensor type.
entities = []
for sensor_key in SENSOR_TYPES:
- sensor = SolarlogSensor(entry.entry_id, platform_name, sensor_key, data)
+ sensor = SolarlogSensor(entry.entry_id, device_name, sensor_key, data)
entities.append(sensor)
async_add_entities(entities, True)
@@ -71,9 +58,9 @@ async def async_setup_entry(hass, entry, async_add_entities):
class SolarlogSensor(Entity):
"""Representation of a Sensor."""
- def __init__(self, entry_id, platform_name, sensor_key, data):
+ def __init__(self, entry_id, device_name, sensor_key, data):
"""Initialize the sensor."""
- self.platform_name = platform_name
+ self.device_name = device_name
self.sensor_key = sensor_key
self.data = data
self.entry_id = entry_id
@@ -92,7 +79,7 @@ class SolarlogSensor(Entity):
@property
def name(self):
"""Return the name of the sensor."""
- return f"{self.platform_name} {self._label}"
+ return f"{self.device_name} {self._label}"
@property
def unit_of_measurement(self):
@@ -109,6 +96,15 @@ class SolarlogSensor(Entity):
"""Return the state of the sensor."""
return self._state
+ @property
+ def device_info(self):
+ """Return the device information."""
+ return {
+ "identifiers": {(DOMAIN, self.entry_id)},
+ "name": self.device_name,
+ "manufacturer": "Solar-Log",
+ }
+
def update(self):
"""Get the latest data from the sensor and update the state."""
self.data.update()
diff --git a/homeassistant/components/solax/manifest.json b/homeassistant/components/solax/manifest.json
index bf2d3d72cc5..232715ebe18 100644
--- a/homeassistant/components/solax/manifest.json
+++ b/homeassistant/components/solax/manifest.json
@@ -2,6 +2,6 @@
"domain": "solax",
"name": "SolaX Power",
"documentation": "https://www.home-assistant.io/integrations/solax",
- "requirements": ["solax==0.2.4"],
+ "requirements": ["solax==0.2.5"],
"codeowners": ["@squishykid"]
}
diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py
index 99b0a2ee564..728e54b456f 100644
--- a/homeassistant/components/somfy/__init__.py
+++ b/homeassistant/components/somfy/__init__.py
@@ -164,12 +164,12 @@ class SomfyEntity(CoordinatorEntity, Entity):
return self.coordinator.data[self._id]
@property
- def unique_id(self):
+ def unique_id(self) -> str:
"""Return the unique id base on the id returned by Somfy."""
return self._id
@property
- def name(self):
+ def name(self) -> str:
"""Return the name of the device."""
return self.device.name
@@ -188,13 +188,18 @@ class SomfyEntity(CoordinatorEntity, Entity):
"manufacturer": "Somfy",
}
- def has_capability(self, capability):
+ def has_capability(self, capability: str) -> bool:
"""Test if device has a capability."""
capabilities = self.device.capabilities
return bool([c for c in capabilities if c.name == capability])
+ def has_state(self, state: str) -> bool:
+ """Test if device has a state."""
+ states = self.device.states
+ return bool([c for c in states if c.name == state])
+
@property
- def assumed_state(self):
+ def assumed_state(self) -> bool:
"""Return if the device has an assumed state."""
return not bool(self.device.states)
diff --git a/homeassistant/components/somfy/cover.py b/homeassistant/components/somfy/cover.py
index 605d58a941b..696412ac3c7 100644
--- a/homeassistant/components/somfy/cover.py
+++ b/homeassistant/components/somfy/cover.py
@@ -8,6 +8,14 @@ from homeassistant.components.cover import (
ATTR_TILT_POSITION,
DEVICE_CLASS_BLIND,
DEVICE_CLASS_SHUTTER,
+ 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.const import STATE_CLOSED, STATE_OPEN
@@ -57,10 +65,32 @@ class SomfyCover(SomfyEntity, RestoreEntity, CoverEntity):
self.cover = None
self._create_device()
- def _create_device(self):
+ def _create_device(self) -> Blind:
"""Update the device with the latest data."""
self.cover = Blind(self.device, self.api)
+ @property
+ def supported_features(self) -> int:
+ """Flag supported features."""
+ supported_features = 0
+ if self.has_capability("open"):
+ supported_features |= SUPPORT_OPEN
+ if self.has_capability("close"):
+ supported_features |= SUPPORT_CLOSE
+ if self.has_capability("stop"):
+ supported_features |= SUPPORT_STOP
+ if self.has_capability("position"):
+ supported_features |= SUPPORT_SET_POSITION
+ if self.has_capability("rotation"):
+ supported_features |= (
+ SUPPORT_OPEN_TILT
+ | SUPPORT_CLOSE_TILT
+ | SUPPORT_STOP_TILT
+ | SUPPORT_SET_TILT_POSITION
+ )
+
+ return supported_features
+
async def async_close_cover(self, **kwargs):
"""Close the cover."""
self._is_closing = True
@@ -105,10 +135,9 @@ class SomfyCover(SomfyEntity, RestoreEntity, CoverEntity):
@property
def current_cover_position(self):
"""Return the current position of cover shutter."""
- position = None
- if self.has_capability("position"):
- position = 100 - self.cover.get_position()
- return position
+ if not self.has_state("position"):
+ return None
+ return 100 - self.cover.get_position()
@property
def is_opening(self):
@@ -125,25 +154,24 @@ class SomfyCover(SomfyEntity, RestoreEntity, CoverEntity):
return self._is_closing
@property
- def is_closed(self):
+ def is_closed(self) -> bool:
"""Return if the cover is closed."""
is_closed = None
- if self.has_capability("position"):
+ if self.has_state("position"):
is_closed = self.cover.is_closed()
elif self.optimistic:
is_closed = self._closed
return is_closed
@property
- def current_cover_tilt_position(self):
+ def current_cover_tilt_position(self) -> int:
"""Return current position of cover tilt.
None is unknown, 0 is closed, 100 is fully open.
"""
- orientation = None
- if self.has_capability("rotation"):
- orientation = 100 - self.cover.orientation
- return orientation
+ if not self.has_state("orientation"):
+ return None
+ return 100 - self.cover.orientation
def set_cover_tilt_position(self, **kwargs):
"""Move the cover tilt to a specific position."""
diff --git a/homeassistant/components/somfy/translations/hu.json b/homeassistant/components/somfy/translations/hu.json
index 3df2fb30477..86927570c85 100644
--- a/homeassistant/components/somfy/translations/hu.json
+++ b/homeassistant/components/somfy/translations/hu.json
@@ -1,5 +1,8 @@
{
"config": {
+ "create_entry": {
+ "default": "Sikeres autentik\u00e1ci\u00f3"
+ },
"step": {
"pick_implementation": {
"title": "V\u00e1lassza ki a hiteles\u00edt\u00e9si m\u00f3dszert"
diff --git a/homeassistant/components/sonarr/translations/sl.json b/homeassistant/components/sonarr/translations/sl.json
new file mode 100644
index 00000000000..b8c5332be9c
--- /dev/null
+++ b/homeassistant/components/sonarr/translations/sl.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "ssl": "Uporablja SSL certifikat",
+ "verify_ssl": "Preverite SSL certifikat"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json
index 49826ebc410..66e6587b9ff 100644
--- a/homeassistant/components/sonos/manifest.json
+++ b/homeassistant/components/sonos/manifest.json
@@ -3,7 +3,7 @@
"name": "Sonos",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sonos",
- "requirements": ["pysonos==0.0.36"],
+ "requirements": ["pysonos==0.0.37"],
"ssdp": [
{
"st": "urn:schemas-upnp-org:device:ZonePlayer:1"
diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py
index cd36ee82b51..48b22256030 100644
--- a/homeassistant/components/sonos/media_player.py
+++ b/homeassistant/components/sonos/media_player.py
@@ -9,7 +9,13 @@ import urllib.parse
import async_timeout
import pysonos
from pysonos import alarms
-from pysonos.core import PLAY_MODE_BY_MEANING, PLAY_MODES
+from pysonos.core import (
+ PLAY_MODE_BY_MEANING,
+ PLAY_MODES,
+ PLAYING_LINE_IN,
+ PLAYING_RADIO,
+ PLAYING_TV,
+)
from pysonos.exceptions import SoCoException, SoCoUPnPException
import pysonos.music_library
import pysonos.snapshot
@@ -529,7 +535,6 @@ class SonosEntity(MediaPlayerEntity):
self._media_artist = None
self._media_album_name = None
self._media_title = None
- self._is_playing_local_queue = None
self._queue_position = None
self._night_sound = None
self._speech_enhance = None
@@ -725,8 +730,13 @@ class SonosEntity(MediaPlayerEntity):
def update_media(self, event=None):
"""Update information about currently playing media."""
- transport_info = self.soco.get_current_transport_info()
- new_status = transport_info.get("current_transport_state")
+ variables = event and event.variables
+
+ if variables:
+ new_status = variables["transport_state"]
+ else:
+ transport_info = self.soco.get_current_transport_info()
+ new_status = transport_info["current_transport_state"]
# Ignore transitions, we should get the target state soon
if new_status == "TRANSITIONING":
@@ -740,16 +750,18 @@ class SonosEntity(MediaPlayerEntity):
self._media_artist = None
self._media_album_name = None
self._media_title = None
+ self._queue_position = None
self._source_name = None
update_position = new_status != self._status
self._status = new_status
- self._is_playing_local_queue = self.soco.is_playing_local_queue
+ track_uri = variables["current_track_uri"] if variables else None
+ whats_playing = self.soco.whats_playing(track_uri)
- if self.soco.is_playing_tv:
+ if whats_playing == PLAYING_TV:
self.update_media_linein(SOURCE_TV)
- elif self.soco.is_playing_line_in:
+ elif whats_playing == PLAYING_LINE_IN:
self.update_media_linein(SOURCE_LINEIN)
else:
track_info = self.soco.get_current_track_info()
@@ -761,8 +773,7 @@ class SonosEntity(MediaPlayerEntity):
self._media_album_name = track_info.get("album")
self._media_title = track_info.get("title")
- if self.soco.is_radio_uri(track_info["uri"]):
- variables = event and event.variables
+ if whats_playing == PLAYING_RADIO:
self.update_media_radio(variables, track_info)
else:
self.update_media_music(update_position, track_info)
@@ -849,7 +860,9 @@ class SonosEntity(MediaPlayerEntity):
self._media_image_url = track_info.get("album_art")
- self._queue_position = int(track_info.get("playlist_position")) - 1
+ playlist_position = int(track_info.get("playlist_position"))
+ if playlist_position > 0:
+ self._queue_position = playlist_position - 1
def update_volume(self, event=None):
"""Update information about currently volume settings."""
@@ -947,8 +960,9 @@ class SonosEntity(MediaPlayerEntity):
def update_content(self, event=None):
"""Update information about available content."""
- self._set_favorites()
- self.schedule_update_ha_state()
+ if event and "favorites_update_id" in event.variables:
+ self._set_favorites()
+ self.schedule_update_ha_state()
@property
def volume_level(self):
@@ -1038,10 +1052,7 @@ class SonosEntity(MediaPlayerEntity):
@soco_coordinator
def queue_position(self):
"""If playing local queue return the position in the queue else None."""
- if self._is_playing_local_queue:
- return self._queue_position
-
- return None
+ return self._queue_position
@property
@soco_coordinator
diff --git a/homeassistant/components/sonos/services.yaml b/homeassistant/components/sonos/services.yaml
index effb19c47b7..8a35e9a7790 100644
--- a/homeassistant/components/sonos/services.yaml
+++ b/homeassistant/components/sonos/services.yaml
@@ -7,6 +7,10 @@ join:
entity_id:
description: Name(s) of entities that will join the master.
example: "media_player.living_room_sonos"
+ selector:
+ entity:
+ integration: sonos
+ domain: media_player
unjoin:
description: Unjoin the player from a group.
@@ -14,6 +18,10 @@ unjoin:
entity_id:
description: Name(s) of entities that will be unjoined from their group.
example: "media_player.living_room_sonos"
+ selector:
+ entity:
+ integration: sonos
+ domain: media_player
snapshot:
description: Take a snapshot of the media player.
@@ -21,6 +29,10 @@ snapshot:
entity_id:
description: Name(s) of entities that will be snapshot.
example: "media_player.living_room_sonos"
+ selector:
+ entity:
+ integration: sonos
+ domain: media_player
with_group:
description: True (default) or False. Also snapshot the group layout.
example: "true"
@@ -31,6 +43,10 @@ restore:
entity_id:
description: Name(s) of entities that will be restored.
example: "media_player.living_room_sonos"
+ selector:
+ entity:
+ integration: sonos
+ domain: media_player
with_group:
description: True (default) or False. Also restore the group layout.
example: "true"
@@ -41,6 +57,10 @@ set_sleep_timer:
entity_id:
description: Name(s) of entities that will have a timer set.
example: "media_player.living_room_sonos"
+ selector:
+ entity:
+ integration: sonos
+ domain: media_player
sleep_time:
description: Number of seconds to set the timer.
example: "900"
@@ -51,6 +71,10 @@ clear_sleep_timer:
entity_id:
description: Name(s) of entities that will have the timer cleared.
example: "media_player.living_room_sonos"
+ selector:
+ entity:
+ integration: sonos
+ domain: media_player
set_option:
description: Set Sonos sound options.
@@ -58,6 +82,10 @@ set_option:
entity_id:
description: Name(s) of entities that will have options set.
example: "media_player.living_room_sonos"
+ selector:
+ entity:
+ integration: sonos
+ domain: media_player
night_sound:
description: Enable Night Sound mode
example: "true"
@@ -74,6 +102,10 @@ play_queue:
entity_id:
description: Name(s) of entities that will start playing.
example: "media_player.living_room_sonos"
+ selector:
+ entity:
+ integration: sonos
+ domain: media_player
queue_position:
description: Position of the song in the queue to start playing from.
example: "0"
@@ -84,6 +116,10 @@ remove_from_queue:
entity_id:
description: Name(s) of entities that will remove an item.
example: "media_player.living_room_sonos"
+ selector:
+ entity:
+ integration: sonos
+ domain: media_player
queue_position:
description: Position in the queue to remove.
example: "0"
diff --git a/homeassistant/components/speedtestdotnet/translations/no.json b/homeassistant/components/speedtestdotnet/translations/no.json
index 98a372f81dc..a079bc1fa7b 100644
--- a/homeassistant/components/speedtestdotnet/translations/no.json
+++ b/homeassistant/components/speedtestdotnet/translations/no.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.",
- "wrong_server_id": "Server-ID er ikke gyldig"
+ "wrong_server_id": "Server ID er ikke gyldig"
},
"step": {
"user": {
diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py
index 10fe70c611e..e28e1fcf315 100644
--- a/homeassistant/components/spotify/__init__.py
+++ b/homeassistant/components/spotify/__init__.py
@@ -1,5 +1,6 @@
"""The spotify integration."""
+import aiohttp
from spotipy import Spotify, SpotifyException
import voluptuous as vol
@@ -62,7 +63,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Spotify from a config entry."""
implementation = await async_get_config_entry_implementation(hass, entry)
session = OAuth2Session(hass, entry, implementation)
- await session.async_ensure_token_valid()
+
+ try:
+ await session.async_ensure_token_valid()
+ except aiohttp.ClientError as err:
+ raise ConfigEntryNotReady from err
+
spotify = Spotify(auth=session.token["access_token"])
try:
diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py
index 5f376493c65..ef3f1224a4b 100644
--- a/homeassistant/components/spotify/media_player.py
+++ b/homeassistant/components/spotify/media_player.py
@@ -25,12 +25,16 @@ from homeassistant.components.media_player.const import (
MEDIA_TYPE_MUSIC,
MEDIA_TYPE_PLAYLIST,
MEDIA_TYPE_TRACK,
+ REPEAT_MODE_ALL,
+ REPEAT_MODE_OFF,
+ REPEAT_MODE_ONE,
SUPPORT_BROWSE_MEDIA,
SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE,
SUPPORT_PLAY,
SUPPORT_PLAY_MEDIA,
SUPPORT_PREVIOUS_TRACK,
+ SUPPORT_REPEAT_SET,
SUPPORT_SEEK,
SUPPORT_SELECT_SOURCE,
SUPPORT_SHUFFLE_SET,
@@ -71,12 +75,19 @@ SUPPORT_SPOTIFY = (
| SUPPORT_PLAY
| SUPPORT_PLAY_MEDIA
| SUPPORT_PREVIOUS_TRACK
+ | SUPPORT_REPEAT_SET
| SUPPORT_SEEK
| SUPPORT_SELECT_SOURCE
| SUPPORT_SHUFFLE_SET
| SUPPORT_VOLUME_SET
)
+REPEAT_MODE_MAPPING = {
+ "context": REPEAT_MODE_ALL,
+ "off": REPEAT_MODE_OFF,
+ "track": REPEAT_MODE_ONE,
+}
+
BROWSE_LIMIT = 48
MEDIA_TYPE_SHOW = "show"
@@ -375,6 +386,12 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
"""Shuffling state."""
return bool(self._currently_playing.get("shuffle_state"))
+ @property
+ def repeat(self) -> Optional[str]:
+ """Return current repeat mode."""
+ repeat_state = self._currently_playing.get("repeat_state")
+ return REPEAT_MODE_MAPPING.get(repeat_state)
+
@property
def supported_features(self) -> int:
"""Return the media player features that are supported."""
@@ -449,6 +466,13 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
"""Enable/Disable shuffle mode."""
self._spotify.shuffle(shuffle)
+ @spotify_exception_handler
+ def set_repeat(self, repeat: str) -> None:
+ """Set repeat mode."""
+ for spotify, home_assistant in REPEAT_MODE_MAPPING.items():
+ if home_assistant == repeat:
+ self._spotify.repeat(spotify)
+
@spotify_exception_handler
def update(self) -> None:
"""Update state and attributes."""
diff --git a/homeassistant/components/spotify/strings.json b/homeassistant/components/spotify/strings.json
index 8745df8e56f..74df79c4d78 100644
--- a/homeassistant/components/spotify/strings.json
+++ b/homeassistant/components/spotify/strings.json
@@ -16,5 +16,10 @@
"reauth_account_mismatch": "The Spotify account authenticated with, does not match the account needed re-authentication."
},
"create_entry": { "default": "Successfully authenticated with Spotify." }
+ },
+ "system_health": {
+ "info": {
+ "api_endpoint_reachable": "Spotify API endpoint reachable"
+ }
}
}
diff --git a/homeassistant/components/spotify/system_health.py b/homeassistant/components/spotify/system_health.py
new file mode 100644
index 00000000000..a22f7b8a821
--- /dev/null
+++ b/homeassistant/components/spotify/system_health.py
@@ -0,0 +1,20 @@
+"""Provide info to system health."""
+from homeassistant.components import system_health
+from homeassistant.core import HomeAssistant, callback
+
+
+@callback
+def async_register(
+ hass: HomeAssistant, register: system_health.SystemHealthRegistration
+) -> None:
+ """Register system health callbacks."""
+ register.async_register_info(system_health_info)
+
+
+async def system_health_info(hass):
+ """Get info for the info page."""
+ return {
+ "api_endpoint_reachable": system_health.async_check_can_reach_url(
+ hass, "https://api.spotify.com"
+ )
+ }
diff --git a/homeassistant/components/spotify/translations/en.json b/homeassistant/components/spotify/translations/en.json
index 1c04a5868bc..73ea219105b 100644
--- a/homeassistant/components/spotify/translations/en.json
+++ b/homeassistant/components/spotify/translations/en.json
@@ -18,5 +18,10 @@
"title": "Reauthenticate Integration"
}
}
+ },
+ "system_health": {
+ "info": {
+ "api_endpoint_reachable": "Spotify API endpoint reachable"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/spotify/translations/es.json b/homeassistant/components/spotify/translations/es.json
index 68783fd3caf..ce1966a8edd 100644
--- a/homeassistant/components/spotify/translations/es.json
+++ b/homeassistant/components/spotify/translations/es.json
@@ -18,5 +18,10 @@
"title": "Volver a autenticar con Spotify"
}
}
+ },
+ "system_health": {
+ "info": {
+ "api_endpoint_reachable": "Se puede acceder al punto de conexi\u00f3n de la API de Spotify"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/spotify/translations/et.json b/homeassistant/components/spotify/translations/et.json
index f3ceec4fa5c..01583d1b0f0 100644
--- a/homeassistant/components/spotify/translations/et.json
+++ b/homeassistant/components/spotify/translations/et.json
@@ -18,5 +18,10 @@
"title": "Autendi Spotify uuesti"
}
}
+ },
+ "system_health": {
+ "info": {
+ "api_endpoint_reachable": "Spotify API l\u00f5pp-punkt on k\u00e4ttesaadav"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/spotify/translations/it.json b/homeassistant/components/spotify/translations/it.json
index c201c70cd1c..6911d38be00 100644
--- a/homeassistant/components/spotify/translations/it.json
+++ b/homeassistant/components/spotify/translations/it.json
@@ -18,5 +18,10 @@
"title": "Reautenticare l'integrazione"
}
}
+ },
+ "system_health": {
+ "info": {
+ "api_endpoint_reachable": "Endpoint API Spotify raggiungibile"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/spotify/translations/no.json b/homeassistant/components/spotify/translations/no.json
index f400a2c11ad..eee2386a921 100644
--- a/homeassistant/components/spotify/translations/no.json
+++ b/homeassistant/components/spotify/translations/no.json
@@ -18,5 +18,10 @@
"title": "Bekreft integrering p\u00e5 nytt"
}
}
+ },
+ "system_health": {
+ "info": {
+ "api_endpoint_reachable": "Spotify API-endepunkt n\u00e5s"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/spotify/translations/pl.json b/homeassistant/components/spotify/translations/pl.json
index c503dfccda5..f9e6f429214 100644
--- a/homeassistant/components/spotify/translations/pl.json
+++ b/homeassistant/components/spotify/translations/pl.json
@@ -18,5 +18,10 @@
"title": "Ponownie uwierzytelnij integracj\u0119"
}
}
+ },
+ "system_health": {
+ "info": {
+ "api_endpoint_reachable": "Dost\u0119pno\u015b\u0107 punktu ko\u0144cowego API Spotify"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/spotify/translations/ru.json b/homeassistant/components/spotify/translations/ru.json
index c9451ad76f3..722cb125169 100644
--- a/homeassistant/components/spotify/translations/ru.json
+++ b/homeassistant/components/spotify/translations/ru.json
@@ -18,5 +18,10 @@
"title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f"
}
}
+ },
+ "system_health": {
+ "info": {
+ "api_endpoint_reachable": "\u0414\u043e\u0441\u0442\u0443\u043f \u043a API Spotify"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/spotify/translations/zh-Hant.json b/homeassistant/components/spotify/translations/zh-Hant.json
index 9518c53732d..ce35507e661 100644
--- a/homeassistant/components/spotify/translations/zh-Hant.json
+++ b/homeassistant/components/spotify/translations/zh-Hant.json
@@ -18,5 +18,10 @@
"title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408"
}
}
+ },
+ "system_health": {
+ "info": {
+ "api_endpoint_reachable": "Spotify API \u53ef\u9054\u7aef\u9ede"
+ }
}
}
\ No newline at end of file
diff --git a/homeassistant/components/srp_energy/__init__.py b/homeassistant/components/srp_energy/__init__.py
new file mode 100644
index 00000000000..f7cc1ff8c16
--- /dev/null
+++ b/homeassistant/components/srp_energy/__init__.py
@@ -0,0 +1,52 @@
+"""The SRP Energy integration."""
+import logging
+
+from srpenergy.client import SrpEnergyClient
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME
+from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import ConfigEntryNotReady
+
+from .const import SRP_ENERGY_DOMAIN
+
+_LOGGER = logging.getLogger(__name__)
+
+
+PLATFORMS = ["sensor"]
+
+
+async def async_setup(hass, config):
+ """Old way of setting up the srp_energy component."""
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
+ """Set up the SRP Energy component from a config entry."""
+ # Store an SrpEnergyClient object for your srp_energy to access
+ try:
+ srp_energy_client = SrpEnergyClient(
+ entry.data.get(CONF_ID),
+ entry.data.get(CONF_USERNAME),
+ entry.data.get(CONF_PASSWORD),
+ )
+ hass.data[SRP_ENERGY_DOMAIN] = srp_energy_client
+ except (Exception) as ex:
+ _LOGGER.error("Unable to connect to Srp Energy: %s", str(ex))
+ raise ConfigEntryNotReady from ex
+
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, "sensor")
+ )
+
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
+ """Unload a config entry."""
+ # unload srp client
+ hass.data[SRP_ENERGY_DOMAIN] = None
+ # Remove config entry
+ await hass.config_entries.async_forward_entry_unload(config_entry, "sensor")
+
+ return True
diff --git a/homeassistant/components/srp_energy/config_flow.py b/homeassistant/components/srp_energy/config_flow.py
new file mode 100644
index 00000000000..b65b93e0108
--- /dev/null
+++ b/homeassistant/components/srp_energy/config_flow.py
@@ -0,0 +1,71 @@
+"""Config flow for SRP Energy."""
+import logging
+
+from srpenergy.client import SrpEnergyClient
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_ID, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
+
+from .const import ( # pylint:disable=unused-import
+ CONF_IS_TOU,
+ DEFAULT_NAME,
+ SRP_ENERGY_DOMAIN,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class ConfigFlow(config_entries.ConfigFlow, domain=SRP_ENERGY_DOMAIN):
+ """Handle a config flow for SRP Energy."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
+
+ config = {
+ vol.Required(CONF_ID): str,
+ vol.Required(CONF_USERNAME): str,
+ vol.Required(CONF_PASSWORD): str,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): str,
+ vol.Optional(CONF_IS_TOU, default=False): bool,
+ }
+
+ async def async_step_user(self, user_input=None):
+ """Handle a flow initialized by the user."""
+ errors = {}
+
+ if self._async_current_entries():
+ return self.async_abort(reason="single_instance_allowed")
+
+ if user_input is not None:
+ try:
+
+ srp_client = SrpEnergyClient(
+ user_input[CONF_ID],
+ user_input[CONF_USERNAME],
+ user_input[CONF_PASSWORD],
+ )
+
+ is_valid = await self.hass.async_add_executor_job(srp_client.validate)
+
+ if is_valid:
+ return self.async_create_entry(
+ title=user_input[CONF_NAME], data=user_input
+ )
+
+ errors["base"] = "invalid_auth"
+
+ except ValueError:
+ errors["base"] = "invalid_account"
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected exception")
+ errors["base"] = "unknown"
+
+ return self.async_show_form(
+ step_id="user", data_schema=vol.Schema(self.config), errors=errors
+ )
+
+ async def async_step_import(self, import_config):
+ """Import from config."""
+ # Validate config values
+ return await self.async_step_user(user_input=import_config)
diff --git a/homeassistant/components/srp_energy/const.py b/homeassistant/components/srp_energy/const.py
new file mode 100644
index 00000000000..527a1ed78b1
--- /dev/null
+++ b/homeassistant/components/srp_energy/const.py
@@ -0,0 +1,15 @@
+"""Constants for the SRP Energy integration."""
+from datetime import timedelta
+
+SRP_ENERGY_DOMAIN = "srp_energy"
+DEFAULT_NAME = "SRP Energy"
+
+CONF_IS_TOU = "is_tou"
+
+ATTRIBUTION = "Powered by SRP Energy"
+MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=1440)
+
+SENSOR_NAME = "Usage"
+SENSOR_TYPE = "usage"
+
+ICON = "mdi:flash"
diff --git a/homeassistant/components/srp_energy/manifest.json b/homeassistant/components/srp_energy/manifest.json
new file mode 100644
index 00000000000..fb051fc7b2f
--- /dev/null
+++ b/homeassistant/components/srp_energy/manifest.json
@@ -0,0 +1,16 @@
+{
+ "domain": "srp_energy",
+ "name": "SRP Energy",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/integrations/srp_energy",
+ "requirements": [
+ "srpenergy==1.3.2"
+ ],
+ "ssdp": [],
+ "zeroconf": [],
+ "homekit": {},
+ "dependencies": [],
+ "codeowners": [
+ "@briglx"
+ ]
+}
\ No newline at end of file
diff --git a/homeassistant/components/srp_energy/sensor.py b/homeassistant/components/srp_energy/sensor.py
new file mode 100644
index 00000000000..36a8798b05b
--- /dev/null
+++ b/homeassistant/components/srp_energy/sensor.py
@@ -0,0 +1,153 @@
+"""Support for SRP Energy Sensor."""
+from datetime import datetime, timedelta
+import logging
+
+import async_timeout
+from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout
+
+from homeassistant.const import ATTR_ATTRIBUTION, ENERGY_KILO_WATT_HOUR
+from homeassistant.helpers import entity
+from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
+
+from .const import (
+ ATTRIBUTION,
+ DEFAULT_NAME,
+ ICON,
+ MIN_TIME_BETWEEN_UPDATES,
+ SENSOR_NAME,
+ SENSOR_TYPE,
+ SRP_ENERGY_DOMAIN,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Set up the SRP Energy Usage sensor."""
+ # API object stored here by __init__.py
+ is_time_of_use = False
+ api = hass.data[SRP_ENERGY_DOMAIN]
+ if entry and entry.data:
+ is_time_of_use = entry.data["is_tou"]
+
+ async def async_update_data():
+ """Fetch data from API endpoint.
+
+ This is the place to pre-process the data to lookup tables
+ so entities can quickly look up their data.
+ """
+ try:
+ # Fetch srp_energy data
+ start_date = datetime.now() + timedelta(days=-1)
+ end_date = datetime.now()
+ with async_timeout.timeout(10):
+ hourly_usage = await hass.async_add_executor_job(
+ api.usage,
+ start_date,
+ end_date,
+ is_time_of_use,
+ )
+
+ previous_daily_usage = 0.0
+ for _, _, _, kwh, _ in hourly_usage:
+ previous_daily_usage += float(kwh)
+ return previous_daily_usage
+ except (TimeoutError) as timeout_err:
+ raise UpdateFailed("Timeout communicating with API") from timeout_err
+ except (ConnectError, HTTPError, Timeout, ValueError, TypeError) as err:
+ raise UpdateFailed(f"Error communicating with API: {err}") from err
+
+ coordinator = DataUpdateCoordinator(
+ hass,
+ _LOGGER,
+ name="sensor",
+ update_method=async_update_data,
+ update_interval=MIN_TIME_BETWEEN_UPDATES,
+ )
+
+ # Fetch initial data so we have data when entities subscribe
+ await coordinator.async_refresh()
+
+ async_add_entities([SrpEntity(coordinator)])
+
+
+class SrpEntity(entity.Entity):
+ """Implementation of a Srp Energy Usage sensor."""
+
+ def __init__(self, coordinator):
+ """Initialize the SrpEntity class."""
+ self._name = SENSOR_NAME
+ self.type = SENSOR_TYPE
+ self.coordinator = coordinator
+ self._unit_of_measurement = ENERGY_KILO_WATT_HOUR
+ self._state = None
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return f"{DEFAULT_NAME} {self._name}"
+
+ @property
+ def unique_id(self):
+ """Return sensor unique_id."""
+ return self.type
+
+ @property
+ def state(self):
+ """Return the state of the device."""
+ if self._state:
+ return f"{self._state:.2f}"
+ return None
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement of this entity, if any."""
+ return self._unit_of_measurement
+
+ @property
+ def icon(self):
+ """Return icon."""
+ return ICON
+
+ @property
+ def usage(self):
+ """Return entity state."""
+ if self.coordinator.data:
+ return f"{self.coordinator.data:.2f}"
+ return None
+
+ @property
+ def should_poll(self):
+ """No need to poll. Coordinator notifies entity of updates."""
+ return False
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes."""
+ if not self.coordinator.data:
+ return None
+ attributes = {
+ ATTR_ATTRIBUTION: ATTRIBUTION,
+ }
+
+ return attributes
+
+ @property
+ def available(self):
+ """Return if entity is available."""
+ return self.coordinator.last_update_success
+
+ async def async_added_to_hass(self):
+ """When entity is added to hass."""
+ self.async_on_remove(
+ self.coordinator.async_add_listener(self.async_write_ha_state)
+ )
+ if self.coordinator.data:
+ self._state = self.coordinator.data
+
+ async def async_update(self):
+ """Update the entity.
+
+ Only used by the generic entity update service.
+ """
+ await self.coordinator.async_request_refresh()
diff --git a/homeassistant/components/srp_energy/strings.json b/homeassistant/components/srp_energy/strings.json
new file mode 100644
index 00000000000..8dce61229a9
--- /dev/null
+++ b/homeassistant/components/srp_energy/strings.json
@@ -0,0 +1,24 @@
+{
+ "title": "SRP Energy",
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "id": "Account Id",
+ "username": "[%key:common::config_flow::data::username%]",
+ "password": "[%key:common::config_flow::data::password%]",
+ "is_tou": "Is Time of Use Plan"
+ }
+ }
+ },
+ "error": {
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
+ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
+ "invalid_account": "Account ID should be a 9 digit number",
+ "unknown": "[%key:common::config_flow::error::unknown%]"
+ },
+ "abort": {
+ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
+ }
+ }
+}
diff --git a/homeassistant/components/srp_energy/translations/ca.json b/homeassistant/components/srp_energy/translations/ca.json
new file mode 100644
index 00000000000..c6617e617d4
--- /dev/null
+++ b/homeassistant/components/srp_energy/translations/ca.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3."
+ },
+ "error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3",
+ "invalid_account": "L'ID del compte ha de ser un n\u00famero de 9 d\u00edgits",
+ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida",
+ "unknown": "Error inesperat"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "ID del compte",
+ "is_tou": "\u00c9s tarifa de temps d'\u00fas",
+ "password": "Contrasenya",
+ "username": "Nom d'usuari"
+ }
+ }
+ }
+ },
+ "title": "SRP Energy"
+}
\ No newline at end of file
diff --git a/homeassistant/components/srp_energy/translations/cs.json b/homeassistant/components/srp_energy/translations/cs.json
new file mode 100644
index 00000000000..74b4bd53090
--- /dev/null
+++ b/homeassistant/components/srp_energy/translations/cs.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace."
+ },
+ "error": {
+ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit",
+ "invalid_account": "ID \u00fa\u010dtu by m\u011blo b\u00fdt 9m\u00edstn\u00e9 \u010d\u00edslo",
+ "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed",
+ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "ID \u00fa\u010dtu",
+ "password": "Heslo",
+ "username": "U\u017eivatelsk\u00e9 jm\u00e9no"
+ }
+ }
+ }
+ },
+ "title": "SRP Energy"
+}
\ No newline at end of file
diff --git a/homeassistant/components/srp_energy/translations/en.json b/homeassistant/components/srp_energy/translations/en.json
new file mode 100644
index 00000000000..99926b18b4f
--- /dev/null
+++ b/homeassistant/components/srp_energy/translations/en.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Already configured. Only a single configuration possible."
+ },
+ "error": {
+ "cannot_connect": "Failed to connect",
+ "invalid_account": "Account ID should be a 9 digit number",
+ "invalid_auth": "Invalid authentication",
+ "unknown": "Unexpected error"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "Account Id",
+ "is_tou": "Is Time of Use Plan",
+ "password": "Password",
+ "username": "Username"
+ }
+ }
+ }
+ },
+ "title": "SRP Energy"
+}
\ No newline at end of file
diff --git a/homeassistant/components/srp_energy/translations/es.json b/homeassistant/components/srp_energy/translations/es.json
new file mode 100644
index 00000000000..de15bb80551
--- /dev/null
+++ b/homeassistant/components/srp_energy/translations/es.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Ya configurado. Solo es posible una \u00fanica configuraci\u00f3n."
+ },
+ "error": {
+ "cannot_connect": "No se pudo conectar",
+ "invalid_account": "El ID de la cuenta debe ser un n\u00famero de 9 d\u00edgitos",
+ "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida",
+ "unknown": "Error inesperado"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "ID de la cuenta",
+ "is_tou": "Es el plan de tiempo de uso",
+ "password": "Contrase\u00f1a",
+ "username": "Nombre de usuario"
+ }
+ }
+ }
+ },
+ "title": "SRP Energy"
+}
\ No newline at end of file
diff --git a/homeassistant/components/srp_energy/translations/et.json b/homeassistant/components/srp_energy/translations/et.json
new file mode 100644
index 00000000000..558bb4a19ed
--- /dev/null
+++ b/homeassistant/components/srp_energy/translations/et.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine."
+ },
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus",
+ "invalid_account": "Konto ID peab olema 9-kohaline number",
+ "invalid_auth": "Vigane autentimine",
+ "unknown": "Ootamatu t\u00f5rge"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "Konto ID",
+ "is_tou": "Kas kasutusaja plaan",
+ "password": "Salas\u00f5na",
+ "username": "Kasutajanimi"
+ }
+ }
+ }
+ },
+ "title": ""
+}
\ No newline at end of file
diff --git a/homeassistant/components/srp_energy/translations/it.json b/homeassistant/components/srp_energy/translations/it.json
new file mode 100644
index 00000000000..dd8b93a8de2
--- /dev/null
+++ b/homeassistant/components/srp_energy/translations/it.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione."
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi",
+ "invalid_account": "L'ID account deve essere un numero di 9 cifre",
+ "invalid_auth": "Autenticazione non valida",
+ "unknown": "Errore imprevisto"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "Account ID",
+ "is_tou": "E' la tariffa per periodo di utilizzo",
+ "password": "Password",
+ "username": "Nome utente"
+ }
+ }
+ }
+ },
+ "title": "SRP Energy"
+}
\ No newline at end of file
diff --git a/homeassistant/components/srp_energy/translations/ka.json b/homeassistant/components/srp_energy/translations/ka.json
new file mode 100644
index 00000000000..f4e15ad5d9e
--- /dev/null
+++ b/homeassistant/components/srp_energy/translations/ka.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u10e3\u10d9\u10d5\u10d4 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10da\u10d0. \u10e8\u10d4\u10e1\u10d0\u10eb\u10da\u10d4\u10d1\u10d4\u10da\u10d8\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0"
+ },
+ "error": {
+ "cannot_connect": "\u10e8\u10d4\u10d4\u10e0\u10d7\u10d0\u10d1\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0",
+ "invalid_account": "\u10d0\u10dc\u10d2\u10d0\u10e0\u10d8\u10e8\u10d8\u10e1 ID \u10e3\u10dc\u10d3\u10d0 \u10d8\u10e7\u10dd\u10e1 9 \u10ea\u10d8\u10e4\u10e0\u10d8\u10d0\u10dc\u10d8 \u10dc\u10dd\u10db\u10d4\u10e0\u10d8",
+ "invalid_auth": "\u10d0\u10e0\u10d0\u10e1\u10ec\u10dd\u10e0\u10d8 \u10d0\u10e3\u10d7\u10d4\u10dc\u10d7\u10d8\u10e4\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0",
+ "unknown": "\u10d2\u10d0\u10e3\u10d7\u10d5\u10d0\u10da\u10d8\u10e1\u10ec\u10d8\u10dc\u10d4\u10d1\u10d4\u10da\u10d8 \u10e8\u10d4\u10ea\u10d3\u10dd\u10db\u10d0"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "\u10d0\u10dc\u10d2\u10d0\u10e0\u10d8\u10e8\u10d8\u10e1 ID",
+ "is_tou": "\u10d2\u10d4\u10d2\u10db\u10d8\u10e1 \u10d2\u10d0\u10db\u10dd\u10e7\u10d4\u10dc\u10d4\u10d1\u10d8\u10e1 \u10d3\u10e0\u10dd",
+ "password": "\u10de\u10d0\u10e0\u10dd\u10da\u10d8",
+ "username": "\u10db\u10dd\u10db\u10ee\u10db\u10d0\u10e0\u10d4\u10d1\u10da\u10d8\u10e1 \u10e1\u10d0\u10ee\u10d4\u10da\u10d8"
+ }
+ }
+ }
+ },
+ "title": "SRP Energy"
+}
\ No newline at end of file
diff --git a/homeassistant/components/srp_energy/translations/no.json b/homeassistant/components/srp_energy/translations/no.json
new file mode 100644
index 00000000000..5505e140cd3
--- /dev/null
+++ b/homeassistant/components/srp_energy/translations/no.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig."
+ },
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes",
+ "invalid_account": "Konto ID skal v\u00e6re et ni-sifret nummer",
+ "invalid_auth": "Ugyldig godkjenning",
+ "unknown": "Uventet feil"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "Konto ID",
+ "is_tou": "Er Time of Use Plan",
+ "password": "Passord",
+ "username": "Brukernavn"
+ }
+ }
+ }
+ },
+ "title": "SRP Energy"
+}
\ No newline at end of file
diff --git a/homeassistant/components/srp_energy/translations/pl.json b/homeassistant/components/srp_energy/translations/pl.json
new file mode 100644
index 00000000000..f89165a9065
--- /dev/null
+++ b/homeassistant/components/srp_energy/translations/pl.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja."
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
+ "invalid_account": "Identyfikator konta powinien sk\u0142ada\u0107 si\u0119 z 9 cyfr",
+ "invalid_auth": "Niepoprawne uwierzytelnienie",
+ "unknown": "Nieoczekiwany b\u0142\u0105d"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "Identyfikator konta",
+ "is_tou": "Taryfa dzie\u0144/noc?",
+ "password": "Has\u0142o",
+ "username": "Nazwa u\u017cytkownika"
+ }
+ }
+ }
+ },
+ "title": "SRP Energy"
+}
\ No newline at end of file
diff --git a/homeassistant/components/srp_energy/translations/ru.json b/homeassistant/components/srp_energy/translations/ru.json
new file mode 100644
index 00000000000..3fcbace37df
--- /dev/null
+++ b/homeassistant/components/srp_energy/translations/ru.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
+ "invalid_account": "ID \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0430 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c 9-\u0437\u043d\u0430\u0447\u043d\u044b\u043c \u0447\u0438\u0441\u043b\u043e\u043c.",
+ "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.",
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "ID \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0430",
+ "is_tou": "\u041f\u043b\u0430\u043d \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "username": "\u041b\u043e\u0433\u0438\u043d"
+ }
+ }
+ }
+ },
+ "title": "SRP Energy"
+}
\ No newline at end of file
diff --git a/homeassistant/components/srp_energy/translations/zh-Hant.json b/homeassistant/components/srp_energy/translations/zh-Hant.json
new file mode 100644
index 00000000000..f8cb25f7df5
--- /dev/null
+++ b/homeassistant/components/srp_energy/translations/zh-Hant.json
@@ -0,0 +1,24 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002"
+ },
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
+ "invalid_account": "\u5e33\u865f ID \u5fc5\u9808\u70ba 9 \u4f4d\u6578\u5b57",
+ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548",
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "id": "\u5e33\u865f ID",
+ "is_tou": "\u662f\u5426\u70ba Time of Use \u65b9\u6848",
+ "password": "\u5bc6\u78bc",
+ "username": "\u4f7f\u7528\u8005\u540d\u7a31"
+ }
+ }
+ }
+ },
+ "title": "SRP Energy"
+}
\ No newline at end of file
diff --git a/homeassistant/components/starline/translations/no.json b/homeassistant/components/starline/translations/no.json
index 36545f3efd7..d6d6acb39d3 100644
--- a/homeassistant/components/starline/translations/no.json
+++ b/homeassistant/components/starline/translations/no.json
@@ -8,7 +8,7 @@
"step": {
"auth_app": {
"data": {
- "app_id": "App-ID",
+ "app_id": "",
"app_secret": "Hemmelig"
},
"description": "Applikasjons-ID og hemmelig kode fra [StarLine utviklerkonto](https://my.starline.ru/developer)",
diff --git a/homeassistant/components/switch/translations/zh-Hans.json b/homeassistant/components/switch/translations/zh-Hans.json
index 8820cb9e435..a18455aec6a 100644
--- a/homeassistant/components/switch/translations/zh-Hans.json
+++ b/homeassistant/components/switch/translations/zh-Hans.json
@@ -1,10 +1,13 @@
{
"device_automation": {
"action_type": {
- "turn_off": "\u5173\u95ed {entity_name}"
+ "toggle": "\u5207\u6362 {entity_name} \u5f00\u5173",
+ "turn_off": "\u5173\u95ed {entity_name}",
+ "turn_on": "\u6253\u5f00 {entity_name}"
},
"condition_type": {
- "is_off": "{entity_name} \u5df2\u5173\u95ed"
+ "is_off": "{entity_name} \u5df2\u5173\u95ed",
+ "is_on": "{entity_name} \u5df2\u5f00\u542f"
},
"trigger_type": {
"turned_off": "{entity_name} \u88ab\u5173\u95ed",
diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py
index d8acf29016c..06696865d03 100644
--- a/homeassistant/components/synology_dsm/__init__.py
+++ b/homeassistant/components/synology_dsm/__init__.py
@@ -185,7 +185,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
try:
await api.async_setup()
except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as err:
- _LOGGER.debug("async_setup_entry - Unable to connect to DSM: %s", str(err))
+ _LOGGER.debug("async_setup_entry - Unable to connect to DSM: %s", err)
raise ConfigEntryNotReady from err
undo_listener = entry.add_update_listener(_async_update_listener)
@@ -244,9 +244,6 @@ async def _async_setup_services(hass: HomeAssistantType):
async def service_handler(call: ServiceCall):
"""Handle service call."""
- _LOGGER.debug(
- "service_handler - called as '%s' with data: %s", call.service, call.data
- )
serial = call.data.get(CONF_SERIAL)
dsm_devices = hass.data[DOMAIN]
@@ -268,7 +265,7 @@ async def _async_setup_services(hass: HomeAssistantType):
)
return
- _LOGGER.info("%s DSM with serial %s", call.service, serial)
+ _LOGGER.debug("%s DSM with serial %s", call.service, serial)
dsm_api = dsm_device[SYNO_API]
if call.service == SERVICE_REBOOT:
await dsm_api.async_reboot()
@@ -276,9 +273,6 @@ async def _async_setup_services(hass: HomeAssistantType):
await dsm_api.system.shutdown()
for service in SERVICES:
- _LOGGER.debug(
- "_async_setup_services - register service %s on domain %s", service, DOMAIN
- )
hass.services.async_register(DOMAIN, service, service_handler)
@@ -445,14 +439,14 @@ class SynoApi:
if not self.system:
_LOGGER.debug("async_reboot - System API not ready: %s", self)
return
- self._hass.async_add_executor_job(self.system.reboot)
+ await self._hass.async_add_executor_job(self.system.reboot)
async def async_shutdown(self):
"""Shutdown NAS."""
if not self.system:
_LOGGER.debug("async_shutdown - System API not ready: %s", self)
return
- self._hass.async_add_executor_job(self.system.shutdown)
+ await self._hass.async_add_executor_job(self.system.shutdown)
async def async_unload(self):
"""Stop interacting with the NAS and prepare for removal from hass."""
@@ -465,13 +459,14 @@ class SynoApi:
await self._hass.async_add_executor_job(
self.dsm.update, self._with_information
)
- async_dispatcher_send(self._hass, self.signal_sensor_update)
except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as err:
_LOGGER.warning(
"async_update - connection error during update, fallback by reloading the entry"
)
- _LOGGER.debug("async_update - exception: %s", str(err))
+ _LOGGER.debug("async_update - exception: %s", err)
await self._hass.config_entries.async_reload(self._entry.entry_id)
+ return
+ async_dispatcher_send(self._hass, self.signal_sensor_update)
class SynologyDSMEntity(Entity):
diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json
index 1dc0f2a3242..45cad8acfc2 100644
--- a/homeassistant/components/synology_dsm/manifest.json
+++ b/homeassistant/components/synology_dsm/manifest.json
@@ -2,7 +2,7 @@
"domain": "synology_dsm",
"name": "Synology DSM",
"documentation": "https://www.home-assistant.io/integrations/synology_dsm",
- "requirements": ["python-synology==1.0.0"],
+ "requirements": ["synologydsm-api==1.0.1"],
"codeowners": ["@hacf-fr", "@Quentame", "@mib1185"],
"config_flow": true,
"ssdp": [
diff --git a/homeassistant/components/synology_dsm/translations/hu.json b/homeassistant/components/synology_dsm/translations/hu.json
index 23cb168b9a5..29e520de432 100644
--- a/homeassistant/components/synology_dsm/translations/hu.json
+++ b/homeassistant/components/synology_dsm/translations/hu.json
@@ -1,5 +1,8 @@
{
"config": {
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
"step": {
"link": {
"data": {
diff --git a/homeassistant/components/synology_dsm/translations/ka.json b/homeassistant/components/synology_dsm/translations/ka.json
new file mode 100644
index 00000000000..507e374be42
--- /dev/null
+++ b/homeassistant/components/synology_dsm/translations/ka.json
@@ -0,0 +1,16 @@
+{
+ "config": {
+ "step": {
+ "link": {
+ "data": {
+ "verify_ssl": "SSL \u10e1\u10d4\u10e0\u10e2\u10d8\u10e4\u10d8\u10d9\u10d0\u10e2\u10d8\u10e1 \u10e8\u10d4\u10db\u10dd\u10ec\u10db\u10d4\u10d1\u10d0"
+ }
+ },
+ "user": {
+ "data": {
+ "verify_ssl": "SSL \u10e1\u10d4\u10e0\u10e2\u10d8\u10e4\u10d8\u10d9\u10d0\u10e2\u10d8\u10e1 \u10e8\u10d4\u10db\u10dd\u10ec\u10db\u10d4\u10d1\u10d0"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tag/translations/zh-Hans.json b/homeassistant/components/tag/translations/zh-Hans.json
new file mode 100644
index 00000000000..5d655a9ada6
--- /dev/null
+++ b/homeassistant/components/tag/translations/zh-Hans.json
@@ -0,0 +1,3 @@
+{
+ "title": "\u6807\u7b7e"
+}
\ No newline at end of file
diff --git a/homeassistant/components/tag/trigger.py b/homeassistant/components/tag/trigger.py
index 8da9baa5aaa..9803bd56afe 100644
--- a/homeassistant/components/tag/trigger.py
+++ b/homeassistant/components/tag/trigger.py
@@ -1,8 +1,8 @@
"""Support for tag triggers."""
import voluptuous as vol
-from homeassistant.components.homeassistant.triggers import event as event_trigger
from homeassistant.const import CONF_PLATFORM
+from homeassistant.core import HassJob
from homeassistant.helpers import config_validation as cv
from .const import DEVICE_ID, DOMAIN, EVENT_TAG_SCANNED, TAG_ID
@@ -10,28 +10,39 @@ from .const import DEVICE_ID, DOMAIN, EVENT_TAG_SCANNED, TAG_ID
TRIGGER_SCHEMA = vol.Schema(
{
vol.Required(CONF_PLATFORM): DOMAIN,
- vol.Required(TAG_ID): cv.string,
- vol.Optional(DEVICE_ID): cv.string,
+ vol.Required(TAG_ID): vol.All(cv.ensure_list, [cv.string]),
+ vol.Optional(DEVICE_ID): vol.All(cv.ensure_list, [cv.string]),
}
)
async def async_attach_trigger(hass, config, action, automation_info):
"""Listen for tag_scanned events based on configuration."""
- tag_id = config.get(TAG_ID)
- device_id = config.get(DEVICE_ID)
- event_data = {TAG_ID: tag_id}
+ tag_ids = set(config[TAG_ID])
+ device_ids = set(config[DEVICE_ID]) if DEVICE_ID in config else None
- if device_id:
- event_data[DEVICE_ID] = device_id
+ job = HassJob(action)
- event_config = {
- event_trigger.CONF_PLATFORM: "event",
- event_trigger.CONF_EVENT_TYPE: EVENT_TAG_SCANNED,
- event_trigger.CONF_EVENT_DATA: event_data,
- }
- event_config = event_trigger.TRIGGER_SCHEMA(event_config)
+ async def handle_event(event):
+ """Listen for tag scan events and calls the action when data matches."""
+ if event.data.get(TAG_ID) not in tag_ids or (
+ device_ids is not None and event.data.get(DEVICE_ID) not in device_ids
+ ):
+ return
- return await event_trigger.async_attach_trigger(
- hass, event_config, action, automation_info, platform_type=DOMAIN
- )
+ task = hass.async_run_hass_job(
+ job,
+ {
+ "trigger": {
+ "platform": DOMAIN,
+ "event": event,
+ "description": "Tag scanned",
+ }
+ },
+ event.context,
+ )
+
+ if task:
+ await task
+
+ return hass.bus.async_listen(EVENT_TAG_SCANNED, handle_event)
diff --git a/homeassistant/components/tasmota/binary_sensor.py b/homeassistant/components/tasmota/binary_sensor.py
index ade309840ca..feaafa72b29 100644
--- a/homeassistant/components/tasmota/binary_sensor.py
+++ b/homeassistant/components/tasmota/binary_sensor.py
@@ -6,7 +6,7 @@ from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
import homeassistant.helpers.event as evt
-from .const import DATA_REMOVE_DISCOVER_COMPONENT, DOMAIN as TASMOTA_DOMAIN
+from .const import DATA_REMOVE_DISCOVER_COMPONENT
from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW
from .mixins import TasmotaAvailability, TasmotaDiscoveryUpdate
@@ -29,7 +29,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
DATA_REMOVE_DISCOVER_COMPONENT.format(binary_sensor.DOMAIN)
] = async_dispatcher_connect(
hass,
- TASMOTA_DISCOVERY_ENTITY_NEW.format(binary_sensor.DOMAIN, TASMOTA_DOMAIN),
+ TASMOTA_DISCOVERY_ENTITY_NEW.format(binary_sensor.DOMAIN),
async_discover,
)
@@ -47,7 +47,6 @@ class TasmotaBinarySensor(
self._state = None
super().__init__(
- discovery_update=self.discovery_update,
**kwds,
)
diff --git a/homeassistant/components/tasmota/const.py b/homeassistant/components/tasmota/const.py
index 0f4dfde1646..48026f3e93d 100644
--- a/homeassistant/components/tasmota/const.py
+++ b/homeassistant/components/tasmota/const.py
@@ -10,6 +10,8 @@ DOMAIN = "tasmota"
PLATFORMS = [
"binary_sensor",
+ "cover",
+ "fan",
"light",
"sensor",
"switch",
diff --git a/homeassistant/components/tasmota/cover.py b/homeassistant/components/tasmota/cover.py
new file mode 100644
index 00000000000..681778d0099
--- /dev/null
+++ b/homeassistant/components/tasmota/cover.py
@@ -0,0 +1,107 @@
+"""Support for Tasmota covers."""
+
+from hatasmota import const as tasmota_const
+
+from homeassistant.components import cover
+from homeassistant.components.cover import CoverEntity
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+
+from .const import DATA_REMOVE_DISCOVER_COMPONENT
+from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW
+from .mixins import TasmotaAvailability, TasmotaDiscoveryUpdate
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up Tasmota cover dynamically through discovery."""
+
+ @callback
+ def async_discover(tasmota_entity, discovery_hash):
+ """Discover and add a Tasmota cover."""
+ async_add_entities(
+ [TasmotaCover(tasmota_entity=tasmota_entity, discovery_hash=discovery_hash)]
+ )
+
+ hass.data[
+ DATA_REMOVE_DISCOVER_COMPONENT.format(cover.DOMAIN)
+ ] = async_dispatcher_connect(
+ hass,
+ TASMOTA_DISCOVERY_ENTITY_NEW.format(cover.DOMAIN),
+ async_discover,
+ )
+
+
+class TasmotaCover(
+ TasmotaAvailability,
+ TasmotaDiscoveryUpdate,
+ CoverEntity,
+):
+ """Representation of a Tasmota cover."""
+
+ def __init__(self, **kwds):
+ """Initialize the Tasmota cover."""
+ self._direction = None
+ self._position = None
+
+ super().__init__(
+ **kwds,
+ )
+
+ @callback
+ def state_updated(self, state, **kwargs):
+ """Handle state updates."""
+ self._direction = kwargs["direction"]
+ self._position = kwargs["position"]
+ self.async_write_ha_state()
+
+ @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 supported_features(self):
+ """Flag supported features."""
+ return (
+ cover.SUPPORT_OPEN
+ | cover.SUPPORT_CLOSE
+ | cover.SUPPORT_STOP
+ | cover.SUPPORT_SET_POSITION
+ )
+
+ @property
+ def is_opening(self):
+ """Return if the cover is opening or not."""
+ return self._direction == tasmota_const.SHUTTER_DIRECTION_UP
+
+ @property
+ def is_closing(self):
+ """Return if the cover is closing or not."""
+ return self._direction == tasmota_const.SHUTTER_DIRECTION_DOWN
+
+ @property
+ def is_closed(self):
+ """Return if the cover is closed or not."""
+ if self._position is None:
+ return None
+ return self._position == 0
+
+ async def async_open_cover(self, **kwargs):
+ """Open the cover."""
+ self._tasmota_entity.open()
+
+ async def async_close_cover(self, **kwargs):
+ """Close cover."""
+ self._tasmota_entity.close()
+
+ async def async_set_cover_position(self, **kwargs):
+ """Move the cover to a specific position."""
+ position = kwargs[cover.ATTR_POSITION]
+ self._tasmota_entity.set_position(position)
+
+ async def async_stop_cover(self, **kwargs):
+ """Stop the cover."""
+ self._tasmota_entity.stop()
diff --git a/homeassistant/components/tasmota/device_automation.py b/homeassistant/components/tasmota/device_automation.py
index aab0064bb96..ff431141bef 100644
--- a/homeassistant/components/tasmota/device_automation.py
+++ b/homeassistant/components/tasmota/device_automation.py
@@ -35,7 +35,7 @@ async def async_setup_entry(hass, config_entry):
DATA_REMOVE_DISCOVER_COMPONENT.format("device_automation")
] = async_dispatcher_connect(
hass,
- TASMOTA_DISCOVERY_ENTITY_NEW.format("device_automation", "tasmota"),
+ TASMOTA_DISCOVERY_ENTITY_NEW.format("device_automation"),
async_discover,
)
hass.data[DATA_UNSUB].append(
diff --git a/homeassistant/components/tasmota/discovery.py b/homeassistant/components/tasmota/discovery.py
index 2313a8327c5..22824e9cd71 100644
--- a/homeassistant/components/tasmota/discovery.py
+++ b/homeassistant/components/tasmota/discovery.py
@@ -144,7 +144,9 @@ async def async_start(
orphaned_entities = {
entry.unique_id
- for entry in async_entries_for_device(entity_registry, device.id)
+ for entry in async_entries_for_device(
+ entity_registry, device.id, include_disabled_entities=True
+ )
if entry.domain == sensor.DOMAIN and entry.platform == DOMAIN
}
for (tasmota_sensor_config, discovery_hash) in sensors:
diff --git a/homeassistant/components/tasmota/fan.py b/homeassistant/components/tasmota/fan.py
new file mode 100644
index 00000000000..362149e9fca
--- /dev/null
+++ b/homeassistant/components/tasmota/fan.py
@@ -0,0 +1,87 @@
+"""Support for Tasmota fans."""
+
+from hatasmota import const as tasmota_const
+
+from homeassistant.components import fan
+from homeassistant.components.fan import FanEntity
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+
+from .const import DATA_REMOVE_DISCOVER_COMPONENT
+from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW
+from .mixins import TasmotaAvailability, TasmotaDiscoveryUpdate
+
+HA_TO_TASMOTA_SPEED_MAP = {
+ fan.SPEED_OFF: tasmota_const.FAN_SPEED_OFF,
+ fan.SPEED_LOW: tasmota_const.FAN_SPEED_LOW,
+ fan.SPEED_MEDIUM: tasmota_const.FAN_SPEED_MEDIUM,
+ fan.SPEED_HIGH: tasmota_const.FAN_SPEED_HIGH,
+}
+
+TASMOTA_TO_HA_SPEED_MAP = {v: k for k, v in HA_TO_TASMOTA_SPEED_MAP.items()}
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up Tasmota fan dynamically through discovery."""
+
+ @callback
+ def async_discover(tasmota_entity, discovery_hash):
+ """Discover and add a Tasmota fan."""
+ async_add_entities(
+ [TasmotaFan(tasmota_entity=tasmota_entity, discovery_hash=discovery_hash)]
+ )
+
+ hass.data[
+ DATA_REMOVE_DISCOVER_COMPONENT.format(fan.DOMAIN)
+ ] = async_dispatcher_connect(
+ hass,
+ TASMOTA_DISCOVERY_ENTITY_NEW.format(fan.DOMAIN),
+ async_discover,
+ )
+
+
+class TasmotaFan(
+ TasmotaAvailability,
+ TasmotaDiscoveryUpdate,
+ FanEntity,
+):
+ """Representation of a Tasmota fan."""
+
+ def __init__(self, **kwds):
+ """Initialize the Tasmota fan."""
+ self._state = None
+
+ super().__init__(
+ **kwds,
+ )
+
+ @property
+ def speed(self):
+ """Return the current speed."""
+ return TASMOTA_TO_HA_SPEED_MAP.get(self._state)
+
+ @property
+ def speed_list(self):
+ """Get the list of available speeds."""
+ return list(HA_TO_TASMOTA_SPEED_MAP.keys())
+
+ @property
+ def supported_features(self):
+ """Flag supported features."""
+ return fan.SUPPORT_SET_SPEED
+
+ async def async_set_speed(self, speed):
+ """Set the speed of the fan."""
+ if speed == fan.SPEED_OFF:
+ await self.async_turn_off()
+ else:
+ self._tasmota_entity.set_speed(HA_TO_TASMOTA_SPEED_MAP[speed])
+
+ async def async_turn_on(self, speed=None, **kwargs):
+ """Turn the fan on."""
+ # Tasmota does not support turning a fan on with implicit speed
+ await self.async_set_speed(speed or fan.SPEED_MEDIUM)
+
+ async def async_turn_off(self, **kwargs):
+ """Turn the fan off."""
+ self._tasmota_entity.set_speed(tasmota_const.FAN_SPEED_OFF)
diff --git a/homeassistant/components/tasmota/light.py b/homeassistant/components/tasmota/light.py
index a680d873f9f..efab8dcaae3 100644
--- a/homeassistant/components/tasmota/light.py
+++ b/homeassistant/components/tasmota/light.py
@@ -27,7 +27,7 @@ from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
import homeassistant.util.color as color_util
-from .const import DATA_REMOVE_DISCOVER_COMPONENT, DOMAIN as TASMOTA_DOMAIN
+from .const import DATA_REMOVE_DISCOVER_COMPONENT
from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW
from .mixins import TasmotaAvailability, TasmotaDiscoveryUpdate
@@ -49,7 +49,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
DATA_REMOVE_DISCOVER_COMPONENT.format(light.DOMAIN)
] = async_dispatcher_connect(
hass,
- TASMOTA_DISCOVERY_ENTITY_NEW.format(light.DOMAIN, TASMOTA_DOMAIN),
+ TASMOTA_DISCOVERY_ENTITY_NEW.format(light.DOMAIN),
async_discover,
)
@@ -74,7 +74,6 @@ class TasmotaLight(
self._flash_times = None
super().__init__(
- discovery_update=self.discovery_update,
**kwds,
)
@@ -93,7 +92,6 @@ class TasmotaLight(
if light_type != LIGHT_TYPE_NONE:
supported_features |= SUPPORT_BRIGHTNESS
- supported_features |= SUPPORT_TRANSITION
if light_type in [LIGHT_TYPE_COLDWARM, LIGHT_TYPE_RGBCW]:
supported_features |= SUPPORT_COLOR_TEMP
@@ -105,6 +103,9 @@ class TasmotaLight(
if light_type in [LIGHT_TYPE_RGBW, LIGHT_TYPE_RGBCW]:
supported_features |= SUPPORT_WHITE_VALUE
+ if self._tasmota_entity.supports_transition:
+ supported_features |= SUPPORT_TRANSITION
+
self._supported_features = supported_features
@callback
diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json
index 6140de6025a..a4c6f77fc13 100644
--- a/homeassistant/components/tasmota/manifest.json
+++ b/homeassistant/components/tasmota/manifest.json
@@ -3,7 +3,7 @@
"name": "Tasmota (beta)",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/tasmota",
- "requirements": ["hatasmota==0.0.32"],
+ "requirements": ["hatasmota==0.1.4"],
"dependencies": ["mqtt"],
"mqtt": ["tasmota/discovery/#"],
"codeowners": ["@emontnemery"]
diff --git a/homeassistant/components/tasmota/mixins.py b/homeassistant/components/tasmota/mixins.py
index a860b06c574..d8e0eeeb4cd 100644
--- a/homeassistant/components/tasmota/mixins.py
+++ b/homeassistant/components/tasmota/mixins.py
@@ -116,10 +116,9 @@ class TasmotaAvailability(TasmotaEntity):
class TasmotaDiscoveryUpdate(TasmotaEntity):
"""Mixin used to handle updated discovery message."""
- def __init__(self, discovery_hash, discovery_update, **kwds) -> None:
+ def __init__(self, discovery_hash, **kwds) -> None:
"""Initialize the discovery update mixin."""
self._discovery_hash = discovery_hash
- self._discovery_update = discovery_update
self._removed_from_hass = False
super().__init__(**kwds)
@@ -138,7 +137,7 @@ class TasmotaDiscoveryUpdate(TasmotaEntity):
if not self._tasmota_entity.config_same(config):
# Changed payload: Notify component
_LOGGER.debug("Updating component: %s", self.entity_id)
- await self._discovery_update(config)
+ await self.discovery_update(config)
else:
# Unchanged payload: Ignore to avoid changing states
_LOGGER.debug("Ignoring unchanged update for: %s", self.entity_id)
diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py
index 966bf8648d4..17a6e2a35c2 100644
--- a/homeassistant/components/tasmota/sensor.py
+++ b/homeassistant/components/tasmota/sensor.py
@@ -1,77 +1,7 @@
"""Support for Tasmota sensors."""
from typing import Optional
-from hatasmota import status_sensor
-from hatasmota.const import (
- CONCENTRATION_MICROGRAMS_PER_CUBIC_METER as TASMOTA_CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
- CONCENTRATION_PARTS_PER_BILLION as TASMOTA_CONCENTRATION_PARTS_PER_BILLION,
- CONCENTRATION_PARTS_PER_MILLION as TASMOTA_CONCENTRATION_PARTS_PER_MILLION,
- ELECTRICAL_CURRENT_AMPERE as TASMOTA_ELECTRICAL_CURRENT_AMPERE,
- ELECTRICAL_VOLT_AMPERE as TASMOTA_ELECTRICAL_VOLT_AMPERE,
- ENERGY_KILO_WATT_HOUR as TASMOTA_ENERGY_KILO_WATT_HOUR,
- FREQUENCY_HERTZ as TASMOTA_FREQUENCY_HERTZ,
- LENGTH_CENTIMETERS as TASMOTA_LENGTH_CENTIMETERS,
- LIGHT_LUX as TASMOTA_LIGHT_LUX,
- MASS_KILOGRAMS as TASMOTA_MASS_KILOGRAMS,
- PERCENTAGE as TASMOTA_PERCENTAGE,
- POWER_WATT as TASMOTA_POWER_WATT,
- PRESSURE_HPA as TASMOTA_PRESSURE_HPA,
- SENSOR_AMBIENT,
- SENSOR_APPARENT_POWERUSAGE,
- SENSOR_BATTERY,
- SENSOR_CCT,
- SENSOR_CO2,
- SENSOR_COLOR_BLUE,
- SENSOR_COLOR_GREEN,
- SENSOR_COLOR_RED,
- SENSOR_CURRENT,
- SENSOR_DEWPOINT,
- SENSOR_DISTANCE,
- SENSOR_ECO2,
- SENSOR_FREQUENCY,
- SENSOR_HUMIDITY,
- SENSOR_ILLUMINANCE,
- SENSOR_MOISTURE,
- SENSOR_PB0_3,
- SENSOR_PB0_5,
- SENSOR_PB1,
- SENSOR_PB2_5,
- SENSOR_PB5,
- SENSOR_PB10,
- SENSOR_PM1,
- SENSOR_PM2_5,
- SENSOR_PM10,
- SENSOR_POWERFACTOR,
- SENSOR_POWERUSAGE,
- SENSOR_PRESSURE,
- SENSOR_PRESSUREATSEALEVEL,
- SENSOR_PROXIMITY,
- SENSOR_REACTIVE_POWERUSAGE,
- SENSOR_STATUS_IP,
- SENSOR_STATUS_LAST_RESTART_TIME,
- SENSOR_STATUS_LINK_COUNT,
- SENSOR_STATUS_MQTT_COUNT,
- SENSOR_STATUS_RESTART_REASON,
- SENSOR_STATUS_RSSI,
- SENSOR_STATUS_SIGNAL,
- SENSOR_STATUS_SSID,
- SENSOR_TEMPERATURE,
- SENSOR_TODAY,
- SENSOR_TOTAL,
- SENSOR_TOTAL_START_TIME,
- SENSOR_TVOC,
- SENSOR_VOLTAGE,
- SENSOR_WEIGHT,
- SENSOR_YESTERDAY,
- SIGNAL_STRENGTH_DECIBELS as TASMOTA_SIGNAL_STRENGTH_DECIBELS,
- SPEED_KILOMETERS_PER_HOUR as TASMOTA_SPEED_KILOMETERS_PER_HOUR,
- SPEED_METERS_PER_SECOND as TASMOTA_SPEED_METERS_PER_SECOND,
- SPEED_MILES_PER_HOUR as TASMOTA_SPEED_MILES_PER_HOUR,
- TEMP_CELSIUS as TASMOTA_TEMP_CELSIUS,
- TEMP_FAHRENHEIT as TASMOTA_TEMP_FAHRENHEIT,
- TEMP_KELVIN as TASMOTA_TEMP_KELVIN,
- VOLT as TASMOTA_VOLT,
-)
+from hatasmota import const as hc, status_sensor
from homeassistant.components import sensor
from homeassistant.const import (
@@ -109,7 +39,7 @@ from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
-from .const import DATA_REMOVE_DISCOVER_COMPONENT, DOMAIN as TASMOTA_DOMAIN
+from .const import DATA_REMOVE_DISCOVER_COMPONENT
from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW
from .mixins import TasmotaAvailability, TasmotaDiscoveryUpdate
@@ -118,77 +48,77 @@ ICON = "icon"
# A Tasmota sensor type may be mapped to either a device class or an icon, not both
SENSOR_DEVICE_CLASS_ICON_MAP = {
- SENSOR_AMBIENT: {DEVICE_CLASS: DEVICE_CLASS_ILLUMINANCE},
- SENSOR_APPARENT_POWERUSAGE: {DEVICE_CLASS: DEVICE_CLASS_POWER},
- SENSOR_BATTERY: {DEVICE_CLASS: DEVICE_CLASS_BATTERY},
- SENSOR_CCT: {ICON: "mdi:temperature-kelvin"},
- SENSOR_CO2: {ICON: "mdi:molecule-co2"},
- SENSOR_COLOR_BLUE: {ICON: "mdi:palette"},
- SENSOR_COLOR_GREEN: {ICON: "mdi:palette"},
- SENSOR_COLOR_RED: {ICON: "mdi:palette"},
- SENSOR_CURRENT: {ICON: "mdi:alpha-a-circle-outline"},
- SENSOR_DEWPOINT: {ICON: "mdi:weather-rainy"},
- SENSOR_DISTANCE: {ICON: "mdi:leak"},
- SENSOR_ECO2: {ICON: "mdi:molecule-co2"},
- SENSOR_FREQUENCY: {ICON: "mdi:current-ac"},
- SENSOR_HUMIDITY: {DEVICE_CLASS: DEVICE_CLASS_HUMIDITY},
- SENSOR_ILLUMINANCE: {DEVICE_CLASS: DEVICE_CLASS_ILLUMINANCE},
- SENSOR_STATUS_IP: {ICON: "mdi:ip-network"},
- SENSOR_STATUS_LINK_COUNT: {ICON: "mdi:counter"},
- SENSOR_MOISTURE: {ICON: "mdi:cup-water"},
- SENSOR_STATUS_MQTT_COUNT: {ICON: "mdi:counter"},
- SENSOR_PB0_3: {ICON: "mdi:flask"},
- SENSOR_PB0_5: {ICON: "mdi:flask"},
- SENSOR_PB10: {ICON: "mdi:flask"},
- SENSOR_PB1: {ICON: "mdi:flask"},
- SENSOR_PB2_5: {ICON: "mdi:flask"},
- SENSOR_PB5: {ICON: "mdi:flask"},
- SENSOR_PM10: {ICON: "mdi:air-filter"},
- SENSOR_PM1: {ICON: "mdi:air-filter"},
- SENSOR_PM2_5: {ICON: "mdi:air-filter"},
- SENSOR_POWERFACTOR: {ICON: "mdi:alpha-f-circle-outline"},
- SENSOR_POWERUSAGE: {DEVICE_CLASS: DEVICE_CLASS_POWER},
- SENSOR_PRESSURE: {DEVICE_CLASS: DEVICE_CLASS_PRESSURE},
- SENSOR_PRESSUREATSEALEVEL: {DEVICE_CLASS: DEVICE_CLASS_PRESSURE},
- SENSOR_PROXIMITY: {ICON: "mdi:ruler"},
- SENSOR_REACTIVE_POWERUSAGE: {DEVICE_CLASS: DEVICE_CLASS_POWER},
- SENSOR_STATUS_LAST_RESTART_TIME: {DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP},
- SENSOR_STATUS_RESTART_REASON: {ICON: "mdi:information-outline"},
- SENSOR_STATUS_SIGNAL: {DEVICE_CLASS: DEVICE_CLASS_SIGNAL_STRENGTH},
- SENSOR_STATUS_RSSI: {ICON: "mdi:access-point"},
- SENSOR_STATUS_SSID: {ICON: "mdi:access-point-network"},
- SENSOR_TEMPERATURE: {DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE},
- SENSOR_TODAY: {DEVICE_CLASS: DEVICE_CLASS_POWER},
- SENSOR_TOTAL: {DEVICE_CLASS: DEVICE_CLASS_POWER},
- SENSOR_TOTAL_START_TIME: {ICON: "mdi:progress-clock"},
- SENSOR_TVOC: {ICON: "mdi:air-filter"},
- SENSOR_VOLTAGE: {ICON: "mdi:alpha-v-circle-outline"},
- SENSOR_WEIGHT: {ICON: "mdi:scale"},
- SENSOR_YESTERDAY: {DEVICE_CLASS: DEVICE_CLASS_POWER},
+ hc.SENSOR_AMBIENT: {DEVICE_CLASS: DEVICE_CLASS_ILLUMINANCE},
+ hc.SENSOR_APPARENT_POWERUSAGE: {DEVICE_CLASS: DEVICE_CLASS_POWER},
+ hc.SENSOR_BATTERY: {DEVICE_CLASS: DEVICE_CLASS_BATTERY},
+ hc.SENSOR_CCT: {ICON: "mdi:temperature-kelvin"},
+ hc.SENSOR_CO2: {ICON: "mdi:molecule-co2"},
+ hc.SENSOR_COLOR_BLUE: {ICON: "mdi:palette"},
+ hc.SENSOR_COLOR_GREEN: {ICON: "mdi:palette"},
+ hc.SENSOR_COLOR_RED: {ICON: "mdi:palette"},
+ hc.SENSOR_CURRENT: {ICON: "mdi:alpha-a-circle-outline"},
+ hc.SENSOR_DEWPOINT: {ICON: "mdi:weather-rainy"},
+ hc.SENSOR_DISTANCE: {ICON: "mdi:leak"},
+ hc.SENSOR_ECO2: {ICON: "mdi:molecule-co2"},
+ hc.SENSOR_FREQUENCY: {ICON: "mdi:current-ac"},
+ hc.SENSOR_HUMIDITY: {DEVICE_CLASS: DEVICE_CLASS_HUMIDITY},
+ hc.SENSOR_ILLUMINANCE: {DEVICE_CLASS: DEVICE_CLASS_ILLUMINANCE},
+ hc.SENSOR_STATUS_IP: {ICON: "mdi:ip-network"},
+ hc.SENSOR_STATUS_LINK_COUNT: {ICON: "mdi:counter"},
+ hc.SENSOR_MOISTURE: {ICON: "mdi:cup-water"},
+ hc.SENSOR_STATUS_MQTT_COUNT: {ICON: "mdi:counter"},
+ hc.SENSOR_PB0_3: {ICON: "mdi:flask"},
+ hc.SENSOR_PB0_5: {ICON: "mdi:flask"},
+ hc.SENSOR_PB10: {ICON: "mdi:flask"},
+ hc.SENSOR_PB1: {ICON: "mdi:flask"},
+ hc.SENSOR_PB2_5: {ICON: "mdi:flask"},
+ hc.SENSOR_PB5: {ICON: "mdi:flask"},
+ hc.SENSOR_PM10: {ICON: "mdi:air-filter"},
+ hc.SENSOR_PM1: {ICON: "mdi:air-filter"},
+ hc.SENSOR_PM2_5: {ICON: "mdi:air-filter"},
+ hc.SENSOR_POWERFACTOR: {ICON: "mdi:alpha-f-circle-outline"},
+ hc.SENSOR_POWERUSAGE: {DEVICE_CLASS: DEVICE_CLASS_POWER},
+ hc.SENSOR_PRESSURE: {DEVICE_CLASS: DEVICE_CLASS_PRESSURE},
+ hc.SENSOR_PRESSUREATSEALEVEL: {DEVICE_CLASS: DEVICE_CLASS_PRESSURE},
+ hc.SENSOR_PROXIMITY: {ICON: "mdi:ruler"},
+ hc.SENSOR_REACTIVE_POWERUSAGE: {DEVICE_CLASS: DEVICE_CLASS_POWER},
+ hc.SENSOR_STATUS_LAST_RESTART_TIME: {DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP},
+ hc.SENSOR_STATUS_RESTART_REASON: {ICON: "mdi:information-outline"},
+ hc.SENSOR_STATUS_SIGNAL: {DEVICE_CLASS: DEVICE_CLASS_SIGNAL_STRENGTH},
+ hc.SENSOR_STATUS_RSSI: {ICON: "mdi:access-point"},
+ hc.SENSOR_STATUS_SSID: {ICON: "mdi:access-point-network"},
+ hc.SENSOR_TEMPERATURE: {DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE},
+ hc.SENSOR_TODAY: {DEVICE_CLASS: DEVICE_CLASS_POWER},
+ hc.SENSOR_TOTAL: {DEVICE_CLASS: DEVICE_CLASS_POWER},
+ hc.SENSOR_TOTAL_START_TIME: {ICON: "mdi:progress-clock"},
+ hc.SENSOR_TVOC: {ICON: "mdi:air-filter"},
+ hc.SENSOR_VOLTAGE: {ICON: "mdi:alpha-v-circle-outline"},
+ hc.SENSOR_WEIGHT: {ICON: "mdi:scale"},
+ hc.SENSOR_YESTERDAY: {DEVICE_CLASS: DEVICE_CLASS_POWER},
}
SENSOR_UNIT_MAP = {
- TASMOTA_CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
- TASMOTA_CONCENTRATION_PARTS_PER_BILLION: CONCENTRATION_PARTS_PER_BILLION,
- TASMOTA_CONCENTRATION_PARTS_PER_MILLION: CONCENTRATION_PARTS_PER_MILLION,
- TASMOTA_ELECTRICAL_CURRENT_AMPERE: ELECTRICAL_CURRENT_AMPERE,
- TASMOTA_ELECTRICAL_VOLT_AMPERE: ELECTRICAL_VOLT_AMPERE,
- TASMOTA_ENERGY_KILO_WATT_HOUR: ENERGY_KILO_WATT_HOUR,
- TASMOTA_FREQUENCY_HERTZ: FREQUENCY_HERTZ,
- TASMOTA_LENGTH_CENTIMETERS: LENGTH_CENTIMETERS,
- TASMOTA_LIGHT_LUX: LIGHT_LUX,
- TASMOTA_MASS_KILOGRAMS: MASS_KILOGRAMS,
- TASMOTA_PERCENTAGE: PERCENTAGE,
- TASMOTA_POWER_WATT: POWER_WATT,
- TASMOTA_PRESSURE_HPA: PRESSURE_HPA,
- TASMOTA_SIGNAL_STRENGTH_DECIBELS: SIGNAL_STRENGTH_DECIBELS,
- TASMOTA_SPEED_KILOMETERS_PER_HOUR: SPEED_KILOMETERS_PER_HOUR,
- TASMOTA_SPEED_METERS_PER_SECOND: SPEED_METERS_PER_SECOND,
- TASMOTA_SPEED_MILES_PER_HOUR: SPEED_MILES_PER_HOUR,
- TASMOTA_TEMP_CELSIUS: TEMP_CELSIUS,
- TASMOTA_TEMP_FAHRENHEIT: TEMP_FAHRENHEIT,
- TASMOTA_TEMP_KELVIN: TEMP_KELVIN,
- TASMOTA_VOLT: VOLT,
+ hc.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
+ hc.CONCENTRATION_PARTS_PER_BILLION: CONCENTRATION_PARTS_PER_BILLION,
+ hc.CONCENTRATION_PARTS_PER_MILLION: CONCENTRATION_PARTS_PER_MILLION,
+ hc.ELECTRICAL_CURRENT_AMPERE: ELECTRICAL_CURRENT_AMPERE,
+ hc.ELECTRICAL_VOLT_AMPERE: ELECTRICAL_VOLT_AMPERE,
+ hc.ENERGY_KILO_WATT_HOUR: ENERGY_KILO_WATT_HOUR,
+ hc.FREQUENCY_HERTZ: FREQUENCY_HERTZ,
+ hc.LENGTH_CENTIMETERS: LENGTH_CENTIMETERS,
+ hc.LIGHT_LUX: LIGHT_LUX,
+ hc.MASS_KILOGRAMS: MASS_KILOGRAMS,
+ hc.PERCENTAGE: PERCENTAGE,
+ hc.POWER_WATT: POWER_WATT,
+ hc.PRESSURE_HPA: PRESSURE_HPA,
+ hc.SIGNAL_STRENGTH_DECIBELS: SIGNAL_STRENGTH_DECIBELS,
+ hc.SPEED_KILOMETERS_PER_HOUR: SPEED_KILOMETERS_PER_HOUR,
+ hc.SPEED_METERS_PER_SECOND: SPEED_METERS_PER_SECOND,
+ hc.SPEED_MILES_PER_HOUR: SPEED_MILES_PER_HOUR,
+ hc.TEMP_CELSIUS: TEMP_CELSIUS,
+ hc.TEMP_FAHRENHEIT: TEMP_FAHRENHEIT,
+ hc.TEMP_KELVIN: TEMP_KELVIN,
+ hc.VOLT: VOLT,
}
@@ -209,7 +139,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
DATA_REMOVE_DISCOVER_COMPONENT.format(sensor.DOMAIN)
] = async_dispatcher_connect(
hass,
- TASMOTA_DISCOVERY_ENTITY_NEW.format(sensor.DOMAIN, TASMOTA_DOMAIN),
+ TASMOTA_DISCOVERY_ENTITY_NEW.format(sensor.DOMAIN),
async_discover_sensor,
)
@@ -222,7 +152,6 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, Entity):
self._state = None
super().__init__(
- discovery_update=self.discovery_update,
**kwds,
)
diff --git a/homeassistant/components/tasmota/switch.py b/homeassistant/components/tasmota/switch.py
index 0a97a5e2528..27906bf5dbb 100644
--- a/homeassistant/components/tasmota/switch.py
+++ b/homeassistant/components/tasmota/switch.py
@@ -5,7 +5,7 @@ from homeassistant.components.switch import SwitchEntity
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from .const import DATA_REMOVE_DISCOVER_COMPONENT, DOMAIN as TASMOTA_DOMAIN
+from .const import DATA_REMOVE_DISCOVER_COMPONENT
from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW
from .mixins import TasmotaAvailability, TasmotaDiscoveryUpdate
@@ -28,7 +28,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
DATA_REMOVE_DISCOVER_COMPONENT.format(switch.DOMAIN)
] = async_dispatcher_connect(
hass,
- TASMOTA_DISCOVERY_ENTITY_NEW.format(switch.DOMAIN, TASMOTA_DOMAIN),
+ TASMOTA_DISCOVERY_ENTITY_NEW.format(switch.DOMAIN),
async_discover,
)
@@ -45,7 +45,6 @@ class TasmotaSwitch(
self._state = False
super().__init__(
- discovery_update=self.discovery_update,
**kwds,
)
diff --git a/homeassistant/components/tasmota/translations/ka.json b/homeassistant/components/tasmota/translations/ka.json
new file mode 100644
index 00000000000..fd948a837c2
--- /dev/null
+++ b/homeassistant/components/tasmota/translations/ka.json
@@ -0,0 +1,9 @@
+{
+ "config": {
+ "step": {
+ "confirm": {
+ "description": "\u10d2\u10e1\u10e3\u10e0\u10d7 \u10d3\u10d0\u10d0\u10e7\u10d4\u10dc\u10dd\u10d7 Tasmota?"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/telegram/notify.py b/homeassistant/components/telegram/notify.py
index c0f3f624af9..fd7e731194c 100644
--- a/homeassistant/components/telegram/notify.py
+++ b/homeassistant/components/telegram/notify.py
@@ -23,6 +23,7 @@ ATTR_KEYBOARD = "keyboard"
ATTR_INLINE_KEYBOARD = "inline_keyboard"
ATTR_PHOTO = "photo"
ATTR_VIDEO = "video"
+ATTR_VOICE = "voice"
ATTR_DOCUMENT = "document"
CONF_CHAT_ID = "chat_id"
@@ -65,7 +66,7 @@ class TelegramNotificationService(BaseNotificationService):
keys = keys if isinstance(keys, list) else [keys]
service_data.update(inline_keyboard=keys)
- # Send a photo, video, document, or location
+ # Send a photo, video, document, voice, or location
if data is not None and ATTR_PHOTO in data:
photos = data.get(ATTR_PHOTO)
photos = photos if isinstance(photos, list) else [photos]
@@ -80,6 +81,13 @@ class TelegramNotificationService(BaseNotificationService):
service_data.update(video_data)
self.hass.services.call(DOMAIN, "send_video", service_data=service_data)
return
+ if data is not None and ATTR_VOICE in data:
+ voices = data.get(ATTR_VOICE)
+ voices = voices if isinstance(voices, list) else [voices]
+ for voice_data in voices:
+ service_data.update(voice_data)
+ self.hass.services.call(DOMAIN, "send_voice", service_data=service_data)
+ return
if data is not None and ATTR_LOCATION in data:
service_data.update(data.get(ATTR_LOCATION))
return self.hass.services.call(
diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py
index af00c2cb6d0..fc592c9e5c0 100644
--- a/homeassistant/components/telegram_bot/__init__.py
+++ b/homeassistant/components/telegram_bot/__init__.py
@@ -80,6 +80,7 @@ SERVICE_SEND_MESSAGE = "send_message"
SERVICE_SEND_PHOTO = "send_photo"
SERVICE_SEND_STICKER = "send_sticker"
SERVICE_SEND_VIDEO = "send_video"
+SERVICE_SEND_VOICE = "send_voice"
SERVICE_SEND_DOCUMENT = "send_document"
SERVICE_SEND_LOCATION = "send_location"
SERVICE_EDIT_MESSAGE = "edit_message"
@@ -224,6 +225,7 @@ SERVICE_MAP = {
SERVICE_SEND_PHOTO: SERVICE_SCHEMA_SEND_FILE,
SERVICE_SEND_STICKER: SERVICE_SCHEMA_SEND_FILE,
SERVICE_SEND_VIDEO: SERVICE_SCHEMA_SEND_FILE,
+ SERVICE_SEND_VOICE: SERVICE_SCHEMA_SEND_FILE,
SERVICE_SEND_DOCUMENT: SERVICE_SCHEMA_SEND_FILE,
SERVICE_SEND_LOCATION: SERVICE_SCHEMA_SEND_LOCATION,
SERVICE_EDIT_MESSAGE: SERVICE_SCHEMA_EDIT_MESSAGE,
@@ -366,6 +368,7 @@ async def async_setup(hass, config):
SERVICE_SEND_PHOTO,
SERVICE_SEND_STICKER,
SERVICE_SEND_VIDEO,
+ SERVICE_SEND_VOICE,
SERVICE_SEND_DOCUMENT,
]:
await hass.async_add_executor_job(
@@ -672,6 +675,7 @@ class TelegramNotificationService:
SERVICE_SEND_PHOTO: self.bot.sendPhoto,
SERVICE_SEND_STICKER: self.bot.sendSticker,
SERVICE_SEND_VIDEO: self.bot.sendVideo,
+ SERVICE_SEND_VOICE: self.bot.sendVoice,
SERVICE_SEND_DOCUMENT: self.bot.sendDocument,
}.get(file_type)
file_content = load_data(
@@ -744,11 +748,12 @@ class BaseTelegramBotEntity:
_LOGGER.error("Incoming message does not have required data (%s)", msg_data)
return False, None
- if msg_data["from"].get("id") not in self.allowed_chat_ids or (
- "chat" in msg_data
+ if (
+ msg_data["from"].get("id") not in self.allowed_chat_ids
and msg_data["chat"].get("id") not in self.allowed_chat_ids
):
- # Origin is not allowed.
+ # Neither from id nor chat id was in allowed_chat_ids,
+ # origin is not allowed.
_LOGGER.error("Incoming message is not allowed (%s)", msg_data)
return True, None
diff --git a/homeassistant/components/telegram_bot/services.yaml b/homeassistant/components/telegram_bot/services.yaml
index 8a66d2cab3a..a4e0adc81a8 100644
--- a/homeassistant/components/telegram_bot/services.yaml
+++ b/homeassistant/components/telegram_bot/services.yaml
@@ -151,6 +151,46 @@ send_video:
description: 'Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}'
example: "msg_to_edit"
+send_voice:
+ description: Send a voice message.
+ fields:
+ url:
+ description: Remote path to a voice message.
+ example: "http://example.org/path/to/the/voice.opus"
+ file:
+ description: Local path to a voice message.
+ example: "/path/to/the/voice.opus"
+ caption:
+ description: The title of the voice message.
+ example: "My microphone recording"
+ username:
+ description: Username for a URL which require HTTP basic authentication.
+ example: myuser
+ password:
+ description: Password for a URL which require HTTP basic authentication.
+ example: myuser_pwd
+ target:
+ description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default.
+ example: "[12345, 67890] or 12345"
+ disable_notification:
+ description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound.
+ example: true
+ verify_ssl:
+ description: Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server.
+ example: false
+ timeout:
+ description: Timeout for send voice. Will help with timeout errors (poor internet connection, etc)
+ example: "1000"
+ keyboard:
+ description: List of rows of commands, comma-separated, to make a custom keyboard.
+ example: '["/command1, /command2", "/command3"]'
+ inline_keyboard:
+ description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data.
+ example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
+ message_tag:
+ description: 'Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}'
+ example: "msg_to_edit"
+
send_document:
description: Send a document.
fields:
diff --git a/homeassistant/components/tellduslive/config_flow.py b/homeassistant/components/tellduslive/config_flow.py
index ae8a9d1a690..aabbf88ee1c 100644
--- a/homeassistant/components/tellduslive/config_flow.py
+++ b/homeassistant/components/tellduslive/config_flow.py
@@ -97,12 +97,12 @@ class FlowHandler(config_entries.ConfigFlow):
with async_timeout.timeout(10):
auth_url = await self.hass.async_add_executor_job(self._get_auth_url)
if not auth_url:
- return self.async_abort(reason="authorize_url_fail")
+ return self.async_abort(reason="unknown_authorize_url_generation")
except asyncio.TimeoutError:
return self.async_abort(reason="authorize_url_timeout")
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected error generating auth url")
- return self.async_abort(reason="authorize_url_fail")
+ return self.async_abort(reason="unknown_authorize_url_generation")
_LOGGER.debug("Got authorization URL %s", auth_url)
return self.async_show_form(
diff --git a/homeassistant/components/tellduslive/strings.json b/homeassistant/components/tellduslive/strings.json
index 8d1c2c1acaf..27e74d6d938 100644
--- a/homeassistant/components/tellduslive/strings.json
+++ b/homeassistant/components/tellduslive/strings.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
- "authorize_url_fail": "Unknown error generating an authorize url.",
+ "unknown_authorize_url_generation": "[%key:common::config_flow::abort::unknown_authorize_url_generation%]",
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
diff --git a/homeassistant/components/tellduslive/translations/ca.json b/homeassistant/components/tellduslive/translations/ca.json
index 94c78068dd1..b0fb93241b9 100644
--- a/homeassistant/components/tellduslive/translations/ca.json
+++ b/homeassistant/components/tellduslive/translations/ca.json
@@ -4,7 +4,8 @@
"already_configured": "El servei ja est\u00e0 configurat",
"authorize_url_fail": "S'ha produ\u00eft un error desconegut al generar l'URL d'autoritzaci\u00f3.",
"authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.",
- "unknown": "Error inesperat"
+ "unknown": "Error inesperat",
+ "unknown_authorize_url_generation": "S'ha produ\u00eft un error desconegut al generar URL d'autoritzaci\u00f3."
},
"error": {
"invalid_auth": "Autenticaci\u00f3 inv\u00e0lida"
diff --git a/homeassistant/components/tellduslive/translations/cs.json b/homeassistant/components/tellduslive/translations/cs.json
index 82355d34716..e0780ece8be 100644
--- a/homeassistant/components/tellduslive/translations/cs.json
+++ b/homeassistant/components/tellduslive/translations/cs.json
@@ -4,7 +4,8 @@
"already_configured": "Slu\u017eba je ji\u017e nastavena",
"authorize_url_fail": "Nezn\u00e1m\u00e1 chyba p\u0159i generov\u00e1n\u00ed autoriza\u010dn\u00ed URL adresy.",
"authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el",
- "unknown": "Neo\u010dek\u00e1van\u00e1 chyba"
+ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba",
+ "unknown_authorize_url_generation": "Nezn\u00e1m\u00e1 chyba p\u0159i generov\u00e1n\u00ed autoriza\u010dn\u00ed URL adresy."
},
"error": {
"invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed"
diff --git a/homeassistant/components/tellduslive/translations/en.json b/homeassistant/components/tellduslive/translations/en.json
index 80e6cde7acd..7b14df15fa8 100644
--- a/homeassistant/components/tellduslive/translations/en.json
+++ b/homeassistant/components/tellduslive/translations/en.json
@@ -4,7 +4,8 @@
"already_configured": "Service is already configured",
"authorize_url_fail": "Unknown error generating an authorize url.",
"authorize_url_timeout": "Timeout generating authorize URL.",
- "unknown": "Unexpected error"
+ "unknown": "Unexpected error",
+ "unknown_authorize_url_generation": "Unknown error generating an authorize url."
},
"error": {
"invalid_auth": "Invalid authentication"
diff --git a/homeassistant/components/tellduslive/translations/es.json b/homeassistant/components/tellduslive/translations/es.json
index 2e6f1a64419..7b39b7fe042 100644
--- a/homeassistant/components/tellduslive/translations/es.json
+++ b/homeassistant/components/tellduslive/translations/es.json
@@ -4,7 +4,8 @@
"already_configured": "TelldusLive ya est\u00e1 configurado",
"authorize_url_fail": "Error desconocido generando la url de autorizaci\u00f3n",
"authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n",
- "unknown": "Se produjo un error desconocido"
+ "unknown": "Se produjo un error desconocido",
+ "unknown_authorize_url_generation": "Error desconocido al generar una URL de autorizaci\u00f3n."
},
"error": {
"invalid_auth": "Autenticaci\u00f3n no v\u00e1lida"
diff --git a/homeassistant/components/tellduslive/translations/et.json b/homeassistant/components/tellduslive/translations/et.json
index b9008e7bc79..bab010bd845 100644
--- a/homeassistant/components/tellduslive/translations/et.json
+++ b/homeassistant/components/tellduslive/translations/et.json
@@ -4,7 +4,8 @@
"already_configured": "Teenus on juba seadistatud",
"authorize_url_fail": "Tundmatu viga tuvastamise URL-i loomisel.",
"authorize_url_timeout": "Kinnitus-URLi loomise ajal\u00f5pp.",
- "unknown": "Tundmatu viga"
+ "unknown": "Tundmatu viga",
+ "unknown_authorize_url_generation": "Tundmatu viga tuvastamise URL-i loomisel."
},
"error": {
"invalid_auth": "Tuvastamise viga"
diff --git a/homeassistant/components/tellduslive/translations/it.json b/homeassistant/components/tellduslive/translations/it.json
index d16b69d577b..431b986fdae 100644
--- a/homeassistant/components/tellduslive/translations/it.json
+++ b/homeassistant/components/tellduslive/translations/it.json
@@ -4,7 +4,8 @@
"already_configured": "Il servizio \u00e8 gi\u00e0 configurato",
"authorize_url_fail": "Errore sconosciuto nel generare l'url di autorizzazione",
"authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.",
- "unknown": "Errore imprevisto"
+ "unknown": "Errore imprevisto",
+ "unknown_authorize_url_generation": "Errore sconosciuto durante la generazione di un URL di autorizzazione."
},
"error": {
"invalid_auth": "Autenticazione non valida"
diff --git a/homeassistant/components/tellduslive/translations/ka.json b/homeassistant/components/tellduslive/translations/ka.json
new file mode 100644
index 00000000000..54e8ddb0b3c
--- /dev/null
+++ b/homeassistant/components/tellduslive/translations/ka.json
@@ -0,0 +1,10 @@
+{
+ "config": {
+ "abort": {
+ "unknown_authorize_url_generation": "\u10d0\u10d5\u10e2\u10dd\u10e0\u10d8\u10d6\u10d0\u10ea\u10d8\u10d8 \u10d2\u10d4\u10dc\u10d4\u10e0\u10d8\u10e0\u10d4\u10d1\u10d8\u10e1 \u10e3\u10ea\u10dc\u10dd\u10d1\u10d8 \u10e8\u10d4\u10ea\u10d3\u10dd\u10db\u10d0."
+ },
+ "error": {
+ "invalid_auth": "\u10db\u10ea\u10d3\u10d0\u10e0\u10d8 \u10d0\u10d5\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tellduslive/translations/no.json b/homeassistant/components/tellduslive/translations/no.json
index 13ad5419215..95bd22cbecf 100644
--- a/homeassistant/components/tellduslive/translations/no.json
+++ b/homeassistant/components/tellduslive/translations/no.json
@@ -4,7 +4,8 @@
"already_configured": "Tjenesten er allerede konfigurert",
"authorize_url_fail": "Ukjent feil ved oppretting av godkjenningsadresse.",
"authorize_url_timeout": "Tidsavbrudd som genererer autorer URL-adresse.",
- "unknown": "Uventet feil"
+ "unknown": "Uventet feil",
+ "unknown_authorize_url_generation": "Ukjent feil ved generering av autoriseringsadresse."
},
"error": {
"invalid_auth": "Ugyldig godkjenning"
diff --git a/homeassistant/components/tellduslive/translations/pl.json b/homeassistant/components/tellduslive/translations/pl.json
index 3571ad8e426..81d59e02a12 100644
--- a/homeassistant/components/tellduslive/translations/pl.json
+++ b/homeassistant/components/tellduslive/translations/pl.json
@@ -2,9 +2,10 @@
"config": {
"abort": {
"already_configured": "Us\u0142uga jest ju\u017c skonfigurowana",
- "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji",
+ "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania URL autoryzacji",
"authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji",
- "unknown": "Nieoczekiwany b\u0142\u0105d"
+ "unknown": "Nieoczekiwany b\u0142\u0105d",
+ "unknown_authorize_url_generation": "Nieznany b\u0142\u0105d podczas generowania URL autoryzacji"
},
"error": {
"invalid_auth": "Niepoprawne uwierzytelnienie"
diff --git a/homeassistant/components/tellduslive/translations/ru.json b/homeassistant/components/tellduslive/translations/ru.json
index 0aee4bbd488..0fc0c2f449f 100644
--- a/homeassistant/components/tellduslive/translations/ru.json
+++ b/homeassistant/components/tellduslive/translations/ru.json
@@ -4,7 +4,8 @@
"already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.",
"authorize_url_fail": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.",
"authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.",
- "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430."
+ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.",
+ "unknown_authorize_url_generation": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438."
},
"error": {
"invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f."
diff --git a/homeassistant/components/tellduslive/translations/zh-Hant.json b/homeassistant/components/tellduslive/translations/zh-Hant.json
index e44c2c45416..4ce3d2c478e 100644
--- a/homeassistant/components/tellduslive/translations/zh-Hant.json
+++ b/homeassistant/components/tellduslive/translations/zh-Hant.json
@@ -4,7 +4,8 @@
"already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
"authorize_url_fail": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4",
"authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002",
- "unknown": "\u672a\u9810\u671f\u932f\u8aa4"
+ "unknown": "\u672a\u9810\u671f\u932f\u8aa4",
+ "unknown_authorize_url_generation": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002"
},
"error": {
"invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548"
diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py
index 0fd3205f20c..f996b91a61e 100644
--- a/homeassistant/components/template/binary_sensor.py
+++ b/homeassistant/components/template/binary_sensor.py
@@ -46,8 +46,8 @@ SENSOR_SCHEMA = vol.All(
vol.Optional(ATTR_FRIENDLY_NAME): cv.string,
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
- vol.Optional(CONF_DELAY_ON): cv.positive_time_period,
- vol.Optional(CONF_DELAY_OFF): cv.positive_time_period,
+ vol.Optional(CONF_DELAY_ON): vol.Any(cv.positive_time_period, cv.template),
+ vol.Optional(CONF_DELAY_OFF): vol.Any(cv.positive_time_period, cv.template),
vol.Optional(CONF_UNIQUE_ID): cv.string,
}
),
@@ -71,8 +71,8 @@ async def _async_create_entities(hass, config):
friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device)
device_class = device_config.get(CONF_DEVICE_CLASS)
- delay_on = device_config.get(CONF_DELAY_ON)
- delay_off = device_config.get(CONF_DELAY_OFF)
+ delay_on_raw = device_config.get(CONF_DELAY_ON)
+ delay_off_raw = device_config.get(CONF_DELAY_OFF)
unique_id = device_config.get(CONF_UNIQUE_ID)
sensors.append(
@@ -85,8 +85,8 @@ async def _async_create_entities(hass, config):
icon_template,
entity_picture_template,
availability_template,
- delay_on,
- delay_off,
+ delay_on_raw,
+ delay_off_raw,
attribute_templates,
unique_id,
)
@@ -115,8 +115,8 @@ class BinarySensorTemplate(TemplateEntity, BinarySensorEntity):
icon_template,
entity_picture_template,
availability_template,
- delay_on,
- delay_off,
+ delay_on_raw,
+ delay_off_raw,
attribute_templates,
unique_id,
):
@@ -133,8 +133,10 @@ class BinarySensorTemplate(TemplateEntity, BinarySensorEntity):
self._template = value_template
self._state = None
self._delay_cancel = None
- self._delay_on = delay_on
- self._delay_off = delay_off
+ self._delay_on = None
+ self._delay_on_raw = delay_on_raw
+ self._delay_off = None
+ self._delay_off_raw = delay_off_raw
self._unique_id = unique_id
async def async_added_to_hass(self):
@@ -142,6 +144,22 @@ class BinarySensorTemplate(TemplateEntity, BinarySensorEntity):
self.add_template_attribute("_state", self._template, None, self._update_state)
+ if self._delay_on_raw is not None:
+ try:
+ self._delay_on = cv.positive_time_period(self._delay_on_raw)
+ except vol.Invalid:
+ self.add_template_attribute(
+ "_delay_on", self._delay_on_raw, cv.positive_time_period
+ )
+
+ if self._delay_off_raw is not None:
+ try:
+ self._delay_off = cv.positive_time_period(self._delay_off_raw)
+ except vol.Invalid:
+ self.add_template_attribute(
+ "_delay_off", self._delay_off_raw, cv.positive_time_period
+ )
+
await super().async_added_to_hass()
@callback
diff --git a/homeassistant/components/tesla/switch.py b/homeassistant/components/tesla/switch.py
index 9fcdbae0c1c..efcb955ebf8 100644
--- a/homeassistant/components/tesla/switch.py
+++ b/homeassistant/components/tesla/switch.py
@@ -30,11 +30,13 @@ class ChargerSwitch(TeslaDevice, SwitchEntity):
"""Send the on command."""
_LOGGER.debug("Enable charging: %s", self.name)
await self.tesla_device.start_charge()
+ self.async_write_ha_state()
async def async_turn_off(self, **kwargs):
"""Send the off command."""
_LOGGER.debug("Disable charging for: %s", self.name)
await self.tesla_device.stop_charge()
+ self.async_write_ha_state()
@property
def is_on(self):
@@ -51,11 +53,13 @@ class RangeSwitch(TeslaDevice, SwitchEntity):
"""Send the on command."""
_LOGGER.debug("Enable max range charging: %s", self.name)
await self.tesla_device.set_max()
+ self.async_write_ha_state()
async def async_turn_off(self, **kwargs):
"""Send the off command."""
_LOGGER.debug("Disable max range charging: %s", self.name)
await self.tesla_device.set_standard()
+ self.async_write_ha_state()
@property
def is_on(self):
@@ -87,11 +91,13 @@ class UpdateSwitch(TeslaDevice, SwitchEntity):
"""Send the on command."""
_LOGGER.debug("Enable updates: %s %s", self.name, self.tesla_device.id())
self.controller.set_updates(self.tesla_device.id(), True)
+ self.async_write_ha_state()
async def async_turn_off(self, **kwargs):
"""Send the off command."""
_LOGGER.debug("Disable updates: %s %s", self.name, self.tesla_device.id())
self.controller.set_updates(self.tesla_device.id(), False)
+ self.async_write_ha_state()
@property
def is_on(self):
@@ -108,11 +114,13 @@ class SentryModeSwitch(TeslaDevice, SwitchEntity):
"""Send the on command."""
_LOGGER.debug("Enable sentry mode: %s", self.name)
await self.tesla_device.enable_sentry_mode()
+ self.async_write_ha_state()
async def async_turn_off(self, **kwargs):
"""Send the off command."""
_LOGGER.debug("Disable sentry mode: %s", self.name)
await self.tesla_device.disable_sentry_mode()
+ self.async_write_ha_state()
@property
def is_on(self):
diff --git a/homeassistant/components/tesla/translations/ka.json b/homeassistant/components/tesla/translations/ka.json
new file mode 100644
index 00000000000..249c8f6cffb
--- /dev/null
+++ b/homeassistant/components/tesla/translations/ka.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "\u10d0\u10dc\u10d2\u10d0\u10e0\u10d8\u10e8\u10d8 \u10e3\u10d9\u10d5\u10d4 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/threshold/binary_sensor.py b/homeassistant/components/threshold/binary_sensor.py
index 55ac21f6f13..fa05fc09687 100644
--- a/homeassistant/components/threshold/binary_sensor.py
+++ b/homeassistant/components/threshold/binary_sensor.py
@@ -71,7 +71,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
hass, entity_id, name, lower, upper, hysteresis, device_class
)
],
- True,
)
@@ -88,8 +87,8 @@ class ThresholdSensor(BinarySensorEntity):
self._hysteresis = hysteresis
self._device_class = device_class
- self._state_position = None
- self._state = False
+ self._state_position = POSITION_UNKNOWN
+ self._state = None
self.sensor_value = None
@callback
@@ -107,7 +106,8 @@ class ThresholdSensor(BinarySensorEntity):
self.sensor_value = None
_LOGGER.warning("State is not numerical")
- hass.async_add_job(self.async_update_ha_state, True)
+ self._update_state()
+ self.async_write_ha_state()
async_track_state_change_event(
hass, [entity_id], async_threshold_sensor_state_listener
@@ -156,8 +156,9 @@ class ThresholdSensor(BinarySensorEntity):
ATTR_UPPER: self._threshold_upper,
}
- async def async_update(self):
- """Get the latest data and updates the states."""
+ @callback
+ def _update_state(self):
+ """Update the state."""
def below(threshold):
"""Determine if the sensor value is below a threshold."""
diff --git a/homeassistant/components/tibber/translations/hu.json b/homeassistant/components/tibber/translations/hu.json
index 0b0581d4923..08a622fd238 100644
--- a/homeassistant/components/tibber/translations/hu.json
+++ b/homeassistant/components/tibber/translations/hu.json
@@ -1,6 +1,7 @@
{
"config": {
"error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
"invalid_access_token": "\u00c9rv\u00e9nytelen hozz\u00e1f\u00e9r\u00e9si token"
},
"step": {
diff --git a/homeassistant/components/tibber/translations/ka.json b/homeassistant/components/tibber/translations/ka.json
new file mode 100644
index 00000000000..fa4c9c0abd3
--- /dev/null
+++ b/homeassistant/components/tibber/translations/ka.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u10db\u10d0\u10e0\u10ea\u10ee\u10d8 \u10e8\u10d4\u10d4\u10e0\u10d7\u10d4\u10d1\u10d8\u10e1\u10d0\u10e1"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/time_date/sensor.py b/homeassistant/components/time_date/sensor.py
index b9012385296..4615e9e046c 100644
--- a/homeassistant/components/time_date/sensor.py
+++ b/homeassistant/components/time_date/sensor.py
@@ -80,7 +80,7 @@ class TimeDateSensor(Entity):
return "mdi:clock"
async def async_added_to_hass(self) -> None:
- """Set up next update."""
+ """Set up first update."""
self.unsub = async_track_point_in_utc_time(
self.hass, self.point_in_time_listener, self.get_next_interval()
)
@@ -91,20 +91,27 @@ class TimeDateSensor(Entity):
self.unsub()
self.unsub = None
- def get_next_interval(self, now=None):
+ def get_next_interval(self):
"""Compute next time an update should occur."""
- if now is None:
- now = dt_util.utcnow()
+ now = dt_util.utcnow()
+
if self.type == "date":
- now = dt_util.start_of_local_day(dt_util.as_local(now))
- return now + timedelta(seconds=86400)
+ tomorrow = dt_util.as_local(now) + timedelta(days=1)
+ return dt_util.start_of_local_day(tomorrow)
+
if self.type == "beat":
+ # Add 1 hour because @0 beats is at 23:00:00 UTC.
+ timestamp = dt_util.as_timestamp(now + timedelta(hours=1))
interval = 86.4
else:
+ timestamp = dt_util.as_timestamp(now)
interval = 60
- timestamp = int(dt_util.as_timestamp(now))
+
delta = interval - (timestamp % interval)
- return now + timedelta(seconds=delta)
+ next_interval = now + timedelta(seconds=delta)
+ _LOGGER.debug("%s + %s -> %s (%s)", now, delta, next_interval, self.type)
+
+ return next_interval
def _update_internal_state(self, time_date):
time = dt_util.as_local(time_date).strftime(TIME_STR_FORMAT)
@@ -112,16 +119,6 @@ class TimeDateSensor(Entity):
date = dt_util.as_local(time_date).date().isoformat()
date_utc = time_date.date().isoformat()
- # Calculate Swatch Internet Time.
- time_bmt = time_date + timedelta(hours=1)
- delta = timedelta(
- hours=time_bmt.hour,
- minutes=time_bmt.minute,
- seconds=time_bmt.second,
- microseconds=time_bmt.microsecond,
- )
- beat = int((delta.seconds + delta.microseconds / 1000000.0) / 86.4)
-
if self.type == "time":
self._state = time
elif self.type == "date":
@@ -135,6 +132,19 @@ class TimeDateSensor(Entity):
elif self.type == "time_utc":
self._state = time_utc
elif self.type == "beat":
+ # Calculate Swatch Internet Time.
+ time_bmt = time_date + timedelta(hours=1)
+ delta = timedelta(
+ hours=time_bmt.hour,
+ minutes=time_bmt.minute,
+ seconds=time_bmt.second,
+ microseconds=time_bmt.microsecond,
+ )
+
+ # Use integers to better handle rounding. For example,
+ # int(63763.2/86.4) = 737 but 637632//864 = 738.
+ beat = int(delta.total_seconds() * 10) // 864
+
self._state = f"@{beat:03d}"
elif self.type == "date_time_iso":
self._state = dt_util.parse_datetime(f"{date} {time}").isoformat()
diff --git a/homeassistant/components/toon/oauth2.py b/homeassistant/components/toon/oauth2.py
index 2622e0a9027..e3a83583ac6 100644
--- a/homeassistant/components/toon/oauth2.py
+++ b/homeassistant/components/toon/oauth2.py
@@ -90,8 +90,8 @@ class ToonLocalOAuth2Implementation(config_entry_oauth2_flow.LocalOAuth2Implemen
"""Initialize local Toon auth implementation."""
data = {
"grant_type": "authorization_code",
- "code": external_data,
- "redirect_uri": self.redirect_uri,
+ "code": external_data["code"],
+ "redirect_uri": external_data["state"]["redirect_uri"],
"tenant_id": self.tenant_id,
}
diff --git a/homeassistant/components/toon/strings.json b/homeassistant/components/toon/strings.json
index c5ac07516b6..60d5ed3312c 100644
--- a/homeassistant/components/toon/strings.json
+++ b/homeassistant/components/toon/strings.json
@@ -14,7 +14,7 @@
},
"abort": {
"already_configured": "The selected agreement is already configured.",
- "authorize_url_fail": "Unknown error generating an authorize url.",
+ "unknown_authorize_url_generation": "[%key:common::config_flow::abort::unknown_authorize_url_generation%]",
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"no_agreements": "This account has no Toon displays.",
diff --git a/homeassistant/components/toon/translations/ca.json b/homeassistant/components/toon/translations/ca.json
index 7fc0aef9ec6..b67ff359ceb 100644
--- a/homeassistant/components/toon/translations/ca.json
+++ b/homeassistant/components/toon/translations/ca.json
@@ -6,7 +6,8 @@
"authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.",
"missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.",
"no_agreements": "Aquest compte no t\u00e9 pantalles Toon.",
- "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})"
+ "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})",
+ "unknown_authorize_url_generation": "S'ha produ\u00eft un error desconegut al generar URL d'autoritzaci\u00f3."
},
"step": {
"agreement": {
diff --git a/homeassistant/components/toon/translations/cs.json b/homeassistant/components/toon/translations/cs.json
index 52a2f7b5742..bf4de080873 100644
--- a/homeassistant/components/toon/translations/cs.json
+++ b/homeassistant/components/toon/translations/cs.json
@@ -4,7 +4,8 @@
"authorize_url_fail": "Nezn\u00e1m\u00e1 chyba p\u0159i generov\u00e1n\u00ed autoriza\u010dn\u00ed URL adresy.",
"authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el",
"missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace.",
- "no_url_available": "Nen\u00ed k dispozici \u017e\u00e1dn\u00e1 adresa URL. Informace o t\u00e9to chyb\u011b naleznete [v sekci n\u00e1pov\u011bdy]({docs_url})"
+ "no_url_available": "Nen\u00ed k dispozici \u017e\u00e1dn\u00e1 adresa URL. Informace o t\u00e9to chyb\u011b naleznete [v sekci n\u00e1pov\u011bdy]({docs_url})",
+ "unknown_authorize_url_generation": "Nezn\u00e1m\u00e1 chyba p\u0159i generov\u00e1n\u00ed autoriza\u010dn\u00ed URL adresy."
},
"step": {
"agreement": {
diff --git a/homeassistant/components/toon/translations/en.json b/homeassistant/components/toon/translations/en.json
index eda1dcb1ee3..c64913cfb6c 100644
--- a/homeassistant/components/toon/translations/en.json
+++ b/homeassistant/components/toon/translations/en.json
@@ -6,7 +6,8 @@
"authorize_url_timeout": "Timeout generating authorize URL.",
"missing_configuration": "The component is not configured. Please follow the documentation.",
"no_agreements": "This account has no Toon displays.",
- "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})"
+ "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})",
+ "unknown_authorize_url_generation": "Unknown error generating an authorize url."
},
"step": {
"agreement": {
diff --git a/homeassistant/components/toon/translations/es.json b/homeassistant/components/toon/translations/es.json
index b6c6e7ad67d..6539388f76a 100644
--- a/homeassistant/components/toon/translations/es.json
+++ b/homeassistant/components/toon/translations/es.json
@@ -6,7 +6,8 @@
"authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.",
"missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.",
"no_agreements": "Esta cuenta no tiene pantallas Toon.",
- "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})"
+ "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})",
+ "unknown_authorize_url_generation": "Error desconocido al generar una URL de autorizaci\u00f3n."
},
"step": {
"agreement": {
diff --git a/homeassistant/components/toon/translations/et.json b/homeassistant/components/toon/translations/et.json
index a003702d654..7b70eae433e 100644
--- a/homeassistant/components/toon/translations/et.json
+++ b/homeassistant/components/toon/translations/et.json
@@ -6,7 +6,8 @@
"authorize_url_timeout": "Kinnitus-URLi loomise ajal\u00f5pp.",
"missing_configuration": "Osis pole seadistatud. Palun vaata dokumentatsiooni.",
"no_agreements": "Sellel kontol ei ole Toon-i kuvasid.",
- "no_url_available": "URL pole saadaval. Rohkem teavet [check the help section]({docs_url})"
+ "no_url_available": "URL pole saadaval. Rohkem teavet [check the help section]({docs_url})",
+ "unknown_authorize_url_generation": "Tundmatu viga tuvastamise URL-i loomisel."
},
"step": {
"agreement": {
diff --git a/homeassistant/components/toon/translations/it.json b/homeassistant/components/toon/translations/it.json
index c7cfb228388..d757850a44f 100644
--- a/homeassistant/components/toon/translations/it.json
+++ b/homeassistant/components/toon/translations/it.json
@@ -6,7 +6,8 @@
"authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.",
"missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.",
"no_agreements": "Questo account non ha display Toon.",
- "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})"
+ "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})",
+ "unknown_authorize_url_generation": "Errore sconosciuto durante la generazione di un URL di autorizzazione."
},
"step": {
"agreement": {
diff --git a/homeassistant/components/toon/translations/ka.json b/homeassistant/components/toon/translations/ka.json
new file mode 100644
index 00000000000..8e555221947
--- /dev/null
+++ b/homeassistant/components/toon/translations/ka.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "unknown_authorize_url_generation": "\u10d0\u10d5\u10e2\u10dd\u10e0\u10d8\u10d6\u10d0\u10ea\u10d8\u10d8 \u10d2\u10d4\u10dc\u10d4\u10e0\u10d8\u10e0\u10d4\u10d1\u10d8\u10e1 \u10e3\u10ea\u10dc\u10dd\u10d1\u10d8 \u10e8\u10d4\u10ea\u10d3\u10dd\u10db\u10d0."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/toon/translations/no.json b/homeassistant/components/toon/translations/no.json
index 241f04c5b71..e5a72f35e2b 100644
--- a/homeassistant/components/toon/translations/no.json
+++ b/homeassistant/components/toon/translations/no.json
@@ -6,7 +6,8 @@
"authorize_url_timeout": "Tidsavbrudd som genererer autorer URL-adresse.",
"missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.",
"no_agreements": "Denne kontoen har ingen Toon skjermer.",
- "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})"
+ "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})",
+ "unknown_authorize_url_generation": "Ukjent feil ved generering av autoriseringsadresse."
},
"step": {
"agreement": {
diff --git a/homeassistant/components/toon/translations/pl.json b/homeassistant/components/toon/translations/pl.json
index 2c8715a0044..a41865ddc34 100644
--- a/homeassistant/components/toon/translations/pl.json
+++ b/homeassistant/components/toon/translations/pl.json
@@ -2,11 +2,12 @@
"config": {
"abort": {
"already_configured": "Us\u0142uga jest ju\u017c skonfigurowana",
- "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania autoryzowanego adresu URL",
+ "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania URL autoryzacji",
"authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji",
"missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.",
"no_agreements": "To konto nie posiada wy\u015bwietlaczy Toon",
- "no_url_available": "Brak dost\u0119pnego adresu URL. Aby uzyska\u0107 informacje na temat tego b\u0142\u0119du, [sprawd\u017a sekcj\u0119 pomocy] ({docs_url})"
+ "no_url_available": "Brak dost\u0119pnego adresu URL. Aby uzyska\u0107 informacje na temat tego b\u0142\u0119du, [sprawd\u017a sekcj\u0119 pomocy] ({docs_url})",
+ "unknown_authorize_url_generation": "Nieznany b\u0142\u0105d podczas generowania URL autoryzacji"
},
"step": {
"agreement": {
diff --git a/homeassistant/components/toon/translations/ru.json b/homeassistant/components/toon/translations/ru.json
index 4a601de3c28..162608a0f8c 100644
--- a/homeassistant/components/toon/translations/ru.json
+++ b/homeassistant/components/toon/translations/ru.json
@@ -6,7 +6,8 @@
"authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.",
"missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438.",
"no_agreements": "\u0423 \u044d\u0442\u043e\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u043d\u0435\u0442 \u0434\u0438\u0441\u043f\u043b\u0435\u0435\u0432 Toon.",
- "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435."
+ "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435.",
+ "unknown_authorize_url_generation": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438."
},
"step": {
"agreement": {
diff --git a/homeassistant/components/toon/translations/zh-Hant.json b/homeassistant/components/toon/translations/zh-Hant.json
index 020938792d6..daf5ff0ec18 100644
--- a/homeassistant/components/toon/translations/zh-Hant.json
+++ b/homeassistant/components/toon/translations/zh-Hant.json
@@ -6,7 +6,8 @@
"authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002",
"missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002",
"no_agreements": "\u6b64\u5e33\u865f\u4e26\u672a\u64c1\u6709 Toon \u986f\u793a\u8a2d\u5099\u3002",
- "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})"
+ "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})",
+ "unknown_authorize_url_generation": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002"
},
"step": {
"agreement": {
diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json
index 14de87b7b69..5b49d8ef1b4 100644
--- a/homeassistant/components/tplink/manifest.json
+++ b/homeassistant/components/tplink/manifest.json
@@ -4,10 +4,10 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/tplink",
"requirements": [
- "pyHS100==0.3.5.1"
+ "pyHS100==0.3.5.2"
],
"codeowners": [
"@rytilahti",
"@thegardenmonkey"
]
-}
\ No newline at end of file
+}
diff --git a/homeassistant/components/traccar/translations/hu.json b/homeassistant/components/traccar/translations/hu.json
new file mode 100644
index 00000000000..a14c446e673
--- /dev/null
+++ b/homeassistant/components/traccar/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "create_entry": {
+ "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a webhook funkci\u00f3t a Traccar-ban. \n\n Haszn\u00e1lja a k\u00f6vetkez\u0151 URL-t: \" {webhook_url} \" \n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1sd a [dokument\u00e1ci\u00f3t] ( {docs_url} )."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/traccar/translations/ka.json b/homeassistant/components/traccar/translations/ka.json
new file mode 100644
index 00000000000..a284a55fbcf
--- /dev/null
+++ b/homeassistant/components/traccar/translations/ka.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "webhook_not_internet_accessible": "\u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 Home Assistant \u10d4\u10d9\u10d6\u10d4\u10db\u10de\u10da\u10d0\u10e0\u10d8 \u10e1\u10d0\u10ed\u10d8\u10e0\u10dd\u10e0\u10d4\u10d1\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10dc\u10d4\u10e2\u10d7\u10d0\u10dc \u10ec\u10d5\u10d3\u10dd\u10db\u10d0\u10e1 webhook \u10e8\u10d4\u10e2\u10e7\u10dd\u10d1\u10d8\u10dc\u10d4\u10d1\u10d4\u10d1\u10d8\u10e1 \u10db\u10d8\u10e1\u10d0\u10e6\u10d4\u10d1\u10d0\u10d3."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/traccar/translations/pl.json b/homeassistant/components/traccar/translations/pl.json
index 7b990190d84..619f6e57192 100644
--- a/homeassistant/components/traccar/translations/pl.json
+++ b/homeassistant/components/traccar/translations/pl.json
@@ -5,7 +5,7 @@
"webhook_not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty webhook"
},
"create_entry": {
- "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant, 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/no.json b/homeassistant/components/tradfri/translations/no.json
index 917a6587b84..abdf0a26b12 100644
--- a/homeassistant/components/tradfri/translations/no.json
+++ b/homeassistant/components/tradfri/translations/no.json
@@ -15,7 +15,7 @@
"host": "Vert",
"security_code": "Sikkerhetskode"
},
- "description": "Du finner sikkerhetskoden p\u00e5 baksiden av gatewayen din.",
+ "description": "Du finner sikkerhetskoden p\u00e5 baksiden av gatewayen din",
"title": "Angi sikkerhetskode"
}
}
diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py
index ef1e68e2d0a..ea62de71e8d 100644
--- a/homeassistant/components/transmission/sensor.py
+++ b/homeassistant/components/transmission/sensor.py
@@ -145,12 +145,10 @@ class TransmissionTorrentsSensor(TransmissionSensor):
@property
def device_state_attributes(self):
"""Return the state attributes, if any."""
- limit = self._tm_client.config_entry.options[CONF_LIMIT]
- order = self._tm_client.config_entry.options[CONF_ORDER]
- torrents = self._tm_client.api.torrents[0:limit]
info = _torrents_info(
- torrents,
- order=order,
+ torrents=self._tm_client.api.torrents,
+ order=self._tm_client.config_entry.options[CONF_ORDER],
+ limit=self._tm_client.config_entry.options[CONF_LIMIT],
statuses=self.SUBTYPE_MODES[self._sub_type],
)
return {
@@ -173,11 +171,11 @@ def _filter_torrents(torrents, statuses=None):
]
-def _torrents_info(torrents, order, statuses=None):
+def _torrents_info(torrents, order, limit, statuses=None):
infos = {}
torrents = _filter_torrents(torrents, statuses)
torrents = SUPPORTED_ORDER_MODES[order](torrents)
- for torrent in _filter_torrents(torrents, statuses):
+ for torrent in torrents[:limit]:
info = infos[torrent.name] = {
"added_date": torrent.addedDate,
"percent_done": f"{torrent.percentDone * 100:.2f}",
diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py
index 64a2d203695..5876331ea97 100644
--- a/homeassistant/components/tuya/__init__.py
+++ b/homeassistant/components/tuya/__init__.py
@@ -6,6 +6,7 @@ import logging
from tuyaha import TuyaApi
from tuyaha.tuyaapi import (
TuyaAPIException,
+ TuyaAPIRateLimitException,
TuyaFrequentlyInvokeException,
TuyaNetException,
TuyaServerException,
@@ -137,6 +138,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
) as exc:
raise ConfigEntryNotReady() from exc
+ except TuyaAPIRateLimitException as exc:
+ _LOGGER.error("Tuya login rate limited")
+ raise ConfigEntryNotReady() from exc
+
except TuyaAPIException as exc:
_LOGGER.error(
"Connection error during integration setup. Error: %s",
@@ -269,7 +274,7 @@ async def cleanup_device_registry(hass: HomeAssistant, device_id):
device_registry = await hass.helpers.device_registry.async_get_registry()
entity_registry = await hass.helpers.entity_registry.async_get_registry()
if device_id and not hass.helpers.entity_registry.async_entries_for_device(
- entity_registry, device_id
+ entity_registry, device_id, include_disabled_entities=True
):
device_registry.async_remove_device(device_id)
diff --git a/homeassistant/components/tuya/config_flow.py b/homeassistant/components/tuya/config_flow.py
index e2048aaf7bf..5d22a83e03e 100644
--- a/homeassistant/components/tuya/config_flow.py
+++ b/homeassistant/components/tuya/config_flow.py
@@ -2,7 +2,12 @@
import logging
from tuyaha import TuyaApi
-from tuyaha.tuyaapi import TuyaAPIException, TuyaNetException, TuyaServerException
+from tuyaha.tuyaapi import (
+ TuyaAPIException,
+ TuyaAPIRateLimitException,
+ TuyaNetException,
+ TuyaServerException,
+)
import voluptuous as vol
from homeassistant import config_entries
@@ -103,7 +108,7 @@ class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
tuya.init(
self._username, self._password, self._country_code, self._platform
)
- except (TuyaNetException, TuyaServerException):
+ except (TuyaAPIRateLimitException, TuyaNetException, TuyaServerException):
return RESULT_CONN_ERROR
except TuyaAPIException:
return RESULT_AUTH_FAILED
@@ -249,6 +254,11 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
async def async_step_init(self, user_input=None):
"""Handle options flow."""
+
+ if self.config_entry.state != config_entries.ENTRY_STATE_LOADED:
+ _LOGGER.error("Tuya integration not yet loaded")
+ return self.async_abort(reason="cannot_connect")
+
if user_input is not None:
dev_ids = user_input.get(CONF_LIST_DEVICES)
if dev_ids:
diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json
index 642a4dbe5d1..7481e56f00a 100644
--- a/homeassistant/components/tuya/manifest.json
+++ b/homeassistant/components/tuya/manifest.json
@@ -2,7 +2,7 @@
"domain": "tuya",
"name": "Tuya",
"documentation": "https://www.home-assistant.io/integrations/tuya",
- "requirements": ["tuyaha==0.0.8"],
+ "requirements": ["tuyaha==0.0.9"],
"codeowners": ["@ollo69"],
"config_flow": true
}
diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json
index 84575906010..444ff0b5c21 100644
--- a/homeassistant/components/tuya/strings.json
+++ b/homeassistant/components/tuya/strings.json
@@ -23,6 +23,9 @@
}
},
"options": {
+ "abort": {
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
+ },
"step": {
"init": {
"title": "Configure Tuya Options",
diff --git a/homeassistant/components/tuya/translations/hu.json b/homeassistant/components/tuya/translations/hu.json
index 97359a9a787..7c90b937329 100644
--- a/homeassistant/components/tuya/translations/hu.json
+++ b/homeassistant/components/tuya/translations/hu.json
@@ -1,14 +1,59 @@
{
"config": {
"abort": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen autentik\u00e1ci\u00f3",
"single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges."
},
+ "error": {
+ "invalid_auth": "\u00c9rv\u00e9nytelen autentik\u00e1ci\u00f3"
+ },
+ "flow_title": "Tuya konfigur\u00e1ci\u00f3",
"step": {
"user": {
"data": {
+ "country_code": "A fi\u00f3k orsz\u00e1gk\u00f3dja (pl. 1 USA, 36 Magyarorsz\u00e1g, vagy 86 K\u00edna)",
"password": "Jelsz\u00f3",
+ "platform": "Az alkalmaz\u00e1s, ahol a fi\u00f3k regisztr\u00e1lt",
"username": "Felhaszn\u00e1l\u00f3n\u00e9v"
- }
+ },
+ "description": "Adja meg Tuya hiteles\u00edt\u0151 adatait.",
+ "title": "Tuya"
+ }
+ }
+ },
+ "options": {
+ "error": {
+ "dev_multi_type": "A konfigur\u00e1land\u00f3 eszk\u00f6z\u00f6knek azonos t\u00edpus\u00faaknak kell lennie",
+ "dev_not_config": "Ez az eszk\u00f6zt\u00edpus nem konfigur\u00e1lhat\u00f3",
+ "dev_not_found": "Eszk\u00f6z nem tal\u00e1lhat\u00f3"
+ },
+ "step": {
+ "device": {
+ "data": {
+ "brightness_range_mode": "Az eszk\u00f6z \u00e1ltal haszn\u00e1lt f\u00e9nyer\u0151 tartom\u00e1ny",
+ "curr_temp_divider": "Aktu\u00e1lis h\u0151m\u00e9rs\u00e9klet-\u00e9rt\u00e9k oszt\u00f3 (0 = alap\u00e9rtelmezetten)",
+ "max_kelvin": "Maxim\u00e1lis t\u00e1mogatott sz\u00ednh\u0151m\u00e9rs\u00e9klet kelvinben",
+ "max_temp": "Maxim\u00e1lis c\u00e9l-sz\u00ednh\u0151m\u00e9rs\u00e9klet (haszn\u00e1lja a min-t \u00e9s a max-ot = 0-t az alap\u00e9rtelmezett be\u00e1ll\u00edt\u00e1shoz)",
+ "min_kelvin": "Minimum t\u00e1mogatott sz\u00ednh\u0151m\u00e9rs\u00e9klet kelvinben",
+ "min_temp": "Min. C\u00e9l-sz\u00ednh\u0151m\u00e9rs\u00e9klet (alap\u00e9rtelmez\u00e9s szerint haszn\u00e1ljon min-t \u00e9s max-ot = 0-t az alap\u00e9rtelmezett be\u00e1ll\u00edt\u00e1shoz)",
+ "support_color": "Sz\u00ednt\u00e1mogat\u00e1s k\u00e9nyszer\u00edt\u00e9se",
+ "temp_divider": "Sz\u00ednh\u0151m\u00e9rs\u00e9klet-\u00e9rt\u00e9kek oszt\u00f3ja (0 = alap\u00e9rtelmezett)",
+ "tuya_max_coltemp": "Az eszk\u00f6z \u00e1ltal megadott maxim\u00e1lis sz\u00ednh\u0151m\u00e9rs\u00e9klet",
+ "unit_of_measurement": "Az eszk\u00f6z \u00e1ltal haszn\u00e1lt h\u0151m\u00e9rs\u00e9kleti egys\u00e9g"
+ },
+ "description": "Konfigur\u00e1lja a(z) {device_type} eszk\u00f6zt \" {device_name} {device_type} \" megjelen\u00edtett inform\u00e1ci\u00f3inak be\u00e1ll\u00edt\u00e1s\u00e1hoz",
+ "title": "Konfigur\u00e1lja a Tuya eszk\u00f6zt"
+ },
+ "init": {
+ "data": {
+ "discovery_interval": "Felfedez\u0151 eszk\u00f6z lek\u00e9rdez\u00e9si intervalluma m\u00e1sodpercben",
+ "list_devices": "V\u00e1lassza ki a konfigur\u00e1lni k\u00edv\u00e1nt eszk\u00f6z\u00f6ket, vagy hagyja \u00fcresen a konfigur\u00e1ci\u00f3 ment\u00e9s\u00e9hez",
+ "query_device": "V\u00e1lassza ki azt az eszk\u00f6zt, amely a lek\u00e9rdez\u00e9si m\u00f3dszert haszn\u00e1lja a gyorsabb \u00e1llapotfriss\u00edt\u00e9shez",
+ "query_interval": "Eszk\u00f6z lek\u00e9rdez\u00e9si id\u0151k\u00f6ze m\u00e1sodpercben"
+ },
+ "description": "Ne \u00e1ll\u00edtsa t\u00fal alacsonyra a lek\u00e9rdez\u00e9si intervallum \u00e9rt\u00e9keit, k\u00fcl\u00f6nben a h\u00edv\u00e1sok nem fognak hiba\u00fczenetet gener\u00e1lni a napl\u00f3ban",
+ "title": "Konfigur\u00e1lja a Tuya be\u00e1ll\u00edt\u00e1sokat"
}
}
}
diff --git a/homeassistant/components/tuya/translations/ka.json b/homeassistant/components/tuya/translations/ka.json
new file mode 100644
index 00000000000..7c80ef1ffba
--- /dev/null
+++ b/homeassistant/components/tuya/translations/ka.json
@@ -0,0 +1,37 @@
+{
+ "options": {
+ "error": {
+ "dev_multi_type": "\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d8\u10e1\u10d7\u10d5\u10d8\u10e1 \u10e8\u10d4\u10e0\u10e9\u10d4\u10e3\u10da\u10d8 \u10db\u10e0\u10d0\u10d5\u10da\u10dd\u10d1\u10d8\u10d7\u10d8 \u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0 \u10e3\u10dc\u10d3\u10d0 \u10d8\u10e7\u10dd\u10e1 \u10d4\u10e0\u10d7\u10dc\u10d0\u10d8\u10e0\u10d8 \u10e2\u10d8\u10de\u10d8\u10e1",
+ "dev_not_config": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d8\u10e1 \u10e2\u10d8\u10de\u10d8 \u10d0\u10e0 \u10d0\u10e0\u10d8\u10e1 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10d0\u10d3\u10d8",
+ "dev_not_found": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10d8\u10eb\u10d4\u10d1\u10dc\u10d0"
+ },
+ "step": {
+ "device": {
+ "data": {
+ "brightness_range_mode": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d8\u10e1 \u10db\u10d8\u10d4\u10e0 \u10d2\u10d0\u10db\u10dd\u10e7\u10d4\u10dc\u10d4\u10d1\u10e3\u10da\u10d8 \u10e1\u10d8\u10d9\u10d0\u10e8\u10d9\u10d0\u10e8\u10d8\u10e1 \u10d3\u10d8\u10d0\u10de\u10d0\u10d6\u10dd\u10dc\u10d8",
+ "curr_temp_divider": "\u10db\u10d8\u10db\u10d3\u10d8\u10dc\u10d0\u10e0\u10d4 \u10e2\u10d4\u10db\u10d4\u10de\u10e0\u10d0\u10e2\u10e3\u10e0\u10d8\u10e1 \u10db\u10dc\u10d8\u10e8\u10d5\u10dc\u10d4\u10da\u10d8\u10e1 \u10d2\u10d0\u10db\u10e7\u10dd\u10e4\u10d8 (0 - \u10dc\u10d0\u10d2\u10e3\u10da\u10d8\u10e1\u10ee\u10db\u10d4\u10d5\u10d8)",
+ "max_kelvin": "\u10db\u10ee\u10d0\u10e0\u10d3\u10d0\u10ed\u10d4\u10e0\u10d8\u10da\u10d8 \u10db\u10d0\u10e5\u10e1\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8 \u10e2\u10d4\u10db\u10de\u10d4\u10e0\u10d0\u10e2\u10e3\u10e0\u10d8\u10e1 \u10e4\u10d4\u10e0\u10d8 \u10d9\u10d4\u10da\u10d5\u10d8\u10dc\u10d4\u10d1\u10e8\u10d8",
+ "max_temp": "\u10db\u10d0\u10e5\u10e1\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8 \u10db\u10d8\u10d6\u10dc\u10dd\u10d1\u10e0\u10d8\u10d5\u10d8 \u10e2\u10d4\u10db\u10de\u10d4\u10e0\u10d0\u10e2\u10e3\u10e0\u10d0 (\u10db\u10d8\u10dc\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8 \u10d3\u10d0 \u10db\u10d0\u10e5\u10e1\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8\u10e1\u10d0\u10d7\u10d5\u10d8\u10e1 \u10dc\u10d0\u10d2\u10e3\u10da\u10d8\u10e1\u10ee\u10db\u10d4\u10d5\u10d8\u10d0 0)",
+ "min_kelvin": "\u10db\u10ee\u10d0\u10e0\u10d3\u10d0\u10ed\u10d4\u10e0\u10d8\u10da\u10d8 \u10db\u10d8\u10dc\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8 \u10e2\u10d4\u10db\u10de\u10d4\u10e0\u10d0\u10e2\u10e3\u10e0\u10d8\u10e1 \u10e4\u10d4\u10e0\u10d8 \u10d9\u10d4\u10da\u10d5\u10d8\u10dc\u10d4\u10d1\u10e8\u10d8",
+ "min_temp": "\u10db\u10d8\u10dc\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8 \u10db\u10d8\u10d6\u10dc\u10dd\u10d1\u10e0\u10d8\u10d5\u10d8 \u10e2\u10d4\u10db\u10de\u10d4\u10e0\u10d0\u10e2\u10e3\u10e0\u10d0 (\u10db\u10d8\u10dc\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8 \u10d3\u10d0 \u10db\u10d0\u10e5\u10e1\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8\u10e1\u10d0\u10d7\u10d5\u10d8\u10e1 \u10dc\u10d0\u10d2\u10e3\u10da\u10d8\u10e1\u10ee\u10db\u10d4\u10d5\u10d8\u10d0 0)",
+ "support_color": "\u10e4\u10d4\u10e0\u10d8\u10e1 \u10db\u10ee\u10d0\u10e0\u10d3\u10d0\u10ed\u10d4\u10e0\u10d0 \u10d8\u10eb\u10e3\u10da\u10d4\u10d1\u10d8\u10d7",
+ "temp_divider": "\u10e2\u10d4\u10db\u10de\u10d4\u10e0\u10d0\u10e2\u10e3\u10e0\u10e3\u10da\u10d8 \u10db\u10dc\u10d8\u10e8\u10d5\u10dc\u10d4\u10da\u10d8\u10e1 \u10d2\u10d0\u10db\u10e7\u10dd\u10e4\u10d8 (0 = \u10dc\u10d0\u10d2\u10e3\u10da\u10d8\u10e1\u10ee\u10db\u10d4\u10d5\u10d8)",
+ "tuya_max_coltemp": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d8\u10e1 \u10db\u10d8\u10d4\u10e0 \u10db\u10dd\u10ec\u10dd\u10d3\u10d4\u10d1\u10e3\u10da\u10d8 \u10e4\u10d4\u10e0\u10d8\u10e1 \u10db\u10d0\u10e5\u10e1\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8 \u10e2\u10d4\u10db\u10de\u10d4\u10e0\u10d0\u10e2\u10e3\u10e0\u10d0",
+ "unit_of_measurement": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d8\u10e1 \u10db\u10d8\u10d4\u10e0 \u10d2\u10d0\u10db\u10dd\u10e7\u10d4\u10dc\u10d4\u10d1\u10e3\u10da\u10d8 \u10e2\u10d4\u10db\u10de\u10d4\u10e0\u10d0\u10e2\u10e3\u10e0\u10e3\u10da\u10d8 \u10d4\u10e0\u10d7\u10d4\u10e3\u10da\u10d8"
+ },
+ "description": "\u10d3\u10d0\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d3 {device_type} `{device_name}` \u10de\u10d0\u10e0\u10d0\u10db\u10d4\u10e2\u10d4\u10e0\u10d1\u10d8 \u10d8\u10dc\u10e4\u10dd\u10e0\u10db\u10d0\u10ea\u10d8\u10d8\u10e1 \u10e9\u10d5\u10d4\u10dc\u10d4\u10d1\u10d8\u10e1 \u10db\u10dd\u10e1\u10d0\u10e0\u10d2\u10d4\u10d1\u10d0\u10d3",
+ "title": "Tuya-\u10e1 \u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d8\u10e1 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10d0"
+ },
+ "init": {
+ "data": {
+ "discovery_interval": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d8\u10e1 \u10d0\u10e6\u10db\u10dd\u10e9\u10d4\u10dc\u10d8\u10e1 \u10d2\u10d0\u10db\u10dd\u10d9\u10d8\u10d7\u10ee\u10d5\u10d8\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10d5\u10d0\u10da\u10d8 \u10ec\u10d0\u10db\u10d4\u10d1\u10e8\u10d8",
+ "list_devices": "\u10d0\u10d8\u10e0\u10e9\u10d8\u10d4\u10d7 \u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d8\u10e1\u10d7\u10d5\u10d8\u10e1 \u10d0\u10dc \u10d3\u10d0\u10e2\u10dd\u10d5\u10d4\u10d7 \u10ea\u10d0\u10e0\u10d8\u10d4\u10da\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d8\u10e1 \u10e8\u10d4\u10e1\u10d0\u10dc\u10d0\u10ee\u10d0\u10d3",
+ "query_device": "\u10d0\u10d8\u10e0\u10e9\u10d8\u10d4\u10d7 \u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0, \u10e0\u10dd\u10db\u10d4\u10da\u10d8\u10ea \u10d2\u10d0\u10db\u10dd\u10d8\u10e7\u10d4\u10dc\u10d4\u10d1\u10e1 \u10db\u10dd\u10d7\u10ee\u10dd\u10d5\u10dc\u10d8\u10e1 \u10db\u10d4\u10d7\u10dd\u10d3\u10e1 \u10e1\u10e2\u10d0\u10e2\u10e3\u10e1\u10d8\u10e1 \u10e1\u10ec\u10e0\u10d0\u10e4\u10d8 \u10d2\u10d0\u10dc\u10d0\u10ee\u10da\u10d4\u10d1\u10d8\u10e1\u10d7\u10d5\u10d8\u10e1",
+ "query_interval": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d8\u10e1 \u10d2\u10d0\u10db\u10dd\u10d9\u10d8\u10d7\u10ee\u10d5\u10d8\u10e1 \u10db\u10dd\u10d7\u10ee\u10dd\u10d5\u10dc\u10d8\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10d5\u10d0\u10da\u10d8 \u10ec\u10d0\u10db\u10d4\u10d1\u10e8\u10d8"
+ },
+ "description": "\u10d0\u10e0 \u10d3\u10d0\u10d0\u10e7\u10d4\u10dc\u10dd\u10d7 \u10d2\u10d0\u10db\u10dd\u10d9\u10d8\u10d7\u10ee\u10d5\u10d8\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10d5\u10d0\u10da\u10d8\u10e1 \u10db\u10dc\u10d8\u10e8\u10d5\u10dc\u10d4\u10da\u10dd\u10d1\u10d4\u10d1\u10d8 \u10eb\u10d0\u10da\u10d8\u10d0\u10dc \u10db\u10ea\u10d8\u10e0\u10d4 \u10d7\u10dd\u10e0\u10d4\u10d1 \u10d2\u10d0\u10db\u10dd\u10eb\u10d0\u10ee\u10d4\u10d1\u10d4\u10d1\u10d8 \u10d3\u10d0\u10d0\u10d2\u10d4\u10dc\u10d4\u10e0\u10d8\u10e0\u10d4\u10d1\u10d4\u10dc \u10e8\u10d4\u10ea\u10d3\u10dd\u10db\u10d4\u10d1\u10e1 \u10da\u10dd\u10d2\u10e8\u10d8",
+ "title": "Tuya-\u10e1 \u10de\u10d0\u10e0\u10d0\u10db\u10d4\u10e2\u10e0\u10d4\u10d1\u10d8\u10e1 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10d0"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/tuya/translations/sl.json b/homeassistant/components/tuya/translations/sl.json
new file mode 100644
index 00000000000..4879603af6c
--- /dev/null
+++ b/homeassistant/components/tuya/translations/sl.json
@@ -0,0 +1,8 @@
+{
+ "options": {
+ "error": {
+ "dev_not_config": "Vrsta naprave ni nastavljiva",
+ "dev_not_found": "Naprave ni mogo\u010de najti"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twilio/translations/hu.json b/homeassistant/components/twilio/translations/hu.json
index f68f610bf2f..913e3d2a7a2 100644
--- a/homeassistant/components/twilio/translations/hu.json
+++ b/homeassistant/components/twilio/translations/hu.json
@@ -1,5 +1,8 @@
{
"config": {
+ "create_entry": {
+ "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a [Webhooks Twilio-val] ( {twilio_url} ) alkalmaz\u00e1st. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: \" {webhook_url} \"\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/x-www-form-urlencoded \n\n L\u00e1sd [a dokument\u00e1ci\u00f3] ( {docs_url} ), hogyan konfigur\u00e1lhatja az automatiz\u00e1l\u00e1sokat a bej\u00f6v\u0151 adatok kezel\u00e9s\u00e9re."
+ },
"step": {
"user": {
"description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani a Twilio-t?",
diff --git a/homeassistant/components/twilio/translations/ka.json b/homeassistant/components/twilio/translations/ka.json
new file mode 100644
index 00000000000..a284a55fbcf
--- /dev/null
+++ b/homeassistant/components/twilio/translations/ka.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "webhook_not_internet_accessible": "\u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 Home Assistant \u10d4\u10d9\u10d6\u10d4\u10db\u10de\u10da\u10d0\u10e0\u10d8 \u10e1\u10d0\u10ed\u10d8\u10e0\u10dd\u10e0\u10d4\u10d1\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10dc\u10d4\u10e2\u10d7\u10d0\u10dc \u10ec\u10d5\u10d3\u10dd\u10db\u10d0\u10e1 webhook \u10e8\u10d4\u10e2\u10e7\u10dd\u10d1\u10d8\u10dc\u10d4\u10d1\u10d4\u10d1\u10d8\u10e1 \u10db\u10d8\u10e1\u10d0\u10e6\u10d4\u10d1\u10d0\u10d3."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twilio/translations/pl.json b/homeassistant/components/twilio/translations/pl.json
index 667dddd747e..e6be0a02aed 100644
--- a/homeassistant/components/twilio/translations/pl.json
+++ b/homeassistant/components/twilio/translations/pl.json
@@ -5,7 +5,7 @@
"webhook_not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty webhook"
},
"create_entry": {
- "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant, 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/twinkly/__init__.py b/homeassistant/components/twinkly/__init__.py
new file mode 100644
index 00000000000..2b605104609
--- /dev/null
+++ b/homeassistant/components/twinkly/__init__.py
@@ -0,0 +1,44 @@
+"""The twinkly component."""
+
+import twinkly_client
+
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.typing import HomeAssistantType
+
+from .const import CONF_ENTRY_HOST, CONF_ENTRY_ID, DOMAIN
+
+
+async def async_setup(hass: HomeAssistantType, config: dict):
+ """Set up the twinkly integration."""
+
+ return True
+
+
+async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry):
+ """Set up entries from config flow."""
+
+ # We setup the client here so if at some point we add any other entity for this device,
+ # we will be able to properly share the connection.
+ uuid = config_entry.data[CONF_ENTRY_ID]
+ host = config_entry.data[CONF_ENTRY_HOST]
+
+ hass.data.setdefault(DOMAIN, {})[uuid] = twinkly_client.TwinklyClient(
+ host, async_get_clientsession(hass)
+ )
+
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(config_entry, "light")
+ )
+ return True
+
+
+async def async_unload_entry(hass: HomeAssistantType, config_entry: ConfigEntry):
+ """Remove a twinkly entry."""
+
+ # For now light entries don't have unload method, so we don't have to async_forward_entry_unload
+ # However we still have to cleanup the shared client!
+ uuid = config_entry.data[CONF_ENTRY_ID]
+ hass.data[DOMAIN].pop(uuid)
+
+ return True
diff --git a/homeassistant/components/twinkly/config_flow.py b/homeassistant/components/twinkly/config_flow.py
new file mode 100644
index 00000000000..f1593de5643
--- /dev/null
+++ b/homeassistant/components/twinkly/config_flow.py
@@ -0,0 +1,63 @@
+"""Config flow to configure the Twinkly integration."""
+
+import asyncio
+import logging
+
+from aiohttp import ClientError
+import twinkly_client
+from voluptuous import Required, Schema
+
+from homeassistant import config_entries
+from homeassistant.const import CONF_HOST
+
+from .const import (
+ CONF_ENTRY_HOST,
+ CONF_ENTRY_ID,
+ CONF_ENTRY_MODEL,
+ CONF_ENTRY_NAME,
+ DEV_ID,
+ DEV_MODEL,
+ DEV_NAME,
+)
+
+# https://github.com/PyCQA/pylint/issues/3202
+from .const import DOMAIN # pylint: disable=unused-import
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class TwinklyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
+ """Handle twinkly config flow."""
+
+ VERSION = 1
+
+ async def async_step_user(self, user_input=None):
+ """Handle config steps."""
+ host = user_input[CONF_HOST] if user_input else None
+
+ schema = {Required(CONF_HOST, default=host): str}
+ errors = {}
+
+ if host is not None:
+ try:
+ device_info = await twinkly_client.TwinklyClient(host).get_device_info()
+
+ await self.async_set_unique_id(device_info[DEV_ID])
+ self._abort_if_unique_id_configured()
+
+ return self.async_create_entry(
+ title=device_info[DEV_NAME],
+ data={
+ CONF_ENTRY_HOST: host,
+ CONF_ENTRY_ID: device_info[DEV_ID],
+ CONF_ENTRY_NAME: device_info[DEV_NAME],
+ CONF_ENTRY_MODEL: device_info[DEV_MODEL],
+ },
+ )
+ except (asyncio.TimeoutError, ClientError) as err:
+ _LOGGER.info("Cannot reach Twinkly '%s' (client)", host, exc_info=err)
+ errors[CONF_HOST] = "cannot_connect"
+
+ return self.async_show_form(
+ step_id="user", data_schema=Schema(schema), errors=errors
+ )
diff --git a/homeassistant/components/twinkly/const.py b/homeassistant/components/twinkly/const.py
new file mode 100644
index 00000000000..2e7d4d2fd0e
--- /dev/null
+++ b/homeassistant/components/twinkly/const.py
@@ -0,0 +1,23 @@
+"""Const for Twinkly."""
+
+DOMAIN = "twinkly"
+
+# Keys of the config entry
+CONF_ENTRY_ID = "id"
+CONF_ENTRY_HOST = "host"
+CONF_ENTRY_NAME = "name"
+CONF_ENTRY_MODEL = "model"
+
+# Strongly named HA attributes keys
+ATTR_HOST = "host"
+
+# Keys of attributes read from the get_device_info
+DEV_ID = "uuid"
+DEV_NAME = "device_name"
+DEV_MODEL = "product_code"
+
+HIDDEN_DEV_VALUES = (
+ "code", # This is the internal status code of the API response
+ "copyright", # We should not display a copyright "LEDWORKS 2018" in the Home-Assistant UI
+ "mac", # Does not report the actual device mac address
+)
diff --git a/homeassistant/components/twinkly/light.py b/homeassistant/components/twinkly/light.py
new file mode 100644
index 00000000000..8de51d19d51
--- /dev/null
+++ b/homeassistant/components/twinkly/light.py
@@ -0,0 +1,216 @@
+"""The Twinkly light component."""
+
+import asyncio
+import logging
+from typing import Any, Dict, Optional
+
+from aiohttp import ClientError
+
+from homeassistant.components.light import (
+ ATTR_BRIGHTNESS,
+ SUPPORT_BRIGHTNESS,
+ LightEntity,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.helpers.typing import HomeAssistantType
+
+from .const import (
+ ATTR_HOST,
+ CONF_ENTRY_HOST,
+ CONF_ENTRY_ID,
+ CONF_ENTRY_MODEL,
+ CONF_ENTRY_NAME,
+ DEV_MODEL,
+ DEV_NAME,
+ DOMAIN,
+ HIDDEN_DEV_VALUES,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(
+ hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities
+) -> None:
+ """Setups an entity from a config entry (UI config flow)."""
+
+ entity = TwinklyLight(config_entry, hass)
+
+ async_add_entities([entity], update_before_add=True)
+
+
+class TwinklyLight(LightEntity):
+ """Implementation of the light for the Twinkly service."""
+
+ def __init__(
+ self,
+ conf: ConfigEntry,
+ hass: HomeAssistantType,
+ ):
+ """Initialize a TwinklyLight entity."""
+ self._id = conf.data[CONF_ENTRY_ID]
+ self._hass = hass
+ self._conf = conf
+
+ # Those are saved in the config entry in order to have meaningful values even
+ # if the device is currently offline.
+ # They are expected to be updated using the device_info.
+ self.__name = conf.data[CONF_ENTRY_NAME]
+ self.__model = conf.data[CONF_ENTRY_MODEL]
+
+ self._client = hass.data.get(DOMAIN, {}).get(self._id)
+ if self._client is None:
+ raise ValueError(f"Client for {self._id} has not been configured.")
+
+ # Set default state before any update
+ self._is_on = False
+ self._brightness = 0
+ self._is_available = False
+ self._attributes = {ATTR_HOST: self._client.host}
+
+ @property
+ def supported_features(self):
+ """Get the features supported by this entity."""
+ return SUPPORT_BRIGHTNESS
+
+ @property
+ def should_poll(self) -> bool:
+ """Get a boolean which indicates if this entity should be polled."""
+ return True
+
+ @property
+ def available(self) -> bool:
+ """Get a boolean which indicates if this entity is currently available."""
+ return self._is_available
+
+ @property
+ def unique_id(self) -> Optional[str]:
+ """Id of the device."""
+ return self._id
+
+ @property
+ def name(self) -> str:
+ """Name of the device."""
+ return self.__name if self.__name else "Twinkly light"
+
+ @property
+ def model(self) -> str:
+ """Name of the device."""
+ return self.__model
+
+ @property
+ def icon(self) -> str:
+ """Icon of the device."""
+ return "mdi:string-lights"
+
+ @property
+ def device_info(self) -> Optional[Dict[str, Any]]:
+ """Get device specific attributes."""
+ return (
+ {
+ "identifiers": {(DOMAIN, self._id)},
+ "name": self.name,
+ "manufacturer": "LEDWORKS",
+ "model": self.model,
+ }
+ if self._id
+ else None # device_info is available only for entities configured from the UI
+ )
+
+ @property
+ def is_on(self) -> bool:
+ """Return true if light is on."""
+ return self._is_on
+
+ @property
+ def brightness(self) -> Optional[int]:
+ """Return the brightness of the light."""
+ return self._brightness
+
+ @property
+ def state_attributes(self) -> dict:
+ """Return device specific state attributes."""
+
+ attributes = self._attributes
+
+ # Make sure to update any normalized property
+ attributes[ATTR_HOST] = self._client.host
+ attributes[ATTR_BRIGHTNESS] = self._brightness
+
+ return attributes
+
+ async def async_turn_on(self, **kwargs) -> None:
+ """Turn device on."""
+ if ATTR_BRIGHTNESS in kwargs:
+ brightness = int(int(kwargs[ATTR_BRIGHTNESS]) / 2.55)
+
+ # If brightness is 0, the twinkly will only "disable" the brightness,
+ # which means that it will be 100%.
+ if brightness == 0:
+ await self._client.set_is_on(False)
+ return
+
+ await self._client.set_brightness(brightness)
+
+ await self._client.set_is_on(True)
+
+ async def async_turn_off(self, **kwargs) -> None:
+ """Turn device off."""
+ await self._client.set_is_on(False)
+
+ async def async_update(self) -> None:
+ """Asynchronously updates the device properties."""
+ _LOGGER.info("Updating '%s'", self._client.host)
+
+ try:
+ self._is_on = await self._client.get_is_on()
+
+ self._brightness = (
+ int(round((await self._client.get_brightness()) * 2.55))
+ if self._is_on
+ else 0
+ )
+
+ device_info = await self._client.get_device_info()
+
+ if (
+ DEV_NAME in device_info
+ and DEV_MODEL in device_info
+ and (
+ device_info[DEV_NAME] != self.__name
+ or device_info[DEV_MODEL] != self.__model
+ )
+ ):
+ self.__name = device_info[DEV_NAME]
+ self.__model = device_info[DEV_MODEL]
+
+ if self._conf is not None:
+ # If the name has changed, persist it in conf entry,
+ # so we will be able to restore this new name if hass is started while the LED string is offline.
+ self._hass.config_entries.async_update_entry(
+ self._conf,
+ data={
+ CONF_ENTRY_HOST: self._client.host, # this cannot change
+ CONF_ENTRY_ID: self._id, # this cannot change
+ CONF_ENTRY_NAME: self.__name,
+ CONF_ENTRY_MODEL: self.__model,
+ },
+ )
+
+ for key, value in device_info.items():
+ if key not in HIDDEN_DEV_VALUES:
+ self._attributes[key] = value
+
+ if not self._is_available:
+ _LOGGER.info("Twinkly '%s' is now available", self._client.host)
+
+ # We don't use the echo API to track the availability since we already have to pull
+ # the device to get its state.
+ self._is_available = True
+ except (asyncio.TimeoutError, ClientError):
+ # We log this as "info" as it's pretty common that the christmas light are not reachable in july
+ if self._is_available:
+ _LOGGER.info(
+ "Twinkly '%s' is not reachable (client error)", self._client.host
+ )
+ self._is_available = False
diff --git a/homeassistant/components/twinkly/manifest.json b/homeassistant/components/twinkly/manifest.json
new file mode 100644
index 00000000000..c87394ba3bb
--- /dev/null
+++ b/homeassistant/components/twinkly/manifest.json
@@ -0,0 +1,9 @@
+{
+ "domain": "twinkly",
+ "name": "Twinkly",
+ "documentation": "https://www.home-assistant.io/integrations/twinkly",
+ "requirements": ["twinkly-client==0.0.2"],
+ "dependencies": [],
+ "codeowners": ["@dr1rrb"],
+ "config_flow": true
+}
diff --git a/homeassistant/components/twinkly/strings.json b/homeassistant/components/twinkly/strings.json
new file mode 100644
index 00000000000..70e7f970b58
--- /dev/null
+++ b/homeassistant/components/twinkly/strings.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "title": "Twinkly",
+ "description": "Set up your Twinkly led string",
+ "data": {
+ "host": "Host (or IP address) of your twinkly device"
+ }
+ }
+ },
+ "error": {
+ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
+ },
+ "abort": {
+ "device_exists": "[%key:common::config_flow::abort::already_configured_device%]"
+ }
+ }
+}
diff --git a/homeassistant/components/twinkly/translations/ca.json b/homeassistant/components/twinkly/translations/ca.json
new file mode 100644
index 00000000000..2801361990e
--- /dev/null
+++ b/homeassistant/components/twinkly/translations/ca.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "device_exists": "El dispositiu ja est\u00e0 configurat"
+ },
+ "error": {
+ "cannot_connect": "Ha fallat la connexi\u00f3"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Amfitri\u00f3 (o adre\u00e7a IP) del dispositiu Twinkly"
+ },
+ "description": "Configura la teva tira LED de Twinkly",
+ "title": "Twinkly"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twinkly/translations/cs.json b/homeassistant/components/twinkly/translations/cs.json
new file mode 100644
index 00000000000..edb776e1483
--- /dev/null
+++ b/homeassistant/components/twinkly/translations/cs.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "device_exists": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno"
+ },
+ "error": {
+ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Hostitel (nebo IP adresa) va\u0161eho za\u0159\u00edzen\u00ed Twinkly"
+ },
+ "description": "Nastavte sv\u016fj LED p\u00e1sek Twinkly",
+ "title": "Twinkly"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twinkly/translations/en.json b/homeassistant/components/twinkly/translations/en.json
new file mode 100644
index 00000000000..2126bac3c27
--- /dev/null
+++ b/homeassistant/components/twinkly/translations/en.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "device_exists": "Device is already configured"
+ },
+ "error": {
+ "cannot_connect": "Failed to connect"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host (or IP address) of your twinkly device"
+ },
+ "description": "Set up your Twinkly led string",
+ "title": "Twinkly"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twinkly/translations/es.json b/homeassistant/components/twinkly/translations/es.json
new file mode 100644
index 00000000000..60024dc2d1e
--- /dev/null
+++ b/homeassistant/components/twinkly/translations/es.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "device_exists": "El dispositivo ya est\u00e1 configurado"
+ },
+ "error": {
+ "cannot_connect": "No se pudo conectar"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host (o direcci\u00f3n IP) de tu dispositivo Twinkly"
+ },
+ "description": "Configura tu tira led Twinkly",
+ "title": "Twinkly"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twinkly/translations/et.json b/homeassistant/components/twinkly/translations/et.json
new file mode 100644
index 00000000000..99e417685ae
--- /dev/null
+++ b/homeassistant/components/twinkly/translations/et.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "device_exists": "Seade on juba h\u00e4\u00e4lestatud"
+ },
+ "error": {
+ "cannot_connect": "\u00dchendamine nurjus"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Twinkly seadme host (v\u00f5i IP-aadress)"
+ },
+ "description": "Seadista oma Twinkly LED riba",
+ "title": ""
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twinkly/translations/it.json b/homeassistant/components/twinkly/translations/it.json
new file mode 100644
index 00000000000..ec62abeea01
--- /dev/null
+++ b/homeassistant/components/twinkly/translations/it.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "device_exists": "Il dispositivo \u00e8 gi\u00e0 configurato"
+ },
+ "error": {
+ "cannot_connect": "Impossibile connettersi"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Host (o indirizzo IP) del tuo dispositivo twinkly"
+ },
+ "description": "Configura la tua stringa led Twinkly",
+ "title": "Twinkly"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twinkly/translations/ka.json b/homeassistant/components/twinkly/translations/ka.json
new file mode 100644
index 00000000000..d0d6b61f4cc
--- /dev/null
+++ b/homeassistant/components/twinkly/translations/ka.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "device_exists": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0 \u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0"
+ },
+ "error": {
+ "cannot_connect": "\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d8 \u10d5\u10d4\u10e0 \u10d3\u10d0\u10db\u10e7\u10d0\u10e0\u10d3\u10d0"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 twinkly \u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d8\u10e1 \u10f0\u10dd\u10e1\u10e2\u10d8 (\u10d0\u10dc IP \u10db\u10d8\u10e1\u10d0\u10db\u10d0\u10e0\u10d7\u10d8)"
+ },
+ "description": "\u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 Twinkly \u10e8\u10e3\u10e5\u10d3\u10d8\u10dd\u10d3\u10d8\u10e1 \u10da\u10d4\u10dc\u10e2\u10d8\u10e1 \u10d3\u10d0\u10e7\u10d4\u10dc\u10d4\u10d1\u10d0",
+ "title": "Twinkly"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twinkly/translations/no.json b/homeassistant/components/twinkly/translations/no.json
new file mode 100644
index 00000000000..2bfe2d3606a
--- /dev/null
+++ b/homeassistant/components/twinkly/translations/no.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "device_exists": "Enheten er allerede konfigurert"
+ },
+ "error": {
+ "cannot_connect": "Tilkobling mislyktes"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Vert (eller IP-adresse) for din twinkly-enhet"
+ },
+ "description": "Sett opp Twinkly-led-strengen",
+ "title": "Twinkly"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twinkly/translations/pl.json b/homeassistant/components/twinkly/translations/pl.json
new file mode 100644
index 00000000000..2c1434dc035
--- /dev/null
+++ b/homeassistant/components/twinkly/translations/pl.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "device_exists": "Urz\u0105dzenie jest ju\u017c skonfigurowane"
+ },
+ "error": {
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Nazwa hosta lub adres IP"
+ },
+ "description": "Konfiguracja Twinkly",
+ "title": "Twinkly"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twinkly/translations/ru.json b/homeassistant/components/twinkly/translations/ru.json
new file mode 100644
index 00000000000..c9ea2c6927b
--- /dev/null
+++ b/homeassistant/components/twinkly/translations/ru.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "device_exists": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant."
+ },
+ "error": {
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f."
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "\u0418\u043c\u044f \u0445\u043e\u0441\u0442\u0430 (\u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441) \u0412\u0430\u0448\u0435\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Twinkly"
+ },
+ "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441\u0432\u0435\u0442\u043e\u0434\u0438\u043e\u0434\u043d\u043e\u0439 \u043b\u0435\u043d\u0442\u044b Twinkly",
+ "title": "Twinkly"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twinkly/translations/zh-Hant.json b/homeassistant/components/twinkly/translations/zh-Hant.json
new file mode 100644
index 00000000000..a325d458acb
--- /dev/null
+++ b/homeassistant/components/twinkly/translations/zh-Hant.json
@@ -0,0 +1,19 @@
+{
+ "config": {
+ "abort": {
+ "device_exists": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210"
+ },
+ "error": {
+ "cannot_connect": "\u9023\u7dda\u5931\u6557"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "host": "Twinkly \u8a2d\u5099\u4e3b\u6a5f\u540d\u7a31\uff08\u6216 IP \u4f4d\u5740\uff09"
+ },
+ "description": "\u8a2d\u5b9a Twinkly LED \u71c8\u4e32",
+ "title": "Twinkly"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/twitter/manifest.json b/homeassistant/components/twitter/manifest.json
index 044151094da..c497ebfa6f5 100644
--- a/homeassistant/components/twitter/manifest.json
+++ b/homeassistant/components/twitter/manifest.json
@@ -2,6 +2,6 @@
"domain": "twitter",
"name": "Twitter",
"documentation": "https://www.home-assistant.io/integrations/twitter",
- "requirements": ["TwitterAPI==2.5.13"],
+ "requirements": ["TwitterAPI==2.6.2.1"],
"codeowners": []
}
diff --git a/homeassistant/components/ubee/__init__.py b/homeassistant/components/ubee/__init__.py
deleted file mode 100644
index cc7b131a2bd..00000000000
--- a/homeassistant/components/ubee/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""The ubee component."""
diff --git a/homeassistant/components/ubee/device_tracker.py b/homeassistant/components/ubee/device_tracker.py
deleted file mode 100644
index 266acc49c09..00000000000
--- a/homeassistant/components/ubee/device_tracker.py
+++ /dev/null
@@ -1,79 +0,0 @@
-"""Support for Ubee router."""
-
-import logging
-
-from pyubee import Ubee
-import voluptuous as vol
-
-from homeassistant.components.device_tracker import (
- DOMAIN,
- PLATFORM_SCHEMA,
- DeviceScanner,
-)
-from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
-import homeassistant.helpers.config_validation as cv
-
-_LOGGER = logging.getLogger(__name__)
-
-CONF_MODEL = "model"
-DEFAULT_MODEL = "detect"
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Required(CONF_HOST): cv.string,
- vol.Required(CONF_PASSWORD): cv.string,
- vol.Required(CONF_USERNAME): cv.string,
- vol.Optional(CONF_MODEL, default=DEFAULT_MODEL): vol.Any(
- "EVW32C-0N",
- "EVW320B",
- "EVW321B",
- "EVW3200-Wifi",
- "EVW3226@UPC",
- "DVW32CB",
- "DDW36C",
- ),
- }
-)
-
-
-def get_scanner(hass, config):
- """Validate the configuration and return a Ubee scanner."""
- info = config[DOMAIN]
- host = info[CONF_HOST]
- username = info[CONF_USERNAME]
- password = info[CONF_PASSWORD]
- model = info[CONF_MODEL]
-
- ubee = Ubee(host, username, password, model)
- if not ubee.login():
- _LOGGER.error("Login failed")
- return None
-
- scanner = UbeeDeviceScanner(ubee)
- return scanner
-
-
-class UbeeDeviceScanner(DeviceScanner):
- """This class queries a wireless Ubee router."""
-
- def __init__(self, ubee):
- """Initialize the Ubee scanner."""
- self._ubee = ubee
- self._mac2name = {}
-
- def scan_devices(self):
- """Scan for new devices and return a list with found device IDs."""
- devices = self._get_connected_devices()
- self._mac2name = devices
- return list(devices)
-
- def get_device_name(self, device):
- """Return the name of the given device or None if we don't know."""
- return self._mac2name.get(device)
-
- def _get_connected_devices(self):
- """List connected devices with pyubee."""
- if not self._ubee.session_active():
- self._ubee.login()
-
- return self._ubee.get_connected_devices()
diff --git a/homeassistant/components/ubee/manifest.json b/homeassistant/components/ubee/manifest.json
deleted file mode 100644
index 0603ffe8757..00000000000
--- a/homeassistant/components/ubee/manifest.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "domain": "ubee",
- "name": "Ubee Router",
- "documentation": "https://www.home-assistant.io/integrations/ubee",
- "requirements": ["pyubee==0.10"],
- "codeowners": ["@mzdrale"]
-}
diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py
index 4d5bfa20215..30b82c65c85 100644
--- a/homeassistant/components/unifi/controller.py
+++ b/homeassistant/components/unifi/controller.py
@@ -345,9 +345,9 @@ class UniFiController:
mac = ""
if entity.domain == TRACKER_DOMAIN:
- mac, _ = entity.unique_id.split("-", 1)
+ mac = entity.unique_id.split("-", 1)[0]
elif entity.domain == SWITCH_DOMAIN:
- _, mac = entity.unique_id.split("-", 1)
+ mac = entity.unique_id.split("-", 1)[1]
if mac in self.api.clients or mac not in self.api.clients_all:
continue
@@ -397,7 +397,12 @@ class UniFiController:
await self.api.login()
self.api.start_websocket()
- except (asyncio.TimeoutError, aiounifi.AiounifiException):
+ except (
+ asyncio.TimeoutError,
+ aiounifi.BadGateway,
+ aiounifi.ServiceUnavailable,
+ aiounifi.AiounifiException,
+ ):
self.hass.loop.call_later(RETRY_TIMER, self.reconnect)
@callback
@@ -464,7 +469,12 @@ async def get_controller(
LOGGER.warning("Connected to UniFi at %s but not registered.", host)
raise AuthenticationRequired from err
- except (asyncio.TimeoutError, aiounifi.RequestError) as err:
+ except (
+ asyncio.TimeoutError,
+ aiounifi.BadGateway,
+ aiounifi.ServiceUnavailable,
+ aiounifi.RequestError,
+ ) as err:
LOGGER.error("Error connecting to the UniFi controller at %s", host)
raise CannotConnect from err
diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json
index 48c080f82f7..94b1c90f4f3 100644
--- a/homeassistant/components/unifi/manifest.json
+++ b/homeassistant/components/unifi/manifest.json
@@ -3,7 +3,7 @@
"name": "Ubiquiti UniFi",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/unifi",
- "requirements": ["aiounifi==25"],
+ "requirements": ["aiounifi==26"],
"codeowners": ["@Kane610"],
"quality_scale": "platinum"
}
diff --git a/homeassistant/components/unifi/translations/hu.json b/homeassistant/components/unifi/translations/hu.json
index 91d031334dd..2a7a43d42e9 100644
--- a/homeassistant/components/unifi/translations/hu.json
+++ b/homeassistant/components/unifi/translations/hu.json
@@ -20,6 +20,9 @@
},
"options": {
"step": {
+ "client_control": {
+ "description": "Konfigur\u00e1lja a klienseket\n\n Hozzon l\u00e9tre kapcsol\u00f3kat azokhoz a sorsz\u00e1mokhoz, amelyeknek vez\u00e9relni k\u00edv\u00e1nja a h\u00e1l\u00f3zati hozz\u00e1f\u00e9r\u00e9st."
+ },
"statistics_sensors": {
"data": {
"allow_bandwidth_sensors": "S\u00e1vsz\u00e9less\u00e9g-haszn\u00e1lati \u00e9rz\u00e9kel\u0151k l\u00e9trehoz\u00e1sa a h\u00e1l\u00f3zati \u00fcgyfelek sz\u00e1m\u00e1ra"
diff --git a/homeassistant/components/unifi/translations/it.json b/homeassistant/components/unifi/translations/it.json
index e530f600a04..79a7206923e 100644
--- a/homeassistant/components/unifi/translations/it.json
+++ b/homeassistant/components/unifi/translations/it.json
@@ -45,12 +45,6 @@
"description": "Configurare il tracciamento del dispositivo",
"title": "Opzioni UniFi 1/3"
},
- "init": {
- "data": {
- "one": "uno",
- "other": "altri"
- }
- },
"simple_options": {
"data": {
"block_client": "Client controllati per l'accesso alla rete",
diff --git a/homeassistant/components/unifi/translations/ka.json b/homeassistant/components/unifi/translations/ka.json
new file mode 100644
index 00000000000..31cbabe0d97
--- /dev/null
+++ b/homeassistant/components/unifi/translations/ka.json
@@ -0,0 +1,11 @@
+{
+ "options": {
+ "step": {
+ "client_control": {
+ "data": {
+ "dpi_restrictions": "DPI \u10e8\u10d4\u10d6\u10e6\u10e3\u10d3\u10d5\u10d8\u10e1 \u10ef\u10d2\u10e3\u10e4\u10d4\u10d1\u10d8\u10e1 \u10d9\u10dd\u10dc\u10e2\u10e0\u10dd\u10da\u10d8\u10e1 \u10d3\u10d0\u10e8\u10d5\u10d4\u10d1\u10d0"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/unifi/translations/no.json b/homeassistant/components/unifi/translations/no.json
index 58af2bd5e54..5cda9ad7ab5 100644
--- a/homeassistant/components/unifi/translations/no.json
+++ b/homeassistant/components/unifi/translations/no.json
@@ -14,7 +14,7 @@
"host": "Vert",
"password": "Passord",
"port": "Port",
- "site": "Nettsted-ID",
+ "site": "Nettsted ID",
"username": "Brukernavn",
"verify_ssl": "Verifisere SSL-sertifikat"
},
diff --git a/homeassistant/components/unifi/unifi_entity_base.py b/homeassistant/components/unifi/unifi_entity_base.py
index a730c134603..7b45d309c14 100644
--- a/homeassistant/components/unifi/unifi_entity_base.py
+++ b/homeassistant/components/unifi/unifi_entity_base.py
@@ -95,7 +95,16 @@ class UniFiBase(Entity):
entity_registry.async_remove(self.entity_id)
return
- if len(async_entries_for_device(entity_registry, entity_entry.device_id)) == 1:
+ if (
+ len(
+ async_entries_for_device(
+ entity_registry,
+ entity_entry.device_id,
+ include_disabled_entities=True,
+ )
+ )
+ == 1
+ ):
device_registry.async_remove_device(device_entry.id)
return
diff --git a/homeassistant/components/upb/translations/sl.json b/homeassistant/components/upb/translations/sl.json
new file mode 100644
index 00000000000..8a0996ed92e
--- /dev/null
+++ b/homeassistant/components/upb/translations/sl.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "unknown": "Nepri\u010dakovana napaka"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/upcloud/translations/hu.json b/homeassistant/components/upcloud/translations/hu.json
new file mode 100644
index 00000000000..7a7de0633a7
--- /dev/null
+++ b/homeassistant/components/upcloud/translations/hu.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
+ "invalid_auth": "\u00c9rv\u00e9nytelen autentik\u00e1ci\u00f3"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "Friss\u00edt\u00e9si id\u0151k\u00f6z m\u00e1sodpercben, minimum 30"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/upcloud/translations/ka.json b/homeassistant/components/upcloud/translations/ka.json
new file mode 100644
index 00000000000..84d1882069a
--- /dev/null
+++ b/homeassistant/components/upcloud/translations/ka.json
@@ -0,0 +1,25 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u10d3\u10d0\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d4\u10d1\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0",
+ "invalid_auth": "\u10db\u10ea\u10d3\u10d0\u10e0\u10d8 \u10d0\u10d5\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "password": "\u10de\u10d0\u10e0\u10dd\u10da\u10d8",
+ "username": "\u10db\u10dd\u10db\u10ee\u10db\u10d0\u10e0\u10d4\u10d1\u10d4\u10da\u10d8"
+ }
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "scan_interval": "\u10d2\u10d0\u10dc\u10d0\u10ee\u10da\u10d4\u10d1\u10d8\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10d5\u10d0\u10da\u10d8 \u10ec\u10d0\u10db\u10d4\u10d1\u10e8\u10d8, \u10db\u10d8\u10dc\u10d8\u10db\u10e3\u10db 30"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py
index 773a872f33f..c9f96a0e9d7 100644
--- a/homeassistant/components/upnp/__init__.py
+++ b/homeassistant/components/upnp/__init__.py
@@ -43,6 +43,8 @@ async def async_discover_and_construct(
) -> Device:
"""Discovery devices and construct a Device for one."""
# pylint: disable=invalid-name
+ _LOGGER.debug("Constructing device: %s::%s", udn, st)
+
discovery_infos = await Device.async_discover(hass)
_LOGGER.debug("Discovered devices: %s", discovery_infos)
if not discovery_infos:
@@ -53,7 +55,7 @@ async def async_discover_and_construct(
# Get the discovery info with specified UDN/ST.
filtered = [di for di in discovery_infos if di[DISCOVERY_UDN] == udn]
if st:
- filtered = [di for di in discovery_infos if di[DISCOVERY_ST] == st]
+ filtered = [di for di in filtered if di[DISCOVERY_ST] == st]
if not filtered:
_LOGGER.warning(
'Wanted UPnP/IGD device with UDN/ST "%s"/"%s" not found, aborting',
@@ -74,6 +76,7 @@ async def async_discover_and_construct(
)
_LOGGER.info("Detected multiple UPnP/IGD devices, using: %s", device_name)
+ _LOGGER.debug("Constructing from discovery_info: %s", discovery_info)
location = discovery_info[DISCOVERY_LOCATION]
return await Device.async_create_device(hass, location)
@@ -104,7 +107,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType):
async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) -> bool:
"""Set up UPnP/IGD device from a config entry."""
- _LOGGER.debug("async_setup_entry, config_entry: %s", config_entry.data)
+ _LOGGER.debug("Setting up config entry: %s", config_entry.unique_id)
# Discover and construct.
udn = config_entry.data.get(CONFIG_ENTRY_UDN)
@@ -123,6 +126,11 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry)
# Ensure entry has a unique_id.
if not config_entry.unique_id:
+ _LOGGER.debug(
+ "Setting unique_id: %s, for config_entry: %s",
+ device.unique_id,
+ config_entry,
+ )
hass.config_entries.async_update_entry(
entry=config_entry,
unique_id=device.unique_id,
@@ -152,6 +160,8 @@ async def async_unload_entry(
hass: HomeAssistantType, config_entry: ConfigEntry
) -> bool:
"""Unload a UPnP/IGD device from a config entry."""
+ _LOGGER.debug("Unloading config entry: %s", config_entry.unique_id)
+
udn = config_entry.data.get(CONFIG_ENTRY_UDN)
if udn in hass.data[DOMAIN][DOMAIN_DEVICES]:
del hass.data[DOMAIN][DOMAIN_DEVICES][udn]
diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py
index 72efc4ffd55..7b20c7709a0 100644
--- a/homeassistant/components/upnp/config_flow.py
+++ b/homeassistant/components/upnp/config_flow.py
@@ -154,6 +154,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self._abort_if_unique_id_configured()
# Store discovery.
+ _LOGGER.debug("New discovery, continuing")
name = discovery_info.get("friendlyName", "")
discovery = {
DISCOVERY_UDN: udn,
diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py
index 5f29043a1fe..6bc497170ca 100644
--- a/homeassistant/components/upnp/device.py
+++ b/homeassistant/components/upnp/device.py
@@ -109,7 +109,7 @@ class Device:
def __str__(self) -> str:
"""Get string representation."""
- return f"IGD Device: {self.name}/{self.udn}"
+ return f"IGD Device: {self.name}/{self.udn}::{self.device_type}"
async def async_get_traffic_data(self) -> Mapping[str, any]:
"""
diff --git a/homeassistant/components/upnp/translations/it.json b/homeassistant/components/upnp/translations/it.json
index 4955ffaa52e..ca4e376432c 100644
--- a/homeassistant/components/upnp/translations/it.json
+++ b/homeassistant/components/upnp/translations/it.json
@@ -11,10 +11,6 @@
},
"flow_title": "UPnP/IGD: {name}",
"step": {
- "init": {
- "one": "uno",
- "other": "altri"
- },
"ssdp_confirm": {
"description": "Vuoi configurare questo dispositivo UPnP/IGD?"
},
diff --git a/homeassistant/components/uptime/sensor.py b/homeassistant/components/uptime/sensor.py
index 12c00c7f96d..8363d2da2cb 100644
--- a/homeassistant/components/uptime/sensor.py
+++ b/homeassistant/components/uptime/sensor.py
@@ -1,47 +1,42 @@
"""Platform to retrieve uptime for Home Assistant."""
-import logging
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.sensor import DEVICE_CLASS_TIMESTAMP, PLATFORM_SCHEMA
from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
import homeassistant.util.dt as dt_util
-_LOGGER = logging.getLogger(__name__)
-
DEFAULT_NAME = "Uptime"
-ICON = "mdi:clock"
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(CONF_UNIT_OF_MEASUREMENT, default="days"): vol.All(
- cv.string, vol.In(["minutes", "hours", "days", "seconds"])
- ),
- }
+PLATFORM_SCHEMA = vol.All(
+ cv.deprecated(CONF_UNIT_OF_MEASUREMENT),
+ PLATFORM_SCHEMA.extend(
+ {
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_UNIT_OF_MEASUREMENT, default="days"): vol.All(
+ cv.string, vol.In(["minutes", "hours", "days", "seconds"])
+ ),
+ }
+ ),
)
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the uptime sensor platform."""
name = config.get(CONF_NAME)
- units = config.get(CONF_UNIT_OF_MEASUREMENT)
- async_add_entities([UptimeSensor(name, units)], True)
+ async_add_entities([UptimeSensor(name)], True)
class UptimeSensor(Entity):
"""Representation of an uptime sensor."""
- def __init__(self, name, unit):
+ def __init__(self, name):
"""Initialize the uptime sensor."""
self._name = name
- self._unit = unit
- self.initial = dt_util.now()
- self._state = None
+ self._state = dt_util.now().isoformat()
@property
def name(self):
@@ -49,32 +44,16 @@ class UptimeSensor(Entity):
return self._name
@property
- def icon(self):
- """Icon to display in the front end."""
- return ICON
-
- @property
- def unit_of_measurement(self):
- """Return the unit of measurement the value is expressed in."""
- return self._unit
+ def device_class(self):
+ """Return device class."""
+ return DEVICE_CLASS_TIMESTAMP
@property
def state(self):
"""Return the state of the sensor."""
return self._state
- async def async_update(self):
- """Update the state of the sensor."""
- delta = dt_util.now() - self.initial
- div_factor = 3600
-
- if self.unit_of_measurement == "days":
- div_factor *= 24
- elif self.unit_of_measurement == "minutes":
- div_factor /= 60
- elif self.unit_of_measurement == "seconds":
- div_factor /= 3600
-
- delta = delta.total_seconds() / div_factor
- self._state = round(delta, 2)
- _LOGGER.debug("New value: %s", delta)
+ @property
+ def should_poll(self) -> bool:
+ """Disable polling for this entity."""
+ return False
diff --git a/homeassistant/components/utility_meter/const.py b/homeassistant/components/utility_meter/const.py
index 7b55ec4dcd0..39fd952327b 100644
--- a/homeassistant/components/utility_meter/const.py
+++ b/homeassistant/components/utility_meter/const.py
@@ -1,6 +1,7 @@
"""Constants for the utility meter component."""
DOMAIN = "utility_meter"
+QUARTER_HOURLY = "quarter-hourly"
HOURLY = "hourly"
DAILY = "daily"
WEEKLY = "weekly"
@@ -9,7 +10,16 @@ BIMONTHLY = "bimonthly"
QUARTERLY = "quarterly"
YEARLY = "yearly"
-METER_TYPES = [HOURLY, DAILY, WEEKLY, MONTHLY, BIMONTHLY, QUARTERLY, YEARLY]
+METER_TYPES = [
+ QUARTER_HOURLY,
+ HOURLY,
+ DAILY,
+ WEEKLY,
+ MONTHLY,
+ BIMONTHLY,
+ QUARTERLY,
+ YEARLY,
+]
DATA_UTILITY = "utility_meter_data"
diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py
index e365e77071c..9a4ed9e7782 100644
--- a/homeassistant/components/utility_meter/sensor.py
+++ b/homeassistant/components/utility_meter/sensor.py
@@ -36,6 +36,7 @@ from .const import (
DATA_UTILITY,
HOURLY,
MONTHLY,
+ QUARTER_HOURLY,
QUARTERLY,
SERVICE_CALIBRATE_METER,
SIGNAL_RESET_METER,
@@ -241,7 +242,16 @@ class UtilityMeterSensor(RestoreEntity):
"""Handle entity which will be added."""
await super().async_added_to_hass()
- if self._period == HOURLY:
+ if self._period == QUARTER_HOURLY:
+ for quarter in range(4):
+ async_track_time_change(
+ self.hass,
+ self._async_reset_meter,
+ minute=(quarter * 15)
+ + self._period_offset.seconds % (15 * 60) // 60,
+ second=self._period_offset.seconds % 60,
+ )
+ elif self._period == HOURLY:
async_track_time_change(
self.hass,
self._async_reset_meter,
diff --git a/homeassistant/components/vacuum/translations/no.json b/homeassistant/components/vacuum/translations/no.json
index f39e62058d7..3d722c0927c 100644
--- a/homeassistant/components/vacuum/translations/no.json
+++ b/homeassistant/components/vacuum/translations/no.json
@@ -9,7 +9,7 @@
"is_docked": "{entity_name} er dokket"
},
"trigger_type": {
- "cleaning": "{entity_name} startet rengj\u00f8ringen",
+ "cleaning": "{entity_name} startet rengj\u00f8ring",
"docked": "{entity_name} dokket"
}
},
diff --git a/homeassistant/components/vasttrafik/sensor.py b/homeassistant/components/vasttrafik/sensor.py
index d76fc3b5e3d..8b1609be6ba 100644
--- a/homeassistant/components/vasttrafik/sensor.py
+++ b/homeassistant/components/vasttrafik/sensor.py
@@ -80,14 +80,23 @@ class VasttrafikDepartureSensor(Entity):
"""Initialize the sensor."""
self._planner = planner
self._name = name or departure
- self._departure = planner.location_name(departure)[0]
- self._heading = planner.location_name(heading)[0] if heading else None
+ self._departure = self.get_station_id(departure)
+ self._heading = self.get_station_id(heading) if heading else None
self._lines = lines if lines else None
self._delay = timedelta(minutes=delay)
self._departureboard = None
self._state = None
self._attributes = None
+ def get_station_id(self, location):
+ """Get the station ID."""
+ if location.isdecimal():
+ station_info = {"station_name": location, "station_id": location}
+ else:
+ station_id = self._planner.location_name(location)[0]["id"]
+ station_info = {"station_name": location, "station_id": station_id}
+ return station_info
+
@property
def name(self):
"""Return the name of the sensor."""
@@ -113,8 +122,8 @@ class VasttrafikDepartureSensor(Entity):
"""Get the departure board."""
try:
self._departureboard = self._planner.departureboard(
- self._departure["id"],
- direction=self._heading["id"] if self._heading else None,
+ self._departure["station_id"],
+ direction=self._heading["station_id"] if self._heading else None,
date=now() + self._delay,
)
except vasttrafik.Error:
@@ -123,15 +132,17 @@ class VasttrafikDepartureSensor(Entity):
if not self._departureboard:
_LOGGER.debug(
- "No departures from %s heading %s",
- self._departure["name"],
- self._heading["name"] if self._heading else "ANY",
+ "No departures from departure station %s " "to destination station %s",
+ self._departure["station_name"],
+ self._heading["station_name"] if self._heading else "ANY",
)
self._state = None
self._attributes = {}
else:
for departure in self._departureboard:
line = departure.get("sname")
+ if "cancelled" in departure:
+ continue
if not self._lines or line in self._lines:
if "rtTime" in departure:
self._state = departure["rtTime"]
diff --git a/homeassistant/components/vera/translations/no.json b/homeassistant/components/vera/translations/no.json
index ae1a601d550..7ec6850a7c8 100644
--- a/homeassistant/components/vera/translations/no.json
+++ b/homeassistant/components/vera/translations/no.json
@@ -6,8 +6,8 @@
"step": {
"user": {
"data": {
- "exclude": "Vera-enhets-ID-er som skal ekskluderes fra Home Assistant.",
- "lights": "Vera bytter enhets-ID-er for \u00e5 behandle som lys i Home Assistant.",
+ "exclude": "Vera enhets ID-er som skal ekskluderes fra Home Assistant",
+ "lights": "Vera bytter enhets ID-er for \u00e5 behandle som lys i Home Assistant",
"vera_controller_url": "URL-adresse for kontroller"
},
"description": "Oppgi en Vera-kontroller-url nedenfor. Det skal se slik ut: http://192.168.1.161:3480.",
@@ -19,8 +19,8 @@
"step": {
"init": {
"data": {
- "exclude": "Vera-enhets-ID-er som skal ekskluderes fra Home Assistant.",
- "lights": "Vera bytter enhets-ID-er for \u00e5 behandle som lys i Home Assistant."
+ "exclude": "Vera enhets ID-er som skal ekskluderes fra Home Assistant",
+ "lights": "Vera bytter enhets ID-er for \u00e5 behandle som lys i Home Assistant"
},
"description": "Se Vera dokumentasjonen for detaljer om valgfrie parametere: https://www.home-assistant.io/integrations/vera/. Merk: Eventuelle endringer her vil trenge en omstart av Home Assistant-serveren. For \u00e5 fjerne verdier, gi et rom.",
"title": "Alternativer for Vera-kontroller"
diff --git a/homeassistant/components/vera/translations/pl.json b/homeassistant/components/vera/translations/pl.json
index e7a6696c867..5b7b6886085 100644
--- a/homeassistant/components/vera/translations/pl.json
+++ b/homeassistant/components/vera/translations/pl.json
@@ -6,7 +6,7 @@
"step": {
"user": {
"data": {
- "exclude": "Identyfikatory urz\u0105dze\u0144 Vera do wykluczenia z Home Assistant.",
+ "exclude": "Identyfikatory urz\u0105dze\u0144 Vera do wykluczenia z Home Assistanta.",
"lights": "Identyfikatory prze\u0142\u0105cznik\u00f3w Vera, kt\u00f3re maj\u0105 by\u0107 traktowane jako \u015bwiat\u0142a w Home Assistant.",
"vera_controller_url": "Adres URL kontrolera"
},
@@ -19,7 +19,7 @@
"step": {
"init": {
"data": {
- "exclude": "Identyfikatory urz\u0105dze\u0144 Vera do wykluczenia z Home Assistant.",
+ "exclude": "Identyfikatory urz\u0105dze\u0144 Vera do wykluczenia z Home Assistanta.",
"lights": "Identyfikatory prze\u0142\u0105cznik\u00f3w Vera, kt\u00f3re maj\u0105 by\u0107 traktowane jako \u015bwiat\u0142a w Home Assistant."
},
"description": "Szczeg\u00f3\u0142owe informacje na temat parametr\u00f3w opcjonalnych mo\u017cna znale\u017a\u0107 w dokumentacji Vera: https://www.home-assistant.io/integrations/vera/. Uwaga: Wszelkie zmiany tutaj b\u0119d\u0105 wymaga\u0142y ponownego uruchomienia serwera Home Assistant. Aby wyczy\u015bci\u0107 warto\u015bci, wpisz spacj\u0119.",
diff --git a/homeassistant/components/vizio/translations/ca.json b/homeassistant/components/vizio/translations/ca.json
index 02bfb857d76..d18ffb044c1 100644
--- a/homeassistant/components/vizio/translations/ca.json
+++ b/homeassistant/components/vizio/translations/ca.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured_device": "El dispositiu ja est\u00e0 configurat",
+ "cannot_connect": "Ha fallat la connexi\u00f3",
"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": {
diff --git a/homeassistant/components/vizio/translations/cs.json b/homeassistant/components/vizio/translations/cs.json
index 23fec08499b..cf0c66749dd 100644
--- a/homeassistant/components/vizio/translations/cs.json
+++ b/homeassistant/components/vizio/translations/cs.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured_device": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno",
+ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit",
"updated_entry": "Tato polo\u017eka ji\u017e byla nastavena, ale jm\u00e9no, aplikace nebo mo\u017enosti definovan\u00e9 v konfiguraci neodpov\u00eddaj\u00ed d\u0159\u00edve importovan\u00e9 konfiguraci, tak\u017ee polo\u017eka konfigurace byla odpov\u00eddaj\u00edc\u00edm zp\u016fsobem aktualizov\u00e1na."
},
"error": {
diff --git a/homeassistant/components/vizio/translations/en.json b/homeassistant/components/vizio/translations/en.json
index 41dc47150f9..55176b962f2 100644
--- a/homeassistant/components/vizio/translations/en.json
+++ b/homeassistant/components/vizio/translations/en.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured_device": "Device is already configured",
+ "cannot_connect": "Failed to connect",
"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": {
diff --git a/homeassistant/components/vizio/translations/es.json b/homeassistant/components/vizio/translations/es.json
index 244b9958360..d3ba768e933 100644
--- a/homeassistant/components/vizio/translations/es.json
+++ b/homeassistant/components/vizio/translations/es.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured_device": "El dispositivo ya est\u00e1 configurado",
+ "cannot_connect": "No se pudo conectar",
"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": {
diff --git a/homeassistant/components/vizio/translations/et.json b/homeassistant/components/vizio/translations/et.json
index c4db3bdce26..ab88700d173 100644
--- a/homeassistant/components/vizio/translations/et.json
+++ b/homeassistant/components/vizio/translations/et.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured_device": "Seade on juba h\u00e4\u00e4lestatud",
+ "cannot_connect": "\u00dchendamine nurjus",
"updated_entry": "See kirje on juba seadistatud, kuid konfiguratsioonis m\u00e4\u00e4ratletud nimi, rakendused ja / v\u00f5i suvandid ei \u00fchti varem imporditud konfiguratsiooniga, seega on konfiguratsioonikirjet vastavalt v\u00e4rskendatud."
},
"error": {
diff --git a/homeassistant/components/vizio/translations/it.json b/homeassistant/components/vizio/translations/it.json
index 66d4b2fb914..e7efb348332 100644
--- a/homeassistant/components/vizio/translations/it.json
+++ b/homeassistant/components/vizio/translations/it.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured_device": "Il dispositivo \u00e8 gi\u00e0 configurato",
+ "cannot_connect": "Impossibile connettersi",
"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": {
diff --git a/homeassistant/components/vizio/translations/ka.json b/homeassistant/components/vizio/translations/ka.json
new file mode 100644
index 00000000000..e394fa786b0
--- /dev/null
+++ b/homeassistant/components/vizio/translations/ka.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "cannot_connect": "\u10d9\u10d0\u10d5\u10e8\u10d8\u10e0\u10d8 \u10d5\u10d4\u10e0 \u10d3\u10d0\u10db\u10e7\u10d0\u10e0\u10d3\u10d0"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/vizio/translations/lb.json b/homeassistant/components/vizio/translations/lb.json
index 4eeb9ecbef6..029d75ea2cb 100644
--- a/homeassistant/components/vizio/translations/lb.json
+++ b/homeassistant/components/vizio/translations/lb.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured_device": "Apparat ass scho konfigur\u00e9iert",
+ "cannot_connect": "Feeler beim verbannen",
"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": {
diff --git a/homeassistant/components/vizio/translations/no.json b/homeassistant/components/vizio/translations/no.json
index 3836bc784d5..c5e0b6386b8 100644
--- a/homeassistant/components/vizio/translations/no.json
+++ b/homeassistant/components/vizio/translations/no.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured_device": "Enheten er allerede konfigurert",
+ "cannot_connect": "Tilkobling mislyktes",
"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": {
diff --git a/homeassistant/components/vizio/translations/pl.json b/homeassistant/components/vizio/translations/pl.json
index 420a10c59bd..82339204a16 100644
--- a/homeassistant/components/vizio/translations/pl.json
+++ b/homeassistant/components/vizio/translations/pl.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured_device": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia",
"updated_entry": "Ten wpis zosta\u0142 ju\u017c skonfigurowany, ale nazwa, aplikacje i/lub opcje zdefiniowane w konfiguracji nie pasuj\u0105 do wcze\u015bniej zaimportowanych warto\u015bci, wi\u0119c wpis konfiguracji zosta\u0142 odpowiednio zaktualizowany."
},
"error": {
@@ -18,7 +19,7 @@
"title": "Ko\u0144czenie procesu parowania"
},
"pairing_complete": {
- "description": "Twoje urz\u0105dzenie VIZIO SmartCast jest teraz po\u0142\u0105czone z Home Assistant.",
+ "description": "Twoje urz\u0105dzenie VIZIO SmartCast jest teraz po\u0142\u0105czone z Home Assistantem.",
"title": "Parowanie zako\u0144czone"
},
"pairing_complete_import": {
diff --git a/homeassistant/components/vizio/translations/ru.json b/homeassistant/components/vizio/translations/ru.json
index 4ae162c0a6e..083c9d93dd3 100644
--- a/homeassistant/components/vizio/translations/ru.json
+++ b/homeassistant/components/vizio/translations/ru.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.",
+ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.",
"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": {
diff --git a/homeassistant/components/vizio/translations/zh-Hant.json b/homeassistant/components/vizio/translations/zh-Hant.json
index af72d1e964c..74d6a858d84 100644
--- a/homeassistant/components/vizio/translations/zh-Hant.json
+++ b/homeassistant/components/vizio/translations/zh-Hant.json
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured_device": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "cannot_connect": "\u9023\u7dda\u5931\u6557",
"updated_entry": "\u6b64\u5be6\u9ad4\u5df2\u7d93\u8a2d\u5b9a\uff0c\u4f46\u8a2d\u5b9a\u4e4b\u540d\u7a31\u3001App \u53ca/\u6216\u9078\u9805\u8207\u5148\u524d\u532f\u5165\u7684\u5be6\u9ad4\u9078\u9805\u503c\u4e0d\u5408\uff0c\u56e0\u6b64\u8a2d\u5b9a\u5c07\u6703\u8ddf\u8457\u66f4\u65b0\u3002"
},
"error": {
diff --git a/homeassistant/components/volumio/translations/pl.json b/homeassistant/components/volumio/translations/pl.json
index 2a99bec962d..67d49c4b4be 100644
--- a/homeassistant/components/volumio/translations/pl.json
+++ b/homeassistant/components/volumio/translations/pl.json
@@ -10,7 +10,7 @@
},
"step": {
"discovery_confirm": {
- "description": "Czy chcesz doda\u0107 Volumio (\"{name}\") do Home Assistant?",
+ "description": "Czy chcesz doda\u0107 Volumio (\"{name}\") do Home Assistanta?",
"title": "Wykryte Volumio"
},
"user": {
diff --git a/homeassistant/components/water_heater/translations/hu.json b/homeassistant/components/water_heater/translations/hu.json
new file mode 100644
index 00000000000..c3c47030acb
--- /dev/null
+++ b/homeassistant/components/water_heater/translations/hu.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "action_type": {
+ "turn_off": "{entity_name} kikapcsol\u00e1sa",
+ "turn_on": "{entity_name} bekapcsol\u00e1sa"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/water_heater/translations/ka.json b/homeassistant/components/water_heater/translations/ka.json
new file mode 100644
index 00000000000..17ccabdb18e
--- /dev/null
+++ b/homeassistant/components/water_heater/translations/ka.json
@@ -0,0 +1,8 @@
+{
+ "device_automation": {
+ "action_type": {
+ "turn_off": "\u10d2\u10d0\u10db\u10dd\u10e0\u10d7\u10d4\u10d3 {entity_name}",
+ "turn_on": "\u10e9\u10d0\u10e0\u10d7\u10d4\u10d3 {entity_name}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/watson_tts/tts.py b/homeassistant/components/watson_tts/tts.py
index 27bfff9550b..9b2af2ea7fe 100644
--- a/homeassistant/components/watson_tts/tts.py
+++ b/homeassistant/components/watson_tts/tts.py
@@ -26,6 +26,8 @@ SUPPORTED_VOICES = [
"de-DE_ErikaV3Voice",
"en-GB_KateV3Voice",
"en-GB_KateVoice",
+ "en-GB_CharlotteV3Voice",
+ "en-GB_JamesV3Voice",
"en-US_AllisonV3Voice",
"en-US_AllisonVoice",
"en-US_EmilyV3Voice",
diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json
index a7d2ca585a5..e986913fc70 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.5.2"],
+ "requirements": ["pywemo==0.5.3"],
"ssdp": [
{
"manufacturer": "Belkin International Inc."
diff --git a/homeassistant/components/wiffi/translations/hu.json b/homeassistant/components/wiffi/translations/hu.json
new file mode 100644
index 00000000000..21320afea78
--- /dev/null
+++ b/homeassistant/components/wiffi/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "abort": {
+ "start_server_failed": "A szerver ind\u00edt\u00e1sa nem siker\u00fclt."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py
index 94795a10c83..c6f420d172a 100644
--- a/homeassistant/components/withings/__init__.py
+++ b/homeassistant/components/withings/__init__.py
@@ -40,7 +40,7 @@ DOMAIN = const.DOMAIN
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.All(
- cv.deprecated(const.CONF_PROFILES, invalidation_version="0.114"),
+ cv.deprecated(const.CONF_PROFILES),
vol.Schema(
{
vol.Required(CONF_CLIENT_ID): vol.All(cv.string, vol.Length(min=1)),
diff --git a/homeassistant/components/withings/translations/hu.json b/homeassistant/components/withings/translations/hu.json
index ed0cc9cdc1b..1486048adfc 100644
--- a/homeassistant/components/withings/translations/hu.json
+++ b/homeassistant/components/withings/translations/hu.json
@@ -1,6 +1,7 @@
{
"config": {
"abort": {
+ "already_configured": "A profil konfigur\u00e1ci\u00f3ja friss\u00edtve.",
"authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n.",
"missing_configuration": "A Withings integr\u00e1ci\u00f3 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t."
},
diff --git a/homeassistant/components/withings/translations/ka.json b/homeassistant/components/withings/translations/ka.json
new file mode 100644
index 00000000000..9bcce782255
--- /dev/null
+++ b/homeassistant/components/withings/translations/ka.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "already_configured": "\u10d0\u10dc\u10d2\u10d0\u10e0\u10d8\u10e8\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wled/translations/et.json b/homeassistant/components/wled/translations/et.json
index e32427a1b7d..9fb057a43d8 100644
--- a/homeassistant/components/wled/translations/et.json
+++ b/homeassistant/components/wled/translations/et.json
@@ -16,7 +16,7 @@
"description": "Seadista WLED-i sidumine Home Assistantiga."
},
"zeroconf_confirm": {
- "description": "Kas soovid lisada WLED {nimi} Home Assistanti?",
+ "description": "Kas soovid lisada WLED {name} Home Assistanti?",
"title": "Leitud WLED seade"
}
}
diff --git a/homeassistant/components/wled/translations/hu.json b/homeassistant/components/wled/translations/hu.json
index 40f6f151a2d..b89bd72f704 100644
--- a/homeassistant/components/wled/translations/hu.json
+++ b/homeassistant/components/wled/translations/hu.json
@@ -1,7 +1,11 @@
{
"config": {
"abort": {
- "already_configured": "Ez a WLED eszk\u00f6z m\u00e1r konfigur\u00e1lva van."
+ "already_configured": "Ez a WLED eszk\u00f6z m\u00e1r konfigur\u00e1lva van.",
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
},
"flow_title": "WLED: {name}",
"step": {
diff --git a/homeassistant/components/wled/translations/pl.json b/homeassistant/components/wled/translations/pl.json
index f73e7d05651..6552b6de239 100644
--- a/homeassistant/components/wled/translations/pl.json
+++ b/homeassistant/components/wled/translations/pl.json
@@ -13,10 +13,10 @@
"data": {
"host": "Nazwa hosta lub adres IP"
},
- "description": "Konfiguracja WLED w celu integracji z Home Assistant."
+ "description": "Konfiguracja WLED w celu integracji z Home Assistantem."
},
"zeroconf_confirm": {
- "description": "Czy chcesz doda\u0107 WLED o nazwie `{name}` do Home Assistant?",
+ "description": "Czy chcesz doda\u0107 WLED o nazwie `{name}` do Home Assistanta?",
"title": "Wykryto urz\u0105dzenie WLED"
}
}
diff --git a/homeassistant/components/wolflink/__init__.py b/homeassistant/components/wolflink/__init__.py
index d04cd7a56d4..1bfae6cb900 100644
--- a/homeassistant/components/wolflink/__init__.py
+++ b/homeassistant/components/wolflink/__init__.py
@@ -2,13 +2,14 @@
from datetime import timedelta
import logging
-from httpcore import ConnectError, ConnectTimeout
+from httpx import ConnectError, ConnectTimeout
from wolf_smartset.token_auth import InvalidAuth
from wolf_smartset.wolf_client import FetchFailed, WolfClient
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import (
@@ -45,7 +46,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
wolf_client = WolfClient(username, password)
- parameters = await fetch_parameters(wolf_client, gateway_id, device_id)
+ try:
+ parameters = await fetch_parameters(wolf_client, gateway_id, device_id)
+ except InvalidAuth:
+ _LOGGER.debug("Authentication failed")
+ return False
async def async_update_data():
"""Update all stored entities for Wolf SmartSet."""
@@ -103,7 +108,7 @@ async def fetch_parameters(client: WolfClient, gateway_id: int, device_id: int):
try:
fetched_parameters = await client.fetch_parameters(gateway_id, device_id)
return [param for param in fetched_parameters if param.name != "Reglertyp"]
- except (ConnectError, ConnectTimeout) as exception:
- raise UpdateFailed(f"Error communicating with API: {exception}") from exception
- except InvalidAuth as exception:
- raise UpdateFailed("Invalid authentication during update") from exception
+ except (ConnectError, ConnectTimeout, FetchFailed) as exception:
+ raise ConfigEntryNotReady(
+ f"Error communicating with API: {exception}"
+ ) from exception
diff --git a/homeassistant/components/xbox/media_source.py b/homeassistant/components/xbox/media_source.py
index 5615e11ea7f..750300e49ee 100644
--- a/homeassistant/components/xbox/media_source.py
+++ b/homeassistant/components/xbox/media_source.py
@@ -84,7 +84,7 @@ class XboxSource(MediaSource):
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
"""Resolve media to a url."""
_, category, url = async_parse_identifier(item)
- _, kind = category.split("#", 1)
+ kind = category.split("#", 1)[1]
return PlayMedia(url, MIME_TYPE_MAP[kind])
async def async_browse_media(
@@ -267,7 +267,7 @@ def _build_categories(title):
def _build_media_item(title: str, category: str, item: XboxMediaItem):
"""Build individual media item."""
- _, kind = category.split("#", 1)
+ kind = category.split("#", 1)[1]
return BrowseMediaSource(
domain=DOMAIN,
identifier=f"{title}~~{category}~~{item.uri}",
diff --git a/homeassistant/components/xbox/translations/hu.json b/homeassistant/components/xbox/translations/hu.json
new file mode 100644
index 00000000000..19f706be1c8
--- /dev/null
+++ b/homeassistant/components/xbox/translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "create_entry": {
+ "default": "Sikeres autentik\u00e1ci\u00f3"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/xbox/translations/ka.json b/homeassistant/components/xbox/translations/ka.json
new file mode 100644
index 00000000000..dbe3de7ed4a
--- /dev/null
+++ b/homeassistant/components/xbox/translations/ka.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "authorize_url_timeout": "\u10e3\u10d5\u10e2\u10dd\u10e0\u10d8\u10d6\u10d4\u10d1\u10e3\u10da\u10d8 URL-\u10d8\u10e1 \u10d2\u10d4\u10dc\u10d4\u10e0\u10d0\u10ea\u10d8\u10d8\u10e1 \u10d3\u10e0\u10dd\u10d8\u10e1 \u10d2\u10d0\u10e1\u10d5\u10da\u10d0",
+ "missing_configuration": "\u10d9\u10dd\u10db\u10de\u10dd\u10dc\u10d4\u10dc\u10e2\u10d8 \u10d0\u10e0 \u10d0\u10e0\u10d8\u10e1 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8. \u10d2\u10d7\u10ee\u10dd\u10d5\u10d7 \u10db\u10d8\u10e7\u10d4\u10d5\u10d8\u10d7 \u10d3\u10dd\u10d9\u10e3\u10db\u10d4\u10dc\u10e2\u10d0\u10ea\u10d8\u10d0\u10e1",
+ "single_instance_allowed": "\u10e3\u10d9\u10d5\u10d4 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0. \u10e8\u10d4\u10e1\u10d0\u10eb\u10da\u10d4\u10d1\u10d4\u10da\u10d8\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d0."
+ },
+ "create_entry": {
+ "default": "\u10d0\u10d5\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d0 \u10ec\u10d0\u10e0\u10db\u10d0\u10e2\u10d4\u10d1\u10e3\u10da\u10d8\u10d0"
+ },
+ "step": {
+ "pick_implementation": {
+ "title": "\u10d0\u10db\u10dd\u10d8\u10e0\u10e9\u10d8\u10d4\u10d7 \u10d0\u10d5\u10d7\u10d4\u10dc\u10d7\u10d8\u10d9\u10d0\u10ea\u10d8\u10d8\u10e1 \u10db\u10d4\u10d7\u10dd\u10d3\u10d8"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/xiaomi/camera.py b/homeassistant/components/xiaomi/camera.py
index 97a0aca20f7..359d6c8b896 100644
--- a/homeassistant/components/xiaomi/camera.py
+++ b/homeassistant/components/xiaomi/camera.py
@@ -149,7 +149,7 @@ class XiaomiCamera(Camera):
url = await self.hass.async_add_executor_job(self.get_latest_video_url, host)
if url != self._last_url:
- ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop)
+ ffmpeg = ImageFrame(self._manager.binary)
self._last_image = await asyncio.shield(
ffmpeg.get_image(
url, output_format=IMAGE_JPEG, extra_cmd=self._extra_arguments
@@ -162,7 +162,7 @@ class XiaomiCamera(Camera):
async def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from the camera."""
- stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop)
+ stream = CameraMjpeg(self._manager.binary)
await stream.open_camera(self._last_url, extra_cmd=self._extra_arguments)
try:
diff --git a/homeassistant/components/xiaomi/device_tracker.py b/homeassistant/components/xiaomi/device_tracker.py
index 530140b524f..4fe485b193b 100644
--- a/homeassistant/components/xiaomi/device_tracker.py
+++ b/homeassistant/components/xiaomi/device_tracker.py
@@ -85,12 +85,12 @@ class XiaomiDeviceScanner(DeviceScanner):
Return the list if successful.
"""
- _LOGGER.info("Refreshing device list")
+ _LOGGER.debug("Refreshing device list")
result = _retrieve_list(self.host, self.token)
if result:
return result
- _LOGGER.info("Refreshing token and retrying device list refresh")
+ _LOGGER.debug("Refreshing token and retrying device list refresh")
self.token = _get_token(self.host, self.username, self.password)
return _retrieve_list(self.host, self.token)
diff --git a/homeassistant/components/xiaomi_aqara/translations/hu.json b/homeassistant/components/xiaomi_aqara/translations/hu.json
index 3b2d79a34a7..1a69e20c6b1 100644
--- a/homeassistant/components/xiaomi_aqara/translations/hu.json
+++ b/homeassistant/components/xiaomi_aqara/translations/hu.json
@@ -1,7 +1,42 @@
{
"config": {
"abort": {
- "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "already_in_progress": "A konfigur\u00e1ci\u00f3s folyamat m\u00e1r fut"
+ },
+ "error": {
+ "discovery_error": "Nem siker\u00fclt felfedezni a Xiaomi Aqara K\u00f6zponti egys\u00e9get, pr\u00f3b\u00e1lja meg interf\u00e9szk\u00e9nt haszn\u00e1lni a HomeAssistant futtat\u00f3 eszk\u00f6z IP-j\u00e9t",
+ "invalid_host": " , l\u00e1sd: https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem",
+ "invalid_interface": "\u00c9rv\u00e9nytelen h\u00e1l\u00f3zati interf\u00e9sz",
+ "invalid_key": "\u00c9rv\u00e9nytelen kulcs",
+ "invalid_mac": "\u00c9rv\u00e9nytelen Mac-c\u00edm"
+ },
+ "flow_title": "Xiaomi Aqara K\u00f6zponti egys\u00e9g: {name}",
+ "step": {
+ "select": {
+ "data": {
+ "select_ip": "IP c\u00edm"
+ },
+ "description": "Futtassa \u00fajra a be\u00e1ll\u00edt\u00e1st, ha egy m\u00e1sik K\u00f6zponti egys\u00e9get szeretne csatlakoztatni",
+ "title": "V\u00e1lassza ki a csatlakoztatni k\u00edv\u00e1nt Xiaomi Aqara K\u00f6zponti egys\u00e9get"
+ },
+ "settings": {
+ "data": {
+ "key": "K\u00f6zponti egys\u00e9g kulcsa",
+ "name": "K\u00f6zponti egys\u00e9g neve"
+ },
+ "description": "A kulcs (jelsz\u00f3) az al\u00e1bbi oktat\u00f3anyag seg\u00edts\u00e9g\u00e9vel t\u00f6lthet\u0151 le: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Ha a kulcs nincs megadva, csak az \u00e9rz\u00e9kel\u0151k f\u00e9rhetnek hozz\u00e1",
+ "title": "Xiaomi Aqara k\u00f6zponti egys\u00e9g, opcion\u00e1lis be\u00e1ll\u00edt\u00e1sok"
+ },
+ "user": {
+ "data": {
+ "host": "IP c\u00edm (opcion\u00e1lis)",
+ "interface": "A haszn\u00e1lni k\u00edv\u00e1nt h\u00e1l\u00f3zati interf\u00e9sz",
+ "mac": "Mac-c\u00edm (opcion\u00e1lis)"
+ },
+ "description": "Csatlakozzon a Xiaomi Aqara k\u00f6zponti egys\u00e9ghez, ha az IP- \u00e9s a mac-c\u00edm \u00fcresen marad, akkor az automatikus felder\u00edt\u00e9st haszn\u00e1lja",
+ "title": "Xiaomi Aqara k\u00f6zponti egys\u00e9g"
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py
index 3effe4975ea..8de68cda97f 100644
--- a/homeassistant/components/xiaomi_miio/const.py
+++ b/homeassistant/components/xiaomi_miio/const.py
@@ -21,6 +21,7 @@ SERVICE_SET_EXTRA_FEATURES = "fan_set_extra_features"
SERVICE_SET_TARGET_HUMIDITY = "fan_set_target_humidity"
SERVICE_SET_DRY_ON = "fan_set_dry_on"
SERVICE_SET_DRY_OFF = "fan_set_dry_off"
+SERVICE_SET_MOTOR_SPEED = "fan_set_motor_speed"
# Light Services
SERVICE_SET_SCENE = "light_set_scene"
diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py
index 8d971446a53..e2a1b3b8143 100644
--- a/homeassistant/components/xiaomi_miio/fan.py
+++ b/homeassistant/components/xiaomi_miio/fan.py
@@ -7,6 +7,7 @@ import logging
from miio import ( # pylint: disable=import-error
AirFresh,
AirHumidifier,
+ AirHumidifierMiot,
AirPurifier,
AirPurifierMiot,
Device,
@@ -20,6 +21,11 @@ from miio.airhumidifier import ( # pylint: disable=import-error, import-error
LedBrightness as AirhumidifierLedBrightness,
OperationMode as AirhumidifierOperationMode,
)
+from miio.airhumidifier_miot import ( # pylint: disable=import-error, import-error
+ LedBrightness as AirhumidifierMiotLedBrightness,
+ OperationMode as AirhumidifierMiotOperationMode,
+ PressedButton as AirhumidifierPressedButton,
+)
from miio.airpurifier import ( # pylint: disable=import-error, import-error
LedBrightness as AirpurifierLedBrightness,
OperationMode as AirpurifierOperationMode,
@@ -30,7 +36,14 @@ from miio.airpurifier_miot import ( # pylint: disable=import-error, import-erro
)
import voluptuous as vol
-from homeassistant.components.fan import PLATFORM_SCHEMA, SUPPORT_SET_SPEED, FanEntity
+from homeassistant.components.fan import (
+ PLATFORM_SCHEMA,
+ SPEED_HIGH,
+ SPEED_LOW,
+ SPEED_MEDIUM,
+ SUPPORT_SET_SPEED,
+ FanEntity,
+)
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_MODE,
@@ -60,6 +73,7 @@ from .const import (
SERVICE_SET_LEARN_MODE_OFF,
SERVICE_SET_LEARN_MODE_ON,
SERVICE_SET_LED_BRIGHTNESS,
+ SERVICE_SET_MOTOR_SPEED,
SERVICE_SET_TARGET_HUMIDITY,
SERVICE_SET_VOLUME,
)
@@ -88,6 +102,7 @@ MODEL_AIRPURIFIER_3H = "zhimi.airpurifier.mb3"
MODEL_AIRHUMIDIFIER_V1 = "zhimi.humidifier.v1"
MODEL_AIRHUMIDIFIER_CA1 = "zhimi.humidifier.ca1"
+MODEL_AIRHUMIDIFIER_CA4 = "zhimi.humidifier.ca4"
MODEL_AIRHUMIDIFIER_CB1 = "zhimi.humidifier.cb1"
MODEL_AIRFRESH_VA2 = "zhimi.airfresh.va2"
@@ -116,6 +131,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
MODEL_AIRPURIFIER_3H,
MODEL_AIRHUMIDIFIER_V1,
MODEL_AIRHUMIDIFIER_CA1,
+ MODEL_AIRHUMIDIFIER_CA4,
MODEL_AIRHUMIDIFIER_CB1,
MODEL_AIRFRESH_VA2,
]
@@ -169,10 +185,16 @@ ATTR_HARDWARE_VERSION = "hardware_version"
ATTR_DEPTH = "depth"
ATTR_DRY = "dry"
+# Air Humidifier CA4
+ATTR_ACTUAL_MOTOR_SPEED = "actual_speed"
+ATTR_FAHRENHEIT = "fahrenheit"
+ATTR_FAULT = "fault"
+
# Air Fresh
ATTR_CO2 = "co2"
PURIFIER_MIOT = [MODEL_AIRPURIFIER_3, MODEL_AIRPURIFIER_3H]
+HUMIDIFIER_MIOT = [MODEL_AIRHUMIDIFIER_CA4]
# Map attributes to properties of the state object
AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON = {
@@ -299,13 +321,13 @@ AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_COMMON = {
ATTR_TARGET_HUMIDITY: "target_humidity",
ATTR_LED_BRIGHTNESS: "led_brightness",
ATTR_USE_TIME: "use_time",
- ATTR_HARDWARE_VERSION: "hardware_version",
}
AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER = {
**AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_COMMON,
ATTR_TRANS_LEVEL: "trans_level",
ATTR_BUTTON_PRESSED: "button_pressed",
+ ATTR_HARDWARE_VERSION: "hardware_version",
}
AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA_AND_CB = {
@@ -313,6 +335,16 @@ AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA_AND_CB = {
ATTR_MOTOR_SPEED: "motor_speed",
ATTR_DEPTH: "depth",
ATTR_DRY: "dry",
+ ATTR_HARDWARE_VERSION: "hardware_version",
+}
+
+AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA4 = {
+ **AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_COMMON,
+ ATTR_ACTUAL_MOTOR_SPEED: "actual_speed",
+ ATTR_BUTTON_PRESSED: "button_pressed",
+ ATTR_DRY: "dry",
+ ATTR_FAHRENHEIT: "fahrenheit",
+ ATTR_MOTOR_SPEED: "motor_speed",
}
AVAILABLE_ATTRIBUTES_AIRFRESH = {
@@ -364,6 +396,7 @@ FEATURE_SET_EXTRA_FEATURES = 512
FEATURE_SET_TARGET_HUMIDITY = 1024
FEATURE_SET_DRY = 2048
FEATURE_SET_FAN_LEVEL = 4096
+FEATURE_SET_MOTOR_SPEED = 8192
FEATURE_FLAGS_AIRPURIFIER = (
FEATURE_SET_BUZZER
@@ -421,6 +454,15 @@ FEATURE_FLAGS_AIRHUMIDIFIER = (
FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB = FEATURE_FLAGS_AIRHUMIDIFIER | FEATURE_SET_DRY
+FEATURE_FLAGS_AIRHUMIDIFIER_CA4 = (
+ FEATURE_SET_BUZZER
+ | FEATURE_SET_CHILD_LOCK
+ | FEATURE_SET_LED_BRIGHTNESS
+ | FEATURE_SET_TARGET_HUMIDITY
+ | FEATURE_SET_DRY
+ | FEATURE_SET_MOTOR_SPEED
+)
+
FEATURE_FLAGS_AIRFRESH = (
FEATURE_SET_BUZZER
| FEATURE_SET_CHILD_LOCK
@@ -460,6 +502,14 @@ SERVICE_SCHEMA_TARGET_HUMIDITY = AIRPURIFIER_SERVICE_SCHEMA.extend(
}
)
+SERVICE_SCHEMA_MOTOR_SPEED = AIRPURIFIER_SERVICE_SCHEMA.extend(
+ {
+ vol.Required(ATTR_MOTOR_SPEED): vol.All(
+ vol.Coerce(int), vol.Clamp(min=200, max=2000)
+ )
+ }
+)
+
SERVICE_TO_METHOD = {
SERVICE_SET_BUZZER_ON: {"method": "async_set_buzzer_on"},
SERVICE_SET_BUZZER_OFF: {"method": "async_set_buzzer_off"},
@@ -495,6 +545,10 @@ SERVICE_TO_METHOD = {
},
SERVICE_SET_DRY_ON: {"method": "async_set_dry_on"},
SERVICE_SET_DRY_OFF: {"method": "async_set_dry_off"},
+ SERVICE_SET_MOTOR_SPEED: {
+ "method": "async_set_motor_speed",
+ "schema": SERVICE_SCHEMA_MOTOR_SPEED,
+ },
}
@@ -532,6 +586,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
elif model.startswith("zhimi.airpurifier."):
air_purifier = AirPurifier(host, token)
device = XiaomiAirPurifier(name, air_purifier, model, unique_id)
+ elif model in HUMIDIFIER_MIOT:
+ air_humidifier = AirHumidifierMiot(host, token)
+ device = XiaomiAirHumidifierMiot(name, air_humidifier, model, unique_id)
elif model.startswith("zhimi.humidifier."):
air_humidifier = AirHumidifier(host, token, model=model)
device = XiaomiAirHumidifier(name, air_humidifier, model, unique_id)
@@ -998,6 +1055,10 @@ class XiaomiAirHumidifier(XiaomiGenericDevice):
for mode in AirhumidifierOperationMode
if mode is not AirhumidifierOperationMode.Strong
]
+ elif self._model in [MODEL_AIRHUMIDIFIER_CA4]:
+ self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA4
+ self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA4
+ self._speed_list = [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
else:
self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER
@@ -1107,6 +1168,69 @@ class XiaomiAirHumidifier(XiaomiGenericDevice):
)
+class XiaomiAirHumidifierMiot(XiaomiAirHumidifier):
+ """Representation of a Xiaomi Air Humidifier (MiOT protocol)."""
+
+ MODE_MAPPING = {
+ AirhumidifierMiotOperationMode.Low: SPEED_LOW,
+ AirhumidifierMiotOperationMode.Mid: SPEED_MEDIUM,
+ AirhumidifierMiotOperationMode.High: SPEED_HIGH,
+ }
+
+ REVERSE_MODE_MAPPING = {v: k for k, v in MODE_MAPPING.items()}
+
+ @property
+ def speed(self):
+ """Return the current speed."""
+ if self._state:
+ return self.MODE_MAPPING.get(
+ AirhumidifierMiotOperationMode(self._state_attrs[ATTR_MODE])
+ )
+
+ return None
+
+ @property
+ def button_pressed(self):
+ """Return the last button pressed."""
+ if self._state:
+ return AirhumidifierPressedButton(
+ self._state_attrs[ATTR_BUTTON_PRESSED]
+ ).name
+
+ return None
+
+ async def async_set_speed(self, speed: str) -> None:
+ """Set the speed of the fan."""
+
+ await self._try_command(
+ "Setting operation mode of the miio device failed.",
+ self._device.set_mode,
+ self.REVERSE_MODE_MAPPING[speed],
+ )
+
+ async def async_set_led_brightness(self, brightness: int = 2):
+ """Set the led brightness."""
+ if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0:
+ return
+
+ await self._try_command(
+ "Setting the led brightness of the miio device failed.",
+ self._device.set_led_brightness,
+ AirhumidifierMiotLedBrightness(brightness),
+ )
+
+ async def async_set_motor_speed(self, motor_speed: int = 400):
+ """Set the target motor speed."""
+ if self._device_features & FEATURE_SET_MOTOR_SPEED == 0:
+ return
+
+ await self._try_command(
+ "Setting the target motor speed of the miio device failed.",
+ self._device.set_speed,
+ motor_speed,
+ )
+
+
class XiaomiAirFresh(XiaomiGenericDevice):
"""Representation of a Xiaomi Air Fresh."""
diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json
index 853f8e7920b..2536b0e0aa7 100644
--- a/homeassistant/components/xiaomi_miio/manifest.json
+++ b/homeassistant/components/xiaomi_miio/manifest.json
@@ -3,7 +3,7 @@
"name": "Xiaomi Miio",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/xiaomi_miio",
- "requirements": ["construct==2.9.45", "python-miio==0.5.3"],
+ "requirements": ["construct==2.10.56", "python-miio==0.5.4"],
"codeowners": ["@rytilahti", "@syssi", "@starkillerOG"],
"zeroconf": ["_miio._udp.local."]
}
diff --git a/homeassistant/components/xiaomi_miio/services.yaml b/homeassistant/components/xiaomi_miio/services.yaml
index c61b7f37f22..f0312f01991 100644
--- a/homeassistant/components/xiaomi_miio/services.yaml
+++ b/homeassistant/components/xiaomi_miio/services.yaml
@@ -149,6 +149,16 @@ fan_set_dry_off:
description: Name of the xiaomi miio entity.
example: "fan.xiaomi_miio_device"
+fan_set_motor_speed:
+ description: Set the target motor speed.
+ fields:
+ entity_id:
+ description: Name of the xiaomi miio entity.
+ example: 'fan.xiaomi_miio_device'
+ motor_speed:
+ description: Set RPM of motor speed, between 200 and 2000.
+ example: 1100
+
light_set_scene:
description: Set a fixed scene.
fields:
diff --git a/homeassistant/components/xiaomi_miio/translations/hu.json b/homeassistant/components/xiaomi_miio/translations/hu.json
index ef5ef6c0748..beb5c06c098 100644
--- a/homeassistant/components/xiaomi_miio/translations/hu.json
+++ b/homeassistant/components/xiaomi_miio/translations/hu.json
@@ -1,20 +1,30 @@
{
"config": {
"abort": {
- "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "already_in_progress": "A konfigur\u00e1ci\u00f3s folyamat m\u00e1r fut"
},
"error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s",
"no_device_selected": "Nincs kiv\u00e1lasztva eszk\u00f6z, k\u00e9rj\u00fck, v\u00e1lasszon egyet."
},
+ "flow_title": "Xiaomi Miio: {name}",
"step": {
"gateway": {
"data": {
- "host": "IP c\u00edm"
+ "host": "IP c\u00edm",
+ "name": "K\u00f6zponti egys\u00e9g neve",
+ "token": "API Token"
},
- "description": "Sz\u00fcks\u00e9ge lesz az API Tokenre, tov\u00e1bbi inforaciok: https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token"
+ "description": "Sz\u00fcks\u00e9ge lesz az API Tokenre, tov\u00e1bbi inforaciok: https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token",
+ "title": "Csatlakozzon egy Xiaomi K\u00f6zponti egys\u00e9ghez"
},
"user": {
- "description": "V\u00e1lassza ki, melyik k\u00e9sz\u00fcl\u00e9khez szeretne csatlakozni. "
+ "data": {
+ "gateway": "Csatlakozzon egy Xiaomi K\u00f6zponti egys\u00e9ghez"
+ },
+ "description": "V\u00e1lassza ki, melyik k\u00e9sz\u00fcl\u00e9khez szeretne csatlakozni. ",
+ "title": "Xiaomi Miio"
}
}
}
diff --git a/homeassistant/components/xiaomi_miio/translations/ka.json b/homeassistant/components/xiaomi_miio/translations/ka.json
new file mode 100644
index 00000000000..fa4c9c0abd3
--- /dev/null
+++ b/homeassistant/components/xiaomi_miio/translations/ka.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "cannot_connect": "\u10db\u10d0\u10e0\u10ea\u10ee\u10d8 \u10e8\u10d4\u10d4\u10e0\u10d7\u10d4\u10d1\u10d8\u10e1\u10d0\u10e1"
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py
index ae9d75de54f..324999c7124 100644
--- a/homeassistant/components/yeelight/__init__.py
+++ b/homeassistant/components/yeelight/__init__.py
@@ -17,7 +17,7 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.dispatcher import dispatcher_send
+from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_time_interval
@@ -26,7 +26,7 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = "yeelight"
DATA_YEELIGHT = DOMAIN
DATA_UPDATED = "yeelight_{}_data_updated"
-DEVICE_INITIALIZED = f"{DOMAIN}_device_initialized"
+DEVICE_INITIALIZED = "yeelight_{}_device_initialized"
DEFAULT_NAME = "Yeelight"
DEFAULT_TRANSITION = 350
@@ -181,8 +181,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Yeelight from a config entry."""
async def _initialize(host: str, capabilities: Optional[dict] = None) -> None:
- device = await _async_setup_device(hass, host, entry, capabilities)
+ async_dispatcher_connect(
+ hass,
+ DEVICE_INITIALIZED.format(host),
+ _load_platforms,
+ )
+
+ device = await _async_get_device(hass, host, entry, capabilities)
hass.data[DOMAIN][DATA_CONFIG_ENTRIES][entry.entry_id][DATA_DEVICE] = device
+
+ await device.async_setup()
+
+ async def _load_platforms():
+
for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
@@ -249,28 +260,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
return unload_ok
-async def _async_setup_device(
- hass: HomeAssistant,
- host: str,
- entry: ConfigEntry,
- capabilities: Optional[dict],
-) -> None:
- # Get model from config and capabilities
- model = entry.options.get(CONF_MODEL)
- if not model and capabilities is not None:
- model = capabilities.get("model")
-
- # Set up device
- bulb = Bulb(host, model=model or None)
- if capabilities is None:
- capabilities = await hass.async_add_executor_job(bulb.get_capabilities)
-
- device = YeelightDevice(hass, host, entry.options, bulb, capabilities)
- await hass.async_add_executor_job(device.update)
- await device.async_setup()
- return device
-
-
@callback
def _async_unique_name(capabilities: dict) -> str:
"""Generate name from capabilities."""
@@ -374,6 +363,7 @@ class YeelightDevice:
self._device_type = None
self._available = False
self._remove_time_tracker = None
+ self._initialized = False
self._name = host # Default name is host
if capabilities:
@@ -495,6 +485,8 @@ class YeelightDevice:
try:
self.bulb.get_properties(UPDATE_REQUEST_PROPERTIES)
self._available = True
+ if not self._initialized:
+ self._initialize_device()
except BulbException as ex:
if self._available: # just inform once
_LOGGER.error(
@@ -522,6 +514,11 @@ class YeelightDevice:
ex,
)
+ def _initialize_device(self):
+ self._get_capabilities()
+ self._initialized = True
+ dispatcher_send(self._hass, DEVICE_INITIALIZED.format(self._host))
+
def update(self):
"""Update device properties and send data updated signal."""
self._update_properties()
@@ -584,3 +581,22 @@ class YeelightEntity(Entity):
def update(self) -> None:
"""Update the entity."""
self._device.update()
+
+
+async def _async_get_device(
+ hass: HomeAssistant,
+ host: str,
+ entry: ConfigEntry,
+ capabilities: Optional[dict],
+) -> YeelightDevice:
+ # Get model from config and capabilities
+ model = entry.options.get(CONF_MODEL)
+ if not model and capabilities is not None:
+ model = capabilities.get("model")
+
+ # Set up device
+ bulb = Bulb(host, model=model or None)
+ if capabilities is None:
+ capabilities = await hass.async_add_executor_job(bulb.get_capabilities)
+
+ return YeelightDevice(hass, host, entry.options, bulb, capabilities)
diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py
index 90704a6edfb..98b7f097636 100644
--- a/homeassistant/components/yeelight/light.py
+++ b/homeassistant/components/yeelight/light.py
@@ -9,6 +9,7 @@ from yeelight import (
Flow,
RGBTransition,
SleepTransition,
+ flows,
transitions as yee_transitions,
)
from yeelight.enums import BulbType, LightType, PowerMode, SceneClass
@@ -100,6 +101,15 @@ EFFECT_WHATSAPP = "WhatsApp"
EFFECT_FACEBOOK = "Facebook"
EFFECT_TWITTER = "Twitter"
EFFECT_STOP = "Stop"
+EFFECT_HOME = "Home"
+EFFECT_NIGHT_MODE = "Night Mode"
+EFFECT_DATE_NIGHT = "Date Night"
+EFFECT_MOVIE = "Movie"
+EFFECT_SUNRISE = "Sunrise"
+EFFECT_SUNSET = "Sunset"
+EFFECT_ROMANCE = "Romance"
+EFFECT_HAPPY_BIRTHDAY = "Happy Birthday"
+EFFECT_CANDLE_FLICKER = "Candle Flicker"
YEELIGHT_TEMP_ONLY_EFFECT_LIST = [EFFECT_TEMP, EFFECT_STOP]
@@ -111,6 +121,8 @@ YEELIGHT_MONO_EFFECT_LIST = [
EFFECT_WHATSAPP,
EFFECT_FACEBOOK,
EFFECT_TWITTER,
+ EFFECT_HOME,
+ EFFECT_CANDLE_FLICKER,
*YEELIGHT_TEMP_ONLY_EFFECT_LIST,
]
@@ -123,22 +135,38 @@ YEELIGHT_COLOR_EFFECT_LIST = [
EFFECT_FAST_RANDOM_LOOP,
EFFECT_LSD,
EFFECT_SLOWDOWN,
+ EFFECT_NIGHT_MODE,
+ EFFECT_DATE_NIGHT,
+ EFFECT_MOVIE,
+ EFFECT_SUNRISE,
+ EFFECT_SUNSET,
+ EFFECT_ROMANCE,
+ EFFECT_HAPPY_BIRTHDAY,
*YEELIGHT_MONO_EFFECT_LIST,
]
EFFECTS_MAP = {
- EFFECT_DISCO: yee_transitions.disco,
- EFFECT_TEMP: yee_transitions.temp,
- EFFECT_STROBE: yee_transitions.strobe,
- EFFECT_STROBE_COLOR: yee_transitions.strobe_color,
- EFFECT_ALARM: yee_transitions.alarm,
- EFFECT_POLICE: yee_transitions.police,
- EFFECT_POLICE2: yee_transitions.police2,
- EFFECT_CHRISTMAS: yee_transitions.christmas,
- EFFECT_RGB: yee_transitions.rgb,
- EFFECT_RANDOM_LOOP: yee_transitions.random_loop,
- EFFECT_LSD: yee_transitions.lsd,
- EFFECT_SLOWDOWN: yee_transitions.slowdown,
+ EFFECT_DISCO: flows.disco,
+ EFFECT_TEMP: flows.temp,
+ EFFECT_STROBE: flows.strobe,
+ EFFECT_STROBE_COLOR: flows.strobe_color,
+ EFFECT_ALARM: flows.alarm,
+ EFFECT_POLICE: flows.police,
+ EFFECT_POLICE2: flows.police2,
+ EFFECT_CHRISTMAS: flows.christmas,
+ EFFECT_RGB: flows.rgb,
+ EFFECT_RANDOM_LOOP: flows.random_loop,
+ EFFECT_LSD: flows.lsd,
+ EFFECT_SLOWDOWN: flows.slowdown,
+ EFFECT_HOME: flows.home,
+ EFFECT_NIGHT_MODE: flows.night_mode,
+ EFFECT_DATE_NIGHT: flows.date_night,
+ EFFECT_MOVIE: flows.movie,
+ EFFECT_SUNRISE: flows.sunrise,
+ EFFECT_SUNSET: flows.sunset,
+ EFFECT_ROMANCE: flows.romance,
+ EFFECT_HAPPY_BIRTHDAY: flows.happy_birthday,
+ EFFECT_CANDLE_FLICKER: flows.candle_flicker,
}
VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Range(min=1, max=100))
@@ -652,9 +680,9 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
if effect in self.custom_effects_names:
flow = Flow(**self.custom_effects[effect])
elif effect in EFFECTS_MAP:
- flow = Flow(count=0, transitions=EFFECTS_MAP[effect]())
+ flow = EFFECTS_MAP[effect]()
elif effect == EFFECT_FAST_RANDOM_LOOP:
- flow = Flow(count=0, transitions=yee_transitions.random_loop(duration=250))
+ flow = flows.random_loop(duration=250)
elif effect == EFFECT_WHATSAPP:
flow = Flow(count=2, transitions=yee_transitions.pulse(37, 211, 102))
elif effect == EFFECT_FACEBOOK:
diff --git a/homeassistant/components/yeelight/translations/hu.json b/homeassistant/components/yeelight/translations/hu.json
index 3b2d79a34a7..10a03cebd21 100644
--- a/homeassistant/components/yeelight/translations/hu.json
+++ b/homeassistant/components/yeelight/translations/hu.json
@@ -1,7 +1,38 @@
{
"config": {
"abort": {
- "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van"
+ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van",
+ "no_devices_found": "Nincs eszk\u00f6z a h\u00e1l\u00f3zaton"
+ },
+ "error": {
+ "cannot_connect": "Sikertelen csatlakoz\u00e1s"
+ },
+ "step": {
+ "pick_device": {
+ "data": {
+ "device": "Eszk\u00f6z"
+ }
+ },
+ "user": {
+ "data": {
+ "host": "Gazdag\u00e9p"
+ },
+ "description": "Ha a gazdag\u00e9pet \u00fcresen hagyja, felder\u00edt\u00e9sre ker\u00fcl automatikusan."
+ }
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data": {
+ "model": "Modell (opcion\u00e1lis)",
+ "nightlight_switch": "Haszn\u00e1lja az \u00c9jszakai kapcsol\u00f3t",
+ "save_on_change": "\u00c1llapot ment\u00e9se m\u00f3dos\u00edt\u00e1s ut\u00e1n",
+ "transition": "\u00c1tmeneti id\u0151 (ms)",
+ "use_music_mode": "Zene m\u00f3d enged\u00e9lyez\u00e9se"
+ },
+ "description": "Ha modellt \u00fcresen hagyja, a rendszer automatikusan \u00e9rz\u00e9keli."
+ }
}
}
}
\ No newline at end of file
diff --git a/homeassistant/components/yessssms/__init__.py b/homeassistant/components/yessssms/__init__.py
deleted file mode 100644
index bc5f422ba75..00000000000
--- a/homeassistant/components/yessssms/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""The yessssms component."""
diff --git a/homeassistant/components/yessssms/const.py b/homeassistant/components/yessssms/const.py
deleted file mode 100644
index 473cdfff1e0..00000000000
--- a/homeassistant/components/yessssms/const.py
+++ /dev/null
@@ -1,3 +0,0 @@
-"""Const for YesssSMS."""
-
-CONF_PROVIDER = "provider"
diff --git a/homeassistant/components/yessssms/manifest.json b/homeassistant/components/yessssms/manifest.json
deleted file mode 100644
index 5200408d1d5..00000000000
--- a/homeassistant/components/yessssms/manifest.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "domain": "yessssms",
- "name": "yesss! SMS",
- "documentation": "https://www.home-assistant.io/integrations/yessssms",
- "requirements": ["YesssSMS==0.4.1"],
- "codeowners": ["@flowolf"]
-}
diff --git a/homeassistant/components/yessssms/notify.py b/homeassistant/components/yessssms/notify.py
deleted file mode 100644
index 863602134a4..00000000000
--- a/homeassistant/components/yessssms/notify.py
+++ /dev/null
@@ -1,101 +0,0 @@
-"""Support for the YesssSMS platform."""
-import logging
-
-from YesssSMS import YesssSMS
-import voluptuous as vol
-
-from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService
-from homeassistant.const import CONF_PASSWORD, CONF_RECIPIENT, CONF_USERNAME
-import homeassistant.helpers.config_validation as cv
-
-from .const import CONF_PROVIDER
-
-_LOGGER = logging.getLogger(__name__)
-
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
- {
- vol.Required(CONF_USERNAME): cv.string,
- vol.Required(CONF_PASSWORD): cv.string,
- vol.Required(CONF_RECIPIENT): cv.string,
- vol.Optional(CONF_PROVIDER, default="YESSS"): cv.string,
- }
-)
-
-
-def get_service(hass, config, discovery_info=None):
- """Get the YesssSMS notification service."""
-
- try:
- yesss = YesssSMS(
- config[CONF_USERNAME], config[CONF_PASSWORD], provider=config[CONF_PROVIDER]
- )
- except YesssSMS.UnsupportedProviderError as ex:
- _LOGGER.error("Unknown provider: %s", ex)
- return None
- try:
- if not yesss.login_data_valid():
- _LOGGER.error(
- "Login data is not valid! Please double check your login data at %s",
- yesss.get_login_url(),
- )
- return None
-
- _LOGGER.debug("Login data for '%s' valid", yesss.get_provider())
- except YesssSMS.ConnectionError:
- _LOGGER.warning(
- "Connection Error, could not verify login data for '%s'",
- yesss.get_provider(),
- )
-
- _LOGGER.debug(
- "initialized; library version: %s, with %s",
- yesss.version(),
- yesss.get_provider(),
- )
- return YesssSMSNotificationService(yesss, config[CONF_RECIPIENT])
-
-
-class YesssSMSNotificationService(BaseNotificationService):
- """Implement a notification service for the YesssSMS service."""
-
- def __init__(self, client, recipient):
- """Initialize the service."""
- self.yesss = client
- self._recipient = recipient
-
- def send_message(self, message="", **kwargs):
- """Send a SMS message via Yesss.at's website."""
- if self.yesss.account_is_suspended():
- # only retry to login after Home Assistant was restarted with (hopefully)
- # new login data.
- _LOGGER.error(
- "Account is suspended, cannot send SMS. "
- "Check your login data and restart Home Assistant"
- )
- return
- try:
- self.yesss.send(self._recipient, message)
- except self.yesss.NoRecipientError as ex:
- _LOGGER.error(
- "You need to provide a recipient for SMS notification: %s", ex
- )
- except self.yesss.EmptyMessageError as ex:
- _LOGGER.error("Cannot send empty SMS message: %s", ex)
- except self.yesss.SMSSendingError as ex:
- _LOGGER.error(ex)
- except self.yesss.ConnectionError as ex:
- _LOGGER.error(
- "Unable to connect to server of provider (%s): %s",
- self.yesss.get_provider(),
- ex,
- )
- except self.yesss.AccountSuspendedError as ex:
- _LOGGER.error(
- "Wrong login credentials!! Verify correct credentials and "
- "restart Home Assistant: %s",
- ex,
- )
- except self.yesss.LoginError as ex:
- _LOGGER.error("Wrong login credentials: %s", ex)
- else:
- _LOGGER.info("SMS sent")
diff --git a/homeassistant/components/yi/camera.py b/homeassistant/components/yi/camera.py
index e669f530197..c130532a2e1 100644
--- a/homeassistant/components/yi/camera.py
+++ b/homeassistant/components/yi/camera.py
@@ -123,12 +123,11 @@ class YiCamera(Camera):
"""Return a still image response from the camera."""
url = await self._get_latest_video_url()
if url and url != self._last_url:
- ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop)
+ ffmpeg = ImageFrame(self._manager.binary)
self._last_image = await asyncio.shield(
ffmpeg.get_image(
url, output_format=IMAGE_JPEG, extra_cmd=self._extra_arguments
),
- loop=self.hass.loop,
)
self._last_url = url
@@ -139,7 +138,7 @@ class YiCamera(Camera):
if not self._is_on:
return
- stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop)
+ stream = CameraMjpeg(self._manager.binary)
await stream.open_camera(self._last_url, extra_cmd=self._extra_arguments)
try:
diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json
index 1848c890573..753ac2a2441 100644
--- a/homeassistant/components/zeroconf/manifest.json
+++ b/homeassistant/components/zeroconf/manifest.json
@@ -2,7 +2,7 @@
"domain": "zeroconf",
"name": "Zero-configuration networking (zeroconf)",
"documentation": "https://www.home-assistant.io/integrations/zeroconf",
- "requirements": ["zeroconf==0.28.6"],
+ "requirements": ["zeroconf==0.28.7"],
"dependencies": ["api"],
"codeowners": ["@bdraco"],
"quality_scale": "internal"
diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py
index 728d0f48217..ab0f15f7559 100644
--- a/homeassistant/components/zha/climate.py
+++ b/homeassistant/components/zha/climate.py
@@ -36,7 +36,7 @@ from homeassistant.components.climate.const import (
SUPPORT_TARGET_TEMPERATURE,
SUPPORT_TARGET_TEMPERATURE_RANGE,
)
-from homeassistant.const import ATTR_TEMPERATURE, PRECISION_HALVES, TEMP_CELSIUS
+from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, TEMP_CELSIUS
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.event import async_track_time_interval
@@ -287,7 +287,7 @@ class Thermostat(ZhaEntity, ClimateEntity):
@property
def precision(self):
"""Return the precision of the system."""
- return PRECISION_HALVES
+ return PRECISION_TENTHS
@property
def preset_mode(self) -> Optional[str]:
diff --git a/homeassistant/components/zha/core/channels/base.py b/homeassistant/components/zha/core/channels/base.py
index 9f2fe4f21bd..c6019c10843 100644
--- a/homeassistant/components/zha/core/channels/base.py
+++ b/homeassistant/components/zha/core/channels/base.py
@@ -208,7 +208,7 @@ class ZigbeeChannel(LogMixin):
attributes = []
for report_config in self._report_config:
attributes.append(report_config["attr"])
- if len(attributes) > 0:
+ if attributes:
await self.get_attributes(attributes, from_cache=from_cache)
self._status = ChannelStatus.INITIALIZED
diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py
index f443151de02..dc06d01e596 100644
--- a/homeassistant/components/zha/core/channels/general.py
+++ b/homeassistant/components/zha/core/channels/general.py
@@ -17,10 +17,9 @@ from ..const import (
SIGNAL_ATTR_UPDATED,
SIGNAL_MOVE_LEVEL,
SIGNAL_SET_LEVEL,
- SIGNAL_STATE_ATTR,
SIGNAL_UPDATE_DEVICE,
)
-from .base import ClientChannel, ZigbeeChannel, parse_and_log_command
+from .base import ChannelStatus, ClientChannel, ZigbeeChannel, parse_and_log_command
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Alarms.cluster_id)
@@ -72,13 +71,6 @@ class BasicChannel(ZigbeeChannel):
6: "Emergency mains and transfer switch",
}
- def __init__(
- self, cluster: zha_typing.ZigpyClusterType, ch_pool: zha_typing.ChannelPoolType
- ) -> None:
- """Initialize BasicChannel."""
- super().__init__(cluster, ch_pool)
- self._power_source = None
-
async def async_configure(self):
"""Configure this channel."""
await super().async_configure()
@@ -87,16 +79,12 @@ class BasicChannel(ZigbeeChannel):
async def async_initialize(self, from_cache):
"""Initialize channel."""
if not self._ch_pool.skip_configuration or from_cache:
- power_source = await self.get_attribute_value(
- "power_source", from_cache=from_cache
- )
- if power_source is not None:
- self._power_source = power_source
+ await self.get_attribute_value("power_source", from_cache=from_cache)
await super().async_initialize(from_cache)
def get_power_source(self):
"""Get the power source."""
- return self._power_source
+ return self.cluster.get("power_source")
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.BinaryInput.cluster_id)
@@ -159,7 +147,6 @@ class LevelControlClientChannel(ClientChannel):
@registries.BINDABLE_CLUSTERS.register(general.LevelControl.cluster_id)
-@registries.LIGHT_CLUSTERS.register(general.LevelControl.cluster_id)
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.LevelControl.cluster_id)
class LevelControlChannel(ZigbeeChannel):
"""Channel for the LevelControl Zigbee cluster."""
@@ -234,10 +221,7 @@ class OnOffClientChannel(ClientChannel):
"""OnOff client channel."""
-@registries.BINARY_SENSOR_CLUSTERS.register(general.OnOff.cluster_id)
@registries.BINDABLE_CLUSTERS.register(general.OnOff.cluster_id)
-@registries.LIGHT_CLUSTERS.register(general.OnOff.cluster_id)
-@registries.SWITCH_CLUSTERS.register(general.OnOff.cluster_id)
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.OnOff.cluster_id)
class OnOffChannel(ZigbeeChannel):
"""Channel for the OnOff Zigbee cluster."""
@@ -382,7 +366,6 @@ class PollControl(ZigbeeChannel):
await self.set_long_poll_interval(self.LONG_POLL)
-@registries.DEVICE_TRACKER_CLUSTERS.register(general.PowerConfiguration.cluster_id)
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.PowerConfiguration.cluster_id)
class PowerConfigurationChannel(ZigbeeChannel):
"""Channel for the zigbee power configuration cluster."""
@@ -392,38 +375,8 @@ class PowerConfigurationChannel(ZigbeeChannel):
{"attr": "battery_percentage_remaining", "config": REPORT_CONFIG_BATTERY_SAVE},
)
- @callback
- def attribute_updated(self, attrid, value):
- """Handle attribute updates on this cluster."""
- attr = self._report_config[1].get("attr")
- if isinstance(attr, str):
- attr_id = self.cluster.attridx.get(attr)
- else:
- attr_id = attr
- if attrid == attr_id:
- self.async_send_signal(
- f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}",
- attrid,
- self.cluster.attributes.get(attrid, [attrid])[0],
- value,
- )
- return
- attr_name = self.cluster.attributes.get(attrid, [attrid])[0]
- self.async_send_signal(
- f"{self.unique_id}_{SIGNAL_STATE_ATTR}", attr_name, value
- )
-
async def async_initialize(self, from_cache):
"""Initialize channel."""
- await self.async_read_state(from_cache)
- await super().async_initialize(from_cache)
-
- async def async_update(self):
- """Retrieve latest state."""
- await self.async_read_state(True)
-
- async def async_read_state(self, from_cache):
- """Read data from the cluster."""
attributes = [
"battery_size",
"battery_percentage_remaining",
@@ -431,6 +384,7 @@ class PowerConfigurationChannel(ZigbeeChannel):
"battery_quantity",
]
await self.get_attributes(attributes, from_cache=from_cache)
+ self._status = ChannelStatus.INITIALIZED
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.PowerProfile.cluster_id)
diff --git a/homeassistant/components/zha/core/channels/hvac.py b/homeassistant/components/zha/core/channels/hvac.py
index 1eedf51cd00..ac832aacc61 100644
--- a/homeassistant/components/zha/core/channels/hvac.py
+++ b/homeassistant/components/zha/core/channels/hvac.py
@@ -92,7 +92,6 @@ class Pump(ZigbeeChannel):
"""Pump channel."""
-@registries.CLIMATE_CLUSTERS.register(hvac.Thermostat.cluster_id)
@registries.ZIGBEE_CHANNEL_REGISTRY.register(hvac.Thermostat.cluster_id)
class ThermostatChannel(ZigbeeChannel):
"""Thermostat channel."""
diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py
index 9d52ff12d37..16223582c33 100644
--- a/homeassistant/components/zha/core/channels/lighting.py
+++ b/homeassistant/components/zha/core/channels/lighting.py
@@ -3,7 +3,7 @@ from typing import Optional
import zigpy.zcl.clusters.lighting as lighting
-from .. import registries, typing as zha_typing
+from .. import registries
from ..const import REPORT_CONFIG_DEFAULT
from .base import ClientChannel, ZigbeeChannel
@@ -19,7 +19,6 @@ class ColorClientChannel(ClientChannel):
@registries.BINDABLE_CLUSTERS.register(lighting.Color.cluster_id)
-@registries.LIGHT_CLUSTERS.register(lighting.Color.cluster_id)
@registries.ZIGBEE_CHANNEL_REGISTRY.register(lighting.Color.cluster_id)
class ColorChannel(ZigbeeChannel):
"""Color channel."""
@@ -32,15 +31,19 @@ class ColorChannel(ZigbeeChannel):
{"attr": "current_y", "config": REPORT_CONFIG_DEFAULT},
{"attr": "color_temperature", "config": REPORT_CONFIG_DEFAULT},
)
+ MAX_MIREDS: int = 500
+ MIN_MIREDS: int = 153
- def __init__(
- self, cluster: zha_typing.ZigpyClusterType, ch_pool: zha_typing.ChannelPoolType
- ) -> None:
- """Initialize ColorChannel."""
- super().__init__(cluster, ch_pool)
- self._color_capabilities = None
- self._min_mireds = 153
- self._max_mireds = 500
+ @property
+ def color_capabilities(self) -> int:
+ """Return color capabilities of the light."""
+ try:
+ return self.cluster["color_capabilities"]
+ except KeyError:
+ pass
+ if self.cluster.get("color_temperature") is not None:
+ return self.CAPABILITIES_COLOR_XY | self.CAPABILITIES_COLOR_TEMP
+ return self.CAPABILITIES_COLOR_XY
@property
def color_loop_active(self) -> Optional[int]:
@@ -65,49 +68,30 @@ class ColorChannel(ZigbeeChannel):
@property
def min_mireds(self) -> int:
"""Return the coldest color_temp that this channel supports."""
- return self.cluster.get("color_temp_physical_min", self._min_mireds)
+ return self.cluster.get("color_temp_physical_min", self.MIN_MIREDS)
@property
def max_mireds(self) -> int:
"""Return the warmest color_temp that this channel supports."""
- return self.cluster.get("color_temp_physical_max", self._max_mireds)
+ return self.cluster.get("color_temp_physical_max", self.MAX_MIREDS)
- def get_color_capabilities(self):
- """Return the color capabilities."""
- return self._color_capabilities
-
- async def async_configure(self):
+ async def async_configure(self) -> None:
"""Configure channel."""
await self.fetch_color_capabilities(False)
await super().async_configure()
- async def async_initialize(self, from_cache):
+ async def async_initialize(self, from_cache: bool) -> None:
"""Initialize channel."""
await self.fetch_color_capabilities(True)
- attributes = ["color_temperature", "current_x", "current_y"]
- await self.get_attributes(attributes, from_cache=from_cache)
+ await super().async_initialize(from_cache)
- async def fetch_color_capabilities(self, from_cache):
+ async def fetch_color_capabilities(self, from_cache: bool) -> None:
"""Get the color configuration."""
attributes = [
"color_temp_physical_min",
"color_temp_physical_max",
"color_capabilities",
+ "color_temperature",
]
- results = await self.get_attributes(attributes, from_cache=from_cache)
- capabilities = results.get("color_capabilities")
-
- if capabilities is None:
- # ZCL Version 4 devices don't support the color_capabilities
- # attribute. In this version XY support is mandatory, but we
- # need to probe to determine if the device supports color
- # temperature.
- capabilities = self.CAPABILITIES_COLOR_XY
- result = await self.get_attribute_value(
- "color_temperature", from_cache=from_cache
- )
-
- if result is not None and result is not self.UNSUPPORTED_ATTRIBUTE:
- capabilities |= self.CAPABILITIES_COLOR_TEMP
- self._color_capabilities = capabilities
- await super().async_initialize(from_cache)
+ # just populates the cache, if not already done
+ await self.get_attributes(attributes, from_cache=from_cache)
diff --git a/homeassistant/components/zha/core/channels/measurement.py b/homeassistant/components/zha/core/channels/measurement.py
index 949b7cbc138..64db1aa82ac 100644
--- a/homeassistant/components/zha/core/channels/measurement.py
+++ b/homeassistant/components/zha/core/channels/measurement.py
@@ -36,7 +36,6 @@ class IlluminanceMeasurement(ZigbeeChannel):
REPORT_CONFIG = [{"attr": "measured_value", "config": REPORT_CONFIG_DEFAULT}]
-@registries.BINARY_SENSOR_CLUSTERS.register(measurement.OccupancySensing.cluster_id)
@registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.OccupancySensing.cluster_id)
class OccupancySensing(ZigbeeChannel):
"""Occupancy Sensing channel."""
diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py
index 156ada1e8f1..e37987bc821 100644
--- a/homeassistant/components/zha/core/channels/security.py
+++ b/homeassistant/components/zha/core/channels/security.py
@@ -109,7 +109,6 @@ class IasWd(ZigbeeChannel):
)
-@registries.BINARY_SENSOR_CLUSTERS.register(security.IasZone.cluster_id)
@registries.ZIGBEE_CHANNEL_REGISTRY.register(security.IasZone.cluster_id)
class IASZoneChannel(ZigbeeChannel):
"""Channel for the IASZone Zigbee cluster."""
diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py
index 120d0afdfb6..792b9413294 100644
--- a/homeassistant/components/zha/core/channels/smartenergy.py
+++ b/homeassistant/components/zha/core/channels/smartenergy.py
@@ -1,4 +1,6 @@
"""Smart energy channels module for Zigbee Home Automation."""
+from typing import Union
+
import zigpy.zcl.clusters.smartenergy as smartenergy
from homeassistant.const import (
@@ -82,44 +84,48 @@ class Metering(ZigbeeChannel):
) -> None:
"""Initialize Metering."""
super().__init__(cluster, ch_pool)
- self._divisor = 1
- self._multiplier = 1
- self._unit_enum = None
self._format_spec = None
- async def async_configure(self):
+ @property
+ def divisor(self) -> int:
+ """Return divisor for the value."""
+ return self.cluster.get("divisor")
+
+ @property
+ def multiplier(self) -> int:
+ """Return multiplier for the value."""
+ return self.cluster.get("multiplier")
+
+ async def async_configure(self) -> None:
"""Configure channel."""
await self.fetch_config(False)
await super().async_configure()
- async def async_initialize(self, from_cache):
+ async def async_initialize(self, from_cache: bool) -> None:
"""Initialize channel."""
await self.fetch_config(True)
await super().async_initialize(from_cache)
@callback
- def attribute_updated(self, attrid, value):
+ def attribute_updated(self, attrid: int, value: int) -> None:
"""Handle attribute update from Metering cluster."""
- if None in (self._multiplier, self._divisor, self._format_spec):
+ if None in (self.multiplier, self.divisor, self._format_spec):
return
- super().attribute_updated(attrid, value * self._multiplier / self._divisor)
+ super().attribute_updated(attrid, value)
@property
- def unit_of_measurement(self):
+ def unit_of_measurement(self) -> str:
"""Return unit of measurement."""
- return self.unit_of_measure_map.get(self._unit_enum & 0x7F, "unknown")
+ uom = self.cluster.get("unit_of_measure", 0x7F)
+ return self.unit_of_measure_map.get(uom & 0x7F, "unknown")
- async def fetch_config(self, from_cache):
+ async def fetch_config(self, from_cache: bool) -> None:
"""Fetch config from device and updates format specifier."""
results = await self.get_attributes(
["divisor", "multiplier", "unit_of_measure", "demand_formatting"],
from_cache=from_cache,
)
- self._divisor = results.get("divisor", self._divisor)
- self._multiplier = results.get("multiplier", self._multiplier)
- self._unit_enum = results.get("unit_of_measure", 0x7F) # default to unknown
-
fmting = results.get(
"demand_formatting", 0xF9
) # 1 digit to the right, 15 digits to the left
@@ -135,8 +141,9 @@ class Metering(ZigbeeChannel):
else:
self._format_spec = "{:0" + str(width) + "." + str(r_digits) + "f}"
- def formatter_function(self, value):
+ def formatter_function(self, value: int) -> Union[int, float]:
"""Return formatted value for display."""
+ value = value * self.multiplier / self.divisor
if self.unit_of_measurement == POWER_WATT:
# Zigbee spec power unit is kW, but we show the value in W
value_watt = value * 1000
diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py
index 22f8f0f261d..12d928e172a 100644
--- a/homeassistant/components/zha/core/const.py
+++ b/homeassistant/components/zha/core/const.py
@@ -155,6 +155,9 @@ DEBUG_RELAY_LOGGERS = [DEBUG_COMP_ZHA, DEBUG_COMP_ZIGPY]
DEFAULT_RADIO_TYPE = "ezsp"
DEFAULT_BAUDRATE = 57600
DEFAULT_DATABASE_NAME = "zigbee.db"
+
+DEVICE_PAIRING_STATUS = "pairing_status"
+
DISCOVERY_KEY = "zha_discovery_info"
DOMAIN = "zha"
diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py
index 81b522308ff..cd3b1bd93ce 100644
--- a/homeassistant/components/zha/core/device.py
+++ b/homeassistant/components/zha/core/device.py
@@ -254,8 +254,10 @@ class ZHADevice(LogMixin):
"device_event_type": "device_offline"
}
}
+
if hasattr(self._zigpy_device, "device_automation_triggers"):
triggers.update(self._zigpy_device.device_automation_triggers)
+
return triggers
@property
diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py
index 25f320b0bf1..05a12bc2284 100644
--- a/homeassistant/components/zha/core/discovery.py
+++ b/homeassistant/components/zha/core/discovery.py
@@ -39,12 +39,13 @@ async def async_add_entities(
Tuple[str, zha_typing.ZhaDeviceType, List[zha_typing.ChannelType]],
]
],
+ update_before_add: bool = True,
) -> None:
"""Add entities helper."""
if not entities:
return
to_add = [ent_cls(*args) for ent_cls, args in entities]
- _async_add_entities(to_add, update_before_add=True)
+ _async_add_entities(to_add, update_before_add=update_before_add)
entities.clear()
@@ -242,7 +243,9 @@ class GroupProbe:
if member.device.is_coordinator:
continue
entities = async_entries_for_device(
- zha_gateway.ha_entity_registry, member.device.device_id
+ zha_gateway.ha_entity_registry,
+ member.device.device_id,
+ include_disabled_entities=True,
)
all_domain_occurrences.extend(
[
diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py
index bdfcf1b24f2..812ac168d48 100644
--- a/homeassistant/components/zha/core/gateway.py
+++ b/homeassistant/components/zha/core/gateway.py
@@ -3,6 +3,7 @@
import asyncio
import collections
from datetime import timedelta
+from enum import Enum
import itertools
import logging
import os
@@ -54,6 +55,7 @@ from .const import (
DEBUG_LEVELS,
DEBUG_RELAY_LOGGERS,
DEFAULT_DATABASE_NAME,
+ DEVICE_PAIRING_STATUS,
DOMAIN,
SIGNAL_ADD_ENTITIES,
SIGNAL_GROUP_MEMBERSHIP_CHANGE,
@@ -82,7 +84,6 @@ from .device import (
ZHADevice,
)
from .group import GroupMember, ZHAGroup
-from .patches import apply_application_controller_patch
from .registries import GROUP_ENTITY_DOMAINS
from .store import async_get_registry
from .typing import ZhaGroupType, ZigpyEndpointType, ZigpyGroupType
@@ -95,6 +96,15 @@ EntityReference = collections.namedtuple(
)
+class DevicePairingStatus(Enum):
+ """Status of a device."""
+
+ PAIRED = 1
+ INTERVIEW_COMPLETE = 2
+ CONFIGURED = 3
+ INITIALIZED = 4
+
+
class ZHAGateway:
"""Gateway that handles events that happen on the ZHA Zigbee network."""
@@ -155,7 +165,6 @@ class ZHAGateway:
)
raise ConfigEntryNotReady from exception
- apply_application_controller_patch(self)
self.application_controller.add_listener(self)
self.application_controller.groups.add_listener(self)
self._hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] = self
@@ -242,8 +251,11 @@ class ZHAGateway:
ZHA_GW_MSG,
{
ATTR_TYPE: ZHA_GW_MSG_DEVICE_JOINED,
- ATTR_NWK: device.nwk,
- ATTR_IEEE: str(device.ieee),
+ ZHA_GW_MSG_DEVICE_INFO: {
+ ATTR_NWK: device.nwk,
+ ATTR_IEEE: str(device.ieee),
+ DEVICE_PAIRING_STATUS: DevicePairingStatus.PAIRED.name,
+ },
},
)
@@ -255,11 +267,14 @@ class ZHAGateway:
ZHA_GW_MSG,
{
ATTR_TYPE: ZHA_GW_MSG_RAW_INIT,
- ATTR_NWK: device.nwk,
- ATTR_IEEE: str(device.ieee),
- ATTR_MODEL: device.model if device.model else UNKNOWN_MODEL,
- ATTR_MANUFACTURER: manuf if manuf else UNKNOWN_MANUFACTURER,
- ATTR_SIGNATURE: device.get_signature(),
+ ZHA_GW_MSG_DEVICE_INFO: {
+ ATTR_NWK: device.nwk,
+ ATTR_IEEE: str(device.ieee),
+ DEVICE_PAIRING_STATUS: DevicePairingStatus.INTERVIEW_COMPLETE.name,
+ ATTR_MODEL: device.model if device.model else UNKNOWN_MODEL,
+ ATTR_MANUFACTURER: manuf if manuf else UNKNOWN_MANUFACTURER,
+ ATTR_SIGNATURE: device.get_signature(),
+ },
},
)
@@ -402,7 +417,9 @@ class ZHAGateway:
# then we get all group entity entries tied to the coordinator
all_group_entity_entries = async_entries_for_device(
- self.ha_entity_registry, self.coordinator_zha_device.device_id
+ self.ha_entity_registry,
+ self.coordinator_zha_device.device_id,
+ include_disabled_entities=True,
)
# then we get the entity entries for this specific group by getting the entries that match
@@ -506,13 +523,6 @@ class ZHAGateway:
self._groups[zigpy_group.group_id] = zha_group
return zha_group
- @callback
- def async_device_became_available(
- self, sender, profile, cluster, src_ep, dst_ep, message
- ):
- """Handle tasks when a device becomes available."""
- self.async_update_device(sender, available=True)
-
@callback
def async_update_device(
self, sender: zigpy_dev.Device, available: bool = True
@@ -560,7 +570,7 @@ class ZHAGateway:
await self._async_device_joined(zha_device)
device_info = zha_device.zha_device_info
-
+ device_info[DEVICE_PAIRING_STATUS] = DevicePairingStatus.INITIALIZED.name
async_dispatcher_send(
self._hass,
ZHA_GW_MSG,
@@ -572,7 +582,17 @@ class ZHAGateway:
async def _async_device_joined(self, zha_device: zha_typing.ZhaDeviceType) -> None:
zha_device.available = True
+ device_info = zha_device.device_info
await zha_device.async_configure()
+ device_info[DEVICE_PAIRING_STATUS] = DevicePairingStatus.CONFIGURED.name
+ async_dispatcher_send(
+ self._hass,
+ ZHA_GW_MSG,
+ {
+ ATTR_TYPE: ZHA_GW_MSG_DEVICE_FULL_INIT,
+ ZHA_GW_MSG_DEVICE_INFO: device_info,
+ },
+ )
await zha_device.async_initialize(from_cache=False)
async_dispatcher_send(self._hass, SIGNAL_ADD_ENTITIES)
@@ -584,6 +604,16 @@ class ZHAGateway:
)
# we don't have to do this on a nwk swap but we don't have a way to tell currently
await zha_device.async_configure()
+ device_info = zha_device.device_info
+ device_info[DEVICE_PAIRING_STATUS] = DevicePairingStatus.CONFIGURED.name
+ async_dispatcher_send(
+ self._hass,
+ ZHA_GW_MSG,
+ {
+ ATTR_TYPE: ZHA_GW_MSG_DEVICE_FULL_INIT,
+ ZHA_GW_MSG_DEVICE_INFO: device_info,
+ },
+ )
# force async_initialize() to fire so don't explicitly call it
zha_device.available = False
zha_device.update_available(True)
@@ -639,6 +669,19 @@ class ZHAGateway:
unsubscribe()
await self.application_controller.pre_shutdown()
+ def handle_message(
+ self,
+ sender: zigpy_dev.Device,
+ profile: int,
+ cluster: int,
+ src_ep: int,
+ dst_ep: int,
+ message: bytes,
+ ) -> None:
+ """Handle message from a device Event handler."""
+ if sender.ieee in self.devices and not self.devices[sender.ieee].available:
+ self.async_update_device(sender, available=True)
+
@callback
def async_capture_log_levels():
diff --git a/homeassistant/components/zha/core/group.py b/homeassistant/components/zha/core/group.py
index 2961f335989..59277a394b3 100644
--- a/homeassistant/components/zha/core/group.py
+++ b/homeassistant/components/zha/core/group.py
@@ -194,8 +194,12 @@ class ZHAGroup(LogMixin):
"""Return entity ids from the entity domain for this group."""
domain_entity_ids: List[str] = []
for member in self.members:
+ if member.device.is_coordinator:
+ continue
entities = async_entries_for_device(
- self._zha_gateway.ha_entity_registry, member.device.device_id
+ self._zha_gateway.ha_entity_registry,
+ member.device.device_id,
+ include_disabled_entities=True,
)
domain_entity_ids.extend(
[entity.entity_id for entity in entities if entity.domain == domain]
diff --git a/homeassistant/components/zha/core/patches.py b/homeassistant/components/zha/core/patches.py
deleted file mode 100644
index 633152e253c..00000000000
--- a/homeassistant/components/zha/core/patches.py
+++ /dev/null
@@ -1,18 +0,0 @@
-"""Patch functions for Zigbee Home Automation."""
-
-
-def apply_application_controller_patch(zha_gateway):
- """Apply patches to ZHA objects."""
- # Patch handle_message until zigpy can provide an event here
- def handle_message(sender, profile, cluster, src_ep, dst_ep, message):
- """Handle message from a device."""
- if (
- sender.ieee in zha_gateway.devices
- and not zha_gateway.devices[sender.ieee].available
- ):
- zha_gateway.async_device_became_available(
- sender, profile, cluster, src_ep, dst_ep, message
- )
- return sender.handle_message(profile, cluster, src_ep, dst_ep, message)
-
- zha_gateway.application_controller.handle_message = handle_message
diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py
index 81521748da0..e2b4056cfaa 100644
--- a/homeassistant/components/zha/core/registries.py
+++ b/homeassistant/components/zha/core/registries.py
@@ -25,7 +25,6 @@ from .typing import ChannelType
GROUP_ENTITY_DOMAINS = [LIGHT, SWITCH, FAN]
PHILLIPS_REMOTE_CLUSTER = 0xFC00
-
SMARTTHINGS_ACCELERATION_CLUSTER = 0xFC02
SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE = 0x8000
SMARTTHINGS_HUMIDITY_CLUSTER = 0xFC45
@@ -80,15 +79,8 @@ SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {
zcl.clusters.general.OnOff.cluster_id: BINARY_SENSOR
}
-SWITCH_CLUSTERS = SetRegistry()
-
-BINARY_SENSOR_CLUSTERS = SetRegistry()
-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: {
@@ -119,19 +111,7 @@ DEVICE_CLASS = {
}
DEVICE_CLASS = collections.defaultdict(dict, DEVICE_CLASS)
-DEVICE_TRACKER_CLUSTERS = SetRegistry()
-LIGHT_CLUSTERS = SetRegistry()
-OUTPUT_CHANNEL_ONLY_CLUSTERS = SetRegistry()
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,
-}
-
ZIGBEE_CHANNEL_REGISTRY = DictRegistry()
diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py
index 22ab0cdcb21..6b3a39d0926 100644
--- a/homeassistant/components/zha/light.py
+++ b/homeassistant/components/zha/light.py
@@ -280,7 +280,7 @@ class BaseLight(LogMixin, light.LightEntity):
0x0,
0x0,
0x0,
- 0x0, # update action only, action off, no dir,time,hue
+ 0x0, # update action only, action off, no dir, time, hue
)
t_log["color_loop_set"] = result
self._effect = None
@@ -344,7 +344,7 @@ class Light(BaseLight, ZhaEntity):
self._brightness = self._level_channel.current_level
if self._color_channel:
- color_capabilities = self._color_channel.get_color_capabilities()
+ color_capabilities = self._color_channel.color_capabilities
if color_capabilities & CAPABILITIES_COLOR_TEMP:
self._supported_features |= light.SUPPORT_COLOR_TEMP
self._color_temp = self._color_channel.color_temperature
@@ -421,52 +421,38 @@ class Light(BaseLight, ZhaEntity):
if "effect" in last_state.attributes:
self._effect = last_state.attributes["effect"]
- async def async_get_state(self, from_cache=True):
- """Attempt to retrieve on off state from the light."""
- if not from_cache and not self.available:
+ async def async_get_state(self):
+ """Attempt to retrieve the state from the light."""
+ if not self.available:
return
- self.debug("polling current state - from cache: %s", from_cache)
+ self.debug("polling current state")
if self._on_off_channel:
state = await self._on_off_channel.get_attribute_value(
- "on_off", from_cache=from_cache
+ "on_off", from_cache=False
)
if state is not None:
self._state = state
if self._level_channel:
level = await self._level_channel.get_attribute_value(
- "current_level", from_cache=from_cache
+ "current_level", from_cache=False
)
if level is not None:
self._brightness = level
if self._color_channel:
- attributes = []
- color_capabilities = self._color_channel.get_color_capabilities()
- if (
- color_capabilities is not None
- and color_capabilities & CAPABILITIES_COLOR_TEMP
- ):
- attributes.append("color_temperature")
- if (
- color_capabilities is not None
- and color_capabilities & CAPABILITIES_COLOR_XY
- ):
- attributes.append("current_x")
- attributes.append("current_y")
- if (
- color_capabilities is not None
- and color_capabilities & CAPABILITIES_COLOR_LOOP
- ):
- attributes.append("color_loop_active")
+ attributes = [
+ "color_temperature",
+ "current_x",
+ "current_y",
+ "color_loop_active",
+ ]
results = await self._color_channel.get_attributes(
- attributes, from_cache=from_cache
+ attributes, from_cache=False
)
- if (
- "color_temperature" in results
- and results["color_temperature"] is not None
- ):
- self._color_temp = results["color_temperature"]
+ color_temp = results.get("color_temperature")
+ if color_temp is not None:
+ self._color_temp = color_temp
color_x = results.get("current_x")
color_y = results.get("current_y")
@@ -474,23 +460,27 @@ class Light(BaseLight, ZhaEntity):
self._hs_color = color_util.color_xy_to_hs(
float(color_x / 65535), float(color_y / 65535)
)
- if (
- "color_loop_active" in results
- and results["color_loop_active"] is not None
- ):
- color_loop_active = results["color_loop_active"]
+
+ color_loop_active = results.get("color_loop_active")
+ if color_loop_active is not None:
if color_loop_active == 1:
self._effect = light.EFFECT_COLORLOOP
+ else:
+ self._effect = None
+
+ async def async_update(self):
+ """Update to the latest state."""
+ await self.async_get_state()
async def _refresh(self, time):
"""Call async_get_state at an interval."""
- await self.async_get_state(from_cache=False)
+ await self.async_get_state()
self.async_write_ha_state()
async def _maybe_force_refresh(self, signal):
"""Force update the state if the signal contains the entity id for this entity."""
if self.entity_id in signal["entity_ids"]:
- await self.async_get_state(from_cache=False)
+ await self.async_get_state()
self.async_write_ha_state()
diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json
index ffbdfdf386b..bcaa4038de1 100644
--- a/homeassistant/components/zha/manifest.json
+++ b/homeassistant/components/zha/manifest.json
@@ -4,15 +4,16 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zha",
"requirements": [
- "bellows==0.20.3",
+ "bellows==0.21.0",
"pyserial==3.4",
- "zha-quirks==0.0.46",
+ "pyserial-asyncio==0.4",
+ "zha-quirks==0.0.49",
"zigpy-cc==0.5.2",
"zigpy-deconz==0.11.0",
- "zigpy==0.27.0",
+ "zigpy==0.28.2",
"zigpy-xbee==0.13.0",
"zigpy-zigate==0.7.3",
- "zigpy-znp==0.2.2"
+ "zigpy-znp==0.3.0"
],
"codeowners": ["@dmulcahey", "@adminiuga"]
}
diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py
index 302637cc068..b02b3a549be 100644
--- a/homeassistant/components/zha/sensor.py
+++ b/homeassistant/components/zha/sensor.py
@@ -1,6 +1,7 @@
"""Sensors on Zigbee Home Automation networks."""
import functools
import numbers
+from typing import Any, Callable, Dict, List, Optional, Union
from homeassistant.components.sensor import (
DEVICE_CLASS_BATTERY,
@@ -11,18 +12,17 @@ from homeassistant.components.sensor import (
DEVICE_CLASS_TEMPERATURE,
DOMAIN,
)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
- ATTR_UNIT_OF_MEASUREMENT,
LIGHT_LUX,
PERCENTAGE,
POWER_WATT,
PRESSURE_HPA,
- STATE_UNKNOWN,
TEMP_CELSIUS,
)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
-from homeassistant.util.temperature import fahrenheit_to_celsius
+from homeassistant.helpers.typing import HomeAssistantType, StateType
from .core import discovery
from .core.const import (
@@ -38,9 +38,9 @@ from .core.const import (
DATA_ZHA_DISPATCHERS,
SIGNAL_ADD_ENTITIES,
SIGNAL_ATTR_UPDATED,
- SIGNAL_STATE_ATTR,
)
from .core.registries import SMARTTHINGS_HUMIDITY_CLUSTER, ZHA_ENTITIES
+from .core.typing import ChannelType, ZhaDeviceType
from .entity import ZhaEntity
PARALLEL_UPDATES = 5
@@ -65,7 +65,9 @@ CHANNEL_ST_HUMIDITY_CLUSTER = f"channel_0x{SMARTTHINGS_HUMIDITY_CLUSTER:04x}"
STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN)
-async def async_setup_entry(hass, config_entry, async_add_entities):
+async def async_setup_entry(
+ hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable
+) -> None:
"""Set up the Zigbee Home Automation sensor from config entry."""
entities_to_create = hass.data[DATA_ZHA][DOMAIN]
@@ -73,7 +75,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
hass,
SIGNAL_ADD_ENTITIES,
functools.partial(
- discovery.async_add_entities, async_add_entities, entities_to_create
+ discovery.async_add_entities,
+ async_add_entities,
+ entities_to_create,
+ update_before_add=False,
),
)
hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub)
@@ -82,29 +87,30 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class Sensor(ZhaEntity):
"""Base ZHA sensor."""
- SENSOR_ATTR = None
- _decimals = 1
- _device_class = None
- _divisor = 1
- _multiplier = 1
- _unit = None
+ SENSOR_ATTR: Optional[Union[int, str]] = None
+ _decimals: int = 1
+ _device_class: Optional[str] = None
+ _divisor: int = 1
+ _multiplier: int = 1
+ _unit: Optional[str] = None
- def __init__(self, unique_id, zha_device, channels, **kwargs):
+ def __init__(
+ self,
+ unique_id: str,
+ zha_device: ZhaDeviceType,
+ channels: List[ChannelType],
+ **kwargs,
+ ):
"""Init this sensor."""
super().__init__(unique_id, zha_device, channels, **kwargs)
- self._channel = channels[0]
+ self._channel: ChannelType = channels[0]
- async def async_added_to_hass(self):
+ async def async_added_to_hass(self) -> None:
"""Run when about to be added to hass."""
await super().async_added_to_hass()
- self._device_state_attributes.update(await self.async_state_attr_provider())
-
self.async_accept_signal(
self._channel, SIGNAL_ATTR_UPDATED, self.async_set_state
)
- self.async_accept_signal(
- self._channel, SIGNAL_STATE_ATTR, self.async_update_state_attribute
- )
@property
def device_class(self) -> str:
@@ -112,37 +118,25 @@ class Sensor(ZhaEntity):
return self._device_class
@property
- def unit_of_measurement(self):
+ def unit_of_measurement(self) -> Optional[str]:
"""Return the unit of measurement of this entity."""
return self._unit
@property
- def state(self) -> str:
+ def state(self) -> StateType:
"""Return the state of the entity."""
- if self._state is None:
+ assert self.SENSOR_ATTR is not None
+ raw_state = self._channel.cluster.get(self.SENSOR_ATTR)
+ if raw_state is None:
return None
- return self._state
+ return self.formatter(raw_state)
@callback
- def async_set_state(self, attr_id, attr_name, value):
+ def async_set_state(self, attr_id: int, attr_name: str, value: Any) -> None:
"""Handle state update from channel."""
- if self.SENSOR_ATTR is None or self.SENSOR_ATTR != attr_name:
- return
- if value is not None:
- value = self.formatter(value)
- self._state = value
self.async_write_ha_state()
- @callback
- def async_restore_last_state(self, last_state):
- """Restore previous state."""
- self._state = last_state.state
-
- async def async_state_attr_provider(self):
- """Initialize device state attributes."""
- return {}
-
- def formatter(self, value):
+ def formatter(self, value: int) -> Union[int, float]:
"""Numeric pass-through formatter."""
if self._decimals > 0:
return round(
@@ -151,6 +145,11 @@ class Sensor(ZhaEntity):
return round(float(value * self._multiplier) / self._divisor)
+@STRICT_MATCH(
+ channel_names=CHANNEL_ANALOG_INPUT,
+ manufacturers="LUMI",
+ models={"lumi.plug", "lumi.plug.maus01", "lumi.plug.mmeu01"},
+)
@STRICT_MATCH(channel_names=CHANNEL_ANALOG_INPUT, manufacturers="Digi")
class AnalogInput(Sensor):
"""Sensor that displays analog input values."""
@@ -167,7 +166,7 @@ class Battery(Sensor):
_unit = PERCENTAGE
@staticmethod
- def formatter(value):
+ def formatter(value: int) -> int:
"""Return the state of the entity."""
# per zcl specs battery percent is reported at 200% ¯\_(ツ)_/¯
if not isinstance(value, numbers.Number) or value == -1:
@@ -175,26 +174,21 @@ class Battery(Sensor):
value = round(value / 2)
return value
- async def async_state_attr_provider(self):
+ @property
+ def device_state_attributes(self) -> Dict[str, Any]:
"""Return device state attrs for battery sensors."""
state_attrs = {}
- attributes = ["battery_size", "battery_quantity"]
- results = await self._channel.get_attributes(attributes)
- battery_size = results.get("battery_size")
+ battery_size = self._channel.cluster.get("battery_size")
if battery_size is not None:
state_attrs["battery_size"] = BATTERY_SIZES.get(battery_size, "Unknown")
- battery_quantity = results.get("battery_quantity")
+ battery_quantity = self._channel.cluster.get("battery_quantity")
if battery_quantity is not None:
state_attrs["battery_quantity"] = battery_quantity
+ battery_voltage = self._channel.cluster.get("battery_voltage")
+ if battery_voltage is not None:
+ state_attrs["battery_voltage"] = round(battery_voltage / 10, 1)
return state_attrs
- @callback
- def async_update_state_attribute(self, key, value):
- """Update a single device state attribute."""
- if key == "battery_voltage":
- self._device_state_attributes[key] = round(value / 10, 1)
- self.async_write_ha_state()
-
@STRICT_MATCH(channel_names=CHANNEL_ELECTRICAL_MEASUREMENT)
class ElectricalMeasurement(Sensor):
@@ -202,7 +196,6 @@ class ElectricalMeasurement(Sensor):
SENSOR_ATTR = "active_power"
_device_class = DEVICE_CLASS_POWER
- _divisor = 10
_unit = POWER_WATT
@property
@@ -210,7 +203,7 @@ class ElectricalMeasurement(Sensor):
"""Return True if HA needs to poll for state changes."""
return True
- def formatter(self, value) -> int:
+ def formatter(self, value: int) -> Union[int, float]:
"""Return 'normalized' value."""
value = value * self._channel.multiplier / self._channel.divisor
if value < 100 and self._channel.divisor > 1:
@@ -244,7 +237,7 @@ class Illuminance(Sensor):
_unit = LIGHT_LUX
@staticmethod
- def formatter(value):
+ def formatter(value: int) -> float:
"""Convert illumination data."""
return round(pow(10, ((value - 1) / 10000)), 1)
@@ -256,12 +249,12 @@ class SmartEnergyMetering(Sensor):
SENSOR_ATTR = "instantaneous_demand"
_device_class = DEVICE_CLASS_POWER
- def formatter(self, value):
+ def formatter(self, value: int) -> Union[int, float]:
"""Pass through channel formatter."""
return self._channel.formatter_function(value)
@property
- def unit_of_measurement(self):
+ def unit_of_measurement(self) -> str:
"""Return Unit of measurement."""
return self._channel.unit_of_measurement
@@ -284,14 +277,3 @@ class Temperature(Sensor):
_device_class = DEVICE_CLASS_TEMPERATURE
_divisor = 100
_unit = TEMP_CELSIUS
-
- @callback
- def async_restore_last_state(self, last_state):
- """Restore previous state."""
- if last_state.state == STATE_UNKNOWN:
- return
- if last_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) != TEMP_CELSIUS:
- ftemp = float(last_state.state)
- self._state = round(fahrenheit_to_celsius(ftemp), 1)
- return
- self._state = last_state.state
diff --git a/homeassistant/components/zha/translations/ru.json b/homeassistant/components/zha/translations/ru.json
index b97e27ced5d..13394e5a293 100644
--- a/homeassistant/components/zha/translations/ru.json
+++ b/homeassistant/components/zha/translations/ru.json
@@ -83,7 +83,7 @@
"remote_button_long_release": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u0434\u043e\u043b\u0433\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f",
"remote_button_quadruple_press": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430",
"remote_button_quintuple_press": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437",
- "remote_button_short_press": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430",
+ "remote_button_short_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430",
"remote_button_short_release": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f",
"remote_button_triple_press": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430"
}
diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py
index c3e1beb44af..01a8b9aa0f4 100644
--- a/homeassistant/components/zone/__init__.py
+++ b/homeassistant/components/zone/__init__.py
@@ -325,6 +325,11 @@ class Zone(entity.Entity):
"""Return the state attributes of the zone."""
return self._attrs
+ @property
+ def should_poll(self) -> bool:
+ """Zone does not poll."""
+ return False
+
async def async_update_config(self, config: Dict) -> None:
"""Handle when the config is updated."""
if self._config == config:
diff --git a/homeassistant/components/zoneminder/translations/hu.json b/homeassistant/components/zoneminder/translations/hu.json
new file mode 100644
index 00000000000..f1f99fa2f7c
--- /dev/null
+++ b/homeassistant/components/zoneminder/translations/hu.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "abort": {
+ "connection_error": "Nem siker\u00fclt csatlakozni a ZoneMinder szerverhez."
+ },
+ "create_entry": {
+ "default": "ZoneMinder szerver hozz\u00e1adva."
+ },
+ "error": {
+ "connection_error": "Nem siker\u00fclt csatlakozni a ZoneMinder szerverhez."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zoneminder/translations/sl.json b/homeassistant/components/zoneminder/translations/sl.json
new file mode 100644
index 00000000000..f05e5d6d329
--- /dev/null
+++ b/homeassistant/components/zoneminder/translations/sl.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "verify_ssl": "Preveri SSL certifikat"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/const.py b/homeassistant/const.py
index a1834fc4fe8..1e7d243b9ad 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -1,7 +1,7 @@
"""Constants used by Home Assistant components."""
-MAJOR_VERSION = 0
-MINOR_VERSION = 118
-PATCH_VERSION = "5"
+MAJOR_VERSION = 2020
+MINOR_VERSION = 12
+PATCH_VERSION = "0"
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER = (3, 7, 1)
@@ -128,6 +128,7 @@ CONF_NAME = "name"
CONF_OFFSET = "offset"
CONF_OPTIMISTIC = "optimistic"
CONF_PACKAGES = "packages"
+CONF_PARAMS = "params"
CONF_PASSWORD = "password"
CONF_PATH = "path"
CONF_PAYLOAD = "payload"
@@ -153,6 +154,7 @@ CONF_RGB = "rgb"
CONF_ROOM = "room"
CONF_SCAN_INTERVAL = "scan_interval"
CONF_SCENE = "scene"
+CONF_SELECTOR = "selector"
CONF_SENDER = "sender"
CONF_SENSORS = "sensors"
CONF_SENSOR_TYPE = "sensor_type"
@@ -168,6 +170,7 @@ CONF_STATE = "state"
CONF_STATE_TEMPLATE = "state_template"
CONF_STRUCTURE = "structure"
CONF_SWITCHES = "switches"
+CONF_TARGET = "target"
CONF_TEMPERATURE_UNIT = "temperature_unit"
CONF_TIMEOUT = "timeout"
CONF_TIME_ZONE = "time_zone"
diff --git a/homeassistant/core.py b/homeassistant/core.py
index ed8ae854106..9eeaf6fccca 100644
--- a/homeassistant/core.py
+++ b/homeassistant/core.py
@@ -257,12 +257,9 @@ class HomeAssistant:
fire_coroutine_threadsafe(self.async_start(), self.loop)
# Run forever
- try:
- # Block until stopped
- _LOGGER.info("Starting Home Assistant core loop")
- self.loop.run_forever()
- finally:
- self.loop.close()
+ # Block until stopped
+ _LOGGER.info("Starting Home Assistant core loop")
+ self.loop.run_forever()
return self.exit_code
async def async_run(self, *, attach_signals: bool = True) -> int:
@@ -422,7 +419,9 @@ class HomeAssistant:
self._track_task = False
@callback
- def async_run_hass_job(self, hassjob: HassJob, *args: Any) -> None:
+ def async_run_hass_job(
+ self, hassjob: HassJob, *args: Any
+ ) -> Optional[asyncio.Future]:
"""Run a HassJob from within the event loop.
This method must be run in the event loop.
@@ -432,13 +431,14 @@ class HomeAssistant:
"""
if hassjob.job_type == HassJobType.Callback:
hassjob.target(*args)
- else:
- self.async_add_hass_job(hassjob, *args)
+ return None
+
+ return self.async_add_hass_job(hassjob, *args)
@callback
def async_run_job(
self, target: Callable[..., Union[None, Awaitable]], *args: Any
- ) -> None:
+ ) -> Optional[asyncio.Future]:
"""Run a job from within the event loop.
This method must be run in the event loop.
@@ -447,10 +447,9 @@ class HomeAssistant:
args: parameters for method to call.
"""
if asyncio.iscoroutine(target):
- self.async_create_task(cast(Coroutine, target))
- return
+ return self.async_create_task(cast(Coroutine, target))
- self.async_run_hass_job(HassJob(target), *args)
+ return self.async_run_hass_job(HassJob(target), *args)
def block_till_done(self) -> None:
"""Block until all pending work is done."""
@@ -559,16 +558,11 @@ class HomeAssistant:
"Timed out waiting for shutdown stage 3 to complete, the shutdown will continue"
)
- # Python 3.9+ and backported in runner.py
- await self.loop.shutdown_default_executor() # type: ignore
-
self.exit_code = exit_code
self.state = CoreState.stopped
if self._stopped is not None:
self._stopped.set()
- else:
- self.loop.stop()
@attr.s(slots=True, frozen=True)
diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py
index 015e81f2ca0..e37f68a07bf 100644
--- a/homeassistant/exceptions.py
+++ b/homeassistant/exceptions.py
@@ -80,4 +80,4 @@ class ServiceNotFound(HomeAssistantError):
def __str__(self) -> str:
"""Return string representation."""
- return f"Unable to find service {self.domain}/{self.service}"
+ return f"Unable to find service {self.domain}.{self.service}"
diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py
index 3ac7fbae020..833f11190b6 100644
--- a/homeassistant/generated/config_flows.py
+++ b/homeassistant/generated/config_flows.py
@@ -18,9 +18,11 @@ FLOWS = [
"almond",
"ambiclimate",
"ambient_station",
+ "apple_tv",
"arcam_fmj",
"atag",
"august",
+ "aurora",
"avri",
"awair",
"axis",
@@ -57,6 +59,7 @@ FLOWS = [
"enocean",
"epson",
"esphome",
+ "fireservicerota",
"flick_electric",
"flo",
"flume",
@@ -90,6 +93,7 @@ FLOWS = [
"hue",
"hunterdouglas_powerview",
"hvv_departures",
+ "hyperion",
"iaqualink",
"icloud",
"ifttt",
@@ -104,6 +108,7 @@ FLOWS = [
"juicenet",
"kodi",
"konnected",
+ "kulersky",
"life360",
"lifx",
"local_ip",
@@ -120,6 +125,7 @@ FLOWS = [
"minecraft_server",
"mobile_app",
"monoprice",
+ "motion_blinds",
"mqtt",
"myq",
"neato",
@@ -156,6 +162,7 @@ FLOWS = [
"pvpc_hourly_pricing",
"rachio",
"rainmachine",
+ "recollect_waste",
"rfxtrx",
"ring",
"risco",
@@ -188,6 +195,7 @@ FLOWS = [
"spider",
"spotify",
"squeezebox",
+ "srp_energy",
"starline",
"syncthru",
"synology_dsm",
@@ -206,6 +214,7 @@ FLOWS = [
"tuya",
"twentemilieu",
"twilio",
+ "twinkly",
"unifi",
"upb",
"upcloud",
diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py
index f66c5f0999d..1617cd35435 100644
--- a/homeassistant/generated/ssdp.py
+++ b/homeassistant/generated/ssdp.py
@@ -114,6 +114,12 @@ SSDP = {
"modelName": "Philips hue bridge 2015"
}
],
+ "hyperion": [
+ {
+ "manufacturer": "Hyperion Open Source Ambient Lighting",
+ "st": "urn:hyperion-project.org:device:basic:1"
+ }
+ ],
"isy994": [
{
"deviceType": "urn:udi-com:device:X_Insteon_Lighting_Device:1",
diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py
index 60dcf04d9ed..6efa44e304f 100644
--- a/homeassistant/generated/zeroconf.py
+++ b/homeassistant/generated/zeroconf.py
@@ -90,6 +90,11 @@ ZEROCONF = {
"domain": "ipp"
}
],
+ "_mediaremotetv._tcp.local.": [
+ {
+ "domain": "apple_tv"
+ }
+ ],
"_miio._udp.local.": [
{
"domain": "xiaomi_aqara"
@@ -129,6 +134,11 @@ ZEROCONF = {
"name": "smappee2*"
}
],
+ "_touch-able._tcp.local.": [
+ {
+ "domain": "apple_tv"
+ }
+ ],
"_viziocast._tcp.local.": [
{
"domain": "vizio"
diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py
index b8f7952cd5a..1a919996f86 100644
--- a/homeassistant/helpers/area_registry.py
+++ b/homeassistant/helpers/area_registry.py
@@ -1,13 +1,13 @@
"""Provide a way to connect devices to one physical location."""
from asyncio import Event, gather
from collections import OrderedDict
-from typing import Dict, Iterable, List, MutableMapping, Optional, cast
+from typing import Container, Dict, Iterable, List, MutableMapping, Optional, cast
import attr
from homeassistant.core import callback
from homeassistant.loader import bind_hass
-import homeassistant.util.uuid as uuid_util
+from homeassistant.util import slugify
from .typing import HomeAssistantType
@@ -22,8 +22,17 @@ SAVE_DELAY = 10
class AreaEntry:
"""Area Registry Entry."""
- name: Optional[str] = attr.ib(default=None)
- id: str = attr.ib(factory=uuid_util.random_uuid_hex)
+ name: str = attr.ib()
+ id: Optional[str] = attr.ib(default=None)
+
+ def generate_id(self, existing_ids: Container) -> None:
+ """Initialize ID."""
+ suggestion = suggestion_base = slugify(self.name)
+ tries = 1
+ while suggestion in existing_ids:
+ tries += 1
+ suggestion = f"{suggestion_base}_{tries}"
+ object.__setattr__(self, "id", suggestion)
class AreaRegistry:
@@ -51,16 +60,15 @@ class AreaRegistry:
if self._async_is_registered(name):
raise ValueError("Name is already in use")
- area = AreaEntry()
+ area = AreaEntry(name=name)
+ area.generate_id(self.areas)
+ assert area.id is not None
self.areas[area.id] = area
-
- created = self._async_update(area.id, name=name)
-
+ self.async_schedule_save()
self.hass.bus.async_fire(
- EVENT_AREA_REGISTRY_UPDATED, {"action": "create", "area_id": created.id}
+ EVENT_AREA_REGISTRY_UPDATED, {"action": "create", "area_id": area.id}
)
-
- return created
+ return area
async def async_delete(self, area_id: str) -> None:
"""Delete area."""
diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py
index 7b71b7aae2b..c98b563ac7e 100644
--- a/homeassistant/helpers/check_config.py
+++ b/homeassistant/helpers/check_config.py
@@ -1,9 +1,9 @@
"""Helper to check the configuration file."""
from collections import OrderedDict
+import logging
import os
from typing import List, NamedTuple, Optional
-import attr
import voluptuous as vol
from homeassistant import loader
@@ -36,11 +36,13 @@ class CheckConfigError(NamedTuple):
config: Optional[ConfigType]
-@attr.s
class HomeAssistantConfig(OrderedDict):
"""Configuration result with errors attribute."""
- errors: List[CheckConfigError] = attr.ib(factory=list)
+ def __init__(self) -> None:
+ """Initialize HA config."""
+ super().__init__()
+ self.errors: List[CheckConfigError] = []
def add_error(
self,
@@ -123,6 +125,42 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> HomeAssistantConfig
result.add_error(f"Component error: {domain} - {ex}")
continue
+ # Check if the integration has a custom config validator
+ config_validator = None
+ try:
+ config_validator = integration.get_platform("config")
+ except ImportError as err:
+ # Filter out import error of the config platform.
+ # If the config platform contains bad imports, make sure
+ # that still fails.
+ if err.name != f"{integration.pkg_path}.config":
+ result.add_error(f"Error importing config platform {domain}: {err}")
+ continue
+
+ if config_validator is not None and hasattr(
+ config_validator, "async_validate_config"
+ ):
+ try:
+ result[domain] = (
+ await config_validator.async_validate_config( # type: ignore
+ hass, config
+ )
+ )[domain]
+ continue
+ except (vol.Invalid, HomeAssistantError) as ex:
+ _comp_error(ex, domain, config)
+ continue
+ except Exception as err: # pylint: disable=broad-except
+ logging.getLogger(__name__).exception(
+ "Unexpected error validating config"
+ )
+ result.add_error(
+ f"Unexpected error calling config validator: {err}",
+ domain,
+ config.get(domain),
+ )
+ continue
+
config_schema = getattr(component, "CONFIG_SCHEMA", None)
if config_schema is not None:
try:
diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py
index 9cfa6c00996..5ace4c91bcf 100644
--- a/homeassistant/helpers/condition.py
+++ b/homeassistant/helpers/condition.py
@@ -453,18 +453,12 @@ def async_template(
) -> bool:
"""Test if template condition matches."""
try:
- value = value_template.async_render(variables)
+ value: str = value_template.async_render(variables, parse_result=False)
except TemplateError as ex:
_LOGGER.error("Error during template condition: %s", ex)
return False
- if isinstance(value, bool):
- return value
-
- if isinstance(value, str):
- return value.lower() == "true"
-
- return False
+ return value.lower() == "true"
def async_template_from_config(
diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py
index 4d05ad7beab..526a774cc39 100644
--- a/homeassistant/helpers/config_entry_oauth2_flow.py
+++ b/homeassistant/helpers/config_entry_oauth2_flow.py
@@ -19,9 +19,9 @@ import voluptuous as vol
from yarl import URL
from homeassistant import config_entries
-from homeassistant.components.http import HomeAssistantView
+from homeassistant.components import http
from homeassistant.core import HomeAssistant, callback
-from homeassistant.helpers.network import NoURLAvailableError, get_url
+from homeassistant.helpers.network import NoURLAvailableError
from .aiohttp_client import async_get_clientsession
@@ -32,6 +32,7 @@ DATA_VIEW_REGISTERED = "oauth2_view_reg"
DATA_IMPLEMENTATIONS = "oauth2_impl"
DATA_PROVIDERS = "oauth2_providers"
AUTH_CALLBACK_PATH = "/auth/external/callback"
+HEADER_FRONTEND_BASE = "HA-Frontend-Base"
CLOCK_OUT_OF_SYNC_MAX_SEC = 20
@@ -64,7 +65,7 @@ class AbstractOAuth2Implementation(ABC):
Pass external data in with:
await hass.config_entries.flow.async_configure(
- flow_id=flow_id, user_input=external_data
+ flow_id=flow_id, user_input={'code': 'abcd', 'state': { … }
)
"""
@@ -124,7 +125,17 @@ class LocalOAuth2Implementation(AbstractOAuth2Implementation):
@property
def redirect_uri(self) -> str:
"""Return the redirect uri."""
- return f"{get_url(self.hass, require_current_request=True)}{AUTH_CALLBACK_PATH}"
+ req = http.current_request.get()
+
+ if req is None:
+ raise RuntimeError("No current request in context")
+
+ ha_host = req.headers.get(HEADER_FRONTEND_BASE)
+
+ if ha_host is None:
+ raise RuntimeError("No header in request")
+
+ return f"{ha_host}{AUTH_CALLBACK_PATH}"
@property
def extra_authorize_data(self) -> dict:
@@ -133,14 +144,17 @@ class LocalOAuth2Implementation(AbstractOAuth2Implementation):
async def async_generate_authorize_url(self, flow_id: str) -> str:
"""Generate a url for the user to authorize."""
+ redirect_uri = self.redirect_uri
return str(
URL(self.authorize_url)
.with_query(
{
"response_type": "code",
"client_id": self.client_id,
- "redirect_uri": self.redirect_uri,
- "state": _encode_jwt(self.hass, {"flow_id": flow_id}),
+ "redirect_uri": redirect_uri,
+ "state": _encode_jwt(
+ self.hass, {"flow_id": flow_id, "redirect_uri": redirect_uri}
+ ),
}
)
.update_query(self.extra_authorize_data)
@@ -151,8 +165,8 @@ class LocalOAuth2Implementation(AbstractOAuth2Implementation):
return await self._token_request(
{
"grant_type": "authorization_code",
- "code": external_data,
- "redirect_uri": self.redirect_uri,
+ "code": external_data["code"],
+ "redirect_uri": external_data["state"]["redirect_uri"],
}
)
@@ -384,7 +398,7 @@ def async_add_implementation_provider(
] = async_provide_implementation
-class OAuth2AuthorizeCallbackView(HomeAssistantView):
+class OAuth2AuthorizeCallbackView(http.HomeAssistantView):
"""OAuth2 Authorization Callback View."""
requires_auth = False
@@ -406,7 +420,8 @@ class OAuth2AuthorizeCallbackView(HomeAssistantView):
return web.Response(text="Invalid state")
await hass.config_entries.flow.async_configure(
- flow_id=state["flow_id"], user_input=request.query["code"]
+ flow_id=state["flow_id"],
+ user_input={"state": state, "code": request.query["code"]},
)
return web.Response(
diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py
index 5b2ad0da2ac..0513c5c6e7e 100644
--- a/homeassistant/helpers/config_validation.py
+++ b/homeassistant/helpers/config_validation.py
@@ -28,12 +28,12 @@ from typing import (
from urllib.parse import urlparse
from uuid import UUID
-from pkg_resources import parse_version
import voluptuous as vol
import voluptuous_serialize
from homeassistant.const import (
ATTR_AREA_ID,
+ ATTR_DEVICE_ID,
ATTR_ENTITY_ID,
CONF_ABOVE,
CONF_ALIAS,
@@ -62,6 +62,7 @@ from homeassistant.const import (
CONF_SERVICE,
CONF_SERVICE_TEMPLATE,
CONF_STATE,
+ CONF_TARGET,
CONF_TIMEOUT,
CONF_UNIT_SYSTEM_IMPERIAL,
CONF_UNIT_SYSTEM_METRIC,
@@ -78,7 +79,6 @@ from homeassistant.const import (
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
WEEKDAYS,
- __version__,
)
from homeassistant.core import split_entity_id, valid_entity_id
from homeassistant.exceptions import TemplateError
@@ -286,9 +286,12 @@ def entity_domain(domain: Union[str, List[str]]) -> Callable[[Any], str]:
"""Validate that entity belong to domain."""
ent_domain = entities_domain(domain)
- def validate(value: Any) -> str:
+ def validate(value: str) -> str:
"""Test if entity domain is domain."""
- return ent_domain(value)[0]
+ validated = ent_domain(value)
+ if len(validated) != 1:
+ raise vol.Invalid(f"Expected exactly 1 entity, got {len(validated)}")
+ return validated[0]
return validate
@@ -707,7 +710,6 @@ class multi_select:
def deprecated(
key: str,
replacement_key: Optional[str] = None,
- invalidation_version: Optional[str] = None,
default: Optional[Any] = None,
) -> Callable[[Dict], Dict]:
"""
@@ -720,8 +722,6 @@ def deprecated(
- No warning if only replacement_key provided
- No warning if neither key nor replacement_key are provided
- Adds replacement_key with default value in this case
- - Once the invalidation_version is crossed, raises vol.Invalid if key
- is detected
"""
module = inspect.getmodule(inspect.stack()[1][0])
if module is not None:
@@ -732,56 +732,24 @@ def deprecated(
# https://github.com/home-assistant/core/issues/24982
module_name = __name__
- if replacement_key and invalidation_version:
- warning = (
- "The '{key}' option is deprecated,"
- " please replace it with '{replacement_key}'."
- " This option {invalidation_status} invalid in version"
- " {invalidation_version}"
- )
- elif replacement_key:
+ if replacement_key:
warning = (
"The '{key}' option is deprecated,"
" please replace it with '{replacement_key}'"
)
- elif invalidation_version:
- warning = (
- "The '{key}' option is deprecated,"
- " please remove it from your configuration."
- " This option {invalidation_status} invalid in version"
- " {invalidation_version}"
- )
else:
warning = (
"The '{key}' option is deprecated,"
" please remove it from your configuration"
)
- def check_for_invalid_version() -> None:
- """Raise error if current version has reached invalidation."""
- if not invalidation_version:
- return
-
- if parse_version(__version__) >= parse_version(invalidation_version):
- raise vol.Invalid(
- warning.format(
- key=key,
- replacement_key=replacement_key,
- invalidation_status="became",
- invalidation_version=invalidation_version,
- )
- )
-
def validator(config: Dict) -> Dict:
"""Check if key is in config and log warning."""
if key in config:
- check_for_invalid_version()
KeywordStyleAdapter(logging.getLogger(module_name)).warning(
warning,
key=key,
replacement_key=replacement_key,
- invalidation_status="will become",
- invalidation_version=invalidation_version,
)
value = config[key]
@@ -878,7 +846,13 @@ PLATFORM_SCHEMA = vol.Schema(
PLATFORM_SCHEMA_BASE = PLATFORM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA)
-ENTITY_SERVICE_FIELDS = (ATTR_ENTITY_ID, ATTR_AREA_ID)
+ENTITY_SERVICE_FIELDS = {
+ vol.Optional(ATTR_ENTITY_ID): comp_entity_ids,
+ vol.Optional(ATTR_DEVICE_ID): vol.Any(
+ ENTITY_MATCH_NONE, vol.All(ensure_list, [str])
+ ),
+ vol.Optional(ATTR_AREA_ID): vol.Any(ENTITY_MATCH_NONE, vol.All(ensure_list, [str])),
+}
def make_entity_service_schema(
@@ -889,10 +863,7 @@ def make_entity_service_schema(
vol.Schema(
{
**schema,
- vol.Optional(ATTR_ENTITY_ID): comp_entity_ids,
- vol.Optional(ATTR_AREA_ID): vol.Any(
- ENTITY_MATCH_NONE, vol.All(ensure_list, [str])
- ),
+ **ENTITY_SERVICE_FIELDS,
},
extra=extra,
),
@@ -939,6 +910,7 @@ SERVICE_SCHEMA = vol.All(
vol.Optional("data"): vol.All(dict, template_complex),
vol.Optional("data_template"): vol.All(dict, template_complex),
vol.Optional(CONF_ENTITY_ID): comp_entity_ids,
+ vol.Optional(CONF_TARGET): ENTITY_SERVICE_FIELDS,
}
),
has_at_least_one_key(CONF_SERVICE, CONF_SERVICE_TEMPLATE),
diff --git a/homeassistant/helpers/debounce.py b/homeassistant/helpers/debounce.py
index 988ea9f0051..23727c2a00f 100644
--- a/homeassistant/helpers/debounce.py
+++ b/homeassistant/helpers/debounce.py
@@ -48,7 +48,7 @@ class Debouncer:
async def async_call(self) -> None:
"""Call the function."""
- assert self.function is not None
+ assert self._job is not None
if self._timer_task:
if not self._execute_at_end_of_timer:
@@ -70,13 +70,15 @@ class Debouncer:
if self._timer_task:
return
- await self.hass.async_add_hass_job(self._job) # type: ignore
+ task = self.hass.async_run_hass_job(self._job)
+ if task:
+ await task
self._schedule_timer()
async def _handle_timer_finish(self) -> None:
"""Handle a finished timer."""
- assert self.function is not None
+ assert self._job is not None
self._timer_task = None
@@ -95,7 +97,9 @@ class Debouncer:
return # type: ignore
try:
- await self.hass.async_add_hass_job(self._job) # type: ignore
+ task = self.hass.async_run_hass_job(self._job)
+ if task:
+ await task
except Exception: # pylint: disable=broad-except
self.logger.exception("Unexpected exception from %s", self.function)
diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py
index 388db62ebae..cc8f9a17827 100644
--- a/homeassistant/helpers/device_registry.py
+++ b/homeassistant/helpers/device_registry.py
@@ -37,6 +37,9 @@ IDX_IDENTIFIERS = "identifiers"
REGISTERED_DEVICE = "registered"
DELETED_DEVICE = "deleted"
+DISABLED_INTEGRATION = "integration"
+DISABLED_USER = "user"
+
@attr.s(slots=True, frozen=True)
class DeletedDeviceEntry:
@@ -76,6 +79,21 @@ class DeviceEntry:
id: str = attr.ib(factory=uuid_util.random_uuid_hex)
# This value is not stored, just used to keep track of events to fire.
is_new: bool = attr.ib(default=False)
+ disabled_by: Optional[str] = attr.ib(
+ default=None,
+ validator=attr.validators.in_(
+ (
+ DISABLED_INTEGRATION,
+ DISABLED_USER,
+ None,
+ )
+ ),
+ )
+
+ @property
+ def disabled(self) -> bool:
+ """Return if entry is disabled."""
+ return self.disabled_by is not None
def format_mac(mac: str) -> str:
@@ -215,6 +233,8 @@ class DeviceRegistry:
sw_version=_UNDEF,
entry_type=_UNDEF,
via_device=None,
+ # To disable a device if it gets created
+ disabled_by=_UNDEF,
):
"""Get device. Create if it doesn't exist."""
if not identifiers and not connections:
@@ -267,6 +287,7 @@ class DeviceRegistry:
name=name,
sw_version=sw_version,
entry_type=entry_type,
+ disabled_by=disabled_by,
)
@callback
@@ -283,6 +304,7 @@ class DeviceRegistry:
sw_version=_UNDEF,
via_device_id=_UNDEF,
remove_config_entry_id=_UNDEF,
+ disabled_by=_UNDEF,
):
"""Update properties of a device."""
return self._async_update_device(
@@ -296,6 +318,7 @@ class DeviceRegistry:
sw_version=sw_version,
via_device_id=via_device_id,
remove_config_entry_id=remove_config_entry_id,
+ disabled_by=disabled_by,
)
@callback
@@ -316,6 +339,7 @@ class DeviceRegistry:
via_device_id=_UNDEF,
area_id=_UNDEF,
name_by_user=_UNDEF,
+ disabled_by=_UNDEF,
):
"""Update device attributes."""
old = self.devices[device_id]
@@ -362,6 +386,7 @@ class DeviceRegistry:
("sw_version", sw_version),
("entry_type", entry_type),
("via_device_id", via_device_id),
+ ("disabled_by", disabled_by),
):
if value is not _UNDEF and value != getattr(old, attr_name):
changes[attr_name] = value
@@ -440,6 +465,8 @@ class DeviceRegistry:
# Introduced in 0.87
area_id=device.get("area_id"),
name_by_user=device.get("name_by_user"),
+ # Introduced in 0.119
+ disabled_by=device.get("disabled_by"),
)
# Introduced in 0.111
for device in data.get("deleted_devices", []):
@@ -478,6 +505,7 @@ class DeviceRegistry:
"via_device_id": entry.via_device_id,
"area_id": entry.area_id,
"name_by_user": entry.name_by_user,
+ "disabled_by": entry.disabled_by,
}
for entry in self.devices.values()
]
diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py
index d41e8174bc9..acde8d73a50 100644
--- a/homeassistant/helpers/discovery.py
+++ b/homeassistant/helpers/discovery.py
@@ -44,13 +44,14 @@ def async_listen(
job = core.HassJob(callback)
- @core.callback
- def discovery_event_listener(event: core.Event) -> None:
+ async def discovery_event_listener(event: core.Event) -> None:
"""Listen for discovery events."""
if ATTR_SERVICE in event.data and event.data[ATTR_SERVICE] in service:
- hass.async_add_hass_job(
+ task = hass.async_run_hass_job(
job, event.data[ATTR_SERVICE], event.data.get(ATTR_DISCOVERED)
)
+ if task:
+ await task
hass.bus.async_listen(EVENT_PLATFORM_DISCOVERED, discovery_event_listener)
@@ -114,8 +115,7 @@ def async_listen_platform(
service = EVENT_LOAD_PLATFORM.format(component)
job = core.HassJob(callback)
- @core.callback
- def discovery_platform_listener(event: core.Event) -> None:
+ async def discovery_platform_listener(event: core.Event) -> None:
"""Listen for platform discovery events."""
if event.data.get(ATTR_SERVICE) != service:
return
@@ -125,7 +125,9 @@ def async_listen_platform(
if not platform:
return
- hass.async_run_hass_job(job, platform, event.data.get(ATTR_DISCOVERED))
+ task = hass.async_run_hass_job(job, platform, event.data.get(ATTR_DISCOVERED))
+ if task:
+ await task
hass.bus.async_listen(EVENT_PLATFORM_DISCOVERED, discovery_platform_listener)
diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py
index b9d073dbe2e..0f1f04e3aec 100644
--- a/homeassistant/helpers/entity_component.py
+++ b/homeassistant/helpers/entity_component.py
@@ -192,7 +192,7 @@ class EntityComponent:
self,
name: str,
schema: Union[Dict[str, Any], vol.Schema],
- func: str,
+ func: Union[str, Callable[..., Any]],
required_features: Optional[List[int]] = None,
) -> None:
"""Register an entity service."""
diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py
index 872d87e732f..4582fc5f3b6 100644
--- a/homeassistant/helpers/entity_registry.py
+++ b/homeassistant/helpers/entity_registry.py
@@ -53,9 +53,10 @@ SAVE_DELAY = 10
_LOGGER = logging.getLogger(__name__)
_UNDEF = object()
DISABLED_CONFIG_ENTRY = "config_entry"
+DISABLED_DEVICE = "device"
DISABLED_HASS = "hass"
-DISABLED_USER = "user"
DISABLED_INTEGRATION = "integration"
+DISABLED_USER = "user"
STORAGE_VERSION = 1
STORAGE_KEY = "core.entity_registry"
@@ -89,10 +90,11 @@ class RegistryEntry:
default=None,
validator=attr.validators.in_(
(
- DISABLED_HASS,
- DISABLED_USER,
- DISABLED_INTEGRATION,
DISABLED_CONFIG_ENTRY,
+ DISABLED_DEVICE,
+ DISABLED_HASS,
+ DISABLED_INTEGRATION,
+ DISABLED_USER,
None,
)
),
@@ -127,7 +129,7 @@ class EntityRegistry:
self._index: Dict[Tuple[str, str, str], str] = {}
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
self.hass.bus.async_listen(
- EVENT_DEVICE_REGISTRY_UPDATED, self.async_device_removed
+ EVENT_DEVICE_REGISTRY_UPDATED, self.async_device_modified
)
@callback
@@ -286,18 +288,45 @@ class EntityRegistry:
)
self.async_schedule_save()
- @callback
- def async_device_removed(self, event: Event) -> None:
- """Handle the removal of a device.
+ async def async_device_modified(self, event: Event) -> None:
+ """Handle the removal or update of a device.
Remove entities from the registry that are associated to a device when
the device is removed.
+
+ Disable entities in the registry that are associated to a device when
+ the device is disabled.
"""
- if event.data["action"] != "remove":
+ if event.data["action"] == "remove":
+ entities = async_entries_for_device(
+ self, event.data["device_id"], include_disabled_entities=True
+ )
+ for entity in entities:
+ self.async_remove(entity.entity_id)
return
+
+ if event.data["action"] != "update":
+ return
+
+ device_registry = await self.hass.helpers.device_registry.async_get_registry()
+ device = device_registry.async_get(event.data["device_id"])
+ if not device.disabled:
+ entities = async_entries_for_device(
+ self, event.data["device_id"], include_disabled_entities=True
+ )
+ for entity in entities:
+ if entity.disabled_by != DISABLED_DEVICE:
+ continue
+ self.async_update_entity( # type: ignore
+ entity.entity_id, disabled_by=None
+ )
+ return
+
entities = async_entries_for_device(self, event.data["device_id"])
for entity in entities:
- self.async_remove(entity.entity_id)
+ self.async_update_entity( # type: ignore
+ entity.entity_id, disabled_by=DISABLED_DEVICE
+ )
@callback
def async_update_entity(
@@ -530,11 +559,14 @@ async def async_get_registry(hass: HomeAssistantType) -> EntityRegistry:
@callback
def async_entries_for_device(
- registry: EntityRegistry, device_id: str
+ registry: EntityRegistry, device_id: str, include_disabled_entities: bool = False
) -> List[RegistryEntry]:
"""Return entries that match a device."""
return [
- entry for entry in registry.entities.values() if entry.device_id == device_id
+ entry
+ for entry in registry.entities.values()
+ if entry.device_id == device_id
+ and (not entry.disabled_by or include_disabled_entities)
]
diff --git a/homeassistant/helpers/network.py b/homeassistant/helpers/network.py
index 3990662dc02..4e066eaa13c 100644
--- a/homeassistant/helpers/network.py
+++ b/homeassistant/helpers/network.py
@@ -4,7 +4,7 @@ from typing import Optional, cast
import yarl
-from homeassistant.components.http import current_request
+from homeassistant.components import http
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.loader import bind_hass
@@ -49,7 +49,7 @@ def get_url(
prefer_cloud: bool = False,
) -> str:
"""Get a URL to this instance."""
- if require_current_request and current_request.get() is None:
+ if require_current_request and http.current_request.get() is None:
raise NoURLAvailableError
order = [TYPE_URL_INTERNAL, TYPE_URL_EXTERNAL]
@@ -125,7 +125,7 @@ def get_url(
def _get_request_host() -> Optional[str]:
"""Get the host address of the current request."""
- request = current_request.get()
+ request = http.current_request.get()
if request is None:
raise NoURLAvailableError
return yarl.URL(request.url).host
diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py
index 645131b60b5..48a662e3a81 100644
--- a/homeassistant/helpers/script.py
+++ b/homeassistant/helpers/script.py
@@ -22,10 +22,10 @@ from async_timeout import timeout
import voluptuous as vol
from homeassistant import exceptions
-import homeassistant.components.device_automation as device_automation
+from homeassistant.components import device_automation, scene
from homeassistant.components.logger import LOGSEVERITY
-import homeassistant.components.scene as scene
from homeassistant.const import (
+ ATTR_DEVICE_ID,
ATTR_ENTITY_ID,
CONF_ALIAS,
CONF_CHOOSE,
@@ -44,6 +44,7 @@ from homeassistant.const import (
CONF_REPEAT,
CONF_SCENE,
CONF_SEQUENCE,
+ CONF_TARGET,
CONF_TIMEOUT,
CONF_UNTIL,
CONF_VARIABLES,
@@ -60,13 +61,9 @@ from homeassistant.core import (
HomeAssistant,
callback,
)
-from homeassistant.helpers import condition, config_validation as cv, template
+from homeassistant.helpers import condition, config_validation as cv, service, template
from homeassistant.helpers.event import async_call_later, async_track_template
from homeassistant.helpers.script_variables import ScriptVariables
-from homeassistant.helpers.service import (
- CONF_SERVICE_DATA,
- async_prepare_call_from_config,
-)
from homeassistant.helpers.trigger import (
async_initialize_triggers,
async_validate_trigger_config,
@@ -429,13 +426,13 @@ class _ScriptRun:
self._script.last_action = self._action.get(CONF_ALIAS, "call service")
self._log("Executing step %s", self._script.last_action)
- domain, service, service_data = async_prepare_call_from_config(
+ domain, service_name, service_data = service.async_prepare_call_from_config(
self._hass, self._action, self._variables
)
running_script = (
domain == "automation"
- and service == "trigger"
+ and service_name == "trigger"
or domain in ("python_script", "script")
)
# If this might start a script then disable the call timeout.
@@ -448,7 +445,7 @@ class _ScriptRun:
service_task = self._hass.async_create_task(
self._hass.services.async_call(
domain,
- service,
+ service_name,
service_data,
blocking=True,
context=self._context,
@@ -755,6 +752,23 @@ async def _async_stop_scripts_at_shutdown(hass, event):
_VarsType = Union[Dict[str, Any], MappingProxyType]
+def _referenced_extract_ids(data: Dict, key: str, found: Set[str]) -> None:
+ """Extract referenced IDs."""
+ if not data:
+ return
+
+ item_ids = data.get(key)
+
+ if item_ids is None or isinstance(item_ids, template.Template):
+ return
+
+ if isinstance(item_ids, str):
+ item_ids = [item_ids]
+
+ for item_id in item_ids:
+ found.add(item_id)
+
+
class Script:
"""Representation of a script."""
@@ -889,7 +903,16 @@ class Script:
for step in self.sequence:
action = cv.determine_script_action(step)
- if action == cv.SCRIPT_ACTION_CHECK_CONDITION:
+ if action == cv.SCRIPT_ACTION_CALL_SERVICE:
+ for data in (
+ step,
+ step.get(CONF_TARGET),
+ step.get(service.CONF_SERVICE_DATA),
+ step.get(service.CONF_SERVICE_DATA_TEMPLATE),
+ ):
+ _referenced_extract_ids(data, ATTR_DEVICE_ID, referenced)
+
+ elif action == cv.SCRIPT_ACTION_CHECK_CONDITION:
referenced |= condition.async_extract_devices(step)
elif action == cv.SCRIPT_ACTION_DEVICE_AUTOMATION:
@@ -910,20 +933,13 @@ class Script:
action = cv.determine_script_action(step)
if action == cv.SCRIPT_ACTION_CALL_SERVICE:
- data = step.get(CONF_SERVICE_DATA)
- if not data:
- continue
-
- entity_ids = data.get(ATTR_ENTITY_ID)
-
- if entity_ids is None or isinstance(entity_ids, template.Template):
- continue
-
- if isinstance(entity_ids, str):
- entity_ids = [entity_ids]
-
- for entity_id in entity_ids:
- referenced.add(entity_id)
+ for data in (
+ step,
+ step.get(CONF_TARGET),
+ step.get(service.CONF_SERVICE_DATA),
+ step.get(service.CONF_SERVICE_DATA_TEMPLATE),
+ ):
+ _referenced_extract_ids(data, ATTR_ENTITY_ID, referenced)
elif action == cv.SCRIPT_ACTION_CHECK_CONDITION:
referenced |= condition.async_extract_entities(step)
diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py
new file mode 100644
index 00000000000..5cc7ada1bc5
--- /dev/null
+++ b/homeassistant/helpers/selector.py
@@ -0,0 +1,164 @@
+"""Selectors for Home Assistant."""
+from typing import Any, Callable, Dict, cast
+
+import voluptuous as vol
+
+from homeassistant.const import CONF_MODE, CONF_UNIT_OF_MEASUREMENT
+from homeassistant.util import decorator
+
+SELECTORS = decorator.Registry()
+
+
+def validate_selector(config: Any) -> Dict:
+ """Validate a selector."""
+ if not isinstance(config, dict):
+ raise vol.Invalid("Expected a dictionary")
+
+ if len(config) != 1:
+ raise vol.Invalid(f"Only one type can be specified. Found {', '.join(config)}")
+
+ selector_type = list(config)[0]
+
+ selector_class = SELECTORS.get(selector_type)
+
+ if selector_class is None:
+ raise vol.Invalid(f"Unknown selector type {selector_type} found")
+
+ # Selectors can be empty
+ if config[selector_type] is None:
+ return {selector_type: {}}
+
+ return {
+ selector_type: cast(Dict, selector_class.CONFIG_SCHEMA(config[selector_type]))
+ }
+
+
+class Selector:
+ """Base class for selectors."""
+
+ CONFIG_SCHEMA: Callable
+
+
+@SELECTORS.register("entity")
+class EntitySelector(Selector):
+ """Selector of a single entity."""
+
+ CONFIG_SCHEMA = vol.Schema(
+ {
+ # Integration that provided the entity
+ vol.Optional("integration"): str,
+ # Domain the entity belongs to
+ vol.Optional("domain"): str,
+ # Device class of the entity
+ vol.Optional("device_class"): str,
+ }
+ )
+
+
+@SELECTORS.register("device")
+class DeviceSelector(Selector):
+ """Selector of a single device."""
+
+ CONFIG_SCHEMA = vol.Schema(
+ {
+ # Integration linked to it with a config entry
+ vol.Optional("integration"): str,
+ # Manufacturer of device
+ vol.Optional("manufacturer"): str,
+ # Model of device
+ vol.Optional("model"): str,
+ # Device has to contain entities matching this selector
+ vol.Optional(
+ "entity"
+ ): EntitySelector.CONFIG_SCHEMA, # pylint: disable=no-member
+ }
+ )
+
+
+@SELECTORS.register("area")
+class AreaSelector(Selector):
+ """Selector of a single area."""
+
+ CONFIG_SCHEMA = vol.Schema(
+ {
+ vol.Optional("entity"): vol.Schema(
+ {
+ vol.Optional("domain"): str,
+ vol.Optional("device_class"): str,
+ vol.Optional("integration"): str,
+ }
+ ),
+ vol.Optional("device"): vol.Schema(
+ {
+ vol.Optional("integration"): str,
+ vol.Optional("manufacturer"): str,
+ vol.Optional("model"): str,
+ }
+ ),
+ }
+ )
+
+
+@SELECTORS.register("number")
+class NumberSelector(Selector):
+ """Selector of a numeric value."""
+
+ CONFIG_SCHEMA = vol.Schema(
+ {
+ vol.Required("min"): vol.Coerce(float),
+ vol.Required("max"): vol.Coerce(float),
+ vol.Optional("step", default=1): vol.All(
+ vol.Coerce(float), vol.Range(min=1e-3)
+ ),
+ vol.Optional(CONF_UNIT_OF_MEASUREMENT): str,
+ vol.Optional(CONF_MODE, default="slider"): vol.In(["box", "slider"]),
+ }
+ )
+
+
+@SELECTORS.register("boolean")
+class BooleanSelector(Selector):
+ """Selector of a boolean value."""
+
+ CONFIG_SCHEMA = vol.Schema({})
+
+
+@SELECTORS.register("time")
+class TimeSelector(Selector):
+ """Selector of a time value."""
+
+ CONFIG_SCHEMA = vol.Schema({})
+
+
+@SELECTORS.register("target")
+class TargetSelector(Selector):
+ """Selector of a target value (area ID, device ID, entity ID etc).
+
+ Value should follow cv.ENTITY_SERVICE_FIELDS format.
+ """
+
+ CONFIG_SCHEMA = vol.Schema(
+ {
+ vol.Optional("entity"): vol.Schema(
+ {
+ vol.Optional("domain"): str,
+ vol.Optional("device_class"): str,
+ vol.Optional("integration"): str,
+ }
+ ),
+ vol.Optional("device"): vol.Schema(
+ {
+ vol.Optional("integration"): str,
+ vol.Optional("manufacturer"): str,
+ vol.Optional("model"): str,
+ }
+ ),
+ }
+ )
+
+
+@SELECTORS.register("action")
+class ActionSelector(Selector):
+ """Selector of an action sequence (script syntax)."""
+
+ CONFIG_SCHEMA = vol.Schema({})
diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py
index 06d0ae46ae3..25a88bb59cb 100644
--- a/homeassistant/helpers/service.py
+++ b/homeassistant/helpers/service.py
@@ -1,5 +1,6 @@
"""Service calling related helpers."""
import asyncio
+import dataclasses
from functools import partial, wraps
import logging
from typing import (
@@ -14,6 +15,7 @@ from typing import (
Set,
Tuple,
Union,
+ cast,
)
import voluptuous as vol
@@ -21,9 +23,11 @@ import voluptuous as vol
from homeassistant.auth.permissions.const import CAT_ENTITIES, POLICY_CONTROL
from homeassistant.const import (
ATTR_AREA_ID,
+ ATTR_DEVICE_ID,
ATTR_ENTITY_ID,
CONF_SERVICE,
CONF_SERVICE_TEMPLATE,
+ CONF_TARGET,
ENTITY_MATCH_ALL,
ENTITY_MATCH_NONE,
)
@@ -34,9 +38,13 @@ from homeassistant.exceptions import (
Unauthorized,
UnknownUser,
)
-from homeassistant.helpers import template
-import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.template import Template
+from homeassistant.helpers import (
+ area_registry,
+ config_validation as cv,
+ device_registry,
+ entity_registry,
+ template,
+)
from homeassistant.helpers.typing import ConfigType, HomeAssistantType, TemplateVarsType
from homeassistant.loader import (
MAX_LOAD_CONCURRENTLY,
@@ -62,6 +70,38 @@ _LOGGER = logging.getLogger(__name__)
SERVICE_DESCRIPTION_CACHE = "service_description_cache"
+@dataclasses.dataclass
+class SelectedEntities:
+ """Class to hold the selected entities."""
+
+ # Entities that were explicitly mentioned.
+ referenced: Set[str] = dataclasses.field(default_factory=set)
+
+ # Entities that were referenced via device/area ID.
+ # Should not trigger a warning when they don't exist.
+ indirectly_referenced: Set[str] = dataclasses.field(default_factory=set)
+
+ # Referenced items that could not be found.
+ missing_devices: Set[str] = dataclasses.field(default_factory=set)
+ missing_areas: Set[str] = dataclasses.field(default_factory=set)
+
+ def log_missing(self, missing_entities: Set[str]) -> None:
+ """Log about missing items."""
+ parts = []
+ for label, items in (
+ ("areas", self.missing_areas),
+ ("devices", self.missing_devices),
+ ("entities", missing_entities),
+ ):
+ if items:
+ parts.append(f"{label} {', '.join(sorted(items))}")
+
+ if not parts:
+ return
+
+ _LOGGER.warning("Unable to find referenced %s", ", ".join(parts))
+
+
@bind_hass
def call_from_config(
hass: HomeAssistantType,
@@ -119,7 +159,7 @@ def async_prepare_call_from_config(
else:
domain_service = config[CONF_SERVICE_TEMPLATE]
- if isinstance(domain_service, Template):
+ if isinstance(domain_service, template.Template):
try:
domain_service.hass = hass
domain_service = domain_service.async_render(variables)
@@ -136,6 +176,10 @@ def async_prepare_call_from_config(
domain, service = domain_service.split(".", 1)
service_data = {}
+
+ if CONF_TARGET in config:
+ service_data.update(config[CONF_TARGET])
+
for conf in [CONF_SERVICE_DATA, CONF_SERVICE_DATA_TEMPLATE]:
if conf not in config:
continue
@@ -180,25 +224,25 @@ async def async_extract_entities(
if data_ent_id == ENTITY_MATCH_ALL:
return [entity for entity in entities if entity.available]
- entity_ids = await async_extract_entity_ids(hass, service_call, expand_group)
+ referenced = await async_extract_referenced_entity_ids(
+ hass, service_call, expand_group
+ )
+ combined = referenced.referenced | referenced.indirectly_referenced
found = []
for entity in entities:
- if entity.entity_id not in entity_ids:
+ if entity.entity_id not in combined:
continue
- entity_ids.remove(entity.entity_id)
+ combined.remove(entity.entity_id)
if not entity.available:
continue
found.append(entity)
- if entity_ids:
- _LOGGER.warning(
- "Unable to find referenced entities %s", ", ".join(sorted(entity_ids))
- )
+ referenced.log_missing(referenced.referenced & combined)
return found
@@ -207,22 +251,37 @@ async def async_extract_entities(
async def async_extract_entity_ids(
hass: HomeAssistantType, service_call: ha.ServiceCall, expand_group: bool = True
) -> Set[str]:
- """Extract a list of entity ids from a service call.
+ """Extract a set of entity ids from a service call.
Will convert group entity ids to the entity ids it represents.
"""
+ referenced = await async_extract_referenced_entity_ids(
+ hass, service_call, expand_group
+ )
+ return referenced.referenced | referenced.indirectly_referenced
+
+
+@bind_hass
+async def async_extract_referenced_entity_ids(
+ hass: HomeAssistantType, service_call: ha.ServiceCall, expand_group: bool = True
+) -> SelectedEntities:
+ """Extract referenced entity IDs from a service call."""
entity_ids = service_call.data.get(ATTR_ENTITY_ID)
+ device_ids = service_call.data.get(ATTR_DEVICE_ID)
area_ids = service_call.data.get(ATTR_AREA_ID)
- extracted: Set[str] = set()
+ selects_entity_ids = entity_ids not in (None, ENTITY_MATCH_NONE)
+ selects_device_ids = device_ids not in (None, ENTITY_MATCH_NONE)
+ selects_area_ids = area_ids not in (None, ENTITY_MATCH_NONE)
- if entity_ids in (None, ENTITY_MATCH_NONE) and area_ids in (
- None,
- ENTITY_MATCH_NONE,
- ):
- return extracted
+ selected = SelectedEntities()
+
+ if not selects_entity_ids and not selects_device_ids and not selects_area_ids:
+ return selected
+
+ if selects_entity_ids:
+ assert entity_ids is not None
- if entity_ids and entity_ids != ENTITY_MATCH_NONE:
# Entity ID attr can be a list or a string
if isinstance(entity_ids, str):
entity_ids = [entity_ids]
@@ -230,42 +289,68 @@ async def async_extract_entity_ids(
if expand_group:
entity_ids = hass.components.group.expand_entity_ids(entity_ids)
- extracted.update(entity_ids)
+ selected.referenced.update(entity_ids)
+
+ if not selects_device_ids and not selects_area_ids:
+ return selected
+
+ area_reg, dev_reg, ent_reg = cast(
+ Tuple[
+ area_registry.AreaRegistry,
+ device_registry.DeviceRegistry,
+ entity_registry.EntityRegistry,
+ ],
+ await asyncio.gather(
+ area_registry.async_get_registry(hass),
+ device_registry.async_get_registry(hass),
+ entity_registry.async_get_registry(hass),
+ ),
+ )
+
+ picked_devices = set()
+
+ if selects_device_ids:
+ if isinstance(device_ids, str):
+ picked_devices = {device_ids}
+ else:
+ assert isinstance(device_ids, list)
+ picked_devices = set(device_ids)
+
+ for device_id in picked_devices:
+ if device_id not in dev_reg.devices:
+ selected.missing_devices.add(device_id)
+
+ if selects_area_ids:
+ assert area_ids is not None
- if area_ids and area_ids != ENTITY_MATCH_NONE:
if isinstance(area_ids, str):
- area_ids = [area_ids]
+ area_lookup = {area_ids}
+ else:
+ area_lookup = set(area_ids)
- dev_reg, ent_reg = await asyncio.gather(
- hass.helpers.device_registry.async_get_registry(),
- hass.helpers.entity_registry.async_get_registry(),
- )
+ for area_id in area_lookup:
+ if area_id not in area_reg.areas:
+ selected.missing_areas.add(area_id)
+ continue
- extracted.update(
- entry.entity_id
- for area_id in area_ids
- for entry in hass.helpers.entity_registry.async_entries_for_area(
- ent_reg, area_id
- )
- )
+ # Find entities tied to an area
+ for entity_entry in ent_reg.entities.values():
+ if entity_entry.area_id in area_lookup:
+ selected.indirectly_referenced.add(entity_entry.entity_id)
- devices = [
- device
- for area_id in area_ids
- for device in hass.helpers.device_registry.async_entries_for_area(
- dev_reg, area_id
- )
- ]
- extracted.update(
- entry.entity_id
- for device in devices
- for entry in hass.helpers.entity_registry.async_entries_for_device(
- ent_reg, device.id
- )
- if not entry.area_id
- )
+ # Find devices for this area
+ for device_entry in dev_reg.devices.values():
+ if device_entry.area_id in area_lookup:
+ picked_devices.add(device_entry.id)
- return extracted
+ if not picked_devices:
+ return selected
+
+ for entity_entry in ent_reg.entities.values():
+ if not entity_entry.area_id and entity_entry.device_id in picked_devices:
+ selected.indirectly_referenced.add(entity_entry.entity_id)
+
+ return selected
def _load_services_file(hass: HomeAssistantType, integration: Integration) -> JSON_TYPE:
@@ -392,9 +477,13 @@ async def entity_service_call(
target_all_entities = call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL
- if not target_all_entities:
+ if target_all_entities:
+ referenced: Optional[SelectedEntities] = None
+ all_referenced: Optional[Set[str]] = None
+ else:
# A set of entities we're trying to target.
- entity_ids = await async_extract_entity_ids(hass, call, True)
+ referenced = await async_extract_referenced_entity_ids(hass, call, True)
+ all_referenced = referenced.referenced | referenced.indirectly_referenced
# If the service function is a string, we'll pass it the service call data
if isinstance(func, str):
@@ -417,11 +506,12 @@ async def entity_service_call(
if target_all_entities:
entity_candidates.extend(platform.entities.values())
else:
+ assert all_referenced is not None
entity_candidates.extend(
[
entity
for entity in platform.entities.values()
- if entity.entity_id in entity_ids
+ if entity.entity_id in all_referenced
]
)
@@ -438,11 +528,13 @@ async def entity_service_call(
)
else:
+ assert all_referenced is not None
+
for platform in platforms:
platform_entities = []
for entity in platform.entities.values():
- if entity.entity_id not in entity_ids:
+ if entity.entity_id not in all_referenced:
continue
if not entity_perms(entity.entity_id, POLICY_CONTROL):
@@ -457,13 +549,15 @@ async def entity_service_call(
entity_candidates.extend(platform_entities)
if not target_all_entities:
- for entity in entity_candidates:
- entity_ids.remove(entity.entity_id)
+ assert referenced is not None
- if entity_ids:
- _LOGGER.warning(
- "Unable to find referenced entities %s", ", ".join(sorted(entity_ids))
- )
+ # Only report on explicit referenced entities
+ missing = set(referenced.referenced)
+
+ for entity in entity_candidates:
+ missing.discard(entity.entity_id)
+
+ referenced.log_missing(missing)
entities = []
@@ -527,11 +621,11 @@ async def _handle_entity_call(
entity.async_set_context(context)
if isinstance(func, str):
- result = hass.async_add_job(partial(getattr(entity, func), **data)) # type: ignore
+ result = hass.async_run_job(partial(getattr(entity, func), **data)) # type: ignore
else:
- result = hass.async_add_job(func, entity, data)
+ result = hass.async_run_job(func, entity, data)
- # Guard because callback functions do not return a task when passed to async_add_job.
+ # Guard because callback functions do not return a task when passed to async_run_job.
if result is not None:
await result
@@ -564,7 +658,7 @@ def async_register_admin_service(
if not user.is_admin:
raise Unauthorized(context=call.context)
- result = hass.async_add_job(service_func, call)
+ result = hass.async_run_job(service_func, call)
if result is not None:
await result
diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index 33a9466f4dc..7aa59bd5836 100644
--- a/homeassistant/package_constraints.txt
+++ b/homeassistant/package_constraints.txt
@@ -12,8 +12,8 @@ cryptography==3.2
defusedxml==0.6.0
distro==1.5.0
emoji==0.5.4
-hass-nabucasa==0.37.2
-home-assistant-frontend==20201111.2
+hass-nabucasa==0.39.0
+home-assistant-frontend==20201212.0
httpx==0.16.1
importlib-metadata==1.6.0;python_version<'3.8'
jinja2>=2.11.2
@@ -30,7 +30,7 @@ sqlalchemy==1.3.20
voluptuous-serialize==2.4.0
voluptuous==0.12.0
yarl==1.4.2
-zeroconf==0.28.6
+zeroconf==0.28.7
pycryptodome>=3.6.6
diff --git a/homeassistant/runner.py b/homeassistant/runner.py
index a5cf0f88a40..0f8bb836da5 100644
--- a/homeassistant/runner.py
+++ b/homeassistant/runner.py
@@ -4,7 +4,6 @@ from concurrent.futures import ThreadPoolExecutor
import dataclasses
import logging
import sys
-import threading
from typing import Any, Dict, Optional
from homeassistant import bootstrap
@@ -77,29 +76,14 @@ class HassEventLoopPolicy(PolicyBase): # type: ignore
loop.set_default_executor, "sets default executor on the event loop"
)
- # Python 3.9+
- if hasattr(loop, "shutdown_default_executor"):
- return loop
+ # Shut down executor when we shut down loop
+ orig_close = loop.close
- # Copied from Python 3.9 source
- def _do_shutdown(future: asyncio.Future) -> None:
- try:
- executor.shutdown(wait=True)
- loop.call_soon_threadsafe(future.set_result, None)
- except Exception as ex: # pylint: disable=broad-except
- loop.call_soon_threadsafe(future.set_exception, ex)
+ def close() -> None:
+ executor.shutdown(wait=True)
+ orig_close()
- async def shutdown_default_executor() -> None:
- """Schedule the shutdown of the default executor."""
- future = loop.create_future()
- thread = threading.Thread(target=_do_shutdown, args=(future,))
- thread.start()
- try:
- await future
- finally:
- thread.join()
-
- setattr(loop, "shutdown_default_executor", shutdown_default_executor)
+ loop.close = close # type: ignore
return loop
diff --git a/homeassistant/strings.json b/homeassistant/strings.json
index f36c62b91ce..e2a85637fbb 100644
--- a/homeassistant/strings.json
+++ b/homeassistant/strings.json
@@ -70,7 +70,8 @@
"oauth2_missing_configuration": "The component is not configured. Please follow the documentation.",
"oauth2_authorize_url_timeout": "Timeout generating authorize URL.",
"oauth2_no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})",
- "reauth_successful": "Re-authentication was successful"
+ "reauth_successful": "Re-authentication was successful",
+ "unknown_authorize_url_generation": "Unknown error generating an authorize url."
}
}
}
diff --git a/homeassistant/util/distance.py b/homeassistant/util/distance.py
index 3bb3c258516..0e0a060c49c 100644
--- a/homeassistant/util/distance.py
+++ b/homeassistant/util/distance.py
@@ -1,5 +1,6 @@
"""Distance util functions."""
from numbers import Number
+from typing import Callable, Dict
from homeassistant.const import (
LENGTH,
@@ -25,6 +26,28 @@ VALID_UNITS = [
LENGTH_YARD,
]
+TO_METERS: Dict[str, Callable[[float], float]] = {
+ LENGTH_METERS: lambda meters: meters,
+ LENGTH_MILES: lambda miles: miles * 1609.344,
+ LENGTH_YARD: lambda yards: yards * 0.9144,
+ LENGTH_FEET: lambda feet: feet * 0.3048,
+ LENGTH_INCHES: lambda inches: inches * 0.0254,
+ LENGTH_KILOMETERS: lambda kilometers: kilometers * 1000,
+ LENGTH_CENTIMETERS: lambda centimeters: centimeters * 0.01,
+ LENGTH_MILLIMETERS: lambda millimeters: millimeters * 0.001,
+}
+
+METERS_TO: Dict[str, Callable[[float], float]] = {
+ LENGTH_METERS: lambda meters: meters,
+ LENGTH_MILES: lambda meters: meters * 0.000621371,
+ LENGTH_YARD: lambda meters: meters * 1.09361,
+ LENGTH_FEET: lambda meters: meters * 3.28084,
+ LENGTH_INCHES: lambda meters: meters * 39.3701,
+ LENGTH_KILOMETERS: lambda meters: meters * 0.001,
+ LENGTH_CENTIMETERS: lambda meters: meters * 100,
+ LENGTH_MILLIMETERS: lambda meters: meters * 1000,
+}
+
def convert(value: float, unit_1: str, unit_2: str) -> float:
"""Convert one unit of measurement to another."""
@@ -39,108 +62,6 @@ def convert(value: float, unit_1: str, unit_2: str) -> float:
if unit_1 == unit_2 or unit_1 not in VALID_UNITS:
return value
- meters: float = value
+ meters: float = TO_METERS[unit_1](value)
- if unit_1 == LENGTH_MILES:
- meters = __miles_to_meters(value)
- elif unit_1 == LENGTH_YARD:
- meters = __yards_to_meters(value)
- elif unit_1 == LENGTH_FEET:
- meters = __feet_to_meters(value)
- elif unit_1 == LENGTH_INCHES:
- meters = __inches_to_meters(value)
- elif unit_1 == LENGTH_KILOMETERS:
- meters = __kilometers_to_meters(value)
- elif unit_1 == LENGTH_CENTIMETERS:
- meters = __centimeters_to_meters(value)
- elif unit_1 == LENGTH_MILLIMETERS:
- meters = __millimeters_to_meters(value)
-
- result = meters
-
- if unit_2 == LENGTH_MILES:
- result = __meters_to_miles(meters)
- elif unit_2 == LENGTH_YARD:
- result = __meters_to_yards(meters)
- elif unit_2 == LENGTH_FEET:
- result = __meters_to_feet(meters)
- elif unit_2 == LENGTH_INCHES:
- result = __meters_to_inches(meters)
- elif unit_2 == LENGTH_KILOMETERS:
- result = __meters_to_kilometers(meters)
- elif unit_2 == LENGTH_CENTIMETERS:
- result = __meters_to_centimeters(meters)
- elif unit_2 == LENGTH_MILLIMETERS:
- result = __meters_to_millimeters(meters)
-
- return result
-
-
-def __miles_to_meters(miles: float) -> float:
- """Convert miles to meters."""
- return miles * 1609.344
-
-
-def __yards_to_meters(yards: float) -> float:
- """Convert yards to meters."""
- return yards * 0.9144
-
-
-def __feet_to_meters(feet: float) -> float:
- """Convert feet to meters."""
- return feet * 0.3048
-
-
-def __inches_to_meters(inches: float) -> float:
- """Convert inches to meters."""
- return inches * 0.0254
-
-
-def __kilometers_to_meters(kilometers: float) -> float:
- """Convert kilometers to meters."""
- return kilometers * 1000
-
-
-def __centimeters_to_meters(centimeters: float) -> float:
- """Convert centimeters to meters."""
- return centimeters * 0.01
-
-
-def __millimeters_to_meters(millimeters: float) -> float:
- """Convert millimeters to meters."""
- return millimeters * 0.001
-
-
-def __meters_to_miles(meters: float) -> float:
- """Convert meters to miles."""
- return meters * 0.000621371
-
-
-def __meters_to_yards(meters: float) -> float:
- """Convert meters to yards."""
- return meters * 1.09361
-
-
-def __meters_to_feet(meters: float) -> float:
- """Convert meters to feet."""
- return meters * 3.28084
-
-
-def __meters_to_inches(meters: float) -> float:
- """Convert meters to inches."""
- return meters * 39.3701
-
-
-def __meters_to_kilometers(meters: float) -> float:
- """Convert meters to kilometers."""
- return meters * 0.001
-
-
-def __meters_to_centimeters(meters: float) -> float:
- """Convert meters to centimeters."""
- return meters * 100
-
-
-def __meters_to_millimeters(meters: float) -> float:
- """Convert meters to millimeters."""
- return meters * 1000
+ return METERS_TO[unit_2](meters)
diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py
index 8b52170f0e4..a4d5fd81c4f 100644
--- a/homeassistant/util/dt.py
+++ b/homeassistant/util/dt.py
@@ -108,6 +108,9 @@ def start_of_local_day(
date: dt.date = now().date()
elif isinstance(dt_or_d, dt.datetime):
date = dt_or_d.date()
+ else:
+ date = dt_or_d
+
return DEFAULT_TIME_ZONE.localize( # type: ignore
dt.datetime.combine(date, dt.time())
)
diff --git a/homeassistant/util/yaml/__init__.py b/homeassistant/util/yaml/__init__.py
index bb6eb122de5..ac4ac2f9a16 100644
--- a/homeassistant/util/yaml/__init__.py
+++ b/homeassistant/util/yaml/__init__.py
@@ -1,17 +1,21 @@
"""YAML utility functions."""
from .const import _SECRET_NAMESPACE, SECRET_YAML
from .dumper import dump, save_yaml
+from .input import UndefinedSubstitution, extract_inputs, substitute
from .loader import clear_secret_cache, load_yaml, parse_yaml, secret_yaml
-from .objects import Placeholder
+from .objects import Input
__all__ = [
"SECRET_YAML",
"_SECRET_NAMESPACE",
- "Placeholder",
+ "Input",
"dump",
"save_yaml",
"clear_secret_cache",
"load_yaml",
"secret_yaml",
"parse_yaml",
+ "UndefinedSubstitution",
+ "extract_inputs",
+ "substitute",
]
diff --git a/homeassistant/util/yaml/dumper.py b/homeassistant/util/yaml/dumper.py
index 6834323ed72..8e9cb382b6c 100644
--- a/homeassistant/util/yaml/dumper.py
+++ b/homeassistant/util/yaml/dumper.py
@@ -3,7 +3,7 @@ from collections import OrderedDict
import yaml
-from .objects import NodeListClass, Placeholder
+from .objects import Input, NodeListClass
# mypy: allow-untyped-calls, no-warn-return-any
@@ -62,6 +62,6 @@ yaml.SafeDumper.add_representer(
)
yaml.SafeDumper.add_representer(
- Placeholder,
- lambda dumper, value: dumper.represent_scalar("!placeholder", value.name),
+ Input,
+ lambda dumper, value: dumper.represent_scalar("!input", value.name),
)
diff --git a/homeassistant/helpers/placeholder.py b/homeassistant/util/yaml/input.py
similarity index 57%
rename from homeassistant/helpers/placeholder.py
rename to homeassistant/util/yaml/input.py
index 3da5eaba76f..6282509fae2 100644
--- a/homeassistant/helpers/placeholder.py
+++ b/homeassistant/util/yaml/input.py
@@ -1,45 +1,46 @@
-"""Placeholder helpers."""
+"""Deal with YAML input."""
+
from typing import Any, Dict, Set
-from homeassistant.util.yaml import Placeholder
+from .objects import Input
class UndefinedSubstitution(Exception):
"""Error raised when we find a substitution that is not defined."""
- def __init__(self, placeholder: str) -> None:
+ def __init__(self, input_name: str) -> None:
"""Initialize the undefined substitution exception."""
- super().__init__(f"No substitution found for placeholder {placeholder}")
- self.placeholder = placeholder
+ super().__init__(f"No substitution found for input {input_name}")
+ self.input = input
-def extract_placeholders(obj: Any) -> Set[str]:
- """Extract placeholders from a structure."""
+def extract_inputs(obj: Any) -> Set[str]:
+ """Extract input from a structure."""
found: Set[str] = set()
- _extract_placeholders(obj, found)
+ _extract_inputs(obj, found)
return found
-def _extract_placeholders(obj: Any, found: Set[str]) -> None:
- """Extract placeholders from a structure."""
- if isinstance(obj, Placeholder):
+def _extract_inputs(obj: Any, found: Set[str]) -> None:
+ """Extract input from a structure."""
+ if isinstance(obj, Input):
found.add(obj.name)
return
if isinstance(obj, list):
for val in obj:
- _extract_placeholders(val, found)
+ _extract_inputs(val, found)
return
if isinstance(obj, dict):
for val in obj.values():
- _extract_placeholders(val, found)
+ _extract_inputs(val, found)
return
def substitute(obj: Any, substitutions: Dict[str, Any]) -> Any:
"""Substitute values."""
- if isinstance(obj, Placeholder):
+ if isinstance(obj, Input):
if obj.name not in substitutions:
raise UndefinedSubstitution(obj.name)
return substitutions[obj.name]
diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py
index c9e191db5de..746806f527d 100644
--- a/homeassistant/util/yaml/loader.py
+++ b/homeassistant/util/yaml/loader.py
@@ -11,7 +11,7 @@ import yaml
from homeassistant.exceptions import HomeAssistantError
from .const import _SECRET_NAMESPACE, SECRET_YAML
-from .objects import NodeListClass, NodeStrClass, Placeholder
+from .objects import Input, NodeListClass, NodeStrClass
try:
import keyring
@@ -32,6 +32,9 @@ DICT_T = TypeVar("DICT_T", bound=Dict) # pylint: disable=invalid-name
_LOGGER = logging.getLogger(__name__)
__SECRET_CACHE: Dict[str, JSON_TYPE] = {}
+CREDSTASH_WARN = False
+KEYRING_WARN = False
+
def clear_secret_cache() -> None:
"""Clear the secret cache.
@@ -295,6 +298,14 @@ def secret_yaml(loader: SafeLineLoader, node: yaml.nodes.Node) -> JSON_TYPE:
# do some keyring stuff
pwd = keyring.get_password(_SECRET_NAMESPACE, node.value)
if pwd:
+ global KEYRING_WARN # pylint: disable=global-statement
+
+ if not KEYRING_WARN:
+ KEYRING_WARN = True
+ _LOGGER.warning(
+ "Keyring is deprecated and will be removed in March 2021."
+ )
+
_LOGGER.debug("Secret %s retrieved from keyring", node.value)
return pwd
@@ -305,6 +316,13 @@ def secret_yaml(loader: SafeLineLoader, node: yaml.nodes.Node) -> JSON_TYPE:
try:
pwd = credstash.getSecret(node.value, table=_SECRET_NAMESPACE)
if pwd:
+ global CREDSTASH_WARN # pylint: disable=global-statement
+
+ if not CREDSTASH_WARN:
+ CREDSTASH_WARN = True
+ _LOGGER.warning(
+ "Credstash is deprecated and will be removed in March 2021."
+ )
_LOGGER.debug("Secret %s retrieved from credstash", node.value)
return pwd
except credstash.ItemNotFound:
@@ -331,4 +349,4 @@ yaml.SafeLoader.add_constructor("!include_dir_named", _include_dir_named_yaml)
yaml.SafeLoader.add_constructor(
"!include_dir_merge_named", _include_dir_merge_named_yaml
)
-yaml.SafeLoader.add_constructor("!placeholder", Placeholder.from_node)
+yaml.SafeLoader.add_constructor("!input", Input.from_node)
diff --git a/homeassistant/util/yaml/objects.py b/homeassistant/util/yaml/objects.py
index c3f4c0ff140..0e46820e0db 100644
--- a/homeassistant/util/yaml/objects.py
+++ b/homeassistant/util/yaml/objects.py
@@ -13,12 +13,12 @@ class NodeStrClass(str):
@dataclass(frozen=True)
-class Placeholder:
- """A placeholder that should be substituted."""
+class Input:
+ """Input that should be substituted."""
name: str
@classmethod
- def from_node(cls, loader: yaml.Loader, node: yaml.nodes.Node) -> "Placeholder":
+ def from_node(cls, loader: yaml.Loader, node: yaml.nodes.Node) -> "Input":
"""Create a new placeholder from a node."""
return cls(node.value)
diff --git a/requirements_all.txt b/requirements_all.txt
index 82b0ff13c5e..858b747b2ae 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -25,11 +25,8 @@ Mastodon.py==1.5.1
# homeassistant.components.orangepi_gpio
OPi.GPIO==0.4.0
-# homeassistant.components.plugwise
-Plugwise_Smile==1.6.0
-
# homeassistant.components.essent
-PyEssent==0.13
+PyEssent==0.14
# homeassistant.components.flick_electric
PyFlick==0.0.2
@@ -84,7 +81,7 @@ RtmAPI==0.7.2
TravisPy==0.3.5
# homeassistant.components.twitter
-TwitterAPI==2.5.13
+TwitterAPI==2.6.2.1
# homeassistant.components.tof
# VL53L1X2==0.1.5
@@ -95,11 +92,8 @@ WSDiscovery==2.0.0
# homeassistant.components.waze_travel_time
WazeRouteCalculator==0.12
-# homeassistant.components.yessssms
-YesssSMS==0.4.1
-
# homeassistant.components.abode
-abodepy==1.1.0
+abodepy==1.2.0
# homeassistant.components.accuweather
accuweather==0.0.11
@@ -144,7 +138,7 @@ aio_georss_gdacs==0.4
aioambient==1.2.1
# homeassistant.components.asuswrt
-aioasuswrt==1.3.0
+aioasuswrt==1.3.1
# homeassistant.components.azure_devops
aioazuredevops==1.3.5
@@ -178,7 +172,7 @@ aioguardian==1.0.4
aioharmony==0.2.6
# homeassistant.components.homekit_controller
-aiohomekit==0.2.54
+aiohomekit==0.2.60
# homeassistant.components.emulated_hue
# homeassistant.components.http
@@ -209,7 +203,7 @@ aionotify==0.2.0
aionotion==1.1.0
# homeassistant.components.acmeda
-aiopulse==0.4.0
+aiopulse==0.4.2
# homeassistant.components.hunterdouglas_powerview
aiopvapi==1.6.14
@@ -221,7 +215,7 @@ aiopvpc==2.0.2
aiopylgtv==0.3.3
# homeassistant.components.recollect_waste
-aiorecollect==0.2.1
+aiorecollect==0.2.2
# homeassistant.components.shelly
aioshelly==0.5.1
@@ -230,7 +224,7 @@ aioshelly==0.5.1
aioswitcher==1.2.1
# homeassistant.components.unifi
-aiounifi==25
+aiounifi==26
# homeassistant.components.yandex_transport
aioymaps==1.1.0
@@ -251,7 +245,7 @@ ambiclimate==0.2.1
amcrest==1.7.0
# homeassistant.components.androidtv
-androidtv[async]==0.0.54
+androidtv[async]==0.0.56
# homeassistant.components.anel_pwrctrl
anel_pwrctrl-homeassistant==0.0.1.dev2
@@ -296,6 +290,9 @@ asyncpysupla==0.0.5
# homeassistant.components.aten_pe
atenpdu==0.3.0
+# homeassistant.components.aurora
+auroranoaa==0.0.2
+
# homeassistant.components.aurora_abb_powerone
aurorapy==0.2.6
@@ -339,10 +336,10 @@ batinfo==0.4.2
beautifulsoup4==4.9.1
# homeassistant.components.beewi_smartclim
-# beewi_smartclim==0.0.7
+# beewi_smartclim==0.0.10
# homeassistant.components.zha
-bellows==0.20.3
+bellows==0.21.0
# homeassistant.components.bmw_connected_drive
bimmer_connected==0.7.13
@@ -354,7 +351,7 @@ bizkaibus==0.1.1
blebox_uniapi==1.3.2
# homeassistant.components.blink
-blinkpy==0.16.3
+blinkpy==0.16.4
# homeassistant.components.blinksticklight
blinkstick==1.1.8
@@ -386,7 +383,7 @@ bravia-tv==1.0.8
broadlink==0.16.0
# homeassistant.components.brother
-brother==0.1.18
+brother==0.1.20
# homeassistant.components.brottsplatskartan
brottsplatskartan==0.0.1
@@ -448,7 +445,7 @@ connect-box==0.2.8
# homeassistant.components.eddystone_temperature
# homeassistant.components.eq3btsmart
# homeassistant.components.xiaomi_miio
-construct==2.9.45
+construct==2.10.56
# homeassistant.components.coronavirus
coronavirus==1.1.1
@@ -466,7 +463,7 @@ datadog==0.15.0
datapoint==0.9.5
# homeassistant.components.debugpy
-debugpy==1.1.0
+debugpy==1.2.0
# homeassistant.components.decora
# decora==0.6
@@ -484,19 +481,19 @@ defusedxml==0.6.0
deluge-client==1.7.1
# homeassistant.components.denonavr
-denonavr==0.9.5
+denonavr==0.9.7
# homeassistant.components.devolo_home_control
devolo-home-control-api==0.16.0
# homeassistant.components.directv
-directv==0.3.0
+directv==0.4.0
# homeassistant.components.discogs
discogs_client==2.3.0
# homeassistant.components.discord
-discord.py==1.4.1
+discord.py==1.5.1
# homeassistant.components.updater
distro==1.5.0
@@ -511,7 +508,7 @@ doorbirdpy==2.1.0
dovado==0.4.1
# homeassistant.components.dsmr
-dsmr_parser==0.18
+dsmr_parser==0.23
# homeassistant.components.dwd_weather_warnings
dwdwfsapi==1.0.3
@@ -535,7 +532,7 @@ ecoaliface==0.4.0
eebrightbox==0.0.4
# homeassistant.components.elgato
-elgato==0.2.0
+elgato==1.0.0
# homeassistant.components.eliqonline
eliqonline==1.2.2
@@ -562,7 +559,7 @@ env_canada==0.2.4
# envirophat==0.0.6
# homeassistant.components.enphase_envoy
-envoy_reader==0.16.2
+envoy_reader==0.17.0
# homeassistant.components.season
ephem==3.7.7.0
@@ -611,7 +608,7 @@ flux_led==0.22
fnvhash==0.1.0
# homeassistant.components.foobot
-foobot_async==0.3.2
+foobot_async==1.0.0
# homeassistant.components.fortios
fortiosapi==0.10.8
@@ -687,7 +684,7 @@ google-cloud-pubsub==2.1.0
google-cloud-texttospeech==0.4.0
# homeassistant.components.nest
-google-nest-sdm==0.1.14
+google-nest-sdm==0.2.0
# homeassistant.components.google_travel_time
googlemaps==2.5.1
@@ -723,7 +720,7 @@ gstreamer-player==1.1.2
guppy3==3.1.0
# homeassistant.components.ffmpeg
-ha-ffmpeg==2.0
+ha-ffmpeg==3.0.2
# homeassistant.components.philips_js
ha-philipsjs==0.0.8
@@ -735,13 +732,13 @@ habitipy==0.2.0
hangups==0.4.11
# homeassistant.components.cloud
-hass-nabucasa==0.37.2
+hass-nabucasa==0.39.0
# homeassistant.components.splunk
hass_splunk==0.1.1
# homeassistant.components.tasmota
-hatasmota==0.0.32
+hatasmota==0.1.4
# homeassistant.components.jewish_calendar
hdate==0.9.12
@@ -768,7 +765,7 @@ hole==0.5.1
holidays==0.10.3
# homeassistant.components.frontend
-home-assistant-frontend==20201111.2
+home-assistant-frontend==20201212.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@@ -793,7 +790,7 @@ huawei-lte-api==1.4.12
hydrawiser==0.2
# homeassistant.components.hyperion
-hyperion-py==0.3.0
+hyperion-py==0.6.0
# homeassistant.components.bh1750
# homeassistant.components.bme280
@@ -859,7 +856,7 @@ konnected==1.2.0
lakeside==0.12
# homeassistant.components.dyson
-libpurecool==0.6.3
+libpurecool==0.6.4
# homeassistant.components.foscam
libpyfoscam==1.0
@@ -946,7 +943,7 @@ mficlient==0.3.0
miflora==0.7.0
# homeassistant.components.mill
-millheater==0.3.4
+millheater==0.4.0
# homeassistant.components.minio
minio==4.0.9
@@ -954,6 +951,9 @@ minio==4.0.9
# homeassistant.components.mitemp_bt
mitemp_bt==0.0.3
+# homeassistant.components.motion_blinds
+motionblinds==0.1.6
+
# homeassistant.components.tts
mutagen==1.45.1
@@ -1073,7 +1073,7 @@ oru==0.1.11
orvibo==1.1.1
# homeassistant.components.ovo_energy
-ovoenergy==1.1.7
+ovoenergy==1.1.11
# homeassistant.components.mqtt
# homeassistant.components.shiftr
@@ -1139,6 +1139,9 @@ plexauth==0.0.6
# homeassistant.components.plex
plexwebsocket==0.0.12
+# homeassistant.components.plugwise
+plugwise==0.8.3
+
# homeassistant.components.plum_lightpad
plumlightpad==0.0.11
@@ -1228,7 +1231,7 @@ pyCEC==0.4.14
pyControl4==0.0.6
# homeassistant.components.tplink
-pyHS100==0.3.5.1
+pyHS100==0.3.5.2
# homeassistant.components.met
# homeassistant.components.norway_air
@@ -1268,7 +1271,7 @@ pyairvisual==5.0.4
pyalmond==0.0.2
# homeassistant.components.arlo
-pyarlo==0.2.3
+pyarlo==0.2.4
# homeassistant.components.atag
pyatag==0.3.4.4
@@ -1280,7 +1283,7 @@ pyatmo==4.2.1
pyatome==0.1.1
# homeassistant.components.apple_tv
-pyatv==0.3.13
+pyatv==0.7.5
# homeassistant.components.bbox
pybbox==0.0.5-alpha
@@ -1334,7 +1337,7 @@ pydaikin==2.3.1
pydanfossair==0.1.0
# homeassistant.components.deconz
-pydeconz==73
+pydeconz==76
# homeassistant.components.delijn
pydelijn==0.6.1
@@ -1378,6 +1381,9 @@ pyeverlights==0.1.0
# homeassistant.components.fido
pyfido==2.1.1
+# homeassistant.components.fireservicerota
+pyfireservicerota==0.0.40
+
# homeassistant.components.flexit
pyflexit==0.3
@@ -1419,7 +1425,7 @@ pygti==0.9.2
pyhaversion==3.4.2
# homeassistant.components.heos
-pyheos==0.6.0
+pyheos==0.7.2
# homeassistant.components.hikvision
pyhik==0.2.8
@@ -1469,6 +1475,9 @@ pykira==0.1.1
# homeassistant.components.kodi
pykodi==0.2.1
+# homeassistant.components.kulersky
+pykulersky==0.4.0
+
# homeassistant.components.kwb
pykwb==0.0.8
@@ -1476,7 +1485,7 @@ pykwb==0.0.8
pylacrosse==0.4
# homeassistant.components.lastfm
-pylast==3.3.0
+pylast==4.0.0
# homeassistant.components.launch_library
pylaunches==1.0.0
@@ -1533,7 +1542,7 @@ pymsteams==0.1.12
pymusiccast==0.1.6
# homeassistant.components.myq
-pymyq==2.0.8
+pymyq==2.0.11
# homeassistant.components.mysensors
pymysensors==0.18.0
@@ -1601,7 +1610,7 @@ pyownet==0.10.0.post1
pypca==0.0.7
# homeassistant.components.lcn
-pypck==0.7.4
+pypck==0.7.7
# homeassistant.components.pjlink
pypjlink2==1.2.1
@@ -1652,6 +1661,7 @@ pysdcp==1
pysensibo==1.0.3
# homeassistant.components.serial
+# homeassistant.components.zha
pyserial-asyncio==0.4
# homeassistant.components.acer_projector
@@ -1677,16 +1687,16 @@ pysma==0.3.5
pysmappee==0.2.13
# homeassistant.components.smartthings
-pysmartapp==0.3.2
+pysmartapp==0.3.3
# homeassistant.components.smartthings
-pysmartthings==0.7.4
+pysmartthings==0.7.6
# homeassistant.components.smarty
pysmarty==0.8
# homeassistant.components.edl21
-pysml==0.0.2
+pysml==0.0.3
# homeassistant.components.snmp
pysnmp==4.4.12
@@ -1695,7 +1705,7 @@ pysnmp==4.4.12
pysoma==0.0.10
# homeassistant.components.sonos
-pysonos==0.0.36
+pysonos==0.0.37
# homeassistant.components.spc
pyspcwebgw==0.4.0
@@ -1773,7 +1783,7 @@ python-juicenet==1.0.1
# python-lirc==1.2.3
# homeassistant.components.xiaomi_miio
-python-miio==0.5.3
+python-miio==0.5.4
# homeassistant.components.mpd
python-mpd2==1.0.0
@@ -1788,7 +1798,7 @@ python-nest==4.1.0
python-nmap==0.6.1
# homeassistant.components.ozw
-python-openzwave-mqtt==1.3.2
+python-openzwave-mqtt[mqtt-client]==1.4.0
# homeassistant.components.qbittorrent
python-qbittorrent==0.4.1
@@ -1802,9 +1812,6 @@ python-sochain-api==0.0.2
# homeassistant.components.songpal
python-songpal==0.12
-# homeassistant.components.synology_dsm
-python-synology==1.0.0
-
# homeassistant.components.tado
python-tado==0.8.1
@@ -1830,7 +1837,7 @@ python-whois==0.7.3
python-wink==1.10.5
# homeassistant.components.awair
-python_awair==0.1.1
+python_awair==0.2.1
# homeassistant.components.swiss_public_transport
python_opendata_transport==0.2.1
@@ -1857,9 +1864,6 @@ pytradfri[async]==7.0.4
# homeassistant.components.trafikverket_weatherstation
pytrafikverket==0.1.6.2
-# homeassistant.components.ubee
-pyubee==0.10
-
# homeassistant.components.uptimerobot
pyuptimerobot==0.0.5
@@ -1888,7 +1892,7 @@ pyvolumio==0.1.3
pywebpush==1.9.2
# homeassistant.components.wemo
-pywemo==0.5.2
+pywemo==0.5.3
# homeassistant.components.wilight
pywilight==0.0.65
@@ -1933,10 +1937,10 @@ restrictedpython==5.0
rfk101py==0.0.1
# homeassistant.components.rflink
-rflink==0.0.54
+rflink==0.0.55
# homeassistant.components.ring
-ring_doorbell==0.6.0
+ring_doorbell==0.6.2
# homeassistant.components.fleetgo
ritassist==0.9.2
@@ -1951,7 +1955,7 @@ rocketchat-API==0.6.1
rokuecp==0.6.0
# homeassistant.components.roomba
-roombapy==1.6.1
+roombapy==1.6.2
# homeassistant.components.roon
roonapi==0.0.25
@@ -1960,7 +1964,7 @@ roonapi==0.0.25
rova==0.1.0
# homeassistant.components.rpi_power
-rpi-bad-power==0.0.3
+rpi-bad-power==0.1.0
# homeassistant.components.rpi_rf
# rpi-rf==0.9.7
@@ -1974,9 +1978,6 @@ russound_rio==0.1.7
# homeassistant.components.yamaha
rxv==0.6.0
-# homeassistant.components.salt
-saltbox==0.1.3
-
# homeassistant.components.samsungtv
samsungctl[websocket]==0.7.1
@@ -2003,7 +2004,7 @@ sense-hat==2.2.0
sense_energy==0.8.1
# homeassistant.components.sentry
-sentry-sdk==0.19.2
+sentry-sdk==0.19.4
# homeassistant.components.sharkiq
sharkiqpy==0.1.8
@@ -2021,7 +2022,7 @@ simplehound==0.3
simplepush==1.1.4
# homeassistant.components.simplisafe
-simplisafe-python==9.6.0
+simplisafe-python==9.6.2
# homeassistant.components.sisyphus
sisyphus-control==3.0
@@ -2068,7 +2069,7 @@ solaredge-local==0.2.0
solaredge==0.0.2
# homeassistant.components.solax
-solax==0.2.4
+solax==0.2.5
# homeassistant.components.honeywell
somecomfort==0.5.2
@@ -2098,6 +2099,9 @@ spotipy==2.16.1
# homeassistant.components.sql
sqlalchemy==1.3.20
+# homeassistant.components.srp_energy
+srpenergy==1.3.2
+
# homeassistant.components.starline
starline==0.1.3
@@ -2137,6 +2141,9 @@ swisshydrodata==0.0.3
# homeassistant.components.synology_srm
synology-srm==0.2.0
+# homeassistant.components.synology_dsm
+synologydsm-api==1.0.1
+
# homeassistant.components.tahoma
tahoma-api==0.0.16
@@ -2201,7 +2208,7 @@ tp-connected==0.0.4
transmissionrpc==0.11
# homeassistant.components.tuya
-tuyaha==0.0.8
+tuyaha==0.0.9
# homeassistant.components.twentemilieu
twentemilieu==0.3.0
@@ -2209,6 +2216,9 @@ twentemilieu==0.3.0
# homeassistant.components.twilio
twilio==6.32.0
+# homeassistant.components.twinkly
+twinkly-client==0.0.2
+
# homeassistant.components.rainforest_eagle
uEagle==0.0.2
@@ -2301,7 +2311,7 @@ xboxapi==2.0.1
xfinity-gateway==0.0.4
# homeassistant.components.knx
-xknx==0.15.3
+xknx==0.15.6
# homeassistant.components.bluesound
# homeassistant.components.rest
@@ -2323,7 +2333,7 @@ yeelight==0.5.4
yeelightsunflower==0.0.10
# homeassistant.components.media_extractor
-youtube_dl==2020.11.01.1
+youtube_dl==2020.11.12
# homeassistant.components.onvif
zeep[async]==4.0.0
@@ -2332,10 +2342,10 @@ zeep[async]==4.0.0
zengge==0.2
# homeassistant.components.zeroconf
-zeroconf==0.28.6
+zeroconf==0.28.7
# homeassistant.components.zha
-zha-quirks==0.0.46
+zha-quirks==0.0.49
# homeassistant.components.zhong_hong
zhong_hong_hvac==1.0.9
@@ -2356,10 +2366,10 @@ zigpy-xbee==0.13.0
zigpy-zigate==0.7.3
# homeassistant.components.zha
-zigpy-znp==0.2.2
+zigpy-znp==0.3.0
# homeassistant.components.zha
-zigpy==0.27.0
+zigpy==0.28.2
# homeassistant.components.zoneminder
zm-py==0.4.0
diff --git a/requirements_test.txt b/requirements_test.txt
index 77c68763894..8ec5a611f1d 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -10,7 +10,7 @@ coverage==5.3
jsonpickle==1.4.1
mock-open==1.4.0
mypy==0.790
-pre-commit==2.8.2
+pre-commit==2.9.2
pylint==2.6.0
astroid==2.4.2
pipdeptree==1.0.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 79b5a6ec707..9dc1bd8d860 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -6,9 +6,6 @@
# homeassistant.components.homekit
HAP-python==3.0.0
-# homeassistant.components.plugwise
-Plugwise_Smile==1.6.0
-
# homeassistant.components.flick_electric
PyFlick==0.0.2
@@ -38,11 +35,8 @@ RtmAPI==0.7.2
# homeassistant.components.onvif
WSDiscovery==2.0.0
-# homeassistant.components.yessssms
-YesssSMS==0.4.1
-
# homeassistant.components.abode
-abodepy==1.1.0
+abodepy==1.2.0
# homeassistant.components.accuweather
accuweather==0.0.11
@@ -78,7 +72,7 @@ aio_georss_gdacs==0.4
aioambient==1.2.1
# homeassistant.components.asuswrt
-aioasuswrt==1.3.0
+aioasuswrt==1.3.1
# homeassistant.components.azure_devops
aioazuredevops==1.3.5
@@ -109,7 +103,7 @@ aioguardian==1.0.4
aioharmony==0.2.6
# homeassistant.components.homekit_controller
-aiohomekit==0.2.54
+aiohomekit==0.2.60
# homeassistant.components.emulated_hue
# homeassistant.components.http
@@ -125,7 +119,7 @@ aiokafka==0.6.0
aionotion==1.1.0
# homeassistant.components.acmeda
-aiopulse==0.4.0
+aiopulse==0.4.2
# homeassistant.components.hunterdouglas_powerview
aiopvapi==1.6.14
@@ -136,6 +130,9 @@ aiopvpc==2.0.2
# homeassistant.components.webostv
aiopylgtv==0.3.3
+# homeassistant.components.recollect_waste
+aiorecollect==0.2.2
+
# homeassistant.components.shelly
aioshelly==0.5.1
@@ -143,7 +140,7 @@ aioshelly==0.5.1
aioswitcher==1.2.1
# homeassistant.components.unifi
-aiounifi==25
+aiounifi==26
# homeassistant.components.yandex_transport
aioymaps==1.1.0
@@ -155,7 +152,7 @@ airly==1.0.0
ambiclimate==0.2.1
# homeassistant.components.androidtv
-androidtv[async]==0.0.54
+androidtv[async]==0.0.56
# homeassistant.components.apns
apns2==0.3.0
@@ -173,6 +170,9 @@ arcam-fmj==0.5.3
# homeassistant.components.upnp
async-upnp-client==0.14.13
+# homeassistant.components.aurora
+auroranoaa==0.0.2
+
# homeassistant.components.stream
av==8.0.2
@@ -189,13 +189,13 @@ azure-eventhub==5.1.0
base36==0.1.1
# homeassistant.components.zha
-bellows==0.20.3
+bellows==0.21.0
# homeassistant.components.blebox
blebox_uniapi==1.3.2
# homeassistant.components.blink
-blinkpy==0.16.3
+blinkpy==0.16.4
# homeassistant.components.bond
bond-api==0.1.8
@@ -207,7 +207,7 @@ bravia-tv==1.0.8
broadlink==0.16.0
# homeassistant.components.brother
-brother==0.1.18
+brother==0.1.20
# homeassistant.components.bsblan
bsblan==0.4.0
@@ -230,7 +230,7 @@ colorthief==0.2.1
# homeassistant.components.eddystone_temperature
# homeassistant.components.eq3btsmart
# homeassistant.components.xiaomi_miio
-construct==2.9.45
+construct==2.10.56
# homeassistant.components.coronavirus
coronavirus==1.1.1
@@ -245,7 +245,7 @@ datadog==0.15.0
datapoint==0.9.5
# homeassistant.components.debugpy
-debugpy==1.1.0
+debugpy==1.2.0
# homeassistant.components.ihc
# homeassistant.components.namecheapdns
@@ -254,13 +254,13 @@ debugpy==1.1.0
defusedxml==0.6.0
# homeassistant.components.denonavr
-denonavr==0.9.5
+denonavr==0.9.7
# homeassistant.components.devolo_home_control
devolo-home-control-api==0.16.0
# homeassistant.components.directv
-directv==0.3.0
+directv==0.4.0
# homeassistant.components.updater
distro==1.5.0
@@ -269,7 +269,7 @@ distro==1.5.0
doorbirdpy==2.1.0
# homeassistant.components.dsmr
-dsmr_parser==0.18
+dsmr_parser==0.23
# homeassistant.components.dynalite
dynalite_devices==0.1.46
@@ -278,7 +278,7 @@ dynalite_devices==0.1.46
eebrightbox==0.0.4
# homeassistant.components.elgato
-elgato==0.2.0
+elgato==1.0.0
# homeassistant.components.elkm1
elkm1-lib==0.8.8
@@ -305,7 +305,7 @@ feedparser-homeassistant==5.2.2.dev1
fnvhash==0.1.0
# homeassistant.components.foobot
-foobot_async==0.3.2
+foobot_async==1.0.0
# homeassistant.components.google_translate
gTTS==2.2.1
@@ -355,7 +355,7 @@ google-api-python-client==1.6.4
google-cloud-pubsub==2.1.0
# homeassistant.components.nest
-google-nest-sdm==0.1.14
+google-nest-sdm==0.2.0
# homeassistant.components.gree
greeclimate==0.10.3
@@ -367,16 +367,16 @@ griddypower==0.1.0
guppy3==3.1.0
# homeassistant.components.ffmpeg
-ha-ffmpeg==2.0
+ha-ffmpeg==3.0.2
# homeassistant.components.hangouts
hangups==0.4.11
# homeassistant.components.cloud
-hass-nabucasa==0.37.2
+hass-nabucasa==0.39.0
# homeassistant.components.tasmota
-hatasmota==0.0.32
+hatasmota==0.1.4
# homeassistant.components.jewish_calendar
hdate==0.9.12
@@ -394,7 +394,7 @@ hole==0.5.1
holidays==0.10.3
# homeassistant.components.frontend
-home-assistant-frontend==20201111.2
+home-assistant-frontend==20201212.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@@ -413,7 +413,7 @@ httplib2==0.10.3
huawei-lte-api==1.4.12
# homeassistant.components.hyperion
-hyperion-py==0.3.0
+hyperion-py==0.6.0
# homeassistant.components.iaqualink
iaqualink==0.3.4
@@ -441,7 +441,7 @@ keyrings.alt==3.4.0
konnected==1.2.0
# homeassistant.components.dyson
-libpurecool==0.6.3
+libpurecool==0.6.4
# homeassistant.components.mikrotik
librouteros==3.0.0
@@ -468,11 +468,14 @@ meteofrance-api==0.1.1
mficlient==0.3.0
# homeassistant.components.mill
-millheater==0.3.4
+millheater==0.4.0
# homeassistant.components.minio
minio==4.0.9
+# homeassistant.components.motion_blinds
+motionblinds==0.1.6
+
# homeassistant.components.tts
mutagen==1.45.1
@@ -517,7 +520,7 @@ onvif-zeep-async==1.0.0
openerz-api==0.1.0
# homeassistant.components.ovo_energy
-ovoenergy==1.1.7
+ovoenergy==1.1.11
# homeassistant.components.mqtt
# homeassistant.components.shiftr
@@ -559,6 +562,9 @@ plexauth==0.0.6
# homeassistant.components.plex
plexwebsocket==0.0.12
+# homeassistant.components.plugwise
+plugwise==0.8.3
+
# homeassistant.components.plum_lightpad
plumlightpad==0.0.11
@@ -609,7 +615,7 @@ py17track==2.2.2
pyControl4==0.0.6
# homeassistant.components.tplink
-pyHS100==0.3.5.1
+pyHS100==0.3.5.2
# homeassistant.components.met
# homeassistant.components.norway_air
@@ -634,7 +640,7 @@ pyairvisual==5.0.4
pyalmond==0.0.2
# homeassistant.components.arlo
-pyarlo==0.2.3
+pyarlo==0.2.4
# homeassistant.components.atag
pyatag==0.3.4.4
@@ -642,6 +648,9 @@ pyatag==0.3.4.4
# homeassistant.components.netatmo
pyatmo==4.2.1
+# homeassistant.components.apple_tv
+pyatv==0.7.5
+
# homeassistant.components.blackbird
pyblackbird==0.5
@@ -664,7 +673,7 @@ pycountry==19.8.18
pydaikin==2.3.1
# homeassistant.components.deconz
-pydeconz==73
+pydeconz==76
# homeassistant.components.dexcom
pydexcom==0.2.0
@@ -678,6 +687,9 @@ pyeverlights==0.1.0
# homeassistant.components.fido
pyfido==2.1.1
+# homeassistant.components.fireservicerota
+pyfireservicerota==0.0.40
+
# homeassistant.components.flume
pyflume==0.5.5
@@ -704,7 +716,7 @@ pygti==0.9.2
pyhaversion==3.4.2
# homeassistant.components.heos
-pyheos==0.6.0
+pyheos==0.7.2
# homeassistant.components.homematic
pyhomematic==0.1.70
@@ -733,8 +745,11 @@ pykira==0.1.1
# homeassistant.components.kodi
pykodi==0.2.1
+# homeassistant.components.kulersky
+pykulersky==0.4.0
+
# homeassistant.components.lastfm
-pylast==3.3.0
+pylast==4.0.0
# homeassistant.components.forked_daapd
pylibrespot-java==0.1.0
@@ -767,7 +782,7 @@ pymodbus==2.3.0
pymonoprice==0.3
# homeassistant.components.myq
-pymyq==2.0.8
+pymyq==2.0.11
# homeassistant.components.nut
pynut2==2.1.2
@@ -819,6 +834,10 @@ pyrisco==0.3.1
# homeassistant.components.ruckus_unleashed
pyruckus==0.12
+# homeassistant.components.serial
+# homeassistant.components.zha
+pyserial-asyncio==0.4
+
# homeassistant.components.acer_projector
# homeassistant.components.zha
pyserial==3.4
@@ -833,16 +852,16 @@ pysma==0.3.5
pysmappee==0.2.13
# homeassistant.components.smartthings
-pysmartapp==0.3.2
+pysmartapp==0.3.3
# homeassistant.components.smartthings
-pysmartthings==0.7.4
+pysmartthings==0.7.6
# homeassistant.components.soma
pysoma==0.0.10
# homeassistant.components.sonos
-pysonos==0.0.36
+pysonos==0.0.37
# homeassistant.components.spc
pyspcwebgw==0.4.0
@@ -866,20 +885,17 @@ python-izone==1.1.2
python-juicenet==1.0.1
# homeassistant.components.xiaomi_miio
-python-miio==0.5.3
+python-miio==0.5.4
# homeassistant.components.nest
python-nest==4.1.0
# homeassistant.components.ozw
-python-openzwave-mqtt==1.3.2
+python-openzwave-mqtt[mqtt-client]==1.4.0
# homeassistant.components.songpal
python-songpal==0.12
-# homeassistant.components.synology_dsm
-python-synology==1.0.0
-
# homeassistant.components.tado
python-tado==0.8.1
@@ -890,7 +906,7 @@ python-twitch-client==0.6.0
python-velbus==2.1.1
# homeassistant.components.awair
-python_awair==0.1.1
+python_awair==0.2.1
# homeassistant.components.tile
pytile==4.0.0
@@ -932,22 +948,22 @@ regenmaschine==3.0.0
restrictedpython==5.0
# homeassistant.components.rflink
-rflink==0.0.54
+rflink==0.0.55
# homeassistant.components.ring
-ring_doorbell==0.6.0
+ring_doorbell==0.6.2
# homeassistant.components.roku
rokuecp==0.6.0
# homeassistant.components.roomba
-roombapy==1.6.1
+roombapy==1.6.2
# homeassistant.components.roon
roonapi==0.0.25
# homeassistant.components.rpi_power
-rpi-bad-power==0.0.3
+rpi-bad-power==0.1.0
# homeassistant.components.yamaha
rxv==0.6.0
@@ -963,7 +979,7 @@ samsungtvws==1.4.0
sense_energy==0.8.1
# homeassistant.components.sentry
-sentry-sdk==0.19.2
+sentry-sdk==0.19.4
# homeassistant.components.sharkiq
sharkiqpy==0.1.8
@@ -972,7 +988,7 @@ sharkiqpy==0.1.8
simplehound==0.3
# homeassistant.components.simplisafe
-simplisafe-python==9.6.0
+simplisafe-python==9.6.2
# homeassistant.components.slack
slackclient==2.5.0
@@ -1014,6 +1030,9 @@ spotipy==2.16.1
# homeassistant.components.sql
sqlalchemy==1.3.20
+# homeassistant.components.srp_energy
+srpenergy==1.3.2
+
# homeassistant.components.starline
starline==0.1.3
@@ -1032,6 +1051,9 @@ sunwatcher==0.2.1
# homeassistant.components.surepetcare
surepy==0.2.6
+# homeassistant.components.synology_dsm
+synologydsm-api==1.0.1
+
# homeassistant.components.tellduslive
tellduslive==0.10.11
@@ -1051,7 +1073,7 @@ total_connect_client==0.55
transmissionrpc==0.11
# homeassistant.components.tuya
-tuyaha==0.0.8
+tuyaha==0.0.9
# homeassistant.components.twentemilieu
twentemilieu==0.3.0
@@ -1059,6 +1081,9 @@ twentemilieu==0.3.0
# homeassistant.components.twilio
twilio==6.32.0
+# homeassistant.components.twinkly
+twinkly-client==0.0.2
+
# homeassistant.components.upb
upb_lib==0.4.11
@@ -1116,10 +1141,10 @@ yeelight==0.5.4
zeep[async]==4.0.0
# homeassistant.components.zeroconf
-zeroconf==0.28.6
+zeroconf==0.28.7
# homeassistant.components.zha
-zha-quirks==0.0.46
+zha-quirks==0.0.49
# homeassistant.components.zha
zigpy-cc==0.5.2
@@ -1134,7 +1159,7 @@ zigpy-xbee==0.13.0
zigpy-zigate==0.7.3
# homeassistant.components.zha
-zigpy-znp==0.2.2
+zigpy-znp==0.3.0
# homeassistant.components.zha
-zigpy==0.27.0
+zigpy==0.28.2
diff --git a/script/bootstrap b/script/bootstrap
index 3166b8c7701..f58268ff1a8 100755
--- a/script/bootstrap
+++ b/script/bootstrap
@@ -6,6 +6,14 @@ set -e
cd "$(dirname "$0")/.."
+# Add default vscode settings if not existing
+SETTINGS_FILE=./.vscode/settings.json
+SETTINGS_TEMPLATE_FILE=./.vscode/settings.default.json
+if [ ! -f "$SETTINGS_FILE" ]; then
+ echo "Copy $SETTINGS_TEMPLATE_FILE to $SETTINGS_FILE."
+ cp "$SETTINGS_TEMPLATE_FILE" "$SETTINGS_FILE"
+fi
+
echo "Installing development dependencies..."
python3 -m pip install wheel --constraint homeassistant/package_constraints.txt
python3 -m pip install tox colorlog pre-commit $(grep mypy requirements_test.txt) $(grep stdlib-list requirements_test.txt) $(grep tqdm requirements_test.txt) $(grep pipdeptree requirements_test.txt) --constraint homeassistant/package_constraints.txt
diff --git a/script/hassfest/services.py b/script/hassfest/services.py
index 1e05ef63efb..c07d3bbc6ef 100644
--- a/script/hassfest/services.py
+++ b/script/hassfest/services.py
@@ -6,8 +6,9 @@ from typing import Dict
import voluptuous as vol
from voluptuous.humanize import humanize_error
+from homeassistant.const import CONF_SELECTOR
from homeassistant.exceptions import HomeAssistantError
-from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers import config_validation as cv, selector
from homeassistant.util.yaml import load_yaml
from .model import Integration
@@ -27,6 +28,7 @@ FIELD_SCHEMA = vol.Schema(
vol.Optional("default"): exists,
vol.Optional("values"): exists,
vol.Optional("required"): bool,
+ vol.Optional(CONF_SELECTOR): selector.validate_selector,
}
)
diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py
index 17da80c3e8a..75886eedc6f 100644
--- a/script/hassfest/translations.py
+++ b/script/hassfest/translations.py
@@ -98,6 +98,7 @@ def gen_data_entry_schema(
},
vol.Optional("error"): {str: cv.string_with_no_html},
vol.Optional("abort"): {str: cv.string_with_no_html},
+ vol.Optional("progress"): {str: cv.string_with_no_html},
vol.Optional("create_entry"): {str: cv.string_with_no_html},
}
if flow_title == REQUIRED:
diff --git a/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py b/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py
index 36a42431cf3..ed974601646 100644
--- a/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py
+++ b/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py
@@ -13,7 +13,9 @@ CLIENT_ID = "1234"
CLIENT_SECRET = "5678"
-async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request):
+async def test_full_flow(
+ hass, aiohttp_client, aioclient_mock, current_request_with_host
+):
"""Check full flow."""
assert await setup.async_setup_component(
hass,
@@ -27,7 +29,13 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request):
result = await hass.config_entries.flow.async_init(
"NEW_DOMAIN", context={"source": config_entries.SOURCE_USER}
)
- state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]})
+ state = config_entry_oauth2_flow._encode_jwt(
+ hass,
+ {
+ "flow_id": result["flow_id"],
+ "redirect_uri": "https://example.com/auth/external/callback",
+ },
+ )
assert result["url"] == (
f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}"
diff --git a/script/scaffold/templates/device_action/integration/device_action.py b/script/scaffold/templates/device_action/integration/device_action.py
index 3861ee8ebe9..27a27cb95ee 100644
--- a/script/scaffold/templates/device_action/integration/device_action.py
+++ b/script/scaffold/templates/device_action/integration/device_action.py
@@ -1,4 +1,4 @@
-"""Provides device automations for NEW_NAME."""
+"""Provides device actions for NEW_NAME."""
from typing import List, Optional
import voluptuous as vol
@@ -72,8 +72,6 @@ async def async_call_action_from_config(
hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context]
) -> None:
"""Execute a device action."""
- config = ACTION_SCHEMA(config)
-
service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]}
if config[CONF_TYPE] == "turn_on":
diff --git a/script/scaffold/templates/device_action/tests/test_device_action.py b/script/scaffold/templates/device_action/tests/test_device_action.py
index 3c7c7bb71a4..91a4693ebeb 100644
--- a/script/scaffold/templates/device_action/tests/test_device_action.py
+++ b/script/scaffold/templates/device_action/tests/test_device_action.py
@@ -1,8 +1,8 @@
"""The tests for NEW_NAME device actions."""
import pytest
+from homeassistant.components import automation
from homeassistant.components.NEW_DOMAIN import DOMAIN
-import homeassistant.components.automation as automation
from homeassistant.helpers import device_registry
from homeassistant.setup import async_setup_component
diff --git a/script/scaffold/templates/device_condition/integration/device_condition.py b/script/scaffold/templates/device_condition/integration/device_condition.py
index cb2489e4279..6ad89332f8e 100644
--- a/script/scaffold/templates/device_condition/integration/device_condition.py
+++ b/script/scaffold/templates/device_condition/integration/device_condition.py
@@ -1,4 +1,4 @@
-"""Provide the device automations for NEW_NAME."""
+"""Provide the device conditions for NEW_NAME."""
from typing import Dict, List
import voluptuous as vol
diff --git a/script/scaffold/templates/device_condition/tests/test_device_condition.py b/script/scaffold/templates/device_condition/tests/test_device_condition.py
index 34217a61f9e..07e0afd05eb 100644
--- a/script/scaffold/templates/device_condition/tests/test_device_condition.py
+++ b/script/scaffold/templates/device_condition/tests/test_device_condition.py
@@ -1,8 +1,8 @@
"""The tests for NEW_NAME device conditions."""
import pytest
+from homeassistant.components import automation
from homeassistant.components.NEW_DOMAIN import DOMAIN
-import homeassistant.components.automation as automation
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.helpers import device_registry
from homeassistant.setup import async_setup_component
diff --git a/script/scaffold/templates/device_trigger/integration/device_trigger.py b/script/scaffold/templates/device_trigger/integration/device_trigger.py
index e1312148cbe..7709813957e 100644
--- a/script/scaffold/templates/device_trigger/integration/device_trigger.py
+++ b/script/scaffold/templates/device_trigger/integration/device_trigger.py
@@ -1,4 +1,4 @@
-"""Provides device automations for NEW_NAME."""
+"""Provides device triggers for NEW_NAME."""
from typing import List
import voluptuous as vol
@@ -80,11 +80,8 @@ async def async_attach_trigger(
automation_info: dict,
) -> CALLBACK_TYPE:
"""Attach a trigger."""
- config = TRIGGER_SCHEMA(config)
-
# TODO Implement your own logic to attach triggers.
- # Generally we suggest to re-use the existing state or event
- # triggers from the automation integration.
+ # Use the existing state or event triggers from the automation integration.
if config[CONF_TYPE] == "turned_on":
from_state = STATE_OFF
diff --git a/script/scaffold/templates/device_trigger/tests/test_device_trigger.py b/script/scaffold/templates/device_trigger/tests/test_device_trigger.py
index 82540566318..23daaf8dadd 100644
--- a/script/scaffold/templates/device_trigger/tests/test_device_trigger.py
+++ b/script/scaffold/templates/device_trigger/tests/test_device_trigger.py
@@ -1,8 +1,8 @@
"""The tests for NEW_NAME device triggers."""
import pytest
+from homeassistant.components import automation
from homeassistant.components.NEW_DOMAIN import DOMAIN
-import homeassistant.components.automation as automation
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.helpers import device_registry
from homeassistant.setup import async_setup_component
diff --git a/setup.cfg b/setup.cfg
index 8286e58c7cf..de5092dcecf 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -40,7 +40,8 @@ warn_incomplete_stub = true
warn_redundant_casts = true
warn_unused_configs = true
-[mypy-homeassistant.block_async_io,homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.__init__,homeassistant.loader,homeassistant.__main__,homeassistant.requirements,homeassistant.runner,homeassistant.setup,homeassistant.util,homeassistant.auth.*,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,homeassistant.components.history.*,homeassistant.components.http.*,homeassistant.components.huawei_lte.*,homeassistant.components.image_processing.*,homeassistant.components.integration.*,homeassistant.components.light.*,homeassistant.components.lock.*,homeassistant.components.mailbox.*,homeassistant.components.media_player.*,homeassistant.components.notify.*,homeassistant.components.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.sun.*,homeassistant.components.switch.*,homeassistant.components.systemmonitor.*,homeassistant.components.tts.*,homeassistant.components.vacuum.*,homeassistant.components.water_heater.*,homeassistant.components.weather.*,homeassistant.components.websocket_api.*,homeassistant.components.zone.*,homeassistant.helpers.*,homeassistant.scripts.*,homeassistant.util.*]
+
+[mypy-homeassistant.block_async_io,homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.__init__,homeassistant.loader,homeassistant.__main__,homeassistant.requirements,homeassistant.runner,homeassistant.setup,homeassistant.util,homeassistant.auth.*,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,homeassistant.components.history.*,homeassistant.components.http.*,homeassistant.components.huawei_lte.*,homeassistant.components.hyperion.*,homeassistant.components.image_processing.*,homeassistant.components.integration.*,homeassistant.components.light.*,homeassistant.components.lock.*,homeassistant.components.mailbox.*,homeassistant.components.media_player.*,homeassistant.components.notify.*,homeassistant.components.number.*,homeassistant.components.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.sun.*,homeassistant.components.switch.*,homeassistant.components.systemmonitor.*,homeassistant.components.tts.*,homeassistant.components.vacuum.*,homeassistant.components.water_heater.*,homeassistant.components.weather.*,homeassistant.components.websocket_api.*,homeassistant.components.zone.*,homeassistant.helpers.*,homeassistant.scripts.*,homeassistant.util.*,tests.components.hyperion.*]
strict = true
ignore_errors = false
warn_unreachable = true
diff --git a/tests/common.py b/tests/common.py
index 611becabe33..66303ad96b3 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -9,7 +9,7 @@ from io import StringIO
import json
import logging
import os
-import sys
+import pathlib
import threading
import time
import uuid
@@ -109,24 +109,21 @@ def get_test_config_dir(*add_path):
def get_test_home_assistant():
"""Return a Home Assistant object pointing at test config directory."""
- if sys.platform == "win32":
- loop = asyncio.ProactorEventLoop()
- else:
- loop = asyncio.new_event_loop()
-
+ loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
hass = loop.run_until_complete(async_test_home_assistant(loop))
- stop_event = threading.Event()
+ loop_stop_event = threading.Event()
def run_loop():
"""Run event loop."""
# pylint: disable=protected-access
loop._thread_ident = threading.get_ident()
loop.run_forever()
- stop_event.set()
+ loop_stop_event.set()
orig_stop = hass.stop
+ hass._stopped = Mock(set=loop.stop)
def start_hass(*mocks):
"""Start hass."""
@@ -135,7 +132,7 @@ def get_test_home_assistant():
def stop_hass():
"""Stop hass."""
orig_stop()
- stop_event.wait()
+ loop_stop_event.wait()
loop.close()
hass.start = start_hass
@@ -198,6 +195,8 @@ async def async_test_home_assistant(loop):
hass.async_add_executor_job = async_add_executor_job
hass.async_create_task = async_create_task
+ hass.data[loader.DATA_CUSTOM_COMPONENTS] = {}
+
hass.config.location_name = "test home"
hass.config.config_dir = get_test_config_dir()
hass.config.latitude = 32.87336
@@ -708,6 +707,9 @@ def patch_yaml_files(files_dict, endswith=True):
def mock_open_f(fname, **_):
"""Mock open() in the yaml module, used by load_yaml."""
# Return the mocked file on full match
+ if isinstance(fname, pathlib.Path):
+ fname = str(fname)
+
if fname in files_dict:
_LOGGER.debug("patch_yaml_files match %s", fname)
res = StringIO(files_dict[fname])
diff --git a/tests/components/abode/test_config_flow.py b/tests/components/abode/test_config_flow.py
index 509a68eda4b..f1445db340f 100644
--- a/tests/components/abode/test_config_flow.py
+++ b/tests/components/abode/test_config_flow.py
@@ -1,9 +1,17 @@
"""Tests for the Abode config flow."""
from abodepy.exceptions import AbodeAuthenticationException
+from abodepy.helpers.errors import MFA_CODE_REQUIRED
from homeassistant import data_entry_flow
from homeassistant.components.abode import config_flow
-from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, HTTP_INTERNAL_SERVER_ERROR
+from homeassistant.components.abode.const import DOMAIN
+from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
+from homeassistant.const import (
+ CONF_PASSWORD,
+ CONF_USERNAME,
+ HTTP_BAD_REQUEST,
+ HTTP_INTERNAL_SERVER_ERROR,
+)
from tests.async_mock import patch
from tests.common import MockConfigEntry
@@ -28,7 +36,7 @@ async def test_one_config_allowed(hass):
flow.hass = hass
MockConfigEntry(
- domain="abode",
+ domain=DOMAIN,
data={CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"},
).add_to_hass(hass)
@@ -58,7 +66,7 @@ async def test_invalid_credentials(hass):
with patch(
"homeassistant.components.abode.config_flow.Abode",
- side_effect=AbodeAuthenticationException((400, "auth error")),
+ side_effect=AbodeAuthenticationException((HTTP_BAD_REQUEST, "auth error")),
):
result = await flow.async_step_user(user_input=conf)
assert result["errors"] == {"base": "invalid_auth"}
@@ -89,13 +97,13 @@ async def test_step_import(hass):
CONF_POLLING: False,
}
- flow = config_flow.AbodeFlowHandler()
- flow.hass = hass
-
- with patch("homeassistant.components.abode.config_flow.Abode"):
- result = await flow.async_step_import(import_config=conf)
+ with patch("homeassistant.components.abode.config_flow.Abode"), patch(
+ "abodepy.UTILS"
+ ):
+ 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
- result = await flow.async_step_user(user_input=result["data"])
assert result["title"] == "user@email.com"
assert result["data"] == {
CONF_USERNAME: "user@email.com",
@@ -108,11 +116,14 @@ async def test_step_user(hass):
"""Test that the user step works."""
conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}
- flow = config_flow.AbodeFlowHandler()
- flow.hass = hass
+ with patch("homeassistant.components.abode.config_flow.Abode"), patch(
+ "abodepy.UTILS"
+ ):
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}, data=conf
+ )
- with patch("homeassistant.components.abode.config_flow.Abode"):
- result = await flow.async_step_user(user_input=conf)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "user@email.com"
assert result["data"] == {
@@ -120,3 +131,78 @@ async def test_step_user(hass):
CONF_PASSWORD: "password",
CONF_POLLING: False,
}
+
+
+async def test_step_mfa(hass):
+ """Test that the MFA step works."""
+ conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}
+
+ with patch(
+ "homeassistant.components.abode.config_flow.Abode",
+ side_effect=AbodeAuthenticationException(MFA_CODE_REQUIRED),
+ ):
+ 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["step_id"] == "mfa"
+
+ with patch(
+ "homeassistant.components.abode.config_flow.Abode",
+ side_effect=AbodeAuthenticationException((HTTP_BAD_REQUEST, "invalid mfa")),
+ ):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={"mfa_code": "123456"}
+ )
+
+ assert result["errors"] == {"base": "invalid_mfa_code"}
+
+ with patch("homeassistant.components.abode.config_flow.Abode"), patch(
+ "abodepy.UTILS"
+ ):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input={"mfa_code": "123456"}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == "user@email.com"
+ assert result["data"] == {
+ CONF_USERNAME: "user@email.com",
+ CONF_PASSWORD: "password",
+ CONF_POLLING: False,
+ }
+
+
+async def test_step_reauth(hass):
+ """Test the reauth flow."""
+ conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}
+
+ MockConfigEntry(
+ domain=DOMAIN,
+ unique_id="user@email.com",
+ data=conf,
+ ).add_to_hass(hass)
+
+ with patch("homeassistant.components.abode.config_flow.Abode"), patch(
+ "abodepy.UTILS"
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": "reauth"},
+ data=conf,
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "reauth_confirm"
+
+ with patch("homeassistant.config_entries.ConfigEntries.async_reload"):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input=conf,
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "reauth_successful"
+
+ assert len(hass.config_entries.async_entries()) == 1
diff --git a/tests/components/abode/test_init.py b/tests/components/abode/test_init.py
index 1598e7bfa91..68f7ce9dd03 100644
--- a/tests/components/abode/test_init.py
+++ b/tests/components/abode/test_init.py
@@ -1,4 +1,6 @@
"""Tests for the Abode module."""
+from abodepy.exceptions import AbodeAuthenticationException
+
from homeassistant.components.abode import (
DOMAIN as ABODE_DOMAIN,
SERVICE_CAPTURE_IMAGE,
@@ -6,6 +8,7 @@ from homeassistant.components.abode import (
SERVICE_TRIGGER_AUTOMATION,
)
from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN
+from homeassistant.const import CONF_USERNAME, HTTP_BAD_REQUEST
from .common import setup_platform
@@ -27,6 +30,22 @@ async def test_change_settings(hass):
mock_set_setting.assert_called_once()
+async def test_add_unique_id(hass):
+ """Test unique_id is set to Abode username."""
+ mock_entry = await setup_platform(hass, ALARM_DOMAIN)
+ # Set unique_id to None to match previous config entries
+ hass.config_entries.async_update_entry(entry=mock_entry, unique_id=None)
+ await hass.async_block_till_done()
+
+ assert mock_entry.unique_id is None
+
+ with patch("abodepy.UTILS"):
+ await hass.config_entries.async_reload(mock_entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert mock_entry.unique_id == mock_entry.data[CONF_USERNAME]
+
+
async def test_unload_entry(hass):
"""Test unloading the Abode entry."""
mock_entry = await setup_platform(hass, ALARM_DOMAIN)
@@ -41,3 +60,16 @@ async def test_unload_entry(hass):
assert not hass.services.has_service(ABODE_DOMAIN, SERVICE_SETTINGS)
assert not hass.services.has_service(ABODE_DOMAIN, SERVICE_CAPTURE_IMAGE)
assert not hass.services.has_service(ABODE_DOMAIN, SERVICE_TRIGGER_AUTOMATION)
+
+
+async def test_invalid_credentials(hass):
+ """Test Abode credentials changing."""
+ with patch(
+ "homeassistant.components.abode.Abode",
+ side_effect=AbodeAuthenticationException((HTTP_BAD_REQUEST, "auth error")),
+ ), patch(
+ "homeassistant.components.abode.config_flow.AbodeFlowHandler.async_step_reauth"
+ ) as mock_async_step_reauth:
+ await setup_platform(hass, ALARM_DOMAIN)
+
+ mock_async_step_reauth.assert_called_once()
diff --git a/tests/components/accuweather/test_system_health.py b/tests/components/accuweather/test_system_health.py
new file mode 100644
index 00000000000..cf8c931e123
--- /dev/null
+++ b/tests/components/accuweather/test_system_health.py
@@ -0,0 +1,58 @@
+"""Test AccuWeather system health."""
+import asyncio
+
+from aiohttp import ClientError
+
+from homeassistant.components.accuweather.const import COORDINATOR, DOMAIN
+from homeassistant.setup import async_setup_component
+
+from tests.async_mock import Mock
+from tests.common import get_system_health_info
+
+
+async def test_accuweather_system_health(hass, aioclient_mock):
+ """Test AccuWeather system health."""
+ aioclient_mock.get("https://dataservice.accuweather.com/", text="")
+ hass.config.components.add(DOMAIN)
+ assert await async_setup_component(hass, "system_health", {})
+
+ hass.data[DOMAIN] = {}
+ hass.data[DOMAIN]["0123xyz"] = {}
+ hass.data[DOMAIN]["0123xyz"][COORDINATOR] = Mock(
+ accuweather=Mock(requests_remaining="42")
+ )
+
+ info = await get_system_health_info(hass, DOMAIN)
+
+ for key, val in info.items():
+ if asyncio.iscoroutine(val):
+ info[key] = await val
+
+ assert info == {
+ "can_reach_server": "ok",
+ "remaining_requests": "42",
+ }
+
+
+async def test_accuweather_system_health_fail(hass, aioclient_mock):
+ """Test AccuWeather system health."""
+ aioclient_mock.get("https://dataservice.accuweather.com/", exc=ClientError)
+ hass.config.components.add(DOMAIN)
+ assert await async_setup_component(hass, "system_health", {})
+
+ hass.data[DOMAIN] = {}
+ hass.data[DOMAIN]["0123xyz"] = {}
+ hass.data[DOMAIN]["0123xyz"][COORDINATOR] = Mock(
+ accuweather=Mock(requests_remaining="0")
+ )
+
+ info = await get_system_health_info(hass, DOMAIN)
+
+ for key, val in info.items():
+ if asyncio.iscoroutine(val):
+ info[key] = await val
+
+ assert info == {
+ "can_reach_server": {"type": "failed", "error": "unreachable"},
+ "remaining_requests": "0",
+ }
diff --git a/tests/components/airly/test_system_health.py b/tests/components/airly/test_system_health.py
new file mode 100644
index 00000000000..b1f8119e880
--- /dev/null
+++ b/tests/components/airly/test_system_health.py
@@ -0,0 +1,50 @@
+"""Test Airly system health."""
+import asyncio
+
+from aiohttp import ClientError
+
+from homeassistant.components.airly.const import DOMAIN
+from homeassistant.setup import async_setup_component
+
+from tests.async_mock import Mock
+from tests.common import get_system_health_info
+
+
+async def test_airly_system_health(hass, aioclient_mock):
+ """Test Airly system health."""
+ aioclient_mock.get("https://airapi.airly.eu/v2/", text="")
+ hass.config.components.add(DOMAIN)
+ assert await async_setup_component(hass, "system_health", {})
+
+ hass.data[DOMAIN] = {}
+ hass.data[DOMAIN]["0123xyz"] = Mock(
+ airly=Mock(AIRLY_API_URL="https://airapi.airly.eu/v2/")
+ )
+
+ info = await get_system_health_info(hass, DOMAIN)
+
+ for key, val in info.items():
+ if asyncio.iscoroutine(val):
+ info[key] = await val
+
+ assert info == {"can_reach_server": "ok"}
+
+
+async def test_airly_system_health_fail(hass, aioclient_mock):
+ """Test Airly system health."""
+ aioclient_mock.get("https://airapi.airly.eu/v2/", exc=ClientError)
+ hass.config.components.add(DOMAIN)
+ assert await async_setup_component(hass, "system_health", {})
+
+ hass.data[DOMAIN] = {}
+ hass.data[DOMAIN]["0123xyz"] = Mock(
+ airly=Mock(AIRLY_API_URL="https://airapi.airly.eu/v2/")
+ )
+
+ info = await get_system_health_info(hass, DOMAIN)
+
+ for key, val in info.items():
+ if asyncio.iscoroutine(val):
+ info[key] = await val
+
+ assert info == {"can_reach_server": {"type": "failed", "error": "unreachable"}}
diff --git a/tests/components/alarm_control_panel/test_device_action.py b/tests/components/alarm_control_panel/test_device_action.py
index 74c98da3189..514e8fa81f2 100644
--- a/tests/components/alarm_control_panel/test_device_action.py
+++ b/tests/components/alarm_control_panel/test_device_action.py
@@ -23,6 +23,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/alarm_control_panel/test_device_condition.py b/tests/components/alarm_control_panel/test_device_condition.py
index fcb2ba5a09b..33f717e1893 100644
--- a/tests/components/alarm_control_panel/test_device_condition.py
+++ b/tests/components/alarm_control_panel/test_device_condition.py
@@ -22,6 +22,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/alarm_control_panel/test_device_trigger.py b/tests/components/alarm_control_panel/test_device_trigger.py
index 9d414d179ab..82432bc37ab 100644
--- a/tests/components/alarm_control_panel/test_device_trigger.py
+++ b/tests/components/alarm_control_panel/test_device_trigger.py
@@ -22,6 +22,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/almond/test_config_flow.py b/tests/components/almond/test_config_flow.py
index b2144205895..afcad55bf2a 100644
--- a/tests/components/almond/test_config_flow.py
+++ b/tests/components/almond/test_config_flow.py
@@ -91,7 +91,9 @@ async def test_abort_if_existing_entry(hass):
assert result["reason"] == "single_instance_allowed"
-async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request):
+async def test_full_flow(
+ hass, aiohttp_client, aioclient_mock, current_request_with_host
+):
"""Check full flow."""
assert await setup.async_setup_component(
hass,
@@ -109,7 +111,13 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
- state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]})
+ state = config_entry_oauth2_flow._encode_jwt(
+ hass,
+ {
+ "flow_id": result["flow_id"],
+ "redirect_uri": "https://example.com/auth/external/callback",
+ },
+ )
assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP
assert result["url"] == (
diff --git a/tests/components/apple_tv/__init__.py b/tests/components/apple_tv/__init__.py
new file mode 100644
index 00000000000..118c3d6f735
--- /dev/null
+++ b/tests/components/apple_tv/__init__.py
@@ -0,0 +1,5 @@
+"""Tests for Apple TV."""
+import pytest
+
+# Make asserts in the common module display differences
+pytest.register_assert_rewrite("tests.components.apple_tv.common")
diff --git a/tests/components/apple_tv/common.py b/tests/components/apple_tv/common.py
new file mode 100644
index 00000000000..6f13239edcb
--- /dev/null
+++ b/tests/components/apple_tv/common.py
@@ -0,0 +1,49 @@
+"""Test code shared between test files."""
+
+from pyatv import conf, interface
+from pyatv.const import Protocol
+
+
+class MockPairingHandler(interface.PairingHandler):
+ """Mock for PairingHandler in pyatv."""
+
+ def __init__(self, *args):
+ """Initialize a new MockPairingHandler."""
+ super().__init__(*args)
+ self.pin_code = None
+ self.paired = False
+ self.always_fail = False
+
+ def pin(self, pin):
+ """Pin code used for pairing."""
+ self.pin_code = pin
+ self.paired = False
+
+ @property
+ def device_provides_pin(self):
+ """Return True if remote device presents PIN code, else False."""
+ return self.service.protocol in [Protocol.MRP, Protocol.AirPlay]
+
+ @property
+ def has_paired(self):
+ """If a successful pairing has been performed.
+
+ The value will be reset when stop() is called.
+ """
+ return not self.always_fail and self.paired
+
+ async def begin(self):
+ """Start pairing process."""
+
+ async def finish(self):
+ """Stop pairing process."""
+ self.paired = True
+ self.service.credentials = self.service.protocol.name.lower() + "_creds"
+
+
+def create_conf(name, address, *services):
+ """Create an Apple TV configuration."""
+ atv = conf.AppleTV(name, address)
+ for service in services:
+ atv.add_service(service)
+ return atv
diff --git a/tests/components/apple_tv/conftest.py b/tests/components/apple_tv/conftest.py
new file mode 100644
index 00000000000..50b57e073d9
--- /dev/null
+++ b/tests/components/apple_tv/conftest.py
@@ -0,0 +1,131 @@
+"""Fixtures for component."""
+
+from pyatv import conf, net
+import pytest
+
+from .common import MockPairingHandler, create_conf
+
+from tests.async_mock import patch
+
+
+@pytest.fixture(autouse=True, name="mock_scan")
+def mock_scan_fixture():
+ """Mock pyatv.scan."""
+ with patch("homeassistant.components.apple_tv.config_flow.scan") as mock_scan:
+
+ async def _scan(loop, timeout=5, identifier=None, protocol=None, hosts=None):
+ if not mock_scan.hosts:
+ mock_scan.hosts = hosts
+ return mock_scan.result
+
+ mock_scan.result = []
+ mock_scan.hosts = None
+ mock_scan.side_effect = _scan
+ yield mock_scan
+
+
+@pytest.fixture(name="dmap_pin")
+def dmap_pin_fixture():
+ """Mock pyatv.scan."""
+ with patch("homeassistant.components.apple_tv.config_flow.randrange") as mock_pin:
+ mock_pin.side_effect = lambda start, stop: 1111
+ yield mock_pin
+
+
+@pytest.fixture
+def pairing():
+ """Mock pyatv.scan."""
+ with patch("homeassistant.components.apple_tv.config_flow.pair") as mock_pair:
+
+ async def _pair(config, protocol, loop, session=None, **kwargs):
+ handler = MockPairingHandler(
+ await net.create_session(session), config.get_service(protocol)
+ )
+ handler.always_fail = mock_pair.always_fail
+ return handler
+
+ mock_pair.always_fail = False
+ mock_pair.side_effect = _pair
+ yield mock_pair
+
+
+@pytest.fixture
+def pairing_mock():
+ """Mock pyatv.scan."""
+ with patch("homeassistant.components.apple_tv.config_flow.pair") as mock_pair:
+
+ async def _pair(config, protocol, loop, session=None, **kwargs):
+ return mock_pair
+
+ async def _begin():
+ pass
+
+ async def _close():
+ pass
+
+ mock_pair.close.side_effect = _close
+ mock_pair.begin.side_effect = _begin
+ mock_pair.pin = lambda pin: None
+ mock_pair.side_effect = _pair
+ yield mock_pair
+
+
+@pytest.fixture
+def full_device(mock_scan, dmap_pin):
+ """Mock pyatv.scan."""
+ mock_scan.result.append(
+ create_conf(
+ "127.0.0.1",
+ "MRP Device",
+ conf.MrpService("mrpid", 5555),
+ conf.DmapService("dmapid", None, port=6666),
+ conf.AirPlayService("airplayid", port=7777),
+ )
+ )
+ yield mock_scan
+
+
+@pytest.fixture
+def mrp_device(mock_scan):
+ """Mock pyatv.scan."""
+ mock_scan.result.append(
+ create_conf("127.0.0.1", "MRP Device", conf.MrpService("mrpid", 5555))
+ )
+ yield mock_scan
+
+
+@pytest.fixture
+def dmap_device(mock_scan):
+ """Mock pyatv.scan."""
+ mock_scan.result.append(
+ create_conf(
+ "127.0.0.1",
+ "DMAP Device",
+ conf.DmapService("dmapid", None, port=6666),
+ )
+ )
+ yield mock_scan
+
+
+@pytest.fixture
+def dmap_device_with_credentials(mock_scan):
+ """Mock pyatv.scan."""
+ mock_scan.result.append(
+ create_conf(
+ "127.0.0.1",
+ "DMAP Device",
+ conf.DmapService("dmapid", "dummy_creds", port=6666),
+ )
+ )
+ yield mock_scan
+
+
+@pytest.fixture
+def airplay_device(mock_scan):
+ """Mock pyatv.scan."""
+ mock_scan.result.append(
+ create_conf(
+ "127.0.0.1", "AirPlay Device", conf.AirPlayService("airplayid", port=7777)
+ )
+ )
+ yield mock_scan
diff --git a/tests/components/apple_tv/test_config_flow.py b/tests/components/apple_tv/test_config_flow.py
new file mode 100644
index 00000000000..50344dc3c05
--- /dev/null
+++ b/tests/components/apple_tv/test_config_flow.py
@@ -0,0 +1,582 @@
+"""Test config flow."""
+
+from pyatv import exceptions
+from pyatv.const import Protocol
+import pytest
+
+from homeassistant import config_entries, data_entry_flow
+from homeassistant.components.apple_tv.const import CONF_START_OFF, DOMAIN
+
+from tests.async_mock import patch
+from tests.common import MockConfigEntry
+
+DMAP_SERVICE = {
+ "type": "_touch-able._tcp.local.",
+ "name": "dmapid.something",
+ "properties": {"CtlN": "Apple TV"},
+}
+
+
+@pytest.fixture(autouse=True)
+def mock_setup_entry():
+ """Mock setting up a config entry."""
+ with patch(
+ "homeassistant.components.apple_tv.async_setup_entry", return_value=True
+ ):
+ yield
+
+
+# User Flows
+
+
+async def test_user_input_device_not_found(hass, mrp_device):
+ """Test when user specifies a non-existing device."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["description_placeholders"] == {"devices": "`MRP Device (127.0.0.1)`"}
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"device_input": "none"},
+ )
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["errors"] == {"base": "no_devices_found"}
+
+
+async def test_user_input_unexpected_error(hass, mock_scan):
+ """Test that unexpected error yields an error message."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ mock_scan.side_effect = Exception
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"device_input": "dummy"},
+ )
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["errors"] == {"base": "unknown"}
+
+
+async def test_user_adds_full_device(hass, full_device, pairing):
+ """Test adding device with all services."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"] == {}
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"device_input": "MRP Device"},
+ )
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["description_placeholders"] == {"name": "MRP Device"}
+
+ result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
+ assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result3["description_placeholders"] == {"protocol": "MRP"}
+
+ result4 = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"pin": 1111}
+ )
+ assert result4["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result4["description_placeholders"] == {"protocol": "DMAP", "pin": 1111}
+
+ result5 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
+ assert result5["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result5["description_placeholders"] == {"protocol": "AirPlay"}
+
+ result6 = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"pin": 1234}
+ )
+ assert result6["type"] == "create_entry"
+ assert result6["data"] == {
+ "address": "127.0.0.1",
+ "credentials": {
+ Protocol.DMAP.value: "dmap_creds",
+ Protocol.MRP.value: "mrp_creds",
+ Protocol.AirPlay.value: "airplay_creds",
+ },
+ "name": "MRP Device",
+ "protocol": Protocol.MRP.value,
+ }
+
+
+async def test_user_adds_dmap_device(hass, dmap_device, dmap_pin, pairing):
+ """Test adding device with only DMAP service."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"device_input": "DMAP Device"},
+ )
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["description_placeholders"] == {"name": "DMAP Device"}
+
+ result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
+ assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result3["description_placeholders"] == {"pin": 1111, "protocol": "DMAP"}
+
+ result6 = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"pin": 1234}
+ )
+ assert result6["type"] == "create_entry"
+ assert result6["data"] == {
+ "address": "127.0.0.1",
+ "credentials": {Protocol.DMAP.value: "dmap_creds"},
+ "name": "DMAP Device",
+ "protocol": Protocol.DMAP.value,
+ }
+
+
+async def test_user_adds_dmap_device_failed(hass, dmap_device, dmap_pin, pairing):
+ """Test adding DMAP device where remote device did not attempt to pair."""
+ pairing.always_fail = True
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"device_input": "DMAP Device"},
+ )
+
+ await hass.config_entries.flow.async_configure(result["flow_id"], {})
+
+ result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result2["reason"] == "device_did_not_pair"
+
+
+async def test_user_adds_device_with_credentials(hass, dmap_device_with_credentials):
+ """Test adding DMAP device with existing credentials (home sharing)."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"device_input": "DMAP Device"},
+ )
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["description_placeholders"] == {"name": "DMAP Device"}
+
+ result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
+ assert result3["type"] == "create_entry"
+ assert result3["data"] == {
+ "address": "127.0.0.1",
+ "credentials": {Protocol.DMAP.value: "dummy_creds"},
+ "name": "DMAP Device",
+ "protocol": Protocol.DMAP.value,
+ }
+
+
+async def test_user_adds_device_with_ip_filter(
+ hass, dmap_device_with_credentials, mock_scan
+):
+ """Test add device filtering by IP."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"device_input": "127.0.0.1"},
+ )
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["description_placeholders"] == {"name": "DMAP Device"}
+
+ result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
+ assert result3["type"] == "create_entry"
+ assert result3["data"] == {
+ "address": "127.0.0.1",
+ "credentials": {Protocol.DMAP.value: "dummy_creds"},
+ "name": "DMAP Device",
+ "protocol": Protocol.DMAP.value,
+ }
+
+
+async def test_user_adds_device_by_ip_uses_unicast_scan(hass, mock_scan):
+ """Test add device by IP-address, verify unicast scan is used."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"device_input": "127.0.0.1"},
+ )
+
+ assert str(mock_scan.hosts[0]) == "127.0.0.1"
+
+
+async def test_user_adds_existing_device(hass, mrp_device):
+ """Test that it is not possible to add existing device."""
+ MockConfigEntry(domain="apple_tv", unique_id="mrpid").add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"device_input": "127.0.0.1"},
+ )
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["errors"] == {"base": "already_configured"}
+
+
+async def test_user_adds_unusable_device(hass, airplay_device):
+ """Test that it is not possible to add pure AirPlay device."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"device_input": "AirPlay Device"},
+ )
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["errors"] == {"base": "no_usable_service"}
+
+
+async def test_user_connection_failed(hass, mrp_device, pairing_mock):
+ """Test error message when connection to device fails."""
+ pairing_mock.begin.side_effect = exceptions.ConnectionFailedError
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"device_input": "MRP Device"},
+ )
+
+ await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {},
+ )
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {},
+ )
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result2["reason"] == "invalid_config"
+
+
+async def test_user_start_pair_error_failed(hass, mrp_device, pairing_mock):
+ """Test initiating pairing fails."""
+ pairing_mock.begin.side_effect = exceptions.PairingError
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"device_input": "MRP Device"},
+ )
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {},
+ )
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result2["reason"] == "invalid_auth"
+
+
+async def test_user_pair_invalid_pin(hass, mrp_device, pairing_mock):
+ """Test pairing with invalid pin."""
+ pairing_mock.finish.side_effect = exceptions.PairingError
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"device_input": "MRP Device"},
+ )
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {},
+ )
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"pin": 1111},
+ )
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["errors"] == {"base": "invalid_auth"}
+
+
+async def test_user_pair_unexpected_error(hass, mrp_device, pairing_mock):
+ """Test unexpected error when entering PIN code."""
+
+ pairing_mock.finish.side_effect = Exception
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"device_input": "MRP Device"},
+ )
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {},
+ )
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"pin": 1111},
+ )
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["errors"] == {"base": "unknown"}
+
+
+async def test_user_pair_backoff_error(hass, mrp_device, pairing_mock):
+ """Test that backoff error is displayed in case device requests it."""
+ pairing_mock.begin.side_effect = exceptions.BackOffError
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"device_input": "MRP Device"},
+ )
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {},
+ )
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result2["reason"] == "backoff"
+
+
+async def test_user_pair_begin_unexpected_error(hass, mrp_device, pairing_mock):
+ """Test unexpected error during start of pairing."""
+ pairing_mock.begin.side_effect = Exception
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {"device_input": "MRP Device"},
+ )
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {},
+ )
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result2["reason"] == "unknown"
+
+
+# Zeroconf
+
+
+async def test_zeroconf_unsupported_service_aborts(hass):
+ """Test discovering unsupported zeroconf service."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_ZEROCONF},
+ data={
+ "type": "_dummy._tcp.local.",
+ "properties": {},
+ },
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "unknown"
+
+
+async def test_zeroconf_add_mrp_device(hass, mrp_device, pairing):
+ """Test add MRP device discovered by zeroconf."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_ZEROCONF},
+ data={
+ "type": "_mediaremotetv._tcp.local.",
+ "properties": {"UniqueIdentifier": "mrpid", "Name": "Kitchen"},
+ },
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["description_placeholders"] == {"name": "MRP Device"}
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {},
+ )
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["description_placeholders"] == {"protocol": "MRP"}
+
+ result3 = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"pin": 1111}
+ )
+ assert result3["type"] == "create_entry"
+ assert result3["data"] == {
+ "address": "127.0.0.1",
+ "credentials": {Protocol.MRP.value: "mrp_creds"},
+ "name": "MRP Device",
+ "protocol": Protocol.MRP.value,
+ }
+
+
+async def test_zeroconf_add_dmap_device(hass, dmap_device, dmap_pin, pairing):
+ """Test add DMAP device discovered by zeroconf."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["description_placeholders"] == {"name": "DMAP Device"}
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {},
+ )
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["description_placeholders"] == {"protocol": "DMAP", "pin": 1111}
+
+ result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
+ assert result3["type"] == "create_entry"
+ assert result3["data"] == {
+ "address": "127.0.0.1",
+ "credentials": {Protocol.DMAP.value: "dmap_creds"},
+ "name": "DMAP Device",
+ "protocol": Protocol.DMAP.value,
+ }
+
+
+async def test_zeroconf_add_existing_aborts(hass, dmap_device):
+ """Test start new zeroconf flow while existing flow is active aborts."""
+ await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE
+ )
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "already_in_progress"
+
+
+async def test_zeroconf_add_but_device_not_found(hass, mock_scan):
+ """Test add device which is not found with another scan."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "no_devices_found"
+
+
+async def test_zeroconf_add_existing_device(hass, dmap_device):
+ """Test add already existing device from zeroconf."""
+ MockConfigEntry(domain="apple_tv", unique_id="dmapid").add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "already_configured"
+
+
+async def test_zeroconf_unexpected_error(hass, mock_scan):
+ """Test unexpected error aborts in zeroconf."""
+ mock_scan.side_effect = Exception
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "unknown"
+
+
+# Re-configuration
+
+
+async def test_reconfigure_update_credentials(hass, mrp_device, pairing):
+ """Test that reconfigure flow updates config entry."""
+ config_entry = MockConfigEntry(domain="apple_tv", unique_id="mrpid")
+ config_entry.add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": "reauth"},
+ data={"identifier": "mrpid", "name": "apple tv"},
+ )
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {},
+ )
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["description_placeholders"] == {"protocol": "MRP"}
+
+ result3 = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"pin": 1111}
+ )
+ assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result3["reason"] == "already_configured"
+
+ assert config_entry.data == {
+ "address": "127.0.0.1",
+ "protocol": Protocol.MRP.value,
+ "name": "MRP Device",
+ "credentials": {Protocol.MRP.value: "mrp_creds"},
+ }
+
+
+async def test_reconfigure_ongoing_aborts(hass, mrp_device):
+ """Test start additional reconfigure flow aborts."""
+ data = {
+ "identifier": "mrpid",
+ "name": "Apple TV",
+ }
+
+ await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "reauth"}, data=data
+ )
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "reauth"}, data=data
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "already_in_progress"
+
+
+# Options
+
+
+async def test_option_start_off(hass):
+ """Test start off-option flag."""
+ config_entry = MockConfigEntry(
+ domain=DOMAIN, unique_id="dmapid", options={"start_off": False}
+ )
+ config_entry.add_to_hass(hass)
+
+ result = await hass.config_entries.options.async_init(config_entry.entry_id)
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+
+ result2 = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input={CONF_START_OFF: True}
+ )
+ assert result2["type"] == "create_entry"
+
+ assert config_entry.options[CONF_START_OFF]
diff --git a/tests/components/arcam_fmj/test_device_trigger.py b/tests/components/arcam_fmj/test_device_trigger.py
index 2a119fd2017..0f2cfaf2893 100644
--- a/tests/components/arcam_fmj/test_device_trigger.py
+++ b/tests/components/arcam_fmj/test_device_trigger.py
@@ -13,6 +13,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/asuswrt/test_sensor.py b/tests/components/asuswrt/test_sensor.py
index 6de3f1b2dcb..7c929992473 100644
--- a/tests/components/asuswrt/test_sensor.py
+++ b/tests/components/asuswrt/test_sensor.py
@@ -1,5 +1,4 @@
"""The tests for the AsusWrt sensor platform."""
-from datetime import timedelta
from aioasuswrt.asuswrt import Device
@@ -16,10 +15,8 @@ from homeassistant.components.asuswrt import (
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
-from homeassistant.util.dt import utcnow
from tests.async_mock import AsyncMock, patch
-from tests.common import async_fire_time_changed
VALID_CONFIG_ROUTER_SSH = {
DOMAIN: {
@@ -62,8 +59,6 @@ async def test_sensors(hass: HomeAssistant, mock_device_tracker_conf):
assert await async_setup_component(hass, DOMAIN, VALID_CONFIG_ROUTER_SSH)
await hass.async_block_till_done()
- async_fire_time_changed(hass, utcnow() + timedelta(seconds=30))
- await hass.async_block_till_done()
assert (
hass.states.get(f"{sensor.DOMAIN}.asuswrt_devices_connected").state == "3"
diff --git a/tests/components/aurora/test_binary_sensor.py b/tests/components/aurora/test_binary_sensor.py
deleted file mode 100644
index d4eea423244..00000000000
--- a/tests/components/aurora/test_binary_sensor.py
+++ /dev/null
@@ -1,60 +0,0 @@
-"""The tests for the Aurora sensor platform."""
-import re
-
-from homeassistant.components.aurora import binary_sensor as aurora
-
-from tests.common import load_fixture
-
-
-def test_setup_and_initial_state(hass, requests_mock):
- """Test that the component is created and initialized as expected."""
- uri = re.compile(r"http://services\.swpc\.noaa\.gov/text/aurora-nowcast-map\.txt")
- requests_mock.get(uri, text=load_fixture("aurora.txt"))
-
- entities = []
-
- def mock_add_entities(new_entities, update_before_add=False):
- """Mock add entities."""
- if update_before_add:
- for entity in new_entities:
- entity.update()
-
- for entity in new_entities:
- entities.append(entity)
-
- config = {"name": "Test", "forecast_threshold": 75}
- aurora.setup_platform(hass, config, mock_add_entities)
-
- aurora_component = entities[0]
- assert len(entities) == 1
- assert aurora_component.name == "Test"
- assert aurora_component.device_state_attributes["visibility_level"] == "0"
- assert aurora_component.device_state_attributes["message"] == "nothing's out"
- assert not aurora_component.is_on
-
-
-def test_custom_threshold_works(hass, requests_mock):
- """Test that the config can take a custom forecast threshold."""
- uri = re.compile(r"http://services\.swpc\.noaa\.gov/text/aurora-nowcast-map\.txt")
- requests_mock.get(uri, text=load_fixture("aurora.txt"))
-
- entities = []
-
- def mock_add_entities(new_entities, update_before_add=False):
- """Mock add entities."""
- if update_before_add:
- for entity in new_entities:
- entity.update()
-
- for entity in new_entities:
- entities.append(entity)
-
- config = {"name": "Test", "forecast_threshold": 1}
- hass.config.longitude = 18.987
- hass.config.latitude = 69.648
-
- aurora.setup_platform(hass, config, mock_add_entities)
-
- aurora_component = entities[0]
- assert aurora_component.aurora_data.visibility_level == "16"
- assert aurora_component.is_on
diff --git a/tests/components/aurora/test_config_flow.py b/tests/components/aurora/test_config_flow.py
new file mode 100644
index 00000000000..2f4b457a9dd
--- /dev/null
+++ b/tests/components/aurora/test_config_flow.py
@@ -0,0 +1,115 @@
+"""Test the Aurora config flow."""
+
+from homeassistant import config_entries, data_entry_flow, setup
+from homeassistant.components.aurora.const import DOMAIN
+
+from tests.async_mock import patch
+from tests.common import MockConfigEntry
+
+DATA = {
+ "name": "Home",
+ "latitude": -10,
+ "longitude": 10.2,
+}
+
+
+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.aurora.config_flow.AuroraForecast.get_forecast_data",
+ return_value=True,
+ ), patch(
+ "homeassistant.components.aurora.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.aurora.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ DATA,
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == "create_entry"
+ assert result2["title"] == "Aurora - Home"
+ assert result2["data"] == DATA
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_form_cannot_connect(hass):
+ """Test if invalid response or no connection returned from the API."""
+
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.aurora.AuroraForecast.get_forecast_data",
+ side_effect=ConnectionError,
+ ):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ DATA,
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+ assert result["errors"] == {"base": "cannot_connect"}
+
+
+async def test_with_unknown_error(hass):
+ """Test with unknown error response from the API."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.aurora.AuroraForecast.get_forecast_data",
+ side_effect=Exception,
+ ):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ DATA,
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+ assert result["errors"] == {"base": "unknown"}
+
+
+async def test_option_flow(hass):
+ """Test option flow."""
+ entry = MockConfigEntry(domain=DOMAIN, data=DATA)
+ entry.add_to_hass(hass)
+
+ assert not entry.options
+
+ with patch("homeassistant.components.aurora.async_setup_entry", return_value=True):
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+ result = await hass.config_entries.options.async_init(
+ entry.entry_id,
+ 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={"forecast_threshold": 65},
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == ""
+ assert result["data"]["forecast_threshold"] == 65
diff --git a/tests/components/automation/conftest.py b/tests/components/automation/conftest.py
new file mode 100644
index 00000000000..a967e0af192
--- /dev/null
+++ b/tests/components/automation/conftest.py
@@ -0,0 +1,3 @@
+"""Conftest for automation tests."""
+
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
diff --git a/tests/components/automation/test_blueprint.py b/tests/components/automation/test_blueprint.py
new file mode 100644
index 00000000000..56062af17b7
--- /dev/null
+++ b/tests/components/automation/test_blueprint.py
@@ -0,0 +1,213 @@
+"""Test built-in blueprints."""
+import asyncio
+import contextlib
+from datetime import timedelta
+import pathlib
+
+from homeassistant.components import automation
+from homeassistant.components.blueprint import models
+from homeassistant.core import callback
+from homeassistant.setup import async_setup_component
+from homeassistant.util import dt as dt_util, yaml
+
+from tests.async_mock import patch
+from tests.common import async_fire_time_changed, async_mock_service
+
+BUILTIN_BLUEPRINT_FOLDER = pathlib.Path(automation.__file__).parent / "blueprints"
+
+
+@contextlib.contextmanager
+def patch_blueprint(blueprint_path: str, data_path):
+ """Patch blueprint loading from a different source."""
+ orig_load = models.DomainBlueprints._load_blueprint
+
+ @callback
+ def mock_load_blueprint(self, path):
+ if path != blueprint_path:
+ assert False, f"Unexpected blueprint {path}"
+ return orig_load(self, path)
+
+ return models.Blueprint(
+ yaml.load_yaml(data_path), expected_domain=self.domain, path=path
+ )
+
+ with patch(
+ "homeassistant.components.blueprint.models.DomainBlueprints._load_blueprint",
+ mock_load_blueprint,
+ ):
+ yield
+
+
+async def test_notify_leaving_zone(hass):
+ """Test notifying leaving a zone blueprint."""
+
+ def set_person_state(state, extra={}):
+ hass.states.async_set(
+ "person.test_person", state, {"friendly_name": "Paulus", **extra}
+ )
+
+ set_person_state("School")
+
+ assert await async_setup_component(
+ hass, "zone", {"zone": {"name": "School", "latitude": 1, "longitude": 2}}
+ )
+
+ with patch_blueprint(
+ "notify_leaving_zone.yaml",
+ BUILTIN_BLUEPRINT_FOLDER / "notify_leaving_zone.yaml",
+ ):
+ assert await async_setup_component(
+ hass,
+ "automation",
+ {
+ "automation": {
+ "use_blueprint": {
+ "path": "notify_leaving_zone.yaml",
+ "input": {
+ "person_entity": "person.test_person",
+ "zone_entity": "zone.school",
+ "notify_device": "abcdefgh",
+ },
+ }
+ }
+ },
+ )
+
+ with patch(
+ "homeassistant.components.mobile_app.device_action.async_call_action_from_config"
+ ) as mock_call_action:
+ # Leaving zone to no zone
+ set_person_state("not_home")
+ await hass.async_block_till_done()
+
+ assert len(mock_call_action.mock_calls) == 1
+ _hass, config, variables, _context = mock_call_action.mock_calls[0][1]
+ message_tpl = config.pop("message")
+ assert config == {
+ "domain": "mobile_app",
+ "type": "notify",
+ "device_id": "abcdefgh",
+ }
+ message_tpl.hass = hass
+ assert message_tpl.async_render(variables) == "Paulus has left School"
+
+ # Should not increase when we go to another zone
+ set_person_state("bla")
+ await hass.async_block_till_done()
+
+ assert len(mock_call_action.mock_calls) == 1
+
+ # Should not increase when we go into the zone
+ set_person_state("School")
+ await hass.async_block_till_done()
+
+ assert len(mock_call_action.mock_calls) == 1
+
+ # Should not increase when we move in the zone
+ set_person_state("School", {"extra_key": "triggers change with same state"})
+ await hass.async_block_till_done()
+
+ assert len(mock_call_action.mock_calls) == 1
+
+ # Should increase when leaving zone for another zone
+ set_person_state("Just Outside School")
+ await hass.async_block_till_done()
+
+ assert len(mock_call_action.mock_calls) == 2
+
+ # Verify trigger works
+ await hass.services.async_call(
+ "automation",
+ "trigger",
+ {"entity_id": "automation.automation_0"},
+ blocking=True,
+ )
+ assert len(mock_call_action.mock_calls) == 3
+
+
+async def test_motion_light(hass):
+ """Test motion light blueprint."""
+ hass.states.async_set("binary_sensor.kitchen", "off")
+
+ with patch_blueprint(
+ "motion_light.yaml",
+ BUILTIN_BLUEPRINT_FOLDER / "motion_light.yaml",
+ ):
+ assert await async_setup_component(
+ hass,
+ "automation",
+ {
+ "automation": {
+ "use_blueprint": {
+ "path": "motion_light.yaml",
+ "input": {
+ "light_target": {"entity_id": "light.kitchen"},
+ "motion_entity": "binary_sensor.kitchen",
+ },
+ }
+ }
+ },
+ )
+
+ turn_on_calls = async_mock_service(hass, "light", "turn_on")
+ turn_off_calls = async_mock_service(hass, "light", "turn_off")
+
+ # Turn on motion
+ hass.states.async_set("binary_sensor.kitchen", "on")
+ # Can't block till done because delay is active
+ # So wait 5 event loop iterations to process script
+ for _ in range(5):
+ await asyncio.sleep(0)
+
+ assert len(turn_on_calls) == 1
+
+ # Test light doesn't turn off if motion stays
+ async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200))
+
+ for _ in range(5):
+ await asyncio.sleep(0)
+
+ assert len(turn_off_calls) == 0
+
+ # Test light turns off off 120s after last motion
+ hass.states.async_set("binary_sensor.kitchen", "off")
+
+ for _ in range(5):
+ await asyncio.sleep(0)
+
+ async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=120))
+ await hass.async_block_till_done()
+
+ assert len(turn_off_calls) == 1
+
+ # Test restarting the script
+ hass.states.async_set("binary_sensor.kitchen", "on")
+
+ for _ in range(5):
+ await asyncio.sleep(0)
+
+ assert len(turn_on_calls) == 2
+ assert len(turn_off_calls) == 1
+
+ hass.states.async_set("binary_sensor.kitchen", "off")
+
+ for _ in range(5):
+ await asyncio.sleep(0)
+
+ hass.states.async_set("binary_sensor.kitchen", "on")
+
+ for _ in range(15):
+ await asyncio.sleep(0)
+
+ assert len(turn_on_calls) == 3
+ assert len(turn_off_calls) == 1
+
+ # Verify trigger works
+ await hass.services.async_call(
+ "automation",
+ "trigger",
+ {"entity_id": "automation.automation_0"},
+ )
+ for _ in range(25):
+ await asyncio.sleep(0)
+ assert len(turn_on_calls) == 4
diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py
index 95667d9a690..5f258fc28b7 100644
--- a/tests/components/automation/test_init.py
+++ b/tests/components/automation/test_init.py
@@ -1254,3 +1254,6 @@ async def test_blueprint_automation(hass, calls):
hass.bus.async_fire("blueprint_event")
await hass.async_block_till_done()
assert len(calls) == 1
+ assert automation.entities_in_automation(hass, "automation.automation_0") == [
+ "light.kitchen"
+ ]
diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py
index 24f888ea6ef..b9dceec7477 100644
--- a/tests/components/axis/test_config_flow.py
+++ b/tests/components/axis/test_config_flow.py
@@ -8,7 +8,7 @@ from homeassistant.components.axis.const import (
DEFAULT_STREAM_PROFILE,
DOMAIN as AXIS_DOMAIN,
)
-from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
+from homeassistant.config_entries import SOURCE_IGNORE, SOURCE_USER, SOURCE_ZEROCONF
from homeassistant.const import (
CONF_HOST,
CONF_MAC,
@@ -31,6 +31,8 @@ from tests.common import MockConfigEntry
async def test_flow_manual_configuration(hass):
"""Test that config flow works."""
+ MockConfigEntry(domain=AXIS_DOMAIN, source=SOURCE_IGNORE).add_to_hass(hass)
+
result = await hass.config_entries.flow.async_init(
AXIS_DOMAIN, context={"source": SOURCE_USER}
)
diff --git a/tests/components/binary_sensor/test_device_condition.py b/tests/components/binary_sensor/test_device_condition.py
index 9fd50277d23..7a7e80e36a3 100644
--- a/tests/components/binary_sensor/test_device_condition.py
+++ b/tests/components/binary_sensor/test_device_condition.py
@@ -20,6 +20,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/binary_sensor/test_device_trigger.py b/tests/components/binary_sensor/test_device_trigger.py
index cea0103acdb..fef109eb9d5 100644
--- a/tests/components/binary_sensor/test_device_trigger.py
+++ b/tests/components/binary_sensor/test_device_trigger.py
@@ -20,6 +20,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/blueprint/conftest.py b/tests/components/blueprint/conftest.py
new file mode 100644
index 00000000000..c8110ddaf08
--- /dev/null
+++ b/tests/components/blueprint/conftest.py
@@ -0,0 +1,14 @@
+"""Blueprints conftest."""
+
+import pytest
+
+from tests.async_mock import patch
+
+
+@pytest.fixture(autouse=True)
+def stub_blueprint_populate():
+ """Stub copying the blueprint automations to the config folder."""
+ with patch(
+ "homeassistant.components.blueprint.models.DomainBlueprints.async_populate"
+ ):
+ yield
diff --git a/tests/components/blueprint/test_default_blueprints.py b/tests/components/blueprint/test_default_blueprints.py
new file mode 100644
index 00000000000..5e4016fcb88
--- /dev/null
+++ b/tests/components/blueprint/test_default_blueprints.py
@@ -0,0 +1,28 @@
+"""Test default blueprints."""
+import importlib
+import logging
+import pathlib
+
+import pytest
+
+from homeassistant.components.blueprint import models
+from homeassistant.components.blueprint.const import BLUEPRINT_FOLDER
+from homeassistant.util import yaml
+
+DOMAINS = ["automation"]
+LOGGER = logging.getLogger(__name__)
+
+
+@pytest.mark.parametrize("domain", DOMAINS)
+def test_default_blueprints(domain: str):
+ """Validate a folder of blueprints."""
+ integration = importlib.import_module(f"homeassistant.components.{domain}")
+ blueprint_folder = pathlib.Path(integration.__file__).parent / BLUEPRINT_FOLDER
+ items = list(blueprint_folder.glob("*"))
+ assert len(items) > 0, "Folder cannot be empty"
+
+ for fil in items:
+ LOGGER.info("Processing %s", fil)
+ assert fil.name.endswith(".yaml")
+ data = yaml.load_yaml(fil)
+ models.Blueprint(data, expected_domain=domain)
diff --git a/tests/components/blueprint/test_importer.py b/tests/components/blueprint/test_importer.py
index 263f82ab230..382363aa560 100644
--- a/tests/components/blueprint/test_importer.py
+++ b/tests/components/blueprint/test_importer.py
@@ -16,6 +16,70 @@ def community_post():
return load_fixture("blueprint/community_post.json")
+COMMUNITY_POST_INPUTS = {
+ "remote": {
+ "name": "Remote",
+ "description": "IKEA remote to use",
+ "selector": {
+ "device": {
+ "integration": "zha",
+ "manufacturer": "IKEA of Sweden",
+ "model": "TRADFRI remote control",
+ }
+ },
+ },
+ "light": {
+ "name": "Light(s)",
+ "description": "The light(s) to control",
+ "selector": {"target": {"entity": {"domain": "light"}}},
+ },
+ "force_brightness": {
+ "name": "Force turn on brightness",
+ "description": 'Force the brightness to the set level below, when the "on" button on the remote is pushed and lights turn on.\n',
+ "default": False,
+ "selector": {"boolean": {}},
+ },
+ "brightness": {
+ "name": "Brightness",
+ "description": "Brightness of the light(s) when turning on",
+ "default": 50,
+ "selector": {
+ "number": {
+ "min": 0.0,
+ "max": 100.0,
+ "mode": "slider",
+ "step": 1.0,
+ "unit_of_measurement": "%",
+ }
+ },
+ },
+ "button_left_short": {
+ "name": "Left button - short press",
+ "description": "Action to run on short left button press",
+ "default": [],
+ "selector": {"action": {}},
+ },
+ "button_left_long": {
+ "name": "Left button - long press",
+ "description": "Action to run on long left button press",
+ "default": [],
+ "selector": {"action": {}},
+ },
+ "button_right_short": {
+ "name": "Right button - short press",
+ "description": "Action to run on short right button press",
+ "default": [],
+ "selector": {"action": {}},
+ },
+ "button_right_long": {
+ "name": "Right button - long press",
+ "description": "Action to run on long right button press",
+ "default": [],
+ "selector": {"action": {}},
+ },
+}
+
+
def test_get_community_post_import_url():
"""Test variations of generating import forum url."""
assert (
@@ -56,12 +120,8 @@ def test_extract_blueprint_from_community_topic(community_post):
"http://example.com", json.loads(community_post)
)
assert imported_blueprint is not None
- assert imported_blueprint.url == "http://example.com"
assert imported_blueprint.blueprint.domain == "automation"
- assert imported_blueprint.blueprint.placeholders == {
- "service_to_call",
- "trigger_event",
- }
+ assert imported_blueprint.blueprint.inputs == COMMUNITY_POST_INPUTS
def test_extract_blueprint_from_community_topic_invalid_yaml():
@@ -79,10 +139,10 @@ def test_extract_blueprint_from_community_topic_invalid_yaml():
)
-def test__extract_blueprint_from_community_topic_wrong_lang():
+def test_extract_blueprint_from_community_topic_wrong_lang():
"""Test extracting blueprint with invalid YAML."""
- assert (
- importer._extract_blueprint_from_community_topic(
+ with pytest.raises(importer.HomeAssistantError):
+ assert importer._extract_blueprint_from_community_topic(
"http://example.com",
{
"post_stream": {
@@ -92,8 +152,6 @@ def test__extract_blueprint_from_community_topic_wrong_lang():
}
},
)
- is None
- )
async def test_fetch_blueprint_from_community_url(hass, aioclient_mock, community_post):
@@ -106,10 +164,16 @@ async def test_fetch_blueprint_from_community_url(hass, aioclient_mock, communit
)
assert isinstance(imported_blueprint, importer.ImportedBlueprint)
assert imported_blueprint.blueprint.domain == "automation"
- assert imported_blueprint.blueprint.placeholders == {
- "service_to_call",
- "trigger_event",
- }
+ assert imported_blueprint.blueprint.inputs == COMMUNITY_POST_INPUTS
+ assert (
+ imported_blueprint.suggested_filename
+ == "frenck/zha-ikea-five-button-remote-for-lights"
+ )
+ assert (
+ imported_blueprint.blueprint.metadata["source_url"]
+ == "https://community.home-assistant.io/t/test-topic/123/2"
+ )
+ assert "gt;" not in imported_blueprint.raw_data
@pytest.mark.parametrize(
@@ -131,7 +195,33 @@ async def test_fetch_blueprint_from_github_url(hass, aioclient_mock, url):
imported_blueprint = await importer.fetch_blueprint_from_url(hass, url)
assert isinstance(imported_blueprint, importer.ImportedBlueprint)
assert imported_blueprint.blueprint.domain == "automation"
- assert imported_blueprint.blueprint.placeholders == {
- "service_to_call",
- "trigger_event",
+ assert imported_blueprint.blueprint.inputs == {
+ "service_to_call": None,
+ "trigger_event": None,
}
+ assert imported_blueprint.suggested_filename == "balloob/motion_light"
+ assert imported_blueprint.blueprint.metadata["source_url"] == url
+
+
+async def test_fetch_blueprint_from_github_gist_url(hass, aioclient_mock):
+ """Test fetching blueprint from url."""
+ aioclient_mock.get(
+ "https://api.github.com/gists/e717ce85dd0d2f1bdcdfc884ea25a344",
+ text=load_fixture("blueprint/github_gist.json"),
+ )
+
+ url = "https://gist.github.com/balloob/e717ce85dd0d2f1bdcdfc884ea25a344"
+ imported_blueprint = await importer.fetch_blueprint_from_url(hass, url)
+ assert isinstance(imported_blueprint, importer.ImportedBlueprint)
+ assert imported_blueprint.blueprint.domain == "automation"
+ assert imported_blueprint.blueprint.inputs == {
+ "motion_entity": {
+ "name": "Motion Sensor",
+ "selector": {
+ "entity": {"domain": "binary_sensor", "device_class": "motion"}
+ },
+ },
+ "light_entity": {"name": "Light", "selector": {"entity": {"domain": "light"}}},
+ }
+ assert imported_blueprint.suggested_filename == "balloob/motion_light"
+ assert imported_blueprint.blueprint.metadata["source_url"] == url
diff --git a/tests/components/blueprint/test_models.py b/tests/components/blueprint/test_models.py
index 56fe13599d7..6e15bd952a4 100644
--- a/tests/components/blueprint/test_models.py
+++ b/tests/components/blueprint/test_models.py
@@ -4,7 +4,7 @@ import logging
import pytest
from homeassistant.components.blueprint import errors, models
-from homeassistant.util.yaml import Placeholder
+from homeassistant.util.yaml import Input
from tests.async_mock import patch
@@ -17,9 +17,30 @@ def blueprint_1():
"blueprint": {
"name": "Hello",
"domain": "automation",
- "input": {"test-placeholder": None},
+ "source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml",
+ "input": {"test-input": {"name": "Name", "description": "Description"}},
},
- "example": Placeholder("test-placeholder"),
+ "example": Input("test-input"),
+ }
+ )
+
+
+@pytest.fixture
+def blueprint_2():
+ """Blueprint fixture with default inputs."""
+ return models.Blueprint(
+ {
+ "blueprint": {
+ "name": "Hello",
+ "domain": "automation",
+ "source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml",
+ "input": {
+ "test-input": {"name": "Name", "description": "Description"},
+ "test-input-default": {"default": "test"},
+ },
+ },
+ "example": Input("test-input"),
+ "example-default": Input("test-input-default"),
}
)
@@ -49,7 +70,7 @@ def test_blueprint_model_init():
"domain": "automation",
"input": {"something": None},
},
- "trigger": {"platform": Placeholder("non-existing")},
+ "trigger": {"platform": Input("non-existing")},
}
)
@@ -59,15 +80,18 @@ def test_blueprint_properties(blueprint_1):
assert blueprint_1.metadata == {
"name": "Hello",
"domain": "automation",
- "input": {"test-placeholder": None},
+ "source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml",
+ "input": {"test-input": {"name": "Name", "description": "Description"}},
}
assert blueprint_1.domain == "automation"
assert blueprint_1.name == "Hello"
- assert blueprint_1.placeholders == {"test-placeholder"}
+ assert blueprint_1.inputs == {
+ "test-input": {"name": "Name", "description": "Description"}
+ }
def test_blueprint_update_metadata():
- """Test properties."""
+ """Test update metadata."""
bp = models.Blueprint(
{
"blueprint": {
@@ -81,15 +105,52 @@ def test_blueprint_update_metadata():
assert bp.metadata["source_url"] == "http://bla.com"
-def test_blueprint_inputs(blueprint_1):
+def test_blueprint_validate():
+ """Test validate blueprint."""
+ assert (
+ models.Blueprint(
+ {
+ "blueprint": {
+ "name": "Hello",
+ "domain": "automation",
+ },
+ }
+ ).validate()
+ is None
+ )
+
+ assert (
+ models.Blueprint(
+ {
+ "blueprint": {
+ "name": "Hello",
+ "domain": "automation",
+ "homeassistant": {"min_version": "100000.0.0"},
+ },
+ }
+ ).validate()
+ == ["Requires at least Home Assistant 100000.0.0"]
+ )
+
+
+def test_blueprint_inputs(blueprint_2):
"""Test blueprint inputs."""
inputs = models.BlueprintInputs(
- blueprint_1,
- {"use_blueprint": {"path": "bla", "input": {"test-placeholder": 1}}},
+ blueprint_2,
+ {
+ "use_blueprint": {
+ "path": "bla",
+ "input": {"test-input": 1, "test-input-default": 12},
+ },
+ "example-default": {"overridden": "via-config"},
+ },
)
inputs.validate()
- assert inputs.inputs == {"test-placeholder": 1}
- assert inputs.async_substitute() == {"example": 1}
+ assert inputs.inputs == {"test-input": 1, "test-input-default": 12}
+ assert inputs.async_substitute() == {
+ "example": 1,
+ "example-default": {"overridden": "via-config"},
+ }
def test_blueprint_inputs_validation(blueprint_1):
@@ -98,10 +159,48 @@ def test_blueprint_inputs_validation(blueprint_1):
blueprint_1,
{"use_blueprint": {"path": "bla", "input": {"non-existing-placeholder": 1}}},
)
- with pytest.raises(errors.MissingPlaceholder):
+ with pytest.raises(errors.MissingInput):
inputs.validate()
+def test_blueprint_inputs_default(blueprint_2):
+ """Test blueprint inputs."""
+ inputs = models.BlueprintInputs(
+ blueprint_2,
+ {"use_blueprint": {"path": "bla", "input": {"test-input": 1}}},
+ )
+ inputs.validate()
+ assert inputs.inputs == {"test-input": 1}
+ assert inputs.inputs_with_default == {
+ "test-input": 1,
+ "test-input-default": "test",
+ }
+ assert inputs.async_substitute() == {"example": 1, "example-default": "test"}
+
+
+def test_blueprint_inputs_override_default(blueprint_2):
+ """Test blueprint inputs."""
+ inputs = models.BlueprintInputs(
+ blueprint_2,
+ {
+ "use_blueprint": {
+ "path": "bla",
+ "input": {"test-input": 1, "test-input-default": "custom"},
+ }
+ },
+ )
+ inputs.validate()
+ assert inputs.inputs == {
+ "test-input": 1,
+ "test-input-default": "custom",
+ }
+ assert inputs.inputs_with_default == {
+ "test-input": 1,
+ "test-input-default": "custom",
+ }
+ assert inputs.async_substitute() == {"example": 1, "example-default": "custom"}
+
+
async def test_domain_blueprints_get_blueprint_errors(hass, domain_bps):
"""Test domain blueprints."""
assert hass.data["blueprint"]["automation"] is domain_bps
@@ -113,8 +212,8 @@ async def test_domain_blueprints_get_blueprint_errors(hass, domain_bps):
with patch(
"homeassistant.util.yaml.load_yaml", return_value={"blueprint": "invalid"}
- ):
- assert await domain_bps.async_get_blueprint("non-existing-path") is None
+ ), pytest.raises(errors.FailedToLoad):
+ await domain_bps.async_get_blueprint("non-existing-path")
async def test_domain_blueprints_caching(domain_bps):
@@ -139,7 +238,7 @@ async def test_domain_blueprints_inputs_from_config(domain_bps, blueprint_1):
with pytest.raises(errors.InvalidBlueprintInputs):
await domain_bps.async_inputs_from_config({"not-referencing": "use_blueprint"})
- with pytest.raises(errors.MissingPlaceholder), patch.object(
+ with pytest.raises(errors.MissingInput), patch.object(
domain_bps, "async_get_blueprint", return_value=blueprint_1
):
await domain_bps.async_inputs_from_config(
@@ -148,7 +247,31 @@ async def test_domain_blueprints_inputs_from_config(domain_bps, blueprint_1):
with patch.object(domain_bps, "async_get_blueprint", return_value=blueprint_1):
inputs = await domain_bps.async_inputs_from_config(
- {"use_blueprint": {"path": "bla.yaml", "input": {"test-placeholder": None}}}
+ {"use_blueprint": {"path": "bla.yaml", "input": {"test-input": None}}}
)
assert inputs.blueprint is blueprint_1
- assert inputs.inputs == {"test-placeholder": None}
+ assert inputs.inputs == {"test-input": None}
+
+
+async def test_domain_blueprints_add_blueprint(domain_bps, blueprint_1):
+ """Test DomainBlueprints.async_add_blueprint."""
+ with patch.object(domain_bps, "_create_file") as create_file_mock:
+ # Should add extension when not present.
+ await domain_bps.async_add_blueprint(blueprint_1, "something")
+ assert create_file_mock.call_args[0][1] == ("something.yaml")
+
+ await domain_bps.async_add_blueprint(blueprint_1, "something2.yaml")
+ assert create_file_mock.call_args[0][1] == ("something2.yaml")
+
+ # Should be in cache.
+ with patch.object(domain_bps, "_load_blueprint") as mock_load:
+ assert await domain_bps.async_get_blueprint("something.yaml") == blueprint_1
+ assert not mock_load.mock_calls
+
+
+async def test_inputs_from_config_nonexisting_blueprint(domain_bps):
+ """Test referring non-existing blueprint."""
+ with pytest.raises(errors.FailedToLoad):
+ await domain_bps.async_inputs_from_config(
+ {"use_blueprint": {"path": "non-existing.yaml"}}
+ )
diff --git a/tests/components/blueprint/test_schemas.py b/tests/components/blueprint/test_schemas.py
index bf50bdad975..7c91c1e4117 100644
--- a/tests/components/blueprint/test_schemas.py
+++ b/tests/components/blueprint/test_schemas.py
@@ -31,6 +31,26 @@ _LOGGER = logging.getLogger(__name__)
},
}
},
+ # With selector
+ {
+ "blueprint": {
+ "name": "Test Name",
+ "domain": "automation",
+ "input": {
+ "some_placeholder": {"selector": {"entity": {}}},
+ },
+ }
+ },
+ # With min version
+ {
+ "blueprint": {
+ "name": "Test Name",
+ "domain": "automation",
+ "homeassistant": {
+ "min_version": "1000000.0.0",
+ },
+ }
+ },
),
)
def test_blueprint_schema(blueprint):
@@ -63,9 +83,32 @@ def test_blueprint_schema(blueprint):
"input": {"some_placeholder": {"non_existing": "bla"}},
}
},
+ # Invalid version
+ {
+ "blueprint": {
+ "name": "Test Name",
+ "domain": "automation",
+ "homeassistant": {
+ "min_version": "1000000.invalid.0",
+ },
+ }
+ },
),
)
def test_blueprint_schema_invalid(blueprint):
"""Test different schemas."""
with pytest.raises(vol.Invalid):
schemas.BLUEPRINT_SCHEMA(blueprint)
+
+
+@pytest.mark.parametrize(
+ "bp_instance",
+ (
+ {"path": "hello.yaml"},
+ {"path": "hello.yaml", "input": {}},
+ {"path": "hello.yaml", "input": {"hello": None}},
+ ),
+)
+def test_blueprint_instance_fields(bp_instance):
+ """Test blueprint instance fields."""
+ schemas.BLUEPRINT_INSTANCE_FIELDS({"use_blueprint": bp_instance})
diff --git a/tests/components/blueprint/test_websocket_api.py b/tests/components/blueprint/test_websocket_api.py
index 54fcc0a5891..bb08414b6e8 100644
--- a/tests/components/blueprint/test_websocket_api.py
+++ b/tests/components/blueprint/test_websocket_api.py
@@ -3,9 +3,10 @@ from pathlib import Path
import pytest
-from homeassistant.components import automation
from homeassistant.setup import async_setup_component
+from tests.async_mock import Mock, patch
+
@pytest.fixture(autouse=True)
async def setup_bp(hass):
@@ -13,20 +14,20 @@ async def setup_bp(hass):
assert await async_setup_component(hass, "blueprint", {})
# Trigger registration of automation blueprints
- automation.async_get_blueprints(hass)
+ await async_setup_component(hass, "automation", {})
async def test_list_blueprints(hass, hass_ws_client):
"""Test listing blueprints."""
client = await hass_ws_client(hass)
- await client.send_json({"id": 5, "type": "blueprint/list"})
+ await client.send_json({"id": 5, "type": "blueprint/list", "domain": "automation"})
msg = await client.receive_json()
assert msg["id"] == 5
assert msg["success"]
blueprints = msg["result"]
- assert blueprints.get("automation") == {
+ assert blueprints == {
"test_event_service.yaml": {
"metadata": {
"domain": "automation",
@@ -44,8 +45,23 @@ async def test_list_blueprints(hass, hass_ws_client):
}
-async def test_import_blueprint(hass, aioclient_mock, hass_ws_client):
+async def test_list_blueprints_non_existing_domain(hass, hass_ws_client):
"""Test listing blueprints."""
+ client = await hass_ws_client(hass)
+ await client.send_json(
+ {"id": 5, "type": "blueprint/list", "domain": "not_existsing"}
+ )
+
+ msg = await client.receive_json()
+
+ assert msg["id"] == 5
+ assert msg["success"]
+ blueprints = msg["result"]
+ assert blueprints == {}
+
+
+async def test_import_blueprint(hass, aioclient_mock, hass_ws_client):
+ """Test importing blueprints."""
raw_data = Path(
hass.config.path("blueprints/automation/test_event_service.yaml")
).read_text()
@@ -69,14 +85,152 @@ async def test_import_blueprint(hass, aioclient_mock, hass_ws_client):
assert msg["id"] == 5
assert msg["success"]
assert msg["result"] == {
- "suggested_filename": "balloob-motion_light",
- "url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml",
+ "suggested_filename": "balloob/motion_light",
"raw_data": raw_data,
"blueprint": {
"metadata": {
"domain": "automation",
"input": {"service_to_call": None, "trigger_event": None},
"name": "Call service based on event",
+ "source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml",
},
},
+ "validation_errors": None,
}
+
+
+async def test_save_blueprint(hass, aioclient_mock, hass_ws_client):
+ """Test saving blueprints."""
+ raw_data = Path(
+ hass.config.path("blueprints/automation/test_event_service.yaml")
+ ).read_text()
+
+ with patch("pathlib.Path.write_text") as write_mock:
+ client = await hass_ws_client(hass)
+ await client.send_json(
+ {
+ "id": 6,
+ "type": "blueprint/save",
+ "path": "test_save",
+ "yaml": raw_data,
+ "domain": "automation",
+ "source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml",
+ }
+ )
+
+ msg = await client.receive_json()
+
+ assert msg["id"] == 6
+ assert msg["success"]
+ assert write_mock.mock_calls
+ assert write_mock.call_args[0] == (
+ "blueprint:\n name: Call service based on event\n domain: automation\n input:\n trigger_event:\n service_to_call:\n source_url: https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n platform: event\n event_type: !input 'trigger_event'\naction:\n service: !input 'service_to_call'\n entity_id: light.kitchen\n",
+ )
+
+
+async def test_save_existing_file(hass, aioclient_mock, hass_ws_client):
+ """Test saving blueprints."""
+
+ client = await hass_ws_client(hass)
+ await client.send_json(
+ {
+ "id": 7,
+ "type": "blueprint/save",
+ "path": "test_event_service",
+ "yaml": 'blueprint: {name: "name", domain: "automation"}',
+ "domain": "automation",
+ "source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml",
+ }
+ )
+
+ msg = await client.receive_json()
+
+ assert msg["id"] == 7
+ assert not msg["success"]
+ assert msg["error"] == {"code": "already_exists", "message": "File already exists"}
+
+
+async def test_save_file_error(hass, aioclient_mock, hass_ws_client):
+ """Test saving blueprints with OS error."""
+ with patch("pathlib.Path.write_text", side_effect=OSError):
+ client = await hass_ws_client(hass)
+ await client.send_json(
+ {
+ "id": 8,
+ "type": "blueprint/save",
+ "path": "test_save",
+ "yaml": "raw_data",
+ "domain": "automation",
+ "source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml",
+ }
+ )
+
+ msg = await client.receive_json()
+
+ assert msg["id"] == 8
+ assert not msg["success"]
+
+
+async def test_save_invalid_blueprint(hass, aioclient_mock, hass_ws_client):
+ """Test saving invalid blueprints."""
+
+ client = await hass_ws_client(hass)
+ await client.send_json(
+ {
+ "id": 8,
+ "type": "blueprint/save",
+ "path": "test_wrong",
+ "yaml": "wrong_blueprint",
+ "domain": "automation",
+ "source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml",
+ }
+ )
+
+ msg = await client.receive_json()
+
+ assert msg["id"] == 8
+ assert not msg["success"]
+ assert msg["error"] == {
+ "code": "invalid_format",
+ "message": "Invalid blueprint: expected a dictionary. Got 'wrong_blueprint'",
+ }
+
+
+async def test_delete_blueprint(hass, aioclient_mock, hass_ws_client):
+ """Test deleting blueprints."""
+
+ with patch("pathlib.Path.unlink", return_value=Mock()) as unlink_mock:
+ client = await hass_ws_client(hass)
+ await client.send_json(
+ {
+ "id": 9,
+ "type": "blueprint/delete",
+ "path": "test_delete",
+ "domain": "automation",
+ }
+ )
+
+ msg = await client.receive_json()
+
+ assert unlink_mock.mock_calls
+ assert msg["id"] == 9
+ assert msg["success"]
+
+
+async def test_delete_non_exist_file_blueprint(hass, aioclient_mock, hass_ws_client):
+ """Test deleting non existing blueprints."""
+
+ client = await hass_ws_client(hass)
+ await client.send_json(
+ {
+ "id": 9,
+ "type": "blueprint/delete",
+ "path": "none_existing",
+ "domain": "automation",
+ }
+ )
+
+ msg = await client.receive_json()
+
+ assert msg["id"] == 9
+ assert not msg["success"]
diff --git a/tests/components/brother/__init__.py b/tests/components/brother/__init__.py
index 1a3ba2a3e20..b24ef97705b 100644
--- a/tests/components/brother/__init__.py
+++ b/tests/components/brother/__init__.py
@@ -8,7 +8,7 @@ from tests.async_mock import patch
from tests.common import MockConfigEntry, load_fixture
-async def init_integration(hass) -> MockConfigEntry:
+async def init_integration(hass, skip_setup=False) -> MockConfigEntry:
"""Set up the Brother integration in Home Assistant."""
entry = MockConfigEntry(
domain=DOMAIN,
@@ -16,12 +16,15 @@ async def init_integration(hass) -> MockConfigEntry:
unique_id="0123456789",
data={CONF_HOST: "localhost", CONF_TYPE: "laser"},
)
- with patch(
- "brother.Brother._get_data",
- return_value=json.loads(load_fixture("brother_printer_data.json")),
- ):
- entry.add_to_hass(hass)
- await hass.config_entries.async_setup(entry.entry_id)
- await hass.async_block_till_done()
+
+ entry.add_to_hass(hass)
+
+ if not skip_setup:
+ with patch(
+ "brother.Brother._get_data",
+ return_value=json.loads(load_fixture("brother_printer_data.json")),
+ ):
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
return entry
diff --git a/tests/components/brother/test_sensor.py b/tests/components/brother/test_sensor.py
index c8dc91ebcf4..3f9ee9394b7 100644
--- a/tests/components/brother/test_sensor.py
+++ b/tests/components/brother/test_sensor.py
@@ -2,7 +2,8 @@
from datetime import datetime, timedelta
import json
-from homeassistant.components.brother.const import UNIT_PAGES
+from homeassistant.components.brother.const import DOMAIN, UNIT_PAGES
+from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID,
@@ -15,7 +16,7 @@ from homeassistant.const import (
from homeassistant.setup import async_setup_component
from homeassistant.util.dt import UTC, utcnow
-from tests.async_mock import patch
+from tests.async_mock import Mock, patch
from tests.common import async_fire_time_changed, load_fixture
from tests.components.brother import init_integration
@@ -25,14 +26,26 @@ ATTR_COUNTER = "counter"
async def test_sensors(hass):
"""Test states of the sensors."""
- test_time = datetime(2019, 11, 11, 9, 10, 32, tzinfo=UTC)
- with patch(
- "homeassistant.components.brother.sensor.utcnow", return_value=test_time
- ):
- await init_integration(hass)
+ entry = await init_integration(hass, skip_setup=True)
registry = await hass.helpers.entity_registry.async_get_registry()
+ # Pre-create registry entries for disabled by default sensors
+ registry.async_get_or_create(
+ SENSOR_DOMAIN,
+ DOMAIN,
+ "0123456789_uptime",
+ suggested_object_id="hl_l2340dw_uptime",
+ disabled_by=None,
+ )
+ test_time = datetime(2019, 11, 11, 9, 10, 32, tzinfo=UTC)
+ with patch("brother.datetime", utcnow=Mock(return_value=test_time)), patch(
+ "brother.Brother._get_data",
+ return_value=json.loads(load_fixture("brother_printer_data.json")),
+ ):
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
state = hass.states.get("sensor.hl_l2340dw_status")
assert state
assert state.attributes.get(ATTR_ICON) == "mdi:printer"
@@ -224,6 +237,21 @@ async def test_sensors(hass):
assert entry.unique_id == "0123456789_uptime"
+async def test_disabled_by_default_sensors(hass):
+ """Test the disabled by default Brother sensors."""
+ await init_integration(hass)
+
+ registry = await hass.helpers.entity_registry.async_get_registry()
+ state = hass.states.get("sensor.hl_l2340dw_uptime")
+ assert state is None
+
+ entry = registry.async_get("sensor.hl_l2340dw_uptime")
+ assert entry
+ assert entry.unique_id == "0123456789_uptime"
+ assert entry.disabled
+ assert entry.disabled_by == "integration"
+
+
async def test_availability(hass):
"""Ensure that we mark the entities unavailable correctly when device is offline."""
await init_integration(hass)
diff --git a/tests/components/bsblan/__init__.py b/tests/components/bsblan/__init__.py
index 511b566ce41..f2e88d97ba2 100644
--- a/tests/components/bsblan/__init__.py
+++ b/tests/components/bsblan/__init__.py
@@ -5,7 +5,13 @@ from homeassistant.components.bsblan.const import (
CONF_PASSKEY,
DOMAIN,
)
-from homeassistant.const import CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON
+from homeassistant.const import (
+ CONF_HOST,
+ CONF_PASSWORD,
+ CONF_PORT,
+ CONF_USERNAME,
+ CONTENT_TYPE_JSON,
+)
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry, load_fixture
@@ -26,6 +32,42 @@ async def init_integration(
headers={"Content-Type": CONTENT_TYPE_JSON},
)
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ unique_id="RVS21.831F/127",
+ data={
+ CONF_HOST: "example.local",
+ CONF_USERNAME: "nobody",
+ CONF_PASSWORD: "qwerty",
+ CONF_PASSKEY: "1234",
+ CONF_PORT: 80,
+ CONF_DEVICE_IDENT: "RVS21.831F/127",
+ },
+ )
+
+ 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
+
+
+async def init_integration_without_auth(
+ hass: HomeAssistant,
+ aioclient_mock: AiohttpClientMocker,
+ skip_setup: bool = False,
+) -> MockConfigEntry:
+ """Set up the BSBLan integration in Home Assistant."""
+
+ aioclient_mock.post(
+ "http://example.local:80/1234/JQ?Parameter=6224,6225,6226",
+ params={"Parameter": "6224,6225,6226"},
+ text=load_fixture("bsblan/info.json"),
+ headers={"Content-Type": CONTENT_TYPE_JSON},
+ )
+
entry = MockConfigEntry(
domain=DOMAIN,
unique_id="RVS21.831F/127",
diff --git a/tests/components/bsblan/test_config_flow.py b/tests/components/bsblan/test_config_flow.py
index 4c04db012ba..38485fb7959 100644
--- a/tests/components/bsblan/test_config_flow.py
+++ b/tests/components/bsblan/test_config_flow.py
@@ -5,7 +5,13 @@ from homeassistant import data_entry_flow
from homeassistant.components.bsblan import config_flow
from homeassistant.components.bsblan.const import CONF_DEVICE_IDENT, CONF_PASSKEY
from homeassistant.config_entries import SOURCE_USER
-from homeassistant.const import CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON
+from homeassistant.const import (
+ CONF_HOST,
+ CONF_PASSWORD,
+ CONF_PORT,
+ CONF_USERNAME,
+ CONTENT_TYPE_JSON,
+)
from homeassistant.core import HomeAssistant
from . import init_integration
@@ -37,7 +43,13 @@ async def test_connection_error(
result = await hass.config_entries.flow.async_init(
config_flow.DOMAIN,
context={"source": SOURCE_USER},
- data={CONF_HOST: "example.local", CONF_PASSKEY: "1234", CONF_PORT: 80},
+ data={
+ CONF_HOST: "example.local",
+ CONF_USERNAME: "nobody",
+ CONF_PASSWORD: "qwerty",
+ CONF_PASSKEY: "1234",
+ CONF_PORT: 80,
+ },
)
assert result["errors"] == {"base": "cannot_connect"}
@@ -54,7 +66,13 @@ async def test_user_device_exists_abort(
result = await hass.config_entries.flow.async_init(
config_flow.DOMAIN,
context={"source": SOURCE_USER},
- data={CONF_HOST: "example.local", CONF_PASSKEY: "1234", CONF_PORT: 80},
+ data={
+ CONF_HOST: "example.local",
+ CONF_USERNAME: "nobody",
+ CONF_PASSWORD: "qwerty",
+ CONF_PASSKEY: "1234",
+ CONF_PORT: 80,
+ },
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
@@ -80,10 +98,18 @@ async def test_full_user_flow_implementation(
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
- user_input={CONF_HOST: "example.local", CONF_PASSKEY: "1234", CONF_PORT: 80},
+ user_input={
+ CONF_HOST: "example.local",
+ CONF_USERNAME: "nobody",
+ CONF_PASSWORD: "qwerty",
+ CONF_PASSKEY: "1234",
+ CONF_PORT: 80,
+ },
)
assert result["data"][CONF_HOST] == "example.local"
+ assert result["data"][CONF_USERNAME] == "nobody"
+ assert result["data"][CONF_PASSWORD] == "qwerty"
assert result["data"][CONF_PASSKEY] == "1234"
assert result["data"][CONF_PORT] == 80
assert result["data"][CONF_DEVICE_IDENT] == "RVS21.831F/127"
@@ -92,3 +118,42 @@ async def test_full_user_flow_implementation(
entries = hass.config_entries.async_entries(config_flow.DOMAIN)
assert entries[0].unique_id == "RVS21.831F/127"
+
+
+async def test_full_user_flow_implementation_without_auth(
+ hass: HomeAssistant, aioclient_mock
+) -> None:
+ """Test the full manual user flow from start to finish."""
+ aioclient_mock.post(
+ "http://example2.local:80/JQ?Parameter=6224,6225,6226",
+ text=load_fixture("bsblan/info.json"),
+ headers={"Content-Type": CONTENT_TYPE_JSON},
+ )
+
+ result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN,
+ context={"source": SOURCE_USER},
+ )
+
+ assert result["step_id"] == "user"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input={
+ CONF_HOST: "example2.local",
+ CONF_PORT: 80,
+ },
+ )
+
+ assert result["data"][CONF_HOST] == "example2.local"
+ assert result["data"][CONF_USERNAME] is None
+ assert result["data"][CONF_PASSWORD] is None
+ assert result["data"][CONF_PASSKEY] is None
+ assert result["data"][CONF_PORT] == 80
+ assert result["data"][CONF_DEVICE_IDENT] == "RVS21.831F/127"
+ assert result["title"] == "RVS21.831F/127"
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+ entries = hass.config_entries.async_entries(config_flow.DOMAIN)
+ assert entries[0].unique_id == "RVS21.831F/127"
diff --git a/tests/components/bsblan/test_init.py b/tests/components/bsblan/test_init.py
new file mode 100644
index 00000000000..b6096ced0ac
--- /dev/null
+++ b/tests/components/bsblan/test_init.py
@@ -0,0 +1,47 @@
+"""Tests for the BSBLan integration."""
+import aiohttp
+
+from homeassistant.components.bsblan.const import DOMAIN
+from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY
+from homeassistant.core import HomeAssistant
+
+from tests.components.bsblan import init_integration, init_integration_without_auth
+from tests.test_util.aiohttp import AiohttpClientMocker
+
+
+async def test_config_entry_not_ready(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+) -> None:
+ """Test the BSBLan configuration entry not ready."""
+ aioclient_mock.post(
+ "http://example.local:80/1234/JQ?Parameter=6224,6225,6226",
+ exc=aiohttp.ClientError,
+ )
+
+ entry = await init_integration(hass, aioclient_mock)
+ assert entry.state == ENTRY_STATE_SETUP_RETRY
+
+
+async def test_unload_config_entry(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+) -> None:
+ """Test the BSBLan 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)
+
+
+async def test_config_entry_no_authentication(
+ hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
+) -> None:
+ """Test the BSBLan configuration entry not ready."""
+ aioclient_mock.post(
+ "http://example.local:80/1234/JQ?Parameter=6224,6225,6226",
+ exc=aiohttp.ClientError,
+ )
+
+ entry = await init_integration_without_auth(hass, aioclient_mock)
+ assert entry.state == ENTRY_STATE_SETUP_RETRY
diff --git a/tests/components/cert_expiry/test_init.py b/tests/components/cert_expiry/test_init.py
index 2f5e4ce9a1c..6aa8568a9d1 100644
--- a/tests/components/cert_expiry/test_init.py
+++ b/tests/components/cert_expiry/test_init.py
@@ -76,21 +76,22 @@ async def test_unload_config_entry(mock_now, hass):
assert len(config_entries) == 1
assert entry is config_entries[0]
+ timestamp = future_timestamp(100)
with patch(
"homeassistant.components.cert_expiry.get_cert_expiry_timestamp",
- return_value=future_timestamp(100),
+ return_value=timestamp,
):
assert await async_setup_component(hass, DOMAIN, {}) is True
await hass.async_block_till_done()
assert entry.state == ENTRY_STATE_LOADED
- state = hass.states.get("sensor.cert_expiry_example_com")
- assert state.state == "100"
+ state = hass.states.get("sensor.cert_expiry_timestamp_example_com")
+ assert state.state == timestamp.isoformat()
assert state.attributes.get("error") == "None"
assert state.attributes.get("is_valid")
await hass.config_entries.async_unload(entry.entry_id)
assert entry.state == ENTRY_STATE_NOT_LOADED
- state = hass.states.get("sensor.cert_expiry_example_com")
+ state = hass.states.get("sensor.cert_expiry_timestamp_example_com")
assert state is None
diff --git a/tests/components/cert_expiry/test_sensors.py b/tests/components/cert_expiry/test_sensors.py
index 76c6716411b..4a78f02b39c 100644
--- a/tests/components/cert_expiry/test_sensors.py
+++ b/tests/components/cert_expiry/test_sensors.py
@@ -34,13 +34,6 @@ async def test_async_setup_entry(mock_now, hass):
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
- state = hass.states.get("sensor.cert_expiry_example_com")
- assert state is not None
- assert state.state != STATE_UNAVAILABLE
- assert state.state == "100"
- assert state.attributes.get("error") == "None"
- assert state.attributes.get("is_valid")
-
state = hass.states.get("sensor.cert_expiry_timestamp_example_com")
assert state is not None
assert state.state != STATE_UNAVAILABLE
@@ -65,10 +58,9 @@ async def test_async_setup_entry_bad_cert(hass):
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
- state = hass.states.get("sensor.cert_expiry_example_com")
+ state = hass.states.get("sensor.cert_expiry_timestamp_example_com")
assert state is not None
assert state.state != STATE_UNAVAILABLE
- assert state.state == "0"
assert state.attributes.get("error") == "some error"
assert not state.attributes.get("is_valid")
@@ -99,7 +91,7 @@ async def test_async_setup_entry_host_unavailable(hass):
):
await hass.async_block_till_done()
- state = hass.states.get("sensor.cert_expiry_example_com")
+ state = hass.states.get("sensor.cert_expiry_timestamp_example_com")
assert state is None
@@ -122,13 +114,6 @@ async def test_update_sensor(hass):
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
- state = hass.states.get("sensor.cert_expiry_example_com")
- assert state is not None
- assert state.state != STATE_UNAVAILABLE
- assert state.state == "100"
- assert state.attributes.get("error") == "None"
- assert state.attributes.get("is_valid")
-
state = hass.states.get("sensor.cert_expiry_timestamp_example_com")
assert state is not None
assert state.state != STATE_UNAVAILABLE
@@ -144,13 +129,6 @@ async def test_update_sensor(hass):
async_fire_time_changed(hass, utcnow() + timedelta(hours=24))
await hass.async_block_till_done()
- state = hass.states.get("sensor.cert_expiry_example_com")
- assert state is not None
- assert state.state != STATE_UNAVAILABLE
- assert state.state == "99"
- assert state.attributes.get("error") == "None"
- assert state.attributes.get("is_valid")
-
state = hass.states.get("sensor.cert_expiry_timestamp_example_com")
assert state is not None
assert state.state != STATE_UNAVAILABLE
@@ -178,13 +156,6 @@ async def test_update_sensor_network_errors(hass):
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
- state = hass.states.get("sensor.cert_expiry_example_com")
- assert state is not None
- assert state.state != STATE_UNAVAILABLE
- assert state.state == "100"
- assert state.attributes.get("error") == "None"
- assert state.attributes.get("is_valid")
-
state = hass.states.get("sensor.cert_expiry_timestamp_example_com")
assert state is not None
assert state.state != STATE_UNAVAILABLE
@@ -203,7 +174,7 @@ async def test_update_sensor_network_errors(hass):
next_update = starting_time + timedelta(hours=48)
- state = hass.states.get("sensor.cert_expiry_example_com")
+ state = hass.states.get("sensor.cert_expiry_timestamp_example_com")
assert state.state == STATE_UNAVAILABLE
with patch("homeassistant.util.dt.utcnow", return_value=next_update), patch(
@@ -213,12 +184,12 @@ async def test_update_sensor_network_errors(hass):
async_fire_time_changed(hass, utcnow() + timedelta(hours=48))
await hass.async_block_till_done()
- state = hass.states.get("sensor.cert_expiry_example_com")
- assert state is not None
- assert state.state != STATE_UNAVAILABLE
- assert state.state == "98"
- assert state.attributes.get("error") == "None"
- assert state.attributes.get("is_valid")
+ state = hass.states.get("sensor.cert_expiry_timestamp_example_com")
+ assert state is not None
+ assert state.state != STATE_UNAVAILABLE
+ assert state.state == timestamp.isoformat()
+ assert state.attributes.get("error") == "None"
+ assert state.attributes.get("is_valid")
next_update = starting_time + timedelta(hours=72)
@@ -229,13 +200,6 @@ async def test_update_sensor_network_errors(hass):
async_fire_time_changed(hass, utcnow() + timedelta(hours=72))
await hass.async_block_till_done()
- state = hass.states.get("sensor.cert_expiry_example_com")
- assert state is not None
- assert state.state != STATE_UNAVAILABLE
- assert state.state == "0"
- assert state.attributes.get("error") == "something bad"
- assert not state.attributes.get("is_valid")
-
state = hass.states.get("sensor.cert_expiry_timestamp_example_com")
assert state is not None
assert state.state == STATE_UNKNOWN
@@ -250,5 +214,5 @@ async def test_update_sensor_network_errors(hass):
async_fire_time_changed(hass, utcnow() + timedelta(hours=96))
await hass.async_block_till_done()
- state = hass.states.get("sensor.cert_expiry_example_com")
+ state = hass.states.get("sensor.cert_expiry_timestamp_example_com")
assert state.state == STATE_UNAVAILABLE
diff --git a/tests/components/climate/test_device_action.py b/tests/components/climate/test_device_action.py
index ff78b837591..4084d37358e 100644
--- a/tests/components/climate/test_device_action.py
+++ b/tests/components/climate/test_device_action.py
@@ -15,6 +15,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/climate/test_device_condition.py b/tests/components/climate/test_device_condition.py
index 431849ae761..8e6d5829c41 100644
--- a/tests/components/climate/test_device_condition.py
+++ b/tests/components/climate/test_device_condition.py
@@ -15,6 +15,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/climate/test_device_trigger.py b/tests/components/climate/test_device_trigger.py
index 58aa3311771..69bb4626e49 100644
--- a/tests/components/climate/test_device_trigger.py
+++ b/tests/components/climate/test_device_trigger.py
@@ -16,6 +16,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/cloud/test_tts.py b/tests/components/cloud/test_tts.py
new file mode 100644
index 00000000000..32a4ca7cb50
--- /dev/null
+++ b/tests/components/cloud/test_tts.py
@@ -0,0 +1,15 @@
+"""Tests for cloud tts."""
+from homeassistant.components.cloud import tts
+
+
+def test_schema():
+ """Test schema."""
+ assert "nl-NL" in tts.SUPPORT_LANGUAGES
+
+ processed = tts.PLATFORM_SCHEMA({"platform": "cloud", "language": "nl-NL"})
+ assert processed["gender"] == "female"
+
+ # Should not raise
+ processed = tts.PLATFORM_SCHEMA(
+ {"platform": "cloud", "language": "nl-NL", "gender": "female"}
+ )
diff --git a/tests/components/config/test_auth.py b/tests/components/config/test_auth.py
index 53defb4cd6e..2d3cfe54f5a 100644
--- a/tests/components/config/test_auth.py
+++ b/tests/components/config/test_auth.py
@@ -34,7 +34,9 @@ async def test_list(hass, hass_ws_client, hass_admin_user):
owner.credentials.append(
auth_models.Credentials(
- auth_provider_type="homeassistant", auth_provider_id=None, data={}
+ auth_provider_type="homeassistant",
+ auth_provider_id=None,
+ data={"username": "test-owner"},
)
)
@@ -58,6 +60,7 @@ async def test_list(hass, hass_ws_client, hass_admin_user):
assert len(data) == 4
assert data[0] == {
"id": hass_admin_user.id,
+ "username": None,
"name": "Mock User",
"is_owner": False,
"is_active": True,
@@ -67,6 +70,7 @@ async def test_list(hass, hass_ws_client, hass_admin_user):
}
assert data[1] == {
"id": owner.id,
+ "username": "test-owner",
"name": "Test Owner",
"is_owner": True,
"is_active": True,
@@ -76,6 +80,7 @@ async def test_list(hass, hass_ws_client, hass_admin_user):
}
assert data[2] == {
"id": system.id,
+ "username": None,
"name": "Test Hass.io",
"is_owner": False,
"is_active": True,
@@ -85,6 +90,7 @@ async def test_list(hass, hass_ws_client, hass_admin_user):
}
assert data[3] == {
"id": inactive.id,
+ "username": None,
"name": "Inactive User",
"is_owner": False,
"is_active": False,
@@ -279,3 +285,76 @@ async def test_update_system_generated(hass, hass_ws_client):
assert not result["success"], result
assert result["error"]["code"] == "cannot_modify_system_generated"
assert user.name == "Test user"
+
+
+async def test_deactivate(hass, hass_ws_client):
+ """Test deactivation and reactivation of regular user."""
+ client = await hass_ws_client(hass)
+
+ user = await hass.auth.async_create_user("Test user")
+ assert user.is_active is True
+
+ await client.send_json(
+ {
+ "id": 5,
+ "type": "config/auth/update",
+ "user_id": user.id,
+ "name": "Updated name",
+ "is_active": False,
+ }
+ )
+
+ result = await client.receive_json()
+ assert result["success"], result
+ data_user = result["result"]["user"]
+ assert data_user["is_active"] is False
+
+ await client.send_json(
+ {
+ "id": 6,
+ "type": "config/auth/update",
+ "user_id": user.id,
+ "name": "Updated name",
+ "is_active": True,
+ }
+ )
+
+ result = await client.receive_json()
+ assert result["success"], result
+ data_user = result["result"]["user"]
+ assert data_user["is_active"] is True
+
+
+async def test_deactivate_owner(hass, hass_ws_client):
+ """Test that owner cannot be deactivated."""
+ user = MockUser(id="abc", name="Test Owner", is_owner=True).add_to_hass(hass)
+
+ assert user.is_active is True
+ assert user.is_owner is True
+
+ client = await hass_ws_client(hass)
+ await client.send_json(
+ {"id": 5, "type": "config/auth/update", "user_id": user.id, "is_active": False}
+ )
+
+ result = await client.receive_json()
+ assert not result["success"], result
+ assert result["error"]["code"] == "cannot_deactivate_owner"
+
+
+async def test_deactivate_system_generated(hass, hass_ws_client):
+ """Test that owner cannot be deactivated."""
+ client = await hass_ws_client(hass)
+
+ user = await hass.auth.async_create_system_user("Test user")
+ assert user.is_active is True
+ assert user.system_generated is True
+ assert user.is_owner is False
+
+ await client.send_json(
+ {"id": 5, "type": "config/auth/update", "user_id": user.id, "is_active": False}
+ )
+
+ result = await client.receive_json()
+ assert not result["success"], result
+ assert result["error"]["code"] == "cannot_modify_system_generated"
diff --git a/tests/components/config/test_auth_provider_homeassistant.py b/tests/components/config/test_auth_provider_homeassistant.py
index 19568ff450b..6af3e6507d5 100644
--- a/tests/components/config/test_auth_provider_homeassistant.py
+++ b/tests/components/config/test_auth_provider_homeassistant.py
@@ -290,7 +290,7 @@ async def test_change_password_wrong_pw(
result = await client.receive_json()
assert not result["success"], result
- assert result["error"]["code"] == "invalid_password"
+ assert result["error"]["code"] == "invalid_current_password"
with pytest.raises(prov_ha.InvalidAuth):
await auth_provider.async_validate_login("test-user", "new-pass")
diff --git a/tests/components/config/test_automation.py b/tests/components/config/test_automation.py
index 1d160870169..347ac96f892 100644
--- a/tests/components/config/test_automation.py
+++ b/tests/components/config/test_automation.py
@@ -5,6 +5,7 @@ from homeassistant.bootstrap import async_setup_component
from homeassistant.components import config
from tests.async_mock import patch
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
async def test_get_device_config(hass, hass_client):
diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py
index 1f82434c7a6..b2273d640de 100644
--- a/tests/components/config/test_device_registry.py
+++ b/tests/components/config/test_device_registry.py
@@ -4,6 +4,7 @@ import pytest
from homeassistant.components.config import device_registry
from tests.common import mock_device_registry
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
@@ -55,6 +56,7 @@ async def test_list_devices(hass, client, registry):
"via_device_id": None,
"area_id": None,
"name_by_user": None,
+ "disabled_by": None,
},
{
"config_entries": ["1234"],
@@ -68,6 +70,7 @@ async def test_list_devices(hass, client, registry):
"via_device_id": dev1,
"area_id": None,
"name_by_user": None,
+ "disabled_by": None,
},
]
@@ -91,6 +94,7 @@ async def test_update_device(hass, client, registry):
"device_id": device.id,
"area_id": "12345A",
"name_by_user": "Test Friendly Name",
+ "disabled_by": "user",
"type": "config/device_registry/update",
}
)
@@ -100,4 +104,5 @@ async def test_update_device(hass, client, registry):
assert msg["result"]["id"] == device.id
assert msg["result"]["area_id"] == "12345A"
assert msg["result"]["name_by_user"] == "Test Friendly Name"
+ assert msg["result"]["disabled_by"] == "user"
assert len(registry.devices) == 1
diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py
index a506135c16d..93d33bc9562 100644
--- a/tests/components/config/test_entity_registry.py
+++ b/tests/components/config/test_entity_registry.py
@@ -7,7 +7,13 @@ from homeassistant.components.config import entity_registry
from homeassistant.const import ATTR_ICON
from homeassistant.helpers.entity_registry import RegistryEntry
-from tests.common import MockConfigEntry, MockEntity, MockEntityPlatform, mock_registry
+from tests.common import (
+ MockConfigEntry,
+ MockEntity,
+ MockEntityPlatform,
+ mock_device_registry,
+ mock_registry,
+)
@pytest.fixture
@@ -17,6 +23,12 @@ def client(hass, hass_ws_client):
yield hass.loop.run_until_complete(hass_ws_client(hass))
+@pytest.fixture
+def device_registry(hass):
+ """Return an empty, loaded, registry."""
+ return mock_device_registry(hass)
+
+
async def test_list_entities(hass, client):
"""Test list entries."""
entities = OrderedDict()
@@ -282,6 +294,55 @@ async def test_update_entity_require_restart(hass, client):
}
+async def test_enable_entity_disabled_device(hass, client, device_registry):
+ """Test enabling entity of disabled device."""
+ config_entry = MockConfigEntry(domain="test_platform")
+ config_entry.add_to_hass(hass)
+
+ device = device_registry.async_get_or_create(
+ config_entry_id="1234",
+ connections={("ethernet", "12:34:56:78:90:AB:CD:EF")},
+ identifiers={("bridgeid", "0123")},
+ manufacturer="manufacturer",
+ model="model",
+ disabled_by="user",
+ )
+
+ mock_registry(
+ hass,
+ {
+ "test_domain.world": RegistryEntry(
+ config_entry_id=config_entry.entry_id,
+ entity_id="test_domain.world",
+ unique_id="1234",
+ # Using component.async_add_entities is equal to platform "domain"
+ platform="test_platform",
+ device_id=device.id,
+ )
+ },
+ )
+ platform = MockEntityPlatform(hass)
+ entity = MockEntity(unique_id="1234")
+ await platform.async_add_entities([entity])
+
+ state = hass.states.get("test_domain.world")
+ assert state is not None
+
+ # UPDATE DISABLED_BY TO NONE
+ await client.send_json(
+ {
+ "id": 8,
+ "type": "config/entity_registry/update",
+ "entity_id": "test_domain.world",
+ "disabled_by": None,
+ }
+ )
+
+ msg = await client.receive_json()
+
+ assert not msg["success"]
+
+
async def test_update_entity_no_changes(hass, client):
"""Test update entity with no changes."""
mock_registry(
diff --git a/tests/components/cover/test_device_action.py b/tests/components/cover/test_device_action.py
index d302353582c..cad6074ff34 100644
--- a/tests/components/cover/test_device_action.py
+++ b/tests/components/cover/test_device_action.py
@@ -16,6 +16,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/cover/test_device_condition.py b/tests/components/cover/test_device_condition.py
index 511c7ced898..b3098ceeca9 100644
--- a/tests/components/cover/test_device_condition.py
+++ b/tests/components/cover/test_device_condition.py
@@ -22,6 +22,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/cover/test_device_trigger.py b/tests/components/cover/test_device_trigger.py
index 1996cf9d6df..ab054ad8223 100644
--- a/tests/components/cover/test_device_trigger.py
+++ b/tests/components/cover/test_device_trigger.py
@@ -22,6 +22,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py
index 5872aee1bf1..78a4f1e937d 100644
--- a/tests/components/deconz/test_binary_sensor.py
+++ b/tests/components/deconz/test_binary_sensor.py
@@ -10,15 +10,19 @@ from homeassistant.components.binary_sensor import (
from homeassistant.components.deconz.const import (
CONF_ALLOW_CLIP_SENSOR,
CONF_ALLOW_NEW_DEVICES,
+ CONF_MASTER_GATEWAY,
DOMAIN as DECONZ_DOMAIN,
)
from homeassistant.components.deconz.gateway import get_gateway_from_config_entry
+from homeassistant.components.deconz.services import SERVICE_DEVICE_REFRESH
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.helpers.entity_registry import async_entries_for_config_entry
from homeassistant.setup import async_setup_component
from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration
+from tests.async_mock import patch
+
SENSORS = {
"1": {
"id": "Presence sensor id",
@@ -40,7 +44,7 @@ SENSORS = {
"id": "CLIP presence sensor id",
"name": "CLIP presence sensor",
"type": "CLIPPresence",
- "state": {},
+ "state": {"presence": False},
"config": {},
"uniqueid": "00:00:00:00:00:00:00:02-00",
},
@@ -172,7 +176,7 @@ async def test_add_new_binary_sensor_ignored(hass):
"""Test that adding a new binary sensor is not allowed."""
config_entry = await setup_deconz_integration(
hass,
- options={CONF_ALLOW_NEW_DEVICES: False},
+ options={CONF_MASTER_GATEWAY: True, CONF_ALLOW_NEW_DEVICES: False},
)
gateway = get_gateway_from_config_entry(hass, config_entry)
assert len(hass.states.async_all()) == 0
@@ -188,8 +192,23 @@ async def test_add_new_binary_sensor_ignored(hass):
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 0
+ assert not hass.states.get("binary_sensor.presence_sensor")
entity_registry = await hass.helpers.entity_registry.async_get_registry()
assert (
len(async_entries_for_config_entry(entity_registry, config_entry.entry_id)) == 0
)
+
+ with patch(
+ "pydeconz.DeconzSession.request",
+ return_value={
+ "groups": {},
+ "lights": {},
+ "sensors": {"1": deepcopy(SENSORS["1"])},
+ },
+ ):
+ await hass.services.async_call(DECONZ_DOMAIN, SERVICE_DEVICE_REFRESH)
+ await hass.async_block_till_done()
+
+ assert len(hass.states.async_all()) == 1
+ assert hass.states.get("binary_sensor.presence_sensor")
diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py
index 5d2e6d614a3..319675cf6f7 100644
--- a/tests/components/deconz/test_climate.py
+++ b/tests/components/deconz/test_climate.py
@@ -6,17 +6,32 @@ import pytest
from homeassistant.components.climate import (
DOMAIN as CLIMATE_DOMAIN,
+ SERVICE_SET_FAN_MODE,
SERVICE_SET_HVAC_MODE,
+ SERVICE_SET_PRESET_MODE,
SERVICE_SET_TEMPERATURE,
)
from homeassistant.components.climate.const import (
+ ATTR_FAN_MODE,
ATTR_HVAC_MODE,
+ ATTR_PRESET_MODE,
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
+ FAN_AUTO,
+ FAN_HIGH,
+ FAN_LOW,
+ FAN_MEDIUM,
+ FAN_OFF,
+ FAN_ON,
HVAC_MODE_AUTO,
HVAC_MODE_COOL,
HVAC_MODE_HEAT,
HVAC_MODE_OFF,
+ PRESET_COMFORT,
+)
+from homeassistant.components.deconz.climate import (
+ DECONZ_FAN_SMART,
+ DECONZ_PRESET_MANUAL,
)
from homeassistant.components.deconz.const import (
CONF_ALLOW_CLIP_SENSOR,
@@ -73,7 +88,128 @@ async def test_no_sensors(hass):
assert len(hass.states.async_all()) == 0
-async def test_climate_devices(hass):
+async def test_simple_climate_device(hass):
+ """Test successful creation of climate entities.
+
+ This is a simple water heater that only supports setting temperature and on and off.
+ """
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ data["sensors"] = {
+ "0": {
+ "config": {
+ "battery": 59,
+ "displayflipped": None,
+ "heatsetpoint": 2100,
+ "locked": None,
+ "mountingmode": None,
+ "offset": 0,
+ "on": True,
+ "reachable": True,
+ },
+ "ep": 1,
+ "etag": "6130553ac247174809bae47144ee23f8",
+ "lastseen": "2020-11-29T19:31Z",
+ "manufacturername": "Danfoss",
+ "modelid": "eTRV0100",
+ "name": "thermostat",
+ "state": {
+ "errorcode": None,
+ "lastupdated": "2020-11-29T19:28:40.665",
+ "mountingmodeactive": False,
+ "on": True,
+ "temperature": 2102,
+ "valve": 24,
+ "windowopen": "Closed",
+ },
+ "swversion": "01.02.0008 01.02",
+ "type": "ZHAThermostat",
+ "uniqueid": "14:b4:57:ff:fe:d5:4e:77-01-0201",
+ }
+ }
+ config_entry = await setup_deconz_integration(hass, get_state_response=data)
+ gateway = get_gateway_from_config_entry(hass, config_entry)
+
+ assert len(hass.states.async_all()) == 2
+ climate_thermostat = hass.states.get("climate.thermostat")
+ assert climate_thermostat.state == HVAC_MODE_HEAT
+ assert climate_thermostat.attributes["hvac_modes"] == [
+ HVAC_MODE_HEAT,
+ HVAC_MODE_OFF,
+ ]
+ assert climate_thermostat.attributes["current_temperature"] == 21.0
+ assert climate_thermostat.attributes["temperature"] == 21.0
+ assert hass.states.get("sensor.thermostat_battery_level").state == "59"
+
+ # Event signals thermostat configured off
+
+ state_changed_event = {
+ "t": "event",
+ "e": "changed",
+ "r": "sensors",
+ "id": "0",
+ "state": {"on": False},
+ }
+ gateway.api.event_handler(state_changed_event)
+ await hass.async_block_till_done()
+
+ assert hass.states.get("climate.thermostat").state == STATE_OFF
+
+ # Event signals thermostat state on
+
+ state_changed_event = {
+ "t": "event",
+ "e": "changed",
+ "r": "sensors",
+ "id": "0",
+ "state": {"on": True},
+ }
+ gateway.api.event_handler(state_changed_event)
+ await hass.async_block_till_done()
+
+ assert hass.states.get("climate.thermostat").state == HVAC_MODE_HEAT
+
+ # Verify service calls
+
+ thermostat_device = gateway.api.sensors["0"]
+
+ # Service turn on thermostat
+
+ with patch.object(thermostat_device, "_request", return_value=True) as set_callback:
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_HVAC_MODE,
+ {ATTR_ENTITY_ID: "climate.thermostat", ATTR_HVAC_MODE: HVAC_MODE_HEAT},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ set_callback.assert_called_with("put", "/sensors/0/config", json={"on": True})
+
+ # Service turn on thermostat
+
+ with patch.object(thermostat_device, "_request", return_value=True) as set_callback:
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_HVAC_MODE,
+ {ATTR_ENTITY_ID: "climate.thermostat", ATTR_HVAC_MODE: HVAC_MODE_OFF},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ set_callback.assert_called_with("put", "/sensors/0/config", json={"on": False})
+
+ # Service set HVAC mode to unsupported value
+
+ with patch.object(
+ thermostat_device, "_request", return_value=True
+ ) as set_callback, pytest.raises(ValueError):
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_HVAC_MODE,
+ {ATTR_ENTITY_ID: "climate.thermostat", ATTR_HVAC_MODE: HVAC_MODE_AUTO},
+ blocking=True,
+ )
+
+
+async def test_climate_device_without_cooling_support(hass):
"""Test successful creation of sensor entities."""
data = deepcopy(DECONZ_WEB_REQUEST)
data["sensors"] = deepcopy(SENSORS)
@@ -81,7 +217,15 @@ async def test_climate_devices(hass):
gateway = get_gateway_from_config_entry(hass, config_entry)
assert len(hass.states.async_all()) == 2
- assert hass.states.get("climate.thermostat").state == HVAC_MODE_AUTO
+ climate_thermostat = hass.states.get("climate.thermostat")
+ assert climate_thermostat.state == HVAC_MODE_AUTO
+ assert climate_thermostat.attributes["hvac_modes"] == [
+ HVAC_MODE_AUTO,
+ HVAC_MODE_HEAT,
+ HVAC_MODE_OFF,
+ ]
+ assert climate_thermostat.attributes["current_temperature"] == 22.6
+ assert climate_thermostat.attributes["temperature"] == 22.0
assert hass.states.get("sensor.thermostat") is None
assert hass.states.get("sensor.thermostat_battery_level").state == "100"
assert hass.states.get("climate.presence_sensor") is None
@@ -221,6 +365,344 @@ async def test_climate_devices(hass):
assert len(hass.states.async_all()) == 0
+async def test_climate_device_with_cooling_support(hass):
+ """Test successful creation of sensor entities."""
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ data["sensors"] = {
+ "0": {
+ "config": {
+ "battery": 25,
+ "coolsetpoint": None,
+ "fanmode": None,
+ "heatsetpoint": 2222,
+ "mode": "heat",
+ "offset": 0,
+ "on": True,
+ "reachable": True,
+ },
+ "ep": 1,
+ "etag": "074549903686a77a12ef0f06c499b1ef",
+ "lastseen": "2020-11-27T13:45Z",
+ "manufacturername": "Zen Within",
+ "modelid": "Zen-01",
+ "name": "Zen-01",
+ "state": {
+ "lastupdated": "2020-11-27T13:42:40.863",
+ "on": False,
+ "temperature": 2320,
+ },
+ "type": "ZHAThermostat",
+ "uniqueid": "00:24:46:00:00:11:6f:56-01-0201",
+ }
+ }
+ config_entry = await setup_deconz_integration(hass, get_state_response=data)
+ gateway = get_gateway_from_config_entry(hass, config_entry)
+
+ assert len(hass.states.async_all()) == 2
+ climate_thermostat = hass.states.get("climate.zen_01")
+ assert climate_thermostat.state == HVAC_MODE_HEAT
+ assert climate_thermostat.attributes["hvac_modes"] == [
+ HVAC_MODE_AUTO,
+ HVAC_MODE_COOL,
+ HVAC_MODE_HEAT,
+ HVAC_MODE_OFF,
+ ]
+ assert climate_thermostat.attributes["current_temperature"] == 23.2
+ assert climate_thermostat.attributes["temperature"] == 22.2
+ assert hass.states.get("sensor.zen_01_battery_level").state == "25"
+
+ # Event signals thermostat state cool
+
+ state_changed_event = {
+ "t": "event",
+ "e": "changed",
+ "r": "sensors",
+ "id": "0",
+ "config": {"mode": "cool"},
+ }
+ gateway.api.event_handler(state_changed_event)
+ await hass.async_block_till_done()
+
+ assert hass.states.get("climate.zen_01").state == HVAC_MODE_COOL
+
+ # Verify service calls
+
+ thermostat_device = gateway.api.sensors["0"]
+
+ # Service set temperature to 20
+
+ with patch.object(thermostat_device, "_request", return_value=True) as set_callback:
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_TEMPERATURE,
+ {ATTR_ENTITY_ID: "climate.zen_01", ATTR_TEMPERATURE: 20},
+ blocking=True,
+ )
+ set_callback.assert_called_with(
+ "put", "/sensors/0/config", json={"coolsetpoint": 2000.0}
+ )
+
+
+async def test_climate_device_with_fan_support(hass):
+ """Test successful creation of sensor entities."""
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ data["sensors"] = {
+ "0": {
+ "config": {
+ "battery": 25,
+ "coolsetpoint": None,
+ "fanmode": "auto",
+ "heatsetpoint": 2222,
+ "mode": "heat",
+ "offset": 0,
+ "on": True,
+ "reachable": True,
+ },
+ "ep": 1,
+ "etag": "074549903686a77a12ef0f06c499b1ef",
+ "lastseen": "2020-11-27T13:45Z",
+ "manufacturername": "Zen Within",
+ "modelid": "Zen-01",
+ "name": "Zen-01",
+ "state": {
+ "lastupdated": "2020-11-27T13:42:40.863",
+ "on": False,
+ "temperature": 2320,
+ },
+ "type": "ZHAThermostat",
+ "uniqueid": "00:24:46:00:00:11:6f:56-01-0201",
+ }
+ }
+ config_entry = await setup_deconz_integration(hass, get_state_response=data)
+ gateway = get_gateway_from_config_entry(hass, config_entry)
+
+ assert len(hass.states.async_all()) == 2
+ climate_thermostat = hass.states.get("climate.zen_01")
+ assert climate_thermostat.state == HVAC_MODE_HEAT
+ assert climate_thermostat.attributes["fan_mode"] == FAN_AUTO
+ assert climate_thermostat.attributes["fan_modes"] == [
+ DECONZ_FAN_SMART,
+ FAN_AUTO,
+ FAN_HIGH,
+ FAN_MEDIUM,
+ FAN_LOW,
+ FAN_ON,
+ FAN_OFF,
+ ]
+
+ # Event signals fan mode defaults to off
+
+ state_changed_event = {
+ "t": "event",
+ "e": "changed",
+ "r": "sensors",
+ "id": "0",
+ "config": {"fanmode": "unsupported"},
+ }
+ gateway.api.event_handler(state_changed_event)
+ await hass.async_block_till_done()
+
+ assert hass.states.get("climate.zen_01").attributes["fan_mode"] == FAN_OFF
+
+ # Event signals unsupported fan mode
+
+ state_changed_event = {
+ "t": "event",
+ "e": "changed",
+ "r": "sensors",
+ "id": "0",
+ "config": {"fanmode": "unsupported"},
+ "state": {"on": True},
+ }
+ gateway.api.event_handler(state_changed_event)
+ await hass.async_block_till_done()
+
+ assert hass.states.get("climate.zen_01").attributes["fan_mode"] == FAN_ON
+
+ # Event signals unsupported fan mode
+
+ state_changed_event = {
+ "t": "event",
+ "e": "changed",
+ "r": "sensors",
+ "id": "0",
+ "config": {"fanmode": "unsupported"},
+ }
+ gateway.api.event_handler(state_changed_event)
+ await hass.async_block_till_done()
+
+ assert hass.states.get("climate.zen_01").attributes["fan_mode"] == FAN_ON
+
+ # Verify service calls
+
+ thermostat_device = gateway.api.sensors["0"]
+
+ # Service set fan mode to off
+
+ with patch.object(thermostat_device, "_request", return_value=True) as set_callback:
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_FAN_MODE,
+ {ATTR_ENTITY_ID: "climate.zen_01", ATTR_FAN_MODE: FAN_OFF},
+ blocking=True,
+ )
+ set_callback.assert_called_with(
+ "put", "/sensors/0/config", json={"fanmode": "off"}
+ )
+
+ # Service set fan mode to custom deCONZ mode smart
+
+ with patch.object(thermostat_device, "_request", return_value=True) as set_callback:
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_FAN_MODE,
+ {ATTR_ENTITY_ID: "climate.zen_01", ATTR_FAN_MODE: DECONZ_FAN_SMART},
+ blocking=True,
+ )
+ set_callback.assert_called_with(
+ "put", "/sensors/0/config", json={"fanmode": "smart"}
+ )
+
+ # Service set fan mode to unsupported value
+
+ with patch.object(
+ thermostat_device, "_request", return_value=True
+ ) as set_callback, pytest.raises(ValueError):
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_FAN_MODE,
+ {ATTR_ENTITY_ID: "climate.zen_01", ATTR_FAN_MODE: "unsupported"},
+ blocking=True,
+ )
+
+
+async def test_climate_device_with_preset(hass):
+ """Test successful creation of sensor entities."""
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ data["sensors"] = {
+ "0": {
+ "config": {
+ "battery": 25,
+ "coolsetpoint": None,
+ "fanmode": None,
+ "heatsetpoint": 2222,
+ "mode": "heat",
+ "preset": "auto",
+ "offset": 0,
+ "on": True,
+ "reachable": True,
+ },
+ "ep": 1,
+ "etag": "074549903686a77a12ef0f06c499b1ef",
+ "lastseen": "2020-11-27T13:45Z",
+ "manufacturername": "Zen Within",
+ "modelid": "Zen-01",
+ "name": "Zen-01",
+ "state": {
+ "lastupdated": "2020-11-27T13:42:40.863",
+ "on": False,
+ "temperature": 2320,
+ },
+ "type": "ZHAThermostat",
+ "uniqueid": "00:24:46:00:00:11:6f:56-01-0201",
+ }
+ }
+ config_entry = await setup_deconz_integration(hass, get_state_response=data)
+ gateway = get_gateway_from_config_entry(hass, config_entry)
+
+ assert len(hass.states.async_all()) == 2
+
+ climate_zen_01 = hass.states.get("climate.zen_01")
+ assert climate_zen_01.state == HVAC_MODE_HEAT
+ assert climate_zen_01.attributes["current_temperature"] == 23.2
+ assert climate_zen_01.attributes["temperature"] == 22.2
+ assert climate_zen_01.attributes["preset_mode"] == "auto"
+ assert climate_zen_01.attributes["preset_modes"] == [
+ "auto",
+ "boost",
+ "comfort",
+ "complex",
+ "eco",
+ "holiday",
+ "manual",
+ ]
+
+ # Event signals deCONZ preset
+
+ state_changed_event = {
+ "t": "event",
+ "e": "changed",
+ "r": "sensors",
+ "id": "0",
+ "config": {"preset": "manual"},
+ }
+ gateway.api.event_handler(state_changed_event)
+ await hass.async_block_till_done()
+
+ assert (
+ hass.states.get("climate.zen_01").attributes["preset_mode"]
+ == DECONZ_PRESET_MANUAL
+ )
+
+ # Event signals unknown preset
+
+ state_changed_event = {
+ "t": "event",
+ "e": "changed",
+ "r": "sensors",
+ "id": "0",
+ "config": {"preset": "unsupported"},
+ }
+ gateway.api.event_handler(state_changed_event)
+ await hass.async_block_till_done()
+
+ assert hass.states.get("climate.zen_01").attributes["preset_mode"] is None
+
+ # Verify service calls
+
+ thermostat_device = gateway.api.sensors["0"]
+
+ # Service set preset to HASS preset
+
+ with patch.object(thermostat_device, "_request", return_value=True) as set_callback:
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_PRESET_MODE,
+ {ATTR_ENTITY_ID: "climate.zen_01", ATTR_PRESET_MODE: PRESET_COMFORT},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ set_callback.assert_called_with(
+ "put", "/sensors/0/config", json={"preset": "comfort"}
+ )
+
+ # Service set preset to custom deCONZ preset
+
+ with patch.object(thermostat_device, "_request", return_value=True) as set_callback:
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_PRESET_MODE,
+ {ATTR_ENTITY_ID: "climate.zen_01", ATTR_PRESET_MODE: DECONZ_PRESET_MANUAL},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ set_callback.assert_called_with(
+ "put", "/sensors/0/config", json={"preset": "manual"}
+ )
+
+ # Service set preset to unsupported value
+
+ with patch.object(
+ thermostat_device, "_request", return_value=True
+ ) as set_callback, pytest.raises(ValueError):
+ await hass.services.async_call(
+ CLIMATE_DOMAIN,
+ SERVICE_SET_PRESET_MODE,
+ {ATTR_ENTITY_ID: "climate.zen_01", ATTR_PRESET_MODE: "unsupported"},
+ blocking=True,
+ )
+
+
async def test_clip_climate_device(hass):
"""Test successful creation of sensor entities."""
data = deepcopy(DECONZ_WEB_REQUEST)
diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py
index dd477e76e7f..d922dffb623 100644
--- a/tests/components/deconz/test_config_flow.py
+++ b/tests/components/deconz/test_config_flow.py
@@ -402,47 +402,6 @@ async def test_flow_ssdp_discovery(hass, aioclient_mock):
}
-async def test_flow_ssdp_discovery_bad_bridge_id_aborts(hass, aioclient_mock):
- """Test that config flow aborts if deCONZ signals no radio hardware available."""
- result = await hass.config_entries.flow.async_init(
- DOMAIN,
- data={
- ATTR_SSDP_LOCATION: "http://1.2.3.4:80/",
- ATTR_UPNP_MANUFACTURER_URL: DECONZ_MANUFACTURERURL,
- ATTR_UPNP_SERIAL: BAD_BRIDGEID,
- },
- context={"source": SOURCE_SSDP},
- )
-
- assert result["type"] == RESULT_TYPE_FORM
- assert result["step_id"] == "link"
-
- aioclient_mock.post(
- "http://1.2.3.4:80/api",
- json=[{"success": {"username": API_KEY}}],
- headers={"content-type": CONTENT_TYPE_JSON},
- )
-
- result = await hass.config_entries.flow.async_configure(
- result["flow_id"], user_input={}
- )
-
- assert result["type"] == RESULT_TYPE_ABORT
- assert result["reason"] == "no_hardware_available"
-
-
-async def test_ssdp_discovery_not_deconz_bridge(hass):
- """Test a non deconz bridge being discovered over ssdp."""
- result = await hass.config_entries.flow.async_init(
- DOMAIN,
- data={ATTR_UPNP_MANUFACTURER_URL: "not deconz bridge"},
- context={"source": SOURCE_SSDP},
- )
-
- assert result["type"] == RESULT_TYPE_ABORT
- assert result["reason"] == "not_deconz_bridge"
-
-
async def test_ssdp_discovery_update_configuration(hass):
"""Test if a discovered bridge is configured but updates with new attributes."""
config_entry = await setup_deconz_integration(hass)
diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py
index 485ae4239be..374d3683a6e 100644
--- a/tests/components/deconz/test_cover.py
+++ b/tests/components/deconz/test_cover.py
@@ -3,10 +3,18 @@
from copy import deepcopy
from homeassistant.components.cover import (
+ ATTR_CURRENT_TILT_POSITION,
+ ATTR_POSITION,
+ ATTR_TILT_POSITION,
DOMAIN as COVER_DOMAIN,
SERVICE_CLOSE_COVER,
+ SERVICE_CLOSE_COVER_TILT,
SERVICE_OPEN_COVER,
+ SERVICE_OPEN_COVER_TILT,
+ SERVICE_SET_COVER_POSITION,
+ SERVICE_SET_COVER_TILT_POSITION,
SERVICE_STOP_COVER,
+ SERVICE_STOP_COVER_TILT,
)
from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN
from homeassistant.components.deconz.gateway import get_gateway_from_config_entry
@@ -30,7 +38,7 @@ COVERS = {
"id": "Window covering device id",
"name": "Window covering device",
"type": "Window covering device",
- "state": {"bri": 254, "on": True, "reachable": True},
+ "state": {"lift": 100, "open": False, "reachable": True},
"modelid": "lumi.curtain",
"uniqueid": "00:00:00:00:00:00:00:01-00",
},
@@ -53,7 +61,7 @@ COVERS = {
"id": "Window covering controller id",
"name": "Window covering controller",
"type": "Window covering controller",
- "state": {"bri": 254, "on": True, "reachable": True},
+ "state": {"bri": 253, "on": True, "reachable": True},
"modelid": "Motor controller",
"uniqueid": "00:00:00:00:00:00:00:04-00",
},
@@ -105,7 +113,67 @@ async def test_cover(hass):
assert hass.states.get("cover.level_controllable_cover").state == STATE_CLOSED
- # Verify service calls
+ # Verify service calls for cover
+
+ windows_covering_device = gateway.api.lights["2"]
+
+ # Service open cover
+
+ with patch.object(
+ windows_covering_device, "_request", return_value=True
+ ) as set_callback:
+ await hass.services.async_call(
+ COVER_DOMAIN,
+ SERVICE_OPEN_COVER,
+ {ATTR_ENTITY_ID: "cover.window_covering_device"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ set_callback.assert_called_with("put", "/lights/2/state", json={"open": True})
+
+ # Service close cover
+
+ with patch.object(
+ windows_covering_device, "_request", return_value=True
+ ) as set_callback:
+ await hass.services.async_call(
+ COVER_DOMAIN,
+ SERVICE_CLOSE_COVER,
+ {ATTR_ENTITY_ID: "cover.window_covering_device"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ set_callback.assert_called_with("put", "/lights/2/state", json={"open": False})
+
+ # Service set cover position
+
+ with patch.object(
+ windows_covering_device, "_request", return_value=True
+ ) as set_callback:
+ await hass.services.async_call(
+ COVER_DOMAIN,
+ SERVICE_SET_COVER_POSITION,
+ {ATTR_ENTITY_ID: "cover.window_covering_device", ATTR_POSITION: 40},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ set_callback.assert_called_with("put", "/lights/2/state", json={"lift": 60})
+
+ # Service stop cover movement
+
+ with patch.object(
+ windows_covering_device, "_request", return_value=True
+ ) as set_callback:
+ await hass.services.async_call(
+ COVER_DOMAIN,
+ SERVICE_STOP_COVER,
+ {ATTR_ENTITY_ID: "cover.window_covering_device"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ set_callback.assert_called_with("put", "/lights/2/state", json={"stop": True})
+
+ # Verify service calls for legacy cover
level_controllable_cover_device = gateway.api.lights["1"]
@@ -135,9 +203,21 @@ async def test_cover(hass):
blocking=True,
)
await hass.async_block_till_done()
- set_callback.assert_called_with(
- "put", "/lights/1/state", json={"on": True, "bri": 254}
+ set_callback.assert_called_with("put", "/lights/1/state", json={"on": True})
+
+ # Service set cover position
+
+ with patch.object(
+ level_controllable_cover_device, "_request", return_value=True
+ ) as set_callback:
+ await hass.services.async_call(
+ COVER_DOMAIN,
+ SERVICE_SET_COVER_POSITION,
+ {ATTR_ENTITY_ID: "cover.level_controllable_cover", ATTR_POSITION: 40},
+ blocking=True,
)
+ await hass.async_block_till_done()
+ set_callback.assert_called_with("put", "/lights/1/state", json={"bri": 152})
# Service stop cover movement
@@ -173,3 +253,80 @@ async def test_cover(hass):
await hass.config_entries.async_unload(config_entry.entry_id)
assert len(hass.states.async_all()) == 0
+
+
+async def test_tilt_cover(hass):
+ """Test that tilting a cover works."""
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ data["lights"] = {
+ "0": {
+ "etag": "87269755b9b3a046485fdae8d96b252c",
+ "lastannounced": None,
+ "lastseen": "2020-08-01T16:22:05Z",
+ "manufacturername": "AXIS",
+ "modelid": "Gear",
+ "name": "Covering device",
+ "state": {
+ "bri": 0,
+ "lift": 0,
+ "on": False,
+ "open": True,
+ "reachable": True,
+ "tilt": 0,
+ },
+ "swversion": "100-5.3.5.1122",
+ "type": "Window covering device",
+ "uniqueid": "00:24:46:00:00:12:34:56-01",
+ }
+ }
+ config_entry = await setup_deconz_integration(hass, get_state_response=data)
+ gateway = get_gateway_from_config_entry(hass, config_entry)
+
+ assert len(hass.states.async_all()) == 1
+ entity = hass.states.get("cover.covering_device")
+ assert entity.state == STATE_OPEN
+ assert entity.attributes[ATTR_CURRENT_TILT_POSITION] == 100
+
+ covering_device = gateway.api.lights["0"]
+
+ with patch.object(covering_device, "_request", return_value=True) as set_callback:
+ await hass.services.async_call(
+ COVER_DOMAIN,
+ SERVICE_SET_COVER_TILT_POSITION,
+ {ATTR_ENTITY_ID: "cover.covering_device", ATTR_TILT_POSITION: 40},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ set_callback.assert_called_with("put", "/lights/0/state", json={"tilt": 60})
+
+ with patch.object(covering_device, "_request", return_value=True) as set_callback:
+ await hass.services.async_call(
+ COVER_DOMAIN,
+ SERVICE_OPEN_COVER_TILT,
+ {ATTR_ENTITY_ID: "cover.covering_device"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ set_callback.assert_called_with("put", "/lights/0/state", json={"tilt": 0})
+
+ with patch.object(covering_device, "_request", return_value=True) as set_callback:
+ await hass.services.async_call(
+ COVER_DOMAIN,
+ SERVICE_CLOSE_COVER_TILT,
+ {ATTR_ENTITY_ID: "cover.covering_device"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ set_callback.assert_called_with("put", "/lights/0/state", json={"tilt": 100})
+
+ # Service stop cover movement
+
+ with patch.object(covering_device, "_request", return_value=True) as set_callback:
+ await hass.services.async_call(
+ COVER_DOMAIN,
+ SERVICE_STOP_COVER_TILT,
+ {ATTR_ENTITY_ID: "cover.covering_device"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ set_callback.assert_called_with("put", "/lights/0/state", json={"stop": True})
diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py
index e1492fb0fcf..14faf1a938c 100644
--- a/tests/components/deconz/test_deconz_event.py
+++ b/tests/components/deconz/test_deconz_event.py
@@ -77,6 +77,7 @@ async def test_deconz_events(hass):
"id": "switch_1",
"unique_id": "00:00:00:00:00:00:00:01",
"event": 2000,
+ "device_id": gateway.events[0].device_id,
}
gateway.api.sensors["3"].update({"state": {"buttonevent": 2000}})
@@ -88,6 +89,7 @@ async def test_deconz_events(hass):
"unique_id": "00:00:00:00:00:00:00:03",
"event": 2000,
"gesture": 1,
+ "device_id": gateway.events[2].device_id,
}
gateway.api.sensors["4"].update({"state": {"gesture": 0}})
@@ -99,6 +101,7 @@ async def test_deconz_events(hass):
"unique_id": "00:00:00:00:00:00:00:04",
"event": 1000,
"gesture": 0,
+ "device_id": gateway.events[3].device_id,
}
gateway.api.sensors["5"].update(
@@ -113,6 +116,7 @@ async def test_deconz_events(hass):
"event": 6002,
"angle": 110,
"xy": [0.5982, 0.3897],
+ "device_id": gateway.events[4].device_id,
}
await hass.config_entries.async_unload(config_entry.entry_id)
diff --git a/tests/components/deconz/test_device_trigger.py b/tests/components/deconz/test_device_trigger.py
index 69c9ae94727..a5399fe4796 100644
--- a/tests/components/deconz/test_device_trigger.py
+++ b/tests/components/deconz/test_device_trigger.py
@@ -19,6 +19,7 @@ from homeassistant.const import (
from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration
from tests.common import assert_lists_same, async_get_device_automations
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
SENSORS = {
"1": {
diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py
index 460f81e830c..18a135a5e05 100644
--- a/tests/components/deconz/test_light.py
+++ b/tests/components/deconz/test_light.py
@@ -330,3 +330,78 @@ async def test_disable_light_groups(hass):
assert len(hass.states.async_all()) == 5
assert hass.states.get("light.light_group") is None
+
+
+async def test_configuration_tool(hass):
+ """Test that lights or groups entities are created."""
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ data["lights"] = {
+ "0": {
+ "etag": "26839cb118f5bf7ba1f2108256644010",
+ "hascolor": False,
+ "lastannounced": None,
+ "lastseen": "2020-11-22T11:27Z",
+ "manufacturername": "dresden elektronik",
+ "modelid": "ConBee II",
+ "name": "Configuration tool 1",
+ "state": {"reachable": True},
+ "swversion": "0x264a0700",
+ "type": "Configuration tool",
+ "uniqueid": "00:21:2e:ff:ff:05:a7:a3-01",
+ }
+ }
+ await setup_deconz_integration(hass, get_state_response=data)
+
+ assert len(hass.states.async_all()) == 0
+
+
+async def test_lidl_christmas_light(hass):
+ """Test that lights or groups entities are created."""
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ data["lights"] = {
+ "0": {
+ "etag": "87a89542bf9b9d0aa8134919056844f8",
+ "hascolor": True,
+ "lastannounced": None,
+ "lastseen": "2020-12-05T22:57Z",
+ "manufacturername": "_TZE200_s8gkrkxk",
+ "modelid": "TS0601",
+ "name": "xmas light",
+ "state": {
+ "bri": 25,
+ "colormode": "hs",
+ "effect": "none",
+ "hue": 53691,
+ "on": True,
+ "reachable": True,
+ "sat": 141,
+ },
+ "swversion": None,
+ "type": "Color dimmable light",
+ "uniqueid": "58:8e:81:ff:fe:db:7b:be-01",
+ }
+ }
+ config_entry = await setup_deconz_integration(hass, get_state_response=data)
+ gateway = get_gateway_from_config_entry(hass, config_entry)
+ xmas_light_device = gateway.api.lights["0"]
+
+ assert len(hass.states.async_all()) == 1
+
+ with patch.object(xmas_light_device, "_request", return_value=True) as set_callback:
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ {
+ ATTR_ENTITY_ID: "light.xmas_light",
+ ATTR_HS_COLOR: (20, 30),
+ },
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ set_callback.assert_called_with(
+ "put",
+ "/lights/0/state",
+ json={"on": True, "hue": 3640, "sat": 76},
+ )
+
+ assert hass.states.get("light.xmas_light")
diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py
index 8b2f1e4da76..def2a1412e5 100644
--- a/tests/components/deconz/test_sensor.py
+++ b/tests/components/deconz/test_sensor.py
@@ -242,3 +242,81 @@ async def test_add_battery_later(hass):
assert len(remote._callbacks) == 2 # Event and battery entity
assert hass.states.get("sensor.switch_1_battery_level")
+
+
+async def test_air_quality_sensor(hass):
+ """Test successful creation of air quality sensor entities."""
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ data["sensors"] = {
+ "0": {
+ "config": {"on": True, "reachable": True},
+ "ep": 2,
+ "etag": "c2d2e42396f7c78e11e46c66e2ec0200",
+ "lastseen": "2020-11-20T22:48Z",
+ "manufacturername": "BOSCH",
+ "modelid": "AIR",
+ "name": "Air quality",
+ "state": {
+ "airquality": "poor",
+ "airqualityppb": 809,
+ "lastupdated": "2020-11-20T22:48:00.209",
+ },
+ "swversion": "20200402",
+ "type": "ZHAAirQuality",
+ "uniqueid": "00:12:4b:00:14:4d:00:07-02-fdef",
+ }
+ }
+ await setup_deconz_integration(hass, get_state_response=data)
+
+ assert len(hass.states.async_all()) == 1
+
+ air_quality = hass.states.get("sensor.air_quality")
+ assert air_quality.state == "poor"
+
+
+async def test_time_sensor(hass):
+ """Test successful creation of time sensor entities."""
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ data["sensors"] = {
+ "0": {
+ "config": {"battery": 40, "on": True, "reachable": True},
+ "ep": 1,
+ "etag": "28e796678d9a24712feef59294343bb6",
+ "lastseen": "2020-11-22T11:26Z",
+ "manufacturername": "Danfoss",
+ "modelid": "eTRV0100",
+ "name": "Time",
+ "state": {
+ "lastset": "2020-11-19T08:07:08Z",
+ "lastupdated": "2020-11-22T10:51:03.444",
+ "localtime": "2020-11-22T10:51:01",
+ "utc": "2020-11-22T10:51:01Z",
+ },
+ "swversion": "20200429",
+ "type": "ZHATime",
+ "uniqueid": "cc:cc:cc:ff:fe:38:4d:b3-01-000a",
+ }
+ }
+ await setup_deconz_integration(hass, get_state_response=data)
+
+ assert len(hass.states.async_all()) == 2
+
+ time = hass.states.get("sensor.time")
+ assert time.state == "2020-11-19T08:07:08Z"
+
+ time_battery = hass.states.get("sensor.time_battery_level")
+ assert time_battery.state == "40"
+
+
+async def test_unsupported_sensor(hass):
+ """Test that unsupported sensors doesn't break anything."""
+ data = deepcopy(DECONZ_WEB_REQUEST)
+ data["sensors"] = {
+ "0": {"type": "not supported", "name": "name", "state": {}, "config": {}}
+ }
+ await setup_deconz_integration(hass, get_state_response=data)
+
+ assert len(hass.states.async_all()) == 1
+
+ unsupported_sensor = hass.states.get("sensor.name")
+ assert unsupported_sensor.state == "unknown"
diff --git a/tests/components/default_config/test_init.py b/tests/components/default_config/test_init.py
index 6bf9cc44c56..a4a5898982b 100644
--- a/tests/components/default_config/test_init.py
+++ b/tests/components/default_config/test_init.py
@@ -4,6 +4,7 @@ import pytest
from homeassistant.setup import async_setup_component
from tests.async_mock import patch
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture(autouse=True)
diff --git a/tests/components/demo/test_number.py b/tests/components/demo/test_number.py
new file mode 100644
index 00000000000..711332b7817
--- /dev/null
+++ b/tests/components/demo/test_number.py
@@ -0,0 +1,97 @@
+"""The tests for the demo number component."""
+
+import pytest
+import voluptuous as vol
+
+from homeassistant.components.number.const import (
+ ATTR_MAX,
+ ATTR_MIN,
+ ATTR_STEP,
+ ATTR_VALUE,
+ DOMAIN,
+ SERVICE_SET_VALUE,
+)
+from homeassistant.const import ATTR_ENTITY_ID
+from homeassistant.setup import async_setup_component
+
+ENTITY_VOLUME = "number.volume"
+ENTITY_PWM = "number.pwm_1"
+
+
+@pytest.fixture(autouse=True)
+async def setup_demo_number(hass):
+ """Initialize setup demo Number entity."""
+ assert await async_setup_component(hass, DOMAIN, {"number": {"platform": "demo"}})
+ await hass.async_block_till_done()
+
+
+def test_setup_params(hass):
+ """Test the initial parameters."""
+ state = hass.states.get(ENTITY_VOLUME)
+ assert state.state == "42.0"
+
+
+def test_default_setup_params(hass):
+ """Test the setup with default parameters."""
+ state = hass.states.get(ENTITY_VOLUME)
+ assert state.attributes.get(ATTR_MIN) == 0.0
+ assert state.attributes.get(ATTR_MAX) == 100.0
+ assert state.attributes.get(ATTR_STEP) == 1.0
+
+ state = hass.states.get(ENTITY_PWM)
+ assert state.attributes.get(ATTR_MIN) == 0.0
+ assert state.attributes.get(ATTR_MAX) == 1.0
+ assert state.attributes.get(ATTR_STEP) == 0.01
+
+
+async def test_set_value_bad_attr(hass):
+ """Test setting the value without required attribute."""
+ state = hass.states.get(ENTITY_VOLUME)
+ assert state.state == "42.0"
+
+ with pytest.raises(vol.Invalid):
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_VALUE,
+ {ATTR_VALUE: None, ATTR_ENTITY_ID: ENTITY_VOLUME},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get(ENTITY_VOLUME)
+ assert state.state == "42.0"
+
+
+async def test_set_value_bad_range(hass):
+ """Test setting the value out of range."""
+ state = hass.states.get(ENTITY_VOLUME)
+ assert state.state == "42.0"
+
+ with pytest.raises(vol.Invalid):
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_VALUE,
+ {ATTR_VALUE: 1024, ATTR_ENTITY_ID: ENTITY_VOLUME},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get(ENTITY_VOLUME)
+ assert state.state == "42.0"
+
+
+async def test_set_set_value(hass):
+ """Test the setting of the value."""
+ state = hass.states.get(ENTITY_VOLUME)
+ assert state.state == "42.0"
+
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_SET_VALUE,
+ {ATTR_VALUE: 23, ATTR_ENTITY_ID: ENTITY_VOLUME},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get(ENTITY_VOLUME)
+ assert state.state == "23.0"
diff --git a/tests/components/derivative/test_sensor.py b/tests/components/derivative/test_sensor.py
index 96fee84126a..58090f1587d 100644
--- a/tests/components/derivative/test_sensor.py
+++ b/tests/components/derivative/test_sensor.py
@@ -23,11 +23,13 @@ async def test_state(hass):
assert await async_setup_component(hass, "sensor", config)
entity_id = config["sensor"]["source"]
- hass.states.async_set(entity_id, 1, {})
- await hass.async_block_till_done()
+ base = dt_util.utcnow()
+ with patch("homeassistant.util.dt.utcnow") as now:
+ now.return_value = base
+ hass.states.async_set(entity_id, 1, {})
+ await hass.async_block_till_done()
- now = dt_util.utcnow() + timedelta(seconds=3600)
- with patch("homeassistant.util.dt.utcnow", return_value=now):
+ now.return_value += timedelta(seconds=3600)
hass.states.async_set(entity_id, 1, {}, force_update=True)
await hass.async_block_till_done()
@@ -63,9 +65,10 @@ async def setup_tests(hass, config, times, values, expected_state):
config, entity_id = await _setup_sensor(hass, config)
# Testing a energy sensor with non-monotonic intervals and values
- for time, value in zip(times, values):
- now = dt_util.utcnow() + timedelta(seconds=time)
- with patch("homeassistant.util.dt.utcnow", return_value=now):
+ base = dt_util.utcnow()
+ with patch("homeassistant.util.dt.utcnow") as now:
+ for time, value in zip(times, values):
+ now.return_value = base + timedelta(seconds=time)
hass.states.async_set(entity_id, value, {}, force_update=True)
await hass.async_block_till_done()
@@ -163,8 +166,9 @@ async def test_data_moving_average_for_discrete_sensor(hass):
},
) # two minute window
+ base = dt_util.utcnow()
for time, value in zip(times, temperature_values):
- now = dt_util.utcnow() + timedelta(seconds=time)
+ now = base + timedelta(seconds=time)
with patch("homeassistant.util.dt.utcnow", return_value=now):
hass.states.async_set(entity_id, value, {}, force_update=True)
await hass.async_block_till_done()
@@ -192,13 +196,15 @@ async def test_prefix(hass):
assert await async_setup_component(hass, "sensor", config)
entity_id = config["sensor"]["source"]
- hass.states.async_set(
- entity_id, 1000, {"unit_of_measurement": POWER_WATT}, force_update=True
- )
- await hass.async_block_till_done()
+ base = dt_util.utcnow()
+ with patch("homeassistant.util.dt.utcnow") as now:
+ now.return_value = base
+ hass.states.async_set(
+ entity_id, 1000, {"unit_of_measurement": POWER_WATT}, force_update=True
+ )
+ await hass.async_block_till_done()
- now = dt_util.utcnow() + timedelta(seconds=3600)
- with patch("homeassistant.util.dt.utcnow", return_value=now):
+ now.return_value += timedelta(seconds=3600)
hass.states.async_set(
entity_id, 1000, {"unit_of_measurement": POWER_WATT}, force_update=True
)
@@ -228,11 +234,13 @@ async def test_suffix(hass):
assert await async_setup_component(hass, "sensor", config)
entity_id = config["sensor"]["source"]
- hass.states.async_set(entity_id, 1000, {})
- await hass.async_block_till_done()
+ base = dt_util.utcnow()
+ with patch("homeassistant.util.dt.utcnow") as now:
+ now.return_value = base
+ hass.states.async_set(entity_id, 1000, {})
+ await hass.async_block_till_done()
- now = dt_util.utcnow() + timedelta(seconds=10)
- with patch("homeassistant.util.dt.utcnow", return_value=now):
+ now.return_value += timedelta(seconds=10)
hass.states.async_set(entity_id, 1000, {}, force_update=True)
await hass.async_block_till_done()
diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py
index 19786bb08e8..83ec146b53e 100644
--- a/tests/components/device_automation/test_init.py
+++ b/tests/components/device_automation/test_init.py
@@ -13,6 +13,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/device_tracker/test_device_condition.py b/tests/components/device_tracker/test_device_condition.py
index 950ace24335..a187f21e954 100644
--- a/tests/components/device_tracker/test_device_condition.py
+++ b/tests/components/device_tracker/test_device_condition.py
@@ -15,6 +15,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/device_tracker/test_device_trigger.py b/tests/components/device_tracker/test_device_trigger.py
index 963dae3127d..2f0ec14ec4b 100644
--- a/tests/components/device_tracker/test_device_trigger.py
+++ b/tests/components/device_tracker/test_device_trigger.py
@@ -16,6 +16,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
AWAY_LATITUDE = 32.881011
AWAY_LONGITUDE = -117.234758
diff --git a/tests/components/ecobee/test_climate.py b/tests/components/ecobee/test_climate.py
index 4e88d7083c4..32575e7188a 100644
--- a/tests/components/ecobee/test_climate.py
+++ b/tests/components/ecobee/test_climate.py
@@ -1,288 +1,310 @@
"""The test for the Ecobee thermostat module."""
-import unittest
from unittest import mock
+import pytest
+
from homeassistant.components.ecobee import climate as ecobee
import homeassistant.const as const
from homeassistant.const import STATE_OFF
-class TestEcobee(unittest.TestCase):
- """Tests for Ecobee climate."""
-
- def setUp(self):
- """Set up test variables."""
- vals = {
- "name": "Ecobee",
- "program": {
- "climates": [
- {"name": "Climate1", "climateRef": "c1"},
- {"name": "Climate2", "climateRef": "c2"},
- ],
- "currentClimateRef": "c1",
- },
- "runtime": {
- "actualTemperature": 300,
- "actualHumidity": 15,
- "desiredHeat": 400,
- "desiredCool": 200,
- "desiredFanMode": "on",
- },
- "settings": {
- "hvacMode": "auto",
- "heatStages": 1,
- "coolStages": 1,
- "fanMinOnTime": 10,
- "heatCoolMinDelta": 50,
- "holdAction": "nextTransition",
- },
- "equipmentStatus": "fan",
- "events": [
- {
- "name": "Event1",
- "running": True,
- "type": "hold",
- "holdClimateRef": "away",
- "endDate": "2017-01-01 10:00:00",
- "startDate": "2017-02-02 11:00:00",
- }
+@pytest.fixture
+def ecobee_fixture():
+ """Set up ecobee mock."""
+ vals = {
+ "name": "Ecobee",
+ "program": {
+ "climates": [
+ {"name": "Climate1", "climateRef": "c1"},
+ {"name": "Climate2", "climateRef": "c2"},
],
- }
+ "currentClimateRef": "c1",
+ },
+ "runtime": {
+ "actualTemperature": 300,
+ "actualHumidity": 15,
+ "desiredHeat": 400,
+ "desiredCool": 200,
+ "desiredFanMode": "on",
+ },
+ "settings": {
+ "hvacMode": "auto",
+ "heatStages": 1,
+ "coolStages": 1,
+ "fanMinOnTime": 10,
+ "heatCoolMinDelta": 50,
+ "holdAction": "nextTransition",
+ },
+ "equipmentStatus": "fan",
+ "events": [
+ {
+ "name": "Event1",
+ "running": True,
+ "type": "hold",
+ "holdClimateRef": "away",
+ "endDate": "2017-01-01 10:00:00",
+ "startDate": "2017-02-02 11:00:00",
+ }
+ ],
+ }
+ mock_ecobee = mock.Mock()
+ mock_ecobee.__getitem__ = mock.Mock(side_effect=vals.__getitem__)
+ mock_ecobee.__setitem__ = mock.Mock(side_effect=vals.__setitem__)
+ return mock_ecobee
- self.ecobee = mock.Mock()
- self.ecobee.__getitem__ = mock.Mock(side_effect=vals.__getitem__)
- self.ecobee.__setitem__ = mock.Mock(side_effect=vals.__setitem__)
- self.data = mock.Mock()
- self.data.ecobee.get_thermostat.return_value = self.ecobee
- self.thermostat = ecobee.Thermostat(self.data, 1)
+@pytest.fixture(name="data")
+def data_fixture(ecobee_fixture):
+ """Set up data mock."""
+ data = mock.Mock()
+ data.ecobee.get_thermostat.return_value = ecobee_fixture
+ return data
- def test_name(self):
- """Test name property."""
- assert "Ecobee" == self.thermostat.name
- def test_current_temperature(self):
- """Test current temperature."""
- assert 30 == self.thermostat.current_temperature
- self.ecobee["runtime"]["actualTemperature"] = const.HTTP_NOT_FOUND
- assert 40.4 == self.thermostat.current_temperature
+@pytest.fixture(name="thermostat")
+def thermostat_fixture(data):
+ """Set up ecobee thermostat object."""
+ return ecobee.Thermostat(data, 1)
- def test_target_temperature_low(self):
- """Test target low temperature."""
- assert 40 == self.thermostat.target_temperature_low
- self.ecobee["runtime"]["desiredHeat"] = 502
- assert 50.2 == self.thermostat.target_temperature_low
- def test_target_temperature_high(self):
- """Test target high temperature."""
- assert 20 == self.thermostat.target_temperature_high
- self.ecobee["runtime"]["desiredCool"] = 103
- assert 10.3 == self.thermostat.target_temperature_high
+async def test_name(thermostat):
+ """Test name property."""
+ assert thermostat.name == "Ecobee"
- def test_target_temperature(self):
- """Test target temperature."""
- assert self.thermostat.target_temperature is None
- self.ecobee["settings"]["hvacMode"] = "heat"
- assert 40 == self.thermostat.target_temperature
- self.ecobee["settings"]["hvacMode"] = "cool"
- assert 20 == self.thermostat.target_temperature
- self.ecobee["settings"]["hvacMode"] = "auxHeatOnly"
- assert 40 == self.thermostat.target_temperature
- self.ecobee["settings"]["hvacMode"] = "off"
- assert self.thermostat.target_temperature is None
- def test_desired_fan_mode(self):
- """Test desired fan mode property."""
- assert "on" == self.thermostat.fan_mode
- self.ecobee["runtime"]["desiredFanMode"] = "auto"
- assert "auto" == self.thermostat.fan_mode
+async def test_current_temperature(ecobee_fixture, thermostat):
+ """Test current temperature."""
+ assert thermostat.current_temperature == 30
+ ecobee_fixture["runtime"]["actualTemperature"] = const.HTTP_NOT_FOUND
+ assert thermostat.current_temperature == 40.4
- def test_fan(self):
- """Test fan property."""
- assert const.STATE_ON == self.thermostat.fan
- self.ecobee["equipmentStatus"] = ""
- assert STATE_OFF == self.thermostat.fan
- self.ecobee["equipmentStatus"] = "heatPump, heatPump2"
- assert STATE_OFF == self.thermostat.fan
- def test_hvac_mode(self):
- """Test current operation property."""
- assert self.thermostat.hvac_mode == "heat_cool"
- self.ecobee["settings"]["hvacMode"] = "heat"
- assert self.thermostat.hvac_mode == "heat"
- self.ecobee["settings"]["hvacMode"] = "cool"
- assert self.thermostat.hvac_mode == "cool"
- self.ecobee["settings"]["hvacMode"] = "auxHeatOnly"
- assert self.thermostat.hvac_mode == "heat"
- self.ecobee["settings"]["hvacMode"] = "off"
- assert self.thermostat.hvac_mode == "off"
+async def test_target_temperature_low(ecobee_fixture, thermostat):
+ """Test target low temperature."""
+ assert thermostat.target_temperature_low == 40
+ ecobee_fixture["runtime"]["desiredHeat"] = 502
+ assert thermostat.target_temperature_low == 50.2
- def test_hvac_modes(self):
- """Test operation list property."""
- assert ["heat_cool", "heat", "cool", "off"] == self.thermostat.hvac_modes
- def test_hvac_mode2(self):
- """Test operation mode property."""
- assert self.thermostat.hvac_mode == "heat_cool"
- self.ecobee["settings"]["hvacMode"] = "heat"
- assert self.thermostat.hvac_mode == "heat"
+async def test_target_temperature_high(ecobee_fixture, thermostat):
+ """Test target high temperature."""
+ assert thermostat.target_temperature_high == 20
+ ecobee_fixture["runtime"]["desiredCool"] = 103
+ assert thermostat.target_temperature_high == 10.3
- def test_device_state_attributes(self):
- """Test device state attributes property."""
- self.ecobee["equipmentStatus"] = "heatPump2"
- assert {
- "fan": "off",
- "climate_mode": "Climate1",
- "fan_min_on_time": 10,
- "equipment_running": "heatPump2",
- } == self.thermostat.device_state_attributes
- self.ecobee["equipmentStatus"] = "auxHeat2"
- assert {
- "fan": "off",
- "climate_mode": "Climate1",
- "fan_min_on_time": 10,
- "equipment_running": "auxHeat2",
- } == self.thermostat.device_state_attributes
- self.ecobee["equipmentStatus"] = "compCool1"
- assert {
- "fan": "off",
- "climate_mode": "Climate1",
- "fan_min_on_time": 10,
- "equipment_running": "compCool1",
- } == self.thermostat.device_state_attributes
- self.ecobee["equipmentStatus"] = ""
- assert {
- "fan": "off",
- "climate_mode": "Climate1",
- "fan_min_on_time": 10,
- "equipment_running": "",
- } == self.thermostat.device_state_attributes
+async def test_target_temperature(ecobee_fixture, thermostat):
+ """Test target temperature."""
+ assert thermostat.target_temperature is None
+ ecobee_fixture["settings"]["hvacMode"] = "heat"
+ assert thermostat.target_temperature == 40
+ ecobee_fixture["settings"]["hvacMode"] = "cool"
+ assert thermostat.target_temperature == 20
+ ecobee_fixture["settings"]["hvacMode"] = "auxHeatOnly"
+ assert thermostat.target_temperature == 40
+ ecobee_fixture["settings"]["hvacMode"] = "off"
+ assert thermostat.target_temperature is None
- self.ecobee["equipmentStatus"] = "Unknown"
- assert {
- "fan": "off",
- "climate_mode": "Climate1",
- "fan_min_on_time": 10,
- "equipment_running": "Unknown",
- } == self.thermostat.device_state_attributes
- self.ecobee["program"]["currentClimateRef"] = "c2"
- assert {
- "fan": "off",
- "climate_mode": "Climate2",
- "fan_min_on_time": 10,
- "equipment_running": "Unknown",
- } == self.thermostat.device_state_attributes
+async def test_desired_fan_mode(ecobee_fixture, thermostat):
+ """Test desired fan mode property."""
+ assert thermostat.fan_mode == "on"
+ ecobee_fixture["runtime"]["desiredFanMode"] = "auto"
+ assert thermostat.fan_mode == "auto"
- def test_is_aux_heat_on(self):
- """Test aux heat property."""
- assert not self.thermostat.is_aux_heat
- self.ecobee["equipmentStatus"] = "fan, auxHeat"
- assert self.thermostat.is_aux_heat
- def test_set_temperature(self):
- """Test set temperature."""
- # Auto -> Auto
- self.data.reset_mock()
- self.thermostat.set_temperature(target_temp_low=20, target_temp_high=30)
- self.data.ecobee.set_hold_temp.assert_has_calls(
- [mock.call(1, 30, 20, "nextTransition")]
- )
+async def test_fan(ecobee_fixture, thermostat):
+ """Test fan property."""
+ assert const.STATE_ON == thermostat.fan
+ ecobee_fixture["equipmentStatus"] = ""
+ assert STATE_OFF == thermostat.fan
+ ecobee_fixture["equipmentStatus"] = "heatPump, heatPump2"
+ assert STATE_OFF == thermostat.fan
- # Auto -> Hold
- self.data.reset_mock()
- self.thermostat.set_temperature(temperature=20)
- self.data.ecobee.set_hold_temp.assert_has_calls(
- [mock.call(1, 25, 15, "nextTransition")]
- )
- # Cool -> Hold
- self.data.reset_mock()
- self.ecobee["settings"]["hvacMode"] = "cool"
- self.thermostat.set_temperature(temperature=20.5)
- self.data.ecobee.set_hold_temp.assert_has_calls(
- [mock.call(1, 20.5, 20.5, "nextTransition")]
- )
+async def test_hvac_mode(ecobee_fixture, thermostat):
+ """Test current operation property."""
+ assert thermostat.hvac_mode == "heat_cool"
+ ecobee_fixture["settings"]["hvacMode"] = "heat"
+ assert thermostat.hvac_mode == "heat"
+ ecobee_fixture["settings"]["hvacMode"] = "cool"
+ assert thermostat.hvac_mode == "cool"
+ ecobee_fixture["settings"]["hvacMode"] = "auxHeatOnly"
+ assert thermostat.hvac_mode == "heat"
+ ecobee_fixture["settings"]["hvacMode"] = "off"
+ assert thermostat.hvac_mode == "off"
- # Heat -> Hold
- self.data.reset_mock()
- self.ecobee["settings"]["hvacMode"] = "heat"
- self.thermostat.set_temperature(temperature=20)
- self.data.ecobee.set_hold_temp.assert_has_calls(
- [mock.call(1, 20, 20, "nextTransition")]
- )
- # Heat -> Auto
- self.data.reset_mock()
- self.ecobee["settings"]["hvacMode"] = "heat"
- self.thermostat.set_temperature(target_temp_low=20, target_temp_high=30)
- assert not self.data.ecobee.set_hold_temp.called
+async def test_hvac_modes(thermostat):
+ """Test operation list property."""
+ assert ["heat_cool", "heat", "cool", "off"] == thermostat.hvac_modes
- def test_set_hvac_mode(self):
- """Test operation mode setter."""
- self.data.reset_mock()
- self.thermostat.set_hvac_mode("heat_cool")
- self.data.ecobee.set_hvac_mode.assert_has_calls([mock.call(1, "auto")])
- self.data.reset_mock()
- self.thermostat.set_hvac_mode("heat")
- self.data.ecobee.set_hvac_mode.assert_has_calls([mock.call(1, "heat")])
- def test_set_fan_min_on_time(self):
- """Test fan min on time setter."""
- self.data.reset_mock()
- self.thermostat.set_fan_min_on_time(15)
- self.data.ecobee.set_fan_min_on_time.assert_has_calls([mock.call(1, 15)])
- self.data.reset_mock()
- self.thermostat.set_fan_min_on_time(20)
- self.data.ecobee.set_fan_min_on_time.assert_has_calls([mock.call(1, 20)])
+async def test_hvac_mode2(ecobee_fixture, thermostat):
+ """Test operation mode property."""
+ assert thermostat.hvac_mode == "heat_cool"
+ ecobee_fixture["settings"]["hvacMode"] = "heat"
+ assert thermostat.hvac_mode == "heat"
- def test_resume_program(self):
- """Test resume program."""
- # False
- self.data.reset_mock()
- self.thermostat.resume_program(False)
- self.data.ecobee.resume_program.assert_has_calls([mock.call(1, "false")])
- self.data.reset_mock()
- self.thermostat.resume_program(None)
- self.data.ecobee.resume_program.assert_has_calls([mock.call(1, "false")])
- self.data.reset_mock()
- self.thermostat.resume_program(0)
- self.data.ecobee.resume_program.assert_has_calls([mock.call(1, "false")])
- # True
- self.data.reset_mock()
- self.thermostat.resume_program(True)
- self.data.ecobee.resume_program.assert_has_calls([mock.call(1, "true")])
- self.data.reset_mock()
- self.thermostat.resume_program(1)
- self.data.ecobee.resume_program.assert_has_calls([mock.call(1, "true")])
+async def test_device_state_attributes(ecobee_fixture, thermostat):
+ """Test device state attributes property."""
+ ecobee_fixture["equipmentStatus"] = "heatPump2"
+ assert {
+ "fan": "off",
+ "climate_mode": "Climate1",
+ "fan_min_on_time": 10,
+ "equipment_running": "heatPump2",
+ } == thermostat.device_state_attributes
- def test_hold_preference(self):
- """Test hold preference."""
- assert "nextTransition" == self.thermostat.hold_preference()
- for action in [
- "useEndTime4hour",
- "useEndTime2hour",
- "nextPeriod",
- "indefinite",
- "askMe",
- ]:
- self.ecobee["settings"]["holdAction"] = action
- assert "nextTransition" == self.thermostat.hold_preference()
+ ecobee_fixture["equipmentStatus"] = "auxHeat2"
+ assert {
+ "fan": "off",
+ "climate_mode": "Climate1",
+ "fan_min_on_time": 10,
+ "equipment_running": "auxHeat2",
+ } == thermostat.device_state_attributes
+ ecobee_fixture["equipmentStatus"] = "compCool1"
+ assert {
+ "fan": "off",
+ "climate_mode": "Climate1",
+ "fan_min_on_time": 10,
+ "equipment_running": "compCool1",
+ } == thermostat.device_state_attributes
+ ecobee_fixture["equipmentStatus"] = ""
+ assert {
+ "fan": "off",
+ "climate_mode": "Climate1",
+ "fan_min_on_time": 10,
+ "equipment_running": "",
+ } == thermostat.device_state_attributes
- def test_set_fan_mode_on(self):
- """Test set fan mode to on."""
- self.data.reset_mock()
- self.thermostat.set_fan_mode("on")
- self.data.ecobee.set_fan_mode.assert_has_calls(
- [mock.call(1, "on", 20, 40, "nextTransition")]
- )
+ ecobee_fixture["equipmentStatus"] = "Unknown"
+ assert {
+ "fan": "off",
+ "climate_mode": "Climate1",
+ "fan_min_on_time": 10,
+ "equipment_running": "Unknown",
+ } == thermostat.device_state_attributes
- def test_set_fan_mode_auto(self):
- """Test set fan mode to auto."""
- self.data.reset_mock()
- self.thermostat.set_fan_mode("auto")
- self.data.ecobee.set_fan_mode.assert_has_calls(
- [mock.call(1, "auto", 20, 40, "nextTransition")]
- )
+ ecobee_fixture["program"]["currentClimateRef"] = "c2"
+ assert {
+ "fan": "off",
+ "climate_mode": "Climate2",
+ "fan_min_on_time": 10,
+ "equipment_running": "Unknown",
+ } == thermostat.device_state_attributes
+
+
+async def test_is_aux_heat_on(ecobee_fixture, thermostat):
+ """Test aux heat property."""
+ assert not thermostat.is_aux_heat
+ ecobee_fixture["equipmentStatus"] = "fan, auxHeat"
+ assert thermostat.is_aux_heat
+
+
+async def test_set_temperature(ecobee_fixture, thermostat, data):
+ """Test set temperature."""
+ # Auto -> Auto
+ data.reset_mock()
+ thermostat.set_temperature(target_temp_low=20, target_temp_high=30)
+ data.ecobee.set_hold_temp.assert_has_calls([mock.call(1, 30, 20, "nextTransition")])
+
+ # Auto -> Hold
+ data.reset_mock()
+ thermostat.set_temperature(temperature=20)
+ data.ecobee.set_hold_temp.assert_has_calls([mock.call(1, 25, 15, "nextTransition")])
+
+ # Cool -> Hold
+ data.reset_mock()
+ ecobee_fixture["settings"]["hvacMode"] = "cool"
+ thermostat.set_temperature(temperature=20.5)
+ data.ecobee.set_hold_temp.assert_has_calls(
+ [mock.call(1, 20.5, 20.5, "nextTransition")]
+ )
+
+ # Heat -> Hold
+ data.reset_mock()
+ ecobee_fixture["settings"]["hvacMode"] = "heat"
+ thermostat.set_temperature(temperature=20)
+ data.ecobee.set_hold_temp.assert_has_calls([mock.call(1, 20, 20, "nextTransition")])
+
+ # Heat -> Auto
+ data.reset_mock()
+ ecobee_fixture["settings"]["hvacMode"] = "heat"
+ thermostat.set_temperature(target_temp_low=20, target_temp_high=30)
+ assert not data.ecobee.set_hold_temp.called
+
+
+async def test_set_hvac_mode(thermostat, data):
+ """Test operation mode setter."""
+ data.reset_mock()
+ thermostat.set_hvac_mode("heat_cool")
+ data.ecobee.set_hvac_mode.assert_has_calls([mock.call(1, "auto")])
+ data.reset_mock()
+ thermostat.set_hvac_mode("heat")
+ data.ecobee.set_hvac_mode.assert_has_calls([mock.call(1, "heat")])
+
+
+async def test_set_fan_min_on_time(thermostat, data):
+ """Test fan min on time setter."""
+ data.reset_mock()
+ thermostat.set_fan_min_on_time(15)
+ data.ecobee.set_fan_min_on_time.assert_has_calls([mock.call(1, 15)])
+ data.reset_mock()
+ thermostat.set_fan_min_on_time(20)
+ data.ecobee.set_fan_min_on_time.assert_has_calls([mock.call(1, 20)])
+
+
+async def test_resume_program(thermostat, data):
+ """Test resume program."""
+ # False
+ data.reset_mock()
+ thermostat.resume_program(False)
+ data.ecobee.resume_program.assert_has_calls([mock.call(1, "false")])
+ data.reset_mock()
+ thermostat.resume_program(None)
+ data.ecobee.resume_program.assert_has_calls([mock.call(1, "false")])
+ data.reset_mock()
+ thermostat.resume_program(0)
+ data.ecobee.resume_program.assert_has_calls([mock.call(1, "false")])
+
+ # True
+ data.reset_mock()
+ thermostat.resume_program(True)
+ data.ecobee.resume_program.assert_has_calls([mock.call(1, "true")])
+ data.reset_mock()
+ thermostat.resume_program(1)
+ data.ecobee.resume_program.assert_has_calls([mock.call(1, "true")])
+
+
+async def test_hold_preference(ecobee_fixture, thermostat):
+ """Test hold preference."""
+ assert thermostat.hold_preference() == "nextTransition"
+ for action in [
+ "useEndTime4hour",
+ "useEndTime2hour",
+ "nextPeriod",
+ "indefinite",
+ "askMe",
+ ]:
+ ecobee_fixture["settings"]["holdAction"] = action
+ assert thermostat.hold_preference() == "nextTransition"
+
+
+async def test_set_fan_mode_on(thermostat, data):
+ """Test set fan mode to on."""
+ data.reset_mock()
+ thermostat.set_fan_mode("on")
+ data.ecobee.set_fan_mode.assert_has_calls(
+ [mock.call(1, "on", 20, 40, "nextTransition")]
+ )
+
+
+async def test_set_fan_mode_auto(thermostat, data):
+ """Test set fan mode to auto."""
+ data.reset_mock()
+ thermostat.set_fan_mode("auto")
+ data.ecobee.set_fan_mode.assert_has_calls(
+ [mock.call(1, "auto", 20, 40, "nextTransition")]
+ )
diff --git a/tests/components/ecobee/test_config_flow.py b/tests/components/ecobee/test_config_flow.py
index 88b7eb7241d..8311b4aba2c 100644
--- a/tests/components/ecobee/test_config_flow.py
+++ b/tests/components/ecobee/test_config_flow.py
@@ -46,8 +46,8 @@ async def test_pin_request_succeeds(hass):
flow.hass = hass
flow.hass.data[DATA_ECOBEE_CONFIG] = {}
- with patch("homeassistant.components.ecobee.config_flow.Ecobee") as MockEcobee:
- mock_ecobee = MockEcobee.return_value
+ with patch("homeassistant.components.ecobee.config_flow.Ecobee") as mock_ecobee:
+ mock_ecobee = mock_ecobee.return_value
mock_ecobee.request_pin.return_value = True
mock_ecobee.pin = "test-pin"
@@ -64,8 +64,8 @@ async def test_pin_request_fails(hass):
flow.hass = hass
flow.hass.data[DATA_ECOBEE_CONFIG] = {}
- with patch("homeassistant.components.ecobee.config_flow.Ecobee") as MockEcobee:
- mock_ecobee = MockEcobee.return_value
+ with patch("homeassistant.components.ecobee.config_flow.Ecobee") as mock_ecobee:
+ mock_ecobee = mock_ecobee.return_value
mock_ecobee.request_pin.return_value = False
result = await flow.async_step_user(user_input={CONF_API_KEY: "api-key"})
@@ -81,12 +81,14 @@ async def test_token_request_succeeds(hass):
flow.hass = hass
flow.hass.data[DATA_ECOBEE_CONFIG] = {}
- with patch("homeassistant.components.ecobee.config_flow.Ecobee") as MockEcobee:
- mock_ecobee = MockEcobee.return_value
+ with patch("homeassistant.components.ecobee.config_flow.Ecobee") as mock_ecobee:
+ mock_ecobee = mock_ecobee.return_value
mock_ecobee.request_tokens.return_value = True
mock_ecobee.api_key = "test-api-key"
mock_ecobee.refresh_token = "test-token"
+ # pylint: disable=protected-access
flow._ecobee = mock_ecobee
+ # pylint: enable=protected-access
result = await flow.async_step_authorize(user_input={})
@@ -104,11 +106,13 @@ async def test_token_request_fails(hass):
flow.hass = hass
flow.hass.data[DATA_ECOBEE_CONFIG] = {}
- with patch("homeassistant.components.ecobee.config_flow.Ecobee") as MockEcobee:
- mock_ecobee = MockEcobee.return_value
+ with patch("homeassistant.components.ecobee.config_flow.Ecobee") as mock_ecobee:
+ mock_ecobee = mock_ecobee.return_value
mock_ecobee.request_tokens.return_value = False
mock_ecobee.pin = "test-pin"
+ # pylint: disable=protected-access
flow._ecobee = mock_ecobee
+ # pylint: enable=protected-access
result = await flow.async_step_authorize(user_input={})
@@ -143,8 +147,8 @@ async def test_import_flow_triggered_with_ecobee_conf_and_valid_data_and_valid_t
with patch(
"homeassistant.components.ecobee.config_flow.load_json",
return_value=MOCK_ECOBEE_CONF,
- ), patch("homeassistant.components.ecobee.config_flow.Ecobee") as MockEcobee:
- mock_ecobee = MockEcobee.return_value
+ ), patch("homeassistant.components.ecobee.config_flow.Ecobee") as mock_ecobee:
+ mock_ecobee = mock_ecobee.return_value
mock_ecobee.refresh_tokens.return_value = True
mock_ecobee.api_key = "test-api-key"
mock_ecobee.refresh_token = "test-token"
@@ -196,10 +200,10 @@ async def test_import_flow_triggered_with_ecobee_conf_and_valid_data_and_stale_t
return_value=MOCK_ECOBEE_CONF,
), patch(
"homeassistant.components.ecobee.config_flow.Ecobee"
- ) as MockEcobee, patch.object(
+ ) as mock_ecobee, patch.object(
flow, "async_step_user", return_value=mock_coro()
) as mock_async_step_user:
- mock_ecobee = MockEcobee.return_value
+ mock_ecobee = mock_ecobee.return_value
mock_ecobee.refresh_tokens.return_value = False
await flow.async_step_import(import_data=None)
diff --git a/tests/components/ecobee/test_util.py b/tests/components/ecobee/test_util.py
index ee02f2a33aa..d159fd697d5 100644
--- a/tests/components/ecobee/test_util.py
+++ b/tests/components/ecobee/test_util.py
@@ -5,14 +5,14 @@ import voluptuous as vol
from homeassistant.components.ecobee.util import ecobee_date, ecobee_time
-def test_ecobee_date_with_valid_input():
+async def test_ecobee_date_with_valid_input():
"""Test that the date function returns the expected result."""
test_input = "2019-09-27"
assert ecobee_date(test_input) == test_input
-def test_ecobee_date_with_invalid_input():
+async def test_ecobee_date_with_invalid_input():
"""Test that the date function raises the expected exception."""
test_input = "20190927"
@@ -20,14 +20,14 @@ def test_ecobee_date_with_invalid_input():
ecobee_date(test_input)
-def test_ecobee_time_with_valid_input():
+async def test_ecobee_time_with_valid_input():
"""Test that the time function returns the expected result."""
test_input = "20:55:15"
assert ecobee_time(test_input) == test_input
-def test_ecobee_time_with_invalid_input():
+async def test_ecobee_time_with_invalid_input():
"""Test that the time function raises the expected exception."""
test_input = "20:55"
diff --git a/tests/components/fan/test_device_action.py b/tests/components/fan/test_device_action.py
index 70da4bd1fca..d3a9aedcf9c 100644
--- a/tests/components/fan/test_device_action.py
+++ b/tests/components/fan/test_device_action.py
@@ -14,6 +14,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/fan/test_device_condition.py b/tests/components/fan/test_device_condition.py
index 939fee154c5..6725587aeda 100644
--- a/tests/components/fan/test_device_condition.py
+++ b/tests/components/fan/test_device_condition.py
@@ -15,6 +15,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/fan/test_device_trigger.py b/tests/components/fan/test_device_trigger.py
index c46b3a6fcec..d96f0a828f3 100644
--- a/tests/components/fan/test_device_trigger.py
+++ b/tests/components/fan/test_device_trigger.py
@@ -15,6 +15,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/fan/test_init.py b/tests/components/fan/test_init.py
index c481e9f0937..a8beed73a07 100644
--- a/tests/components/fan/test_init.py
+++ b/tests/components/fan/test_init.py
@@ -20,7 +20,8 @@ def test_fanentity():
assert fan.supported_features == 0
assert fan.capability_attributes == {}
# Test set_speed not required
- fan.oscillate(True)
+ with pytest.raises(NotImplementedError):
+ fan.oscillate(True)
with pytest.raises(NotImplementedError):
fan.set_speed("slow")
with pytest.raises(NotImplementedError):
diff --git a/tests/components/ffmpeg/test_sensor.py b/tests/components/ffmpeg/test_sensor.py
index 5a89daa624c..a6c9c1f441a 100644
--- a/tests/components/ffmpeg/test_sensor.py
+++ b/tests/components/ffmpeg/test_sensor.py
@@ -61,7 +61,7 @@ class TestFFmpegNoiseSetup:
entity = self.hass.states.get("binary_sensor.ffmpeg_noise")
assert entity.state == "off"
- self.hass.add_job(mock_ffmpeg.call_args[0][2], True)
+ self.hass.add_job(mock_ffmpeg.call_args[0][1], True)
self.hass.block_till_done()
entity = self.hass.states.get("binary_sensor.ffmpeg_noise")
@@ -123,7 +123,7 @@ class TestFFmpegMotionSetup:
entity = self.hass.states.get("binary_sensor.ffmpeg_motion")
assert entity.state == "off"
- self.hass.add_job(mock_ffmpeg.call_args[0][2], True)
+ self.hass.add_job(mock_ffmpeg.call_args[0][1], True)
self.hass.block_till_done()
entity = self.hass.states.get("binary_sensor.ffmpeg_motion")
diff --git a/tests/components/fireservicerota/__init__.py b/tests/components/fireservicerota/__init__.py
new file mode 100644
index 00000000000..37e5364d782
--- /dev/null
+++ b/tests/components/fireservicerota/__init__.py
@@ -0,0 +1 @@
+"""Tests for the FireServiceRota integration."""
diff --git a/tests/components/fireservicerota/test_config_flow.py b/tests/components/fireservicerota/test_config_flow.py
new file mode 100644
index 00000000000..8ccaae5fbbc
--- /dev/null
+++ b/tests/components/fireservicerota/test_config_flow.py
@@ -0,0 +1,111 @@
+"""Test the FireServiceRota config flow."""
+from pyfireservicerota import InvalidAuthError
+
+from homeassistant import data_entry_flow
+from homeassistant.components.fireservicerota.const import DOMAIN
+from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME
+
+from tests.async_mock import patch
+from tests.common import MockConfigEntry
+
+MOCK_CONF = {
+ CONF_USERNAME: "my@email.address",
+ CONF_PASSWORD: "mypassw0rd",
+ CONF_URL: "www.brandweerrooster.nl",
+}
+
+MOCK_DATA = {
+ "auth_implementation": DOMAIN,
+ CONF_URL: MOCK_CONF[CONF_URL],
+ CONF_USERNAME: MOCK_CONF[CONF_USERNAME],
+ "token": {
+ "access_token": "test-access-token",
+ "token_type": "Bearer",
+ "expires_in": 1234,
+ "refresh_token": "test-refresh-token",
+ "created_at": 4321,
+ },
+}
+
+MOCK_TOKEN_INFO = {
+ "access_token": "test-access-token",
+ "token_type": "Bearer",
+ "expires_in": 1234,
+ "refresh_token": "test-refresh-token",
+ "created_at": 4321,
+}
+
+
+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": "user"}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "user"
+
+
+async def test_abort_if_already_setup(hass):
+ """Test abort if already setup."""
+ entry = MockConfigEntry(
+ domain=DOMAIN, data=MOCK_CONF, unique_id=MOCK_CONF[CONF_USERNAME]
+ )
+ entry.add_to_hass(hass)
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "user"}, data=MOCK_CONF
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "already_configured"
+
+
+async def test_invalid_credentials(hass):
+ """Test that invalid credentials throws an error."""
+
+ with patch(
+ "homeassistant.components.fireservicerota.FireServiceRota.request_tokens",
+ side_effect=InvalidAuthError,
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "user"}, data=MOCK_CONF
+ )
+ assert result["errors"] == {"base": "invalid_auth"}
+
+
+async def test_step_user(hass):
+ """Test the start of the config flow."""
+
+ with patch(
+ "homeassistant.components.fireservicerota.config_flow.FireServiceRota"
+ ) as mock_fsr, patch(
+ "homeassistant.components.fireservicerota.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.fireservicerota.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+
+ mock_fireservicerota = mock_fsr.return_value
+ mock_fireservicerota.request_tokens.return_value = MOCK_TOKEN_INFO
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "user"}, data=MOCK_CONF
+ )
+
+ await hass.async_block_till_done()
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == MOCK_CONF[CONF_USERNAME]
+ assert result["data"] == {
+ "auth_implementation": "fireservicerota",
+ CONF_URL: "www.brandweerrooster.nl",
+ CONF_USERNAME: "my@email.address",
+ "token": {
+ "access_token": "test-access-token",
+ "token_type": "Bearer",
+ "expires_in": 1234,
+ "refresh_token": "test-refresh-token",
+ "created_at": 4321,
+ },
+ }
+
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py
index eaf7c8e5651..71c6f41282b 100644
--- a/tests/components/generic_thermostat/test_climate.py
+++ b/tests/components/generic_thermostat/test_climate.py
@@ -209,6 +209,30 @@ async def test_setup_defaults_to_unknown(hass):
assert HVAC_MODE_OFF == hass.states.get(ENTITY).state
+async def test_setup_gets_current_temp_from_sensor(hass):
+ """Test that current temperature is updated on entity addition."""
+ hass.config.units = METRIC_SYSTEM
+ _setup_sensor(hass, 18)
+ await hass.async_block_till_done()
+ 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,
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get(ENTITY).attributes["current_temperature"] == 18
+
+
async def test_default_setup_params(hass, setup_comp_2):
"""Test the setup with default parameters."""
state = hass.states.get(ENTITY)
diff --git a/tests/components/geo_location/test_trigger.py b/tests/components/geo_location/test_trigger.py
index 8bf1c6abe15..ab984a2c309 100644
--- a/tests/components/geo_location/test_trigger.py
+++ b/tests/components/geo_location/test_trigger.py
@@ -7,6 +7,7 @@ from homeassistant.core import Context
from homeassistant.setup import async_setup_component
from tests.common import async_mock_service, mock_component
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py
index f4e26a77f48..bbc5b92615c 100644
--- a/tests/components/google_assistant/__init__.py
+++ b/tests/components/google_assistant/__init__.py
@@ -136,14 +136,20 @@ DEMO_DEVICES = [
{
"id": "cover.living_room_window",
"name": {"name": "Living Room Window"},
- "traits": ["action.devices.traits.OpenClose"],
+ "traits": [
+ "action.devices.traits.StartStop",
+ "action.devices.traits.OpenClose",
+ ],
"type": "action.devices.types.BLINDS",
"willReportState": False,
},
{
"id": "cover.hall_window",
"name": {"name": "Hall Window"},
- "traits": ["action.devices.traits.OpenClose"],
+ "traits": [
+ "action.devices.traits.StartStop",
+ "action.devices.traits.OpenClose",
+ ],
"type": "action.devices.types.BLINDS",
"willReportState": False,
},
@@ -157,7 +163,10 @@ DEMO_DEVICES = [
{
"id": "cover.kitchen_window",
"name": {"name": "Kitchen Window"},
- "traits": ["action.devices.traits.OpenClose"],
+ "traits": [
+ "action.devices.traits.StartStop",
+ "action.devices.traits.OpenClose",
+ ],
"type": "action.devices.types.BLINDS",
"willReportState": False,
},
diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py
index f35415ee9e4..27e62fafc73 100644
--- a/tests/components/google_assistant/test_smart_home.py
+++ b/tests/components/google_assistant/test_smart_home.py
@@ -839,10 +839,13 @@ async def test_device_class_cover(hass, device_class, google_type):
"agentUserId": "test-agent",
"devices": [
{
- "attributes": {},
+ "attributes": {"discreteOnlyOpenClose": True},
"id": "cover.demo_sensor",
"name": {"name": "Demo Sensor"},
- "traits": ["action.devices.traits.OpenClose"],
+ "traits": [
+ "action.devices.traits.StartStop",
+ "action.devices.traits.OpenClose",
+ ],
"type": google_type,
"willReportState": False,
}
diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py
index d1f5f9a1293..4946416e1c8 100644
--- a/tests/components/google_assistant/test_trait.py
+++ b/tests/components/google_assistant/test_trait.py
@@ -384,6 +384,51 @@ async def test_startstop_vacuum(hass):
assert unpause_calls[0].data == {ATTR_ENTITY_ID: "vacuum.bla"}
+async def test_startstop_covert(hass):
+ """Test startStop trait support for vacuum domain."""
+ assert helpers.get_google_type(cover.DOMAIN, None) is not None
+ assert trait.StartStopTrait.supported(cover.DOMAIN, cover.SUPPORT_STOP, None)
+
+ state = State(
+ "cover.bla",
+ cover.STATE_CLOSED,
+ {ATTR_SUPPORTED_FEATURES: cover.SUPPORT_STOP},
+ )
+
+ trt = trait.StartStopTrait(
+ hass,
+ state,
+ BASIC_CONFIG,
+ )
+
+ assert trt.sync_attributes() == {}
+
+ for state_value in (cover.STATE_CLOSING, cover.STATE_OPENING):
+ state.state = state_value
+ assert trt.query_attributes() == {"isRunning": True}
+
+ stop_calls = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_STOP_COVER)
+ await trt.execute(trait.COMMAND_STARTSTOP, BASIC_DATA, {"start": False}, {})
+ assert len(stop_calls) == 1
+ assert stop_calls[0].data == {ATTR_ENTITY_ID: "cover.bla"}
+
+ for state_value in (cover.STATE_CLOSED, cover.STATE_OPEN):
+ state.state = state_value
+ assert trt.query_attributes() == {"isRunning": False}
+
+ with pytest.raises(SmartHomeError, match="Cover is already stopped"):
+ await trt.execute(trait.COMMAND_STARTSTOP, BASIC_DATA, {"start": False}, {})
+
+ with pytest.raises(SmartHomeError, match="Starting a cover is not supported"):
+ await trt.execute(trait.COMMAND_STARTSTOP, BASIC_DATA, {"start": True}, {})
+
+ with pytest.raises(
+ SmartHomeError,
+ match="Command action.devices.commands.PauseUnpause is not supported",
+ ):
+ await trt.execute(trait.COMMAND_PAUSEUNPAUSE, BASIC_DATA, {"start": True}, {})
+
+
async def test_color_setting_color_light(hass):
"""Test ColorSpectrum trait support for light domain."""
assert helpers.get_google_type(light.DOMAIN, None) is not None
@@ -1860,8 +1905,12 @@ async def test_openclose_cover(hass):
calls = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_SET_COVER_POSITION)
await trt.execute(trait.COMMAND_OPENCLOSE, BASIC_DATA, {"openPercent": 50}, {})
- assert len(calls) == 1
+ await trt.execute(
+ trait.COMMAND_OPENCLOSE_RELATIVE, BASIC_DATA, {"openRelativePercent": 50}, {}
+ )
+ assert len(calls) == 2
assert calls[0].data == {ATTR_ENTITY_ID: "cover.bla", cover.ATTR_POSITION: 50}
+ assert calls[1].data == {ATTR_ENTITY_ID: "cover.bla", cover.ATTR_POSITION: 100}
async def test_openclose_cover_unknown_state(hass):
@@ -1873,10 +1922,14 @@ async def test_openclose_cover_unknown_state(hass):
# No state
trt = trait.OpenCloseTrait(
- hass, State("cover.bla", STATE_UNKNOWN, {}), BASIC_CONFIG
+ hass,
+ State(
+ "cover.bla", STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: cover.SUPPORT_OPEN}
+ ),
+ BASIC_CONFIG,
)
- assert trt.sync_attributes() == {}
+ assert trt.sync_attributes() == {"discreteOnlyOpenClose": True}
with pytest.raises(helpers.SmartHomeError):
trt.query_attributes()
@@ -1886,7 +1939,8 @@ async def test_openclose_cover_unknown_state(hass):
assert len(calls) == 1
assert calls[0].data == {ATTR_ENTITY_ID: "cover.bla"}
- assert trt.query_attributes() == {"openPercent": 100}
+ with pytest.raises(helpers.SmartHomeError):
+ trt.query_attributes()
async def test_openclose_cover_assumed_state(hass):
@@ -1909,38 +1963,91 @@ async def test_openclose_cover_assumed_state(hass):
BASIC_CONFIG,
)
- assert trt.sync_attributes() == {}
+ assert trt.sync_attributes() == {"commandOnlyOpenClose": True}
- with pytest.raises(helpers.SmartHomeError):
- trt.query_attributes()
+ assert trt.query_attributes() == {}
calls = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_SET_COVER_POSITION)
await trt.execute(trait.COMMAND_OPENCLOSE, BASIC_DATA, {"openPercent": 40}, {})
assert len(calls) == 1
assert calls[0].data == {ATTR_ENTITY_ID: "cover.bla", cover.ATTR_POSITION: 40}
- assert trt.query_attributes() == {"openPercent": 40}
+
+async def test_openclose_cover_query_only(hass):
+ """Test OpenClose trait support for cover domain."""
+ assert helpers.get_google_type(cover.DOMAIN, None) is not None
+ assert trait.OpenCloseTrait.supported(cover.DOMAIN, 0, None)
+
+ state = State(
+ "cover.bla",
+ cover.STATE_OPEN,
+ )
+
+ trt = trait.OpenCloseTrait(
+ hass,
+ state,
+ BASIC_CONFIG,
+ )
+
+ assert trt.sync_attributes() == {
+ "discreteOnlyOpenClose": True,
+ "queryOnlyOpenClose": True,
+ }
+ assert trt.query_attributes() == {"openPercent": 100}
async def test_openclose_cover_no_position(hass):
"""Test OpenClose trait support for cover domain."""
assert helpers.get_google_type(cover.DOMAIN, None) is not None
assert trait.OpenCloseTrait.supported(
- cover.DOMAIN, cover.SUPPORT_SET_POSITION, None
+ cover.DOMAIN, cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE, None
+ )
+
+ state = State(
+ "cover.bla",
+ cover.STATE_OPEN,
+ {
+ ATTR_SUPPORTED_FEATURES: cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE,
+ },
)
trt = trait.OpenCloseTrait(
- hass, State("cover.bla", cover.STATE_OPEN, {}), BASIC_CONFIG
+ hass,
+ state,
+ BASIC_CONFIG,
)
- assert trt.sync_attributes() == {}
+ assert trt.sync_attributes() == {"discreteOnlyOpenClose": True}
assert trt.query_attributes() == {"openPercent": 100}
+ state.state = cover.STATE_CLOSED
+
+ assert trt.sync_attributes() == {"discreteOnlyOpenClose": True}
+ assert trt.query_attributes() == {"openPercent": 0}
+
calls = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_CLOSE_COVER)
await trt.execute(trait.COMMAND_OPENCLOSE, BASIC_DATA, {"openPercent": 0}, {})
assert len(calls) == 1
assert calls[0].data == {ATTR_ENTITY_ID: "cover.bla"}
+ calls = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_OPEN_COVER)
+ await trt.execute(trait.COMMAND_OPENCLOSE, BASIC_DATA, {"openPercent": 100}, {})
+ assert len(calls) == 1
+ assert calls[0].data == {ATTR_ENTITY_ID: "cover.bla"}
+
+ with pytest.raises(
+ SmartHomeError, match=r"Current position not know for relative command"
+ ):
+ await trt.execute(
+ trait.COMMAND_OPENCLOSE_RELATIVE,
+ BASIC_DATA,
+ {"openRelativePercent": 100},
+ {},
+ )
+
+ with pytest.raises(SmartHomeError, match=r"No support for partial open close"):
+ await trt.execute(trait.COMMAND_OPENCLOSE, BASIC_DATA, {"openPercent": 50}, {})
+
@pytest.mark.parametrize(
"device_class",
@@ -1998,10 +2105,9 @@ async def test_openclose_cover_secure(hass, device_class):
assert calls[0].data == {ATTR_ENTITY_ID: "cover.bla", cover.ATTR_POSITION: 50}
# no challenge on close
- calls = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_CLOSE_COVER)
await trt.execute(trait.COMMAND_OPENCLOSE, PIN_DATA, {"openPercent": 0}, {})
- assert len(calls) == 1
- assert calls[0].data == {ATTR_ENTITY_ID: "cover.bla"}
+ assert len(calls) == 2
+ assert calls[1].data == {ATTR_ENTITY_ID: "cover.bla", cover.ATTR_POSITION: 0}
@pytest.mark.parametrize(
diff --git a/tests/components/google_wifi/test_sensor.py b/tests/components/google_wifi/test_sensor.py
index ff3ec0429fa..a9e09c8b66d 100644
--- a/tests/components/google_wifi/test_sensor.py
+++ b/tests/components/google_wifi/test_sensor.py
@@ -1,17 +1,13 @@
"""The tests for the Google Wifi platform."""
from datetime import datetime, timedelta
-import unittest
-import requests_mock
-
-from homeassistant import core as ha
import homeassistant.components.google_wifi.sensor as google_wifi
from homeassistant.const import STATE_UNKNOWN
-from homeassistant.setup import setup_component
+from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
from tests.async_mock import Mock, patch
-from tests.common import assert_setup_component, get_test_home_assistant
+from tests.common import assert_setup_component, async_fire_time_changed
NAME = "foo"
@@ -34,185 +30,174 @@ MOCK_DATA_NEXT = (
MOCK_DATA_MISSING = '{"software": {},' '"system": {},' '"wan": {}}'
-class TestGoogleWifiSetup(unittest.TestCase):
- """Tests for setting up the Google Wifi sensor platform."""
-
- def setUp(self):
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- self.addCleanup(self.hass.stop)
-
- @requests_mock.Mocker()
- def test_setup_minimum(self, mock_req):
- """Test setup with minimum configuration."""
- resource = f"http://{google_wifi.DEFAULT_HOST}{google_wifi.ENDPOINT}"
- mock_req.get(resource, status_code=200)
- assert setup_component(
- self.hass,
- "sensor",
- {"sensor": {"platform": "google_wifi", "monitored_conditions": ["uptime"]}},
- )
- assert_setup_component(1, "sensor")
-
- @requests_mock.Mocker()
- def test_setup_get(self, mock_req):
- """Test setup with full configuration."""
- resource = f"http://localhost{google_wifi.ENDPOINT}"
- mock_req.get(resource, status_code=200)
- assert setup_component(
- self.hass,
- "sensor",
- {
- "sensor": {
- "platform": "google_wifi",
- "host": "localhost",
- "name": "Test Wifi",
- "monitored_conditions": [
- "current_version",
- "new_version",
- "uptime",
- "last_restart",
- "local_ip",
- "status",
- ],
- }
- },
- )
- assert_setup_component(6, "sensor")
+async def test_setup_minimum(hass, requests_mock):
+ """Test setup with minimum configuration."""
+ resource = f"http://{google_wifi.DEFAULT_HOST}{google_wifi.ENDPOINT}"
+ requests_mock.get(resource, status_code=200)
+ assert await async_setup_component(
+ hass,
+ "sensor",
+ {"sensor": {"platform": "google_wifi", "monitored_conditions": ["uptime"]}},
+ )
+ assert_setup_component(1, "sensor")
-class TestGoogleWifiSensor(unittest.TestCase):
- """Tests for Google Wifi sensor platform."""
-
- def setUp(self):
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- with requests_mock.Mocker() as mock_req:
- self.setup_api(MOCK_DATA, mock_req)
- self.addCleanup(self.hass.stop)
-
- def setup_api(self, data, mock_req):
- """Set up API with fake data."""
- resource = f"http://localhost{google_wifi.ENDPOINT}"
- now = datetime(1970, month=1, day=1)
- with patch("homeassistant.util.dt.now", return_value=now):
- mock_req.get(resource, text=data, status_code=200)
- conditions = google_wifi.MONITORED_CONDITIONS.keys()
- self.api = google_wifi.GoogleWifiAPI("localhost", conditions)
- self.name = NAME
- self.sensor_dict = {}
- for condition, cond_list in google_wifi.MONITORED_CONDITIONS.items():
- sensor = google_wifi.GoogleWifiSensor(self.api, self.name, condition)
- name = f"{self.name}_{condition}"
- units = cond_list[1]
- icon = cond_list[2]
- self.sensor_dict[condition] = {
- "sensor": sensor,
- "name": name,
- "units": units,
- "icon": icon,
+async def test_setup_get(hass, requests_mock):
+ """Test setup with full configuration."""
+ resource = f"http://localhost{google_wifi.ENDPOINT}"
+ requests_mock.get(resource, status_code=200)
+ assert await async_setup_component(
+ hass,
+ "sensor",
+ {
+ "sensor": {
+ "platform": "google_wifi",
+ "host": "localhost",
+ "name": "Test Wifi",
+ "monitored_conditions": [
+ "current_version",
+ "new_version",
+ "uptime",
+ "last_restart",
+ "local_ip",
+ "status",
+ ],
}
+ },
+ )
+ assert_setup_component(6, "sensor")
- def fake_delay(self, ha_delay):
- """Fake delay to prevent update throttle."""
- hass_now = dt_util.utcnow()
- shifted_time = hass_now + timedelta(seconds=ha_delay)
- self.hass.bus.fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: shifted_time})
- def test_name(self):
- """Test the name."""
- for name in self.sensor_dict:
- sensor = self.sensor_dict[name]["sensor"]
- test_name = self.sensor_dict[name]["name"]
- assert test_name == sensor.name
+def setup_api(data, requests_mock):
+ """Set up API with fake data."""
+ resource = f"http://localhost{google_wifi.ENDPOINT}"
+ now = datetime(1970, month=1, day=1)
+ sensor_dict = {}
+ with patch("homeassistant.util.dt.now", return_value=now):
+ requests_mock.get(resource, text=data, status_code=200)
+ conditions = google_wifi.MONITORED_CONDITIONS.keys()
+ api = google_wifi.GoogleWifiAPI("localhost", conditions)
+ for condition, cond_list in google_wifi.MONITORED_CONDITIONS.items():
+ sensor_dict[condition] = {
+ "sensor": google_wifi.GoogleWifiSensor(api, NAME, condition),
+ "name": f"{NAME}_{condition}",
+ "units": cond_list[1],
+ "icon": cond_list[2],
+ }
+ return api, sensor_dict
- def test_unit_of_measurement(self):
- """Test the unit of measurement."""
- for name in self.sensor_dict:
- sensor = self.sensor_dict[name]["sensor"]
- assert self.sensor_dict[name]["units"] == sensor.unit_of_measurement
- def test_icon(self):
- """Test the icon."""
- for name in self.sensor_dict:
- sensor = self.sensor_dict[name]["sensor"]
- assert self.sensor_dict[name]["icon"] == sensor.icon
+def fake_delay(hass, ha_delay):
+ """Fake delay to prevent update throttle."""
+ hass_now = dt_util.utcnow()
+ shifted_time = hass_now + timedelta(seconds=ha_delay)
+ async_fire_time_changed(hass, shifted_time)
- @requests_mock.Mocker()
- def test_state(self, mock_req):
- """Test the initial state."""
- self.setup_api(MOCK_DATA, mock_req)
- now = datetime(1970, month=1, day=1)
- with patch("homeassistant.util.dt.now", return_value=now):
- for name in self.sensor_dict:
- sensor = self.sensor_dict[name]["sensor"]
- self.fake_delay(2)
- sensor.update()
- if name == google_wifi.ATTR_LAST_RESTART:
- assert "1969-12-31 00:00:00" == sensor.state
- elif name == google_wifi.ATTR_UPTIME:
- assert 1 == sensor.state
- elif name == google_wifi.ATTR_STATUS:
- assert "Online" == sensor.state
- else:
- assert "initial" == sensor.state
- @requests_mock.Mocker()
- def test_update_when_value_is_none(self, mock_req):
- """Test state gets updated to unknown when sensor returns no data."""
- self.setup_api(None, mock_req)
- for name in self.sensor_dict:
- sensor = self.sensor_dict[name]["sensor"]
- self.fake_delay(2)
+def test_name(requests_mock):
+ """Test the name."""
+ api, sensor_dict = setup_api(MOCK_DATA, requests_mock)
+ for name in sensor_dict:
+ sensor = sensor_dict[name]["sensor"]
+ test_name = sensor_dict[name]["name"]
+ assert test_name == sensor.name
+
+
+def test_unit_of_measurement(requests_mock):
+ """Test the unit of measurement."""
+ api, sensor_dict = setup_api(MOCK_DATA, requests_mock)
+ for name in sensor_dict:
+ sensor = sensor_dict[name]["sensor"]
+ assert sensor_dict[name]["units"] == sensor.unit_of_measurement
+
+
+def test_icon(requests_mock):
+ """Test the icon."""
+ api, sensor_dict = setup_api(MOCK_DATA, requests_mock)
+ for name in sensor_dict:
+ sensor = sensor_dict[name]["sensor"]
+ assert sensor_dict[name]["icon"] == sensor.icon
+
+
+def test_state(hass, requests_mock):
+ """Test the initial state."""
+ api, sensor_dict = setup_api(MOCK_DATA, requests_mock)
+ now = datetime(1970, month=1, day=1)
+ with patch("homeassistant.util.dt.now", return_value=now):
+ for name in sensor_dict:
+ sensor = sensor_dict[name]["sensor"]
+ fake_delay(hass, 2)
sensor.update()
- assert sensor.state is None
+ if name == google_wifi.ATTR_LAST_RESTART:
+ assert "1969-12-31 00:00:00" == sensor.state
+ elif name == google_wifi.ATTR_UPTIME:
+ assert 1 == sensor.state
+ elif name == google_wifi.ATTR_STATUS:
+ assert "Online" == sensor.state
+ else:
+ assert "initial" == sensor.state
- @requests_mock.Mocker()
- def test_update_when_value_changed(self, mock_req):
- """Test state gets updated when sensor returns a new status."""
- self.setup_api(MOCK_DATA_NEXT, mock_req)
- now = datetime(1970, month=1, day=1)
- with patch("homeassistant.util.dt.now", return_value=now):
- for name in self.sensor_dict:
- sensor = self.sensor_dict[name]["sensor"]
- self.fake_delay(2)
- sensor.update()
- if name == google_wifi.ATTR_LAST_RESTART:
- assert "1969-12-30 00:00:00" == sensor.state
- elif name == google_wifi.ATTR_UPTIME:
- assert 2 == sensor.state
- elif name == google_wifi.ATTR_STATUS:
- assert "Offline" == sensor.state
- elif name == google_wifi.ATTR_NEW_VERSION:
- assert "Latest" == sensor.state
- elif name == google_wifi.ATTR_LOCAL_IP:
- assert STATE_UNKNOWN == sensor.state
- else:
- assert "next" == sensor.state
- @requests_mock.Mocker()
- def test_when_api_data_missing(self, mock_req):
- """Test state logs an error when data is missing."""
- self.setup_api(MOCK_DATA_MISSING, mock_req)
- now = datetime(1970, month=1, day=1)
- with patch("homeassistant.util.dt.now", return_value=now):
- for name in self.sensor_dict:
- sensor = self.sensor_dict[name]["sensor"]
- self.fake_delay(2)
- sensor.update()
+def test_update_when_value_is_none(hass, requests_mock):
+ """Test state gets updated to unknown when sensor returns no data."""
+ api, sensor_dict = setup_api(None, requests_mock)
+ for name in sensor_dict:
+ sensor = sensor_dict[name]["sensor"]
+ fake_delay(hass, 2)
+ sensor.update()
+ assert sensor.state is None
+
+
+def test_update_when_value_changed(hass, requests_mock):
+ """Test state gets updated when sensor returns a new status."""
+ api, sensor_dict = setup_api(MOCK_DATA_NEXT, requests_mock)
+ now = datetime(1970, month=1, day=1)
+ with patch("homeassistant.util.dt.now", return_value=now):
+ for name in sensor_dict:
+ sensor = sensor_dict[name]["sensor"]
+ fake_delay(hass, 2)
+ sensor.update()
+ if name == google_wifi.ATTR_LAST_RESTART:
+ assert "1969-12-30 00:00:00" == sensor.state
+ elif name == google_wifi.ATTR_UPTIME:
+ assert 2 == sensor.state
+ elif name == google_wifi.ATTR_STATUS:
+ assert "Offline" == sensor.state
+ elif name == google_wifi.ATTR_NEW_VERSION:
+ assert "Latest" == sensor.state
+ elif name == google_wifi.ATTR_LOCAL_IP:
assert STATE_UNKNOWN == sensor.state
+ else:
+ assert "next" == sensor.state
- def test_update_when_unavailable(self):
- """Test state updates when Google Wifi unavailable."""
- self.api.update = Mock(
- "google_wifi.GoogleWifiAPI.update", side_effect=self.update_side_effect()
- )
- for name in self.sensor_dict:
- sensor = self.sensor_dict[name]["sensor"]
+
+def test_when_api_data_missing(hass, requests_mock):
+ """Test state logs an error when data is missing."""
+ api, sensor_dict = setup_api(MOCK_DATA_MISSING, requests_mock)
+ now = datetime(1970, month=1, day=1)
+ with patch("homeassistant.util.dt.now", return_value=now):
+ for name in sensor_dict:
+ sensor = sensor_dict[name]["sensor"]
+ fake_delay(hass, 2)
sensor.update()
- assert sensor.state is None
+ assert STATE_UNKNOWN == sensor.state
- def update_side_effect(self):
- """Mock representation of update function."""
- self.api.data = None
- self.api.available = False
+
+def test_update_when_unavailable(requests_mock):
+ """Test state updates when Google Wifi unavailable."""
+ api, sensor_dict = setup_api(None, requests_mock)
+ api.update = Mock(
+ "google_wifi.GoogleWifiAPI.update",
+ side_effect=update_side_effect(requests_mock),
+ )
+ for name in sensor_dict:
+ sensor = sensor_dict[name]["sensor"]
+ sensor.update()
+ assert sensor.state is None
+
+
+def update_side_effect(requests_mock):
+ """Mock representation of update function."""
+ api, sensor_dict = setup_api(MOCK_DATA, requests_mock)
+ api.data = None
+ api.available = False
diff --git a/tests/components/group/test_cover.py b/tests/components/group/test_cover.py
index 2ffe02570c9..59bde36b46b 100644
--- a/tests/components/group/test_cover.py
+++ b/tests/components/group/test_cover.py
@@ -63,6 +63,16 @@ CONFIG_POS = {
]
}
+CONFIG_TILT_ONLY = {
+ DOMAIN: [
+ {"platform": "demo"},
+ {
+ "platform": "group",
+ CONF_ENTITIES: [DEMO_COVER_TILT, DEMO_TILT],
+ },
+ ]
+}
+
CONFIG_ATTRIBUTES = {
DOMAIN: {
"platform": "group",
@@ -211,6 +221,34 @@ async def test_attributes(hass, setup_comp):
assert state.attributes[ATTR_ASSUMED_STATE] is True
+@pytest.mark.parametrize("config_count", [(CONFIG_TILT_ONLY, 2)])
+async def test_cover_that_only_supports_tilt_removed(hass, setup_comp):
+ """Test removing a cover that support tilt."""
+ hass.states.async_set(
+ DEMO_COVER_TILT,
+ STATE_OPEN,
+ {ATTR_SUPPORTED_FEATURES: 128, ATTR_CURRENT_TILT_POSITION: 60},
+ )
+ hass.states.async_set(
+ DEMO_TILT,
+ STATE_OPEN,
+ {ATTR_SUPPORTED_FEATURES: 128, ATTR_CURRENT_TILT_POSITION: 60},
+ )
+ state = hass.states.get(COVER_GROUP)
+ assert state.state == STATE_OPEN
+ assert state.attributes[ATTR_FRIENDLY_NAME] == DEFAULT_NAME
+ assert state.attributes[ATTR_ENTITY_ID] == [
+ DEMO_COVER_TILT,
+ DEMO_TILT,
+ ]
+ assert ATTR_ASSUMED_STATE not in state.attributes
+ assert ATTR_CURRENT_TILT_POSITION in state.attributes
+
+ hass.states.async_remove(DEMO_COVER_TILT)
+ hass.states.async_set(DEMO_TILT, STATE_CLOSED)
+ await hass.async_block_till_done()
+
+
@pytest.mark.parametrize("config_count", [(CONFIG_ALL, 2)])
async def test_open_covers(hass, setup_comp):
"""Test open cover function."""
diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py
index bab6eae4564..db6d7476912 100644
--- a/tests/components/history_stats/test_sensor.py
+++ b/tests/components/history_stats/test_sensor.py
@@ -50,6 +50,28 @@ class TestHistoryStatsSensor(unittest.TestCase):
state = self.hass.states.get("sensor.test")
assert state.state == STATE_UNKNOWN
+ def test_setup_multiple_states(self):
+ """Test the history statistics sensor setup for multiple states."""
+ self.init_recorder()
+ config = {
+ "history": {},
+ "sensor": {
+ "platform": "history_stats",
+ "entity_id": "binary_sensor.test_id",
+ "state": ["on", "true"],
+ "start": "{{ now().replace(hour=0)"
+ ".replace(minute=0).replace(second=0) }}",
+ "duration": "02:00",
+ "name": "Test",
+ },
+ }
+
+ assert setup_component(self.hass, "sensor", config)
+ self.hass.block_till_done()
+
+ state = self.hass.states.get("sensor.test")
+ assert state.state == STATE_UNKNOWN
+
@patch(
"homeassistant.helpers.template.TemplateEnvironment.is_safe_callable",
return_value=True,
@@ -152,6 +174,90 @@ class TestHistoryStatsSensor(unittest.TestCase):
assert sensor3.state == 2
assert sensor4.state == 50
+ def test_measure_multiple(self):
+ """Test the history statistics sensor measure for multiple states."""
+ t0 = dt_util.utcnow() - timedelta(minutes=40)
+ t1 = t0 + timedelta(minutes=20)
+ t2 = dt_util.utcnow() - timedelta(minutes=10)
+
+ # Start t0 t1 t2 End
+ # |--20min--|--20min--|--10min--|--10min--|
+ # |---------|--orange-|-default-|---blue--|
+
+ fake_states = {
+ "input_select.test_id": [
+ ha.State("input_select.test_id", "orange", last_changed=t0),
+ ha.State("input_select.test_id", "default", last_changed=t1),
+ ha.State("input_select.test_id", "blue", last_changed=t2),
+ ]
+ }
+
+ start = Template("{{ as_timestamp(now()) - 3600 }}", self.hass)
+ end = Template("{{ now() }}", self.hass)
+
+ sensor1 = HistoryStatsSensor(
+ self.hass,
+ "input_select.test_id",
+ ["orange", "blue"],
+ start,
+ end,
+ None,
+ "time",
+ "Test",
+ )
+
+ sensor2 = HistoryStatsSensor(
+ self.hass,
+ "unknown.id",
+ ["orange", "blue"],
+ start,
+ end,
+ None,
+ "time",
+ "Test",
+ )
+
+ sensor3 = HistoryStatsSensor(
+ self.hass,
+ "input_select.test_id",
+ ["orange", "blue"],
+ start,
+ end,
+ None,
+ "count",
+ "test",
+ )
+
+ sensor4 = HistoryStatsSensor(
+ self.hass,
+ "input_select.test_id",
+ ["orange", "blue"],
+ start,
+ end,
+ None,
+ "ratio",
+ "test",
+ )
+
+ assert sensor1._type == "time"
+ assert sensor3._type == "count"
+ assert sensor4._type == "ratio"
+
+ with patch(
+ "homeassistant.components.history.state_changes_during_period",
+ return_value=fake_states,
+ ):
+ with patch("homeassistant.components.history.get_state", return_value=None):
+ sensor1.update()
+ sensor2.update()
+ sensor3.update()
+ sensor4.update()
+
+ assert sensor1.state == 0.5
+ assert sensor2.state is None
+ assert sensor3.state == 2
+ assert sensor4.state == 50
+
def test_wrong_date(self):
"""Test when start or end value is not a timestamp or a date."""
good = Template("{{ now() }}", self.hass)
diff --git a/tests/components/home_connect/test_config_flow.py b/tests/components/home_connect/test_config_flow.py
index 5d65df98e5b..5c94f8b3362 100644
--- a/tests/components/home_connect/test_config_flow.py
+++ b/tests/components/home_connect/test_config_flow.py
@@ -14,7 +14,9 @@ CLIENT_ID = "1234"
CLIENT_SECRET = "5678"
-async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request):
+async def test_full_flow(
+ hass, aiohttp_client, aioclient_mock, current_request_with_host
+):
"""Check full flow."""
assert await setup.async_setup_component(
hass,
@@ -31,7 +33,13 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request):
result = await hass.config_entries.flow.async_init(
"home_connect", context={"source": config_entries.SOURCE_USER}
)
- state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]})
+ state = config_entry_oauth2_flow._encode_jwt(
+ hass,
+ {
+ "flow_id": result["flow_id"],
+ "redirect_uri": "https://example.com/auth/external/callback",
+ },
+ )
assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP
assert result["url"] == (
diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py
index 3ad3ef76483..ca2f116a06a 100644
--- a/tests/components/homeassistant/test_init.py
+++ b/tests/components/homeassistant/test_init.py
@@ -248,7 +248,7 @@ class TestComponentsCore(unittest.TestCase):
assert not mock_stop.called
-async def test_turn_on_to_not_block_for_domains_without_service(hass):
+async def test_turn_on_skips_domains_without_service(hass, caplog):
"""Test if turn_on is blocking domain with no service."""
await async_setup_component(hass, "homeassistant", {})
async_mock_service(hass, "light", SERVICE_TURN_ON)
@@ -261,7 +261,7 @@ async def test_turn_on_to_not_block_for_domains_without_service(hass):
service_call = ha.ServiceCall(
"homeassistant",
"turn_on",
- {"entity_id": ["light.test", "sensor.bla", "light.bla"]},
+ {"entity_id": ["light.test", "sensor.bla", "binary_sensor.blub", "light.bla"]},
)
service = hass.services._services["homeassistant"]["turn_on"]
@@ -271,18 +271,19 @@ async def test_turn_on_to_not_block_for_domains_without_service(hass):
) as mock_call:
await service.job.target(service_call)
- assert mock_call.call_count == 2
+ assert mock_call.call_count == 1
assert mock_call.call_args_list[0][0] == (
"light",
"turn_on",
{"entity_id": ["light.bla", "light.test"]},
- True,
)
- assert mock_call.call_args_list[1][0] == (
- "sensor",
- "turn_on",
- {"entity_id": ["sensor.bla"]},
- False,
+ assert mock_call.call_args_list[0][1] == {
+ "blocking": True,
+ "context": service_call.context,
+ }
+ assert (
+ "The service homeassistant.turn_on does not support entities binary_sensor.blub, sensor.bla"
+ in caplog.text
)
@@ -381,6 +382,6 @@ async def test_not_allowing_recursion(hass, caplog):
blocking=True,
)
assert (
- f"Called service homeassistant.{service} with invalid entity IDs homeassistant.light"
+ f"Called service homeassistant.{service} with invalid entities homeassistant.light"
in caplog.text
), service
diff --git a/tests/components/homeassistant/triggers/conftest.py b/tests/components/homeassistant/triggers/conftest.py
new file mode 100644
index 00000000000..5c983ba698e
--- /dev/null
+++ b/tests/components/homeassistant/triggers/conftest.py
@@ -0,0 +1,3 @@
+"""Conftest for HA triggers."""
+
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
diff --git a/tests/components/homeassistant/triggers/test_event.py b/tests/components/homeassistant/triggers/test_event.py
index babb2bf4d87..8fedaac3815 100644
--- a/tests/components/homeassistant/triggers/test_event.py
+++ b/tests/components/homeassistant/triggers/test_event.py
@@ -59,6 +59,33 @@ async def test_if_fires_on_event(hass, calls):
assert len(calls) == 1
+async def test_if_fires_on_multiple_events(hass, calls):
+ """Test the firing of events."""
+ context = Context()
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: {
+ "trigger": {
+ "platform": "event",
+ "event_type": ["test_event", "test2_event"],
+ },
+ "action": {"service": "test.automation"},
+ }
+ },
+ )
+
+ hass.bus.async_fire("test_event", context=context)
+ await hass.async_block_till_done()
+ hass.bus.async_fire("test2_event", context=context)
+ await hass.async_block_till_done()
+ assert len(calls) == 2
+ assert calls[0].context.parent_id == context.id
+ assert calls[1].context.parent_id == context.id
+
+
async def test_if_fires_on_event_extra_data(hass, calls, context_with_user):
"""Test the firing of events still matches with event data and context."""
assert await async_setup_component(
diff --git a/tests/components/homeassistant/triggers/test_numeric_state.py b/tests/components/homeassistant/triggers/test_numeric_state.py
index 09a13f95603..c8a9cd6d50f 100644
--- a/tests/components/homeassistant/triggers/test_numeric_state.py
+++ b/tests/components/homeassistant/triggers/test_numeric_state.py
@@ -34,6 +34,31 @@ def setup_comp(hass):
mock_component(hass, "group")
+async def test_if_not_fires_on_entity_removal(hass, calls):
+ """Test the firing with removed entity."""
+ hass.states.async_set("test.entity", 11)
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: {
+ "trigger": {
+ "platform": "numeric_state",
+ "entity_id": "test.entity",
+ "below": 10,
+ },
+ "action": {"service": "test.automation"},
+ }
+ },
+ )
+
+ # Entity disappears
+ hass.states.async_remove("test.entity")
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+
+
async def test_if_fires_on_entity_change_below(hass, calls):
"""Test the firing with changed entity."""
context = Context()
diff --git a/tests/components/homekit_controller/specific_devices/test_anker_eufycam.py b/tests/components/homekit_controller/specific_devices/test_anker_eufycam.py
new file mode 100644
index 00000000000..45e0466ceea
--- /dev/null
+++ b/tests/components/homekit_controller/specific_devices/test_anker_eufycam.py
@@ -0,0 +1,53 @@
+"""Test against characteristics captured from a eufycam."""
+
+from tests.components.homekit_controller.common import (
+ Helper,
+ setup_accessories_from_file,
+ setup_test_accessories,
+)
+
+
+async def test_eufycam_setup(hass):
+ """Test that a eufycam can be correctly setup in HA."""
+ accessories = await setup_accessories_from_file(hass, "anker_eufycam.json")
+ config_entry, pairing = await setup_test_accessories(hass, accessories)
+
+ entity_registry = await hass.helpers.entity_registry.async_get_registry()
+
+ # Check that the camera is correctly found and set up
+ camera_id = "camera.eufycam2_0000"
+ camera = entity_registry.async_get(camera_id)
+ assert camera.unique_id == "homekit-A0000A000000000D-aid:4"
+
+ camera_helper = Helper(
+ hass,
+ "camera.eufycam2_0000",
+ pairing,
+ accessories[0],
+ config_entry,
+ )
+
+ camera_state = await camera_helper.poll_and_get_state()
+ assert camera_state.attributes["friendly_name"] == "eufyCam2-0000"
+ assert camera_state.state == "idle"
+ assert camera_state.attributes["supported_features"] == 0
+
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+
+ device = device_registry.async_get(camera.device_id)
+ assert device.manufacturer == "Anker"
+ assert device.name == "eufyCam2-0000"
+ assert device.model == "T8113"
+ assert device.sw_version == "1.6.7"
+
+ # These cameras are via a bridge, so via should be set
+ assert device.via_device_id is not None
+
+ cameras_count = 0
+ for state in hass.states.async_all():
+ if state.entity_id.startswith("camera."):
+ cameras_count += 1
+
+ # There are multiple rtsp services, we only want to create 1
+ # camera entity per accessory, not 1 camera per service.
+ assert cameras_count == 3
diff --git a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py
index 67b7508eb94..168ae85b228 100644
--- a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py
+++ b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py
@@ -51,16 +51,15 @@ async def test_hue_bridge_setup(hass):
]
for button in ("button1", "button2", "button3", "button4"):
- for subtype in ("single_press", "double_press", "long_press"):
- expected.append(
- {
- "device_id": device.id,
- "domain": "homekit_controller",
- "platform": "device",
- "type": button,
- "subtype": subtype,
- }
- )
+ expected.append(
+ {
+ "device_id": device.id,
+ "domain": "homekit_controller",
+ "platform": "device",
+ "type": button,
+ "subtype": "single_press",
+ }
+ )
triggers = await async_get_device_automations(hass, "trigger", device.id)
assert_lists_same(triggers, expected)
diff --git a/tests/components/homekit_controller/test_camera.py b/tests/components/homekit_controller/test_camera.py
new file mode 100644
index 00000000000..ddb25fddffc
--- /dev/null
+++ b/tests/components/homekit_controller/test_camera.py
@@ -0,0 +1,29 @@
+"""Basic checks for HomeKit cameras."""
+import base64
+
+from aiohomekit.model.services import ServicesTypes
+from aiohomekit.testing import FAKE_CAMERA_IMAGE
+
+from homeassistant.components import camera
+
+from tests.components.homekit_controller.common import setup_test_component
+
+
+def create_camera(accessory):
+ """Define camera characteristics."""
+ accessory.add_service(ServicesTypes.CAMERA_RTP_STREAM_MANAGEMENT)
+
+
+async def test_read_state(hass, utcnow):
+ """Test reading the state of a HomeKit camera."""
+ helper = await setup_test_component(hass, create_camera)
+
+ state = await helper.poll_and_get_state()
+ assert state.state == "idle"
+
+
+async def test_get_image(hass, utcnow):
+ """Test getting a JPEG from a camera."""
+ helper = await setup_test_component(hass, create_camera)
+ image = await camera.async_get_image(hass, helper.entity_id)
+ assert image.content == base64.b64decode(FAKE_CAMERA_IMAGE)
diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py
index a8eb869abf4..72a8133159d 100644
--- a/tests/components/homekit_controller/test_config_flow.py
+++ b/tests/components/homekit_controller/test_config_flow.py
@@ -257,6 +257,21 @@ async def test_discovery_ignored_model(hass, controller):
"""Already paired."""
device = setup_mock_accessory(controller)
discovery_info = get_device_discovery_info(device)
+ discovery_info["properties"]["id"] = "AA:BB:CC:DD:EE:FF"
+ discovery_info["properties"]["md"] = "HHKBridge1,1"
+
+ # Device is discovered
+ result = await hass.config_entries.flow.async_init(
+ "homekit_controller", context={"source": "zeroconf"}, data=discovery_info
+ )
+ assert result["type"] == "abort"
+ assert result["reason"] == "ignored_model"
+
+
+async def test_discovery_ignored_hk_bridge(hass, controller):
+ """Already paired."""
+ device = setup_mock_accessory(controller)
+ discovery_info = get_device_discovery_info(device)
config_entry = MockConfigEntry(domain=config_flow.HOMEKIT_BRIDGE_DOMAIN, data={})
formatted_mac = device_registry.format_mac("AA:BB:CC:DD:EE:FF")
diff --git a/tests/components/homekit_controller/test_device_trigger.py b/tests/components/homekit_controller/test_device_trigger.py
index c8ef2cbef38..9de9a30f99f 100644
--- a/tests/components/homekit_controller/test_device_trigger.py
+++ b/tests/components/homekit_controller/test_device_trigger.py
@@ -12,6 +12,7 @@ from tests.common import (
async_get_device_automations,
async_mock_service,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
from tests.components.homekit_controller.common import setup_test_component
diff --git a/tests/components/homekit_controller/test_humidifier.py b/tests/components/homekit_controller/test_humidifier.py
new file mode 100644
index 00000000000..0af795e2ce9
--- /dev/null
+++ b/tests/components/homekit_controller/test_humidifier.py
@@ -0,0 +1,333 @@
+"""Basic checks for HomeKit Humidifier/Dehumidifier."""
+from aiohomekit.model.characteristics import CharacteristicsTypes
+from aiohomekit.model.services import ServicesTypes
+
+from homeassistant.components.humidifier import DOMAIN
+from homeassistant.components.humidifier.const import MODE_AUTO, MODE_NORMAL
+
+from tests.components.homekit_controller.common import setup_test_component
+
+ACTIVE = ("humidifier-dehumidifier", "active")
+CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE = (
+ "humidifier-dehumidifier",
+ "humidifier-dehumidifier.state.current",
+)
+TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE = (
+ "humidifier-dehumidifier",
+ "humidifier-dehumidifier.state.target",
+)
+RELATIVE_HUMIDITY_CURRENT = ("humidifier-dehumidifier", "relative-humidity.current")
+RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD = (
+ "humidifier-dehumidifier",
+ "relative-humidity.humidifier-threshold",
+)
+RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD = (
+ "humidifier-dehumidifier",
+ "relative-humidity.dehumidifier-threshold",
+)
+
+
+def create_humidifier_service(accessory):
+ """Define a humidifier characteristics as per page 219 of HAP spec."""
+ service = accessory.add_service(ServicesTypes.HUMIDIFIER_DEHUMIDIFIER)
+
+ service.add_char(CharacteristicsTypes.ACTIVE, value=False)
+
+ cur_state = service.add_char(CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT)
+ cur_state.value = 0
+
+ cur_state = service.add_char(
+ CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE
+ )
+ cur_state.value = -1
+
+ targ_state = service.add_char(
+ CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE
+ )
+ targ_state.value = 0
+
+ cur_state = service.add_char(
+ CharacteristicsTypes.RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD
+ )
+ cur_state.value = 0
+
+ return service
+
+
+def create_dehumidifier_service(accessory):
+ """Define a dehumidifier characteristics as per page 219 of HAP spec."""
+ service = accessory.add_service(ServicesTypes.HUMIDIFIER_DEHUMIDIFIER)
+
+ service.add_char(CharacteristicsTypes.ACTIVE, value=False)
+
+ cur_state = service.add_char(CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT)
+ cur_state.value = 0
+
+ cur_state = service.add_char(
+ CharacteristicsTypes.CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE
+ )
+ cur_state.value = -1
+
+ targ_state = service.add_char(
+ CharacteristicsTypes.TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE
+ )
+ targ_state.value = 0
+
+ targ_state = service.add_char(
+ CharacteristicsTypes.RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD
+ )
+ targ_state.value = 0
+
+ return service
+
+
+async def test_humidifier_active_state(hass, utcnow):
+ """Test that we can turn a HomeKit humidifier on and off again."""
+ helper = await setup_test_component(hass, create_humidifier_service)
+
+ await hass.services.async_call(
+ DOMAIN, "turn_on", {"entity_id": helper.entity_id}, blocking=True
+ )
+
+ assert helper.characteristics[ACTIVE].value == 1
+
+ await hass.services.async_call(
+ DOMAIN, "turn_off", {"entity_id": helper.entity_id}, blocking=True
+ )
+
+ assert helper.characteristics[ACTIVE].value == 0
+
+
+async def test_dehumidifier_active_state(hass, utcnow):
+ """Test that we can turn a HomeKit dehumidifier on and off again."""
+ helper = await setup_test_component(hass, create_dehumidifier_service)
+
+ await hass.services.async_call(
+ DOMAIN, "turn_on", {"entity_id": helper.entity_id}, blocking=True
+ )
+
+ assert helper.characteristics[ACTIVE].value == 1
+
+ await hass.services.async_call(
+ DOMAIN, "turn_off", {"entity_id": helper.entity_id}, blocking=True
+ )
+
+ assert helper.characteristics[ACTIVE].value == 0
+
+
+async def test_humidifier_read_humidity(hass, utcnow):
+ """Test that we can read the state of a HomeKit humidifier accessory."""
+ helper = await setup_test_component(hass, create_humidifier_service)
+
+ helper.characteristics[ACTIVE].value = True
+ helper.characteristics[RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD].value = 75
+ state = await helper.poll_and_get_state()
+ assert state.state == "on"
+ assert state.attributes["humidity"] == 75
+
+ helper.characteristics[ACTIVE].value = False
+ helper.characteristics[RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD].value = 10
+ state = await helper.poll_and_get_state()
+ assert state.state == "off"
+ assert state.attributes["humidity"] == 10
+
+ helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 3
+ state = await helper.poll_and_get_state()
+ assert state.attributes["humidity"] == 10
+
+
+async def test_dehumidifier_read_humidity(hass, utcnow):
+ """Test that we can read the state of a HomeKit dehumidifier accessory."""
+ helper = await setup_test_component(hass, create_dehumidifier_service)
+
+ helper.characteristics[ACTIVE].value = True
+ helper.characteristics[RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD].value = 75
+ state = await helper.poll_and_get_state()
+ assert state.state == "on"
+ assert state.attributes["humidity"] == 75
+
+ helper.characteristics[ACTIVE].value = False
+ helper.characteristics[RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD].value = 40
+ state = await helper.poll_and_get_state()
+ assert state.state == "off"
+ assert state.attributes["humidity"] == 40
+
+ helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 2
+ state = await helper.poll_and_get_state()
+ assert state.attributes["humidity"] == 40
+
+
+async def test_humidifier_set_humidity(hass, utcnow):
+ """Test that we can set the state of a HomeKit humidifier accessory."""
+ helper = await setup_test_component(hass, create_humidifier_service)
+
+ await hass.services.async_call(
+ DOMAIN,
+ "set_humidity",
+ {"entity_id": helper.entity_id, "humidity": 20},
+ blocking=True,
+ )
+ assert helper.characteristics[RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD].value == 20
+
+
+async def test_dehumidifier_set_humidity(hass, utcnow):
+ """Test that we can set the state of a HomeKit dehumidifier accessory."""
+ helper = await setup_test_component(hass, create_dehumidifier_service)
+
+ await hass.services.async_call(
+ DOMAIN,
+ "set_humidity",
+ {"entity_id": helper.entity_id, "humidity": 20},
+ blocking=True,
+ )
+ assert helper.characteristics[RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD].value == 20
+
+
+async def test_humidifier_set_mode(hass, utcnow):
+ """Test that we can set the mode of a HomeKit humidifier accessory."""
+ helper = await setup_test_component(hass, create_humidifier_service)
+
+ await hass.services.async_call(
+ DOMAIN,
+ "set_mode",
+ {"entity_id": helper.entity_id, "mode": MODE_AUTO},
+ blocking=True,
+ )
+ assert helper.characteristics[TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE].value == 0
+ assert helper.characteristics[ACTIVE].value == 1
+
+ await hass.services.async_call(
+ DOMAIN,
+ "set_mode",
+ {"entity_id": helper.entity_id, "mode": MODE_NORMAL},
+ blocking=True,
+ )
+ assert helper.characteristics[TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE].value == 1
+ assert helper.characteristics[ACTIVE].value == 1
+
+
+async def test_dehumidifier_set_mode(hass, utcnow):
+ """Test that we can set the mode of a HomeKit dehumidifier accessory."""
+ helper = await setup_test_component(hass, create_dehumidifier_service)
+
+ await hass.services.async_call(
+ DOMAIN,
+ "set_mode",
+ {"entity_id": helper.entity_id, "mode": MODE_AUTO},
+ blocking=True,
+ )
+ assert helper.characteristics[TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE].value == 0
+ assert helper.characteristics[ACTIVE].value == 1
+
+ await hass.services.async_call(
+ DOMAIN,
+ "set_mode",
+ {"entity_id": helper.entity_id, "mode": MODE_NORMAL},
+ blocking=True,
+ )
+ assert helper.characteristics[TARGET_HUMIDIFIER_DEHUMIDIFIER_STATE].value == 2
+ assert helper.characteristics[ACTIVE].value == 1
+
+
+async def test_humidifier_read_only_mode(hass, utcnow):
+ """Test that we can read the state of a HomeKit humidifier accessory."""
+ helper = await setup_test_component(hass, create_humidifier_service)
+
+ state = await helper.poll_and_get_state()
+ assert state.attributes["mode"] == "normal"
+
+ helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 0
+ state = await helper.poll_and_get_state()
+ assert state.attributes["mode"] == "normal"
+
+ helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 1
+ state = await helper.poll_and_get_state()
+ assert state.attributes["mode"] == "auto"
+
+ helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 2
+ state = await helper.poll_and_get_state()
+ assert state.attributes["mode"] == "normal"
+
+ helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 3
+ state = await helper.poll_and_get_state()
+ assert state.attributes["mode"] == "normal"
+
+
+async def test_dehumidifier_read_only_mode(hass, utcnow):
+ """Test that we can read the state of a HomeKit dehumidifier accessory."""
+ helper = await setup_test_component(hass, create_dehumidifier_service)
+
+ state = await helper.poll_and_get_state()
+ assert state.attributes["mode"] == "normal"
+
+ helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 0
+ state = await helper.poll_and_get_state()
+ assert state.attributes["mode"] == "normal"
+
+ helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 1
+ state = await helper.poll_and_get_state()
+ assert state.attributes["mode"] == "auto"
+
+ helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 2
+ state = await helper.poll_and_get_state()
+ assert state.attributes["mode"] == "normal"
+
+ helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 3
+ state = await helper.poll_and_get_state()
+ assert state.attributes["mode"] == "normal"
+
+
+async def test_humidifier_target_humidity_modes(hass, utcnow):
+ """Test that we can read the state of a HomeKit humidifier accessory."""
+ helper = await setup_test_component(hass, create_humidifier_service)
+
+ helper.characteristics[RELATIVE_HUMIDITY_HUMIDIFIER_THRESHOLD].value = 37
+ helper.characteristics[RELATIVE_HUMIDITY_CURRENT].value = 51
+ helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 1
+
+ state = await helper.poll_and_get_state()
+ assert state.attributes["mode"] == "auto"
+ assert state.attributes["humidity"] == 37
+
+ helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 3
+ state = await helper.poll_and_get_state()
+ assert state.attributes["mode"] == "normal"
+ assert state.attributes["humidity"] == 37
+
+ helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 2
+ state = await helper.poll_and_get_state()
+ assert state.attributes["mode"] == "normal"
+ assert state.attributes["humidity"] == 37
+
+ helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 0
+ state = await helper.poll_and_get_state()
+ assert state.attributes["mode"] == "normal"
+ assert state.attributes["humidity"] == 37
+
+
+async def test_dehumidifier_target_humidity_modes(hass, utcnow):
+ """Test that we can read the state of a HomeKit dehumidifier accessory."""
+ helper = await setup_test_component(hass, create_dehumidifier_service)
+
+ helper.characteristics[RELATIVE_HUMIDITY_DEHUMIDIFIER_THRESHOLD].value = 73
+ helper.characteristics[RELATIVE_HUMIDITY_CURRENT].value = 51
+ helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 1
+
+ state = await helper.poll_and_get_state()
+ assert state.attributes["mode"] == "auto"
+ assert state.attributes["humidity"] == 73
+
+ helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 3
+ state = await helper.poll_and_get_state()
+ assert state.attributes["mode"] == "normal"
+ assert state.attributes["humidity"] == 73
+
+ helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 2
+ state = await helper.poll_and_get_state()
+ assert state.attributes["mode"] == "normal"
+ assert state.attributes["humidity"] == 73
+
+ helper.characteristics[CURRENT_HUMIDIFIER_DEHUMIDIFIER_STATE].value = 0
+ state = await helper.poll_and_get_state()
+ assert state.attributes["mode"] == "normal"
+ assert state.attributes["humidity"] == 73
diff --git a/tests/components/hue/test_device_trigger.py b/tests/components/hue/test_device_trigger.py
index b6d3f4f2f50..0975c644e61 100644
--- a/tests/components/hue/test_device_trigger.py
+++ b/tests/components/hue/test_device_trigger.py
@@ -15,6 +15,7 @@ from tests.common import (
async_mock_service,
mock_device_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
REMOTES_RESPONSE = {"7": HUE_TAP_REMOTE_1, "8": HUE_DIMMER_REMOTE_1}
diff --git a/tests/components/humidifier/test_device_action.py b/tests/components/humidifier/test_device_action.py
index 91b7819e18b..93b97408c39 100644
--- a/tests/components/humidifier/test_device_action.py
+++ b/tests/components/humidifier/test_device_action.py
@@ -16,6 +16,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/humidifier/test_device_condition.py b/tests/components/humidifier/test_device_condition.py
index 76a850887ca..ad001d52ae0 100644
--- a/tests/components/humidifier/test_device_condition.py
+++ b/tests/components/humidifier/test_device_condition.py
@@ -17,6 +17,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/humidifier/test_device_trigger.py b/tests/components/humidifier/test_device_trigger.py
index 4f93b4be4de..7cd736b79f4 100644
--- a/tests/components/humidifier/test_device_trigger.py
+++ b/tests/components/humidifier/test_device_trigger.py
@@ -20,6 +20,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/hyperion/__init__.py b/tests/components/hyperion/__init__.py
index e4c1ee67efa..a2febcca2a5 100644
--- a/tests/components/hyperion/__init__.py
+++ b/tests/components/hyperion/__init__.py
@@ -1 +1,136 @@
"""Tests for the Hyperion component."""
+from __future__ import annotations
+
+import logging
+from types import TracebackType
+from typing import Any, Dict, Optional, Type
+
+from hyperion import const
+
+from homeassistant.components.hyperion.const import CONF_PRIORITY, DOMAIN
+from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import CONF_HOST, CONF_PORT
+from homeassistant.helpers.typing import HomeAssistantType
+
+from tests.async_mock import AsyncMock, Mock, patch # type: ignore[attr-defined]
+from tests.common import MockConfigEntry
+
+TEST_HOST = "test"
+TEST_PORT = const.DEFAULT_PORT_JSON + 1
+TEST_PORT_UI = const.DEFAULT_PORT_UI + 1
+TEST_INSTANCE = 1
+TEST_SYSINFO_ID = "f9aab089-f85a-55cf-b7c1-222a72faebe9"
+TEST_SYSINFO_VERSION = "2.0.0-alpha.8"
+TEST_PRIORITY = 180
+TEST_YAML_NAME = f"{TEST_HOST}_{TEST_PORT}_{TEST_INSTANCE}"
+TEST_YAML_ENTITY_ID = f"{LIGHT_DOMAIN}.{TEST_YAML_NAME}"
+TEST_ENTITY_ID_1 = "light.test_instance_1"
+TEST_ENTITY_ID_2 = "light.test_instance_2"
+TEST_ENTITY_ID_3 = "light.test_instance_3"
+TEST_TITLE = f"{TEST_HOST}:{TEST_PORT}"
+
+TEST_TOKEN = "sekr1t"
+TEST_CONFIG_ENTRY_ID = "74565ad414754616000674c87bdc876c"
+TEST_CONFIG_ENTRY_OPTIONS: Dict[str, Any] = {CONF_PRIORITY: TEST_PRIORITY}
+
+TEST_INSTANCE_1: Dict[str, Any] = {
+ "friendly_name": "Test instance 1",
+ "instance": 1,
+ "running": True,
+}
+TEST_INSTANCE_2: Dict[str, Any] = {
+ "friendly_name": "Test instance 2",
+ "instance": 2,
+ "running": True,
+}
+TEST_INSTANCE_3: Dict[str, Any] = {
+ "friendly_name": "Test instance 3",
+ "instance": 3,
+ "running": True,
+}
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class AsyncContextManagerMock(Mock): # type: ignore[misc]
+ """An async context manager mock for Hyperion."""
+
+ async def __aenter__(self) -> Optional[AsyncContextManagerMock]:
+ """Enter context manager and connect the client."""
+ result = await self.async_client_connect()
+ return self if result else None
+
+ async def __aexit__(
+ self,
+ exc_type: Optional[Type[BaseException]],
+ exc: Optional[BaseException],
+ traceback: Optional[TracebackType],
+ ) -> None:
+ """Leave context manager and disconnect the client."""
+ await self.async_client_disconnect()
+
+
+def create_mock_client() -> Mock:
+ """Create a mock Hyperion client."""
+ mock_client = AsyncContextManagerMock()
+ # pylint: disable=attribute-defined-outside-init
+ mock_client.async_client_connect = AsyncMock(return_value=True)
+ mock_client.async_client_disconnect = AsyncMock(return_value=True)
+ mock_client.async_is_auth_required = AsyncMock(
+ return_value={
+ "command": "authorize-tokenRequired",
+ "info": {"required": False},
+ "success": True,
+ "tan": 1,
+ }
+ )
+ mock_client.async_login = AsyncMock(
+ return_value={"command": "authorize-login", "success": True, "tan": 0}
+ )
+
+ mock_client.async_sysinfo_id = AsyncMock(return_value=TEST_SYSINFO_ID)
+ mock_client.async_sysinfo_version = AsyncMock(return_value=TEST_SYSINFO_ID)
+ mock_client.adjustment = None
+ mock_client.effects = None
+ mock_client.instances = [
+ {"friendly_name": "Test instance 1", "instance": 0, "running": True}
+ ]
+
+ return mock_client
+
+
+def add_test_config_entry(hass: HomeAssistantType) -> ConfigEntry:
+ """Add a test config entry."""
+ config_entry: MockConfigEntry = MockConfigEntry( # type: ignore[no-untyped-call]
+ entry_id=TEST_CONFIG_ENTRY_ID,
+ domain=DOMAIN,
+ data={
+ CONF_HOST: TEST_HOST,
+ CONF_PORT: TEST_PORT,
+ },
+ title=f"Hyperion {TEST_SYSINFO_ID}",
+ unique_id=TEST_SYSINFO_ID,
+ options=TEST_CONFIG_ENTRY_OPTIONS,
+ )
+ config_entry.add_to_hass(hass) # type: ignore[no-untyped-call]
+ return config_entry
+
+
+async def setup_test_config_entry(
+ hass: HomeAssistantType, hyperion_client: Optional[Mock] = None
+) -> ConfigEntry:
+ """Add a test Hyperion entity to hass."""
+ config_entry = add_test_config_entry(hass)
+
+ hyperion_client = hyperion_client or create_mock_client()
+ # pylint: disable=attribute-defined-outside-init
+ hyperion_client.instances = [TEST_INSTANCE_1]
+
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient",
+ return_value=hyperion_client,
+ ):
+ await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+ return config_entry
diff --git a/tests/components/hyperion/test_config_flow.py b/tests/components/hyperion/test_config_flow.py
new file mode 100644
index 00000000000..807a3829e7b
--- /dev/null
+++ b/tests/components/hyperion/test_config_flow.py
@@ -0,0 +1,696 @@
+"""Tests for the Hyperion config flow."""
+
+import logging
+from typing import Any, Dict, Optional
+
+from hyperion import const
+
+from homeassistant import data_entry_flow
+from homeassistant.components.hyperion.const import (
+ CONF_AUTH_ID,
+ CONF_CREATE_TOKEN,
+ CONF_PRIORITY,
+ DOMAIN,
+ SOURCE_IMPORT,
+)
+from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
+from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ CONF_HOST,
+ CONF_PORT,
+ CONF_TOKEN,
+ SERVICE_TURN_ON,
+)
+from homeassistant.helpers.typing import HomeAssistantType
+
+from . import (
+ TEST_CONFIG_ENTRY_ID,
+ TEST_ENTITY_ID_1,
+ TEST_HOST,
+ TEST_INSTANCE,
+ TEST_PORT,
+ TEST_PORT_UI,
+ TEST_SYSINFO_ID,
+ TEST_TITLE,
+ TEST_TOKEN,
+ add_test_config_entry,
+ create_mock_client,
+)
+
+from tests.async_mock import AsyncMock, patch # type: ignore[attr-defined]
+from tests.common import MockConfigEntry
+
+_LOGGER = logging.getLogger(__name__)
+
+TEST_IP_ADDRESS = "192.168.0.1"
+TEST_HOST_PORT: Dict[str, Any] = {
+ CONF_HOST: TEST_HOST,
+ CONF_PORT: TEST_PORT,
+}
+
+TEST_AUTH_REQUIRED_RESP = {
+ "command": "authorize-tokenRequired",
+ "info": {
+ "required": True,
+ },
+ "success": True,
+ "tan": 1,
+}
+
+TEST_AUTH_ID = "ABCDE"
+TEST_REQUEST_TOKEN_SUCCESS = {
+ "command": "authorize-requestToken",
+ "success": True,
+ "info": {"comment": const.DEFAULT_ORIGIN, "id": TEST_AUTH_ID, "token": TEST_TOKEN},
+}
+
+TEST_REQUEST_TOKEN_FAIL = {
+ "command": "authorize-requestToken",
+ "success": False,
+ "error": "Token request timeout or denied",
+}
+
+TEST_SSDP_SERVICE_INFO = {
+ "ssdp_location": f"http://{TEST_HOST}:{TEST_PORT_UI}/description.xml",
+ "ssdp_st": "upnp:rootdevice",
+ "deviceType": "urn:schemas-upnp-org:device:Basic:1",
+ "friendlyName": f"Hyperion ({TEST_HOST})",
+ "manufacturer": "Hyperion Open Source Ambient Lighting",
+ "manufacturerURL": "https://www.hyperion-project.org",
+ "modelDescription": "Hyperion Open Source Ambient Light",
+ "modelName": "Hyperion",
+ "modelNumber": "2.0.0-alpha.8",
+ "modelURL": "https://www.hyperion-project.org",
+ "serialNumber": f"{TEST_SYSINFO_ID}",
+ "UDN": f"uuid:{TEST_SYSINFO_ID}",
+ "ports": {
+ "jsonServer": f"{TEST_PORT}",
+ "sslServer": "8092",
+ "protoBuffer": "19445",
+ "flatBuffer": "19400",
+ },
+ "presentationURL": "index.html",
+ "iconList": {
+ "icon": {
+ "mimetype": "image/png",
+ "height": "100",
+ "width": "100",
+ "depth": "32",
+ "url": "img/hyperion/ssdp_icon.png",
+ }
+ },
+ "ssdp_usn": f"uuid:{TEST_SYSINFO_ID}",
+ "ssdp_ext": "",
+ "ssdp_server": "Raspbian GNU/Linux 10 (buster)/10 UPnP/1.0 Hyperion/2.0.0-alpha.8",
+}
+
+
+async def _create_mock_entry(hass: HomeAssistantType) -> MockConfigEntry:
+ """Add a test Hyperion entity to hass."""
+ entry: MockConfigEntry = MockConfigEntry( # type: ignore[no-untyped-call]
+ entry_id=TEST_CONFIG_ENTRY_ID,
+ domain=DOMAIN,
+ unique_id=TEST_SYSINFO_ID,
+ title=TEST_TITLE,
+ data={
+ "host": TEST_HOST,
+ "port": TEST_PORT,
+ "instance": TEST_INSTANCE,
+ },
+ )
+ entry.add_to_hass(hass) # type: ignore[no-untyped-call]
+
+ # Setup
+ client = create_mock_client()
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient", return_value=client
+ ):
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ return entry
+
+
+async def _init_flow(
+ hass: HomeAssistantType,
+ source: str = SOURCE_USER,
+ data: Optional[Dict[str, Any]] = None,
+) -> Any:
+ """Initialize a flow."""
+ data = data or {}
+
+ return await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": source}, data=data
+ )
+
+
+async def _configure_flow(
+ hass: HomeAssistantType, result: Dict, user_input: Optional[Dict[str, Any]] = None
+) -> Any:
+ """Provide input to a flow."""
+ user_input = user_input or {}
+
+ with patch(
+ "homeassistant.components.hyperion.async_setup", return_value=True
+ ), patch(
+ "homeassistant.components.hyperion.async_setup_entry",
+ return_value=True,
+ ):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], user_input=user_input
+ )
+ await hass.async_block_till_done()
+ return result
+
+
+async def test_user_if_no_configuration(hass: HomeAssistantType) -> None:
+ """Check flow behavior when no configuration is present."""
+ result = await _init_flow(hass)
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "user"
+ assert result["handler"] == DOMAIN
+
+
+async def test_user_existing_id_abort(hass: HomeAssistantType) -> None:
+ """Verify a duplicate ID results in an abort."""
+ result = await _init_flow(hass)
+
+ await _create_mock_entry(hass)
+ client = create_mock_client()
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient", return_value=client
+ ):
+ result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT)
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "already_configured"
+
+
+async def test_user_client_errors(hass: HomeAssistantType) -> None:
+ """Verify correct behaviour with client errors."""
+ result = await _init_flow(hass)
+
+ client = create_mock_client()
+
+ # Fail the connection.
+ client.async_client_connect = AsyncMock(return_value=False)
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient", return_value=client
+ ):
+ result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT)
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"]["base"] == "cannot_connect"
+
+ # Fail the auth check call.
+ client.async_client_connect = AsyncMock(return_value=True)
+ client.async_is_auth_required = AsyncMock(return_value={"success": False})
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient", return_value=client
+ ):
+ result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT)
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "auth_required_error"
+
+
+async def test_user_confirm_cannot_connect(hass: HomeAssistantType) -> None:
+ """Test a failure to connect during confirmation."""
+
+ result = await _init_flow(hass)
+
+ good_client = create_mock_client()
+ bad_client = create_mock_client()
+ bad_client.async_client_connect = AsyncMock(return_value=False)
+
+ # Confirmation sync_client_connect fails.
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient",
+ side_effect=[good_client, bad_client],
+ ):
+ result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT)
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "cannot_connect"
+
+
+async def test_user_confirm_id_error(hass: HomeAssistantType) -> None:
+ """Test a failure fetching the server id during confirmation."""
+ result = await _init_flow(hass)
+
+ client = create_mock_client()
+ client.async_sysinfo_id = AsyncMock(return_value=None)
+
+ # Confirmation sync_client_connect fails.
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient", return_value=client
+ ):
+ result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT)
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "no_id"
+
+
+async def test_user_noauth_flow_success(hass: HomeAssistantType) -> None:
+ """Check a full flow without auth."""
+ result = await _init_flow(hass)
+
+ client = create_mock_client()
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient", return_value=client
+ ):
+ result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT)
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["handler"] == DOMAIN
+ assert result["title"] == TEST_TITLE
+ assert result["data"] == {
+ **TEST_HOST_PORT,
+ }
+
+
+async def test_user_auth_required(hass: HomeAssistantType) -> None:
+ """Verify correct behaviour when auth is required."""
+ result = await _init_flow(hass)
+
+ client = create_mock_client()
+ client.async_is_auth_required = AsyncMock(return_value=TEST_AUTH_REQUIRED_RESP)
+
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient", return_value=client
+ ):
+ result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT)
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "auth"
+
+
+async def test_auth_static_token_auth_required_fail(hass: HomeAssistantType) -> None:
+ """Verify correct behaviour with a failed auth required call."""
+ result = await _init_flow(hass)
+
+ client = create_mock_client()
+ client.async_is_auth_required = AsyncMock(return_value=None)
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient", return_value=client
+ ):
+ result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT)
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "auth_required_error"
+
+
+async def test_auth_static_token_success(hass: HomeAssistantType) -> None:
+ """Test a successful flow with a static token."""
+ result = await _init_flow(hass)
+ assert result["step_id"] == "user"
+
+ client = create_mock_client()
+ client.async_is_auth_required = AsyncMock(return_value=TEST_AUTH_REQUIRED_RESP)
+
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient", return_value=client
+ ):
+ result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT)
+ result = await _configure_flow(
+ hass, result, user_input={CONF_CREATE_TOKEN: False, CONF_TOKEN: TEST_TOKEN}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["handler"] == DOMAIN
+ assert result["title"] == TEST_TITLE
+ assert result["data"] == {
+ **TEST_HOST_PORT,
+ CONF_TOKEN: TEST_TOKEN,
+ }
+
+
+async def test_auth_static_token_login_fail(hass: HomeAssistantType) -> None:
+ """Test correct behavior with a bad static token."""
+ result = await _init_flow(hass)
+ assert result["step_id"] == "user"
+
+ client = create_mock_client()
+ client.async_is_auth_required = AsyncMock(return_value=TEST_AUTH_REQUIRED_RESP)
+
+ # Fail the login call.
+ client.async_login = AsyncMock(
+ return_value={"command": "authorize-login", "success": False, "tan": 0}
+ )
+
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient", return_value=client
+ ):
+ result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT)
+ result = await _configure_flow(
+ hass, result, user_input={CONF_CREATE_TOKEN: False, CONF_TOKEN: TEST_TOKEN}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["errors"]["base"] == "invalid_access_token"
+
+
+async def test_auth_create_token_approval_declined(hass: HomeAssistantType) -> None:
+ """Verify correct behaviour when a token request is declined."""
+ result = await _init_flow(hass)
+
+ client = create_mock_client()
+ client.async_is_auth_required = AsyncMock(return_value=TEST_AUTH_REQUIRED_RESP)
+
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient", return_value=client
+ ):
+ result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT)
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "auth"
+
+ client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_FAIL)
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient", return_value=client
+ ), patch(
+ "homeassistant.components.hyperion.config_flow.client.generate_random_auth_id",
+ return_value=TEST_AUTH_ID,
+ ):
+ result = await _configure_flow(
+ hass, result, user_input={CONF_CREATE_TOKEN: True}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "create_token"
+ assert result["description_placeholders"] == {
+ CONF_AUTH_ID: TEST_AUTH_ID,
+ }
+
+ result = await _configure_flow(hass, result)
+ await hass.async_block_till_done()
+ assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP
+ assert result["step_id"] == "create_token_external"
+
+ # The flow will be automatically advanced by the auth token response.
+
+ result = await _configure_flow(hass, result)
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "auth_new_token_not_granted_error"
+
+
+async def test_auth_create_token_when_issued_token_fails(
+ hass: HomeAssistantType,
+) -> None:
+ """Verify correct behaviour when a token is granted by fails to authenticate."""
+ result = await _init_flow(hass)
+
+ client = create_mock_client()
+ client.async_is_auth_required = AsyncMock(return_value=TEST_AUTH_REQUIRED_RESP)
+
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient", return_value=client
+ ):
+ result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT)
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "auth"
+
+ client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_SUCCESS)
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient", return_value=client
+ ), patch(
+ "homeassistant.components.hyperion.config_flow.client.generate_random_auth_id",
+ return_value=TEST_AUTH_ID,
+ ):
+ result = await _configure_flow(
+ hass, result, user_input={CONF_CREATE_TOKEN: True}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "create_token"
+ assert result["description_placeholders"] == {
+ CONF_AUTH_ID: TEST_AUTH_ID,
+ }
+
+ result = await _configure_flow(hass, result)
+ assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP
+ assert result["step_id"] == "create_token_external"
+
+ # The flow will be automatically advanced by the auth token response.
+
+ # Make the last verification fail.
+ client.async_client_connect = AsyncMock(return_value=False)
+
+ result = await _configure_flow(hass, result)
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "cannot_connect"
+
+
+async def test_auth_create_token_success(hass: HomeAssistantType) -> None:
+ """Verify correct behaviour when a token is successfully created."""
+ result = await _init_flow(hass)
+
+ client = create_mock_client()
+ client.async_is_auth_required = AsyncMock(return_value=TEST_AUTH_REQUIRED_RESP)
+
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient", return_value=client
+ ):
+ result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT)
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "auth"
+
+ client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_SUCCESS)
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient", return_value=client
+ ), patch(
+ "homeassistant.components.hyperion.config_flow.client.generate_random_auth_id",
+ return_value=TEST_AUTH_ID,
+ ):
+ result = await _configure_flow(
+ hass, result, user_input={CONF_CREATE_TOKEN: True}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "create_token"
+ assert result["description_placeholders"] == {
+ CONF_AUTH_ID: TEST_AUTH_ID,
+ }
+
+ result = await _configure_flow(hass, result)
+ assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP
+ assert result["step_id"] == "create_token_external"
+
+ # The flow will be automatically advanced by the auth token response.
+ result = await _configure_flow(hass, result)
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["handler"] == DOMAIN
+ assert result["title"] == TEST_TITLE
+ assert result["data"] == {
+ **TEST_HOST_PORT,
+ CONF_TOKEN: TEST_TOKEN,
+ }
+
+
+async def test_ssdp_success(hass: HomeAssistantType) -> None:
+ """Check an SSDP flow."""
+
+ client = create_mock_client()
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient", return_value=client
+ ):
+ result = await _init_flow(hass, source=SOURCE_SSDP, data=TEST_SSDP_SERVICE_INFO)
+ await hass.async_block_till_done()
+
+ # Accept the confirmation.
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient", return_value=client
+ ):
+ result = await _configure_flow(hass, result)
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["handler"] == DOMAIN
+ assert result["title"] == TEST_TITLE
+ assert result["data"] == {
+ CONF_HOST: TEST_HOST,
+ CONF_PORT: TEST_PORT,
+ }
+
+
+async def test_ssdp_cannot_connect(hass: HomeAssistantType) -> None:
+ """Check an SSDP flow that cannot connect."""
+
+ client = create_mock_client()
+ client.async_client_connect = AsyncMock(return_value=False)
+
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient", return_value=client
+ ):
+ result = await _init_flow(hass, source=SOURCE_SSDP, data=TEST_SSDP_SERVICE_INFO)
+ await hass.async_block_till_done()
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "cannot_connect"
+
+
+async def test_ssdp_missing_serial(hass: HomeAssistantType) -> None:
+ """Check an SSDP flow where no id is provided."""
+
+ client = create_mock_client()
+ bad_data = {**TEST_SSDP_SERVICE_INFO}
+ del bad_data["serialNumber"]
+
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient", return_value=client
+ ):
+ result = await _init_flow(hass, source=SOURCE_SSDP, data=bad_data)
+ await hass.async_block_till_done()
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "no_id"
+
+
+async def test_ssdp_failure_bad_port_json(hass: HomeAssistantType) -> None:
+ """Check an SSDP flow with bad json port."""
+
+ client = create_mock_client()
+ bad_data: Dict[str, Any] = {**TEST_SSDP_SERVICE_INFO}
+ bad_data["ports"]["jsonServer"] = "not_a_port"
+
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient", return_value=client
+ ):
+ result = await _init_flow(hass, source=SOURCE_SSDP, data=bad_data)
+ result = await _configure_flow(hass, result)
+ await hass.async_block_till_done()
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["data"][CONF_PORT] == const.DEFAULT_PORT_JSON
+
+
+async def test_ssdp_failure_bad_port_ui(hass: HomeAssistantType) -> None:
+ """Check an SSDP flow with bad ui port."""
+
+ client = create_mock_client()
+ client.async_is_auth_required = AsyncMock(return_value=TEST_AUTH_REQUIRED_RESP)
+
+ bad_data = {**TEST_SSDP_SERVICE_INFO}
+ bad_data["ssdp_location"] = f"http://{TEST_HOST}:not_a_port/description.xml"
+
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient", return_value=client
+ ), patch(
+ "homeassistant.components.hyperion.config_flow.client.generate_random_auth_id",
+ return_value=TEST_AUTH_ID,
+ ):
+ result = await _init_flow(hass, source=SOURCE_SSDP, data=bad_data)
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "auth"
+
+ client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_FAIL)
+
+ result = await _configure_flow(
+ hass, result, user_input={CONF_CREATE_TOKEN: True}
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "create_token"
+
+ # Verify a working URL is used despite the bad port number
+ assert result["description_placeholders"] == {
+ CONF_AUTH_ID: TEST_AUTH_ID,
+ }
+
+
+async def test_ssdp_abort_duplicates(hass: HomeAssistantType) -> None:
+ """Check an SSDP flow where no id is provided."""
+
+ client = create_mock_client()
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient", return_value=client
+ ):
+ result_1 = await _init_flow(
+ hass, source=SOURCE_SSDP, data=TEST_SSDP_SERVICE_INFO
+ )
+ result_2 = await _init_flow(
+ hass, source=SOURCE_SSDP, data=TEST_SSDP_SERVICE_INFO
+ )
+ await hass.async_block_till_done()
+
+ assert result_1["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result_2["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result_2["reason"] == "already_in_progress"
+
+
+async def test_import_success(hass: HomeAssistantType) -> None:
+ """Check an import flow from the old-style YAML."""
+
+ client = create_mock_client()
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient", return_value=client
+ ):
+ result = await _init_flow(
+ hass,
+ source=SOURCE_IMPORT,
+ data={
+ CONF_HOST: TEST_HOST,
+ CONF_PORT: TEST_PORT,
+ },
+ )
+ await hass.async_block_till_done()
+
+ # No human interaction should be required.
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["handler"] == DOMAIN
+ assert result["title"] == TEST_TITLE
+ assert result["data"] == {
+ CONF_HOST: TEST_HOST,
+ CONF_PORT: TEST_PORT,
+ }
+
+
+async def test_import_cannot_connect(hass: HomeAssistantType) -> None:
+ """Check an import flow that cannot connect."""
+
+ client = create_mock_client()
+ client.async_client_connect = AsyncMock(return_value=False)
+
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient", return_value=client
+ ):
+ result = await _init_flow(
+ hass,
+ source=SOURCE_IMPORT,
+ data={
+ CONF_HOST: TEST_HOST,
+ CONF_PORT: TEST_PORT,
+ },
+ )
+ await hass.async_block_till_done()
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "cannot_connect"
+
+
+async def test_options(hass: HomeAssistantType) -> None:
+ """Check an options flow."""
+
+ config_entry = add_test_config_entry(hass)
+
+ client = create_mock_client()
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient", return_value=client
+ ):
+ await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+ assert hass.states.get(TEST_ENTITY_ID_1) is not None
+
+ result = await hass.config_entries.options.async_init(config_entry.entry_id)
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "init"
+
+ new_priority = 1
+ result = await hass.config_entries.options.async_configure(
+ result["flow_id"], user_input={CONF_PRIORITY: new_priority}
+ )
+ await hass.async_block_till_done()
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["data"] == {CONF_PRIORITY: new_priority}
+
+ # Turn the light on and ensure the new priority is used.
+ client.async_send_set_color = AsyncMock(return_value=True)
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: TEST_ENTITY_ID_1},
+ blocking=True,
+ )
+ assert client.async_send_set_color.call_args[1][CONF_PRIORITY] == new_priority
diff --git a/tests/components/hyperion/test_light.py b/tests/components/hyperion/test_light.py
index 8250cc6c9c2..5366f6e14d1 100644
--- a/tests/components/hyperion/test_light.py
+++ b/tests/components/hyperion/test_light.py
@@ -1,82 +1,306 @@
"""Tests for the Hyperion integration."""
+import logging
+from types import MappingProxyType
+from typing import Any, Optional
+
from hyperion import const
-from homeassistant.components.hyperion import light as hyperion_light
+from homeassistant import setup
+from homeassistant.components.hyperion import (
+ get_hyperion_unique_id,
+ light as hyperion_light,
+)
+from homeassistant.components.hyperion.const import DOMAIN, TYPE_HYPERION_LIGHT
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_EFFECT,
ATTR_HS_COLOR,
- DOMAIN,
+ DOMAIN as LIGHT_DOMAIN,
)
+from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON
-from homeassistant.setup import async_setup_component
+from homeassistant.helpers.entity_registry import async_get_registry
+from homeassistant.helpers.typing import HomeAssistantType
-from tests.async_mock import AsyncMock, Mock, call, patch
+from . import (
+ TEST_CONFIG_ENTRY_OPTIONS,
+ TEST_ENTITY_ID_1,
+ TEST_ENTITY_ID_2,
+ TEST_ENTITY_ID_3,
+ TEST_HOST,
+ TEST_INSTANCE_1,
+ TEST_INSTANCE_2,
+ TEST_INSTANCE_3,
+ TEST_PORT,
+ TEST_PRIORITY,
+ TEST_SYSINFO_ID,
+ TEST_YAML_ENTITY_ID,
+ TEST_YAML_NAME,
+ add_test_config_entry,
+ create_mock_client,
+ setup_test_config_entry,
+)
-TEST_HOST = "test-hyperion-host"
-TEST_PORT = const.DEFAULT_PORT
-TEST_NAME = "test_hyperion_name"
-TEST_PRIORITY = 128
-TEST_ENTITY_ID = f"{DOMAIN}.{TEST_NAME}"
+from tests.async_mock import AsyncMock, call, patch # type: ignore[attr-defined]
+
+_LOGGER = logging.getLogger(__name__)
-def create_mock_client():
- """Create a mock Hyperion client."""
- mock_client = Mock()
- mock_client.async_client_connect = AsyncMock(return_value=True)
- mock_client.adjustment = None
- mock_client.effects = None
- mock_client.id = "%s:%i" % (TEST_HOST, TEST_PORT)
- return mock_client
-
-
-def call_registered_callback(client, key, *args, **kwargs):
+def _call_registered_callback(
+ client: AsyncMock, key: str, *args: Any, **kwargs: Any
+) -> None:
"""Call a Hyperion entity callback that was registered with the client."""
- return client.set_callbacks.call_args[0][0][key](*args, **kwargs)
+ client.set_callbacks.call_args[0][0][key](*args, **kwargs)
-async def setup_entity(hass, client=None):
+async def _setup_entity_yaml(hass: HomeAssistantType, client: AsyncMock = None) -> None:
"""Add a test Hyperion entity to hass."""
client = client or create_mock_client()
- with patch("hyperion.client.HyperionClient", return_value=client):
- assert await async_setup_component(
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient", return_value=client
+ ):
+ assert await setup.async_setup_component(
hass,
- DOMAIN,
+ LIGHT_DOMAIN,
{
- DOMAIN: {
+ LIGHT_DOMAIN: {
"platform": "hyperion",
- "name": TEST_NAME,
+ "name": TEST_YAML_NAME,
"host": TEST_HOST,
- "port": const.DEFAULT_PORT,
+ "port": TEST_PORT,
"priority": TEST_PRIORITY,
}
},
)
- await hass.async_block_till_done()
+ await hass.async_block_till_done()
-async def test_setup_platform(hass):
- """Test setting up the platform."""
+def _get_config_entry_from_unique_id(
+ hass: HomeAssistantType, unique_id: str
+) -> Optional[ConfigEntry]:
+ for entry in hass.config_entries.async_entries(domain=DOMAIN):
+ if TEST_SYSINFO_ID == entry.unique_id:
+ return entry
+ return None
+
+
+async def test_setup_yaml_already_converted(hass: HomeAssistantType) -> None:
+ """Test an already converted YAML style config."""
+ # This tests "Possibility 1" from async_setup_platform()
+
+ # Add a pre-existing config entry.
+ add_test_config_entry(hass)
client = create_mock_client()
- await setup_entity(hass, client=client)
- assert hass.states.get(TEST_ENTITY_ID) is not None
+ await _setup_entity_yaml(hass, client=client)
+
+ # Setup should be skipped for the YAML config as there is a pre-existing config
+ # entry.
+ assert hass.states.get(TEST_YAML_ENTITY_ID) is None
-async def test_setup_platform_not_ready(hass):
- """Test the platform not being ready."""
+async def test_setup_yaml_old_style_unique_id(hass: HomeAssistantType) -> None:
+ """Test an already converted YAML style config."""
+ # This tests "Possibility 2" from async_setup_platform()
+ old_unique_id = f"{TEST_HOST}:{TEST_PORT}-0"
+
+ # Add a pre-existing registry entry.
+ registry = await async_get_registry(hass)
+ registry.async_get_or_create(
+ domain=LIGHT_DOMAIN,
+ platform=DOMAIN,
+ unique_id=old_unique_id,
+ suggested_object_id=TEST_YAML_NAME,
+ )
+
+ client = create_mock_client()
+ await _setup_entity_yaml(hass, client=client)
+
+ # The entity should have been created with the same entity_id.
+ assert hass.states.get(TEST_YAML_ENTITY_ID) is not None
+
+ # The unique_id should have been updated in the registry (rather than the one
+ # specified above).
+ assert registry.async_get(TEST_YAML_ENTITY_ID).unique_id == get_hyperion_unique_id(
+ TEST_SYSINFO_ID, 0, TYPE_HYPERION_LIGHT
+ )
+ assert registry.async_get_entity_id(LIGHT_DOMAIN, DOMAIN, old_unique_id) is None
+
+ # There should be a config entry with the correct server unique_id.
+ entry = _get_config_entry_from_unique_id(hass, TEST_SYSINFO_ID)
+ assert entry
+ assert entry.options == MappingProxyType(TEST_CONFIG_ENTRY_OPTIONS)
+
+
+async def test_setup_yaml_new_style_unique_id_wo_config(
+ hass: HomeAssistantType,
+) -> None:
+ """Test an a new unique_id without a config entry."""
+ # Note: This casde should not happen in the wild, as no released version of Home
+ # Assistant should this combination, but verify correct behavior for defense in
+ # depth.
+
+ new_unique_id = get_hyperion_unique_id(TEST_SYSINFO_ID, 0, TYPE_HYPERION_LIGHT)
+ entity_id_to_preserve = "light.magic_entity"
+
+ # Add a pre-existing registry entry.
+ registry = await async_get_registry(hass)
+ registry.async_get_or_create(
+ domain=LIGHT_DOMAIN,
+ platform=DOMAIN,
+ unique_id=new_unique_id,
+ suggested_object_id=entity_id_to_preserve.split(".")[1],
+ )
+
+ client = create_mock_client()
+ await _setup_entity_yaml(hass, client=client)
+
+ # The entity should have been created with the same entity_id.
+ assert hass.states.get(entity_id_to_preserve) is not None
+
+ # The unique_id should have been updated in the registry (rather than the one
+ # specified above).
+ assert registry.async_get(entity_id_to_preserve).unique_id == new_unique_id
+
+ # There should be a config entry with the correct server unique_id.
+ entry = _get_config_entry_from_unique_id(hass, TEST_SYSINFO_ID)
+ assert entry
+ assert entry.options == MappingProxyType(TEST_CONFIG_ENTRY_OPTIONS)
+
+
+async def test_setup_yaml_no_registry_entity(hass: HomeAssistantType) -> None:
+ """Test an already converted YAML style config."""
+ # This tests "Possibility 3" from async_setup_platform()
+
+ registry = await async_get_registry(hass)
+
+ # Add a pre-existing config entry.
+ client = create_mock_client()
+ await _setup_entity_yaml(hass, client=client)
+
+ # The entity should have been created with the same entity_id.
+ assert hass.states.get(TEST_YAML_ENTITY_ID) is not None
+
+ # The unique_id should have been updated in the registry (rather than the one
+ # specified above).
+ assert registry.async_get(TEST_YAML_ENTITY_ID).unique_id == get_hyperion_unique_id(
+ TEST_SYSINFO_ID, 0, TYPE_HYPERION_LIGHT
+ )
+
+ # There should be a config entry with the correct server unique_id.
+ entry = _get_config_entry_from_unique_id(hass, TEST_SYSINFO_ID)
+ assert entry
+ assert entry.options == MappingProxyType(TEST_CONFIG_ENTRY_OPTIONS)
+
+
+async def test_setup_yaml_not_ready(hass: HomeAssistantType) -> None:
+ """Test the component not being ready."""
client = create_mock_client()
client.async_client_connect = AsyncMock(return_value=False)
-
- await setup_entity(hass, client=client)
- assert hass.states.get(TEST_ENTITY_ID) is None
+ await _setup_entity_yaml(hass, client=client)
+ assert hass.states.get(TEST_YAML_ENTITY_ID) is None
-async def test_light_basic_properies(hass):
+async def test_setup_config_entry(hass: HomeAssistantType) -> None:
+ """Test setting up the component via config entries."""
+ await setup_test_config_entry(hass, hyperion_client=create_mock_client())
+ assert hass.states.get(TEST_ENTITY_ID_1) is not None
+
+
+async def test_setup_config_entry_not_ready(hass: HomeAssistantType) -> None:
+ """Test the component not being ready."""
+ client = create_mock_client()
+ client.async_client_connect = AsyncMock(return_value=False)
+ await setup_test_config_entry(hass, hyperion_client=client)
+ assert hass.states.get(TEST_ENTITY_ID_1) is None
+
+
+async def test_setup_config_entry_dynamic_instances(hass: HomeAssistantType) -> None:
+ """Test dynamic changes in the omstamce configuration."""
+ config_entry = add_test_config_entry(hass)
+
+ master_client = create_mock_client()
+ master_client.instances = [TEST_INSTANCE_1, TEST_INSTANCE_2]
+
+ entity_client = create_mock_client()
+ entity_client.instances = master_client.instances
+
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient",
+ side_effect=[master_client, entity_client, entity_client],
+ ):
+ await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert hass.states.get(TEST_ENTITY_ID_1) is not None
+ assert hass.states.get(TEST_ENTITY_ID_2) is not None
+
+ # Inject a new instances update (remove instance 1, add instance 3)
+ assert master_client.set_callbacks.called
+ instance_callback = master_client.set_callbacks.call_args[0][0][
+ f"{const.KEY_INSTANCE}-{const.KEY_UPDATE}"
+ ]
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient",
+ return_value=entity_client,
+ ):
+ instance_callback(
+ {
+ const.KEY_SUCCESS: True,
+ const.KEY_DATA: [TEST_INSTANCE_2, TEST_INSTANCE_3],
+ }
+ )
+ await hass.async_block_till_done()
+
+ assert hass.states.get(TEST_ENTITY_ID_1) is None
+ assert hass.states.get(TEST_ENTITY_ID_2) is not None
+ assert hass.states.get(TEST_ENTITY_ID_3) is not None
+
+ # Inject a new instances update (re-add instance 1, but not running)
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient",
+ return_value=entity_client,
+ ):
+ instance_callback(
+ {
+ const.KEY_SUCCESS: True,
+ const.KEY_DATA: [
+ {**TEST_INSTANCE_1, "running": False},
+ TEST_INSTANCE_2,
+ TEST_INSTANCE_3,
+ ],
+ }
+ )
+ await hass.async_block_till_done()
+
+ assert hass.states.get(TEST_ENTITY_ID_1) is None
+ assert hass.states.get(TEST_ENTITY_ID_2) is not None
+ assert hass.states.get(TEST_ENTITY_ID_3) is not None
+
+ # Inject a new instances update (re-add instance 1, running)
+ with patch(
+ "homeassistant.components.hyperion.client.HyperionClient",
+ return_value=entity_client,
+ ):
+ instance_callback(
+ {
+ const.KEY_SUCCESS: True,
+ const.KEY_DATA: [TEST_INSTANCE_1, TEST_INSTANCE_2, TEST_INSTANCE_3],
+ }
+ )
+ await hass.async_block_till_done()
+
+ assert hass.states.get(TEST_ENTITY_ID_1) is not None
+ assert hass.states.get(TEST_ENTITY_ID_2) is not None
+ assert hass.states.get(TEST_ENTITY_ID_3) is not None
+
+
+async def test_light_basic_properies(hass: HomeAssistantType) -> None:
"""Test the basic properties."""
client = create_mock_client()
- await setup_entity(hass, client=client)
+ await setup_test_config_entry(hass, hyperion_client=client)
- entity_state = hass.states.get(TEST_ENTITY_ID)
+ entity_state = hass.states.get(TEST_ENTITY_ID_1)
+ assert entity_state
assert entity_state.state == "on"
assert entity_state.attributes["brightness"] == 255
assert entity_state.attributes["hs_color"] == (0.0, 0.0)
@@ -91,15 +315,15 @@ async def test_light_basic_properies(hass):
)
-async def test_light_async_turn_on(hass):
+async def test_light_async_turn_on(hass: HomeAssistantType) -> None:
"""Test turning the light on."""
client = create_mock_client()
- await setup_entity(hass, client=client)
+ await setup_test_config_entry(hass, hyperion_client=client)
# On (=), 100% (=), solid (=), [255,255,255] (=)
client.async_send_set_color = AsyncMock(return_value=True)
await hass.services.async_call(
- DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True
+ LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: TEST_ENTITY_ID_1}, blocking=True
)
assert client.async_send_set_color.call_args == call(
@@ -116,9 +340,9 @@ async def test_light_async_turn_on(hass):
client.async_send_set_color = AsyncMock(return_value=True)
client.async_send_set_adjustment = AsyncMock(return_value=True)
await hass.services.async_call(
- DOMAIN,
+ LIGHT_DOMAIN,
SERVICE_TURN_ON,
- {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_BRIGHTNESS: brightness},
+ {ATTR_ENTITY_ID: TEST_ENTITY_ID_1, ATTR_BRIGHTNESS: brightness},
blocking=True,
)
@@ -135,8 +359,9 @@ async def test_light_async_turn_on(hass):
# Simulate a state callback from Hyperion.
client.adjustment = [{const.KEY_BRIGHTNESS: 50}]
- call_registered_callback(client, "adjustment-update")
- entity_state = hass.states.get(TEST_ENTITY_ID)
+ _call_registered_callback(client, "adjustment-update")
+ entity_state = hass.states.get(TEST_ENTITY_ID_1)
+ assert entity_state
assert entity_state.state == "on"
assert entity_state.attributes["brightness"] == brightness
@@ -144,9 +369,9 @@ async def test_light_async_turn_on(hass):
hs_color = (180.0, 100.0)
client.async_send_set_color = AsyncMock(return_value=True)
await hass.services.async_call(
- DOMAIN,
+ LIGHT_DOMAIN,
SERVICE_TURN_ON,
- {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_HS_COLOR: hs_color},
+ {ATTR_ENTITY_ID: TEST_ENTITY_ID_1, ATTR_HS_COLOR: hs_color},
blocking=True,
)
@@ -164,8 +389,9 @@ async def test_light_async_turn_on(hass):
const.KEY_VALUE: {const.KEY_RGB: (0, 255, 255)},
}
- call_registered_callback(client, "priorities-update")
- entity_state = hass.states.get(TEST_ENTITY_ID)
+ _call_registered_callback(client, "priorities-update")
+ entity_state = hass.states.get(TEST_ENTITY_ID_1)
+ assert entity_state
assert entity_state.attributes["hs_color"] == hs_color
assert entity_state.attributes["icon"] == hyperion_light.ICON_LIGHTBULB
@@ -175,9 +401,9 @@ async def test_light_async_turn_on(hass):
client.async_send_set_adjustment = AsyncMock(return_value=True)
await hass.services.async_call(
- DOMAIN,
+ LIGHT_DOMAIN,
SERVICE_TURN_ON,
- {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_BRIGHTNESS: brightness},
+ {ATTR_ENTITY_ID: TEST_ENTITY_ID_1, ATTR_BRIGHTNESS: brightness},
blocking=True,
)
@@ -192,8 +418,9 @@ async def test_light_async_turn_on(hass):
}
)
client.adjustment = [{const.KEY_BRIGHTNESS: 100}]
- call_registered_callback(client, "adjustment-update")
- entity_state = hass.states.get(TEST_ENTITY_ID)
+ _call_registered_callback(client, "adjustment-update")
+ entity_state = hass.states.get(TEST_ENTITY_ID_1)
+ assert entity_state
assert entity_state.attributes["brightness"] == brightness
# On (=), 100% (=), V4L (!), [0,255,255] (=)
@@ -201,9 +428,9 @@ async def test_light_async_turn_on(hass):
client.async_send_clear = AsyncMock(return_value=True)
client.async_send_set_component = AsyncMock(return_value=True)
await hass.services.async_call(
- DOMAIN,
+ LIGHT_DOMAIN,
SERVICE_TURN_ON,
- {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_EFFECT: effect},
+ {ATTR_ENTITY_ID: TEST_ENTITY_ID_1, ATTR_EFFECT: effect},
blocking=True,
)
@@ -237,8 +464,9 @@ async def test_light_async_turn_on(hass):
),
]
client.visible_priority = {const.KEY_COMPONENTID: effect}
- call_registered_callback(client, "priorities-update")
- entity_state = hass.states.get(TEST_ENTITY_ID)
+ _call_registered_callback(client, "priorities-update")
+ entity_state = hass.states.get(TEST_ENTITY_ID_1)
+ assert entity_state
assert entity_state.attributes["icon"] == hyperion_light.ICON_EXTERNAL_SOURCE
assert entity_state.attributes["effect"] == effect
@@ -248,9 +476,9 @@ async def test_light_async_turn_on(hass):
client.async_send_set_effect = AsyncMock(return_value=True)
await hass.services.async_call(
- DOMAIN,
+ LIGHT_DOMAIN,
SERVICE_TURN_ON,
- {ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_EFFECT: effect},
+ {ATTR_ENTITY_ID: TEST_ENTITY_ID_1, ATTR_EFFECT: effect},
blocking=True,
)
@@ -268,33 +496,67 @@ async def test_light_async_turn_on(hass):
const.KEY_COMPONENTID: const.KEY_COMPONENTID_EFFECT,
const.KEY_OWNER: effect,
}
- call_registered_callback(client, "priorities-update")
- entity_state = hass.states.get(TEST_ENTITY_ID)
+ _call_registered_callback(client, "priorities-update")
+ entity_state = hass.states.get(TEST_ENTITY_ID_1)
+ assert entity_state
assert entity_state.attributes["icon"] == hyperion_light.ICON_EFFECT
assert entity_state.attributes["effect"] == effect
+ # On (=), 100% (=), [0,0,255] (!)
+ # Ensure changing the color will move the effect to 'Solid' automatically.
+ hs_color = (240.0, 100.0)
+ client.async_send_set_color = AsyncMock(return_value=True)
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: TEST_ENTITY_ID_1, ATTR_HS_COLOR: hs_color},
+ blocking=True,
+ )
+
+ assert client.async_send_set_color.call_args == call(
+ **{
+ const.KEY_PRIORITY: TEST_PRIORITY,
+ const.KEY_COLOR: (0, 0, 255),
+ const.KEY_ORIGIN: hyperion_light.DEFAULT_ORIGIN,
+ }
+ )
+ # Simulate a state callback from Hyperion.
+ client.visible_priority = {
+ const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR,
+ const.KEY_VALUE: {const.KEY_RGB: (0, 0, 255)},
+ }
+ _call_registered_callback(client, "priorities-update")
+ entity_state = hass.states.get(TEST_ENTITY_ID_1)
+ assert entity_state
+ assert entity_state.attributes["hs_color"] == hs_color
+ assert entity_state.attributes["icon"] == hyperion_light.ICON_LIGHTBULB
+ assert entity_state.attributes["effect"] == hyperion_light.KEY_EFFECT_SOLID
+
# No calls if disconnected.
client.has_loaded_state = False
- call_registered_callback(client, "client-update", {"loaded-state": False})
+ _call_registered_callback(client, "client-update", {"loaded-state": False})
client.async_send_clear = AsyncMock(return_value=True)
client.async_send_set_effect = AsyncMock(return_value=True)
await hass.services.async_call(
- DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True
+ LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: TEST_ENTITY_ID_1}, blocking=True
)
assert not client.async_send_clear.called
assert not client.async_send_set_effect.called
-async def test_light_async_turn_off(hass):
+async def test_light_async_turn_off(hass: HomeAssistantType) -> None:
"""Test turning the light off."""
client = create_mock_client()
- await setup_entity(hass, client=client)
+ await setup_test_config_entry(hass, hyperion_client=client)
client.async_send_set_component = AsyncMock(return_value=True)
await hass.services.async_call(
- DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True
+ LIGHT_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: TEST_ENTITY_ID_1},
+ blocking=True,
)
assert client.async_send_set_component.call_args == call(
@@ -309,50 +571,60 @@ async def test_light_async_turn_off(hass):
# No calls if no state loaded.
client.has_loaded_state = False
client.async_send_set_component = AsyncMock(return_value=True)
- call_registered_callback(client, "client-update", {"loaded-state": False})
+ _call_registered_callback(client, "client-update", {"loaded-state": False})
await hass.services.async_call(
- DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True
+ LIGHT_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: TEST_ENTITY_ID_1},
+ blocking=True,
)
assert not client.async_send_set_component.called
-async def test_light_async_updates_from_hyperion_client(hass):
+async def test_light_async_updates_from_hyperion_client(
+ hass: HomeAssistantType,
+) -> None:
"""Test receiving a variety of Hyperion client callbacks."""
client = create_mock_client()
- await setup_entity(hass, client=client)
+ await setup_test_config_entry(hass, hyperion_client=client)
# Bright change gets accepted.
brightness = 10
client.adjustment = [{const.KEY_BRIGHTNESS: brightness}]
- call_registered_callback(client, "adjustment-update")
- entity_state = hass.states.get(TEST_ENTITY_ID)
+ _call_registered_callback(client, "adjustment-update")
+ entity_state = hass.states.get(TEST_ENTITY_ID_1)
+ assert entity_state
assert entity_state.attributes["brightness"] == round(255 * (brightness / 100.0))
# Broken brightness value is ignored.
bad_brightness = -200
client.adjustment = [{const.KEY_BRIGHTNESS: bad_brightness}]
- call_registered_callback(client, "adjustment-update")
- entity_state = hass.states.get(TEST_ENTITY_ID)
+ _call_registered_callback(client, "adjustment-update")
+ entity_state = hass.states.get(TEST_ENTITY_ID_1)
+ assert entity_state
assert entity_state.attributes["brightness"] == round(255 * (brightness / 100.0))
# Update components.
client.is_on.return_value = True
- call_registered_callback(client, "components-update")
- entity_state = hass.states.get(TEST_ENTITY_ID)
+ _call_registered_callback(client, "components-update")
+ entity_state = hass.states.get(TEST_ENTITY_ID_1)
+ assert entity_state
assert entity_state.state == "on"
client.is_on.return_value = False
- call_registered_callback(client, "components-update")
- entity_state = hass.states.get(TEST_ENTITY_ID)
+ _call_registered_callback(client, "components-update")
+ entity_state = hass.states.get(TEST_ENTITY_ID_1)
+ assert entity_state
assert entity_state.state == "off"
# Update priorities (V4L)
client.is_on.return_value = True
client.visible_priority = {const.KEY_COMPONENTID: const.KEY_COMPONENTID_V4L}
- call_registered_callback(client, "priorities-update")
- entity_state = hass.states.get(TEST_ENTITY_ID)
+ _call_registered_callback(client, "priorities-update")
+ entity_state = hass.states.get(TEST_ENTITY_ID_1)
+ assert entity_state
assert entity_state.attributes["icon"] == hyperion_light.ICON_EXTERNAL_SOURCE
assert entity_state.attributes["hs_color"] == (0.0, 0.0)
assert entity_state.attributes["effect"] == const.KEY_COMPONENTID_V4L
@@ -364,8 +636,9 @@ async def test_light_async_updates_from_hyperion_client(hass):
const.KEY_OWNER: effect,
}
- call_registered_callback(client, "priorities-update")
- entity_state = hass.states.get(TEST_ENTITY_ID)
+ _call_registered_callback(client, "priorities-update")
+ entity_state = hass.states.get(TEST_ENTITY_ID_1)
+ assert entity_state
assert entity_state.attributes["effect"] == effect
assert entity_state.attributes["icon"] == hyperion_light.ICON_EFFECT
assert entity_state.attributes["hs_color"] == (0.0, 0.0)
@@ -377,17 +650,27 @@ async def test_light_async_updates_from_hyperion_client(hass):
const.KEY_VALUE: {const.KEY_RGB: rgb},
}
- call_registered_callback(client, "priorities-update")
- entity_state = hass.states.get(TEST_ENTITY_ID)
+ _call_registered_callback(client, "priorities-update")
+ entity_state = hass.states.get(TEST_ENTITY_ID_1)
+ assert entity_state
assert entity_state.attributes["effect"] == hyperion_light.KEY_EFFECT_SOLID
assert entity_state.attributes["icon"] == hyperion_light.ICON_LIGHTBULB
assert entity_state.attributes["hs_color"] == (180.0, 100.0)
+ # Update priorities (None)
+ client.visible_priority = None
+
+ _call_registered_callback(client, "priorities-update")
+ entity_state = hass.states.get(TEST_ENTITY_ID_1)
+ assert entity_state
+ assert entity_state.state == "off"
+
# Update effect list
effects = [{const.KEY_NAME: "One"}, {const.KEY_NAME: "Two"}]
client.effects = effects
- call_registered_callback(client, "effects-update")
- entity_state = hass.states.get(TEST_ENTITY_ID)
+ _call_registered_callback(client, "effects-update")
+ entity_state = hass.states.get(TEST_ENTITY_ID_1)
+ assert entity_state
assert entity_state.attributes["effect_list"] == [
effect[const.KEY_NAME] for effect in effects
] + const.KEY_COMPONENTID_EXTERNAL_SOURCES + [hyperion_light.KEY_EFFECT_SOLID]
@@ -396,18 +679,24 @@ async def test_light_async_updates_from_hyperion_client(hass):
# Turn on late, check state, disconnect, ensure it cannot be turned off.
client.has_loaded_state = False
- call_registered_callback(client, "client-update", {"loaded-state": False})
- entity_state = hass.states.get(TEST_ENTITY_ID)
+ _call_registered_callback(client, "client-update", {"loaded-state": False})
+ entity_state = hass.states.get(TEST_ENTITY_ID_1)
+ assert entity_state
assert entity_state.state == "unavailable"
# Update connection status (e.g. re-connection)
client.has_loaded_state = True
- call_registered_callback(client, "client-update", {"loaded-state": True})
- entity_state = hass.states.get(TEST_ENTITY_ID)
+ client.visible_priority = {
+ const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR,
+ const.KEY_VALUE: {const.KEY_RGB: rgb},
+ }
+ _call_registered_callback(client, "client-update", {"loaded-state": True})
+ entity_state = hass.states.get(TEST_ENTITY_ID_1)
+ assert entity_state
assert entity_state.state == "on"
-async def test_full_state_loaded_on_start(hass):
+async def test_full_state_loaded_on_start(hass: HomeAssistantType) -> None:
"""Test receiving a variety of Hyperion client callbacks."""
client = create_mock_client()
@@ -420,11 +709,43 @@ async def test_full_state_loaded_on_start(hass):
}
client.effects = [{const.KEY_NAME: "One"}, {const.KEY_NAME: "Two"}]
- await setup_entity(hass, client=client)
-
- entity_state = hass.states.get(TEST_ENTITY_ID)
+ await setup_test_config_entry(hass, hyperion_client=client)
+ entity_state = hass.states.get(TEST_ENTITY_ID_1)
+ assert entity_state
assert entity_state.attributes["brightness"] == round(255 * (brightness / 100.0))
assert entity_state.attributes["effect"] == hyperion_light.KEY_EFFECT_SOLID
assert entity_state.attributes["icon"] == hyperion_light.ICON_LIGHTBULB
assert entity_state.attributes["hs_color"] == (180.0, 100.0)
+
+
+async def test_unload_entry(hass: HomeAssistantType) -> None:
+ """Test unload."""
+ client = create_mock_client()
+ await setup_test_config_entry(hass, hyperion_client=client)
+ assert hass.states.get(TEST_ENTITY_ID_1) is not None
+ assert client.async_client_connect.called
+ assert not client.async_client_disconnect.called
+ entry = _get_config_entry_from_unique_id(hass, TEST_SYSINFO_ID)
+ assert entry
+
+ await hass.config_entries.async_unload(entry.entry_id)
+ assert client.async_client_disconnect.call_count == 2
+
+
+async def test_version_log_warning(caplog, hass: HomeAssistantType) -> None: # type: ignore[no-untyped-def]
+ """Test warning on old version."""
+ client = create_mock_client()
+ client.async_sysinfo_version = AsyncMock(return_value="2.0.0-alpha.7")
+ await setup_test_config_entry(hass, hyperion_client=client)
+ assert hass.states.get(TEST_ENTITY_ID_1) is not None
+ assert "Please consider upgrading" in caplog.text
+
+
+async def test_version_no_log_warning(caplog, hass: HomeAssistantType) -> None: # type: ignore[no-untyped-def]
+ """Test no warning on acceptable version."""
+ client = create_mock_client()
+ client.async_sysinfo_version = AsyncMock(return_value="2.0.0-alpha.9")
+ await setup_test_config_entry(hass, hyperion_client=client)
+ assert hass.states.get(TEST_ENTITY_ID_1) is not None
+ assert "Please consider upgrading" not in caplog.text
diff --git a/tests/components/input_boolean/test_init.py b/tests/components/input_boolean/test_init.py
index 1e3150e687a..9911ced28dd 100644
--- a/tests/components/input_boolean/test_init.py
+++ b/tests/components/input_boolean/test_init.py
@@ -63,23 +63,21 @@ async def test_methods(hass):
assert not is_on(hass, entity_id)
- await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id})
-
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True
+ )
assert is_on(hass, entity_id)
await hass.services.async_call(
- DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}
+ DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True
)
- await hass.async_block_till_done()
-
assert not is_on(hass, entity_id)
- await hass.services.async_call(DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: entity_id})
-
- await hass.async_block_till_done()
+ await hass.services.async_call(
+ DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: entity_id}, blocking=True
+ )
assert is_on(hass, entity_id)
@@ -246,7 +244,6 @@ async def test_reload(hass, hass_admin_user):
blocking=True,
context=Context(user_id=hass_admin_user.id),
)
- await hass.async_block_till_done()
assert count_start + 2 == len(hass.states.async_entity_ids())
@@ -349,6 +346,5 @@ async def test_setup_no_config(hass, hass_admin_user):
blocking=True,
context=Context(user_id=hass_admin_user.id),
)
- await hass.async_block_till_done()
assert count_start == len(hass.states.async_entity_ids())
diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py
index 70f0b69d3ef..d40a88e3f43 100644
--- a/tests/components/input_datetime/test_init.py
+++ b/tests/components/input_datetime/test_init.py
@@ -16,10 +16,13 @@ from homeassistant.components.input_datetime import (
CONF_ID,
CONF_INITIAL,
CONF_NAME,
+ CONFIG_SCHEMA,
DEFAULT_TIME,
DOMAIN,
+ FMT_DATE,
+ FMT_DATETIME,
+ FMT_TIME,
SERVICE_RELOAD,
- SERVICE_SET_DATETIME,
)
from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_NAME
from homeassistant.core import Context, CoreState, State
@@ -35,6 +38,8 @@ INITIAL_DATE = "2020-01-10"
INITIAL_TIME = "23:45:56"
INITIAL_DATETIME = f"{INITIAL_DATE} {INITIAL_TIME}"
+ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE
+
@pytest.fixture
def storage_setup(hass, hass_storage):
@@ -74,7 +79,7 @@ async def async_set_date_and_time(hass, entity_id, dt_value):
"""Set date and / or time of input_datetime."""
await hass.services.async_call(
DOMAIN,
- SERVICE_SET_DATETIME,
+ "set_datetime",
{
ATTR_ENTITY_ID: entity_id,
ATTR_DATE: dt_value.date(),
@@ -88,7 +93,7 @@ async def async_set_datetime(hass, entity_id, dt_value):
"""Set date and / or time of input_datetime."""
await hass.services.async_call(
DOMAIN,
- SERVICE_SET_DATETIME,
+ "set_datetime",
{ATTR_ENTITY_ID: entity_id, ATTR_DATETIME: dt_value},
blocking=True,
)
@@ -98,22 +103,24 @@ async def async_set_timestamp(hass, entity_id, timestamp):
"""Set date and / or time of input_datetime."""
await hass.services.async_call(
DOMAIN,
- SERVICE_SET_DATETIME,
+ "set_datetime",
{ATTR_ENTITY_ID: entity_id, ATTR_TIMESTAMP: timestamp},
blocking=True,
)
-async def test_invalid_configs(hass):
- """Test config."""
- invalid_configs = [
+@pytest.mark.parametrize(
+ "config",
+ [
None,
- {},
{"name with space": None},
{"test_no_value": {"has_time": False, "has_date": False}},
- ]
- for cfg in invalid_configs:
- assert not await async_setup_component(hass, DOMAIN, {DOMAIN: cfg})
+ ],
+)
+def test_invalid_configs(config):
+ """Test config."""
+ with pytest.raises(vol.Invalid):
+ CONFIG_SCHEMA({DOMAIN: config})
async def test_set_datetime(hass):
@@ -129,7 +136,7 @@ async def test_set_datetime(hass):
await async_set_date_and_time(hass, entity_id, dt_obj)
state = hass.states.get(entity_id)
- assert state.state == str(dt_obj)
+ assert state.state == dt_obj.strftime(FMT_DATETIME)
assert state.attributes["has_time"]
assert state.attributes["has_date"]
@@ -155,7 +162,7 @@ async def test_set_datetime_2(hass):
await async_set_datetime(hass, entity_id, dt_obj)
state = hass.states.get(entity_id)
- assert state.state == str(dt_obj)
+ assert state.state == dt_obj.strftime(FMT_DATETIME)
assert state.attributes["has_time"]
assert state.attributes["has_date"]
@@ -181,7 +188,7 @@ async def test_set_datetime_3(hass):
await async_set_timestamp(hass, entity_id, dt_util.as_utc(dt_obj).timestamp())
state = hass.states.get(entity_id)
- assert state.state == str(dt_obj)
+ assert state.state == dt_obj.strftime(FMT_DATETIME)
assert state.attributes["has_time"]
assert state.attributes["has_date"]
@@ -203,12 +210,11 @@ async def test_set_datetime_time(hass):
entity_id = "input_datetime.test_time"
dt_obj = datetime.datetime(2017, 9, 7, 19, 46, 30)
- time_portion = dt_obj.time()
await async_set_date_and_time(hass, entity_id, dt_obj)
state = hass.states.get(entity_id)
- assert state.state == str(time_portion)
+ assert state.state == dt_obj.strftime(FMT_TIME)
assert state.attributes["has_time"]
assert not state.attributes["has_date"]
@@ -240,7 +246,6 @@ async def test_set_invalid(hass):
{"entity_id": entity_id, "time": time_portion},
blocking=True,
)
- await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.state == initial
@@ -271,7 +276,6 @@ async def test_set_invalid_2(hass):
{"entity_id": entity_id, "time": time_portion, "datetime": dt_obj},
blocking=True,
)
- await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.state == initial
@@ -329,7 +333,7 @@ async def test_restore_state(hass):
"test_bogus_data": {
"has_time": True,
"has_date": True,
- "initial": str(initial),
+ "initial": initial.strftime(FMT_DATETIME),
},
"test_was_time": {"has_time": False, "has_date": True},
"test_was_date": {"has_time": True, "has_date": False},
@@ -339,22 +343,22 @@ async def test_restore_state(hass):
dt_obj = datetime.datetime(2017, 9, 7, 19, 46)
state_time = hass.states.get("input_datetime.test_time")
- assert state_time.state == str(dt_obj.time())
+ assert state_time.state == dt_obj.strftime(FMT_TIME)
state_date = hass.states.get("input_datetime.test_date")
- assert state_date.state == str(dt_obj.date())
+ assert state_date.state == dt_obj.strftime(FMT_DATE)
state_datetime = hass.states.get("input_datetime.test_datetime")
- assert state_datetime.state == str(dt_obj)
+ assert state_datetime.state == dt_obj.strftime(FMT_DATETIME)
state_bogus = hass.states.get("input_datetime.test_bogus_data")
- assert state_bogus.state == str(initial)
+ assert state_bogus.state == initial.strftime(FMT_DATETIME)
state_was_time = hass.states.get("input_datetime.test_was_time")
- assert state_was_time.state == str(default.date())
+ assert state_was_time.state == default.strftime(FMT_DATE)
state_was_date = hass.states.get("input_datetime.test_was_date")
- assert state_was_date.state == str(default.time())
+ assert state_was_date.state == default.strftime(FMT_TIME)
async def test_default_value(hass):
@@ -373,15 +377,15 @@ async def test_default_value(hass):
dt_obj = datetime.datetime(1970, 1, 1, 0, 0)
state_time = hass.states.get("input_datetime.test_time")
- assert state_time.state == str(dt_obj.time())
+ assert state_time.state == dt_obj.strftime(FMT_TIME)
assert state_time.attributes.get("timestamp") is not None
state_date = hass.states.get("input_datetime.test_date")
- assert state_date.state == str(dt_obj.date())
+ assert state_date.state == dt_obj.strftime(FMT_DATE)
assert state_date.attributes.get("timestamp") is not None
state_datetime = hass.states.get("input_datetime.test_datetime")
- assert state_datetime.state == str(dt_obj)
+ assert state_datetime.state == dt_obj.strftime(FMT_DATETIME)
assert state_datetime.attributes.get("timestamp") is not None
@@ -434,7 +438,7 @@ async def test_reload(hass, hass_admin_user, hass_read_only_user):
assert state_1 is not None
assert state_2 is None
assert state_3 is not None
- assert str(dt_obj.date()) == state_1.state
+ assert dt_obj.strftime(FMT_DATE) == state_1.state
assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "dt1") == f"{DOMAIN}.dt1"
assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "dt2") is None
assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "dt3") == f"{DOMAIN}.dt3"
@@ -462,7 +466,6 @@ async def test_reload(hass, hass_admin_user, hass_read_only_user):
blocking=True,
context=Context(user_id=hass_admin_user.id),
)
- await hass.async_block_till_done()
assert count_start + 2 == len(hass.states.async_entity_ids())
@@ -473,8 +476,8 @@ async def test_reload(hass, hass_admin_user, hass_read_only_user):
assert state_1 is not None
assert state_2 is not None
assert state_3 is None
- assert str(DEFAULT_TIME) == state_1.state
- assert str(datetime.datetime(1970, 1, 1, 0, 0)) == state_2.state
+ assert state_1.state == DEFAULT_TIME.strftime(FMT_TIME)
+ assert state_2.state == datetime.datetime(1970, 1, 1, 0, 0).strftime(FMT_DATETIME)
assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "dt1") == f"{DOMAIN}.dt1"
assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "dt2") == f"{DOMAIN}.dt2"
@@ -638,6 +641,68 @@ async def test_setup_no_config(hass, hass_admin_user):
blocking=True,
context=Context(user_id=hass_admin_user.id),
)
- await hass.async_block_till_done()
assert count_start == len(hass.states.async_entity_ids())
+
+
+async def test_timestamp(hass):
+ """Test timestamp."""
+ try:
+ dt_util.set_default_time_zone(dt_util.get_time_zone("America/Los_Angeles"))
+
+ assert await async_setup_component(
+ hass,
+ DOMAIN,
+ {
+ DOMAIN: {
+ "test_datetime_initial_with_tz": {
+ "has_time": True,
+ "has_date": True,
+ "initial": "2020-12-13 10:00:00+01:00",
+ },
+ "test_datetime_initial_without_tz": {
+ "has_time": True,
+ "has_date": True,
+ "initial": "2020-12-13 10:00:00",
+ },
+ "test_time_initial": {
+ "has_time": True,
+ "has_date": False,
+ "initial": "10:00:00",
+ },
+ }
+ },
+ )
+
+ # initial has been converted to the set timezone
+ state_with_tz = hass.states.get("input_datetime.test_datetime_initial_with_tz")
+ assert state_with_tz is not None
+ assert state_with_tz.state == "2020-12-13 01:00:00"
+ assert (
+ dt_util.as_local(
+ dt_util.utc_from_timestamp(state_with_tz.attributes[ATTR_TIMESTAMP])
+ ).strftime(FMT_DATETIME)
+ == "2020-12-13 01:00:00"
+ )
+
+ # initial has been interpreted as being part of set timezone
+ state_without_tz = hass.states.get(
+ "input_datetime.test_datetime_initial_without_tz"
+ )
+ assert state_without_tz is not None
+ assert state_without_tz.state == "2020-12-13 10:00:00"
+ assert (
+ dt_util.as_local(
+ dt_util.utc_from_timestamp(state_without_tz.attributes[ATTR_TIMESTAMP])
+ ).strftime(FMT_DATETIME)
+ == "2020-12-13 10:00:00"
+ )
+
+ # Test initial time sets timestamp correctly.
+ state_time = hass.states.get("input_datetime.test_time_initial")
+ assert state_time is not None
+ assert state_time.state == "10:00:00"
+ assert state_time.attributes[ATTR_TIMESTAMP] == 10 * 60 * 60
+
+ finally:
+ dt_util.set_default_time_zone(ORIG_TIMEZONE)
diff --git a/tests/components/input_datetime/test_reproduce_state.py b/tests/components/input_datetime/test_reproduce_state.py
index 87d737dfb4f..f2d9dd4d445 100644
--- a/tests/components/input_datetime/test_reproduce_state.py
+++ b/tests/components/input_datetime/test_reproduce_state.py
@@ -19,6 +19,11 @@ async def test_reproducing_states(hass, caplog):
"2010-10-10",
{"has_date": True, "has_time": False},
)
+ hass.states.async_set(
+ "input_datetime.invalid_data",
+ "unavailable",
+ {"has_date": False, "has_time": False},
+ )
datetime_calls = async_mock_service(hass, "input_datetime", "set_datetime")
@@ -57,6 +62,7 @@ async def test_reproducing_states(hass, caplog):
State("input_datetime.entity_date", "2011-10-10"),
# Should not raise
State("input_datetime.non_existing", "2010-10-10 01:20:00"),
+ State("input_datetime.invalid_data", "2010-10-10 01:20:00"),
],
)
diff --git a/tests/components/ipma/test_system_health.py b/tests/components/ipma/test_system_health.py
new file mode 100644
index 00000000000..301621514f9
--- /dev/null
+++ b/tests/components/ipma/test_system_health.py
@@ -0,0 +1,23 @@
+"""Test ipma system health."""
+import asyncio
+
+from homeassistant.components.ipma.system_health import IPMA_API_URL
+from homeassistant.setup import async_setup_component
+
+from tests.common import get_system_health_info
+
+
+async def test_ipma_system_health(hass, aioclient_mock):
+ """Test ipma system health."""
+ aioclient_mock.get(IPMA_API_URL, json={"result": "ok", "data": {}})
+
+ hass.config.components.add("ipma")
+ assert await async_setup_component(hass, "system_health", {})
+
+ info = await get_system_health_info(hass, "ipma")
+
+ for key, val in info.items():
+ if asyncio.iscoroutine(val):
+ info[key] = await val
+
+ assert info == {"api_endpoint_reachable": "ok"}
diff --git a/tests/components/kodi/test_device_trigger.py b/tests/components/kodi/test_device_trigger.py
index de2d69212ef..8cf6c635393 100644
--- a/tests/components/kodi/test_device_trigger.py
+++ b/tests/components/kodi/test_device_trigger.py
@@ -16,6 +16,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/kulersky/__init__.py b/tests/components/kulersky/__init__.py
new file mode 100644
index 00000000000..2b723b28fbd
--- /dev/null
+++ b/tests/components/kulersky/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Kuler Sky integration."""
diff --git a/tests/components/kulersky/test_config_flow.py b/tests/components/kulersky/test_config_flow.py
new file mode 100644
index 00000000000..59e3188fd7e
--- /dev/null
+++ b/tests/components/kulersky/test_config_flow.py
@@ -0,0 +1,104 @@
+"""Test the Kuler Sky config flow."""
+import pykulersky
+
+from homeassistant import config_entries, setup
+from homeassistant.components.kulersky.config_flow import DOMAIN
+
+from tests.async_mock import patch
+
+
+async def test_flow_success(hass):
+ """Test we get the form."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == "form"
+ assert result["errors"] is None
+
+ with patch(
+ "homeassistant.components.kulersky.config_flow.pykulersky.discover_bluetooth_devices",
+ return_value=[
+ {
+ "address": "AA:BB:CC:11:22:33",
+ "name": "Bedroom",
+ }
+ ],
+ ), patch(
+ "homeassistant.components.kulersky.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.kulersky.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {},
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == "create_entry"
+ assert result2["title"] == "Kuler Sky"
+ assert result2["data"] == {}
+
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_flow_no_devices_found(hass):
+ """Test we get the form."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == "form"
+ assert result["errors"] is None
+
+ with patch(
+ "homeassistant.components.kulersky.config_flow.pykulersky.discover_bluetooth_devices",
+ return_value=[],
+ ), patch(
+ "homeassistant.components.kulersky.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.kulersky.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {},
+ )
+
+ assert result2["type"] == "abort"
+ assert result2["reason"] == "no_devices_found"
+ await hass.async_block_till_done()
+ assert len(mock_setup.mock_calls) == 0
+ assert len(mock_setup_entry.mock_calls) == 0
+
+
+async def test_flow_exceptions_caught(hass):
+ """Test we get the form."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+ assert result["type"] == "form"
+ assert result["errors"] is None
+
+ with patch(
+ "homeassistant.components.kulersky.config_flow.pykulersky.discover_bluetooth_devices",
+ side_effect=pykulersky.PykulerskyException("TEST"),
+ ), patch(
+ "homeassistant.components.kulersky.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.kulersky.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {},
+ )
+
+ assert result2["type"] == "abort"
+ assert result2["reason"] == "no_devices_found"
+ await hass.async_block_till_done()
+ assert len(mock_setup.mock_calls) == 0
+ assert len(mock_setup_entry.mock_calls) == 0
diff --git a/tests/components/kulersky/test_light.py b/tests/components/kulersky/test_light.py
new file mode 100644
index 00000000000..5403f7cedde
--- /dev/null
+++ b/tests/components/kulersky/test_light.py
@@ -0,0 +1,315 @@
+"""Test the Kuler Sky lights."""
+import asyncio
+
+import pykulersky
+import pytest
+
+from homeassistant import setup
+from homeassistant.components.kulersky.light import DOMAIN
+from homeassistant.components.light import (
+ ATTR_BRIGHTNESS,
+ ATTR_HS_COLOR,
+ ATTR_RGB_COLOR,
+ ATTR_WHITE_VALUE,
+ ATTR_XY_COLOR,
+ SCAN_INTERVAL,
+ SUPPORT_BRIGHTNESS,
+ SUPPORT_COLOR,
+ SUPPORT_WHITE_VALUE,
+)
+from homeassistant.const import (
+ ATTR_ENTITY_ID,
+ ATTR_FRIENDLY_NAME,
+ ATTR_SUPPORTED_FEATURES,
+ STATE_OFF,
+ STATE_ON,
+ STATE_UNAVAILABLE,
+)
+import homeassistant.util.dt as dt_util
+
+from tests.async_mock import MagicMock, patch
+from tests.common import MockConfigEntry, async_fire_time_changed
+
+
+@pytest.fixture
+async def mock_entry(hass):
+ """Create a mock light entity."""
+ return MockConfigEntry(domain=DOMAIN)
+
+
+@pytest.fixture
+async def mock_light(hass, mock_entry):
+ """Create a mock light entity."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ light = MagicMock(spec=pykulersky.Light)
+ light.address = "AA:BB:CC:11:22:33"
+ light.name = "Bedroom"
+ light.connected = False
+ with patch(
+ "homeassistant.components.kulersky.light.pykulersky.discover_bluetooth_devices",
+ return_value=[
+ {
+ "address": "AA:BB:CC:11:22:33",
+ "name": "Bedroom",
+ }
+ ],
+ ):
+ with patch(
+ "homeassistant.components.kulersky.light.pykulersky.Light",
+ return_value=light,
+ ), patch.object(light, "connect") as mock_connect, patch.object(
+ light, "get_color", return_value=(0, 0, 0, 0)
+ ):
+ mock_entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(mock_entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert mock_connect.called
+ light.connected = True
+
+ yield light
+
+
+async def test_init(hass, mock_light):
+ """Test platform setup."""
+ state = hass.states.get("light.bedroom")
+ assert state.state == STATE_OFF
+ assert state.attributes == {
+ ATTR_FRIENDLY_NAME: "Bedroom",
+ ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS
+ | SUPPORT_COLOR
+ | SUPPORT_WHITE_VALUE,
+ }
+
+ with patch.object(hass.loop, "stop"), patch.object(
+ mock_light, "disconnect"
+ ) as mock_disconnect:
+ await hass.async_stop()
+ await hass.async_block_till_done()
+
+ assert mock_disconnect.called
+
+
+async def test_discovery_lock(hass, mock_entry):
+ """Test discovery lock."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ discovery_finished = None
+ first_discovery_started = asyncio.Event()
+
+ async def mock_discovery(*args):
+ """Block to simulate multiple discovery calls while one still running."""
+ nonlocal discovery_finished
+ if discovery_finished:
+ first_discovery_started.set()
+ await discovery_finished.wait()
+ return []
+
+ with patch(
+ "homeassistant.components.kulersky.light.pykulersky.discover_bluetooth_devices",
+ return_value=[],
+ ), patch(
+ "homeassistant.components.kulersky.light.async_track_time_interval",
+ ) as mock_track_time_interval:
+ mock_entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(mock_entry.entry_id)
+ await hass.async_block_till_done()
+
+ with patch.object(
+ hass, "async_add_executor_job", side_effect=mock_discovery
+ ) as mock_run_discovery:
+ discovery_coroutine = mock_track_time_interval.call_args[0][1]
+
+ discovery_finished = asyncio.Event()
+
+ # Schedule multiple discoveries
+ hass.async_create_task(discovery_coroutine())
+ hass.async_create_task(discovery_coroutine())
+ hass.async_create_task(discovery_coroutine())
+
+ # Wait until the first discovery call is blocked
+ await first_discovery_started.wait()
+
+ # Unblock the first discovery
+ discovery_finished.set()
+
+ # Flush the remaining jobs
+ await hass.async_block_till_done()
+
+ # The discovery method should only have been called once
+ mock_run_discovery.assert_called_once()
+
+
+async def test_discovery_connection_error(hass, mock_entry):
+ """Test that invalid devices are skipped."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ light = MagicMock(spec=pykulersky.Light)
+ light.address = "AA:BB:CC:11:22:33"
+ light.name = "Bedroom"
+ light.connected = False
+ with patch(
+ "homeassistant.components.kulersky.light.pykulersky.discover_bluetooth_devices",
+ return_value=[
+ {
+ "address": "AA:BB:CC:11:22:33",
+ "name": "Bedroom",
+ }
+ ],
+ ):
+ with patch(
+ "homeassistant.components.kulersky.light.pykulersky.Light"
+ ) as mockdevice, patch.object(
+ light, "connect", side_effect=pykulersky.PykulerskyException
+ ):
+ mockdevice.return_value = light
+ mock_entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(mock_entry.entry_id)
+ await hass.async_block_till_done()
+
+ # Assert entity was not added
+ state = hass.states.get("light.bedroom")
+ assert state is None
+
+
+async def test_remove_entry(hass, mock_light, mock_entry):
+ """Test platform setup."""
+ with patch.object(mock_light, "disconnect") as mock_disconnect:
+ await hass.config_entries.async_remove(mock_entry.entry_id)
+
+ assert mock_disconnect.called
+
+
+async def test_update_exception(hass, mock_light):
+ """Test platform setup."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ with patch.object(
+ mock_light, "get_color", side_effect=pykulersky.PykulerskyException
+ ):
+ await hass.helpers.entity_component.async_update_entity("light.bedroom")
+ state = hass.states.get("light.bedroom")
+ assert state is not None
+ assert state.state == STATE_UNAVAILABLE
+
+
+async def test_light_turn_on(hass, mock_light):
+ """Test KulerSkyLight turn_on."""
+ with patch.object(mock_light, "set_color") as mock_set_color, patch.object(
+ mock_light, "get_color", return_value=(255, 255, 255, 255)
+ ):
+ await hass.services.async_call(
+ "light",
+ "turn_on",
+ {ATTR_ENTITY_ID: "light.bedroom"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ mock_set_color.assert_called_with(255, 255, 255, 255)
+
+ with patch.object(mock_light, "set_color") as mock_set_color, patch.object(
+ mock_light, "get_color", return_value=(50, 50, 50, 255)
+ ):
+ await hass.services.async_call(
+ "light",
+ "turn_on",
+ {ATTR_ENTITY_ID: "light.bedroom", ATTR_BRIGHTNESS: 50},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ mock_set_color.assert_called_with(50, 50, 50, 255)
+
+ with patch.object(mock_light, "set_color") as mock_set_color, patch.object(
+ mock_light, "get_color", return_value=(50, 45, 25, 255)
+ ):
+ await hass.services.async_call(
+ "light",
+ "turn_on",
+ {ATTR_ENTITY_ID: "light.bedroom", ATTR_HS_COLOR: (50, 50)},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ mock_set_color.assert_called_with(50, 45, 25, 255)
+
+ with patch.object(mock_light, "set_color") as mock_set_color, patch.object(
+ mock_light, "get_color", return_value=(220, 201, 110, 180)
+ ):
+ await hass.services.async_call(
+ "light",
+ "turn_on",
+ {ATTR_ENTITY_ID: "light.bedroom", ATTR_WHITE_VALUE: 180},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ mock_set_color.assert_called_with(50, 45, 25, 180)
+
+
+async def test_light_turn_off(hass, mock_light):
+ """Test KulerSkyLight turn_on."""
+ with patch.object(mock_light, "set_color") as mock_set_color, patch.object(
+ mock_light, "get_color", return_value=(0, 0, 0, 0)
+ ):
+ await hass.services.async_call(
+ "light",
+ "turn_off",
+ {ATTR_ENTITY_ID: "light.bedroom"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ mock_set_color.assert_called_with(0, 0, 0, 0)
+
+
+async def test_light_update(hass, mock_light):
+ """Test KulerSkyLight update."""
+ utcnow = dt_util.utcnow()
+
+ state = hass.states.get("light.bedroom")
+ assert state.state == STATE_OFF
+ assert state.attributes == {
+ ATTR_FRIENDLY_NAME: "Bedroom",
+ ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS
+ | SUPPORT_COLOR
+ | SUPPORT_WHITE_VALUE,
+ }
+
+ # Test an exception during discovery
+ with patch.object(
+ mock_light, "get_color", side_effect=pykulersky.PykulerskyException("TEST")
+ ):
+ utcnow = utcnow + SCAN_INTERVAL
+ async_fire_time_changed(hass, utcnow)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("light.bedroom")
+ assert state.state == STATE_UNAVAILABLE
+ assert state.attributes == {
+ ATTR_FRIENDLY_NAME: "Bedroom",
+ ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS
+ | SUPPORT_COLOR
+ | SUPPORT_WHITE_VALUE,
+ }
+
+ with patch.object(
+ mock_light,
+ "get_color",
+ return_value=(80, 160, 200, 240),
+ ):
+ utcnow = utcnow + SCAN_INTERVAL
+ async_fire_time_changed(hass, utcnow)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("light.bedroom")
+ assert state.state == STATE_ON
+ assert state.attributes == {
+ ATTR_FRIENDLY_NAME: "Bedroom",
+ ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS
+ | SUPPORT_COLOR
+ | SUPPORT_WHITE_VALUE,
+ ATTR_BRIGHTNESS: 200,
+ ATTR_HS_COLOR: (200, 60),
+ ATTR_RGB_COLOR: (102, 203, 255),
+ ATTR_WHITE_VALUE: 240,
+ ATTR_XY_COLOR: (0.184, 0.261),
+ }
diff --git a/tests/components/light/test_device_action.py b/tests/components/light/test_device_action.py
index aeedde82af5..63670d9bfab 100644
--- a/tests/components/light/test_device_action.py
+++ b/tests/components/light/test_device_action.py
@@ -21,6 +21,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/light/test_device_condition.py b/tests/components/light/test_device_condition.py
index 2a43a0abebe..eea443b7d34 100644
--- a/tests/components/light/test_device_condition.py
+++ b/tests/components/light/test_device_condition.py
@@ -19,6 +19,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/light/test_device_trigger.py b/tests/components/light/test_device_trigger.py
index d3c630cd0dc..fad39898467 100644
--- a/tests/components/light/test_device_trigger.py
+++ b/tests/components/light/test_device_trigger.py
@@ -19,6 +19,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/litejet/test_trigger.py b/tests/components/litejet/test_trigger.py
index b8699785d5e..3cbbd474b88 100644
--- a/tests/components/litejet/test_trigger.py
+++ b/tests/components/litejet/test_trigger.py
@@ -11,6 +11,7 @@ import homeassistant.components.automation as automation
import homeassistant.util.dt as dt_util
from tests.common import async_fire_time_changed, async_mock_service
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
_LOGGER = logging.getLogger(__name__)
diff --git a/tests/components/lock/test_device_action.py b/tests/components/lock/test_device_action.py
index dbf390df57b..91cab9cdaf4 100644
--- a/tests/components/lock/test_device_action.py
+++ b/tests/components/lock/test_device_action.py
@@ -15,6 +15,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/lock/test_device_condition.py b/tests/components/lock/test_device_condition.py
index c2db984f16f..949100daa55 100644
--- a/tests/components/lock/test_device_condition.py
+++ b/tests/components/lock/test_device_condition.py
@@ -15,6 +15,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/lock/test_device_trigger.py b/tests/components/lock/test_device_trigger.py
index 006df742c6d..20674c483fd 100644
--- a/tests/components/lock/test_device_trigger.py
+++ b/tests/components/lock/test_device_trigger.py
@@ -15,6 +15,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/media_player/test_device_condition.py b/tests/components/media_player/test_device_condition.py
index c52daa80320..c7668d748af 100644
--- a/tests/components/media_player/test_device_condition.py
+++ b/tests/components/media_player/test_device_condition.py
@@ -21,6 +21,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py
index 02012a1f71d..9434fb1a411 100644
--- a/tests/components/media_player/test_init.py
+++ b/tests/components/media_player/test_init.py
@@ -92,6 +92,33 @@ async def test_get_image_http_remote(hass, aiohttp_client):
assert content == b"image"
+async def test_get_async_get_browse_image(hass, aiohttp_client, hass_ws_client):
+ """Test get browse image."""
+ await async_setup_component(
+ hass, "media_player", {"media_player": {"platform": "demo"}}
+ )
+ await hass.async_block_till_done()
+
+ entity_comp = hass.data.get("entity_components", {}).get("media_player")
+ assert entity_comp
+
+ player = entity_comp.get_entity("media_player.bedroom")
+ assert player
+
+ client = await aiohttp_client(hass.http.app)
+
+ with patch(
+ "homeassistant.components.media_player.MediaPlayerEntity."
+ "async_get_browse_image",
+ return_value=(b"image", "image/jpeg"),
+ ):
+ url = player.get_browse_image_url("album", "abcd")
+ resp = await client.get(url)
+ content = await resp.read()
+
+ assert content == b"image"
+
+
def test_deprecated_base_class(caplog):
"""Test deprecated base class."""
diff --git a/tests/components/mfi/test_sensor.py b/tests/components/mfi/test_sensor.py
index 966dcbfd567..ad23039237c 100644
--- a/tests/components/mfi/test_sensor.py
+++ b/tests/components/mfi/test_sensor.py
@@ -97,7 +97,8 @@ async def test_setup_adds_proper_devices(hass):
"homeassistant.components.mfi.sensor.MfiSensor", side_effect=mfi.MfiSensor
) as mock_sensor:
ports = {
- i: mock.MagicMock(model=model) for i, model in enumerate(mfi.SENSOR_MODELS)
+ i: mock.MagicMock(model=model, label=f"Port {i}", value=0)
+ for i, model in enumerate(mfi.SENSOR_MODELS)
}
ports["bad"] = mock.MagicMock(model="notasensor")
mock_client.return_value.get_devices.return_value = [
diff --git a/tests/components/mfi/test_switch.py b/tests/components/mfi/test_switch.py
index e700aad8353..b11dcdccb6e 100644
--- a/tests/components/mfi/test_switch.py
+++ b/tests/components/mfi/test_switch.py
@@ -31,7 +31,10 @@ async def test_setup_adds_proper_devices(hass):
"homeassistant.components.mfi.switch.MfiSwitch", side_effect=mfi.MfiSwitch
) as mock_switch:
ports = {
- i: mock.MagicMock(model=model) for i, model in enumerate(mfi.SWITCH_MODELS)
+ i: mock.MagicMock(
+ model=model, label=f"Port {i}", output=False, data={}, ident=f"abcd-{i}"
+ )
+ for i, model in enumerate(mfi.SWITCH_MODELS)
}
ports["bad"] = mock.MagicMock(model="notaswitch")
print(ports["bad"].model)
diff --git a/tests/components/mobile_app/conftest.py b/tests/components/mobile_app/conftest.py
index e15c5732ac4..7c611eb1010 100644
--- a/tests/components/mobile_app/conftest.py
+++ b/tests/components/mobile_app/conftest.py
@@ -40,6 +40,26 @@ async def create_registrations(hass, authed_api_client):
return (enc_reg_json, clear_reg_json)
+@pytest.fixture
+async def push_registration(hass, authed_api_client):
+ """Return registration with push notifications enabled."""
+ await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
+
+ enc_reg = await authed_api_client.post(
+ "/api/mobile_app/registrations",
+ json={
+ **REGISTER,
+ "app_data": {
+ "push_url": "http://localhost/mock-push",
+ "push_token": "abcd",
+ },
+ },
+ )
+
+ assert enc_reg.status == 201
+ return await enc_reg.json()
+
+
@pytest.fixture
async def webhook_client(hass, authed_api_client, aiohttp_client):
"""mobile_app mock client."""
diff --git a/tests/components/mobile_app/test_device_action.py b/tests/components/mobile_app/test_device_action.py
new file mode 100644
index 00000000000..e5b15412e4d
--- /dev/null
+++ b/tests/components/mobile_app/test_device_action.py
@@ -0,0 +1,68 @@
+"""The tests for Mobile App device actions."""
+from homeassistant.components import automation, device_automation
+from homeassistant.components.mobile_app import DATA_DEVICES, DOMAIN, util
+from homeassistant.setup import async_setup_component
+
+from tests.common import async_get_device_automations, patch
+
+
+async def test_get_actions(hass, push_registration):
+ """Test we get the expected actions from a mobile_app."""
+ webhook_id = push_registration["webhook_id"]
+ device_id = hass.data[DOMAIN][DATA_DEVICES][webhook_id].id
+
+ assert await async_get_device_automations(hass, "action", device_id) == [
+ {"domain": DOMAIN, "device_id": device_id, "type": "notify"}
+ ]
+
+ capabilitites = await device_automation._async_get_device_automation_capabilities(
+ hass, "action", {"domain": DOMAIN, "device_id": device_id, "type": "notify"}
+ )
+ assert "extra_fields" in capabilitites
+
+
+async def test_action(hass, push_registration):
+ """Test for turn_on and turn_off actions."""
+ webhook_id = push_registration["webhook_id"]
+
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {
+ "platform": "event",
+ "event_type": "test_notify",
+ },
+ "action": [
+ {"variables": {"name": "Paulus"}},
+ {
+ "domain": DOMAIN,
+ "device_id": hass.data[DOMAIN]["devices"][webhook_id].id,
+ "type": "notify",
+ "message": "Hello {{ name }}",
+ },
+ ],
+ },
+ ]
+ },
+ )
+
+ service_name = util.get_notify_service(hass, webhook_id)
+
+ # Make sure it was actually registered
+ assert hass.services.has_service("notify", service_name)
+
+ with patch(
+ "homeassistant.components.mobile_app.notify.MobileAppNotificationService.async_send_message"
+ ) as mock_send_message:
+ hass.bus.async_fire("test_notify")
+ await hass.async_block_till_done()
+ assert len(mock_send_message.mock_calls) == 1
+
+ assert mock_send_message.mock_calls[0][2] == {
+ "target": [webhook_id],
+ "message": "Hello Paulus",
+ "data": None,
+ }
diff --git a/tests/components/motion_blinds/__init__.py b/tests/components/motion_blinds/__init__.py
new file mode 100644
index 00000000000..1c77ce16922
--- /dev/null
+++ b/tests/components/motion_blinds/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Motion Blinds integration."""
diff --git a/tests/components/motion_blinds/test_config_flow.py b/tests/components/motion_blinds/test_config_flow.py
new file mode 100644
index 00000000000..faa3e7115b8
--- /dev/null
+++ b/tests/components/motion_blinds/test_config_flow.py
@@ -0,0 +1,75 @@
+"""Test the Motion Blinds config flow."""
+import socket
+
+import pytest
+
+from homeassistant import config_entries
+from homeassistant.components.motion_blinds.config_flow import DEFAULT_GATEWAY_NAME
+from homeassistant.components.motion_blinds.const import DOMAIN
+from homeassistant.const import CONF_API_KEY, CONF_HOST
+
+from tests.async_mock import patch
+
+TEST_HOST = "1.2.3.4"
+TEST_API_KEY = "12ab345c-d67e-8f"
+
+
+@pytest.fixture(name="motion_blinds_connect", autouse=True)
+def motion_blinds_connect_fixture():
+ """Mock motion blinds connection and entry setup."""
+ with patch(
+ "homeassistant.components.motion_blinds.gateway.MotionGateway.GetDeviceList",
+ return_value=True,
+ ), patch(
+ "homeassistant.components.motion_blinds.gateway.MotionGateway.Update",
+ return_value=True,
+ ), patch(
+ "homeassistant.components.motion_blinds.async_setup_entry", return_value=True
+ ):
+ yield
+
+
+async def test_config_flow_manual_host_success(hass):
+ """Successful flow manually initialized by the user."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_HOST: TEST_HOST, CONF_API_KEY: TEST_API_KEY},
+ )
+
+ assert result["type"] == "create_entry"
+ assert result["title"] == DEFAULT_GATEWAY_NAME
+ assert result["data"] == {
+ CONF_HOST: TEST_HOST,
+ CONF_API_KEY: TEST_API_KEY,
+ }
+
+
+async def test_config_flow_connection_error(hass):
+ """Failed flow manually initialized by the user with connection timeout."""
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+ assert result["errors"] == {}
+
+ with patch(
+ "homeassistant.components.motion_blinds.gateway.MotionGateway.GetDeviceList",
+ side_effect=socket.timeout,
+ ):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_HOST: TEST_HOST, CONF_API_KEY: TEST_API_KEY},
+ )
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "connection_error"
diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py
index 844f34e8d4f..a46406c330f 100644
--- a/tests/components/mqtt/test_device_trigger.py
+++ b/tests/components/mqtt/test_device_trigger.py
@@ -16,6 +16,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py
index 39ea8bc92ec..86b905f2b0f 100644
--- a/tests/components/mqtt/test_init.py
+++ b/tests/components/mqtt/test_init.py
@@ -638,8 +638,9 @@ async def test_restore_subscriptions_on_reconnect(hass, mqtt_client_mock, mqtt_m
assert mqtt_client_mock.subscribe.call_count == 1
mqtt_mock._mqtt_on_disconnect(None, None, 0)
- mqtt_mock._mqtt_on_connect(None, None, None, 0)
- await hass.async_block_till_done()
+ with patch("homeassistant.components.mqtt.DISCOVERY_COOLDOWN", 0):
+ mqtt_mock._mqtt_on_connect(None, None, None, 0)
+ await hass.async_block_till_done()
assert mqtt_client_mock.subscribe.call_count == 2
@@ -671,8 +672,9 @@ async def test_restore_all_active_subscriptions_on_reconnect(
assert mqtt_client_mock.unsubscribe.call_count == 0
mqtt_mock._mqtt_on_disconnect(None, None, 0)
- mqtt_mock._mqtt_on_connect(None, None, None, 0)
- await hass.async_block_till_done()
+ with patch("homeassistant.components.mqtt.DISCOVERY_COOLDOWN", 0):
+ mqtt_mock._mqtt_on_connect(None, None, None, 0)
+ await hass.async_block_till_done()
expected.append(call("test/state", 1))
assert mqtt_client_mock.subscribe.mock_calls == expected
diff --git a/tests/components/mqtt/test_scene.py b/tests/components/mqtt/test_scene.py
new file mode 100644
index 00000000000..0e3341bd15f
--- /dev/null
+++ b/tests/components/mqtt/test_scene.py
@@ -0,0 +1,181 @@
+"""The tests for the MQTT scene platform."""
+import copy
+import json
+
+import pytest
+
+from homeassistant.components import scene
+from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON
+import homeassistant.core as ha
+from homeassistant.setup import async_setup_component
+
+from .test_common import (
+ help_test_availability_when_connection_lost,
+ help_test_availability_without_topic,
+ help_test_custom_availability_payload,
+ help_test_default_availability_payload,
+ help_test_discovery_broken,
+ help_test_discovery_removal,
+ help_test_discovery_update,
+ help_test_discovery_update_unchanged,
+ help_test_unique_id,
+)
+
+from tests.async_mock import patch
+
+DEFAULT_CONFIG = {
+ scene.DOMAIN: {
+ "platform": "mqtt",
+ "name": "test",
+ "command_topic": "test-topic",
+ "payload_on": "test-payload-on",
+ }
+}
+
+
+async def test_sending_mqtt_commands(hass, mqtt_mock):
+ """Test the sending MQTT commands."""
+ fake_state = ha.State("scene.test", scene.STATE)
+
+ with patch(
+ "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state",
+ return_value=fake_state,
+ ):
+ assert await async_setup_component(
+ hass,
+ scene.DOMAIN,
+ {
+ scene.DOMAIN: {
+ "platform": "mqtt",
+ "name": "test",
+ "command_topic": "command-topic",
+ "payload_on": "beer on",
+ },
+ },
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("scene.test")
+ assert state.state == scene.STATE
+
+ data = {ATTR_ENTITY_ID: "scene.test"}
+ await hass.services.async_call(scene.DOMAIN, SERVICE_TURN_ON, data, blocking=True)
+
+ mqtt_mock.async_publish.assert_called_once_with(
+ "command-topic", "beer on", 0, False
+ )
+
+
+async def test_availability_when_connection_lost(hass, mqtt_mock):
+ """Test availability after MQTT disconnection."""
+ await help_test_availability_when_connection_lost(
+ hass, mqtt_mock, scene.DOMAIN, DEFAULT_CONFIG
+ )
+
+
+async def test_availability_without_topic(hass, mqtt_mock):
+ """Test availability without defined availability topic."""
+ await help_test_availability_without_topic(
+ hass, mqtt_mock, scene.DOMAIN, DEFAULT_CONFIG
+ )
+
+
+async def test_default_availability_payload(hass, mqtt_mock):
+ """Test availability by default payload with defined topic."""
+ config = {
+ scene.DOMAIN: {
+ "platform": "mqtt",
+ "name": "test",
+ "command_topic": "command-topic",
+ "payload_on": 1,
+ }
+ }
+
+ await help_test_default_availability_payload(
+ hass, mqtt_mock, scene.DOMAIN, config, True, "state-topic", "1"
+ )
+
+
+async def test_custom_availability_payload(hass, mqtt_mock):
+ """Test availability by custom payload with defined topic."""
+ config = {
+ scene.DOMAIN: {
+ "platform": "mqtt",
+ "name": "test",
+ "command_topic": "command-topic",
+ "payload_on": 1,
+ }
+ }
+
+ await help_test_custom_availability_payload(
+ hass, mqtt_mock, scene.DOMAIN, config, True, "state-topic", "1"
+ )
+
+
+async def test_unique_id(hass, mqtt_mock):
+ """Test unique id option only creates one scene per unique_id."""
+ config = {
+ scene.DOMAIN: [
+ {
+ "platform": "mqtt",
+ "name": "Test 1",
+ "command_topic": "command-topic",
+ "unique_id": "TOTALLY_UNIQUE",
+ },
+ {
+ "platform": "mqtt",
+ "name": "Test 2",
+ "command_topic": "command-topic",
+ "unique_id": "TOTALLY_UNIQUE",
+ },
+ ]
+ }
+ await help_test_unique_id(hass, mqtt_mock, scene.DOMAIN, config)
+
+
+async def test_discovery_removal_scene(hass, mqtt_mock, caplog):
+ """Test removal of discovered scene."""
+ data = '{ "name": "test",' ' "command_topic": "test_topic" }'
+ await help_test_discovery_removal(hass, mqtt_mock, caplog, scene.DOMAIN, data)
+
+
+async def test_discovery_update_payload(hass, mqtt_mock, caplog):
+ """Test update of discovered scene."""
+ config1 = copy.deepcopy(DEFAULT_CONFIG[scene.DOMAIN])
+ config2 = copy.deepcopy(DEFAULT_CONFIG[scene.DOMAIN])
+ config1["name"] = "Beer"
+ config2["name"] = "Milk"
+ config1["payload_on"] = "ON"
+ config2["payload_on"] = "ACTIVATE"
+
+ data1 = json.dumps(config1)
+ data2 = json.dumps(config2)
+ await help_test_discovery_update(
+ hass,
+ mqtt_mock,
+ caplog,
+ scene.DOMAIN,
+ data1,
+ data2,
+ )
+
+
+async def test_discovery_update_unchanged_scene(hass, mqtt_mock, caplog):
+ """Test update of discovered scene."""
+ data1 = '{ "name": "Beer",' ' "command_topic": "test_topic" }'
+ with patch(
+ "homeassistant.components.mqtt.scene.MqttScene.discovery_update"
+ ) as discovery_update:
+ await help_test_discovery_update_unchanged(
+ hass, mqtt_mock, caplog, scene.DOMAIN, data1, discovery_update
+ )
+
+
+@pytest.mark.no_fail_on_log_exception
+async def test_discovery_broken(hass, mqtt_mock, caplog):
+ """Test handling of bad discovery message."""
+ data1 = '{ "name": "Beer" }'
+ data2 = '{ "name": "Milk",' ' "command_topic": "test_topic" }'
+ await help_test_discovery_broken(
+ hass, mqtt_mock, caplog, scene.DOMAIN, data1, data2
+ )
diff --git a/tests/components/mqtt/test_trigger.py b/tests/components/mqtt/test_trigger.py
index b35082ef2c6..a26ecadb6bd 100644
--- a/tests/components/mqtt/test_trigger.py
+++ b/tests/components/mqtt/test_trigger.py
@@ -7,6 +7,7 @@ from homeassistant.setup import async_setup_component
from tests.async_mock import ANY
from tests.common import async_fire_mqtt_message, async_mock_service, mock_component
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/nest/camera_sdm_test.py b/tests/components/nest/camera_sdm_test.py
index e3397129ec9..4a018305bcf 100644
--- a/tests/components/nest/camera_sdm_test.py
+++ b/tests/components/nest/camera_sdm_test.py
@@ -6,10 +6,8 @@ pubsub subscriber.
"""
import datetime
-from typing import List
-from aiohttp.client_exceptions import ClientConnectionError
-from google_nest_sdm.auth import AbstractAuth
+import aiohttp
from google_nest_sdm.device import Device
from homeassistant.components import camera
@@ -41,47 +39,6 @@ DATETIME_FORMAT = "YY-MM-DDTHH:MM:SS"
DOMAIN = "nest"
-class FakeResponse:
- """A fake web response used for returning results of commands."""
-
- def __init__(self, json=None, error=None):
- """Initialize the FakeResponse."""
- self._json = json
- self._error = error
-
- def raise_for_status(self):
- """Mimics a successful response status."""
- if self._error:
- raise self._error
- pass
-
- async def json(self):
- """Return a dict with the response."""
- assert self._json
- return self._json
-
-
-class FakeAuth(AbstractAuth):
- """Fake authentication object that returns fake responses."""
-
- def __init__(self, responses: List[FakeResponse]):
- """Initialize the FakeAuth."""
- super().__init__(None, "")
- self._responses = responses
-
- async def async_get_access_token(self):
- """Return a fake access token."""
- return "some-token"
-
- async def creds(self):
- """Return a fake creds."""
- return None
-
- async def request(self, method: str, url: str, **kwargs):
- """Pass through the FakeResponse."""
- return self._responses.pop(0)
-
-
async def async_setup_camera(hass, traits={}, auth=None):
"""Set up the platform and prerequisites."""
devices = {}
@@ -145,21 +102,25 @@ async def test_camera_device(hass):
assert device.identifiers == {("nest", DEVICE_ID)}
-async def test_camera_stream(hass, aiohttp_client):
+async def test_camera_stream(hass, auth):
"""Test a basic camera and fetch its live stream."""
now = utcnow()
expiration = now + datetime.timedelta(seconds=100)
- response = FakeResponse(
- {
- "results": {
- "streamUrls": {"rtspUrl": "rtsp://some/url?auth=g.0.streamingToken"},
- "streamExtensionToken": "g.1.extensionToken",
- "streamToken": "g.0.streamingToken",
- "expiresAt": expiration.isoformat(timespec="seconds"),
- },
- }
- )
- await async_setup_camera(hass, DEVICE_TRAITS, auth=FakeAuth([response]))
+ auth.responses = [
+ aiohttp.web.json_response(
+ {
+ "results": {
+ "streamUrls": {
+ "rtspUrl": "rtsp://some/url?auth=g.0.streamingToken"
+ },
+ "streamExtensionToken": "g.1.extensionToken",
+ "streamToken": "g.0.streamingToken",
+ "expiresAt": expiration.isoformat(timespec="seconds"),
+ },
+ }
+ )
+ ]
+ await async_setup_camera(hass, DEVICE_TRAITS, auth=auth)
assert len(hass.states.async_all()) == 1
cam = hass.states.get("camera.my_camera")
@@ -179,15 +140,15 @@ async def test_camera_stream(hass, aiohttp_client):
assert image.content == b"image bytes"
-async def test_refresh_expired_stream_token(hass, aiohttp_client):
+async def test_refresh_expired_stream_token(hass, auth):
"""Test a camera stream expiration and refresh."""
now = utcnow()
stream_1_expiration = now + datetime.timedelta(seconds=90)
stream_2_expiration = now + datetime.timedelta(seconds=180)
stream_3_expiration = now + datetime.timedelta(seconds=360)
- responses = [
+ auth.responses = [
# Stream URL #1
- FakeResponse(
+ aiohttp.web.json_response(
{
"results": {
"streamUrls": {
@@ -200,7 +161,7 @@ async def test_refresh_expired_stream_token(hass, aiohttp_client):
}
),
# Stream URL #2
- FakeResponse(
+ aiohttp.web.json_response(
{
"results": {
"streamExtensionToken": "g.2.extensionToken",
@@ -210,7 +171,7 @@ async def test_refresh_expired_stream_token(hass, aiohttp_client):
}
),
# Stream URL #3
- FakeResponse(
+ aiohttp.web.json_response(
{
"results": {
"streamExtensionToken": "g.3.extensionToken",
@@ -223,7 +184,7 @@ async def test_refresh_expired_stream_token(hass, aiohttp_client):
await async_setup_camera(
hass,
DEVICE_TRAITS,
- auth=FakeAuth(responses),
+ auth=auth,
)
assert len(hass.states.async_all()) == 1
@@ -259,12 +220,12 @@ async def test_refresh_expired_stream_token(hass, aiohttp_client):
assert stream_source == "rtsp://some/url?auth=g.3.streamingToken"
-async def test_camera_removed(hass, aiohttp_client):
+async def test_camera_removed(hass, auth):
"""Test case where entities are removed and stream tokens expired."""
now = utcnow()
expiration = now + datetime.timedelta(seconds=100)
- responses = [
- FakeResponse(
+ auth.responses = [
+ aiohttp.web.json_response(
{
"results": {
"streamUrls": {
@@ -276,12 +237,12 @@ async def test_camera_removed(hass, aiohttp_client):
},
}
),
- FakeResponse({"results": {}}),
+ aiohttp.web.json_response({"results": {}}),
]
await async_setup_camera(
hass,
DEVICE_TRAITS,
- auth=FakeAuth(responses),
+ auth=auth,
)
assert len(hass.states.async_all()) == 1
@@ -297,13 +258,13 @@ async def test_camera_removed(hass, aiohttp_client):
assert len(hass.states.async_all()) == 0
-async def test_refresh_expired_stream_failure(hass, aiohttp_client):
+async def test_refresh_expired_stream_failure(hass, auth):
"""Tests a failure when refreshing the stream."""
now = utcnow()
stream_1_expiration = now + datetime.timedelta(seconds=90)
stream_2_expiration = now + datetime.timedelta(seconds=180)
- responses = [
- FakeResponse(
+ auth.responses = [
+ aiohttp.web.json_response(
{
"results": {
"streamUrls": {
@@ -316,9 +277,9 @@ async def test_refresh_expired_stream_failure(hass, aiohttp_client):
}
),
# Extending the stream fails with arbitrary error
- FakeResponse(error=ClientConnectionError()),
+ aiohttp.web.Response(status=500),
# Next attempt to get a stream fetches a new url
- FakeResponse(
+ aiohttp.web.json_response(
{
"results": {
"streamUrls": {
@@ -334,7 +295,7 @@ async def test_refresh_expired_stream_failure(hass, aiohttp_client):
await async_setup_camera(
hass,
DEVICE_TRAITS,
- auth=FakeAuth(responses),
+ auth=auth,
)
assert len(hass.states.async_all()) == 1
diff --git a/tests/components/nest/climate_sdm_test.py b/tests/components/nest/climate_sdm_test.py
index 48efd32d859..bf6716ec966 100644
--- a/tests/components/nest/climate_sdm_test.py
+++ b/tests/components/nest/climate_sdm_test.py
@@ -364,25 +364,8 @@ async def test_thermostat_eco_heat_only(hass):
assert thermostat.attributes[ATTR_PRESET_MODES] == [PRESET_ECO, PRESET_NONE]
-class FakeAuth:
- """A fake implementation of the auth class that records requests."""
-
- def __init__(self):
- """Initialize FakeAuth."""
- self.method = None
- self.url = None
- self.json = None
-
- async def request(self, method, url, json):
- """Capure the request arguments for tests to assert on."""
- self.method = method
- self.url = url
- self.json = json
-
-
-async def test_thermostat_set_hvac_mode(hass):
+async def test_thermostat_set_hvac_mode(hass, auth):
"""Test a thermostat changing hvac modes."""
- auth = FakeAuth()
subscriber = await setup_climate(
hass,
{
@@ -434,7 +417,7 @@ async def test_thermostat_set_hvac_mode(hass):
},
auth=None,
)
- subscriber.receive_event(event)
+ await subscriber.async_receive_event(event)
await hass.async_block_till_done() # Process dispatch/update signal
thermostat = hass.states.get("climate.my_thermostat")
@@ -458,7 +441,7 @@ async def test_thermostat_set_hvac_mode(hass):
},
auth=None,
)
- subscriber.receive_event(event)
+ await subscriber.async_receive_event(event)
await hass.async_block_till_done() # Process dispatch/update signal
thermostat = hass.states.get("climate.my_thermostat")
@@ -467,9 +450,8 @@ async def test_thermostat_set_hvac_mode(hass):
assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT
-async def test_thermostat_set_eco_preset(hass):
+async def test_thermostat_set_eco_preset(hass, auth):
"""Test a thermostat put into eco mode."""
- auth = FakeAuth()
subscriber = await setup_climate(
hass,
{
@@ -532,7 +514,7 @@ async def test_thermostat_set_eco_preset(hass):
},
auth=auth,
)
- subscriber.receive_event(event)
+ await subscriber.async_receive_event(event)
await hass.async_block_till_done() # Process dispatch/update signal
thermostat = hass.states.get("climate.my_thermostat")
@@ -553,9 +535,8 @@ async def test_thermostat_set_eco_preset(hass):
}
-async def test_thermostat_set_cool(hass):
+async def test_thermostat_set_cool(hass, auth):
"""Test a thermostat in cool mode with a temperature change."""
- auth = FakeAuth()
await setup_climate(
hass,
{
@@ -587,9 +568,8 @@ async def test_thermostat_set_cool(hass):
}
-async def test_thermostat_set_heat(hass):
+async def test_thermostat_set_heat(hass, auth):
"""Test a thermostat heating mode with a temperature change."""
- auth = FakeAuth()
await setup_climate(
hass,
{
@@ -621,9 +601,8 @@ async def test_thermostat_set_heat(hass):
}
-async def test_thermostat_set_heat_cool(hass):
+async def test_thermostat_set_heat_cool(hass, auth):
"""Test a thermostat in heatcool mode with a temperature change."""
- auth = FakeAuth()
await setup_climate(
hass,
{
@@ -732,9 +711,8 @@ async def test_thermostat_fan_on(hass):
assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF]
-async def test_thermostat_set_fan(hass):
+async def test_thermostat_set_fan(hass, auth):
"""Test a thermostat enabling the fan."""
- auth = FakeAuth()
await setup_climate(
hass,
{
@@ -805,9 +783,8 @@ async def test_thermostat_fan_empty(hass):
assert ATTR_FAN_MODES not in thermostat.attributes
-async def test_thermostat_target_temp(hass):
+async def test_thermostat_target_temp(hass, auth):
"""Test a thermostat changing hvac modes and affected on target temps."""
- auth = FakeAuth()
subscriber = await setup_climate(
hass,
{
@@ -857,7 +834,7 @@ async def test_thermostat_target_temp(hass):
},
auth=None,
)
- subscriber.receive_event(event)
+ await subscriber.async_receive_event(event)
await hass.async_block_till_done() # Process dispatch/update signal
thermostat = hass.states.get("climate.my_thermostat")
diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py
index c1c8dbd04d7..cd3a06a5afa 100644
--- a/tests/components/nest/common.py
+++ b/tests/components/nest/common.py
@@ -3,7 +3,7 @@
import time
from google_nest_sdm.device_manager import DeviceManager
-from google_nest_sdm.event import EventCallback, EventMessage
+from google_nest_sdm.event import AsyncEventCallback, EventMessage
from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber
from homeassistant.components.nest import DOMAIN
@@ -61,7 +61,7 @@ class FakeSubscriber(GoogleNestSubscriber):
self._device_manager = device_manager
self._callback = None
- def set_update_callback(self, callback: EventCallback):
+ def set_update_callback(self, callback: AsyncEventCallback):
"""Capture the callback set by Home Assistant."""
self._callback = callback
@@ -77,11 +77,11 @@ class FakeSubscriber(GoogleNestSubscriber):
"""No-op to stop the subscriber."""
return None
- def receive_event(self, event_message: EventMessage):
+ async def async_receive_event(self, event_message: EventMessage):
"""Simulate a received pubsub message, invoked by tests."""
# Update device state, then invoke HomeAssistant to refresh
- self._device_manager.handle_event(event_message)
- self._callback.handle_event(event_message)
+ await self._device_manager.async_handle_event(event_message)
+ await self._callback.async_handle_event(event_message)
async def async_setup_sdm_platform(hass, platform, devices={}, structures={}):
diff --git a/tests/components/nest/conftest.py b/tests/components/nest/conftest.py
new file mode 100644
index 00000000000..7e183ab9c82
--- /dev/null
+++ b/tests/components/nest/conftest.py
@@ -0,0 +1,57 @@
+"""Common libraries for test setup."""
+
+import aiohttp
+from google_nest_sdm.auth import AbstractAuth
+import pytest
+
+
+class FakeAuth(AbstractAuth):
+ """A fake implementation of the auth class that records requests.
+
+ This class captures the outgoing requests, and can also be used by
+ tests to set up fake responses. This class is registered as a response
+ handler for a fake aiohttp_server and can simulate successes or failures
+ from the API.
+ """
+
+ # Tests can set fake responses here.
+ responses = []
+ # The last request is recorded here.
+ method = None
+ url = None
+ json = None
+
+ # Set up by fixture
+ client = None
+
+ def __init__(self):
+ """Initialize FakeAuth."""
+ super().__init__(None, None)
+
+ async def async_get_access_token(self) -> str:
+ """Return a valid access token."""
+ return ""
+
+ async def request(self, method, url, json):
+ """Capure the request arguments for tests to assert on."""
+ self.method = method
+ self.url = url
+ self.json = json
+ return await self.client.get("/")
+
+ async def response_handler(self, request):
+ """Handle fake responess for aiohttp_server."""
+ if len(self.responses) > 0:
+ return self.responses.pop(0)
+ return aiohttp.web.json_response()
+
+
+@pytest.fixture
+async def auth(aiohttp_client):
+ """Fixture for an AbstractAuth."""
+ auth = FakeAuth()
+ app = aiohttp.web.Application()
+ app.router.add_get("/", auth.response_handler)
+ app.router.add_post("/", auth.response_handler)
+ auth.client = await aiohttp_client(app)
+ return auth
diff --git a/tests/components/nest/sensor_sdm_test.py b/tests/components/nest/sensor_sdm_test.py
index 7d2e299a1a1..b8b2912b124 100644
--- a/tests/components/nest/sensor_sdm_test.py
+++ b/tests/components/nest/sensor_sdm_test.py
@@ -164,7 +164,7 @@ async def test_event_updates_sensor(hass):
},
auth=None,
)
- subscriber.receive_event(event)
+ await subscriber.async_receive_event(event)
await hass.async_block_till_done() # Process dispatch/update signal
temperature = hass.states.get("sensor.my_sensor_temperature")
diff --git a/tests/components/nest/test_config_flow_legacy.py b/tests/components/nest/test_config_flow_legacy.py
index e0e93bf626a..23e01cf239a 100644
--- a/tests/components/nest/test_config_flow_legacy.py
+++ b/tests/components/nest/test_config_flow_legacy.py
@@ -100,7 +100,7 @@ async def test_abort_if_exception_generating_auth_url(hass):
flow.hass = hass
result = await flow.async_step_init()
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "authorize_url_fail"
+ assert result["reason"] == "unknown_authorize_url_generation"
async def test_verify_code_timeout(hass):
diff --git a/tests/components/nest/test_config_flow_sdm.py b/tests/components/nest/test_config_flow_sdm.py
index 1df751f3980..6573b17980e 100644
--- a/tests/components/nest/test_config_flow_sdm.py
+++ b/tests/components/nest/test_config_flow_sdm.py
@@ -12,7 +12,9 @@ PROJECT_ID = "project-id-4321"
SUBSCRIBER_ID = "subscriber-id-9876"
-async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request):
+async def test_full_flow(
+ hass, aiohttp_client, aioclient_mock, current_request_with_host
+):
"""Check full flow."""
assert await setup.async_setup_component(
hass,
@@ -31,7 +33,13 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
- state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]})
+ state = config_entry_oauth2_flow._encode_jwt(
+ hass,
+ {
+ "flow_id": result["flow_id"],
+ "redirect_uri": "https://example.com/auth/external/callback",
+ },
+ )
oauth_authorize = OAUTH2_AUTHORIZE.format(project_id=PROJECT_ID)
assert result["url"] == (
diff --git a/tests/components/nest/test_device_trigger.py b/tests/components/nest/test_device_trigger.py
new file mode 100644
index 00000000000..89dccf6c31e
--- /dev/null
+++ b/tests/components/nest/test_device_trigger.py
@@ -0,0 +1,313 @@
+"""The tests for Nest device triggers."""
+from google_nest_sdm.device import Device
+from google_nest_sdm.event import EventMessage
+import pytest
+
+import homeassistant.components.automation as automation
+from homeassistant.components.device_automation.exceptions import (
+ InvalidDeviceAutomationConfig,
+)
+from homeassistant.components.nest import DOMAIN, NEST_EVENT
+from homeassistant.setup import async_setup_component
+
+from .common import async_setup_sdm_platform
+
+from tests.common import (
+ assert_lists_same,
+ async_get_device_automations,
+ async_mock_service,
+)
+
+DEVICE_ID = "some-device-id"
+DEVICE_NAME = "My Camera"
+DATA_MESSAGE = {"message": "service-called"}
+
+
+def make_camera(device_id, name=DEVICE_NAME, traits={}):
+ """Create a nest camera."""
+ traits = traits.copy()
+ traits.update(
+ {
+ "sdm.devices.traits.Info": {
+ "customName": name,
+ },
+ "sdm.devices.traits.CameraLiveStream": {
+ "maxVideoResolution": {
+ "width": 640,
+ "height": 480,
+ },
+ "videoCodecs": ["H264"],
+ "audioCodecs": ["AAC"],
+ },
+ }
+ )
+ return Device.MakeDevice(
+ {
+ "name": device_id,
+ "type": "sdm.devices.types.CAMERA",
+ "traits": traits,
+ },
+ auth=None,
+ )
+
+
+async def async_setup_camera(hass, devices=None):
+ """Set up the platform and prerequisites for testing available triggers."""
+ if not devices:
+ devices = {DEVICE_ID: make_camera(device_id=DEVICE_ID)}
+ return await async_setup_sdm_platform(hass, "camera", devices)
+
+
+async def setup_automation(hass, device_id, trigger_type):
+ """Set up an automation trigger for testing triggering."""
+ return await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {
+ "platform": "device",
+ "domain": DOMAIN,
+ "device_id": device_id,
+ "type": trigger_type,
+ },
+ "action": {
+ "service": "test.automation",
+ "data": DATA_MESSAGE,
+ },
+ },
+ ]
+ },
+ )
+
+
+@pytest.fixture
+def calls(hass):
+ """Track calls to a mock service."""
+ return async_mock_service(hass, "test", "automation")
+
+
+async def test_get_triggers(hass):
+ """Test we get the expected triggers from a nest."""
+ camera = make_camera(
+ device_id=DEVICE_ID,
+ traits={
+ "sdm.devices.traits.CameraMotion": {},
+ "sdm.devices.traits.CameraPerson": {},
+ },
+ )
+ await async_setup_camera(hass, {DEVICE_ID: camera})
+
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_entry = device_registry.async_get_device(
+ {("nest", DEVICE_ID)}, connections={}
+ )
+
+ expected_triggers = [
+ {
+ "platform": "device",
+ "domain": DOMAIN,
+ "type": "camera_motion",
+ "device_id": device_entry.id,
+ },
+ {
+ "platform": "device",
+ "domain": DOMAIN,
+ "type": "camera_person",
+ "device_id": device_entry.id,
+ },
+ ]
+ triggers = await async_get_device_automations(hass, "trigger", device_entry.id)
+ assert_lists_same(triggers, expected_triggers)
+
+
+async def test_multiple_devices(hass):
+ """Test we get the expected triggers from a nest."""
+ camera1 = make_camera(
+ device_id="device-id-1",
+ name="Camera 1",
+ traits={
+ "sdm.devices.traits.CameraSound": {},
+ },
+ )
+ camera2 = make_camera(
+ device_id="device-id-2",
+ name="Camera 2",
+ traits={
+ "sdm.devices.traits.DoorbellChime": {},
+ },
+ )
+ await async_setup_camera(hass, {"device-id-1": camera1, "device-id-2": camera2})
+
+ registry = await hass.helpers.entity_registry.async_get_registry()
+ entry1 = registry.async_get("camera.camera_1")
+ assert entry1.unique_id == "device-id-1-camera"
+ entry2 = registry.async_get("camera.camera_2")
+ assert entry2.unique_id == "device-id-2-camera"
+
+ triggers = await async_get_device_automations(hass, "trigger", entry1.device_id)
+ assert len(triggers) == 1
+ assert {
+ "platform": "device",
+ "domain": DOMAIN,
+ "type": "camera_sound",
+ "device_id": entry1.device_id,
+ } == triggers[0]
+
+ triggers = await async_get_device_automations(hass, "trigger", entry2.device_id)
+ assert len(triggers) == 1
+ assert {
+ "platform": "device",
+ "domain": DOMAIN,
+ "type": "doorbell_chime",
+ "device_id": entry2.device_id,
+ } == triggers[0]
+
+
+async def test_triggers_for_invalid_device_id(hass):
+ """Get triggers for a device not found in the API."""
+ camera = make_camera(
+ device_id=DEVICE_ID,
+ traits={
+ "sdm.devices.traits.CameraMotion": {},
+ "sdm.devices.traits.CameraPerson": {},
+ },
+ )
+ await async_setup_camera(hass, {DEVICE_ID: camera})
+
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_entry = device_registry.async_get_device(
+ {("nest", DEVICE_ID)}, connections={}
+ )
+ assert device_entry is not None
+
+ # Create an additional device that does not exist. Fetching supported
+ # triggers for an unknown device will fail.
+ assert len(device_entry.config_entries) == 1
+ config_entry_id = next(iter(device_entry.config_entries))
+ device_entry_2 = device_registry.async_get_or_create(
+ config_entry_id=config_entry_id, identifiers={(DOMAIN, "some-unknown-nest-id")}
+ )
+ assert device_entry_2 is not None
+
+ with pytest.raises(InvalidDeviceAutomationConfig):
+ await async_get_device_automations(hass, "trigger", device_entry_2.id)
+
+
+async def test_no_triggers(hass):
+ """Test we get the expected triggers from a nest."""
+ camera = make_camera(device_id=DEVICE_ID, traits={})
+ await async_setup_camera(hass, {DEVICE_ID: camera})
+
+ registry = await hass.helpers.entity_registry.async_get_registry()
+ entry = registry.async_get("camera.my_camera")
+ assert entry.unique_id == "some-device-id-camera"
+
+ triggers = await async_get_device_automations(hass, "trigger", entry.device_id)
+ assert [] == triggers
+
+
+async def test_fires_on_camera_motion(hass, calls):
+ """Test camera_motion triggers firing."""
+ assert await setup_automation(hass, DEVICE_ID, "camera_motion")
+
+ message = {"device_id": DEVICE_ID, "type": "camera_motion"}
+ hass.bus.async_fire(NEST_EVENT, message)
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert calls[0].data == DATA_MESSAGE
+
+
+async def test_fires_on_camera_person(hass, calls):
+ """Test camera_person triggers firing."""
+ assert await setup_automation(hass, DEVICE_ID, "camera_person")
+
+ message = {"device_id": DEVICE_ID, "type": "camera_person"}
+ hass.bus.async_fire(NEST_EVENT, message)
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert calls[0].data == DATA_MESSAGE
+
+
+async def test_fires_on_camera_sound(hass, calls):
+ """Test camera_person triggers firing."""
+ assert await setup_automation(hass, DEVICE_ID, "camera_sound")
+
+ message = {"device_id": DEVICE_ID, "type": "camera_sound"}
+ hass.bus.async_fire(NEST_EVENT, message)
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert calls[0].data == DATA_MESSAGE
+
+
+async def test_fires_on_doorbell_chime(hass, calls):
+ """Test doorbell_chime triggers firing."""
+ assert await setup_automation(hass, DEVICE_ID, "doorbell_chime")
+
+ message = {"device_id": DEVICE_ID, "type": "doorbell_chime"}
+ hass.bus.async_fire(NEST_EVENT, message)
+ await hass.async_block_till_done()
+ assert len(calls) == 1
+ assert calls[0].data == DATA_MESSAGE
+
+
+async def test_trigger_for_wrong_device_id(hass, calls):
+ """Test for turn_on and turn_off triggers firing."""
+ assert await setup_automation(hass, DEVICE_ID, "camera_motion")
+
+ message = {"device_id": "wrong-device-id", "type": "camera_motion"}
+ hass.bus.async_fire(NEST_EVENT, message)
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+
+
+async def test_trigger_for_wrong_event_type(hass, calls):
+ """Test for turn_on and turn_off triggers firing."""
+ assert await setup_automation(hass, DEVICE_ID, "camera_motion")
+
+ message = {"device_id": DEVICE_ID, "type": "wrong-event-type"}
+ hass.bus.async_fire(NEST_EVENT, message)
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+
+
+async def test_subscriber_automation(hass, calls):
+ """Test end to end subscriber triggers automation."""
+ camera = make_camera(
+ device_id=DEVICE_ID,
+ traits={
+ "sdm.devices.traits.CameraMotion": {},
+ },
+ )
+ subscriber = await async_setup_camera(hass, {DEVICE_ID: camera})
+
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+ device_entry = device_registry.async_get_device(
+ {("nest", DEVICE_ID)}, connections={}
+ )
+
+ assert await setup_automation(hass, device_entry.id, "camera_motion")
+
+ # Simulate a pubsub message received by the subscriber with a motion event
+ event = EventMessage(
+ {
+ "eventId": "some-event-id",
+ "timestamp": "2019-01-01T00:00:01Z",
+ "resourceUpdate": {
+ "name": DEVICE_ID,
+ "events": {
+ "sdm.devices.events.CameraMotion.Motion": {
+ "eventSessionId": "CjY5Y3VKaTZwR3o4Y19YbTVfMF...",
+ "eventId": "FWWVQVUdGNUlTU2V4MGV2aTNXV...",
+ },
+ },
+ },
+ },
+ auth=None,
+ )
+ await subscriber.async_receive_event(event)
+ await hass.async_block_till_done()
+
+ assert len(calls) == 1
+ assert calls[0].data == DATA_MESSAGE
diff --git a/tests/components/nest/test_events.py b/tests/components/nest/test_events.py
new file mode 100644
index 00000000000..12314f60561
--- /dev/null
+++ b/tests/components/nest/test_events.py
@@ -0,0 +1,237 @@
+"""Test for Nest binary sensor platform for the Smart Device Management API.
+
+These tests fake out the subscriber/devicemanager, and are not using a real
+pubsub subscriber.
+"""
+
+from google_nest_sdm.device import Device
+from google_nest_sdm.event import EventMessage
+
+from homeassistant.util.dt import utcnow
+
+from .common import async_setup_sdm_platform
+
+from tests.common import async_capture_events
+
+DOMAIN = "nest"
+DEVICE_ID = "some-device-id"
+PLATFORM = "camera"
+NEST_EVENT = "nest_event"
+EVENT_SESSION_ID = "CjY5Y3VKaTZwR3o4Y19YbTVfMF..."
+EVENT_ID = "FWWVQVUdGNUlTU2V4MGV2aTNXV..."
+
+
+async def async_setup_devices(hass, device_type, traits={}):
+ """Set up the platform and prerequisites."""
+ devices = {
+ DEVICE_ID: Device.MakeDevice(
+ {
+ "name": DEVICE_ID,
+ "type": device_type,
+ "traits": traits,
+ },
+ auth=None,
+ ),
+ }
+ return await async_setup_sdm_platform(hass, PLATFORM, devices=devices)
+
+
+def create_device_traits(event_trait):
+ """Create fake traits for a device."""
+ return {
+ "sdm.devices.traits.Info": {
+ "customName": "Front",
+ },
+ event_trait: {},
+ "sdm.devices.traits.CameraLiveStream": {
+ "maxVideoResolution": {
+ "width": 640,
+ "height": 480,
+ },
+ "videoCodecs": ["H264"],
+ "audioCodecs": ["AAC"],
+ },
+ }
+
+
+def create_event(event_type, device_id=DEVICE_ID):
+ """Create an EventMessage for a single event type."""
+ events = {
+ event_type: {
+ "eventSessionId": EVENT_SESSION_ID,
+ "eventId": EVENT_ID,
+ },
+ }
+ return create_events(events=events, device_id=device_id)
+
+
+def create_events(events, device_id=DEVICE_ID):
+ """Create an EventMessage for events."""
+ return EventMessage(
+ {
+ "eventId": "some-event-id",
+ "timestamp": utcnow().isoformat(timespec="seconds"),
+ "resourceUpdate": {
+ "name": device_id,
+ "events": events,
+ },
+ },
+ auth=None,
+ )
+
+
+async def test_doorbell_chime_event(hass):
+ """Test a pubsub message for a doorbell event."""
+ events = async_capture_events(hass, NEST_EVENT)
+ subscriber = await async_setup_devices(
+ hass,
+ "sdm.devices.types.DOORBELL",
+ create_device_traits("sdm.devices.traits.DoorbellChime"),
+ )
+
+ registry = await hass.helpers.entity_registry.async_get_registry()
+ entry = registry.async_get("camera.front")
+ assert entry is not None
+ assert entry.unique_id == "some-device-id-camera"
+ assert entry.original_name == "Front"
+ assert entry.domain == "camera"
+
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+ device = device_registry.async_get(entry.device_id)
+ assert device.name == "Front"
+ assert device.model == "Doorbell"
+ assert device.identifiers == {("nest", DEVICE_ID)}
+
+ await subscriber.async_receive_event(
+ create_event("sdm.devices.events.DoorbellChime.Chime")
+ )
+ await hass.async_block_till_done()
+
+ assert len(events) == 1
+ assert events[0].data == {
+ "device_id": entry.device_id,
+ "type": "doorbell_chime",
+ }
+
+
+async def test_camera_motion_event(hass):
+ """Test a pubsub message for a camera motion event."""
+ events = async_capture_events(hass, NEST_EVENT)
+ subscriber = await async_setup_devices(
+ hass,
+ "sdm.devices.types.CAMERA",
+ create_device_traits("sdm.devices.traits.CameraMotion"),
+ )
+ registry = await hass.helpers.entity_registry.async_get_registry()
+ entry = registry.async_get("camera.front")
+ assert entry is not None
+
+ await subscriber.async_receive_event(
+ create_event("sdm.devices.events.CameraMotion.Motion")
+ )
+ await hass.async_block_till_done()
+
+ assert len(events) == 1
+ assert events[0].data == {
+ "device_id": entry.device_id,
+ "type": "camera_motion",
+ }
+
+
+async def test_camera_sound_event(hass):
+ """Test a pubsub message for a camera sound event."""
+ events = async_capture_events(hass, NEST_EVENT)
+ subscriber = await async_setup_devices(
+ hass,
+ "sdm.devices.types.CAMERA",
+ create_device_traits("sdm.devices.traits.CameraSound"),
+ )
+ registry = await hass.helpers.entity_registry.async_get_registry()
+ entry = registry.async_get("camera.front")
+ assert entry is not None
+
+ await subscriber.async_receive_event(
+ create_event("sdm.devices.events.CameraSound.Sound")
+ )
+ await hass.async_block_till_done()
+
+ assert len(events) == 1
+ assert events[0].data == {
+ "device_id": entry.device_id,
+ "type": "camera_sound",
+ }
+
+
+async def test_camera_person_event(hass):
+ """Test a pubsub message for a camera person event."""
+ events = async_capture_events(hass, NEST_EVENT)
+ subscriber = await async_setup_devices(
+ hass,
+ "sdm.devices.types.DOORBELL",
+ create_device_traits("sdm.devices.traits.CameraEventImage"),
+ )
+ registry = await hass.helpers.entity_registry.async_get_registry()
+ entry = registry.async_get("camera.front")
+ assert entry is not None
+
+ await subscriber.async_receive_event(
+ create_event("sdm.devices.events.CameraPerson.Person")
+ )
+ await hass.async_block_till_done()
+
+ assert len(events) == 1
+ assert events[0].data == {
+ "device_id": entry.device_id,
+ "type": "camera_person",
+ }
+
+
+async def test_camera_multiple_event(hass):
+ """Test a pubsub message for a camera person event."""
+ events = async_capture_events(hass, NEST_EVENT)
+ subscriber = await async_setup_devices(
+ hass,
+ "sdm.devices.types.DOORBELL",
+ create_device_traits("sdm.devices.traits.CameraEventImage"),
+ )
+ registry = await hass.helpers.entity_registry.async_get_registry()
+ entry = registry.async_get("camera.front")
+ assert entry is not None
+
+ event_map = {
+ "sdm.devices.events.CameraMotion.Motion": {
+ "eventSessionId": EVENT_SESSION_ID,
+ "eventId": EVENT_ID,
+ },
+ "sdm.devices.events.CameraPerson.Person": {
+ "eventSessionId": EVENT_SESSION_ID,
+ "eventId": EVENT_ID,
+ },
+ }
+
+ await subscriber.async_receive_event(create_events(event_map))
+ await hass.async_block_till_done()
+
+ assert len(events) == 2
+ assert events[0].data == {
+ "device_id": entry.device_id,
+ "type": "camera_motion",
+ }
+ assert events[1].data == {
+ "device_id": entry.device_id,
+ "type": "camera_person",
+ }
+
+
+async def test_unknown_event(hass):
+ """Test a pubsub message for an unknown event type."""
+ events = async_capture_events(hass, NEST_EVENT)
+ subscriber = await async_setup_devices(
+ hass,
+ "sdm.devices.types.DOORBELL",
+ create_device_traits("sdm.devices.traits.DoorbellChime"),
+ )
+ await subscriber.async_receive_event(create_event("some-event-id"))
+ await hass.async_block_till_done()
+
+ assert len(events) == 0
diff --git a/tests/components/netatmo/test_config_flow.py b/tests/components/netatmo/test_config_flow.py
index 8cee7a8c750..74a5d8dcc92 100644
--- a/tests/components/netatmo/test_config_flow.py
+++ b/tests/components/netatmo/test_config_flow.py
@@ -42,7 +42,9 @@ async def test_abort_if_existing_entry(hass):
assert result["reason"] == "already_configured"
-async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request):
+async def test_full_flow(
+ hass, aiohttp_client, aioclient_mock, current_request_with_host
+):
"""Check full flow."""
assert await setup.async_setup_component(
hass,
@@ -56,7 +58,13 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request):
result = await hass.config_entries.flow.async_init(
"netatmo", context={"source": config_entries.SOURCE_USER}
)
- state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]})
+ state = config_entry_oauth2_flow._encode_jwt(
+ hass,
+ {
+ "flow_id": result["flow_id"],
+ "redirect_uri": "https://example.com/auth/external/callback",
+ },
+ )
scope = "+".join(
[
diff --git a/tests/components/number/__init__.py b/tests/components/number/__init__.py
new file mode 100644
index 00000000000..e2e32e7a355
--- /dev/null
+++ b/tests/components/number/__init__.py
@@ -0,0 +1 @@
+"""The tests for Number integration."""
diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py
new file mode 100644
index 00000000000..6037bde5afd
--- /dev/null
+++ b/tests/components/number/test_init.py
@@ -0,0 +1,39 @@
+"""The tests for the Number component."""
+from unittest.mock import MagicMock
+
+from homeassistant.components.number import NumberEntity
+
+
+class MockNumberEntity(NumberEntity):
+ """Mock NumberEntity device to use in tests."""
+
+ @property
+ def max_value(self) -> float:
+ """Return the max value."""
+ return 1.0
+
+ @property
+ def state(self):
+ """Return the current value."""
+ return "0.5"
+
+
+async def test_step(hass):
+ """Test the step calculation."""
+ number = NumberEntity()
+ assert number.step == 1.0
+
+ number_2 = MockNumberEntity()
+ assert number_2.step == 0.1
+
+
+async def test_sync_set_value(hass):
+ """Test if async set_value calls sync set_value."""
+ number = NumberEntity()
+ number.hass = hass
+
+ number.set_value = MagicMock()
+ await number.async_set_value(42)
+
+ assert number.set_value.called
+ assert number.set_value.call_args[0][0] == 42
diff --git a/tests/components/ovo_energy/test_config_flow.py b/tests/components/ovo_energy/test_config_flow.py
index 57192933572..a7f6ea9b9f2 100644
--- a/tests/components/ovo_energy/test_config_flow.py
+++ b/tests/components/ovo_energy/test_config_flow.py
@@ -7,9 +7,13 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from tests.async_mock import patch
+from tests.common import MockConfigEntry
+FIXTURE_REAUTH_INPUT = {CONF_PASSWORD: "something1"}
FIXTURE_USER_INPUT = {CONF_USERNAME: "example@example.com", CONF_PASSWORD: "something"}
+UNIQUE_ID = "example@example.com"
+
async def test_show_form(hass: HomeAssistant) -> None:
"""Test that the setup form is served."""
@@ -94,3 +98,87 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None:
assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result2["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME]
assert result2["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD]
+
+
+async def test_reauth_authorization_error(hass: HomeAssistant) -> None:
+ """Test we show user form on authorization error."""
+ with patch(
+ "homeassistant.components.ovo_energy.config_flow.OVOEnergy.authenticate",
+ return_value=False,
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "reauth"
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ FIXTURE_REAUTH_INPUT,
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["step_id"] == "reauth"
+ assert result2["errors"] == {"base": "authorization_error"}
+
+
+async def test_reauth_connection_error(hass: HomeAssistant) -> None:
+ """Test we show user form on connection error."""
+ with patch(
+ "homeassistant.components.ovo_energy.config_flow.OVOEnergy.authenticate",
+ side_effect=aiohttp.ClientError,
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "reauth"
+
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ FIXTURE_REAUTH_INPUT,
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result2["step_id"] == "reauth"
+ assert result2["errors"] == {"base": "connection_error"}
+
+
+async def test_reauth_flow(hass: HomeAssistant) -> None:
+ """Test reauth works."""
+ with patch(
+ "homeassistant.components.ovo_energy.config_flow.OVOEnergy.authenticate",
+ return_value=False,
+ ):
+ mock_config = MockConfigEntry(
+ domain=DOMAIN, unique_id=UNIQUE_ID, data=FIXTURE_USER_INPUT
+ )
+ mock_config.add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT
+ )
+
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "reauth"
+ assert result["errors"] == {"base": "authorization_error"}
+
+ with patch(
+ "homeassistant.components.ovo_energy.config_flow.OVOEnergy.authenticate",
+ return_value=True,
+ ), patch(
+ "homeassistant.components.ovo_energy.config_flow.OVOEnergy.username",
+ return_value=FIXTURE_USER_INPUT[CONF_USERNAME],
+ ):
+ result2 = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ FIXTURE_REAUTH_INPUT,
+ )
+ await hass.async_block_till_done()
+
+ assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result2["reason"] == "reauth_successful"
diff --git a/tests/components/ozw/conftest.py b/tests/components/ozw/conftest.py
index f2518fb8007..d3f8288658c 100644
--- a/tests/components/ozw/conftest.py
+++ b/tests/components/ozw/conftest.py
@@ -253,3 +253,12 @@ def mock_uninstall_addon():
"homeassistant.components.hassio.async_uninstall_addon"
) as uninstall_addon:
yield uninstall_addon
+
+
+@pytest.fixture(name="get_addon_discovery_info")
+def mock_get_addon_discovery_info():
+ """Mock get add-on discovery info."""
+ with patch(
+ "homeassistant.components.hassio.async_get_addon_discovery_info"
+ ) as get_addon_discovery_info:
+ yield get_addon_discovery_info
diff --git a/tests/components/ozw/test_config_flow.py b/tests/components/ozw/test_config_flow.py
index 7561244999d..e86232adc65 100644
--- a/tests/components/ozw/test_config_flow.py
+++ b/tests/components/ozw/test_config_flow.py
@@ -9,6 +9,14 @@ from homeassistant.components.ozw.const import DOMAIN
from tests.async_mock import patch
from tests.common import MockConfigEntry
+ADDON_DISCOVERY_INFO = {
+ "addon": "OpenZWave",
+ "host": "host1",
+ "port": 1234,
+ "username": "name1",
+ "password": "pass1",
+}
+
@pytest.fixture(name="supervisor")
def mock_supervisor_fixture():
@@ -44,7 +52,7 @@ def mock_addon_installed(addon_info):
def mock_addon_options(addon_info):
"""Mock add-on options."""
addon_info.return_value["options"] = {}
- return addon_info
+ return addon_info.return_value["options"]
@pytest.fixture(name="set_addon_options")
@@ -151,9 +159,10 @@ async def test_not_addon(hass, supervisor):
assert len(mock_setup_entry.mock_calls) == 1
-async def test_addon_running(hass, supervisor, addon_running):
+async def test_addon_running(hass, supervisor, addon_running, addon_options):
"""Test add-on already running on Supervisor."""
- hass.config.components.add("mqtt")
+ addon_options["device"] = "/test"
+ addon_options["network_key"] = "abc123"
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
@@ -174,8 +183,8 @@ async def test_addon_running(hass, supervisor, addon_running):
assert result["type"] == "create_entry"
assert result["title"] == TITLE
assert result["data"] == {
- "usb_path": None,
- "network_key": None,
+ "usb_path": "/test",
+ "network_key": "abc123",
"use_addon": True,
"integration_created_addon": False,
}
@@ -185,7 +194,6 @@ async def test_addon_running(hass, supervisor, addon_running):
async def test_addon_info_failure(hass, supervisor, addon_info):
"""Test add-on info failure."""
- hass.config.components.add("mqtt")
addon_info.side_effect = HassioAPIError()
await setup.async_setup_component(hass, "persistent_notification", {})
@@ -205,7 +213,6 @@ async def test_addon_installed(
hass, supervisor, addon_installed, addon_options, set_addon_options, start_addon
):
"""Test add-on already installed but not running on Supervisor."""
- hass.config.components.add("mqtt")
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
@@ -242,7 +249,6 @@ async def test_set_addon_config_failure(
hass, supervisor, addon_installed, addon_options, set_addon_options
):
"""Test add-on set config failure."""
- hass.config.components.add("mqtt")
set_addon_options.side_effect = HassioAPIError()
await setup.async_setup_component(hass, "persistent_notification", {})
@@ -265,7 +271,6 @@ async def test_start_addon_failure(
hass, supervisor, addon_installed, addon_options, set_addon_options, start_addon
):
"""Test add-on start failure."""
- hass.config.components.add("mqtt")
start_addon.side_effect = HassioAPIError()
await setup.async_setup_component(hass, "persistent_notification", {})
@@ -294,7 +299,6 @@ async def test_addon_not_installed(
start_addon,
):
"""Test add-on not installed."""
- hass.config.components.add("mqtt")
addon_installed.return_value["version"] = None
await setup.async_setup_component(hass, "persistent_notification", {})
@@ -305,6 +309,16 @@ async def test_addon_not_installed(
result["flow_id"], {"use_addon": True}
)
+ assert result["type"] == "progress"
+
+ # Make sure the flow continues when the progress task is done.
+ await hass.async_block_till_done()
+
+ result = await hass.config_entries.flow.async_configure(result["flow_id"])
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "start_addon"
+
with patch(
"homeassistant.components.ozw.async_setup", return_value=True
) as mock_setup, patch(
@@ -330,7 +344,6 @@ async def test_addon_not_installed(
async def test_install_addon_failure(hass, supervisor, addon_installed, install_addon):
"""Test add-on install failure."""
- hass.config.components.add("mqtt")
addon_installed.return_value["version"] = None
install_addon.side_effect = HassioAPIError()
await setup.async_setup_component(hass, "persistent_notification", {})
@@ -342,5 +355,182 @@ async def test_install_addon_failure(hass, supervisor, addon_installed, install_
result["flow_id"], {"use_addon": True}
)
+ assert result["type"] == "progress"
+
+ # Make sure the flow continues when the progress task is done.
+ await hass.async_block_till_done()
+
+ result = await hass.config_entries.flow.async_configure(result["flow_id"])
+
assert result["type"] == "abort"
assert result["reason"] == "addon_install_failed"
+
+
+async def test_supervisor_discovery(hass, supervisor, addon_running, addon_options):
+ """Test flow started from Supervisor discovery."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ addon_options["device"] = "/test"
+ addon_options["network_key"] = "abc123"
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_HASSIO},
+ data=ADDON_DISCOVERY_INFO,
+ )
+
+ with patch(
+ "homeassistant.components.ozw.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.ozw.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
+ await hass.async_block_till_done()
+
+ assert result["type"] == "create_entry"
+ assert result["title"] == TITLE
+ assert result["data"] == {
+ "usb_path": "/test",
+ "network_key": "abc123",
+ "use_addon": True,
+ "integration_created_addon": False,
+ }
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_clean_discovery_on_user_create(
+ hass, supervisor, addon_running, addon_options
+):
+ """Test discovery flow is cleaned up when a user flow is finished."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ addon_options["device"] = "/test"
+ addon_options["network_key"] = "abc123"
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_HASSIO},
+ data=ADDON_DISCOVERY_INFO,
+ )
+
+ assert result["type"] == "form"
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ with patch(
+ "homeassistant.components.ozw.async_setup", return_value=True
+ ) as mock_setup, patch(
+ "homeassistant.components.ozw.async_setup_entry",
+ return_value=True,
+ ) as mock_setup_entry:
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"], {"use_addon": False}
+ )
+ await hass.async_block_till_done()
+
+ assert len(hass.config_entries.flow.async_progress()) == 0
+ assert result["type"] == "create_entry"
+ assert result["title"] == TITLE
+ assert result["data"] == {
+ "usb_path": None,
+ "network_key": None,
+ "use_addon": False,
+ "integration_created_addon": False,
+ }
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+async def test_abort_discovery_with_user_flow(
+ hass, supervisor, addon_running, addon_options
+):
+ """Test discovery flow is aborted if a user flow is in progress."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_HASSIO},
+ data=ADDON_DISCOVERY_INFO,
+ )
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "already_in_progress"
+ assert len(hass.config_entries.flow.async_progress()) == 1
+
+
+async def test_abort_discovery_with_existing_entry(
+ hass, supervisor, addon_running, addon_options
+):
+ """Test discovery flow is aborted if an entry already exists."""
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ entry = MockConfigEntry(domain=DOMAIN, data={}, title=TITLE, unique_id=DOMAIN)
+ entry.add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_HASSIO},
+ data=ADDON_DISCOVERY_INFO,
+ )
+
+ assert result["type"] == "abort"
+ assert result["reason"] == "already_configured"
+
+
+async def test_discovery_addon_not_running(
+ hass, supervisor, addon_installed, addon_options, set_addon_options, start_addon
+):
+ """Test discovery with add-on already installed but not running."""
+ addon_options["device"] = None
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_HASSIO},
+ data=ADDON_DISCOVERY_INFO,
+ )
+
+ assert result["step_id"] == "hassio_confirm"
+ assert result["type"] == "form"
+
+ result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
+
+ assert result["step_id"] == "start_addon"
+ assert result["type"] == "form"
+
+
+async def test_discovery_addon_not_installed(
+ hass, supervisor, addon_installed, install_addon, addon_options
+):
+ """Test discovery with add-on not installed."""
+ addon_installed.return_value["version"] = None
+ await setup.async_setup_component(hass, "persistent_notification", {})
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN,
+ context={"source": config_entries.SOURCE_HASSIO},
+ data=ADDON_DISCOVERY_INFO,
+ )
+
+ assert result["step_id"] == "hassio_confirm"
+ assert result["type"] == "form"
+
+ result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
+
+ assert result["step_id"] == "install_addon"
+ assert result["type"] == "progress"
+
+ await hass.async_block_till_done()
+
+ result = await hass.config_entries.flow.async_configure(result["flow_id"])
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "start_addon"
diff --git a/tests/components/ozw/test_init.py b/tests/components/ozw/test_init.py
index bf1eefe866a..efc38fa63c2 100644
--- a/tests/components/ozw/test_init.py
+++ b/tests/components/ozw/test_init.py
@@ -5,6 +5,7 @@ from homeassistant.components.ozw import DOMAIN, PLATFORMS, const
from .common import setup_ozw
+from tests.async_mock import patch
from tests.common import MockConfigEntry
@@ -23,6 +24,18 @@ async def test_init_entry(hass, generic_data):
assert hass.services.has_service(DOMAIN, const.SERVICE_REMOVE_NODE)
+async def test_setup_entry_without_mqtt(hass):
+ """Test setting up config entry without mqtt integration setup."""
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ title="OpenZWave",
+ connection_class=config_entries.CONN_CLASS_LOCAL_PUSH,
+ )
+ entry.add_to_hass(hass)
+
+ assert not await hass.config_entries.async_setup(entry.entry_id)
+
+
async def test_unload_entry(hass, generic_data, switch_msg, caplog):
"""Test unload the config entry."""
entry = MockConfigEntry(
@@ -128,3 +141,75 @@ async def test_remove_entry(hass, stop_addon, uninstall_addon, caplog):
assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED
assert len(hass.config_entries.async_entries(DOMAIN)) == 0
assert "Failed to uninstall the OpenZWave add-on" in caplog.text
+
+
+async def test_setup_entry_with_addon(hass, get_addon_discovery_info):
+ """Test set up entry using OpenZWave add-on."""
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ title="OpenZWave",
+ connection_class=config_entries.CONN_CLASS_LOCAL_PUSH,
+ data={"use_addon": True},
+ )
+ entry.add_to_hass(hass)
+
+ with patch("homeassistant.components.ozw.MQTTClient", autospec=True) as mock_client:
+ assert await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert mock_client.return_value.start_client.call_count == 1
+
+ # Verify integration + platform loaded.
+ assert "ozw" in hass.config.components
+ for platform in PLATFORMS:
+ assert platform in hass.config.components, platform
+ assert f"{platform}.{DOMAIN}" in hass.config.components, f"{platform}.{DOMAIN}"
+
+ # Verify services registered
+ assert hass.services.has_service(DOMAIN, const.SERVICE_ADD_NODE)
+ assert hass.services.has_service(DOMAIN, const.SERVICE_REMOVE_NODE)
+
+
+async def test_setup_entry_without_addon_info(hass, get_addon_discovery_info):
+ """Test set up entry using OpenZWave add-on but missing discovery info."""
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ title="OpenZWave",
+ connection_class=config_entries.CONN_CLASS_LOCAL_PUSH,
+ data={"use_addon": True},
+ )
+ entry.add_to_hass(hass)
+
+ get_addon_discovery_info.return_value = None
+
+ with patch("homeassistant.components.ozw.MQTTClient", autospec=True) as mock_client:
+ assert not await hass.config_entries.async_setup(entry.entry_id)
+
+ assert mock_client.return_value.start_client.call_count == 0
+ assert entry.state == config_entries.ENTRY_STATE_SETUP_RETRY
+
+
+async def test_unload_entry_with_addon(
+ hass, get_addon_discovery_info, generic_data, switch_msg, caplog
+):
+ """Test unload the config entry using the OpenZWave add-on."""
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ title="OpenZWave",
+ connection_class=config_entries.CONN_CLASS_LOCAL_PUSH,
+ data={"use_addon": True},
+ )
+ entry.add_to_hass(hass)
+
+ assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED
+
+ with patch("homeassistant.components.ozw.MQTTClient", autospec=True) as mock_client:
+ assert await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert mock_client.return_value.start_client.call_count == 1
+ assert entry.state == config_entries.ENTRY_STATE_LOADED
+
+ await hass.config_entries.async_unload(entry.entry_id)
+
+ assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED
diff --git a/tests/components/ozw/test_scenes.py b/tests/components/ozw/test_scenes.py
index 2d776b7faf4..1c510d58a3c 100644
--- a/tests/components/ozw/test_scenes.py
+++ b/tests/components/ozw/test_scenes.py
@@ -86,3 +86,4 @@ async def test_scenes(hass, generic_data, sent_messages):
assert events[1].data["scene_id"] == 1
assert events[1].data["scene_label"] == "Scene 1"
assert events[1].data["scene_value_label"] == "Pressed 1 Time"
+ assert events[1].data["instance_id"] == 1
diff --git a/tests/components/ozw/test_websocket_api.py b/tests/components/ozw/test_websocket_api.py
index d4194b0a537..37c6d184c8e 100644
--- a/tests/components/ozw/test_websocket_api.py
+++ b/tests/components/ozw/test_websocket_api.py
@@ -28,6 +28,7 @@ from homeassistant.components.ozw.websocket_api import (
NODE_ID,
OZW_INSTANCE,
PARAMETER,
+ SCHEMA,
TYPE,
VALUE,
)
@@ -152,16 +153,17 @@ async def test_websocket_api(hass, generic_data, hass_ws_client):
# Test set config parameter
config_param = result[0]
+ print(config_param)
current_val = config_param[ATTR_VALUE]
new_val = next(
- option["Value"]
- for option in config_param[ATTR_OPTIONS]
- if option["Label"] != current_val
+ option[0]
+ for option in config_param[SCHEMA][0][ATTR_OPTIONS]
+ if option[0] != current_val
)
new_label = next(
- option["Label"]
- for option in config_param[ATTR_OPTIONS]
- if option["Label"] != current_val and option["Value"] != new_val
+ option[1]
+ for option in config_param[SCHEMA][0][ATTR_OPTIONS]
+ if option[1] != current_val and option[0] != new_val
)
await client.send_json(
{
diff --git a/tests/components/plex/conftest.py b/tests/components/plex/conftest.py
index b3fc235bfc8..1838df32a05 100644
--- a/tests/components/plex/conftest.py
+++ b/tests/components/plex/conftest.py
@@ -4,6 +4,7 @@ import pytest
from homeassistant.components.plex.const import DOMAIN
from .const import DEFAULT_DATA, DEFAULT_OPTIONS
+from .helpers import websocket_connected
from .mock_classes import MockGDM, MockPlexAccount, MockPlexServer
from tests.async_mock import patch
@@ -52,6 +53,8 @@ def setup_plex_server(hass, entry, mock_plex_account, mock_websocket):
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
+ websocket_connected(mock_websocket)
+ await hass.async_block_till_done()
return plex_server
return _wrapper
diff --git a/tests/components/plex/helpers.py b/tests/components/plex/helpers.py
index a20d70fbb7e..2fca88fae27 100644
--- a/tests/components/plex/helpers.py
+++ b/tests/components/plex/helpers.py
@@ -1,8 +1,39 @@
"""Helper methods for Plex tests."""
-from plexwebsocket import SIGNAL_DATA
+from datetime import timedelta
+
+from plexwebsocket import SIGNAL_CONNECTION_STATE, SIGNAL_DATA, STATE_CONNECTED
+
+import homeassistant.util.dt as dt_util
+
+from tests.common import async_fire_time_changed
+
+UPDATE_PAYLOAD = {
+ "PlaySessionStateNotification": [
+ {
+ "sessionKey": "999",
+ "ratingKey": "12345",
+ "viewOffset": 5050,
+ "playQueueItemID": 54321,
+ "state": "playing",
+ }
+ ]
+}
-def trigger_plex_update(mock_websocket):
- """Call the websocket callback method."""
+def websocket_connected(mock_websocket):
+ """Call the websocket callback method to signal successful connection."""
callback = mock_websocket.call_args[0][1]
- callback(SIGNAL_DATA, None, None)
+ callback(SIGNAL_CONNECTION_STATE, STATE_CONNECTED, None)
+
+
+def trigger_plex_update(mock_websocket, payload=UPDATE_PAYLOAD):
+ """Call the websocket callback method with a Plex update."""
+ callback = mock_websocket.call_args[0][1]
+ callback(SIGNAL_DATA, payload, None)
+
+
+async def wait_for_debouncer(hass):
+ """Move time forward to wait for sensor debouncer."""
+ next_update = dt_util.utcnow() + timedelta(seconds=3)
+ async_fire_time_changed(hass, next_update)
+ await hass.async_block_till_done()
diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py
index 13fe4c4113b..8ac894438be 100644
--- a/tests/components/plex/mock_classes.py
+++ b/tests/components/plex/mock_classes.py
@@ -334,6 +334,7 @@ class MockPlexSession:
self.usernames = [list(MOCK_USERS)[index]]
self.players = [player]
self._section = MockPlexLibrarySection("Movies")
+ self.sessionKey = index + 1
@property
def duration(self):
diff --git a/tests/components/plex/test_browse_media.py b/tests/components/plex/test_browse_media.py
index d96fdd4a00b..66cbc51ef82 100644
--- a/tests/components/plex/test_browse_media.py
+++ b/tests/components/plex/test_browse_media.py
@@ -8,16 +8,12 @@ from homeassistant.components.plex.media_browser import SPECIAL_METHODS
from homeassistant.components.websocket_api.const import ERR_UNKNOWN_ERROR, TYPE_RESULT
from .const import DEFAULT_DATA
-from .helpers import trigger_plex_update
async def test_browse_media(hass, hass_ws_client, mock_plex_server, mock_websocket):
"""Test getting Plex clients from plex.tv."""
websocket_client = await hass_ws_client(hass)
- trigger_plex_update(mock_websocket)
- await hass.async_block_till_done()
-
media_players = hass.states.async_entity_ids("media_player")
msg_id = 1
diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py
index a7a2896d307..d8c010ceb92 100644
--- a/tests/components/plex/test_config_flow.py
+++ b/tests/components/plex/test_config_flow.py
@@ -36,7 +36,7 @@ from homeassistant.const import (
)
from .const import DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN
-from .helpers import trigger_plex_update
+from .helpers import trigger_plex_update, wait_for_debouncer
from .mock_classes import (
MockGDM,
MockPlexAccount,
@@ -440,10 +440,10 @@ async def test_option_flow_new_users_available(
OPTIONS_OWNER_ONLY[MP_DOMAIN][CONF_MONITORED_USERS] = {"Owner": {"enabled": True}}
entry.options = OPTIONS_OWNER_ONLY
- mock_plex_server = await setup_plex_server(config_entry=entry, disable_gdm=False)
-
with patch("homeassistant.components.plex.server.PlexClient", new=MockPlexClient):
- trigger_plex_update(mock_websocket)
+ mock_plex_server = await setup_plex_server(
+ config_entry=entry, disable_gdm=False
+ )
await hass.async_block_till_done()
server_id = mock_plex_server.machineIdentifier
@@ -453,6 +453,8 @@ async def test_option_flow_new_users_available(
assert len(monitored_users) == 1
assert len(new_users) == 2
+ await wait_for_debouncer(hass)
+
sensor = hass.states.get("sensor.plex_plex_server_1")
assert sensor.state == str(len(mock_plex_server.accounts))
@@ -754,7 +756,7 @@ async def test_trigger_reauth(hass, entry, mock_plex_server, mock_websocket):
mock_plex_server, "clients", side_effect=plexapi.exceptions.Unauthorized
), patch("plexapi.server.PlexServer", side_effect=plexapi.exceptions.Unauthorized):
trigger_plex_update(mock_websocket)
- await hass.async_block_till_done()
+ await wait_for_debouncer(hass)
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert entry.state != ENTRY_STATE_LOADED
diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py
index 13e33791459..a1a159010ef 100644
--- a/tests/components/plex/test_init.py
+++ b/tests/components/plex/test_init.py
@@ -13,11 +13,11 @@ from homeassistant.config_entries import (
ENTRY_STATE_SETUP_ERROR,
ENTRY_STATE_SETUP_RETRY,
)
-from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL
+from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL, STATE_IDLE
import homeassistant.util.dt as dt_util
from .const import DEFAULT_DATA, DEFAULT_OPTIONS
-from .helpers import trigger_plex_update
+from .helpers import trigger_plex_update, wait_for_debouncer
from .mock_classes import MockGDM, MockPlexAccount, MockPlexServer
from tests.async_mock import patch
@@ -91,19 +91,19 @@ async def test_unload_config_entry(hass, entry, mock_plex_server):
async def test_setup_with_photo_session(hass, entry, mock_websocket, setup_plex_server):
"""Test setup component with config."""
- mock_plex_server = await setup_plex_server(config_entry=entry, session_type="photo")
+ await setup_plex_server(session_type="photo")
assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1
assert entry.state == ENTRY_STATE_LOADED
-
- trigger_plex_update(mock_websocket)
await hass.async_block_till_done()
media_player = hass.states.get("media_player.plex_product_title")
- assert media_player.state == "idle"
+ assert media_player.state == STATE_IDLE
+
+ await wait_for_debouncer(hass)
sensor = hass.states.get("sensor.plex_plex_server_1")
- assert sensor.state == str(len(mock_plex_server.accounts))
+ assert sensor.state == "0"
async def test_setup_when_certificate_changed(hass, entry):
diff --git a/tests/components/plex/test_media_players.py b/tests/components/plex/test_media_players.py
index 94703d5dfb3..a4bda5467e2 100644
--- a/tests/components/plex/test_media_players.py
+++ b/tests/components/plex/test_media_players.py
@@ -8,45 +8,30 @@ from tests.async_mock import patch
async def test_plex_tv_clients(hass, entry, mock_plex_account, setup_plex_server):
"""Test getting Plex clients from plex.tv."""
- mock_plex_server = await setup_plex_server()
- server_id = mock_plex_server.machineIdentifier
- plex_server = hass.data[DOMAIN][SERVERS][server_id]
-
resource = next(
x
for x in mock_plex_account.resources()
if x.name.startswith("plex.tv Resource Player")
)
with patch.object(resource, "connect", side_effect=NotFound):
- await plex_server._async_update_platforms()
+ mock_plex_server = await setup_plex_server()
await hass.async_block_till_done()
+ server_id = mock_plex_server.machineIdentifier
+ plex_server = hass.data[DOMAIN][SERVERS][server_id]
media_players_before = len(hass.states.async_entity_ids("media_player"))
# Ensure one more client is discovered
await hass.config_entries.async_unload(entry.entry_id)
-
mock_plex_server = await setup_plex_server()
plex_server = hass.data[DOMAIN][SERVERS][server_id]
-
- await plex_server._async_update_platforms()
- await hass.async_block_till_done()
-
media_players_after = len(hass.states.async_entity_ids("media_player"))
assert media_players_after == media_players_before + 1
# Ensure only plex.tv resource client is found
await hass.config_entries.async_unload(entry.entry_id)
-
- mock_plex_server = await setup_plex_server()
- mock_plex_server.clear_clients()
- mock_plex_server.clear_sessions()
-
+ mock_plex_server = await setup_plex_server(num_users=0)
plex_server = hass.data[DOMAIN][SERVERS][server_id]
-
- await plex_server._async_update_platforms()
- await hass.async_block_till_done()
-
assert len(hass.states.async_entity_ids("media_player")) == 1
# Ensure cache gets called
diff --git a/tests/components/plex/test_server.py b/tests/components/plex/test_server.py
index 8ac707c393f..99a324786f6 100644
--- a/tests/components/plex/test_server.py
+++ b/tests/components/plex/test_server.py
@@ -26,9 +26,8 @@ from homeassistant.components.plex.const import (
from homeassistant.const import ATTR_ENTITY_ID
from .const import DEFAULT_DATA, DEFAULT_OPTIONS
-from .helpers import trigger_plex_update
+from .helpers import trigger_plex_update, wait_for_debouncer
from .mock_classes import (
- MockGDM,
MockPlexAccount,
MockPlexAlbum,
MockPlexArtist,
@@ -54,15 +53,14 @@ async def test_new_users_available(hass, entry, mock_websocket, setup_plex_serve
server_id = mock_plex_server.machineIdentifier
- trigger_plex_update(mock_websocket)
- await hass.async_block_till_done()
-
monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users
ignored_users = [x for x in monitored_users if not monitored_users[x]["enabled"]]
assert len(monitored_users) == 1
assert len(ignored_users) == 0
+ await wait_for_debouncer(hass)
+
sensor = hass.states.get("sensor.plex_plex_server_1")
assert sensor.state == str(len(mock_plex_server.accounts))
@@ -81,9 +79,6 @@ async def test_new_ignored_users_available(
server_id = mock_plex_server.machineIdentifier
- trigger_plex_update(mock_websocket)
- await hass.async_block_till_done()
-
monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users
ignored_users = [x for x in mock_plex_server.accounts if x not in monitored_users]
@@ -100,6 +95,8 @@ async def test_new_ignored_users_available(
in caplog.text
)
+ await wait_for_debouncer(hass)
+
sensor = hass.states.get("sensor.plex_plex_server_1")
assert sensor.state == str(len(mock_plex_server.accounts))
@@ -111,8 +108,7 @@ async def test_network_error_during_refresh(
server_id = mock_plex_server.machineIdentifier
loaded_server = hass.data[DOMAIN][SERVERS][server_id]
- trigger_plex_update(mock_websocket)
- await hass.async_block_till_done()
+ await wait_for_debouncer(hass)
sensor = hass.states.get("sensor.plex_plex_server_1")
assert sensor.state == str(len(mock_plex_server.accounts))
@@ -128,14 +124,14 @@ async def test_network_error_during_refresh(
async def test_gdm_client_failure(hass, mock_websocket, setup_plex_server):
"""Test connection failure to a GDM discovered client."""
- mock_plex_server = await setup_plex_server(disable_gdm=False)
-
with patch(
"homeassistant.components.plex.server.PlexClient", side_effect=ConnectionError
):
- trigger_plex_update(mock_websocket)
+ mock_plex_server = await setup_plex_server(disable_gdm=False)
await hass.async_block_till_done()
+ await wait_for_debouncer(hass)
+
sensor = hass.states.get("sensor.plex_plex_server_1")
assert sensor.state == str(len(mock_plex_server.accounts))
@@ -146,11 +142,7 @@ async def test_gdm_client_failure(hass, mock_websocket, setup_plex_server):
async def test_mark_sessions_idle(hass, mock_plex_server, mock_websocket):
"""Test marking media_players as idle when sessions end."""
- server_id = mock_plex_server.machineIdentifier
- loaded_server = hass.data[DOMAIN][SERVERS][server_id]
-
- trigger_plex_update(mock_websocket)
- await hass.async_block_till_done()
+ await wait_for_debouncer(hass)
sensor = hass.states.get("sensor.plex_plex_server_1")
assert sensor.state == str(len(mock_plex_server.accounts))
@@ -158,30 +150,23 @@ async def test_mark_sessions_idle(hass, mock_plex_server, mock_websocket):
mock_plex_server.clear_clients()
mock_plex_server.clear_sessions()
- await loaded_server._async_update_platforms()
+ trigger_plex_update(mock_websocket)
await hass.async_block_till_done()
+ await wait_for_debouncer(hass)
sensor = hass.states.get("sensor.plex_plex_server_1")
assert sensor.state == "0"
-async def test_ignore_plex_web_client(hass, entry, mock_websocket):
+async def test_ignore_plex_web_client(hass, entry, mock_websocket, setup_plex_server):
"""Test option to ignore Plex Web clients."""
OPTIONS = copy.deepcopy(DEFAULT_OPTIONS)
OPTIONS[MP_DOMAIN][CONF_IGNORE_PLEX_WEB_CLIENTS] = True
entry.options = OPTIONS
- mock_plex_server = MockPlexServer(config_entry=entry)
-
- with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch(
- "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(players=0)
- ), patch("homeassistant.components.plex.GDM", return_value=MockGDM(disabled=True)):
- entry.add_to_hass(hass)
- assert await hass.config_entries.async_setup(entry.entry_id)
- await hass.async_block_till_done()
-
- trigger_plex_update(mock_websocket)
- await hass.async_block_till_done()
+ with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(players=0)):
+ mock_plex_server = await setup_plex_server(config_entry=entry)
+ await wait_for_debouncer(hass)
sensor = hass.states.get("sensor.plex_plex_server_1")
assert sensor.state == str(len(mock_plex_server.accounts))
@@ -197,9 +182,6 @@ async def test_media_lookups(hass, mock_plex_server, mock_websocket):
loaded_server = hass.data[DOMAIN][SERVERS][server_id]
# Plex Key searches
- trigger_plex_update(mock_websocket)
- 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(
diff --git a/tests/components/plugwise/common.py b/tests/components/plugwise/common.py
index eb227322aa8..379929ce2f1 100644
--- a/tests/components/plugwise/common.py
+++ b/tests/components/plugwise/common.py
@@ -1,6 +1,6 @@
"""Common initialisation for the Plugwise integration."""
-from homeassistant.components.plugwise import DOMAIN
+from homeassistant.components.plugwise.const import DOMAIN
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
diff --git a/tests/components/plugwise/conftest.py b/tests/components/plugwise/conftest.py
index 0f0e4551b1c..ae934c565bc 100644
--- a/tests/components/plugwise/conftest.py
+++ b/tests/components/plugwise/conftest.py
@@ -3,8 +3,13 @@
from functools import partial
import re
-from Plugwise_Smile.Smile import Smile
import jsonpickle
+from plugwise.exceptions import (
+ ConnectionFailedError,
+ InvalidAuthentication,
+ PlugwiseException,
+ XMLDataMissingError,
+)
import pytest
from tests.async_mock import AsyncMock, Mock, patch
@@ -24,8 +29,8 @@ def mock_smile():
with patch(
"homeassistant.components.plugwise.config_flow.Smile",
) as smile_mock:
- smile_mock.InvalidAuthentication = Smile.InvalidAuthentication
- smile_mock.ConnectionFailedError = Smile.ConnectionFailedError
+ smile_mock.InvalidAuthentication = InvalidAuthentication
+ smile_mock.ConnectionFailedError = ConnectionFailedError
smile_mock.return_value.connect.return_value = True
yield smile_mock.return_value
@@ -48,9 +53,9 @@ def mock_smile_error(aioclient_mock: AiohttpClientMocker) -> None:
def mock_smile_notconnect():
"""Mock the Plugwise Smile general connection failure for Home Assistant."""
with patch("homeassistant.components.plugwise.gateway.Smile") as smile_mock:
- smile_mock.InvalidAuthentication = Smile.InvalidAuthentication
- smile_mock.ConnectionFailedError = Smile.ConnectionFailedError
- smile_mock.PlugwiseError = Smile.PlugwiseError
+ smile_mock.InvalidAuthentication = InvalidAuthentication
+ smile_mock.ConnectionFailedError = ConnectionFailedError
+ smile_mock.PlugwiseException = PlugwiseException
smile_mock.return_value.connect.side_effect = AsyncMock(return_value=False)
yield smile_mock.return_value
@@ -65,9 +70,9 @@ def mock_smile_adam():
"""Create a Mock Adam environment for testing exceptions."""
chosen_env = "adam_multiple_devices_per_zone"
with patch("homeassistant.components.plugwise.gateway.Smile") as smile_mock:
- smile_mock.InvalidAuthentication = Smile.InvalidAuthentication
- smile_mock.ConnectionFailedError = Smile.ConnectionFailedError
- smile_mock.XMLDataMissingError = Smile.XMLDataMissingError
+ smile_mock.InvalidAuthentication = InvalidAuthentication
+ smile_mock.ConnectionFailedError = ConnectionFailedError
+ smile_mock.XMLDataMissingError = XMLDataMissingError
smile_mock.return_value.gateway_id = "fe799307f1624099878210aa0b9f1475"
smile_mock.return_value.heater_id = "90986d591dcd426cae3ec3e8111ff730"
@@ -75,6 +80,8 @@ def mock_smile_adam():
smile_mock.return_value.smile_type = "thermostat"
smile_mock.return_value.smile_hostname = "smile98765"
+ smile_mock.return_value.notifications = _read_json(chosen_env, "notifications")
+
smile_mock.return_value.connect.side_effect = AsyncMock(return_value=True)
smile_mock.return_value.full_update_device.side_effect = AsyncMock(
return_value=True
@@ -108,9 +115,9 @@ def mock_smile_anna():
"""Create a Mock Anna environment for testing exceptions."""
chosen_env = "anna_heatpump"
with patch("homeassistant.components.plugwise.gateway.Smile") as smile_mock:
- smile_mock.InvalidAuthentication = Smile.InvalidAuthentication
- smile_mock.ConnectionFailedError = Smile.ConnectionFailedError
- smile_mock.XMLDataMissingError = Smile.XMLDataMissingError
+ smile_mock.InvalidAuthentication = InvalidAuthentication
+ smile_mock.ConnectionFailedError = ConnectionFailedError
+ smile_mock.XMLDataMissingError = XMLDataMissingError
smile_mock.return_value.gateway_id = "015ae9ea3f964e668e490fa39da3870b"
smile_mock.return_value.heater_id = "1cbf783bb11e4a7c8a6843dee3a86927"
@@ -118,6 +125,8 @@ def mock_smile_anna():
smile_mock.return_value.smile_type = "thermostat"
smile_mock.return_value.smile_hostname = "smile98765"
+ smile_mock.return_value.notifications = _read_json(chosen_env, "notifications")
+
smile_mock.return_value.connect.side_effect = AsyncMock(return_value=True)
smile_mock.return_value.full_update_device.side_effect = AsyncMock(
return_value=True
@@ -151,9 +160,9 @@ def mock_smile_p1():
"""Create a Mock P1 DSMR environment for testing exceptions."""
chosen_env = "p1v3_full_option"
with patch("homeassistant.components.plugwise.gateway.Smile") as smile_mock:
- smile_mock.InvalidAuthentication = Smile.InvalidAuthentication
- smile_mock.ConnectionFailedError = Smile.ConnectionFailedError
- smile_mock.XMLDataMissingError = Smile.XMLDataMissingError
+ smile_mock.InvalidAuthentication = InvalidAuthentication
+ smile_mock.ConnectionFailedError = ConnectionFailedError
+ smile_mock.XMLDataMissingError = XMLDataMissingError
smile_mock.return_value.gateway_id = "e950c7d5e1ee407a858e2a8b5016c8b3"
smile_mock.return_value.heater_id = None
@@ -161,6 +170,8 @@ def mock_smile_p1():
smile_mock.return_value.smile_type = "power"
smile_mock.return_value.smile_hostname = "smile98765"
+ smile_mock.return_value.notifications = _read_json(chosen_env, "notifications")
+
smile_mock.return_value.connect.side_effect = AsyncMock(return_value=True)
smile_mock.return_value.full_update_device.side_effect = AsyncMock(
return_value=True
@@ -185,9 +196,9 @@ def mock_stretch():
"""Create a Mock Stretch environment for testing exceptions."""
chosen_env = "stretch_v31"
with patch("homeassistant.components.plugwise.gateway.Smile") as smile_mock:
- smile_mock.InvalidAuthentication = Smile.InvalidAuthentication
- smile_mock.ConnectionFailedError = Smile.ConnectionFailedError
- smile_mock.XMLDataMissingError = Smile.XMLDataMissingError
+ smile_mock.InvalidAuthentication = InvalidAuthentication
+ smile_mock.ConnectionFailedError = ConnectionFailedError
+ smile_mock.XMLDataMissingError = XMLDataMissingError
smile_mock.return_value.gateway_id = "259882df3c05415b99c2d962534ce820"
smile_mock.return_value.heater_id = None
diff --git a/tests/components/plugwise/test_binary_sensor.py b/tests/components/plugwise/test_binary_sensor.py
index b2221194d8e..6df5b90878a 100644
--- a/tests/components/plugwise/test_binary_sensor.py
+++ b/tests/components/plugwise/test_binary_sensor.py
@@ -35,3 +35,15 @@ async def test_anna_climate_binary_sensor_change(hass, mock_smile_anna):
state = hass.states.get("binary_sensor.auxiliary_dhw_state")
assert str(state.state) == STATE_OFF
+
+
+async def test_adam_climate_binary_sensor_change(hass, mock_smile_adam):
+ """Test change of climate related binary_sensor entities."""
+ entry = await async_init_integration(hass, mock_smile_adam)
+ assert entry.state == ENTRY_STATE_LOADED
+
+ state = hass.states.get("binary_sensor.adam_plugwise_notification")
+ assert str(state.state) == STATE_ON
+ assert "unreachable" in state.attributes.get("warning_msg")[0]
+ assert not state.attributes.get("error_msg")
+ assert not state.attributes.get("other_msg")
diff --git a/tests/components/plugwise/test_climate.py b/tests/components/plugwise/test_climate.py
index 7c74d970d7e..e85140660fd 100644
--- a/tests/components/plugwise/test_climate.py
+++ b/tests/components/plugwise/test_climate.py
@@ -1,5 +1,8 @@
"""Tests for the Plugwise Climate integration."""
+from plugwise.exceptions import PlugwiseException
+
+from homeassistant.components.climate.const import HVAC_MODE_AUTO, HVAC_MODE_HEAT
from homeassistant.config_entries import ENTRY_STATE_LOADED
from tests.components.plugwise.common import async_init_integration
@@ -13,7 +16,7 @@ async def test_adam_climate_entity_attributes(hass, mock_smile_adam):
state = hass.states.get("climate.zone_lisa_wk")
attrs = state.attributes
- assert attrs["hvac_modes"] == ["heat", "auto"]
+ assert attrs["hvac_modes"] == [HVAC_MODE_HEAT, HVAC_MODE_AUTO]
assert "preset_modes" in attrs
assert "no_frost" in attrs["preset_modes"]
@@ -29,7 +32,7 @@ async def test_adam_climate_entity_attributes(hass, mock_smile_adam):
state = hass.states.get("climate.zone_thermostat_jessie")
attrs = state.attributes
- assert attrs["hvac_modes"] == ["heat", "auto"]
+ assert attrs["hvac_modes"] == [HVAC_MODE_HEAT, HVAC_MODE_AUTO]
assert "preset_modes" in attrs
assert "no_frost" in attrs["preset_modes"]
@@ -41,6 +44,44 @@ async def test_adam_climate_entity_attributes(hass, mock_smile_adam):
assert attrs["preset_mode"] == "asleep"
+async def test_adam_climate_adjust_negative_testing(hass, mock_smile_adam):
+ """Test exceptions of climate entities."""
+ mock_smile_adam.set_preset.side_effect = PlugwiseException
+ mock_smile_adam.set_schedule_state.side_effect = PlugwiseException
+ mock_smile_adam.set_temperature.side_effect = PlugwiseException
+ entry = await async_init_integration(hass, mock_smile_adam)
+ assert entry.state == ENTRY_STATE_LOADED
+
+ await hass.services.async_call(
+ "climate",
+ "set_temperature",
+ {"entity_id": "climate.zone_lisa_wk", "temperature": 25},
+ blocking=True,
+ )
+ state = hass.states.get("climate.zone_lisa_wk")
+ attrs = state.attributes
+ assert attrs["temperature"] == 21.5
+
+ await hass.services.async_call(
+ "climate",
+ "set_preset_mode",
+ {"entity_id": "climate.zone_thermostat_jessie", "preset_mode": "home"},
+ blocking=True,
+ )
+ state = hass.states.get("climate.zone_thermostat_jessie")
+ attrs = state.attributes
+ assert attrs["preset_mode"] == "asleep"
+
+ await hass.services.async_call(
+ "climate",
+ "set_hvac_mode",
+ {"entity_id": "climate.zone_thermostat_jessie", "hvac_mode": HVAC_MODE_AUTO},
+ blocking=True,
+ )
+ state = hass.states.get("climate.zone_thermostat_jessie")
+ attrs = state.attributes
+
+
async def test_adam_climate_entity_climate_changes(hass, mock_smile_adam):
"""Test handling of user requests in adam climate device environment."""
entry = await async_init_integration(hass, mock_smile_adam)
@@ -112,7 +153,7 @@ async def test_anna_climate_entity_attributes(hass, mock_smile_anna):
assert attrs["current_temperature"] == 23.3
assert attrs["temperature"] == 21.0
- assert state.state == "auto"
+ assert state.state == HVAC_MODE_AUTO
assert attrs["hvac_action"] == "idle"
assert attrs["preset_mode"] == "home"
diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py
index dea42dfb01d..fc0e5f9e69f 100644
--- a/tests/components/plugwise/test_config_flow.py
+++ b/tests/components/plugwise/test_config_flow.py
@@ -1,5 +1,9 @@
"""Test the Plugwise config flow."""
-from Plugwise_Smile.Smile import Smile
+from plugwise.exceptions import (
+ ConnectionFailedError,
+ InvalidAuthentication,
+ PlugwiseException,
+)
import pytest
from homeassistant import config_entries, data_entry_flow, setup
@@ -47,9 +51,9 @@ def mock_smile():
with patch(
"homeassistant.components.plugwise.config_flow.Smile",
) as smile_mock:
- smile_mock.PlugwiseError = Smile.PlugwiseError
- smile_mock.InvalidAuthentication = Smile.InvalidAuthentication
- smile_mock.ConnectionFailedError = Smile.ConnectionFailedError
+ smile_mock.PlugwiseError = PlugwiseException
+ smile_mock.InvalidAuthentication = InvalidAuthentication
+ smile_mock.ConnectionFailedError = ConnectionFailedError
smile_mock.return_value.connect.return_value = True
yield smile_mock.return_value
@@ -207,7 +211,7 @@ async def test_form_invalid_auth(hass, mock_smile):
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
- mock_smile.connect.side_effect = Smile.InvalidAuthentication
+ mock_smile.connect.side_effect = InvalidAuthentication
mock_smile.gateway_id = "0a636a4fc1704ab4a24e4f7e37fb187a"
result2 = await hass.config_entries.flow.async_configure(
@@ -225,7 +229,7 @@ async def test_form_cannot_connect(hass, mock_smile):
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
- mock_smile.connect.side_effect = Smile.ConnectionFailedError
+ mock_smile.connect.side_effect = ConnectionFailedError
mock_smile.gateway_id = "0a636a4fc1704ab4a24e4f7e37fb187a"
result2 = await hass.config_entries.flow.async_configure(
@@ -243,7 +247,7 @@ async def test_form_cannot_connect_port(hass, mock_smile):
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
- mock_smile.connect.side_effect = Smile.ConnectionFailedError
+ mock_smile.connect.side_effect = ConnectionFailedError
mock_smile.gateway_id = "0a636a4fc1704ab4a24e4f7e37fb187a"
result2 = await hass.config_entries.flow.async_configure(
diff --git a/tests/components/plugwise/test_init.py b/tests/components/plugwise/test_init.py
index d968f1825f0..eded1e55406 100644
--- a/tests/components/plugwise/test_init.py
+++ b/tests/components/plugwise/test_init.py
@@ -2,16 +2,16 @@
import asyncio
-from Plugwise_Smile.Smile import Smile
+from plugwise.exceptions import XMLDataMissingError
-from homeassistant.components.plugwise import DOMAIN
-from homeassistant.components.plugwise.gateway import async_unload_entry
+from homeassistant.components.plugwise.const import DOMAIN
from homeassistant.config_entries import (
+ ENTRY_STATE_NOT_LOADED,
ENTRY_STATE_SETUP_ERROR,
ENTRY_STATE_SETUP_RETRY,
)
-from tests.common import AsyncMock
+from tests.common import AsyncMock, MockConfigEntry
from tests.components.plugwise.common import async_init_integration
@@ -43,7 +43,7 @@ async def test_smile_timeout(hass, mock_smile_notconnect):
async def test_smile_adam_xmlerror(hass, mock_smile_adam):
"""Detect malformed XML by Smile in Adam environment."""
- mock_smile_adam.full_update_device.side_effect = Smile.XMLDataMissingError
+ mock_smile_adam.full_update_device.side_effect = XMLDataMissingError
entry = await async_init_integration(hass, mock_smile_adam)
assert entry.state == ENTRY_STATE_SETUP_RETRY
@@ -53,5 +53,17 @@ async def test_unload_entry(hass, mock_smile_adam):
entry = await async_init_integration(hass, mock_smile_adam)
mock_smile_adam.async_reset = AsyncMock(return_value=True)
- assert await async_unload_entry(hass, entry)
+ await hass.config_entries.async_unload(entry.entry_id)
+ await hass.async_block_till_done()
+ assert entry.state == ENTRY_STATE_NOT_LOADED
assert not hass.data[DOMAIN]
+
+
+async def test_async_setup_entry_fail(hass):
+ """Test async_setup_entry."""
+ entry = MockConfigEntry(domain=DOMAIN, data={})
+
+ entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+ assert entry.state == ENTRY_STATE_SETUP_ERROR
diff --git a/tests/components/plugwise/test_sensor.py b/tests/components/plugwise/test_sensor.py
index a727337d747..a722749496f 100644
--- a/tests/components/plugwise/test_sensor.py
+++ b/tests/components/plugwise/test_sensor.py
@@ -2,6 +2,7 @@
from homeassistant.config_entries import ENTRY_STATE_LOADED
+from tests.common import Mock
from tests.components.plugwise.common import async_init_integration
@@ -30,7 +31,7 @@ async def test_adam_climate_sensor_entities(hass, mock_smile_adam):
assert int(state.state) == 34
-async def test_anna_climate_sensor_entities(hass, mock_smile_anna):
+async def test_anna_as_smt_climate_sensor_entities(hass, mock_smile_anna):
"""Test creation of climate related sensor entities."""
entry = await async_init_integration(hass, mock_smile_anna)
assert entry.state == ENTRY_STATE_LOADED
@@ -45,6 +46,16 @@ async def test_anna_climate_sensor_entities(hass, mock_smile_anna):
assert float(state.state) == 86.0
+async def test_anna_climate_sensor_entities(hass, mock_smile_anna):
+ """Test creation of climate related sensor entities as single master thermostat."""
+ mock_smile_anna.single_master_thermostat.side_effect = Mock(return_value=False)
+ entry = await async_init_integration(hass, mock_smile_anna)
+ assert entry.state == ENTRY_STATE_LOADED
+
+ state = hass.states.get("sensor.auxiliary_outdoor_temperature")
+ assert float(state.state) == 18.0
+
+
async def test_p1_dsmr_sensor_entities(hass, mock_smile_p1):
"""Test creation of power related sensor entities."""
entry = await async_init_integration(hass, mock_smile_p1)
@@ -63,7 +74,7 @@ async def test_p1_dsmr_sensor_entities(hass, mock_smile_p1):
assert float(state.state) == 442.9
state = hass.states.get("sensor.p1_gas_consumed_cumulative")
- assert float(state.state) == 584.9
+ assert float(state.state) == 584.85
async def test_stretch_sensor_entities(hass, mock_stretch):
diff --git a/tests/components/plugwise/test_switch.py b/tests/components/plugwise/test_switch.py
index ded21113f2b..b7237a26150 100644
--- a/tests/components/plugwise/test_switch.py
+++ b/tests/components/plugwise/test_switch.py
@@ -1,5 +1,7 @@
"""Tests for the Plugwise switch integration."""
+from plugwise.exceptions import PlugwiseException
+
from homeassistant.config_entries import ENTRY_STATE_LOADED
from tests.components.plugwise.common import async_init_integration
@@ -17,6 +19,31 @@ async def test_adam_climate_switch_entities(hass, mock_smile_adam):
assert str(state.state) == "on"
+async def test_adam_climate_switch_negative_testing(hass, mock_smile_adam):
+ """Test exceptions of climate related switch entities."""
+ mock_smile_adam.set_relay_state.side_effect = PlugwiseException
+ entry = await async_init_integration(hass, mock_smile_adam)
+ assert entry.state == ENTRY_STATE_LOADED
+
+ await hass.services.async_call(
+ "switch",
+ "turn_off",
+ {"entity_id": "switch.cv_pomp"},
+ blocking=True,
+ )
+ state = hass.states.get("switch.cv_pomp")
+ assert str(state.state) == "on"
+
+ await hass.services.async_call(
+ "switch",
+ "turn_on",
+ {"entity_id": "switch.fibaro_hc2"},
+ blocking=True,
+ )
+ state = hass.states.get("switch.fibaro_hc2")
+ assert str(state.state) == "on"
+
+
async def test_adam_climate_switch_changes(hass, mock_smile_adam):
"""Test changing of climate related switch entities."""
entry = await async_init_integration(hass, mock_smile_adam)
diff --git a/tests/components/point/test_config_flow.py b/tests/components/point/test_config_flow.py
index b6c780e937a..67817b308ce 100644
--- a/tests/components/point/test_config_flow.py
+++ b/tests/components/point/test_config_flow.py
@@ -141,7 +141,7 @@ async def test_abort_if_exception_generating_auth_url(hass):
result = await flow.async_step_user()
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "authorize_url_fail"
+ assert result["reason"] == "unknown_authorize_url_generation"
async def test_abort_no_code(hass):
diff --git a/tests/components/recollect_waste/__init__.py b/tests/components/recollect_waste/__init__.py
new file mode 100644
index 00000000000..0357682f7f9
--- /dev/null
+++ b/tests/components/recollect_waste/__init__.py
@@ -0,0 +1 @@
+"""Define tests for the Recollet Waste integration."""
diff --git a/tests/components/recollect_waste/test_config_flow.py b/tests/components/recollect_waste/test_config_flow.py
new file mode 100644
index 00000000000..bec87b72ee4
--- /dev/null
+++ b/tests/components/recollect_waste/test_config_flow.py
@@ -0,0 +1,91 @@
+"""Define tests for the Recollect Waste config flow."""
+from aiorecollect.errors import RecollectError
+
+from homeassistant import data_entry_flow
+from homeassistant.components.recollect_waste import (
+ CONF_PLACE_ID,
+ CONF_SERVICE_ID,
+ DOMAIN,
+)
+from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
+
+from tests.async_mock import patch
+from tests.common import MockConfigEntry
+
+
+async def test_duplicate_error(hass):
+ """Test that errors are shown when duplicates are added."""
+ conf = {CONF_PLACE_ID: "12345", CONF_SERVICE_ID: "12345"}
+
+ MockConfigEntry(domain=DOMAIN, unique_id="12345, 12345", 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_invalid_place_or_service_id(hass):
+ """Test that an invalid Place or Service ID throws an error."""
+ conf = {CONF_PLACE_ID: "12345", CONF_SERVICE_ID: "12345"}
+
+ with patch(
+ "aiorecollect.client.Client.async_get_next_pickup_event",
+ side_effect=RecollectError,
+ ):
+ 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"] == {"base": "invalid_place_or_service_id"}
+
+
+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 user step works."""
+ conf = {CONF_PLACE_ID: "12345", CONF_SERVICE_ID: "12345"}
+
+ with patch(
+ "homeassistant.components.recollect_waste.async_setup_entry", return_value=True
+ ), patch(
+ "aiorecollect.client.Client.async_get_next_pickup_event", return_value=True
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_IMPORT}, data=conf
+ )
+ await hass.async_block_till_done()
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == "12345, 12345"
+ assert result["data"] == {CONF_PLACE_ID: "12345", CONF_SERVICE_ID: "12345"}
+
+
+async def test_step_user(hass):
+ """Test that the user step works."""
+ conf = {CONF_PLACE_ID: "12345", CONF_SERVICE_ID: "12345"}
+
+ with patch(
+ "homeassistant.components.recollect_waste.async_setup_entry", return_value=True
+ ), patch(
+ "aiorecollect.client.Client.async_get_next_pickup_event", return_value=True
+ ):
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={"source": SOURCE_USER}, data=conf
+ )
+ await hass.async_block_till_done()
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result["title"] == "12345, 12345"
+ assert result["data"] == {CONF_PLACE_ID: "12345", CONF_SERVICE_ID: "12345"}
diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py
index 91a2299e4b6..9cb07819e79 100644
--- a/tests/components/recorder/test_purge.py
+++ b/tests/components/recorder/test_purge.py
@@ -62,6 +62,22 @@ def test_purge_old_events(hass, hass_recorder):
assert events.count() == 2
+def test_purge_old_recorder_runs(hass, hass_recorder):
+ """Test deleting old recorder runs keeps current run."""
+ hass = hass_recorder()
+ _add_test_recorder_runs(hass)
+
+ # make sure we start with 7 recorder runs
+ with session_scope(hass=hass) as session:
+ recorder_runs = session.query(RecorderRuns)
+ assert recorder_runs.count() == 7
+
+ # run purge_old_data()
+ finished = purge_old_data(hass.data[DATA_INSTANCE], 0, repack=False)
+ assert finished
+ assert recorder_runs.count() == 1
+
+
def test_purge_method(hass, hass_recorder):
"""Test purge method."""
hass = hass_recorder()
diff --git a/tests/components/remote/test_device_action.py b/tests/components/remote/test_device_action.py
index 89d0f0de6bf..17165639e25 100644
--- a/tests/components/remote/test_device_action.py
+++ b/tests/components/remote/test_device_action.py
@@ -16,6 +16,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/remote/test_device_condition.py b/tests/components/remote/test_device_condition.py
index 2f01b4ab55f..e5a9bc3a9c9 100644
--- a/tests/components/remote/test_device_condition.py
+++ b/tests/components/remote/test_device_condition.py
@@ -19,6 +19,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/remote/test_device_trigger.py b/tests/components/remote/test_device_trigger.py
index 73feb3ea08f..eccf96c04f6 100644
--- a/tests/components/remote/test_device_trigger.py
+++ b/tests/components/remote/test_device_trigger.py
@@ -19,6 +19,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/rest/test_binary_sensor.py b/tests/components/rest/test_binary_sensor.py
index 1f2c88f4278..48d13a716ab 100644
--- a/tests/components/rest/test_binary_sensor.py
+++ b/tests/components/rest/test_binary_sensor.py
@@ -377,5 +377,28 @@ async def test_reload(hass):
assert hass.states.get("binary_sensor.rollout")
+@respx.mock
+async def test_setup_query_params(hass):
+ """Test setup with query params."""
+ respx.get(
+ "http://localhost?search=something",
+ status_code=200,
+ )
+ assert await async_setup_component(
+ hass,
+ binary_sensor.DOMAIN,
+ {
+ "binary_sensor": {
+ "platform": "rest",
+ "resource": "http://localhost",
+ "method": "GET",
+ "params": {"search": "something"},
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 1
+
+
def _get_fixtures_base_path():
return path.dirname(path.dirname(path.dirname(__file__)))
diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py
index d841f69e45f..71bcbedda88 100644
--- a/tests/components/rest/test_sensor.py
+++ b/tests/components/rest/test_sensor.py
@@ -249,6 +249,29 @@ async def test_setup_get_xml(hass):
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == DATA_MEGABYTES
+@respx.mock
+async def test_setup_query_params(hass):
+ """Test setup with query params."""
+ respx.get(
+ "http://localhost?search=something",
+ status_code=200,
+ )
+ assert await async_setup_component(
+ hass,
+ sensor.DOMAIN,
+ {
+ "sensor": {
+ "platform": "rest",
+ "resource": "http://localhost",
+ "method": "GET",
+ "params": {"search": "something"},
+ }
+ },
+ )
+ await hass.async_block_till_done()
+ assert len(hass.states.async_all()) == 1
+
+
@respx.mock
async def test_update_with_json_attrs(hass):
"""Test attributes get extracted from a JSON result."""
diff --git a/tests/components/rest/test_switch.py b/tests/components/rest/test_switch.py
index f21eea0e242..5e0c9fbeab3 100644
--- a/tests/components/rest/test_switch.py
+++ b/tests/components/rest/test_switch.py
@@ -8,6 +8,7 @@ from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.const import (
CONF_HEADERS,
CONF_NAME,
+ CONF_PARAMS,
CONF_PLATFORM,
CONF_RESOURCE,
CONTENT_TYPE_JSON,
@@ -28,6 +29,7 @@ RESOURCE = "http://localhost/"
STATE_RESOURCE = RESOURCE
HEADERS = {"Content-type": CONTENT_TYPE_JSON}
AUTH = None
+PARAMS = None
async def test_setup_missing_config(hass):
@@ -81,6 +83,25 @@ async def test_setup_minimum(hass, aioclient_mock):
assert aioclient_mock.call_count == 1
+async def test_setup_query_params(hass, aioclient_mock):
+ """Test setup with query params."""
+ aioclient_mock.get("http://localhost/?search=something", status=HTTP_OK)
+ with assert_setup_component(1, SWITCH_DOMAIN):
+ assert await async_setup_component(
+ hass,
+ SWITCH_DOMAIN,
+ {
+ SWITCH_DOMAIN: {
+ CONF_PLATFORM: rest.DOMAIN,
+ CONF_RESOURCE: "http://localhost",
+ CONF_PARAMS: {"search": "something"},
+ }
+ },
+ )
+ print(aioclient_mock)
+ assert aioclient_mock.call_count == 1
+
+
async def test_setup(hass, aioclient_mock):
"""Test setup with valid configuration."""
aioclient_mock.get("http://localhost", status=HTTP_OK)
@@ -137,6 +158,7 @@ def _setup_test_switch(hass):
STATE_RESOURCE,
METHOD,
HEADERS,
+ PARAMS,
AUTH,
body_on,
body_off,
diff --git a/tests/components/roomba/test_config_flow.py b/tests/components/roomba/test_config_flow.py
index d2af07070bb..253250d7d49 100644
--- a/tests/components/roomba/test_config_flow.py
+++ b/tests/components/roomba/test_config_flow.py
@@ -1,5 +1,5 @@
"""Test the iRobot Roomba config flow."""
-from roomba import RoombaConnectionError
+from roombapy import RoombaConnectionError
from homeassistant import config_entries, data_entry_flow, setup
from homeassistant.components.roomba.const import (
diff --git a/tests/components/search/test_init.py b/tests/components/search/test_init.py
index a379b91f82a..5710fa04698 100644
--- a/tests/components/search/test_init.py
+++ b/tests/components/search/test_init.py
@@ -3,6 +3,7 @@ from homeassistant.components import search
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
async def test_search(hass):
diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py
index 0c0c9c6c22b..9a023d6f5ad 100644
--- a/tests/components/sensor/test_device_condition.py
+++ b/tests/components/sensor/test_device_condition.py
@@ -16,6 +16,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
from tests.testing_config.custom_components.test.sensor import DEVICE_CLASSES
diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py
index dda57de0d9d..c39b4597632 100644
--- a/tests/components/sensor/test_device_trigger.py
+++ b/tests/components/sensor/test_device_trigger.py
@@ -20,6 +20,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
from tests.testing_config.custom_components.test.sensor import DEVICE_CLASSES
diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py
new file mode 100644
index 00000000000..3f97c0ef317
--- /dev/null
+++ b/tests/components/shelly/conftest.py
@@ -0,0 +1,11 @@
+"""Test configuration for Shelly."""
+import pytest
+
+from tests.async_mock import patch
+
+
+@pytest.fixture(autouse=True)
+def mock_coap():
+ """Mock out coap."""
+ with patch("homeassistant.components.shelly.get_coap_context"):
+ yield
diff --git a/tests/components/smappee/test_config_flow.py b/tests/components/smappee/test_config_flow.py
index 7434d469f96..55d063c2b1c 100644
--- a/tests/components/smappee/test_config_flow.py
+++ b/tests/components/smappee/test_config_flow.py
@@ -333,7 +333,9 @@ async def test_abort_cloud_flow_if_local_device_exists(hass):
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
-async def test_full_user_flow(hass, aiohttp_client, aioclient_mock, current_request):
+async def test_full_user_flow(
+ hass, aiohttp_client, aioclient_mock, current_request_with_host
+):
"""Check full flow."""
assert await setup.async_setup_component(
hass,
@@ -351,7 +353,13 @@ async def test_full_user_flow(hass, aiohttp_client, aioclient_mock, current_requ
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {"environment": ENV_CLOUD}
)
- state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]})
+ state = config_entry_oauth2_flow._encode_jwt(
+ hass,
+ {
+ "flow_id": result["flow_id"],
+ "redirect_uri": "https://example.com/auth/external/callback",
+ },
+ )
client = await aiohttp_client(hass.http.app)
resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
diff --git a/tests/components/solaredge/test_config_flow.py b/tests/components/solaredge/test_config_flow.py
index 61bc5f9ac6c..835fc300982 100644
--- a/tests/components/solaredge/test_config_flow.py
+++ b/tests/components/solaredge/test_config_flow.py
@@ -85,14 +85,14 @@ async def test_abort_if_already_setup(hass, test_api):
{CONF_NAME: DEFAULT_NAME, CONF_SITE_ID: SITE_ID, CONF_API_KEY: API_KEY}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "site_exists"
+ assert result["reason"] == "already_configured"
# user: Should fail, same SITE_ID
result = await flow.async_step_user(
{CONF_NAME: "test", CONF_SITE_ID: SITE_ID, CONF_API_KEY: "test"}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result["errors"] == {CONF_SITE_ID: "site_exists"}
+ assert result["errors"] == {CONF_SITE_ID: "already_configured"}
async def test_asserts(hass, test_api):
@@ -113,7 +113,7 @@ async def test_asserts(hass, test_api):
{CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
- assert result["errors"] == {CONF_SITE_ID: "api_failure"}
+ assert result["errors"] == {CONF_SITE_ID: "invalid_api_key"}
# test with ConnectionTimeout
test_api.get_details.side_effect = ConnectTimeout()
diff --git a/tests/components/somfy/test_config_flow.py b/tests/components/somfy/test_config_flow.py
index 89b4fbe9b13..4276a6a18d4 100644
--- a/tests/components/somfy/test_config_flow.py
+++ b/tests/components/somfy/test_config_flow.py
@@ -52,7 +52,9 @@ async def test_abort_if_existing_entry(hass):
assert result["reason"] == "single_instance_allowed"
-async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request):
+async def test_full_flow(
+ hass, aiohttp_client, aioclient_mock, current_request_with_host
+):
"""Check full flow."""
assert await setup.async_setup_component(
hass,
@@ -69,7 +71,13 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
- state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]})
+ state = config_entry_oauth2_flow._encode_jwt(
+ hass,
+ {
+ "flow_id": result["flow_id"],
+ "redirect_uri": "https://example.com/auth/external/callback",
+ },
+ )
assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP
assert result["url"] == (
diff --git a/tests/components/spotify/test_config_flow.py b/tests/components/spotify/test_config_flow.py
index 3b3c85dd828..53e87e5bdae 100644
--- a/tests/components/spotify/test_config_flow.py
+++ b/tests/components/spotify/test_config_flow.py
@@ -40,7 +40,9 @@ async def test_zeroconf_abort_if_existing_entry(hass):
assert result["reason"] == "already_configured"
-async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request):
+async def test_full_flow(
+ hass, aiohttp_client, aioclient_mock, current_request_with_host
+):
"""Check a full flow."""
assert await setup.async_setup_component(
hass,
@@ -56,7 +58,13 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request):
)
# pylint: disable=protected-access
- state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]})
+ state = config_entry_oauth2_flow._encode_jwt(
+ hass,
+ {
+ "flow_id": result["flow_id"],
+ "redirect_uri": "https://example.com/auth/external/callback",
+ },
+ )
assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP
assert result["url"] == (
@@ -103,7 +111,7 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request):
async def test_abort_if_spotify_error(
- hass, aiohttp_client, aioclient_mock, current_request
+ hass, aiohttp_client, aioclient_mock, current_request_with_host
):
"""Check Spotify errors causes flow to abort."""
await setup.async_setup_component(
@@ -120,7 +128,13 @@ async def test_abort_if_spotify_error(
)
# pylint: disable=protected-access
- state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]})
+ state = config_entry_oauth2_flow._encode_jwt(
+ hass,
+ {
+ "flow_id": result["flow_id"],
+ "redirect_uri": "https://example.com/auth/external/callback",
+ },
+ )
client = await aiohttp_client(hass.http.app)
await client.get(f"/auth/external/callback?code=abcd&state={state}")
@@ -144,7 +158,9 @@ async def test_abort_if_spotify_error(
assert result["reason"] == "connection_error"
-async def test_reauthentication(hass, aiohttp_client, aioclient_mock, current_request):
+async def test_reauthentication(
+ hass, aiohttp_client, aioclient_mock, current_request_with_host
+):
"""Test Spotify reauthentication."""
await setup.async_setup_component(
hass,
@@ -173,7 +189,13 @@ async def test_reauthentication(hass, aiohttp_client, aioclient_mock, current_re
result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {})
# pylint: disable=protected-access
- state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]})
+ state = config_entry_oauth2_flow._encode_jwt(
+ hass,
+ {
+ "flow_id": result["flow_id"],
+ "redirect_uri": "https://example.com/auth/external/callback",
+ },
+ )
client = await aiohttp_client(hass.http.app)
await client.get(f"/auth/external/callback?code=abcd&state={state}")
@@ -202,7 +224,7 @@ async def test_reauthentication(hass, aiohttp_client, aioclient_mock, current_re
async def test_reauth_account_mismatch(
- hass, aiohttp_client, aioclient_mock, current_request
+ hass, aiohttp_client, aioclient_mock, current_request_with_host
):
"""Test Spotify reauthentication with different account."""
await setup.async_setup_component(
@@ -230,7 +252,13 @@ async def test_reauth_account_mismatch(
result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {})
# pylint: disable=protected-access
- state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]})
+ state = config_entry_oauth2_flow._encode_jwt(
+ hass,
+ {
+ "flow_id": result["flow_id"],
+ "redirect_uri": "https://example.com/auth/external/callback",
+ },
+ )
client = await aiohttp_client(hass.http.app)
await client.get(f"/auth/external/callback?code=abcd&state={state}")
diff --git a/tests/components/srp_energy/__init__.py b/tests/components/srp_energy/__init__.py
new file mode 100644
index 00000000000..34f06e2993e
--- /dev/null
+++ b/tests/components/srp_energy/__init__.py
@@ -0,0 +1,55 @@
+"""Tests for the SRP Energy integration."""
+from homeassistant import config_entries
+from homeassistant.components import srp_energy
+from homeassistant.const import CONF_ID, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
+
+from tests.async_mock import patch
+from tests.common import MockConfigEntry
+
+ENTRY_OPTIONS = {}
+
+ENTRY_CONFIG = {
+ CONF_NAME: "Test",
+ CONF_ID: "123456789",
+ CONF_USERNAME: "abba",
+ CONF_PASSWORD: "ana",
+ srp_energy.const.CONF_IS_TOU: False,
+}
+
+
+async def init_integration(
+ hass,
+ config=None,
+ options=None,
+ entry_id="1",
+ source="user",
+ side_effect=None,
+ usage=None,
+):
+ """Set up the srp_energy integration in Home Assistant."""
+ if not config:
+ config = ENTRY_CONFIG
+
+ if not options:
+ options = ENTRY_OPTIONS
+
+ config_entry = MockConfigEntry(
+ domain=srp_energy.SRP_ENERGY_DOMAIN,
+ source=source,
+ data=config,
+ connection_class=config_entries.CONN_CLASS_CLOUD_POLL,
+ options=options,
+ entry_id=entry_id,
+ )
+
+ with patch("srpenergy.client.SrpEnergyClient"), patch(
+ "homeassistant.components.srp_energy.SrpEnergyClient", side_effect=side_effect
+ ), patch("srpenergy.client.SrpEnergyClient.usage", return_value=usage), patch(
+ "homeassistant.components.srp_energy.SrpEnergyClient.usage", return_value=usage
+ ):
+
+ config_entry.add_to_hass(hass)
+ await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ return config_entry
diff --git a/tests/components/srp_energy/test_config_flow.py b/tests/components/srp_energy/test_config_flow.py
new file mode 100644
index 00000000000..acb9d28f75d
--- /dev/null
+++ b/tests/components/srp_energy/test_config_flow.py
@@ -0,0 +1,105 @@
+"""Test the SRP Energy config flow."""
+from homeassistant import config_entries, data_entry_flow
+from homeassistant.components.srp_energy.const import CONF_IS_TOU, SRP_ENERGY_DOMAIN
+
+from . import ENTRY_CONFIG, init_integration
+
+from tests.async_mock import patch
+
+
+async def test_form(hass):
+ """Test user config."""
+ # First get the form
+ result = await hass.config_entries.flow.async_init(
+ SRP_ENERGY_DOMAIN, context={"source": "user"}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
+ assert result["step_id"] == "user"
+ assert result["errors"] == {}
+
+ # Fill submit form data for config entry
+ with patch("homeassistant.components.srp_energy.config_flow.SrpEnergyClient"):
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input=ENTRY_CONFIG,
+ )
+
+ assert result["type"] == "create_entry"
+ assert result["title"] == "Test"
+ assert result["data"][CONF_IS_TOU] is False
+
+
+async def test_form_invalid_auth(hass):
+ """Test user config with invalid auth."""
+ result = await hass.config_entries.flow.async_init(
+ SRP_ENERGY_DOMAIN, context={"source": "user"}
+ )
+
+ with patch(
+ "homeassistant.components.srp_energy.config_flow.SrpEnergyClient.validate",
+ return_value=False,
+ ):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input=ENTRY_CONFIG,
+ )
+
+ assert result["errors"]["base"] == "invalid_auth"
+
+
+async def test_form_value_error(hass):
+ """Test user config that throws a value error."""
+ result = await hass.config_entries.flow.async_init(
+ SRP_ENERGY_DOMAIN, context={"source": "user"}
+ )
+
+ with patch(
+ "homeassistant.components.srp_energy.config_flow.SrpEnergyClient",
+ side_effect=ValueError(),
+ ):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input=ENTRY_CONFIG,
+ )
+
+ assert result["errors"]["base"] == "invalid_account"
+
+
+async def test_form_unknown_exception(hass):
+ """Test user config that throws an unknown exception."""
+ result = await hass.config_entries.flow.async_init(
+ SRP_ENERGY_DOMAIN, context={"source": "user"}
+ )
+
+ with patch(
+ "homeassistant.components.srp_energy.config_flow.SrpEnergyClient",
+ side_effect=Exception(),
+ ):
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ user_input=ENTRY_CONFIG,
+ )
+
+ assert result["errors"]["base"] == "unknown"
+
+
+async def test_config(hass):
+ """Test handling of configuration imported."""
+ with patch("homeassistant.components.srp_energy.config_flow.SrpEnergyClient"):
+ result = await hass.config_entries.flow.async_init(
+ SRP_ENERGY_DOMAIN,
+ context={"source": config_entries.SOURCE_IMPORT},
+ data=ENTRY_CONFIG,
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+
+async def test_integration_already_configured(hass):
+ """Test integration is already configured."""
+ await init_integration(hass)
+ result = await hass.config_entries.flow.async_init(
+ SRP_ENERGY_DOMAIN, context={"source": "user"}
+ )
+ assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result["reason"] == "single_instance_allowed"
diff --git a/tests/components/srp_energy/test_init.py b/tests/components/srp_energy/test_init.py
new file mode 100644
index 00000000000..8e758d05114
--- /dev/null
+++ b/tests/components/srp_energy/test_init.py
@@ -0,0 +1,26 @@
+"""Tests for Srp Energy component Init."""
+from homeassistant.components import srp_energy
+
+from tests.components.srp_energy import init_integration
+
+
+async def test_setup_entry(hass):
+ """Test setup entry fails if deCONZ is not available."""
+ config_entry = await init_integration(hass)
+ assert config_entry.state == "loaded"
+ assert hass.data[srp_energy.SRP_ENERGY_DOMAIN]
+
+
+async def test_unload_entry(hass):
+ """Test being able to unload an entry."""
+ config_entry = await init_integration(hass)
+ assert hass.data[srp_energy.SRP_ENERGY_DOMAIN]
+
+ assert await srp_energy.async_unload_entry(hass, config_entry)
+ assert not hass.data[srp_energy.SRP_ENERGY_DOMAIN]
+
+
+async def test_async_setup_entry_with_exception(hass):
+ """Test exception when SrpClient can't load."""
+ await init_integration(hass, side_effect=Exception())
+ assert srp_energy.SRP_ENERGY_DOMAIN not in hass.data
diff --git a/tests/components/srp_energy/test_sensor.py b/tests/components/srp_energy/test_sensor.py
new file mode 100644
index 00000000000..3a70a3ec09f
--- /dev/null
+++ b/tests/components/srp_energy/test_sensor.py
@@ -0,0 +1,129 @@
+"""Tests for the srp_energy sensor platform."""
+from homeassistant.components.srp_energy.const import (
+ ATTRIBUTION,
+ DEFAULT_NAME,
+ ICON,
+ SENSOR_NAME,
+ SENSOR_TYPE,
+ SRP_ENERGY_DOMAIN,
+)
+from homeassistant.components.srp_energy.sensor import SrpEntity, async_setup_entry
+from homeassistant.const import ATTR_ATTRIBUTION, ENERGY_KILO_WATT_HOUR
+
+from tests.async_mock import MagicMock
+
+
+async def test_async_setup_entry(hass):
+ """Test the sensor."""
+ fake_async_add_entities = MagicMock()
+ fake_srp_energy_client = MagicMock()
+ fake_srp_energy_client.usage.return_value = [{1, 2, 3, 1.999, 4}]
+ fake_config = MagicMock(
+ data={
+ "name": "SRP Energy",
+ "is_tou": False,
+ "id": "0123456789",
+ "username": "testuser@example.com",
+ "password": "mypassword",
+ }
+ )
+ hass.data[SRP_ENERGY_DOMAIN] = fake_srp_energy_client
+
+ await async_setup_entry(hass, fake_config, fake_async_add_entities)
+
+
+async def test_async_setup_entry_timeout_error(hass):
+ """Test fetching usage data. Failed the first time because was too get response."""
+ fake_async_add_entities = MagicMock()
+ fake_srp_energy_client = MagicMock()
+ fake_srp_energy_client.usage.return_value = [{1, 2, 3, 1.999, 4}]
+ fake_config = MagicMock(
+ data={
+ "name": "SRP Energy",
+ "is_tou": False,
+ "id": "0123456789",
+ "username": "testuser@example.com",
+ "password": "mypassword",
+ }
+ )
+ hass.data[SRP_ENERGY_DOMAIN] = fake_srp_energy_client
+ fake_srp_energy_client.usage.side_effect = TimeoutError()
+
+ await async_setup_entry(hass, fake_config, fake_async_add_entities)
+ assert not fake_async_add_entities.call_args[0][0][
+ 0
+ ].coordinator.last_update_success
+
+
+async def test_async_setup_entry_connect_error(hass):
+ """Test fetching usage data. Failed the first time because was too get response."""
+ fake_async_add_entities = MagicMock()
+ fake_srp_energy_client = MagicMock()
+ fake_srp_energy_client.usage.return_value = [{1, 2, 3, 1.999, 4}]
+ fake_config = MagicMock(
+ data={
+ "name": "SRP Energy",
+ "is_tou": False,
+ "id": "0123456789",
+ "username": "testuser@example.com",
+ "password": "mypassword",
+ }
+ )
+ hass.data[SRP_ENERGY_DOMAIN] = fake_srp_energy_client
+ fake_srp_energy_client.usage.side_effect = ValueError()
+
+ await async_setup_entry(hass, fake_config, fake_async_add_entities)
+ assert not fake_async_add_entities.call_args[0][0][
+ 0
+ ].coordinator.last_update_success
+
+
+async def test_srp_entity(hass):
+ """Test the SrpEntity."""
+ fake_coordinator = MagicMock(data=1.99999999999)
+ srp_entity = SrpEntity(fake_coordinator)
+
+ assert srp_entity is not None
+ assert srp_entity.name == f"{DEFAULT_NAME} {SENSOR_NAME}"
+ assert srp_entity.unique_id == SENSOR_TYPE
+ assert srp_entity.state is None
+ assert srp_entity.unit_of_measurement == ENERGY_KILO_WATT_HOUR
+ assert srp_entity.icon == ICON
+ assert srp_entity.usage == "2.00"
+ assert srp_entity.should_poll is False
+ assert srp_entity.device_state_attributes[ATTR_ATTRIBUTION] == ATTRIBUTION
+ assert srp_entity.available is not None
+
+ await srp_entity.async_added_to_hass()
+ assert srp_entity.state is not None
+ assert fake_coordinator.async_add_listener.called
+ assert not fake_coordinator.async_add_listener.data.called
+
+
+async def test_srp_entity_no_data(hass):
+ """Test the SrpEntity."""
+ fake_coordinator = MagicMock(data=False)
+ srp_entity = SrpEntity(fake_coordinator)
+ assert srp_entity.device_state_attributes is None
+
+
+async def test_srp_entity_no_coord_data(hass):
+ """Test the SrpEntity."""
+ fake_coordinator = MagicMock(data=False)
+ srp_entity = SrpEntity(fake_coordinator)
+
+ assert srp_entity.usage is None
+
+
+async def test_srp_entity_async_update(hass):
+ """Test the SrpEntity."""
+
+ async def async_magic():
+ pass
+
+ MagicMock.__await__ = lambda x: async_magic().__await__()
+ fake_coordinator = MagicMock(data=False)
+ srp_entity = SrpEntity(fake_coordinator)
+
+ await srp_entity.async_update()
+ assert fake_coordinator.async_request_refresh.called
diff --git a/tests/components/sun/test_trigger.py b/tests/components/sun/test_trigger.py
index 2cb21051200..1a3de56964c 100644
--- a/tests/components/sun/test_trigger.py
+++ b/tests/components/sun/test_trigger.py
@@ -18,6 +18,7 @@ import homeassistant.util.dt as dt_util
from tests.async_mock import patch
from tests.common import async_fire_time_changed, async_mock_service, mock_component
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
ORIG_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE
diff --git a/tests/components/surepetcare/conftest.py b/tests/components/surepetcare/conftest.py
index 6d85a2b0189..ed42fe2532b 100644
--- a/tests/components/surepetcare/conftest.py
+++ b/tests/components/surepetcare/conftest.py
@@ -7,8 +7,8 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from tests.async_mock import AsyncMock, patch
-@fixture()
-def surepetcare(hass):
+@fixture
+async def surepetcare(hass):
"""Mock the SurePetcare for easier testing."""
with patch("homeassistant.components.surepetcare.SurePetcare") as mock_surepetcare:
instance = mock_surepetcare.return_value = SurePetcare(
diff --git a/tests/components/switch/test_device_action.py b/tests/components/switch/test_device_action.py
index 204b2370cb8..4da401a215c 100644
--- a/tests/components/switch/test_device_action.py
+++ b/tests/components/switch/test_device_action.py
@@ -16,6 +16,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/switch/test_device_condition.py b/tests/components/switch/test_device_condition.py
index 6e542ee24d1..093758cbe62 100644
--- a/tests/components/switch/test_device_condition.py
+++ b/tests/components/switch/test_device_condition.py
@@ -19,6 +19,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/switch/test_device_trigger.py b/tests/components/switch/test_device_trigger.py
index 512942ca71b..34817b687f8 100644
--- a/tests/components/switch/test_device_trigger.py
+++ b/tests/components/switch/test_device_trigger.py
@@ -19,6 +19,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/synology_dsm/conftest.py b/tests/components/synology_dsm/conftest.py
index 41bd42a98b3..db25bd59ada 100644
--- a/tests/components/synology_dsm/conftest.py
+++ b/tests/components/synology_dsm/conftest.py
@@ -4,10 +4,20 @@ import pytest
from tests.async_mock import patch
+def pytest_configure(config):
+ """Register custom marker for tests."""
+ config.addinivalue_line(
+ "markers", "no_bypass_setup: mark test to disable bypass_setup_fixture"
+ )
+
+
@pytest.fixture(name="bypass_setup", autouse=True)
-def bypass_setup_fixture():
+def bypass_setup_fixture(request):
"""Mock component setup."""
- with patch(
- "homeassistant.components.synology_dsm.async_setup_entry", return_value=True
- ):
+ if "no_bypass_setup" in request.keywords:
yield
+ else:
+ with patch(
+ "homeassistant.components.synology_dsm.async_setup_entry", return_value=True
+ ):
+ yield
diff --git a/tests/components/synology_dsm/consts.py b/tests/components/synology_dsm/consts.py
new file mode 100644
index 00000000000..3c305745aa7
--- /dev/null
+++ b/tests/components/synology_dsm/consts.py
@@ -0,0 +1,14 @@
+"""Constants for the Synology DSM component tests."""
+
+HOST = "nas.meontheinternet.com"
+SERIAL = "mySerial"
+HOST_2 = "nas.worldwide.me"
+SERIAL_2 = "mySerial2"
+PORT = 1234
+USE_SSL = True
+VERIFY_SSL = False
+USERNAME = "Home_Assistant"
+PASSWORD = "password"
+DEVICE_TOKEN = "Dév!cè_T0k€ñ"
+
+MACS = ["00-11-32-XX-XX-59", "00-11-32-XX-XX-5A"]
diff --git a/tests/components/synology_dsm/test_config_flow.py b/tests/components/synology_dsm/test_config_flow.py
index f895ee7e7dc..59ed8eea657 100644
--- a/tests/components/synology_dsm/test_config_flow.py
+++ b/tests/components/synology_dsm/test_config_flow.py
@@ -10,7 +10,6 @@ from synology_dsm.exceptions import (
from homeassistant import data_entry_flow, setup
from homeassistant.components import ssdp
-from homeassistant.components.synology_dsm import _async_setup_services
from homeassistant.components.synology_dsm.config_flow import CONF_OTP_CODE
from homeassistant.components.synology_dsm.const import (
CONF_VOLUMES,
@@ -21,7 +20,6 @@ from homeassistant.components.synology_dsm.const import (
DEFAULT_USE_SSL,
DEFAULT_VERIFY_SSL,
DOMAIN,
- SERVICES,
)
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP, SOURCE_USER
from homeassistant.const import (
@@ -38,22 +36,23 @@ from homeassistant.const import (
)
from homeassistant.helpers.typing import HomeAssistantType
+from .consts import (
+ DEVICE_TOKEN,
+ HOST,
+ HOST_2,
+ MACS,
+ PASSWORD,
+ PORT,
+ SERIAL,
+ SERIAL_2,
+ USE_SSL,
+ USERNAME,
+ VERIFY_SSL,
+)
+
from tests.async_mock import MagicMock, Mock, patch
from tests.common import MockConfigEntry
-HOST = "nas.meontheinternet.com"
-SERIAL = "mySerial"
-HOST_2 = "nas.worldwide.me"
-SERIAL_2 = "mySerial2"
-PORT = 1234
-USE_SSL = True
-VERIFY_SSL = False
-USERNAME = "Home_Assistant"
-PASSWORD = "password"
-DEVICE_TOKEN = "Dév!cè_T0k€ñ"
-
-MACS = ["00-11-32-XX-XX-59", "00-11-32-XX-XX-5A"]
-
@pytest.fixture(name="service")
def mock_controller_service():
@@ -498,23 +497,3 @@ async def test_options_flow(hass: HomeAssistantType, service: MagicMock):
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert config_entry.options[CONF_SCAN_INTERVAL] == 2
assert config_entry.options[CONF_TIMEOUT] == 30
-
-
-async def test_services_registered(hass: HomeAssistantType):
- """Test if all services are registered."""
- with patch(
- "homeassistant.core.ServiceRegistry.async_register", return_value=Mock(True)
- ) as async_register:
- await hass.config_entries.flow.async_init(
- DOMAIN,
- context={"source": SOURCE_USER},
- data={
- CONF_HOST: HOST,
- CONF_PORT: PORT,
- CONF_SSL: USE_SSL,
- CONF_USERNAME: USERNAME,
- CONF_PASSWORD: PASSWORD,
- },
- )
- await _async_setup_services(hass)
- assert async_register.call_count == len(SERVICES)
diff --git a/tests/components/synology_dsm/test_init.py b/tests/components/synology_dsm/test_init.py
new file mode 100644
index 00000000000..b8be375b321
--- /dev/null
+++ b/tests/components/synology_dsm/test_init.py
@@ -0,0 +1,41 @@
+"""Tests for the Synology DSM component."""
+import pytest
+
+from homeassistant.components.synology_dsm.const import DOMAIN, SERVICES
+from homeassistant.const import (
+ CONF_HOST,
+ CONF_MAC,
+ CONF_PASSWORD,
+ CONF_PORT,
+ CONF_SSL,
+ CONF_USERNAME,
+)
+from homeassistant.helpers.typing import HomeAssistantType
+
+from .consts import HOST, MACS, PASSWORD, PORT, USE_SSL, USERNAME
+
+from tests.async_mock import patch
+from tests.common import MockConfigEntry
+
+
+@pytest.mark.no_bypass_setup
+async def test_services_registered(hass: HomeAssistantType):
+ """Test if all services are registered."""
+ with patch(
+ "homeassistant.components.synology_dsm.SynoApi.async_setup", return_value=True
+ ), patch("homeassistant.components.synology_dsm.PLATFORMS", return_value=[]):
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={
+ CONF_HOST: HOST,
+ CONF_PORT: PORT,
+ CONF_SSL: USE_SSL,
+ CONF_USERNAME: USERNAME,
+ CONF_PASSWORD: PASSWORD,
+ CONF_MAC: MACS[0],
+ },
+ )
+ entry.add_to_hass(hass)
+ assert await hass.config_entries.async_setup(entry.entry_id)
+ for service in SERVICES:
+ assert hass.services.has_service(DOMAIN, service)
diff --git a/tests/components/tag/test_trigger.py b/tests/components/tag/test_trigger.py
index 0249acaf29b..9a97d95e7d5 100644
--- a/tests/components/tag/test_trigger.py
+++ b/tests/components/tag/test_trigger.py
@@ -4,10 +4,12 @@ import pytest
import homeassistant.components.automation as automation
from homeassistant.components.tag import async_scan_tag
-from homeassistant.components.tag.const import DOMAIN, TAG_ID
+from homeassistant.components.tag.const import DEVICE_ID, DOMAIN, TAG_ID
+from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF
from homeassistant.setup import async_setup_component
from tests.common import async_mock_service
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
@@ -44,6 +46,7 @@ async def test_triggers(hass, tag_setup, calls):
{
automation.DOMAIN: [
{
+ "alias": "test",
"trigger": {"platform": DOMAIN, TAG_ID: "abc123"},
"action": {
"service": "test.automation",
@@ -62,6 +65,18 @@ async def test_triggers(hass, tag_setup, calls):
assert len(calls) == 1
assert calls[0].data["message"] == "service called"
+ await hass.services.async_call(
+ automation.DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "automation.test"},
+ blocking=True,
+ )
+
+ await async_scan_tag(hass, "abc123", None)
+ await hass.async_block_till_done()
+
+ assert len(calls) == 1
+
async def test_exception_bad_trigger(hass, calls, caplog):
"""Test for exception on event triggers firing."""
@@ -83,3 +98,50 @@ async def test_exception_bad_trigger(hass, calls, caplog):
)
await hass.async_block_till_done()
assert "Invalid config for [automation]" in caplog.text
+
+
+async def test_multiple_tags_and_devices_trigger(hass, tag_setup, calls):
+ """Test multiple tags and devices triggers."""
+ assert await tag_setup()
+ assert await async_setup_component(
+ hass,
+ automation.DOMAIN,
+ {
+ automation.DOMAIN: [
+ {
+ "trigger": {
+ "platform": DOMAIN,
+ TAG_ID: ["abc123", "def456"],
+ DEVICE_ID: ["ghi789", "jkl0123"],
+ },
+ "action": {
+ "service": "test.automation",
+ "data": {"message": "service called"},
+ },
+ }
+ ]
+ },
+ )
+
+ await hass.async_block_till_done()
+
+ # Should not trigger
+ await async_scan_tag(hass, tag_id="abc123", device_id=None)
+ await async_scan_tag(hass, tag_id="abc123", device_id="invalid")
+ await hass.async_block_till_done()
+
+ # Should trigger
+ await async_scan_tag(hass, tag_id="abc123", device_id="ghi789")
+ await hass.async_block_till_done()
+ await async_scan_tag(hass, tag_id="abc123", device_id="jkl0123")
+ await hass.async_block_till_done()
+ await async_scan_tag(hass, "def456", device_id="ghi789")
+ await hass.async_block_till_done()
+ await async_scan_tag(hass, "def456", device_id="jkl0123")
+ await hass.async_block_till_done()
+
+ assert len(calls) == 4
+ assert calls[0].data["message"] == "service called"
+ assert calls[1].data["message"] == "service called"
+ assert calls[2].data["message"] == "service called"
+ assert calls[3].data["message"] == "service called"
diff --git a/tests/components/tasmota/test_cover.py b/tests/components/tasmota/test_cover.py
new file mode 100644
index 00000000000..d5ae01f1666
--- /dev/null
+++ b/tests/components/tasmota/test_cover.py
@@ -0,0 +1,629 @@
+"""The tests for the Tasmota cover platform."""
+import copy
+import json
+
+from hatasmota.utils import (
+ get_topic_stat_result,
+ get_topic_stat_status,
+ get_topic_tele_sensor,
+ get_topic_tele_will,
+)
+
+from homeassistant.components import cover
+from homeassistant.components.tasmota.const import DEFAULT_PREFIX
+from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNKNOWN
+
+from .test_common import (
+ DEFAULT_CONFIG,
+ help_test_availability,
+ help_test_availability_discovery_update,
+ help_test_availability_poll_state,
+ help_test_availability_when_connection_lost,
+ help_test_discovery_device_remove,
+ help_test_discovery_removal,
+ help_test_discovery_update_unchanged,
+ help_test_entity_id_update_discovery_update,
+ help_test_entity_id_update_subscriptions,
+)
+
+from tests.async_mock import patch
+from tests.common import async_fire_mqtt_message
+
+
+async def test_missing_relay(hass, mqtt_mock, setup_tasmota):
+ """Test no cover is discovered if relays are missing."""
+
+
+async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota):
+ """Test state update via MQTT."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["rl"][0] = 3
+ config["rl"][1] = 3
+ mac = config["mac"]
+
+ async_fire_mqtt_message(
+ hass,
+ f"{DEFAULT_PREFIX}/{mac}/config",
+ json.dumps(config),
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("cover.tasmota_cover_1")
+ assert state.state == "unavailable"
+ assert not state.attributes.get(ATTR_ASSUMED_STATE)
+
+ async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online")
+ state = hass.states.get("cover.tasmota_cover_1")
+ assert state.state == STATE_UNKNOWN
+ assert (
+ state.attributes["supported_features"]
+ == cover.SUPPORT_OPEN
+ | cover.SUPPORT_CLOSE
+ | cover.SUPPORT_STOP
+ | cover.SUPPORT_SET_POSITION
+ )
+ assert not state.attributes.get(ATTR_ASSUMED_STATE)
+
+ # Periodic updates
+ async_fire_mqtt_message(
+ hass,
+ "tasmota_49A3BC/tele/SENSOR",
+ '{"Shutter1":{"Position":54,"Direction":-1}}',
+ )
+ state = hass.states.get("cover.tasmota_cover_1")
+ assert state.state == "closing"
+ assert state.attributes["current_position"] == 54
+
+ async_fire_mqtt_message(
+ hass,
+ "tasmota_49A3BC/tele/SENSOR",
+ '{"Shutter1":{"Position":100,"Direction":1}}',
+ )
+ state = hass.states.get("cover.tasmota_cover_1")
+ assert state.state == "opening"
+ assert state.attributes["current_position"] == 100
+
+ async_fire_mqtt_message(
+ hass, "tasmota_49A3BC/tele/SENSOR", '{"Shutter1":{"Position":0,"Direction":0}}'
+ )
+ state = hass.states.get("cover.tasmota_cover_1")
+ assert state.state == "closed"
+ assert state.attributes["current_position"] == 0
+
+ async_fire_mqtt_message(
+ hass, "tasmota_49A3BC/tele/SENSOR", '{"Shutter1":{"Position":1,"Direction":0}}'
+ )
+ state = hass.states.get("cover.tasmota_cover_1")
+ assert state.state == "open"
+ assert state.attributes["current_position"] == 1
+
+ async_fire_mqtt_message(
+ hass,
+ "tasmota_49A3BC/tele/SENSOR",
+ '{"Shutter1":{"Position":100,"Direction":0}}',
+ )
+ state = hass.states.get("cover.tasmota_cover_1")
+ assert state.state == "open"
+ assert state.attributes["current_position"] == 100
+
+ # State poll response
+ async_fire_mqtt_message(
+ hass,
+ "tasmota_49A3BC/stat/STATUS10",
+ '{"StatusSNS":{"Shutter1":{"Position":54,"Direction":-1}}}',
+ )
+ state = hass.states.get("cover.tasmota_cover_1")
+ assert state.state == "closing"
+ assert state.attributes["current_position"] == 54
+
+ async_fire_mqtt_message(
+ hass,
+ "tasmota_49A3BC/stat/STATUS10",
+ '{"StatusSNS":{"Shutter1":{"Position":100,"Direction":1}}}',
+ )
+ state = hass.states.get("cover.tasmota_cover_1")
+ assert state.state == "opening"
+ assert state.attributes["current_position"] == 100
+
+ async_fire_mqtt_message(
+ hass,
+ "tasmota_49A3BC/stat/STATUS10",
+ '{"StatusSNS":{"Shutter1":{"Position":0,"Direction":0}}}',
+ )
+ state = hass.states.get("cover.tasmota_cover_1")
+ assert state.state == "closed"
+ assert state.attributes["current_position"] == 0
+
+ async_fire_mqtt_message(
+ hass,
+ "tasmota_49A3BC/stat/STATUS10",
+ '{"StatusSNS":{"Shutter1":{"Position":1,"Direction":0}}}',
+ )
+ state = hass.states.get("cover.tasmota_cover_1")
+ assert state.state == "open"
+ assert state.attributes["current_position"] == 1
+
+ async_fire_mqtt_message(
+ hass,
+ "tasmota_49A3BC/stat/STATUS10",
+ '{"StatusSNS":{"Shutter1":{"Position":100,"Direction":0}}}',
+ )
+ state = hass.states.get("cover.tasmota_cover_1")
+ assert state.state == "open"
+ assert state.attributes["current_position"] == 100
+
+ # Command response
+ async_fire_mqtt_message(
+ hass,
+ "tasmota_49A3BC/stat/RESULT",
+ '{"Shutter1":{"Position":54,"Direction":-1}}',
+ )
+ state = hass.states.get("cover.tasmota_cover_1")
+ assert state.state == "closing"
+ assert state.attributes["current_position"] == 54
+
+ async_fire_mqtt_message(
+ hass,
+ "tasmota_49A3BC/stat/RESULT",
+ '{"Shutter1":{"Position":100,"Direction":1}}',
+ )
+ state = hass.states.get("cover.tasmota_cover_1")
+ assert state.state == "opening"
+ assert state.attributes["current_position"] == 100
+
+ async_fire_mqtt_message(
+ hass, "tasmota_49A3BC/stat/RESULT", '{"Shutter1":{"Position":0,"Direction":0}}'
+ )
+ state = hass.states.get("cover.tasmota_cover_1")
+ assert state.state == "closed"
+ assert state.attributes["current_position"] == 0
+
+ async_fire_mqtt_message(
+ hass, "tasmota_49A3BC/stat/RESULT", '{"Shutter1":{"Position":1,"Direction":0}}'
+ )
+ state = hass.states.get("cover.tasmota_cover_1")
+ assert state.state == "open"
+ assert state.attributes["current_position"] == 1
+
+ async_fire_mqtt_message(
+ hass,
+ "tasmota_49A3BC/stat/RESULT",
+ '{"Shutter1":{"Position":100,"Direction":0}}',
+ )
+ state = hass.states.get("cover.tasmota_cover_1")
+ assert state.state == "open"
+ assert state.attributes["current_position"] == 100
+
+
+async def test_controlling_state_via_mqtt_inverted(hass, mqtt_mock, setup_tasmota):
+ """Test state update via MQTT."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["rl"][0] = 3
+ config["rl"][1] = 3
+ config["sho"] = [1] # Inverted cover
+ mac = config["mac"]
+
+ async_fire_mqtt_message(
+ hass,
+ f"{DEFAULT_PREFIX}/{mac}/config",
+ json.dumps(config),
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("cover.tasmota_cover_1")
+ assert state.state == "unavailable"
+ assert not state.attributes.get(ATTR_ASSUMED_STATE)
+
+ async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online")
+ state = hass.states.get("cover.tasmota_cover_1")
+ assert state.state == STATE_UNKNOWN
+ assert (
+ state.attributes["supported_features"]
+ == cover.SUPPORT_OPEN
+ | cover.SUPPORT_CLOSE
+ | cover.SUPPORT_STOP
+ | cover.SUPPORT_SET_POSITION
+ )
+ assert not state.attributes.get(ATTR_ASSUMED_STATE)
+
+ # Periodic updates
+ async_fire_mqtt_message(
+ hass,
+ "tasmota_49A3BC/tele/SENSOR",
+ '{"Shutter1":{"Position":54,"Direction":-1}}',
+ )
+ state = hass.states.get("cover.tasmota_cover_1")
+ assert state.state == "opening"
+ assert state.attributes["current_position"] == 46
+
+ async_fire_mqtt_message(
+ hass,
+ "tasmota_49A3BC/tele/SENSOR",
+ '{"Shutter1":{"Position":100,"Direction":1}}',
+ )
+ state = hass.states.get("cover.tasmota_cover_1")
+ assert state.state == "closing"
+ assert state.attributes["current_position"] == 0
+
+ async_fire_mqtt_message(
+ hass, "tasmota_49A3BC/tele/SENSOR", '{"Shutter1":{"Position":0,"Direction":0}}'
+ )
+ state = hass.states.get("cover.tasmota_cover_1")
+ assert state.state == "open"
+ assert state.attributes["current_position"] == 100
+
+ async_fire_mqtt_message(
+ hass, "tasmota_49A3BC/tele/SENSOR", '{"Shutter1":{"Position":99,"Direction":0}}'
+ )
+ state = hass.states.get("cover.tasmota_cover_1")
+ assert state.state == "open"
+ assert state.attributes["current_position"] == 1
+
+ async_fire_mqtt_message(
+ hass,
+ "tasmota_49A3BC/tele/SENSOR",
+ '{"Shutter1":{"Position":100,"Direction":0}}',
+ )
+ state = hass.states.get("cover.tasmota_cover_1")
+ assert state.state == "closed"
+ assert state.attributes["current_position"] == 0
+
+ # State poll response
+ async_fire_mqtt_message(
+ hass,
+ "tasmota_49A3BC/stat/STATUS10",
+ '{"StatusSNS":{"Shutter1":{"Position":54,"Direction":-1}}}',
+ )
+ state = hass.states.get("cover.tasmota_cover_1")
+ assert state.state == "opening"
+ assert state.attributes["current_position"] == 46
+
+ async_fire_mqtt_message(
+ hass,
+ "tasmota_49A3BC/stat/STATUS10",
+ '{"StatusSNS":{"Shutter1":{"Position":100,"Direction":1}}}',
+ )
+ state = hass.states.get("cover.tasmota_cover_1")
+ assert state.state == "closing"
+ assert state.attributes["current_position"] == 0
+
+ async_fire_mqtt_message(
+ hass,
+ "tasmota_49A3BC/stat/STATUS10",
+ '{"StatusSNS":{"Shutter1":{"Position":0,"Direction":0}}}',
+ )
+ state = hass.states.get("cover.tasmota_cover_1")
+ assert state.state == "open"
+ assert state.attributes["current_position"] == 100
+
+ async_fire_mqtt_message(
+ hass,
+ "tasmota_49A3BC/stat/STATUS10",
+ '{"StatusSNS":{"Shutter1":{"Position":99,"Direction":0}}}',
+ )
+ state = hass.states.get("cover.tasmota_cover_1")
+ assert state.state == "open"
+ assert state.attributes["current_position"] == 1
+
+ async_fire_mqtt_message(
+ hass,
+ "tasmota_49A3BC/stat/STATUS10",
+ '{"StatusSNS":{"Shutter1":{"Position":100,"Direction":0}}}',
+ )
+ state = hass.states.get("cover.tasmota_cover_1")
+ assert state.state == "closed"
+ assert state.attributes["current_position"] == 0
+
+ # Command response
+ async_fire_mqtt_message(
+ hass,
+ "tasmota_49A3BC/stat/RESULT",
+ '{"Shutter1":{"Position":54,"Direction":-1}}',
+ )
+ state = hass.states.get("cover.tasmota_cover_1")
+ assert state.state == "opening"
+ assert state.attributes["current_position"] == 46
+
+ async_fire_mqtt_message(
+ hass,
+ "tasmota_49A3BC/stat/RESULT",
+ '{"Shutter1":{"Position":100,"Direction":1}}',
+ )
+ state = hass.states.get("cover.tasmota_cover_1")
+ assert state.state == "closing"
+ assert state.attributes["current_position"] == 0
+
+ async_fire_mqtt_message(
+ hass, "tasmota_49A3BC/stat/RESULT", '{"Shutter1":{"Position":0,"Direction":0}}'
+ )
+ state = hass.states.get("cover.tasmota_cover_1")
+ assert state.state == "open"
+ assert state.attributes["current_position"] == 100
+
+ async_fire_mqtt_message(
+ hass, "tasmota_49A3BC/stat/RESULT", '{"Shutter1":{"Position":1,"Direction":0}}'
+ )
+ state = hass.states.get("cover.tasmota_cover_1")
+ assert state.state == "open"
+ assert state.attributes["current_position"] == 99
+
+ async_fire_mqtt_message(
+ hass,
+ "tasmota_49A3BC/stat/RESULT",
+ '{"Shutter1":{"Position":100,"Direction":0}}',
+ )
+ state = hass.states.get("cover.tasmota_cover_1")
+ assert state.state == "closed"
+ assert state.attributes["current_position"] == 0
+
+
+async def call_service(hass, entity_id, service, **kwargs):
+ """Call a fan service."""
+ await hass.services.async_call(
+ cover.DOMAIN,
+ service,
+ {"entity_id": entity_id, **kwargs},
+ blocking=True,
+ )
+
+
+async def test_sending_mqtt_commands(hass, mqtt_mock, setup_tasmota):
+ """Test the sending MQTT commands."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["dn"] = "Test"
+ config["rl"][0] = 3
+ config["rl"][1] = 3
+ mac = config["mac"]
+
+ async_fire_mqtt_message(
+ hass,
+ f"{DEFAULT_PREFIX}/{mac}/config",
+ json.dumps(config),
+ )
+ await hass.async_block_till_done()
+
+ async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online")
+ state = hass.states.get("cover.test_cover_1")
+ assert state.state == STATE_UNKNOWN
+ await hass.async_block_till_done()
+ await hass.async_block_till_done()
+ mqtt_mock.async_publish.reset_mock()
+
+ # Close the cover and verify MQTT message is sent
+ await call_service(hass, "cover.test_cover_1", "close_cover")
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/ShutterClose1", "", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ # Tasmota is not optimistic, the state should still be unknown
+ state = hass.states.get("cover.test_cover_1")
+ assert state.state == STATE_UNKNOWN
+
+ # Open the cover and verify MQTT message is sent
+ await call_service(hass, "cover.test_cover_1", "open_cover")
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/ShutterOpen1", "", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ # Stop the cover and verify MQTT message is sent
+ await call_service(hass, "cover.test_cover_1", "stop_cover")
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/ShutterStop1", "", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ # Set position and verify MQTT message is sent
+ await call_service(hass, "cover.test_cover_1", "set_cover_position", position=0)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/ShutterPosition1", "0", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ # Set position and verify MQTT message is sent
+ await call_service(hass, "cover.test_cover_1", "set_cover_position", position=99)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/ShutterPosition1", "99", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+
+async def test_sending_mqtt_commands_inverted(hass, mqtt_mock, setup_tasmota):
+ """Test the sending MQTT commands."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["dn"] = "Test"
+ config["rl"][0] = 3
+ config["rl"][1] = 3
+ config["sho"] = [1] # Inverted cover
+ mac = config["mac"]
+
+ async_fire_mqtt_message(
+ hass,
+ f"{DEFAULT_PREFIX}/{mac}/config",
+ json.dumps(config),
+ )
+ await hass.async_block_till_done()
+
+ async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online")
+ state = hass.states.get("cover.test_cover_1")
+ assert state.state == STATE_UNKNOWN
+ await hass.async_block_till_done()
+ await hass.async_block_till_done()
+ mqtt_mock.async_publish.reset_mock()
+
+ # Close the cover and verify MQTT message is sent
+ await call_service(hass, "cover.test_cover_1", "close_cover")
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/ShutterClose1", "", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ # Tasmota is not optimistic, the state should still be unknown
+ state = hass.states.get("cover.test_cover_1")
+ assert state.state == STATE_UNKNOWN
+
+ # Open the cover and verify MQTT message is sent
+ await call_service(hass, "cover.test_cover_1", "open_cover")
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/ShutterOpen1", "", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ # Stop the cover and verify MQTT message is sent
+ await call_service(hass, "cover.test_cover_1", "stop_cover")
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/ShutterStop1", "", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ # Set position and verify MQTT message is sent
+ await call_service(hass, "cover.test_cover_1", "set_cover_position", position=0)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/ShutterPosition1", "100", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ # Set position and verify MQTT message is sent
+ await call_service(hass, "cover.test_cover_1", "set_cover_position", position=99)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/ShutterPosition1", "1", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+
+async def test_availability_when_connection_lost(
+ hass, mqtt_client_mock, mqtt_mock, setup_tasmota
+):
+ """Test availability after MQTT disconnection."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["dn"] = "Test"
+ config["rl"][0] = 3
+ config["rl"][1] = 3
+ await help_test_availability_when_connection_lost(
+ hass,
+ mqtt_client_mock,
+ mqtt_mock,
+ cover.DOMAIN,
+ config,
+ entity_id="test_cover_1",
+ )
+
+
+async def test_availability(hass, mqtt_mock, setup_tasmota):
+ """Test availability."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["dn"] = "Test"
+ config["rl"][0] = 3
+ config["rl"][1] = 3
+ await help_test_availability(
+ hass, mqtt_mock, cover.DOMAIN, config, entity_id="test_cover_1"
+ )
+
+
+async def test_availability_discovery_update(hass, mqtt_mock, setup_tasmota):
+ """Test availability discovery update."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["dn"] = "Test"
+ config["rl"][0] = 3
+ config["rl"][1] = 3
+ await help_test_availability_discovery_update(
+ hass, mqtt_mock, cover.DOMAIN, config, entity_id="test_cover_1"
+ )
+
+
+async def test_availability_poll_state(
+ hass, mqtt_client_mock, mqtt_mock, setup_tasmota
+):
+ """Test polling after MQTT connection (re)established."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["rl"][0] = 3
+ config["rl"][1] = 3
+ poll_topic = "tasmota_49A3BC/cmnd/STATUS"
+ await help_test_availability_poll_state(
+ hass, mqtt_client_mock, mqtt_mock, cover.DOMAIN, config, poll_topic, "10"
+ )
+
+
+async def test_discovery_removal_cover(hass, mqtt_mock, caplog, setup_tasmota):
+ """Test removal of discovered cover."""
+ config1 = copy.deepcopy(DEFAULT_CONFIG)
+ config1["dn"] = "Test"
+ config1["rl"][0] = 3
+ config1["rl"][1] = 3
+ config2 = copy.deepcopy(DEFAULT_CONFIG)
+ config2["dn"] = "Test"
+ config2["rl"][0] = 0
+ config2["rl"][1] = 0
+
+ await help_test_discovery_removal(
+ hass,
+ mqtt_mock,
+ caplog,
+ cover.DOMAIN,
+ config1,
+ config2,
+ entity_id="test_cover_1",
+ name="Test cover 1",
+ )
+
+
+async def test_discovery_update_unchanged_cover(hass, mqtt_mock, caplog, setup_tasmota):
+ """Test update of discovered cover."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["dn"] = "Test"
+ config["rl"][0] = 3
+ config["rl"][1] = 3
+ with patch(
+ "homeassistant.components.tasmota.cover.TasmotaCover.discovery_update"
+ ) as discovery_update:
+ await help_test_discovery_update_unchanged(
+ hass,
+ mqtt_mock,
+ caplog,
+ cover.DOMAIN,
+ config,
+ discovery_update,
+ entity_id="test_cover_1",
+ name="Test cover 1",
+ )
+
+
+async def test_discovery_device_remove(hass, mqtt_mock, setup_tasmota):
+ """Test device registry remove."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["dn"] = "Test"
+ config["rl"][0] = 3
+ config["rl"][1] = 3
+ unique_id = f"{DEFAULT_CONFIG['mac']}_cover_shutter_0"
+ await help_test_discovery_device_remove(
+ hass, mqtt_mock, cover.DOMAIN, unique_id, config
+ )
+
+
+async def test_entity_id_update_subscriptions(hass, mqtt_mock, setup_tasmota):
+ """Test MQTT subscriptions are managed when entity_id is updated."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["dn"] = "Test"
+ config["rl"][0] = 3
+ config["rl"][1] = 3
+ topics = [
+ get_topic_stat_result(config),
+ get_topic_tele_sensor(config),
+ get_topic_stat_status(config, 10),
+ get_topic_tele_will(config),
+ ]
+ await help_test_entity_id_update_subscriptions(
+ hass, mqtt_mock, cover.DOMAIN, config, topics, entity_id="test_cover_1"
+ )
+
+
+async def test_entity_id_update_discovery_update(hass, mqtt_mock, setup_tasmota):
+ """Test MQTT discovery update when entity_id is updated."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["dn"] = "Test"
+ config["rl"][0] = 3
+ config["rl"][1] = 3
+ await help_test_entity_id_update_discovery_update(
+ hass, mqtt_mock, cover.DOMAIN, config, entity_id="test_cover_1"
+ )
diff --git a/tests/components/tasmota/test_device_trigger.py b/tests/components/tasmota/test_device_trigger.py
index 09c3d691b09..42fc5dc7a49 100644
--- a/tests/components/tasmota/test_device_trigger.py
+++ b/tests/components/tasmota/test_device_trigger.py
@@ -18,6 +18,7 @@ from tests.common import (
async_fire_mqtt_message,
async_get_device_automations,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
async def test_get_triggers(hass, device_reg, entity_reg, mqtt_mock, setup_tasmota):
diff --git a/tests/components/tasmota/test_fan.py b/tests/components/tasmota/test_fan.py
new file mode 100644
index 00000000000..5cadc20218e
--- /dev/null
+++ b/tests/components/tasmota/test_fan.py
@@ -0,0 +1,255 @@
+"""The tests for the Tasmota fan platform."""
+import copy
+import json
+
+from hatasmota.utils import (
+ get_topic_stat_result,
+ get_topic_tele_state,
+ get_topic_tele_will,
+)
+
+from homeassistant.components import fan
+from homeassistant.components.tasmota.const import DEFAULT_PREFIX
+from homeassistant.const import ATTR_ASSUMED_STATE, STATE_OFF, STATE_ON
+
+from .test_common import (
+ DEFAULT_CONFIG,
+ help_test_availability,
+ help_test_availability_discovery_update,
+ help_test_availability_poll_state,
+ help_test_availability_when_connection_lost,
+ help_test_discovery_device_remove,
+ help_test_discovery_removal,
+ help_test_discovery_update_unchanged,
+ help_test_entity_id_update_discovery_update,
+ help_test_entity_id_update_subscriptions,
+)
+
+from tests.async_mock import patch
+from tests.common import async_fire_mqtt_message
+from tests.components.fan import common
+
+
+async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota):
+ """Test state update via MQTT."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["if"] = 1
+ mac = config["mac"]
+
+ async_fire_mqtt_message(
+ hass,
+ f"{DEFAULT_PREFIX}/{mac}/config",
+ json.dumps(config),
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("fan.tasmota")
+ assert state.state == "unavailable"
+ assert not state.attributes.get(ATTR_ASSUMED_STATE)
+
+ async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online")
+ state = hass.states.get("fan.tasmota")
+ assert state.state == STATE_OFF
+ assert state.attributes["speed"] is None
+ assert state.attributes["speed_list"] == ["off", "low", "medium", "high"]
+ assert state.attributes["supported_features"] == fan.SUPPORT_SET_SPEED
+ assert not state.attributes.get(ATTR_ASSUMED_STATE)
+
+ async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"FanSpeed":1}')
+ state = hass.states.get("fan.tasmota")
+ assert state.state == STATE_ON
+ assert state.attributes["speed"] == "low"
+
+ async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"FanSpeed":2}')
+ state = hass.states.get("fan.tasmota")
+ assert state.state == STATE_ON
+ assert state.attributes["speed"] == "medium"
+
+ async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"FanSpeed":3}')
+ state = hass.states.get("fan.tasmota")
+ assert state.state == STATE_ON
+ assert state.attributes["speed"] == "high"
+
+ async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"FanSpeed":0}')
+ state = hass.states.get("fan.tasmota")
+ assert state.state == STATE_OFF
+ assert state.attributes["speed"] == "off"
+
+ async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"FanSpeed":1}')
+ state = hass.states.get("fan.tasmota")
+ assert state.state == STATE_ON
+ assert state.attributes["speed"] == "low"
+
+ async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"FanSpeed":0}')
+ state = hass.states.get("fan.tasmota")
+ assert state.state == STATE_OFF
+ assert state.attributes["speed"] == "off"
+
+
+async def test_sending_mqtt_commands(hass, mqtt_mock, setup_tasmota):
+ """Test the sending MQTT commands."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["if"] = 1
+ mac = config["mac"]
+
+ async_fire_mqtt_message(
+ hass,
+ f"{DEFAULT_PREFIX}/{mac}/config",
+ json.dumps(config),
+ )
+ await hass.async_block_till_done()
+
+ async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online")
+ state = hass.states.get("fan.tasmota")
+ assert state.state == STATE_OFF
+ await hass.async_block_till_done()
+ await hass.async_block_till_done()
+ mqtt_mock.async_publish.reset_mock()
+
+ # Turn the fan on and verify MQTT message is sent
+ await common.async_turn_on(hass, "fan.tasmota")
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/FanSpeed", "2", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ # Tasmota is not optimistic, the state should still be off
+ state = hass.states.get("fan.tasmota")
+ assert state.state == STATE_OFF
+
+ # Turn the fan off and verify MQTT message is sent
+ await common.async_turn_off(hass, "fan.tasmota")
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/FanSpeed", "0", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ # Set speed and verify MQTT message is sent
+ await common.async_set_speed(hass, "fan.tasmota", fan.SPEED_OFF)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/FanSpeed", "0", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ # Set speed and verify MQTT message is sent
+ await common.async_set_speed(hass, "fan.tasmota", fan.SPEED_LOW)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/FanSpeed", "1", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ # Set speed and verify MQTT message is sent
+ await common.async_set_speed(hass, "fan.tasmota", fan.SPEED_MEDIUM)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/FanSpeed", "2", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ # Set speed and verify MQTT message is sent
+ await common.async_set_speed(hass, "fan.tasmota", fan.SPEED_HIGH)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/FanSpeed", "3", 0, False
+ )
+
+
+async def test_availability_when_connection_lost(
+ hass, mqtt_client_mock, mqtt_mock, setup_tasmota
+):
+ """Test availability after MQTT disconnection."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["dn"] = "Test"
+ config["if"] = 1
+ await help_test_availability_when_connection_lost(
+ hass, mqtt_client_mock, mqtt_mock, fan.DOMAIN, config
+ )
+
+
+async def test_availability(hass, mqtt_mock, setup_tasmota):
+ """Test availability."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["dn"] = "Test"
+ config["if"] = 1
+ await help_test_availability(hass, mqtt_mock, fan.DOMAIN, config)
+
+
+async def test_availability_discovery_update(hass, mqtt_mock, setup_tasmota):
+ """Test availability discovery update."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["dn"] = "Test"
+ config["if"] = 1
+ await help_test_availability_discovery_update(hass, mqtt_mock, fan.DOMAIN, config)
+
+
+async def test_availability_poll_state(
+ hass, mqtt_client_mock, mqtt_mock, setup_tasmota
+):
+ """Test polling after MQTT connection (re)established."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["if"] = 1
+ poll_topic = "tasmota_49A3BC/cmnd/STATE"
+ await help_test_availability_poll_state(
+ hass, mqtt_client_mock, mqtt_mock, fan.DOMAIN, config, poll_topic, ""
+ )
+
+
+async def test_discovery_removal_fan(hass, mqtt_mock, caplog, setup_tasmota):
+ """Test removal of discovered fan."""
+ config1 = copy.deepcopy(DEFAULT_CONFIG)
+ config1["dn"] = "Test"
+ config1["if"] = 1
+ config2 = copy.deepcopy(DEFAULT_CONFIG)
+ config2["dn"] = "Test"
+ config2["if"] = 0
+
+ await help_test_discovery_removal(
+ hass, mqtt_mock, caplog, fan.DOMAIN, config1, config2
+ )
+
+
+async def test_discovery_update_unchanged_fan(hass, mqtt_mock, caplog, setup_tasmota):
+ """Test update of discovered fan."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["dn"] = "Test"
+ config["if"] = 1
+ with patch(
+ "homeassistant.components.tasmota.fan.TasmotaFan.discovery_update"
+ ) as discovery_update:
+ await help_test_discovery_update_unchanged(
+ hass, mqtt_mock, caplog, fan.DOMAIN, config, discovery_update
+ )
+
+
+async def test_discovery_device_remove(hass, mqtt_mock, setup_tasmota):
+ """Test device registry remove."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["dn"] = "Test"
+ config["if"] = 1
+ unique_id = f"{DEFAULT_CONFIG['mac']}_fan_fan_ifan"
+ await help_test_discovery_device_remove(
+ hass, mqtt_mock, fan.DOMAIN, unique_id, config
+ )
+
+
+async def test_entity_id_update_subscriptions(hass, mqtt_mock, setup_tasmota):
+ """Test MQTT subscriptions are managed when entity_id is updated."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["dn"] = "Test"
+ config["if"] = 1
+ topics = [
+ get_topic_stat_result(config),
+ get_topic_tele_state(config),
+ get_topic_tele_will(config),
+ ]
+ await help_test_entity_id_update_subscriptions(
+ hass, mqtt_mock, fan.DOMAIN, config, topics
+ )
+
+
+async def test_entity_id_update_discovery_update(hass, mqtt_mock, setup_tasmota):
+ """Test MQTT discovery update when entity_id is updated."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["dn"] = "Test"
+ config["if"] = 1
+ await help_test_entity_id_update_discovery_update(
+ hass, mqtt_mock, fan.DOMAIN, config
+ )
diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py
index 9210c577a5e..f09c27da753 100644
--- a/tests/components/tasmota/test_light.py
+++ b/tests/components/tasmota/test_light.py
@@ -62,6 +62,30 @@ async def test_attributes_on_off(hass, mqtt_mock, setup_tasmota):
assert state.attributes.get("supported_features") == 0
+async def test_attributes_dimmer_tuya(hass, mqtt_mock, setup_tasmota):
+ """Test state update via MQTT."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["rl"][0] = 2
+ config["lt_st"] = 1 # 1 channel light (dimmer)
+ config["ty"] = 1 # Tuya device
+ mac = config["mac"]
+
+ async_fire_mqtt_message(
+ hass,
+ f"{DEFAULT_PREFIX}/{mac}/config",
+ json.dumps(config),
+ )
+ await hass.async_block_till_done()
+ async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online")
+ async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}')
+
+ state = hass.states.get("light.test")
+ assert state.attributes.get("effect_list") is None
+ assert state.attributes.get("min_mireds") is None
+ assert state.attributes.get("max_mireds") is None
+ assert state.attributes.get("supported_features") == SUPPORT_BRIGHTNESS
+
+
async def test_attributes_dimmer(hass, mqtt_mock, setup_tasmota):
"""Test state update via MQTT."""
config = copy.deepcopy(DEFAULT_CONFIG)
@@ -469,6 +493,191 @@ async def test_controlling_state_via_mqtt_rgbww(hass, mqtt_mock, setup_tasmota):
assert state.state == STATE_OFF
+async def test_controlling_state_via_mqtt_rgbww_hex(hass, mqtt_mock, setup_tasmota):
+ """Test state update via MQTT."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["rl"][0] = 2
+ config["lt_st"] = 5 # 5 channel light (RGBCW)
+ config["so"]["17"] = 0 # Hex color in state updates
+ mac = config["mac"]
+
+ async_fire_mqtt_message(
+ hass,
+ f"{DEFAULT_PREFIX}/{mac}/config",
+ json.dumps(config),
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("light.test")
+ assert state.state == "unavailable"
+ assert not state.attributes.get(ATTR_ASSUMED_STATE)
+
+ async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online")
+ state = hass.states.get("light.test")
+ assert state.state == STATE_OFF
+ assert not state.attributes.get(ATTR_ASSUMED_STATE)
+
+ async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}')
+ state = hass.states.get("light.test")
+ assert state.state == STATE_ON
+
+ async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}')
+ state = hass.states.get("light.test")
+ assert state.state == STATE_OFF
+
+ async_fire_mqtt_message(
+ hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50}'
+ )
+ state = hass.states.get("light.test")
+ assert state.state == STATE_ON
+ assert state.attributes.get("brightness") == 127.5
+
+ async_fire_mqtt_message(
+ hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Color":"FF8000"}'
+ )
+ state = hass.states.get("light.test")
+ assert state.state == STATE_ON
+ assert state.attributes.get("rgb_color") == (255, 128, 0)
+
+ async_fire_mqtt_message(
+ hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Color":"00FF800000"}'
+ )
+ state = hass.states.get("light.test")
+ assert state.state == STATE_ON
+ assert state.attributes.get("rgb_color") == (0, 255, 128)
+
+ async_fire_mqtt_message(
+ hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":50}'
+ )
+ state = hass.states.get("light.test")
+ assert state.state == STATE_ON
+ assert state.attributes.get("white_value") == 127.5
+ # Setting white > 0 should clear the color
+ assert not state.attributes.get("rgb_color")
+
+ async_fire_mqtt_message(
+ hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","CT":300}'
+ )
+ state = hass.states.get("light.test")
+ assert state.state == STATE_ON
+ assert state.attributes.get("color_temp") == 300
+
+ async_fire_mqtt_message(
+ hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":0}'
+ )
+ state = hass.states.get("light.test")
+ assert state.state == STATE_ON
+ # Setting white to 0 should clear the white_value and color_temp
+ assert not state.attributes.get("white_value")
+ assert not state.attributes.get("color_temp")
+
+ async_fire_mqtt_message(
+ hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Scheme":3}'
+ )
+ state = hass.states.get("light.test")
+ assert state.state == STATE_ON
+ assert state.attributes.get("effect") == "Cycle down"
+
+ async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"ON"}')
+
+ state = hass.states.get("light.test")
+ assert state.state == STATE_ON
+
+ async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"OFF"}')
+
+ state = hass.states.get("light.test")
+ assert state.state == STATE_OFF
+
+
+async def test_controlling_state_via_mqtt_rgbww_tuya(hass, mqtt_mock, setup_tasmota):
+ """Test state update via MQTT."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["rl"][0] = 2
+ config["lt_st"] = 5 # 5 channel light (RGBCW)
+ config["ty"] = 1 # Tuya device
+ mac = config["mac"]
+
+ async_fire_mqtt_message(
+ hass,
+ f"{DEFAULT_PREFIX}/{mac}/config",
+ json.dumps(config),
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get("light.test")
+ assert state.state == "unavailable"
+ assert not state.attributes.get(ATTR_ASSUMED_STATE)
+
+ async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online")
+ state = hass.states.get("light.test")
+ assert state.state == STATE_OFF
+ assert not state.attributes.get(ATTR_ASSUMED_STATE)
+
+ async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}')
+ state = hass.states.get("light.test")
+ assert state.state == STATE_ON
+
+ async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}')
+ state = hass.states.get("light.test")
+ assert state.state == STATE_OFF
+
+ async_fire_mqtt_message(
+ hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50}'
+ )
+ state = hass.states.get("light.test")
+ assert state.state == STATE_ON
+ assert state.attributes.get("brightness") == 127.5
+
+ async_fire_mqtt_message(
+ hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Color":"255,128,0"}'
+ )
+ state = hass.states.get("light.test")
+ assert state.state == STATE_ON
+ assert state.attributes.get("rgb_color") == (255, 128, 0)
+
+ async_fire_mqtt_message(
+ hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":50}'
+ )
+ state = hass.states.get("light.test")
+ assert state.state == STATE_ON
+ assert state.attributes.get("white_value") == 127.5
+ # Setting white > 0 should clear the color
+ assert not state.attributes.get("rgb_color")
+
+ async_fire_mqtt_message(
+ hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","CT":300}'
+ )
+ state = hass.states.get("light.test")
+ assert state.state == STATE_ON
+ assert state.attributes.get("color_temp") == 300
+
+ async_fire_mqtt_message(
+ hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":0}'
+ )
+ state = hass.states.get("light.test")
+ assert state.state == STATE_ON
+ # Setting white to 0 should clear the white_value and color_temp
+ assert not state.attributes.get("white_value")
+ assert not state.attributes.get("color_temp")
+
+ async_fire_mqtt_message(
+ hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Scheme":3}'
+ )
+ state = hass.states.get("light.test")
+ assert state.state == STATE_ON
+ assert state.attributes.get("effect") == "Cycle down"
+
+ async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"ON"}')
+
+ state = hass.states.get("light.test")
+ assert state.state == STATE_ON
+
+ async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"OFF"}')
+
+ state = hass.states.get("light.test")
+ assert state.state == STATE_OFF
+
+
async def test_sending_mqtt_commands_on_off(hass, mqtt_mock, setup_tasmota):
"""Test the sending MQTT commands."""
config = copy.deepcopy(DEFAULT_CONFIG)
@@ -509,6 +718,53 @@ async def test_sending_mqtt_commands_on_off(hass, mqtt_mock, setup_tasmota):
mqtt_mock.async_publish.reset_mock()
+async def test_sending_mqtt_commands_rgbww_tuya(hass, mqtt_mock, setup_tasmota):
+ """Test the sending MQTT commands."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config["rl"][0] = 2
+ config["lt_st"] = 5 # 5 channel light (RGBCW)
+ config["ty"] = 1 # Tuya device
+ mac = config["mac"]
+
+ async_fire_mqtt_message(
+ hass,
+ f"{DEFAULT_PREFIX}/{mac}/config",
+ json.dumps(config),
+ )
+ await hass.async_block_till_done()
+
+ async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online")
+ state = hass.states.get("light.test")
+ assert state.state == STATE_OFF
+ await hass.async_block_till_done()
+ await hass.async_block_till_done()
+ mqtt_mock.async_publish.reset_mock()
+
+ # Turn the light on and verify MQTT message is sent
+ await common.async_turn_on(hass, "light.test")
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ # Tasmota is not optimistic, the state should still be off
+ state = hass.states.get("light.test")
+ assert state.state == STATE_OFF
+
+ # Turn the light off and verify MQTT message is sent
+ await common.async_turn_off(hass, "light.test")
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 OFF", 0, False
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ # Turn the light on and verify MQTT messages are sent
+ await common.async_turn_on(hass, "light.test", brightness=192)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Dimmer3 75", 0, False
+ )
+
+
async def test_sending_mqtt_commands_rgbww(hass, mqtt_mock, setup_tasmota):
"""Test the sending MQTT commands."""
config = copy.deepcopy(DEFAULT_CONFIG)
@@ -674,11 +930,31 @@ async def test_transition(hass, mqtt_mock, setup_tasmota):
)
mqtt_mock.async_publish.reset_mock()
- # Dim the light from 0->50: Speed should be 4*2/2=4
+ # Dim the light from 0->100: Speed should be capped at 40
+ await common.async_turn_on(hass, "light.test", brightness=255, transition=100)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/Backlog",
+ "NoDelay;Fade 1;NoDelay;Speed 40;NoDelay;Dimmer 100",
+ 0,
+ False,
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ # Dim the light from 0->0: Speed should be 1
+ await common.async_turn_on(hass, "light.test", brightness=0, transition=100)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/Backlog",
+ "NoDelay;Fade 1;NoDelay;Speed 1;NoDelay;Power1 OFF",
+ 0,
+ False,
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ # Dim the light from 0->50: Speed should be 4*2*2=16
await common.async_turn_on(hass, "light.test", brightness=128, transition=4)
mqtt_mock.async_publish.assert_called_once_with(
"tasmota_49A3BC/cmnd/Backlog",
- "NoDelay;Fade 1;NoDelay;Speed 4;NoDelay;Dimmer 50",
+ "NoDelay;Fade 1;NoDelay;Speed 16;NoDelay;Dimmer 50",
0,
False,
)
@@ -692,11 +968,91 @@ async def test_transition(hass, mqtt_mock, setup_tasmota):
assert state.state == STATE_ON
assert state.attributes.get("brightness") == 127.5
- # Dim the light from 50->0: Speed should be 6*2/2=6
+ # Dim the light from 50->0: Speed should be 6*2*2=24
await common.async_turn_off(hass, "light.test", transition=6)
mqtt_mock.async_publish.assert_called_once_with(
"tasmota_49A3BC/cmnd/Backlog",
- "NoDelay;Fade 1;NoDelay;Speed 6;NoDelay;Power1 OFF",
+ "NoDelay;Fade 1;NoDelay;Speed 24;NoDelay;Power1 OFF",
+ 0,
+ False,
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ # Fake state update from the light
+ async_fire_mqtt_message(
+ hass,
+ "tasmota_49A3BC/tele/STATE",
+ '{"POWER":"ON","Dimmer":50, "Color":"0,255,0"}',
+ )
+ state = hass.states.get("light.test")
+ assert state.state == STATE_ON
+ assert state.attributes.get("brightness") == 127.5
+ assert state.attributes.get("rgb_color") == (0, 255, 0)
+
+ # Set color of the light from 0,255,0 to 255,0,0 @ 50%: Speed should be 6*2*2=24
+ await common.async_turn_on(hass, "light.test", rgb_color=[255, 0, 0], transition=6)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/Backlog",
+ "NoDelay;Fade 1;NoDelay;Speed 24;NoDelay;Power1 ON;NoDelay;Color2 255,0,0",
+ 0,
+ False,
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ # Fake state update from the light
+ async_fire_mqtt_message(
+ hass,
+ "tasmota_49A3BC/tele/STATE",
+ '{"POWER":"ON","Dimmer":100, "Color":"0,255,0"}',
+ )
+ state = hass.states.get("light.test")
+ assert state.state == STATE_ON
+ assert state.attributes.get("brightness") == 255
+ assert state.attributes.get("rgb_color") == (0, 255, 0)
+
+ # Set color of the light from 0,255,0 to 255,0,0 @ 100%: Speed should be 6*2=12
+ await common.async_turn_on(hass, "light.test", rgb_color=[255, 0, 0], transition=6)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/Backlog",
+ "NoDelay;Fade 1;NoDelay;Speed 12;NoDelay;Power1 ON;NoDelay;Color2 255,0,0",
+ 0,
+ False,
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ # Fake state update from the light
+ async_fire_mqtt_message(
+ hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50, "CT":153}'
+ )
+ state = hass.states.get("light.test")
+ assert state.state == STATE_ON
+ assert state.attributes.get("brightness") == 127.5
+ assert state.attributes.get("color_temp") == 153
+
+ # Set color_temp of the light from 153 to 500 @ 50%: Speed should be 6*2*2=24
+ await common.async_turn_on(hass, "light.test", color_temp=500, transition=6)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/Backlog",
+ "NoDelay;Fade 1;NoDelay;Speed 24;NoDelay;Power1 ON;NoDelay;CT 500",
+ 0,
+ False,
+ )
+ mqtt_mock.async_publish.reset_mock()
+
+ # Fake state update from the light
+ async_fire_mqtt_message(
+ hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50, "CT":500}'
+ )
+ state = hass.states.get("light.test")
+ assert state.state == STATE_ON
+ assert state.attributes.get("brightness") == 127.5
+ assert state.attributes.get("color_temp") == 500
+
+ # Set color_temp of the light from 500 to 326 @ 50%: Speed should be 6*2*2*2=48->40
+ await common.async_turn_on(hass, "light.test", color_temp=326, transition=6)
+ mqtt_mock.async_publish.assert_called_once_with(
+ "tasmota_49A3BC/cmnd/Backlog",
+ "NoDelay;Fade 1;NoDelay;Speed 40;NoDelay;Power1 ON;NoDelay;CT 326",
0,
False,
)
diff --git a/tests/components/tellduslive/test_config_flow.py b/tests/components/tellduslive/test_config_flow.py
index 66476c8735d..7417c87c229 100644
--- a/tests/components/tellduslive/test_config_flow.py
+++ b/tests/components/tellduslive/test_config_flow.py
@@ -229,7 +229,7 @@ async def test_abort_no_auth_url(hass, mock_tellduslive):
result = await flow.async_step_user()
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "authorize_url_fail"
+ assert result["reason"] == "unknown_authorize_url_generation"
async def test_abort_if_exception_generating_auth_url(hass, mock_tellduslive):
@@ -238,7 +238,7 @@ async def test_abort_if_exception_generating_auth_url(hass, mock_tellduslive):
result = await flow.async_step_user()
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
- assert result["reason"] == "authorize_url_fail"
+ assert result["reason"] == "unknown_authorize_url_generation"
async def test_discovery_already_configured(hass, mock_tellduslive):
diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py
index a6aa253e746..e8ff4c83f8d 100644
--- a/tests/components/template/test_binary_sensor.py
+++ b/tests/components/template/test_binary_sensor.py
@@ -383,6 +383,269 @@ async def test_template_delay_off(hass):
assert state.state == "on"
+async def test_template_with_templated_delay_on(hass):
+ """Test binary sensor template with template delay on."""
+ config = {
+ "binary_sensor": {
+ "platform": "template",
+ "sensors": {
+ "test": {
+ "friendly_name": "virtual thingy",
+ "value_template": "{{ states.sensor.test_state.state == 'on' }}",
+ "device_class": "motion",
+ "delay_on": '{{ ({ "seconds": 6 / 2 }) }}',
+ }
+ },
+ }
+ }
+ await setup.async_setup_component(hass, binary_sensor.DOMAIN, config)
+ await hass.async_block_till_done()
+ await hass.async_start()
+
+ hass.states.async_set("sensor.test_state", "on")
+ await hass.async_block_till_done()
+
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == "off"
+
+ future = dt_util.utcnow() + timedelta(seconds=3)
+ async_fire_time_changed(hass, future)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == "on"
+
+ # check with time changes
+ hass.states.async_set("sensor.test_state", "off")
+ await hass.async_block_till_done()
+
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == "off"
+
+ hass.states.async_set("sensor.test_state", "on")
+ await hass.async_block_till_done()
+
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == "off"
+
+ hass.states.async_set("sensor.test_state", "off")
+ await hass.async_block_till_done()
+
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == "off"
+
+ future = dt_util.utcnow() + timedelta(seconds=3)
+ async_fire_time_changed(hass, future)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == "off"
+
+
+async def test_template_with_templated_delay_off(hass):
+ """Test binary sensor template with template delay off."""
+ config = {
+ "binary_sensor": {
+ "platform": "template",
+ "sensors": {
+ "test": {
+ "friendly_name": "virtual thingy",
+ "value_template": "{{ states.sensor.test_state.state == 'on' }}",
+ "device_class": "motion",
+ "delay_off": '{{ ({ "seconds": 6 / 2 }) }}',
+ }
+ },
+ }
+ }
+ hass.states.async_set("sensor.test_state", "on")
+ await setup.async_setup_component(hass, binary_sensor.DOMAIN, config)
+ await hass.async_block_till_done()
+ await hass.async_start()
+
+ hass.states.async_set("sensor.test_state", "off")
+ await hass.async_block_till_done()
+
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == "on"
+
+ future = dt_util.utcnow() + timedelta(seconds=3)
+ async_fire_time_changed(hass, future)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == "off"
+
+ # check with time changes
+ hass.states.async_set("sensor.test_state", "on")
+ await hass.async_block_till_done()
+
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == "on"
+
+ hass.states.async_set("sensor.test_state", "off")
+ await hass.async_block_till_done()
+
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == "on"
+
+ hass.states.async_set("sensor.test_state", "on")
+ await hass.async_block_till_done()
+
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == "on"
+
+ future = dt_util.utcnow() + timedelta(seconds=3)
+ async_fire_time_changed(hass, future)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == "on"
+
+
+async def test_template_with_delay_on_based_on_input(hass):
+ """Test binary sensor template with template delay on based on input number."""
+ config = {
+ "binary_sensor": {
+ "platform": "template",
+ "sensors": {
+ "test": {
+ "friendly_name": "virtual thingy",
+ "value_template": "{{ states.sensor.test_state.state == 'on' }}",
+ "device_class": "motion",
+ "delay_on": '{{ ({ "seconds": states("input_number.delay")|int }) }}',
+ }
+ },
+ }
+ }
+ await setup.async_setup_component(hass, binary_sensor.DOMAIN, config)
+ await hass.async_block_till_done()
+ await hass.async_start()
+
+ hass.states.async_set("sensor.test_state", "off")
+ await hass.async_block_till_done()
+
+ hass.states.async_set("input_number.delay", 3)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == "off"
+
+ hass.states.async_set("sensor.test_state", "on")
+ await hass.async_block_till_done()
+
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == "off"
+
+ future = dt_util.utcnow() + timedelta(seconds=3)
+ async_fire_time_changed(hass, future)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == "on"
+
+ # set input to 4 seconds
+ hass.states.async_set("sensor.test_state", "off")
+ await hass.async_block_till_done()
+
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == "off"
+
+ hass.states.async_set("input_number.delay", 4)
+ await hass.async_block_till_done()
+
+ hass.states.async_set("sensor.test_state", "on")
+ await hass.async_block_till_done()
+
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == "off"
+
+ future = dt_util.utcnow() + timedelta(seconds=2)
+ async_fire_time_changed(hass, future)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == "off"
+
+ future = dt_util.utcnow() + timedelta(seconds=4)
+ async_fire_time_changed(hass, future)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == "on"
+
+
+async def test_template_with_delay_off_based_on_input(hass):
+ """Test binary sensor template with template delay off based on input number."""
+ config = {
+ "binary_sensor": {
+ "platform": "template",
+ "sensors": {
+ "test": {
+ "friendly_name": "virtual thingy",
+ "value_template": "{{ states.sensor.test_state.state == 'on' }}",
+ "device_class": "motion",
+ "delay_off": '{{ ({ "seconds": states("input_number.delay")|int }) }}',
+ }
+ },
+ }
+ }
+ await setup.async_setup_component(hass, binary_sensor.DOMAIN, config)
+ await hass.async_block_till_done()
+ await hass.async_start()
+
+ hass.states.async_set("sensor.test_state", "on")
+ await hass.async_block_till_done()
+
+ hass.states.async_set("input_number.delay", 3)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == "on"
+
+ hass.states.async_set("sensor.test_state", "off")
+ await hass.async_block_till_done()
+
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == "on"
+
+ future = dt_util.utcnow() + timedelta(seconds=3)
+ async_fire_time_changed(hass, future)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == "off"
+
+ # set input to 4 seconds
+ hass.states.async_set("sensor.test_state", "on")
+ await hass.async_block_till_done()
+
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == "on"
+
+ hass.states.async_set("input_number.delay", 4)
+ await hass.async_block_till_done()
+
+ hass.states.async_set("sensor.test_state", "off")
+ await hass.async_block_till_done()
+
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == "on"
+
+ future = dt_util.utcnow() + timedelta(seconds=2)
+ async_fire_time_changed(hass, future)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == "on"
+
+ future = dt_util.utcnow() + timedelta(seconds=4)
+ async_fire_time_changed(hass, future)
+ await hass.async_block_till_done()
+
+ state = hass.states.get("binary_sensor.test")
+ assert state.state == "off"
+
+
async def test_available_without_availability_template(hass):
"""Ensure availability is true without an availability_template."""
config = {
diff --git a/tests/components/template/test_trigger.py b/tests/components/template/test_trigger.py
index 4bda4dc23ca..828cf1fb7b4 100644
--- a/tests/components/template/test_trigger.py
+++ b/tests/components/template/test_trigger.py
@@ -17,6 +17,7 @@ from tests.common import (
async_mock_service,
mock_component,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/time_date/test_sensor.py b/tests/components/time_date/test_sensor.py
index a20afb61fe3..b502dad3ea6 100644
--- a/tests/components/time_date/test_sensor.py
+++ b/tests/components/time_date/test_sensor.py
@@ -20,18 +20,21 @@ def restore_ts():
async def test_intervals(hass):
"""Test timing intervals of sensors."""
device = time_date.TimeDateSensor(hass, "time")
- now = dt_util.utc_from_timestamp(45)
- next_time = device.get_next_interval(now)
+ now = dt_util.utc_from_timestamp(45.5)
+ with patch("homeassistant.util.dt.utcnow", return_value=now):
+ next_time = device.get_next_interval()
assert next_time == dt_util.utc_from_timestamp(60)
device = time_date.TimeDateSensor(hass, "beat")
- now = dt_util.utc_from_timestamp(29)
- next_time = device.get_next_interval(now)
- assert next_time == dt_util.utc_from_timestamp(86.4)
+ now = dt_util.parse_datetime("2020-11-13 00:00:29+01:00")
+ with patch("homeassistant.util.dt.utcnow", return_value=now):
+ next_time = device.get_next_interval()
+ assert next_time == dt_util.parse_datetime("2020-11-13 00:01:26.4+01:00")
device = time_date.TimeDateSensor(hass, "date_time")
now = dt_util.utc_from_timestamp(1495068899)
- next_time = device.get_next_interval(now)
+ with patch("homeassistant.util.dt.utcnow", return_value=now):
+ next_time = device.get_next_interval()
assert next_time == dt_util.utc_from_timestamp(1495068900)
now = dt_util.utcnow()
@@ -66,6 +69,8 @@ async def test_states(hass):
device = time_date.TimeDateSensor(hass, "beat")
device._update_internal_state(now)
assert device.state == "@079"
+ device._update_internal_state(dt_util.utc_from_timestamp(1602952963.2))
+ assert device.state == "@738"
device = time_date.TimeDateSensor(hass, "date_time_iso")
device._update_internal_state(now)
@@ -117,7 +122,8 @@ async def test_timezone_intervals(hass):
device = time_date.TimeDateSensor(hass, "date")
now = dt_util.utc_from_timestamp(50000)
- next_time = device.get_next_interval(now)
+ with patch("homeassistant.util.dt.utcnow", return_value=now):
+ next_time = device.get_next_interval()
# start of local day in EST was 18000.0
# so the second day was 18000 + 86400
assert next_time.timestamp() == 104400
@@ -127,9 +133,36 @@ async def test_timezone_intervals(hass):
dt_util.set_default_time_zone(new_tz)
now = dt_util.parse_datetime("2017-11-13 19:47:19-07:00")
device = time_date.TimeDateSensor(hass, "date")
- next_time = device.get_next_interval(now)
+ with patch("homeassistant.util.dt.utcnow", return_value=now):
+ next_time = device.get_next_interval()
assert next_time.timestamp() == dt_util.as_timestamp("2017-11-14 00:00:00-07:00")
+ # Entering DST
+ new_tz = dt_util.get_time_zone("Europe/Prague")
+ assert new_tz is not None
+ dt_util.set_default_time_zone(new_tz)
+
+ now = dt_util.parse_datetime("2020-03-29 00:00+01:00")
+ with patch("homeassistant.util.dt.utcnow", return_value=now):
+ next_time = device.get_next_interval()
+ assert next_time.timestamp() == dt_util.as_timestamp("2020-03-30 00:00+02:00")
+
+ now = dt_util.parse_datetime("2020-03-29 03:00+02:00")
+ with patch("homeassistant.util.dt.utcnow", return_value=now):
+ next_time = device.get_next_interval()
+ assert next_time.timestamp() == dt_util.as_timestamp("2020-03-30 00:00+02:00")
+
+ # Leaving DST
+ now = dt_util.parse_datetime("2020-10-25 00:00+02:00")
+ with patch("homeassistant.util.dt.utcnow", return_value=now):
+ next_time = device.get_next_interval()
+ assert next_time.timestamp() == dt_util.as_timestamp("2020-10-26 00:00:00+01:00")
+
+ now = dt_util.parse_datetime("2020-10-25 23:59+01:00")
+ with patch("homeassistant.util.dt.utcnow", return_value=now):
+ next_time = device.get_next_interval()
+ assert next_time.timestamp() == dt_util.as_timestamp("2020-10-26 00:00:00+01:00")
+
@patch(
"homeassistant.util.dt.utcnow",
diff --git a/tests/components/toon/test_config_flow.py b/tests/components/toon/test_config_flow.py
index 6fb7a7b53dc..b7eb3898b47 100644
--- a/tests/components/toon/test_config_flow.py
+++ b/tests/components/toon/test_config_flow.py
@@ -40,7 +40,7 @@ async def test_abort_if_no_configuration(hass):
async def test_full_flow_implementation(
- hass, aiohttp_client, aioclient_mock, current_request
+ hass, aiohttp_client, aioclient_mock, current_request_with_host
):
"""Test registering an integration and finishing flow works."""
await setup_component(hass)
@@ -53,7 +53,13 @@ async def test_full_flow_implementation(
assert result["step_id"] == "pick_implementation"
# pylint: disable=protected-access
- state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]})
+ state = config_entry_oauth2_flow._encode_jwt(
+ hass,
+ {
+ "flow_id": result["flow_id"],
+ "redirect_uri": "https://example.com/auth/external/callback",
+ },
+ )
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], {"implementation": "eneco"}
@@ -97,7 +103,9 @@ async def test_full_flow_implementation(
}
-async def test_no_agreements(hass, aiohttp_client, aioclient_mock, current_request):
+async def test_no_agreements(
+ hass, aiohttp_client, aioclient_mock, current_request_with_host
+):
"""Test abort when there are no displays."""
await setup_component(hass)
result = await hass.config_entries.flow.async_init(
@@ -105,7 +113,13 @@ async def test_no_agreements(hass, aiohttp_client, aioclient_mock, current_reque
)
# pylint: disable=protected-access
- state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]})
+ state = config_entry_oauth2_flow._encode_jwt(
+ hass,
+ {
+ "flow_id": result["flow_id"],
+ "redirect_uri": "https://example.com/auth/external/callback",
+ },
+ )
await hass.config_entries.flow.async_configure(
result["flow_id"], {"implementation": "eneco"}
)
@@ -130,7 +144,7 @@ async def test_no_agreements(hass, aiohttp_client, aioclient_mock, current_reque
async def test_multiple_agreements(
- hass, aiohttp_client, aioclient_mock, current_request
+ hass, aiohttp_client, aioclient_mock, current_request_with_host
):
"""Test abort when there are no displays."""
await setup_component(hass)
@@ -139,7 +153,13 @@ async def test_multiple_agreements(
)
# pylint: disable=protected-access
- state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]})
+ state = config_entry_oauth2_flow._encode_jwt(
+ hass,
+ {
+ "flow_id": result["flow_id"],
+ "redirect_uri": "https://example.com/auth/external/callback",
+ },
+ )
await hass.config_entries.flow.async_configure(
result["flow_id"], {"implementation": "eneco"}
)
@@ -174,7 +194,7 @@ async def test_multiple_agreements(
async def test_agreement_already_set_up(
- hass, aiohttp_client, aioclient_mock, current_request
+ hass, aiohttp_client, aioclient_mock, current_request_with_host
):
"""Test showing display form again if display already exists."""
await setup_component(hass)
@@ -184,7 +204,13 @@ async def test_agreement_already_set_up(
)
# pylint: disable=protected-access
- state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]})
+ state = config_entry_oauth2_flow._encode_jwt(
+ hass,
+ {
+ "flow_id": result["flow_id"],
+ "redirect_uri": "https://example.com/auth/external/callback",
+ },
+ )
await hass.config_entries.flow.async_configure(
result["flow_id"], {"implementation": "eneco"}
)
@@ -208,14 +234,22 @@ async def test_agreement_already_set_up(
assert result3["reason"] == "already_configured"
-async def test_toon_abort(hass, aiohttp_client, aioclient_mock, current_request):
+async def test_toon_abort(
+ hass, aiohttp_client, aioclient_mock, current_request_with_host
+):
"""Test we abort on Toon error."""
await setup_component(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
# pylint: disable=protected-access
- state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]})
+ state = config_entry_oauth2_flow._encode_jwt(
+ hass,
+ {
+ "flow_id": result["flow_id"],
+ "redirect_uri": "https://example.com/auth/external/callback",
+ },
+ )
await hass.config_entries.flow.async_configure(
result["flow_id"], {"implementation": "eneco"}
)
@@ -239,7 +273,7 @@ async def test_toon_abort(hass, aiohttp_client, aioclient_mock, current_request)
assert result2["reason"] == "connection_error"
-async def test_import(hass):
+async def test_import(hass, current_request_with_host):
"""Test if importing step works."""
await setup_component(hass)
@@ -253,7 +287,9 @@ async def test_import(hass):
assert result["reason"] == "already_in_progress"
-async def test_import_migration(hass, aiohttp_client, aioclient_mock, current_request):
+async def test_import_migration(
+ hass, aiohttp_client, aioclient_mock, current_request_with_host
+):
"""Test if importing step with migration works."""
old_entry = MockConfigEntry(domain=DOMAIN, unique_id=123, version=1)
old_entry.add_to_hass(hass)
@@ -269,7 +305,13 @@ async def test_import_migration(hass, aiohttp_client, aioclient_mock, current_re
assert flows[0]["context"][CONF_MIGRATE] == old_entry.entry_id
# pylint: disable=protected-access
- state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": flows[0]["flow_id"]})
+ state = config_entry_oauth2_flow._encode_jwt(
+ hass,
+ {
+ "flow_id": flows[0]["flow_id"],
+ "redirect_uri": "https://example.com/auth/external/callback",
+ },
+ )
await hass.config_entries.flow.async_configure(
flows[0]["flow_id"], {"implementation": "eneco"}
)
diff --git a/tests/components/twinkly/__init__.py b/tests/components/twinkly/__init__.py
new file mode 100644
index 00000000000..96f9f450b8a
--- /dev/null
+++ b/tests/components/twinkly/__init__.py
@@ -0,0 +1,69 @@
+"""Constants and mock for the twkinly component tests."""
+
+from uuid import uuid4
+
+from aiohttp.client_exceptions import ClientConnectionError
+
+from homeassistant.components.twinkly.const import DEV_NAME
+
+TEST_HOST = "test.twinkly.com"
+TEST_ID = "twinkly_test_device_id"
+TEST_NAME = "twinkly_test_device_name"
+TEST_NAME_ORIGINAL = "twinkly_test_original_device_name" # the original (deprecated) name stored in the conf
+TEST_MODEL = "twinkly_test_device_model"
+
+
+class ClientMock:
+ """A mock of the twinkly_client.TwinklyClient."""
+
+ def __init__(self) -> None:
+ """Create a mocked client."""
+ self.is_offline = False
+ self.is_on = True
+ self.brightness = 10
+
+ self.id = str(uuid4())
+ self.device_info = {
+ "uuid": self.id,
+ "device_name": self.id, # we make sure that entity id is different for each test
+ "product_code": TEST_MODEL,
+ }
+
+ @property
+ def host(self) -> str:
+ """Get the mocked host."""
+ return TEST_HOST
+
+ async def get_device_info(self):
+ """Get the mocked device info."""
+ if self.is_offline:
+ raise ClientConnectionError()
+ return self.device_info
+
+ async def get_is_on(self) -> bool:
+ """Get the mocked on/off state."""
+ if self.is_offline:
+ raise ClientConnectionError()
+ return self.is_on
+
+ async def set_is_on(self, is_on: bool) -> None:
+ """Set the mocked on/off state."""
+ if self.is_offline:
+ raise ClientConnectionError()
+ self.is_on = is_on
+
+ async def get_brightness(self) -> int:
+ """Get the mocked brightness."""
+ if self.is_offline:
+ raise ClientConnectionError()
+ return self.brightness
+
+ async def set_brightness(self, brightness: int) -> None:
+ """Set the mocked brightness."""
+ if self.is_offline:
+ raise ClientConnectionError()
+ self.brightness = brightness
+
+ def change_name(self, new_name: str) -> None:
+ """Change the name of this virtual device."""
+ self.device_info[DEV_NAME] = new_name
diff --git a/tests/components/twinkly/test_config_flow.py b/tests/components/twinkly/test_config_flow.py
new file mode 100644
index 00000000000..d1a56277fa7
--- /dev/null
+++ b/tests/components/twinkly/test_config_flow.py
@@ -0,0 +1,60 @@
+"""Tests for the config_flow of the twinly component."""
+
+from homeassistant import config_entries
+from homeassistant.components.twinkly.const import (
+ CONF_ENTRY_HOST,
+ CONF_ENTRY_ID,
+ CONF_ENTRY_MODEL,
+ CONF_ENTRY_NAME,
+ DOMAIN as TWINKLY_DOMAIN,
+)
+
+from tests.async_mock import patch
+from tests.components.twinkly import TEST_MODEL, ClientMock
+
+
+async def test_invalid_host(hass):
+ """Test the failure when invalid host provided."""
+ result = await hass.config_entries.flow.async_init(
+ TWINKLY_DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_ENTRY_HOST: "dummy"},
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+ assert result["errors"] == {CONF_ENTRY_HOST: "cannot_connect"}
+
+
+async def test_success_flow(hass):
+ """Test that an entity is created when the flow completes."""
+ client = ClientMock()
+ with patch("twinkly_client.TwinklyClient", return_value=client):
+ result = await hass.config_entries.flow.async_init(
+ TWINKLY_DOMAIN, context={"source": config_entries.SOURCE_USER}
+ )
+
+ assert result["type"] == "form"
+ assert result["step_id"] == "user"
+ assert result["errors"] == {}
+
+ result = await hass.config_entries.flow.async_configure(
+ result["flow_id"],
+ {CONF_ENTRY_HOST: "dummy"},
+ )
+
+ assert result["type"] == "create_entry"
+ assert result["title"] == client.id
+ assert result["data"] == {
+ CONF_ENTRY_HOST: "dummy",
+ CONF_ENTRY_ID: client.id,
+ CONF_ENTRY_NAME: client.id,
+ CONF_ENTRY_MODEL: TEST_MODEL,
+ }
diff --git a/tests/components/twinkly/test_init.py b/tests/components/twinkly/test_init.py
new file mode 100644
index 00000000000..d9dc4623d5e
--- /dev/null
+++ b/tests/components/twinkly/test_init.py
@@ -0,0 +1,67 @@
+"""Tests of the initialization of the twinly integration."""
+
+from uuid import uuid4
+
+from homeassistant.components.twinkly import async_setup_entry, async_unload_entry
+from homeassistant.components.twinkly.const import (
+ CONF_ENTRY_HOST,
+ CONF_ENTRY_ID,
+ CONF_ENTRY_MODEL,
+ CONF_ENTRY_NAME,
+ DOMAIN as TWINKLY_DOMAIN,
+)
+from homeassistant.core import HomeAssistant
+
+from tests.async_mock import patch
+from tests.common import MockConfigEntry
+from tests.components.twinkly import TEST_HOST, TEST_MODEL, TEST_NAME_ORIGINAL
+
+
+async def test_setup_entry(hass: HomeAssistant):
+ """Validate that setup entry also configure the client."""
+
+ id = str(uuid4())
+ config_entry = MockConfigEntry(
+ domain=TWINKLY_DOMAIN,
+ data={
+ CONF_ENTRY_HOST: TEST_HOST,
+ CONF_ENTRY_ID: id,
+ CONF_ENTRY_NAME: TEST_NAME_ORIGINAL,
+ CONF_ENTRY_MODEL: TEST_MODEL,
+ },
+ entry_id=id,
+ )
+
+ def setup_mock(_, __):
+ return True
+
+ with patch(
+ "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup",
+ side_effect=setup_mock,
+ ):
+ await async_setup_entry(hass, config_entry)
+
+ assert hass.data[TWINKLY_DOMAIN][id] is not None
+
+
+async def test_unload_entry(hass: HomeAssistant):
+ """Validate that unload entry also clear the client."""
+
+ id = str(uuid4())
+ config_entry = MockConfigEntry(
+ domain=TWINKLY_DOMAIN,
+ data={
+ CONF_ENTRY_HOST: TEST_HOST,
+ CONF_ENTRY_ID: id,
+ CONF_ENTRY_NAME: TEST_NAME_ORIGINAL,
+ CONF_ENTRY_MODEL: TEST_MODEL,
+ },
+ entry_id=id,
+ )
+
+ # Put random content at the location where the client should have been placed by setup
+ hass.data.setdefault(TWINKLY_DOMAIN, {})[id] = config_entry
+
+ await async_unload_entry(hass, config_entry)
+
+ assert hass.data[TWINKLY_DOMAIN].get(id) is None
diff --git a/tests/components/twinkly/test_twinkly.py b/tests/components/twinkly/test_twinkly.py
new file mode 100644
index 00000000000..7f73589512a
--- /dev/null
+++ b/tests/components/twinkly/test_twinkly.py
@@ -0,0 +1,224 @@
+"""Tests for the integration of a twinly device."""
+
+from typing import Tuple
+
+from homeassistant.components.twinkly.const import (
+ CONF_ENTRY_HOST,
+ CONF_ENTRY_ID,
+ CONF_ENTRY_MODEL,
+ CONF_ENTRY_NAME,
+ DOMAIN as TWINKLY_DOMAIN,
+)
+from homeassistant.components.twinkly.light import TwinklyLight
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.device_registry import DeviceEntry
+from homeassistant.helpers.entity_registry import RegistryEntry
+
+from tests.async_mock import patch
+from tests.common import MockConfigEntry
+from tests.components.twinkly import (
+ TEST_HOST,
+ TEST_ID,
+ TEST_MODEL,
+ TEST_NAME_ORIGINAL,
+ ClientMock,
+)
+
+
+async def test_missing_client(hass: HomeAssistant):
+ """Validate that if client has not been setup, it fails immediately in setup."""
+ try:
+ config_entry = MockConfigEntry(
+ data={
+ CONF_ENTRY_HOST: TEST_HOST,
+ CONF_ENTRY_ID: TEST_ID,
+ CONF_ENTRY_NAME: TEST_NAME_ORIGINAL,
+ CONF_ENTRY_MODEL: TEST_MODEL,
+ }
+ )
+ TwinklyLight(config_entry, hass)
+ except ValueError:
+ return
+
+ assert False
+
+
+async def test_initial_state(hass: HomeAssistant):
+ """Validate that entity and device states are updated on startup."""
+ entity, device, _ = await _create_entries(hass)
+
+ state = hass.states.get(entity.entity_id)
+
+ # Basic state properties
+ assert state.name == entity.unique_id
+ assert state.state == "on"
+ assert state.attributes["host"] == TEST_HOST
+ assert state.attributes["brightness"] == 26
+ assert state.attributes["friendly_name"] == entity.unique_id
+ assert state.attributes["icon"] == "mdi:string-lights"
+
+ # Validates that custom properties of the API device_info are propagated through attributes
+ assert state.attributes["uuid"] == entity.unique_id
+
+ assert entity.original_name == entity.unique_id
+ assert entity.original_icon == "mdi:string-lights"
+
+ assert device.name == entity.unique_id
+ assert device.model == TEST_MODEL
+ assert device.manufacturer == "LEDWORKS"
+
+
+async def test_initial_state_offline(hass: HomeAssistant):
+ """Validate that entity and device are restored from config is offline on startup."""
+ client = ClientMock()
+ client.is_offline = True
+ entity, device, _ = await _create_entries(hass, client)
+
+ state = hass.states.get(entity.entity_id)
+
+ assert state.name == TEST_NAME_ORIGINAL
+ assert state.state == "unavailable"
+ assert state.attributes["friendly_name"] == TEST_NAME_ORIGINAL
+ assert state.attributes["icon"] == "mdi:string-lights"
+
+ assert entity.original_name == TEST_NAME_ORIGINAL
+ assert entity.original_icon == "mdi:string-lights"
+
+ assert device.name == TEST_NAME_ORIGINAL
+ assert device.model == TEST_MODEL
+ assert device.manufacturer == "LEDWORKS"
+
+
+async def test_turn_on(hass: HomeAssistant):
+ """Test support of the light.turn_on service."""
+ client = ClientMock()
+ client.is_on = False
+ client.brightness = 20
+ entity, _, _ = await _create_entries(hass, client)
+
+ assert hass.states.get(entity.entity_id).state == "off"
+
+ await hass.services.async_call(
+ "light", "turn_on", service_data={"entity_id": entity.entity_id}
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get(entity.entity_id)
+
+ assert state.state == "on"
+ assert state.attributes["brightness"] == 51
+
+
+async def test_turn_on_with_brightness(hass: HomeAssistant):
+ """Test support of the light.turn_on service with a brightness parameter."""
+ client = ClientMock()
+ client.is_on = False
+ client.brightness = 20
+ entity, _, _ = await _create_entries(hass, client)
+
+ assert hass.states.get(entity.entity_id).state == "off"
+
+ await hass.services.async_call(
+ "light",
+ "turn_on",
+ service_data={"entity_id": entity.entity_id, "brightness": 255},
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get(entity.entity_id)
+
+ assert state.state == "on"
+ assert state.attributes["brightness"] == 255
+
+
+async def test_turn_off(hass: HomeAssistant):
+ """Test support of the light.turn_off service."""
+ entity, _, _ = await _create_entries(hass)
+
+ assert hass.states.get(entity.entity_id).state == "on"
+
+ await hass.services.async_call(
+ "light", "turn_off", service_data={"entity_id": entity.entity_id}
+ )
+ await hass.async_block_till_done()
+
+ state = hass.states.get(entity.entity_id)
+
+ assert state.state == "off"
+ assert state.attributes["brightness"] == 0
+
+
+async def test_update_name(hass: HomeAssistant):
+ """
+ Validate device's name update behavior.
+
+ Validate that if device name is changed from the Twinkly app,
+ then the name of the entity is updated and it's also persisted,
+ so it can be restored when starting HA while Twinkly is offline.
+ """
+ entity, _, client = await _create_entries(hass)
+
+ updated_config_entry = None
+
+ async def on_update(ha, co):
+ nonlocal updated_config_entry
+ updated_config_entry = co
+
+ hass.config_entries.async_get_entry(entity.unique_id).add_update_listener(on_update)
+
+ client.change_name("new_device_name")
+ await hass.services.async_call(
+ "light", "turn_off", service_data={"entity_id": entity.entity_id}
+ ) # We call turn_off which will automatically cause an async_update
+ await hass.async_block_till_done()
+
+ state = hass.states.get(entity.entity_id)
+
+ assert updated_config_entry is not None
+ assert updated_config_entry.data[CONF_ENTRY_NAME] == "new_device_name"
+ assert state.attributes["friendly_name"] == "new_device_name"
+
+
+async def test_unload(hass: HomeAssistant):
+ """Validate that entities can be unloaded from the UI."""
+
+ _, _, client = await _create_entries(hass)
+ entry_id = client.id
+
+ assert await hass.config_entries.async_unload(entry_id)
+
+
+async def _create_entries(
+ hass: HomeAssistant, client=None
+) -> Tuple[RegistryEntry, DeviceEntry, ClientMock]:
+ client = ClientMock() if client is None else client
+
+ def get_client_mock(client, _):
+ return client
+
+ with patch("twinkly_client.TwinklyClient", side_effect=get_client_mock):
+ config_entry = MockConfigEntry(
+ domain=TWINKLY_DOMAIN,
+ data={
+ CONF_ENTRY_HOST: client,
+ CONF_ENTRY_ID: client.id,
+ CONF_ENTRY_NAME: TEST_NAME_ORIGINAL,
+ CONF_ENTRY_MODEL: TEST_MODEL,
+ },
+ entry_id=client.id,
+ )
+ config_entry.add_to_hass(hass)
+ assert await hass.config_entries.async_setup(client.id)
+ await hass.async_block_till_done()
+
+ device_registry = await hass.helpers.device_registry.async_get_registry()
+ entity_registry = await hass.helpers.entity_registry.async_get_registry()
+
+ entity_id = entity_registry.async_get_entity_id("light", TWINKLY_DOMAIN, client.id)
+ entity = entity_registry.async_get(entity_id)
+ device = device_registry.async_get_device({(TWINKLY_DOMAIN, client.id)}, set())
+
+ assert entity is not None
+ assert device is not None
+
+ return entity, device, client
diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py
index 83732601cd6..8d5cb85bf9f 100644
--- a/tests/components/unifi/test_controller.py
+++ b/tests/components/unifi/test_controller.py
@@ -295,6 +295,22 @@ async def test_get_controller_login_failed(hass):
await get_controller(hass, **CONTROLLER_DATA)
+async def test_get_controller_controller_bad_gateway(hass):
+ """Check that get_controller can handle controller being unavailable."""
+ with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch(
+ "aiounifi.Controller.login", side_effect=aiounifi.BadGateway
+ ), pytest.raises(CannotConnect):
+ await get_controller(hass, **CONTROLLER_DATA)
+
+
+async def test_get_controller_controller_service_unavailable(hass):
+ """Check that get_controller can handle controller being unavailable."""
+ with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch(
+ "aiounifi.Controller.login", side_effect=aiounifi.ServiceUnavailable
+ ), pytest.raises(CannotConnect):
+ await get_controller(hass, **CONTROLLER_DATA)
+
+
async def test_get_controller_controller_unavailable(hass):
"""Check that get_controller can handle controller being unavailable."""
with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch(
diff --git a/tests/components/uptime/test_sensor.py b/tests/components/uptime/test_sensor.py
index fd0c03239f7..fe3ae30a843 100644
--- a/tests/components/uptime/test_sensor.py
+++ b/tests/components/uptime/test_sensor.py
@@ -1,85 +1,11 @@
"""The tests for the uptime sensor platform."""
-from datetime import timedelta
-from homeassistant.components.uptime.sensor import UptimeSensor
from homeassistant.setup import async_setup_component
-from tests.async_mock import patch
-
-
-async def test_uptime_min_config(hass):
- """Test minimum uptime configuration."""
- config = {"sensor": {"platform": "uptime"}}
- assert await async_setup_component(hass, "sensor", config)
- await hass.async_block_till_done()
- state = hass.states.get("sensor.uptime")
- assert state.attributes.get("unit_of_measurement") == "days"
-
async def test_uptime_sensor_name_change(hass):
"""Test uptime sensor with different name."""
config = {"sensor": {"platform": "uptime", "name": "foobar"}}
assert await async_setup_component(hass, "sensor", config)
await hass.async_block_till_done()
- state = hass.states.get("sensor.foobar")
- assert state.attributes.get("unit_of_measurement") == "days"
-
-
-async def test_uptime_sensor_config_hours(hass):
- """Test uptime sensor with hours defined in config."""
- config = {"sensor": {"platform": "uptime", "unit_of_measurement": "hours"}}
- assert await async_setup_component(hass, "sensor", config)
- await hass.async_block_till_done()
- state = hass.states.get("sensor.uptime")
- assert state.attributes.get("unit_of_measurement") == "hours"
-
-
-async def test_uptime_sensor_config_minutes(hass):
- """Test uptime sensor with minutes defined in config."""
- config = {"sensor": {"platform": "uptime", "unit_of_measurement": "minutes"}}
- assert await async_setup_component(hass, "sensor", config)
- await hass.async_block_till_done()
- state = hass.states.get("sensor.uptime")
- assert state.attributes.get("unit_of_measurement") == "minutes"
-
-
-async def test_uptime_sensor_days_output(hass):
- """Test uptime sensor output data."""
- sensor = UptimeSensor("test", "days")
- assert sensor.unit_of_measurement == "days"
- new_time = sensor.initial + timedelta(days=1)
- with patch("homeassistant.util.dt.now", return_value=new_time):
- await sensor.async_update()
- assert sensor.state == 1.00
- new_time = sensor.initial + timedelta(days=111.499)
- with patch("homeassistant.util.dt.now", return_value=new_time):
- await sensor.async_update()
- assert sensor.state == 111.50
-
-
-async def test_uptime_sensor_hours_output(hass):
- """Test uptime sensor output data."""
- sensor = UptimeSensor("test", "hours")
- assert sensor.unit_of_measurement == "hours"
- new_time = sensor.initial + timedelta(hours=16)
- with patch("homeassistant.util.dt.now", return_value=new_time):
- await sensor.async_update()
- assert sensor.state == 16.00
- new_time = sensor.initial + timedelta(hours=72.499)
- with patch("homeassistant.util.dt.now", return_value=new_time):
- await sensor.async_update()
- assert sensor.state == 72.50
-
-
-async def test_uptime_sensor_minutes_output(hass):
- """Test uptime sensor output data."""
- sensor = UptimeSensor("test", "minutes")
- assert sensor.unit_of_measurement == "minutes"
- new_time = sensor.initial + timedelta(minutes=16)
- with patch("homeassistant.util.dt.now", return_value=new_time):
- await sensor.async_update()
- assert sensor.state == 16.00
- new_time = sensor.initial + timedelta(minutes=12.499)
- with patch("homeassistant.util.dt.now", return_value=new_time):
- await sensor.async_update()
- assert sensor.state == 12.50
+ assert hass.states.get("sensor.foobar")
diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py
index 6a2de3a1cd0..2856a63b4f5 100644
--- a/tests/components/utility_meter/test_sensor.py
+++ b/tests/components/utility_meter/test_sensor.py
@@ -264,6 +264,34 @@ async def _test_self_reset(hass, config, start_time, expect_reset=True):
assert state.state == "5"
+async def test_self_reset_quarter_hourly(hass, legacy_patchable_time):
+ """Test quarter-hourly reset of meter."""
+ await _test_self_reset(
+ hass, gen_config("quarter-hourly"), "2017-12-31T23:59:00.000000+00:00"
+ )
+
+
+async def test_self_reset_quarter_hourly_first_quarter(hass, legacy_patchable_time):
+ """Test quarter-hourly reset of meter."""
+ await _test_self_reset(
+ hass, gen_config("quarter-hourly"), "2017-12-31T23:14:00.000000+00:00"
+ )
+
+
+async def test_self_reset_quarter_hourly_second_quarter(hass, legacy_patchable_time):
+ """Test quarter-hourly reset of meter."""
+ await _test_self_reset(
+ hass, gen_config("quarter-hourly"), "2017-12-31T23:29:00.000000+00:00"
+ )
+
+
+async def test_self_reset_quarter_hourly_third_quarter(hass, legacy_patchable_time):
+ """Test quarter-hourly reset of meter."""
+ await _test_self_reset(
+ hass, gen_config("quarter-hourly"), "2017-12-31T23:44:00.000000+00:00"
+ )
+
+
async def test_self_reset_hourly(hass, legacy_patchable_time):
"""Test hourly reset of meter."""
await _test_self_reset(
diff --git a/tests/components/vacuum/test_device_action.py b/tests/components/vacuum/test_device_action.py
index 47ce5423f7d..3edeaba2a41 100644
--- a/tests/components/vacuum/test_device_action.py
+++ b/tests/components/vacuum/test_device_action.py
@@ -14,6 +14,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/vacuum/test_device_condition.py b/tests/components/vacuum/test_device_condition.py
index 16715266b8c..3dc7a628741 100644
--- a/tests/components/vacuum/test_device_condition.py
+++ b/tests/components/vacuum/test_device_condition.py
@@ -19,6 +19,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/vacuum/test_device_trigger.py b/tests/components/vacuum/test_device_trigger.py
index f3439700e33..e3f615891e6 100644
--- a/tests/components/vacuum/test_device_trigger.py
+++ b/tests/components/vacuum/test_device_trigger.py
@@ -14,6 +14,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/water_heater/test_device_action.py b/tests/components/water_heater/test_device_action.py
index 3d3e70444b7..06bd43ec654 100644
--- a/tests/components/water_heater/test_device_action.py
+++ b/tests/components/water_heater/test_device_action.py
@@ -14,6 +14,7 @@ from tests.common import (
mock_device_registry,
mock_registry,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/components/webhook/test_trigger.py b/tests/components/webhook/test_trigger.py
index bb3cb680743..46459bd88c4 100644
--- a/tests/components/webhook/test_trigger.py
+++ b/tests/components/webhook/test_trigger.py
@@ -5,6 +5,7 @@ from homeassistant.core import callback
from homeassistant.setup import async_setup_component
from tests.async_mock import patch
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture(autouse=True)
diff --git a/tests/components/websocket_api/test_decorators.py b/tests/components/websocket_api/test_decorators.py
new file mode 100644
index 00000000000..45d761f6fed
--- /dev/null
+++ b/tests/components/websocket_api/test_decorators.py
@@ -0,0 +1,68 @@
+"""Test decorators."""
+from homeassistant.components import http, websocket_api
+
+
+async def test_async_response_request_context(hass, websocket_client):
+ """Test we can access current request."""
+
+ def handle_request(request, connection, msg):
+ if request is not None:
+ connection.send_result(msg["id"], request.path)
+ else:
+ connection.send_error(msg["id"], "not_found", "")
+
+ @websocket_api.websocket_command({"type": "test-get-request-executor"})
+ @websocket_api.async_response
+ async def executor_get_request(hass, connection, msg):
+ handle_request(
+ await hass.async_add_executor_job(http.current_request.get), connection, msg
+ )
+
+ @websocket_api.websocket_command({"type": "test-get-request-async"})
+ @websocket_api.async_response
+ async def async_get_request(hass, connection, msg):
+ handle_request(http.current_request.get(), connection, msg)
+
+ @websocket_api.websocket_command({"type": "test-get-request"})
+ def get_request(hass, connection, msg):
+ handle_request(http.current_request.get(), connection, msg)
+
+ websocket_api.async_register_command(hass, executor_get_request)
+ websocket_api.async_register_command(hass, async_get_request)
+ websocket_api.async_register_command(hass, get_request)
+
+ await websocket_client.send_json(
+ {
+ "id": 5,
+ "type": "test-get-request",
+ }
+ )
+
+ msg = await websocket_client.receive_json()
+ assert msg["id"] == 5
+ assert msg["success"]
+ assert msg["result"] == "/api/websocket"
+
+ await websocket_client.send_json(
+ {
+ "id": 6,
+ "type": "test-get-request-async",
+ }
+ )
+
+ msg = await websocket_client.receive_json()
+ assert msg["id"] == 6
+ assert msg["success"]
+ assert msg["result"] == "/api/websocket"
+
+ await websocket_client.send_json(
+ {
+ "id": 7,
+ "type": "test-get-request-executor",
+ }
+ )
+
+ msg = await websocket_client.receive_json()
+ assert msg["id"] == 7
+ assert not msg["success"]
+ assert msg["error"]["code"] == "not_found"
diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py
index a09876868a7..000900c3355 100644
--- a/tests/components/withings/common.py
+++ b/tests/components/withings/common.py
@@ -197,7 +197,11 @@ class ComponentFactory:
assert result
# pylint: disable=protected-access
state = config_entry_oauth2_flow._encode_jwt(
- self._hass, {"flow_id": result["flow_id"]}
+ self._hass,
+ {
+ "flow_id": result["flow_id"],
+ "redirect_uri": "http://127.0.0.1:8080/auth/external/callback",
+ },
)
assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP
assert result["url"] == (
diff --git a/tests/components/withings/test_config_flow.py b/tests/components/withings/test_config_flow.py
index cb0ea5b29ab..8380c134013 100644
--- a/tests/components/withings/test_config_flow.py
+++ b/tests/components/withings/test_config_flow.py
@@ -71,7 +71,13 @@ async def test_config_reauth_profile(
)
# pylint: disable=protected-access
- state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]})
+ state = config_entry_oauth2_flow._encode_jwt(
+ hass,
+ {
+ "flow_id": result["flow_id"],
+ "redirect_uri": "https://example.com/auth/external/callback",
+ },
+ )
client: TestClient = await aiohttp_client(hass.http.app)
resp = await client.get(f"{AUTH_CALLBACK_PATH}?code=abcd&state={state}")
diff --git a/tests/components/xbox/test_config_flow.py b/tests/components/xbox/test_config_flow.py
index 176c5eea60a..516a57c039b 100644
--- a/tests/components/xbox/test_config_flow.py
+++ b/tests/components/xbox/test_config_flow.py
@@ -21,7 +21,9 @@ async def test_abort_if_existing_entry(hass):
assert result["reason"] == "single_instance_allowed"
-async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request):
+async def test_full_flow(
+ hass, aiohttp_client, aioclient_mock, current_request_with_host
+):
"""Check full flow."""
assert await setup.async_setup_component(
hass,
@@ -35,7 +37,13 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock, current_request):
result = await hass.config_entries.flow.async_init(
"xbox", context={"source": config_entries.SOURCE_USER}
)
- state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]})
+ state = config_entry_oauth2_flow._encode_jwt(
+ hass,
+ {
+ "flow_id": result["flow_id"],
+ "redirect_uri": "https://example.com/auth/external/callback",
+ },
+ )
scope = "+".join(["Xboxlive.signin", "Xboxlive.offline_access"])
diff --git a/tests/components/yeelight/__init__.py b/tests/components/yeelight/__init__.py
index 9f811586a77..5405b69490b 100644
--- a/tests/components/yeelight/__init__.py
+++ b/tests/components/yeelight/__init__.py
@@ -55,7 +55,8 @@ PROPERTIES = {
"current_brightness": "30",
}
-ENTITY_BINARY_SENSOR = f"binary_sensor.{UNIQUE_NAME}_nightlight"
+ENTITY_BINARY_SENSOR_TEMPLATE = "binary_sensor.{}_nightlight"
+ENTITY_BINARY_SENSOR = ENTITY_BINARY_SENSOR_TEMPLATE.format(UNIQUE_NAME)
ENTITY_LIGHT = f"light.{UNIQUE_NAME}"
ENTITY_NIGHTLIGHT = f"light.{UNIQUE_NAME}_nightlight"
ENTITY_AMBILIGHT = f"light.{UNIQUE_NAME}_ambilight"
diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py
index d9c23cfa1a7..882f9944ca1 100644
--- a/tests/components/yeelight/test_init.py
+++ b/tests/components/yeelight/test_init.py
@@ -1,21 +1,27 @@
"""Test Yeelight."""
+from unittest.mock import MagicMock
+
from yeelight import BulbType
from homeassistant.components.yeelight import (
CONF_NIGHTLIGHT_SWITCH,
CONF_NIGHTLIGHT_SWITCH_TYPE,
+ DATA_CONFIG_ENTRIES,
+ DATA_DEVICE,
DOMAIN,
NIGHTLIGHT_SWITCH_TYPE_LIGHT,
)
-from homeassistant.const import CONF_DEVICES, CONF_NAME
+from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry
from homeassistant.setup import async_setup_component
from . import (
+ CAPABILITIES,
CONFIG_ENTRY_DATA,
ENTITY_AMBILIGHT,
ENTITY_BINARY_SENSOR,
+ ENTITY_BINARY_SENSOR_TEMPLATE,
ENTITY_LIGHT,
ENTITY_NIGHTLIGHT,
ID,
@@ -115,6 +121,7 @@ async def test_unique_ids_entry(hass: HomeAssistant):
mocked_bulb = _mocked_bulb()
mocked_bulb.bulb_type = BulbType.WhiteTempMood
+
with _patch_discovery(MODULE), patch(f"{MODULE}.Bulb", return_value=mocked_bulb):
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
@@ -132,3 +139,40 @@ async def test_unique_ids_entry(hass: HomeAssistant):
assert (
er.async_get(ENTITY_AMBILIGHT).unique_id == f"{config_entry.entry_id}-ambilight"
)
+
+
+async def test_bulb_off_while_adding_in_ha(hass: HomeAssistant):
+ """Test Yeelight off while adding to ha, for example on HA start."""
+ config_entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={
+ **CONFIG_ENTRY_DATA,
+ CONF_HOST: IP_ADDRESS,
+ },
+ unique_id=ID,
+ )
+ config_entry.add_to_hass(hass)
+
+ mocked_bulb = _mocked_bulb(True)
+ mocked_bulb.bulb_type = BulbType.WhiteTempMood
+
+ with patch(f"{MODULE}.Bulb", return_value=mocked_bulb), patch(
+ f"{MODULE}.config_flow.yeelight.Bulb", return_value=mocked_bulb
+ ):
+ assert await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ binary_sensor_entity_id = ENTITY_BINARY_SENSOR_TEMPLATE.format(
+ IP_ADDRESS.replace(".", "_")
+ )
+ er = await entity_registry.async_get_registry(hass)
+ assert er.async_get(binary_sensor_entity_id) is None
+
+ type(mocked_bulb).get_capabilities = MagicMock(CAPABILITIES)
+ type(mocked_bulb).get_properties = MagicMock(None)
+
+ hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][DATA_DEVICE].update()
+ await hass.async_block_till_done()
+
+ er = await entity_registry.async_get_registry(hass)
+ assert er.async_get(binary_sensor_entity_id) is not None
diff --git a/tests/components/yeelight/test_light.py b/tests/components/yeelight/test_light.py
index e6fe16255eb..686ba6d8e82 100644
--- a/tests/components/yeelight/test_light.py
+++ b/tests/components/yeelight/test_light.py
@@ -13,7 +13,7 @@ from yeelight import (
TemperatureTransition,
transitions,
)
-from yeelight.flow import Flow
+from yeelight.flow import Action, Flow
from yeelight.main import _MODEL_SPECS
from homeassistant.components.light import (
@@ -51,10 +51,19 @@ from homeassistant.components.yeelight import (
from homeassistant.components.yeelight.light import (
ATTR_MINUTES,
ATTR_MODE,
+ EFFECT_CANDLE_FLICKER,
+ EFFECT_DATE_NIGHT,
EFFECT_DISCO,
EFFECT_FACEBOOK,
EFFECT_FAST_RANDOM_LOOP,
+ EFFECT_HAPPY_BIRTHDAY,
+ EFFECT_HOME,
+ EFFECT_MOVIE,
+ EFFECT_NIGHT_MODE,
+ EFFECT_ROMANCE,
EFFECT_STOP,
+ EFFECT_SUNRISE,
+ EFFECT_SUNSET,
EFFECT_TWITTER,
EFFECT_WHATSAPP,
SERVICE_SET_AUTO_DELAY_OFF_SCENE,
@@ -569,6 +578,96 @@ async def test_effects(hass: HomeAssistant):
EFFECT_WHATSAPP: Flow(count=2, transitions=transitions.pulse(37, 211, 102)),
EFFECT_FACEBOOK: Flow(count=2, transitions=transitions.pulse(59, 89, 152)),
EFFECT_TWITTER: Flow(count=2, transitions=transitions.pulse(0, 172, 237)),
+ EFFECT_HOME: Flow(
+ count=0,
+ action=Action.recover,
+ transitions=[
+ TemperatureTransition(degrees=3200, duration=500, brightness=80)
+ ],
+ ),
+ EFFECT_NIGHT_MODE: Flow(
+ count=0,
+ action=Action.recover,
+ transitions=[RGBTransition(0xFF, 0x99, 0x00, duration=500, brightness=1)],
+ ),
+ EFFECT_DATE_NIGHT: Flow(
+ count=0,
+ action=Action.recover,
+ transitions=[RGBTransition(0xFF, 0x66, 0x00, duration=500, brightness=50)],
+ ),
+ EFFECT_MOVIE: Flow(
+ count=0,
+ action=Action.recover,
+ transitions=[
+ RGBTransition(
+ red=0x14, green=0x14, blue=0x32, duration=500, brightness=50
+ )
+ ],
+ ),
+ EFFECT_SUNRISE: Flow(
+ count=1,
+ action=Action.stay,
+ transitions=[
+ RGBTransition(
+ red=0xFF, green=0x4D, blue=0x00, duration=50, brightness=1
+ ),
+ TemperatureTransition(degrees=1700, duration=360000, brightness=10),
+ TemperatureTransition(degrees=2700, duration=540000, brightness=100),
+ ],
+ ),
+ EFFECT_SUNSET: Flow(
+ count=1,
+ action=Action.off,
+ transitions=[
+ TemperatureTransition(degrees=2700, duration=50, brightness=10),
+ TemperatureTransition(degrees=1700, duration=180000, brightness=5),
+ RGBTransition(
+ red=0xFF, green=0x4C, blue=0x00, duration=420000, brightness=1
+ ),
+ ],
+ ),
+ EFFECT_ROMANCE: Flow(
+ count=0,
+ action=Action.stay,
+ transitions=[
+ RGBTransition(
+ red=0x59, green=0x15, blue=0x6D, duration=4000, brightness=1
+ ),
+ RGBTransition(
+ red=0x66, green=0x14, blue=0x2A, duration=4000, brightness=1
+ ),
+ ],
+ ),
+ EFFECT_HAPPY_BIRTHDAY: Flow(
+ count=0,
+ action=Action.stay,
+ transitions=[
+ RGBTransition(
+ red=0xDC, green=0x50, blue=0x19, duration=1996, brightness=80
+ ),
+ RGBTransition(
+ red=0xDC, green=0x78, blue=0x1E, duration=1996, brightness=80
+ ),
+ RGBTransition(
+ red=0xAA, green=0x32, blue=0x14, duration=1996, brightness=80
+ ),
+ ],
+ ),
+ EFFECT_CANDLE_FLICKER: Flow(
+ count=0,
+ action=Action.recover,
+ transitions=[
+ TemperatureTransition(degrees=2700, duration=800, brightness=50),
+ TemperatureTransition(degrees=2700, duration=800, brightness=30),
+ TemperatureTransition(degrees=2700, duration=1200, brightness=80),
+ TemperatureTransition(degrees=2700, duration=800, brightness=60),
+ TemperatureTransition(degrees=2700, duration=1200, brightness=90),
+ TemperatureTransition(degrees=2700, duration=2400, brightness=50),
+ TemperatureTransition(degrees=2700, duration=1200, brightness=80),
+ TemperatureTransition(degrees=2700, duration=800, brightness=60),
+ TemperatureTransition(degrees=2700, duration=400, brightness=70),
+ ],
+ ),
}
for name, target in effects.items():
diff --git a/tests/components/yessssms/__init__.py b/tests/components/yessssms/__init__.py
deleted file mode 100644
index bf8e562009b..00000000000
--- a/tests/components/yessssms/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""Tests for the yessssms component."""
diff --git a/tests/components/yessssms/test_notify.py b/tests/components/yessssms/test_notify.py
deleted file mode 100644
index 742b983fa83..00000000000
--- a/tests/components/yessssms/test_notify.py
+++ /dev/null
@@ -1,367 +0,0 @@
-"""The tests for the notify yessssms platform."""
-import logging
-
-import pytest
-
-from homeassistant.components.yessssms.const import CONF_PROVIDER
-import homeassistant.components.yessssms.notify as yessssms
-from homeassistant.const import (
- CONF_PASSWORD,
- CONF_RECIPIENT,
- CONF_USERNAME,
- HTTP_INTERNAL_SERVER_ERROR,
-)
-from homeassistant.setup import async_setup_component
-
-from tests.async_mock import patch
-
-
-@pytest.fixture(name="config")
-def config_data():
- """Set valid config data."""
- config = {
- "notify": {
- "platform": "yessssms",
- "name": "sms",
- CONF_USERNAME: "06641234567",
- CONF_PASSWORD: "secretPassword",
- CONF_RECIPIENT: "06509876543",
- CONF_PROVIDER: "educom",
- }
- }
- return config
-
-
-@pytest.fixture(name="valid_settings")
-def init_valid_settings(hass, config):
- """Initialize component with valid settings."""
- return async_setup_component(hass, "notify", config)
-
-
-@pytest.fixture(name="invalid_provider_settings")
-def init_invalid_provider_settings(hass, config):
- """Set invalid provider data and initialize component."""
- config["notify"][CONF_PROVIDER] = "FantasyMobile" # invalid provider
- return async_setup_component(hass, "notify", config)
-
-
-@pytest.fixture(name="invalid_login_data")
-def mock_invalid_login_data():
- """Mock invalid login data."""
- path = "homeassistant.components.yessssms.notify.YesssSMS.login_data_valid"
- with patch(path, return_value=False):
- yield
-
-
-@pytest.fixture(name="valid_login_data")
-def mock_valid_login_data():
- """Mock valid login data."""
- path = "homeassistant.components.yessssms.notify.YesssSMS.login_data_valid"
- with patch(path, return_value=True):
- yield
-
-
-@pytest.fixture(name="connection_error")
-def mock_connection_error():
- """Mock a connection error."""
- path = "homeassistant.components.yessssms.notify.YesssSMS.login_data_valid"
- with patch(path, side_effect=yessssms.YesssSMS.ConnectionError()):
- yield
-
-
-async def test_unsupported_provider_error(hass, caplog, invalid_provider_settings):
- """Test for error on unsupported provider."""
- await invalid_provider_settings
- for record in caplog.records:
- if (
- record.levelname == "ERROR"
- and record.name == "homeassistant.components.yessssms.notify"
- ):
- assert (
- "Unknown provider: provider (fantasymobile) is not known to YesssSMS"
- in record.message
- )
- assert (
- "Unknown provider: provider (fantasymobile) is not known to YesssSMS"
- in caplog.text
- )
- assert not hass.services.has_service("notify", "sms")
-
-
-async def test_false_login_data_error(hass, caplog, valid_settings, invalid_login_data):
- """Test login data check error."""
- await valid_settings
- assert not hass.services.has_service("notify", "sms")
- for record in caplog.records:
- if (
- record.levelname == "ERROR"
- and record.name == "homeassistant.components.yessssms.notify"
- ):
- assert (
- "Login data is not valid! Please double check your login data at"
- in record.message
- )
-
-
-async def test_init_success(hass, caplog, valid_settings, valid_login_data):
- """Test for successful init of yessssms."""
- caplog.set_level(logging.DEBUG)
- await valid_settings
- assert hass.services.has_service("notify", "sms")
- messages = []
- for record in caplog.records:
- if (
- record.levelname == "DEBUG"
- and record.name == "homeassistant.components.yessssms.notify"
- ):
- messages.append(record.message)
- assert "Login data for 'educom' valid" in messages[0]
- assert (
- "initialized; library version: {}".format(yessssms.YesssSMS("", "").version())
- in messages[1]
- )
-
-
-async def test_connection_error_on_init(hass, caplog, valid_settings, connection_error):
- """Test for connection error on init."""
- caplog.set_level(logging.DEBUG)
- await valid_settings
- assert hass.services.has_service("notify", "sms")
- for record in caplog.records:
- if (
- record.levelname == "WARNING"
- and record.name == "homeassistant.components.yessssms.notify"
- ):
- assert (
- "Connection Error, could not verify login data for 'educom'"
- in record.message
- )
- for record in caplog.records:
- if (
- record.levelname == "DEBUG"
- and record.name == "homeassistant.components.yessssms.notify"
- ):
- assert (
- "initialized; library version: {}".format(
- yessssms.YesssSMS("", "").version()
- )
- in record.message
- )
-
-
-@pytest.fixture(name="yessssms")
-def yessssms_init():
- """Set up things to be run when tests are started."""
- login = "06641234567"
- passwd = "testpasswd"
- recipient = "06501234567"
- client = yessssms.YesssSMS(login, passwd)
- return yessssms.YesssSMSNotificationService(client, recipient)
-
-
-async def test_login_error(yessssms, requests_mock, caplog):
- """Test login that fails."""
- requests_mock.post(
- # pylint: disable=protected-access
- yessssms.yesss._login_url,
- status_code=200,
- text="BlaBlaBlaLogin nicht erfolgreichBlaBla",
- )
-
- message = "Testing YesssSMS platform :)"
-
- with caplog.at_level(logging.ERROR):
- yessssms.send_message(message)
- assert requests_mock.called is True
- assert requests_mock.call_count == 1
-
-
-async def test_empty_message_error(yessssms, caplog):
- """Test for an empty SMS message error."""
- message = ""
- with caplog.at_level(logging.ERROR):
- yessssms.send_message(message)
-
- for record in caplog.records:
- if (
- record.levelname == "ERROR"
- and record.name == "homeassistant.components.yessssms.notify"
- ):
- assert "Cannot send empty SMS message" in record.message
-
-
-async def test_error_account_suspended(yessssms, requests_mock, caplog):
- """Test login that fails after multiple attempts."""
- requests_mock.post(
- # pylint: disable=protected-access
- yessssms.yesss._login_url,
- status_code=200,
- text="BlaBlaBlaLogin nicht erfolgreichBlaBla",
- )
-
- message = "Testing YesssSMS platform :)"
-
- yessssms.send_message(message)
- assert requests_mock.called is True
- assert requests_mock.call_count == 1
-
- requests_mock.post(
- # pylint: disable=protected-access
- yessssms.yesss._login_url,
- status_code=200,
- text="Wegen 3 ungültigen Login-Versuchen ist Ihr Account für "
- "eine Stunde gesperrt.",
- )
-
- message = "Testing YesssSMS platform :)"
-
- with caplog.at_level(logging.ERROR):
- yessssms.send_message(message)
- assert requests_mock.called is True
- assert requests_mock.call_count == 2
-
-
-async def test_error_account_suspended_2(yessssms, caplog):
- """Test login that fails after multiple attempts."""
- message = "Testing YesssSMS platform :)"
- # pylint: disable=protected-access
- yessssms.yesss._suspended = True
-
- with caplog.at_level(logging.ERROR):
- yessssms.send_message(message)
- for record in caplog.records:
- if (
- record.levelname == "ERROR"
- and record.name == "homeassistant.components.yessssms.notify"
- ):
- assert "Account is suspended, cannot send SMS." in record.message
-
-
-async def test_send_message(yessssms, requests_mock, caplog):
- """Test send message."""
- message = "Testing YesssSMS platform :)"
- requests_mock.post(
- # pylint: disable=protected-access
- yessssms.yesss._login_url,
- status_code=302,
- # pylint: disable=protected-access
- headers={"location": yessssms.yesss._kontomanager},
- )
- # pylint: disable=protected-access
- login = yessssms.yesss._logindata["login_rufnummer"]
- requests_mock.get(
- # pylint: disable=protected-access
- yessssms.yesss._kontomanager,
- status_code=200,
- text=f"test...{login}",
- )
- requests_mock.post(
- # pylint: disable=protected-access
- yessssms.yesss._websms_url,
- status_code=200,
- text="Ihre SMS wurde erfolgreich verschickt!
",
- )
- requests_mock.get(
- # pylint: disable=protected-access
- yessssms.yesss._logout_url,
- status_code=200,
- )
-
- with caplog.at_level(logging.INFO):
- yessssms.send_message(message)
- for record in caplog.records:
- if (
- record.levelname == "INFO"
- and record.name == "homeassistant.components.yessssms.notify"
- ):
- assert "SMS sent" in record.message
-
- assert requests_mock.called is True
- assert requests_mock.call_count == 4
- assert (
- requests_mock.last_request.scheme
- + "://"
- + requests_mock.last_request.hostname
- + requests_mock.last_request.path
- + "?"
- + requests_mock.last_request.query
- ) in yessssms.yesss._logout_url # pylint: disable=protected-access
-
-
-async def test_no_recipient_error(yessssms, caplog):
- """Test for missing/empty recipient."""
- message = "Testing YesssSMS platform :)"
- # pylint: disable=protected-access
- yessssms._recipient = ""
-
- with caplog.at_level(logging.ERROR):
- yessssms.send_message(message)
- for record in caplog.records:
- if (
- record.levelname == "ERROR"
- and record.name == "homeassistant.components.yessssms.notify"
- ):
- assert (
- "You need to provide a recipient for SMS notification" in record.message
- )
-
-
-async def test_sms_sending_error(yessssms, requests_mock, caplog):
- """Test sms sending error."""
- requests_mock.post(
- # pylint: disable=protected-access
- yessssms.yesss._login_url,
- status_code=302,
- # pylint: disable=protected-access
- headers={"location": yessssms.yesss._kontomanager},
- )
- # pylint: disable=protected-access
- login = yessssms.yesss._logindata["login_rufnummer"]
- requests_mock.get(
- # pylint: disable=protected-access
- yessssms.yesss._kontomanager,
- status_code=200,
- text=f"test...{login}",
- )
- requests_mock.post(
- # pylint: disable=protected-access
- yessssms.yesss._websms_url,
- status_code=HTTP_INTERNAL_SERVER_ERROR,
- )
-
- message = "Testing YesssSMS platform :)"
-
- with caplog.at_level(logging.ERROR):
- yessssms.send_message(message)
-
- assert requests_mock.called is True
- assert requests_mock.call_count == 3
- for record in caplog.records:
- if (
- record.levelname == "ERROR"
- and record.name == "homeassistant.components.yessssms.notify"
- ):
- assert "YesssSMS: error sending SMS" in record.message
-
-
-async def test_connection_error(yessssms, requests_mock, caplog):
- """Test connection error."""
- requests_mock.post(
- # pylint: disable=protected-access
- yessssms.yesss._login_url,
- exc=yessssms.yesss.ConnectionError,
- )
-
- message = "Testing YesssSMS platform :)"
-
- with caplog.at_level(logging.ERROR):
- yessssms.send_message(message)
-
- assert requests_mock.called is True
- assert requests_mock.call_count == 1
- for record in caplog.records:
- if (
- record.levelname == "ERROR"
- and record.name == "homeassistant.components.yessssms.notify"
- ):
- assert "cannot connect to provider" in record.message
diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py
index 11f390b202e..249e1bf58b2 100644
--- a/tests/components/zha/common.py
+++ b/tests/components/zha/common.py
@@ -70,12 +70,34 @@ FakeEndpoint.remove_from_group = zigpy_ep.remove_from_group
def patch_cluster(cluster):
"""Patch a cluster for testing."""
+ cluster.PLUGGED_ATTR_READS = {}
+
+ async def _read_attribute_raw(attributes, *args, **kwargs):
+ result = []
+ for attr_id in attributes:
+ value = cluster.PLUGGED_ATTR_READS.get(attr_id)
+ if value is None:
+ # try converting attr_id to attr_name and lookup the plugs again
+ attr_name = cluster.attributes.get(attr_id)
+ value = attr_name and cluster.PLUGGED_ATTR_READS.get(attr_name[0])
+ if value is not None:
+ result.append(
+ zcl_f.ReadAttributeRecord(
+ attr_id,
+ zcl_f.Status.SUCCESS,
+ zcl_f.TypeValue(python_type=None, value=value),
+ )
+ )
+ else:
+ result.append(zcl_f.ReadAttributeRecord(attr_id, zcl_f.Status.FAILURE))
+ return (result,)
+
cluster.bind = AsyncMock(return_value=[0])
cluster.configure_reporting = AsyncMock(return_value=[0])
cluster.deserialize = Mock()
cluster.handle_cluster_request = Mock()
- cluster.read_attributes = AsyncMock(return_value=[{}, {}])
- cluster.read_attributes_raw = Mock()
+ cluster.read_attributes = AsyncMock(wraps=cluster.read_attributes)
+ cluster.read_attributes_raw = AsyncMock(side_effect=_read_attribute_raw)
cluster.unbind = AsyncMock(return_value=[0])
cluster.write_attributes = AsyncMock(
return_value=[zcl_f.WriteAttributesResponse.deserialize(b"\x00")[0]]
diff --git a/tests/components/zha/test_climate.py b/tests/components/zha/test_climate.py
index fe69e6536d3..cc152c1a36d 100644
--- a/tests/components/zha/test_climate.py
+++ b/tests/components/zha/test_climate.py
@@ -140,19 +140,8 @@ def device_climate_mock(hass, zigpy_device_mock, zha_device_joined):
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
+ zigpy_device.endpoints[1].thermostat.PLUGGED_ATTR_READS = plugged_attrs
zha_device = await zha_device_joined(zigpy_device)
await async_enable_traffic(hass, [zha_device])
await hass.async_block_till_done()
@@ -1039,7 +1028,6 @@ async def test_occupancy_reset(hass, device_climate_sinope):
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}
)
diff --git a/tests/components/zha/test_cover.py b/tests/components/zha/test_cover.py
index 783637d26d7..97fa5c7579d 100644
--- a/tests/components/zha/test_cover.py
+++ b/tests/components/zha/test_cover.py
@@ -32,7 +32,7 @@ from .common import (
send_attributes_report,
)
-from tests.async_mock import AsyncMock, MagicMock, patch
+from tests.async_mock import AsyncMock, patch
from tests.common import async_capture_events, mock_coro, mock_restore_cache
@@ -104,19 +104,13 @@ def zigpy_keen_vent(zigpy_device_mock):
async def test_cover(m1, hass, zha_device_joined_restored, zigpy_cover_device):
"""Test zha cover platform."""
- async def get_chan_attr(*args, **kwargs):
- return 100
-
- with patch(
- "homeassistant.components.zha.core.channels.base.ZigbeeChannel.get_attribute_value",
- new=MagicMock(side_effect=get_chan_attr),
- ) as get_attr_mock:
- # load up cover domain
- zha_device = await zha_device_joined_restored(zigpy_cover_device)
- assert get_attr_mock.call_count == 2
- assert get_attr_mock.call_args[0][0] == "current_position_lift_percentage"
-
+ # load up cover domain
cluster = zigpy_cover_device.endpoints.get(1).window_covering
+ cluster.PLUGGED_ATTR_READS = {"current_position_lift_percentage": 100}
+ zha_device = await zha_device_joined_restored(zigpy_cover_device)
+ assert cluster.read_attributes.call_count == 2
+ assert "current_position_lift_percentage" in cluster.read_attributes.call_args[0][0]
+
entity_id = await find_entity_id(DOMAIN, zha_device, hass)
assert entity_id is not None
diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py
index de3a4eb1296..c0350ce63a5 100644
--- a/tests/components/zha/test_device_action.py
+++ b/tests/components/zha/test_device_action.py
@@ -16,6 +16,7 @@ from homeassistant.helpers.device_registry import async_get_registry
from homeassistant.setup import async_setup_component
from tests.common import async_mock_service, mock_coro
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
SHORT_PRESS = "remote_button_short_press"
COMMAND = "command"
diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py
index 801b6831379..b72f693e531 100644
--- a/tests/components/zha/test_device_trigger.py
+++ b/tests/components/zha/test_device_trigger.py
@@ -19,6 +19,7 @@ from tests.common import (
async_get_device_automations,
async_mock_service,
)
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
ON = 1
OFF = 0
diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py
index 8d854894ba0..b6b4b343e3b 100644
--- a/tests/components/zha/test_sensor.py
+++ b/tests/components/zha/test_sensor.py
@@ -93,18 +93,59 @@ async def async_test_electrical_measurement(hass, cluster, entity_id):
assert_state(hass, entity_id, "9.9", POWER_WATT)
+async def async_test_powerconfiguration(hass, cluster, entity_id):
+ """Test powerconfiguration/battery sensor."""
+ await send_attributes_report(hass, cluster, {33: 98})
+ assert_state(hass, entity_id, "49", "%")
+ assert hass.states.get(entity_id).attributes["battery_voltage"] == 2.9
+ assert hass.states.get(entity_id).attributes["battery_quantity"] == 3
+ assert hass.states.get(entity_id).attributes["battery_size"] == "AAA"
+ await send_attributes_report(hass, cluster, {32: 20})
+ assert hass.states.get(entity_id).attributes["battery_voltage"] == 2.0
+
+
@pytest.mark.parametrize(
- "cluster_id, test_func, report_count",
+ "cluster_id, test_func, report_count, read_plug",
(
- (measurement.RelativeHumidity.cluster_id, async_test_humidity, 1),
- (measurement.TemperatureMeasurement.cluster_id, async_test_temperature, 1),
- (measurement.PressureMeasurement.cluster_id, async_test_pressure, 1),
- (measurement.IlluminanceMeasurement.cluster_id, async_test_illuminance, 1),
- (smartenergy.Metering.cluster_id, async_test_metering, 1),
+ (measurement.RelativeHumidity.cluster_id, async_test_humidity, 1, None),
+ (
+ measurement.TemperatureMeasurement.cluster_id,
+ async_test_temperature,
+ 1,
+ None,
+ ),
+ (measurement.PressureMeasurement.cluster_id, async_test_pressure, 1, None),
+ (
+ measurement.IlluminanceMeasurement.cluster_id,
+ async_test_illuminance,
+ 1,
+ None,
+ ),
+ (
+ smartenergy.Metering.cluster_id,
+ async_test_metering,
+ 1,
+ {
+ "demand_formatting": 0xF9,
+ "divisor": 1,
+ "multiplier": 1,
+ },
+ ),
(
homeautomation.ElectricalMeasurement.cluster_id,
async_test_electrical_measurement,
1,
+ None,
+ ),
+ (
+ general.PowerConfiguration.cluster_id,
+ async_test_powerconfiguration,
+ 2,
+ {
+ "battery_size": 4, # AAA
+ "battery_voltage": 29,
+ "battery_quantity": 3,
+ },
),
),
)
@@ -115,6 +156,7 @@ async def test_sensor(
cluster_id,
test_func,
report_count,
+ read_plug,
):
"""Test zha sensor platform."""
@@ -128,6 +170,10 @@ async def test_sensor(
}
)
cluster = zigpy_device.endpoints[1].in_clusters[cluster_id]
+ if cluster_id == smartenergy.Metering.cluster_id:
+ # this one is mains powered
+ zigpy_device.node_desc.mac_capability_flags |= 0b_0000_0100
+ cluster.PLUGGED_ATTR_READS = read_plug
zha_device = await zha_device_joined_restored(zigpy_device)
entity_id = await find_entity_id(DOMAIN, zha_device, hass)
diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py
index a6287592395..136af1f4be9 100644
--- a/tests/components/zha/zha_devices_list.py
+++ b/tests/components/zha/zha_devices_list.py
@@ -1372,10 +1372,22 @@ DEVICES = [
},
},
"entities": [
+ "sensor.lumi_lumi_plug_maus01_77665544_analog_input",
+ "sensor.lumi_lumi_plug_maus01_77665544_analog_input_2",
"sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement",
"switch.lumi_lumi_plug_maus01_77665544_on_off",
],
"entity_map": {
+ ("sensor", "00:11:22:33:44:55:66:77-2-12"): {
+ "channels": ["analog_input"],
+ "entity_class": "AnalogInput",
+ "entity_id": "sensor.lumi_lumi_plug_maus01_77665544_analog_input",
+ },
+ ("sensor", "00:11:22:33:44:55:66:77-3-12"): {
+ "channels": ["analog_input"],
+ "entity_class": "AnalogInput",
+ "entity_id": "sensor.lumi_lumi_plug_maus01_77665544_analog_input_2",
+ },
("switch", "00:11:22:33:44:55:66:77-1"): {
"channels": ["on_off"],
"entity_class": "Switch",
diff --git a/tests/components/zone/test_trigger.py b/tests/components/zone/test_trigger.py
index 0477f9bead7..d7f5857b466 100644
--- a/tests/components/zone/test_trigger.py
+++ b/tests/components/zone/test_trigger.py
@@ -7,6 +7,7 @@ from homeassistant.core import Context
from homeassistant.setup import async_setup_component
from tests.common import async_mock_service, mock_component
+from tests.components.blueprint.conftest import stub_blueprint_populate # noqa
@pytest.fixture
diff --git a/tests/conftest.py b/tests/conftest.py
index 285179e3a9b..fa390f9bf3b 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -7,6 +7,7 @@ import ssl
import threading
from aiohttp.test_utils import make_mocked_request
+import multidict
import pytest
import requests_mock as _requests_mock
@@ -22,11 +23,11 @@ from homeassistant.components.websocket_api.auth import (
from homeassistant.components.websocket_api.http import URL
from homeassistant.const import ATTR_NOW, EVENT_TIME_CHANGED
from homeassistant.exceptions import ServiceNotFound
-from homeassistant.helpers import event
+from homeassistant.helpers import config_entry_oauth2_flow, event
from homeassistant.setup import async_setup_component
from homeassistant.util import location
-from tests.async_mock import MagicMock, Mock, patch
+from tests.async_mock import MagicMock, patch
from tests.ignore_uncaught_exceptions import IGNORE_UNCAUGHT_EXCEPTIONS
pytest.register_assert_rewrite("tests.common")
@@ -277,19 +278,29 @@ def hass_client(hass, aiohttp_client, hass_access_token):
@pytest.fixture
-def current_request(hass):
+def current_request():
"""Mock current request."""
- with patch("homeassistant.helpers.network.current_request") as mock_request_context:
+ with patch("homeassistant.components.http.current_request") as mock_request_context:
mocked_request = make_mocked_request(
"GET",
"/some/request",
headers={"Host": "example.com"},
sslcontext=ssl.SSLContext(ssl.PROTOCOL_TLS),
)
- mock_request_context.get = Mock(return_value=mocked_request)
+ mock_request_context.get.return_value = mocked_request
yield mock_request_context
+@pytest.fixture
+def current_request_with_host(current_request):
+ """Mock current request with a host header."""
+ new_headers = multidict.CIMultiDict(current_request.get.return_value.headers)
+ new_headers[config_entry_oauth2_flow.HEADER_FRONTEND_BASE] = "https://example.com"
+ current_request.get.return_value = current_request.get.return_value.clone(
+ headers=new_headers
+ )
+
+
@pytest.fixture
def hass_ws_client(aiohttp_client, hass_access_token, hass):
"""Websocket client fixture connected to websocket server."""
@@ -526,3 +537,9 @@ def legacy_patchable_time():
async_track_utc_time_change,
):
yield
+
+
+@pytest.fixture
+def enable_custom_integrations(hass):
+ """Enable custom integrations defined in the test dir."""
+ hass.data.pop(loader.DATA_CUSTOM_COMPONENTS)
diff --git a/tests/fixtures/blueprint/community_post.json b/tests/fixtures/blueprint/community_post.json
index 28684ec65f7..121d53ad94e 100644
--- a/tests/fixtures/blueprint/community_post.json
+++ b/tests/fixtures/blueprint/community_post.json
@@ -2,39 +2,58 @@
"post_stream": {
"posts": [
{
- "id": 1144853,
- "name": "Paulus Schoutsen",
- "username": "balloob",
- "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png",
- "created_at": "2020-10-16T12:20:12.688Z",
- "cooked": "\u003cp\u003ehere a test topic.\u003cbr\u003e\nhere a test topic.\u003cbr\u003e\nhere a test topic.\u003cbr\u003e\nhere a test topic.\u003c/p\u003e\n\u003ch1\u003eBlock without syntax\u003c/h1\u003e\n\u003cpre\u003e\u003ccode class=\"lang-auto\"\u003eblueprint:\n domain: automation\n name: Example Blueprint from post\n input:\n trigger_event:\n service_to_call:\ntrigger:\n platform: event\n event_type: !placeholder trigger_event\naction:\n service: !placeholder service_to_call\n\u003c/code\u003e\u003c/pre\u003e",
+ "id": 1216212,
+ "name": "Franck Nijhof",
+ "username": "frenck",
+ "avatar_template": "/user_avatar/community.home-assistant.io/frenck/{size}/161777_2.png",
+ "created_at": "2020-12-10T09:20:58.974Z",
+ "cooked": "\u003cp\u003eThis is a blueprint for the IKEA five-button remotes (the round ones), specifically for use with ZHA.\u003c/p\u003e\n\u003cp\u003e\u003cdiv class=\"lightbox-wrapper\"\u003e\u003ca class=\"lightbox\" href=\"https://community-assets.home-assistant.io/original/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80.jpeg\" data-download-href=\"/uploads/short-url/8SdGCUtkzOTNpMjggpBvSFs4WQ.jpeg?dl=1\" title=\"image\"\u003e\u003cimg src=\"https://community-assets.home-assistant.io/optimized/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80_2_500x500.jpeg\" alt=\"image\" data-base62-sha1=\"8SdGCUtkzOTNpMjggpBvSFs4WQ\" width=\"500\" height=\"500\" srcset=\"https://community-assets.home-assistant.io/optimized/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80_2_500x500.jpeg, https://community-assets.home-assistant.io/optimized/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80_2_750x750.jpeg 1.5x, https://community-assets.home-assistant.io/optimized/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80_2_1000x1000.jpeg 2x\" data-small-upload=\"https://community-assets.home-assistant.io/optimized/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80_2_10x10.png\"\u003e\u003cdiv class=\"meta\"\u003e\u003csvg class=\"fa d-icon d-icon-far-image svg-icon\" aria-hidden=\"true\"\u003e\u003cuse xlink:href=\"#far-image\"\u003e\u003c/use\u003e\u003c/svg\u003e\u003cspan class=\"filename\"\u003eimage\u003c/span\u003e\u003cspan class=\"informations\"\u003e1400×1400 150 KB\u003c/span\u003e\u003csvg class=\"fa d-icon d-icon-discourse-expand svg-icon\" aria-hidden=\"true\"\u003e\u003cuse xlink:href=\"#discourse-expand\"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/div\u003e\u003c/a\u003e\u003c/div\u003e\u003c/p\u003e\n\u003cp\u003eIt was specially created for use with (any) light(s). As the basic light controls are already mapped in this blueprint.\u003c/p\u003e\n\u003cp\u003eThe middle “on” button, toggle the lights on/off to the last set brightness (unless the force brightness is toggled on in the blueprint). Dim up/down buttons will change the brightness smoothly and can be pressed and hold until the brightness is satisfactory.\u003c/p\u003e\n\u003cp\u003eThe “left” and “right” buttons can be assigned to a short and long button press action. This allows you to assign, e.g., a scene or anything else.\u003c/p\u003e\n\u003cp\u003eThis is what the Blueprint looks like from the UI:\u003c/p\u003e\n\u003cp\u003e\u003cdiv class=\"lightbox-wrapper\"\u003e\u003ca class=\"lightbox\" href=\"https://community-assets.home-assistant.io/original/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83.png\" data-download-href=\"/uploads/short-url/mf5vhlKYe6yeuFayUzlCTBfveKf.png?dl=1\" title=\"image\"\u003e\u003cimg src=\"https://community-assets.home-assistant.io/optimized/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83_2_610x500.png\" alt=\"image\" data-base62-sha1=\"mf5vhlKYe6yeuFayUzlCTBfveKf\" width=\"610\" height=\"500\" srcset=\"https://community-assets.home-assistant.io/optimized/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83_2_610x500.png, https://community-assets.home-assistant.io/optimized/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83_2_915x750.png 1.5x, https://community-assets.home-assistant.io/original/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83.png 2x\" data-small-upload=\"https://community-assets.home-assistant.io/optimized/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83_2_10x10.png\"\u003e\u003cdiv class=\"meta\"\u003e\u003csvg class=\"fa d-icon d-icon-far-image svg-icon\" aria-hidden=\"true\"\u003e\u003cuse xlink:href=\"#far-image\"\u003e\u003c/use\u003e\u003c/svg\u003e\u003cspan class=\"filename\"\u003eimage\u003c/span\u003e\u003cspan class=\"informations\"\u003e975×799 64.1 KB\u003c/span\u003e\u003csvg class=\"fa d-icon d-icon-discourse-expand svg-icon\" aria-hidden=\"true\"\u003e\u003cuse xlink:href=\"#discourse-expand\"\u003e\u003c/use\u003e\u003c/svg\u003e\u003c/div\u003e\u003c/a\u003e\u003c/div\u003e\u003c/p\u003e\n\u003cp\u003eBlueprint, which you can import by using this forum topic URL:\u003c/p\u003e\n\u003cpre\u003e\u003ccode class=\"lang-yaml\"\u003eblueprint:\n name: ZHA - IKEA five button remote for lights\n description: |\n Control lights with an IKEA five button remote (the round ones).\n\n The middle \"on\" button, toggle the lights on/off to the last set brightness\n (unless the force brightness is toggled on in the blueprint).\n\n Dim up/down buttons will change the brightness smoothly and can be pressed\n and hold until the brightness is satisfactory.\n\n The \"left\" and \"right\" buttons can be assigned to a short and long button\n press action. This allows you to assign, e.g., a scene or anything else.\n\n domain: automation\n input:\n remote:\n name: Remote\n description: IKEA remote to use\n selector:\n device:\n integration: zha\n manufacturer: IKEA of Sweden\n model: TRADFRI remote control\n light:\n name: Light(s)\n description: The light(s) to control\n selector:\n target:\n entity:\n domain: light\n force_brightness:\n name: Force turn on brightness\n description: \u0026gt;\n Force the brightness to the set level below, when the \"on\" button on\n the remote is pushed and lights turn on.\n default: false\n selector:\n boolean:\n brightness:\n name: Brightness\n description: Brightness of the light(s) when turning on\n default: 50\n selector:\n number:\n min: 0\n max: 100\n mode: slider\n step: 1\n unit_of_measurement: \"%\"\n button_left_short:\n name: Left button - short press\n description: Action to run on short left button press\n default: []\n selector:\n action:\n button_left_long:\n name: Left button - long press\n description: Action to run on long left button press\n default: []\n selector:\n action:\n button_right_short:\n name: Right button - short press\n description: Action to run on short right button press\n default: []\n selector:\n action:\n button_right_long:\n name: Right button - long press\n description: Action to run on long right button press\n default: []\n selector:\n action:\n\nmode: restart\nmax_exceeded: silent\n\nvariables:\n force_brightness: !input force_brightness\n\ntrigger:\n - platform: event\n event_type: zha_event\n event_data:\n device_id: !input remote\n\naction:\n - variables:\n command: \"{{ trigger.event.data.command }}\"\n cluster_id: \"{{ trigger.event.data.cluster_id }}\"\n endpoint_id: \"{{ trigger.event.data.endpoint_id }}\"\n args: \"{{ trigger.event.data.args }}\"\n - choose:\n - conditions:\n - \"{{ command == 'toggle' }}\"\n - \"{{ cluster_id == 6 }}\"\n - \"{{ endpoint_id == 1 }}\"\n sequence:\n - choose:\n - conditions: \"{{ force_brightness }}\"\n sequence:\n - service: light.toggle\n target: !input light\n data:\n transition: 1\n brightness_pct: !input brightness\n default:\n - service: light.toggle\n target: !input light\n data:\n transition: 1\n\n - conditions:\n - \"{{ command == 'step_with_on_off' }}\"\n - \"{{ cluster_id == 8 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [0, 43, 5] }}\"\n sequence:\n - service: light.turn_on\n target: !input light\n data:\n brightness_step_pct: 10\n transition: 1\n\n - conditions:\n - \"{{ command == 'move_with_on_off' }}\"\n - \"{{ cluster_id == 8 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [0, 84] }}\"\n sequence:\n - repeat:\n count: 10\n sequence:\n - service: light.turn_on\n target: !input light\n data:\n brightness_step_pct: 10\n transition: 1\n - delay: 1\n\n - conditions:\n - \"{{ command == 'step' }}\"\n - \"{{ cluster_id == 8 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [1, 43, 5] }}\"\n sequence:\n - service: light.turn_on\n target: !input light\n data:\n brightness_step_pct: -10\n transition: 1\n\n - conditions:\n - \"{{ command == 'move' }}\"\n - \"{{ cluster_id == 8 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [1, 84] }}\"\n sequence:\n - repeat:\n count: 10\n sequence:\n - service: light.turn_on\n target: !input light\n data:\n brightness_step_pct: -10\n transition: 1\n - delay: 1\n\n - conditions:\n - \"{{ command == 'press' }}\"\n - \"{{ cluster_id == 5 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [257, 13, 0] }}\"\n sequence: !input button_left_short\n\n - conditions:\n - \"{{ command == 'hold' }}\"\n - \"{{ cluster_id == 5 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [3329, 0] }}\"\n sequence: !input button_left_long\n\n - conditions:\n - \"{{ command == 'press' }}\"\n - \"{{ cluster_id == 5 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [256, 13, 0] }}\"\n sequence: !input button_right_short\n\n - conditions:\n - \"{{ command == 'hold' }}\"\n - \"{{ cluster_id == 5 }}\"\n - \"{{ endpoint_id == 1 }}\"\n - \"{{ args == [3328, 0] }}\"\n sequence: !input button_right_long\n\u003c/code\u003e\u003c/pre\u003e",
"post_number": 1,
"post_type": 1,
- "updated_at": "2020-10-20T08:24:14.189Z",
+ "updated_at": "2020-12-10T09:22:08.993Z",
"reply_count": 0,
"reply_to_post_number": null,
"quote_count": 0,
"incoming_link_count": 0,
- "reads": 2,
- "readers_count": 1,
- "score": 0.4,
- "yours": true,
- "topic_id": 236133,
- "topic_slug": "test-topic",
- "display_username": "Paulus Schoutsen",
+ "reads": 3,
+ "readers_count": 2,
+ "score": 0.6,
+ "yours": false,
+ "topic_id": 253804,
+ "topic_slug": "zha-ikea-five-button-remote-for-lights",
+ "display_username": "Franck Nijhof",
"primary_group_name": null,
"primary_group_flair_url": null,
"primary_group_flair_bg_color": null,
"primary_group_flair_color": null,
- "version": 2,
+ "version": 1,
"can_edit": true,
"can_delete": false,
"can_recover": false,
"can_wiki": true,
+ "link_counts": [
+ {
+ "url": "https://community-assets.home-assistant.io/original/3X/9/b/9be4788b5358284d138c4304fb0b8068c18a2b83.png",
+ "internal": false,
+ "reflection": false,
+ "title": "9be4788b5358284d138c4304fb0b8068c18a2b83.png",
+ "clicks": 0
+ },
+ {
+ "url": "https://community-assets.home-assistant.io/original/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80.jpeg",
+ "internal": false,
+ "reflection": false,
+ "title": "0100d04d2debf34eb11abdfee0707624f3961f80.jpeg",
+ "clicks": 0
+ }
+ ],
"read": true,
- "user_title": "Founder of Home Assistant",
- "title_is_group": false,
+ "user_title": null,
"actions_summary": [
+ {
+ "id": 2,
+ "can_act": true
+ },
{
"id": 3,
"can_act": true
@@ -48,75 +67,7 @@
"can_act": true
},
{
- "id": 7,
- "can_act": true
- }
- ],
- "moderator": true,
- "admin": true,
- "staff": true,
- "user_id": 3,
- "hidden": false,
- "trust_level": 2,
- "deleted_at": null,
- "user_deleted": false,
- "edit_reason": null,
- "can_view_edit_history": true,
- "wiki": false,
- "reviewable_id": 0,
- "reviewable_score_count": 0,
- "reviewable_score_pending_count": 0,
- "user_created_at": "2016-03-30T07:50:25.541Z",
- "user_date_of_birth": null,
- "user_signature": null,
- "can_accept_answer": false,
- "can_unaccept_answer": false,
- "accepted_answer": false
- },
- {
- "id": 1144854,
- "name": "Paulus Schoutsen",
- "username": "balloob",
- "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png",
- "created_at": "2020-10-16T12:20:17.535Z",
- "cooked": "",
- "post_number": 2,
- "post_type": 3,
- "updated_at": "2020-10-16T12:20:17.535Z",
- "reply_count": 0,
- "reply_to_post_number": null,
- "quote_count": 0,
- "incoming_link_count": 1,
- "reads": 2,
- "readers_count": 1,
- "score": 5.4,
- "yours": true,
- "topic_id": 236133,
- "topic_slug": "test-topic",
- "display_username": "Paulus Schoutsen",
- "primary_group_name": null,
- "primary_group_flair_url": null,
- "primary_group_flair_bg_color": null,
- "primary_group_flair_color": null,
- "version": 1,
- "can_edit": true,
- "can_delete": true,
- "can_recover": false,
- "can_wiki": true,
- "read": true,
- "user_title": "Founder of Home Assistant",
- "title_is_group": false,
- "actions_summary": [
- {
- "id": 3,
- "can_act": true
- },
- {
- "id": 4,
- "can_act": true
- },
- {
- "id": 8,
+ "id": 6,
"can_act": true
},
{
@@ -127,82 +78,9 @@
"moderator": true,
"admin": true,
"staff": true,
- "user_id": 3,
+ "user_id": 10250,
"hidden": false,
- "trust_level": 2,
- "deleted_at": null,
- "user_deleted": false,
- "edit_reason": null,
- "can_view_edit_history": true,
- "wiki": false,
- "action_code": "visible.disabled",
- "reviewable_id": 0,
- "reviewable_score_count": 0,
- "reviewable_score_pending_count": 0,
- "user_created_at": "2016-03-30T07:50:25.541Z",
- "user_date_of_birth": null,
- "user_signature": null,
- "can_accept_answer": false,
- "can_unaccept_answer": false,
- "accepted_answer": false
- },
- {
- "id": 1144872,
- "name": "Paulus Schoutsen",
- "username": "balloob",
- "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png",
- "created_at": "2020-10-16T12:27:53.926Z",
- "cooked": "\u003cp\u003eTest reply!\u003c/p\u003e",
- "post_number": 3,
- "post_type": 1,
- "updated_at": "2020-10-16T12:27:53.926Z",
- "reply_count": 0,
- "reply_to_post_number": null,
- "quote_count": 0,
- "incoming_link_count": 0,
- "reads": 2,
- "readers_count": 1,
- "score": 0.4,
- "yours": true,
- "topic_id": 236133,
- "topic_slug": "test-topic",
- "display_username": "Paulus Schoutsen",
- "primary_group_name": null,
- "primary_group_flair_url": null,
- "primary_group_flair_bg_color": null,
- "primary_group_flair_color": null,
- "version": 1,
- "can_edit": true,
- "can_delete": true,
- "can_recover": false,
- "can_wiki": true,
- "read": true,
- "user_title": "Founder of Home Assistant",
- "title_is_group": false,
- "actions_summary": [
- {
- "id": 3,
- "can_act": true
- },
- {
- "id": 4,
- "can_act": true
- },
- {
- "id": 8,
- "can_act": true
- },
- {
- "id": 7,
- "can_act": true
- }
- ],
- "moderator": true,
- "admin": true,
- "staff": true,
- "user_id": 3,
- "hidden": false,
- "trust_level": 2,
+ "trust_level": 4,
"deleted_at": null,
"user_deleted": false,
"edit_reason": null,
@@ -211,7 +89,7 @@
"reviewable_id": 0,
"reviewable_score_count": 0,
"reviewable_score_pending_count": 0,
- "user_created_at": "2016-03-30T07:50:25.541Z",
+ "user_created_at": "2017-08-12T12:46:55.467Z",
"user_date_of_birth": null,
"user_signature": null,
"can_accept_answer": false,
@@ -220,36 +98,34 @@
}
],
"stream": [
- 1144853,
- 1144854,
- 1144872
+ 1216212
]
},
"timeline_lookup": [
[
1,
- 3
+ 0
]
],
"suggested_topics": [
{
- "id": 17750,
- "title": "Tutorial: Creating your first add-on",
- "fancy_title": "Tutorial: Creating your first add-on",
- "slug": "tutorial-creating-your-first-add-on",
- "posts_count": 26,
- "reply_count": 14,
- "highest_post_number": 27,
- "image_url": null,
- "created_at": "2017-05-14T07:51:33.946Z",
- "last_posted_at": "2020-07-28T11:29:27.892Z",
+ "id": 168593,
+ "title": "Dwains Dashboard - 1 CLICK install Lovelace Dashboard for desktop, tablet and mobile. v2.0.0",
+ "fancy_title": "Dwains Dashboard - 1 CLICK install Lovelace Dashboard for desktop, tablet and mobile. v2.0.0",
+ "slug": "dwains-dashboard-1-click-install-lovelace-dashboard-for-desktop-tablet-and-mobile-v2-0-0",
+ "posts_count": 1162,
+ "reply_count": 785,
+ "highest_post_number": 1185,
+ "image_url": "//community-assets.home-assistant.io/original/3X/a/0/a051e5940117bebcb70e8d8545ad4b65f63bd175.jpeg",
+ "created_at": "2020-02-03T13:15:24.364Z",
+ "last_posted_at": "2020-12-10T07:57:47.304Z",
"bumped": true,
- "bumped_at": "2020-07-28T11:29:27.892Z",
+ "bumped_at": "2020-12-10T07:57:47.304Z",
"archetype": "regular",
"unseen": false,
- "last_read_post_number": 18,
- "unread": 7,
- "new_posts": 2,
+ "last_read_post_number": 81,
+ "unread": 0,
+ "new_posts": 1109,
"pinned": false,
"unpinned": null,
"visible": true,
@@ -258,11 +134,19 @@
"notification_level": 2,
"bookmarked": false,
"liked": false,
- "thumbnails": null,
+ "thumbnails": [
+ {
+ "max_width": null,
+ "max_height": null,
+ "width": 296,
+ "height": 50,
+ "url": "//community-assets.home-assistant.io/original/3X/a/0/a051e5940117bebcb70e8d8545ad4b65f63bd175.jpeg"
+ }
+ ],
"tags": [],
- "like_count": 9,
- "views": 4355,
- "category_id": 25,
+ "like_count": 1214,
+ "views": 71580,
+ "category_id": 34,
"featured_link": null,
"has_accepted_answer": false,
"posters": [
@@ -270,50 +154,50 @@
"extras": null,
"description": "Original Poster",
"user": {
- "id": 3,
- "username": "balloob",
- "name": "Paulus Schoutsen",
- "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png"
+ "id": 36674,
+ "username": "dwains",
+ "name": "Dwain Scheeren",
+ "avatar_template": "/user_avatar/community.home-assistant.io/dwains/{size}/100261_2.png"
}
},
{
"extras": null,
"description": "Frequent Poster",
"user": {
- "id": 9852,
- "username": "JSCSJSCS",
+ "id": 16514,
+ "username": "jimpower",
"name": "",
- "avatar_template": "/user_avatar/community.home-assistant.io/jscsjscs/{size}/38256_2.png"
+ "avatar_template": "/user_avatar/community.home-assistant.io/jimpower/{size}/66909_2.png"
}
},
{
"extras": null,
"description": "Frequent Poster",
"user": {
- "id": 11494,
- "username": "so3n",
- "name": "",
- "avatar_template": "/user_avatar/community.home-assistant.io/so3n/{size}/46007_2.png"
+ "id": 1473,
+ "username": "thundergreen",
+ "name": "Thundergreen",
+ "avatar_template": "/user_avatar/community.home-assistant.io/thundergreen/{size}/18379_2.png"
}
},
{
"extras": null,
"description": "Frequent Poster",
"user": {
- "id": 9094,
- "username": "IoTnerd",
- "name": "Balázs Suhajda",
- "avatar_template": "/user_avatar/community.home-assistant.io/iotnerd/{size}/33526_2.png"
+ "id": 64369,
+ "username": "MRobi",
+ "name": "Mike",
+ "avatar_template": "/user_avatar/community.home-assistant.io/mrobi/{size}/113127_2.png"
}
},
{
"extras": "latest",
"description": "Most Recent Poster",
"user": {
- "id": 73134,
- "username": "diord",
- "name": "",
- "avatar_template": "/letter_avatar/diord/{size}/5_70a404e2c8e633b245e797a566d32dc7.png"
+ "id": 9646,
+ "username": "Freshhat",
+ "name": "Freshhat",
+ "avatar_template": "/user_avatar/community.home-assistant.io/freshhat/{size}/24797_2.png"
}
}
]
@@ -323,19 +207,19 @@
"title": "Lovelace: Button card",
"fancy_title": "Lovelace: Button card",
"slug": "lovelace-button-card",
- "posts_count": 4608,
- "reply_count": 3522,
- "highest_post_number": 4691,
+ "posts_count": 4775,
+ "reply_count": 3635,
+ "highest_post_number": 4858,
"image_url": null,
"created_at": "2018-08-28T00:18:19.312Z",
- "last_posted_at": "2020-10-20T07:33:29.523Z",
+ "last_posted_at": "2020-12-10T04:42:58.851Z",
"bumped": true,
- "bumped_at": "2020-10-20T07:33:29.523Z",
+ "bumped_at": "2020-12-10T04:42:58.851Z",
"archetype": "regular",
"unseen": false,
"last_read_post_number": 1938,
"unread": 369,
- "new_posts": 2384,
+ "new_posts": 2551,
"pinned": false,
"unpinned": null,
"visible": true,
@@ -346,8 +230,8 @@
"liked": false,
"thumbnails": null,
"tags": [],
- "like_count": 1700,
- "views": 184752,
+ "like_count": 1740,
+ "views": 199965,
"category_id": 34,
"featured_link": null,
"has_accepted_answer": false,
@@ -366,20 +250,20 @@
"extras": null,
"description": "Frequent Poster",
"user": {
- "id": 2019,
- "username": "iantrich",
- "name": "Ian",
- "avatar_template": "/user_avatar/community.home-assistant.io/iantrich/{size}/154042_2.png"
+ "id": 33228,
+ "username": "jimz011",
+ "name": "Jim",
+ "avatar_template": "/user_avatar/community.home-assistant.io/jimz011/{size}/62413_2.png"
}
},
{
"extras": null,
"description": "Frequent Poster",
"user": {
- "id": 33228,
- "username": "jimz011",
- "name": "Jim",
- "avatar_template": "/user_avatar/community.home-assistant.io/jimz011/{size}/62413_2.png"
+ "id": 12475,
+ "username": "Mariusthvdb",
+ "name": "Marius",
+ "avatar_template": "/user_avatar/community.home-assistant.io/mariusthvdb/{size}/49008_2.png"
}
},
{
@@ -396,32 +280,32 @@
"extras": "latest",
"description": "Most Recent Poster",
"user": {
- "id": 26227,
- "username": "RomRider",
- "name": "",
- "avatar_template": "/user_avatar/community.home-assistant.io/romrider/{size}/41384_2.png"
+ "id": 52090,
+ "username": "parautenbach",
+ "name": "Pieter Rautenbach",
+ "avatar_template": "/user_avatar/community.home-assistant.io/parautenbach/{size}/89345_2.png"
}
}
]
},
{
- "id": 10564,
- "title": "Professional/Commercial Use?",
- "fancy_title": "Professional/Commercial Use?",
- "slug": "professional-commercial-use",
- "posts_count": 54,
- "reply_count": 37,
- "highest_post_number": 54,
+ "id": 58639,
+ "title": "Echo Devices (Alexa) as Media Player - Testers Needed",
+ "fancy_title": "Echo Devices (Alexa) as Media Player - Testers Needed",
+ "slug": "echo-devices-alexa-as-media-player-testers-needed",
+ "posts_count": 4429,
+ "reply_count": 3009,
+ "highest_post_number": 4517,
"image_url": null,
- "created_at": "2017-01-27T05:01:57.453Z",
- "last_posted_at": "2020-10-20T07:03:57.895Z",
+ "created_at": "2018-07-04T03:36:22.187Z",
+ "last_posted_at": "2020-12-10T04:26:11.298Z",
"bumped": true,
- "bumped_at": "2020-10-20T07:03:57.895Z",
+ "bumped_at": "2020-12-10T04:26:11.298Z",
"archetype": "regular",
"unseen": false,
- "last_read_post_number": 7,
+ "last_read_post_number": 3219,
"unread": 0,
- "new_posts": 47,
+ "new_posts": 1298,
"pinned": false,
"unpinned": null,
"visible": true,
@@ -431,104 +315,12 @@
"bookmarked": false,
"liked": false,
"thumbnails": null,
- "tags": [],
- "like_count": 21,
- "views": 10695,
- "category_id": 17,
- "featured_link": null,
- "has_accepted_answer": false,
- "posters": [
- {
- "extras": null,
- "description": "Original Poster",
- "user": {
- "id": 4758,
- "username": "oobie11",
- "name": "Bryan",
- "avatar_template": "/user_avatar/community.home-assistant.io/oobie11/{size}/37858_2.png"
- }
- },
- {
- "extras": null,
- "description": "Frequent Poster",
- "user": {
- "id": 18386,
- "username": "pitp2",
- "name": "",
- "avatar_template": "/letter_avatar/pitp2/{size}/5_70a404e2c8e633b245e797a566d32dc7.png"
- }
- },
- {
- "extras": null,
- "description": "Frequent Poster",
- "user": {
- "id": 23116,
- "username": "jortegamx",
- "name": "Jake",
- "avatar_template": "/user_avatar/community.home-assistant.io/jortegamx/{size}/45515_2.png"
- }
- },
- {
- "extras": null,
- "description": "Frequent Poster",
- "user": {
- "id": 39038,
- "username": "orif73",
- "name": "orif73",
- "avatar_template": "/letter_avatar/orif73/{size}/5_70a404e2c8e633b245e797a566d32dc7.png"
- }
- },
- {
- "extras": "latest",
- "description": "Most Recent Poster",
- "user": {
- "id": 41040,
- "username": "devastator",
- "name": "",
- "avatar_template": "/letter_avatar/devastator/{size}/5_70a404e2c8e633b245e797a566d32dc7.png"
- }
- }
- ]
- },
- {
- "id": 219480,
- "title": "What the heck is with the 'latest state change' not being kept after restart?",
- "fancy_title": "What the heck is with the \u0026lsquo;latest state change\u0026rsquo; not being kept after restart?",
- "slug": "what-the-heck-is-with-the-latest-state-change-not-being-kept-after-restart",
- "posts_count": 37,
- "reply_count": 13,
- "highest_post_number": 38,
- "image_url": "https://community-assets.home-assistant.io/original/3X/3/4/349d096b209d40d5f424b64e970bcf360332cc7f.png",
- "created_at": "2020-08-18T13:10:09.367Z",
- "last_posted_at": "2020-10-20T00:32:07.312Z",
- "bumped": true,
- "bumped_at": "2020-10-20T00:32:07.312Z",
- "archetype": "regular",
- "unseen": false,
- "last_read_post_number": 8,
- "unread": 0,
- "new_posts": 30,
- "pinned": false,
- "unpinned": null,
- "visible": true,
- "closed": false,
- "archived": false,
- "notification_level": 2,
- "bookmarked": false,
- "liked": false,
- "thumbnails": [
- {
- "max_width": null,
- "max_height": null,
- "width": 469,
- "height": 59,
- "url": "https://community-assets.home-assistant.io/original/3X/3/4/349d096b209d40d5f424b64e970bcf360332cc7f.png"
- }
+ "tags": [
+ "alexa"
],
- "tags": [],
- "like_count": 26,
- "views": 1722,
- "category_id": 52,
+ "like_count": 1092,
+ "views": 179580,
+ "category_id": 47,
"featured_link": null,
"has_accepted_answer": false,
"posters": [
@@ -536,72 +328,72 @@
"extras": null,
"description": "Original Poster",
"user": {
- "id": 3124,
- "username": "andriej",
+ "id": 1084,
+ "username": "keatontaylor",
+ "name": "Keatontaylor",
+ "avatar_template": "/letter_avatar/keatontaylor/{size}/5_70a404e2c8e633b245e797a566d32dc7.png"
+ }
+ },
+ {
+ "extras": null,
+ "description": "Frequent Poster",
+ "user": {
+ "id": 24884,
+ "username": "h4nc",
"name": "",
- "avatar_template": "/user_avatar/community.home-assistant.io/andriej/{size}/24457_2.png"
+ "avatar_template": "/user_avatar/community.home-assistant.io/h4nc/{size}/68244_2.png"
}
},
{
"extras": null,
"description": "Frequent Poster",
"user": {
- "id": 15052,
- "username": "Misiu",
+ "id": 9191,
+ "username": "finity",
"name": "",
- "avatar_template": "/user_avatar/community.home-assistant.io/misiu/{size}/20752_2.png"
+ "avatar_template": "/letter_avatar/finity/{size}/5_70a404e2c8e633b245e797a566d32dc7.png"
}
},
{
"extras": null,
"description": "Frequent Poster",
"user": {
- "id": 4629,
- "username": "lolouk44",
- "name": "lolouk44",
- "avatar_template": "/user_avatar/community.home-assistant.io/lolouk44/{size}/119845_2.png"
- }
- },
- {
- "extras": null,
- "description": "Frequent Poster",
- "user": {
- "id": 51736,
- "username": "hmoffatt",
- "name": "Hamish Moffatt",
- "avatar_template": "/user_avatar/community.home-assistant.io/hmoffatt/{size}/88700_2.png"
+ "id": 1269,
+ "username": "ReneTode",
+ "name": "",
+ "avatar_template": "/user_avatar/community.home-assistant.io/renetode/{size}/1533_2.png"
}
},
{
"extras": "latest",
"description": "Most Recent Poster",
"user": {
- "id": 78711,
- "username": "Astrosteve",
- "name": "Steve",
- "avatar_template": "/letter_avatar/astrosteve/{size}/5_70a404e2c8e633b245e797a566d32dc7.png"
+ "id": 46136,
+ "username": "chirad",
+ "name": "Dinoj",
+ "avatar_template": "/letter_avatar/chirad/{size}/5_70a404e2c8e633b245e797a566d32dc7.png"
}
}
]
},
{
- "id": 162594,
- "title": "A different take on designing a Lovelace UI",
- "fancy_title": "A different take on designing a Lovelace UI",
- "slug": "a-different-take-on-designing-a-lovelace-ui",
- "posts_count": 641,
- "reply_count": 425,
- "highest_post_number": 654,
+ "id": 252336,
+ "title": "Unhealthy state",
+ "fancy_title": "Unhealthy state",
+ "slug": "unhealthy-state",
+ "posts_count": 89,
+ "reply_count": 69,
+ "highest_post_number": 91,
"image_url": null,
- "created_at": "2020-01-11T23:09:25.207Z",
- "last_posted_at": "2020-10-19T23:32:15.555Z",
+ "created_at": "2020-12-05T20:32:00.864Z",
+ "last_posted_at": "2020-12-09T22:41:30.212Z",
"bumped": true,
- "bumped_at": "2020-10-19T23:32:15.555Z",
+ "bumped_at": "2020-12-09T22:41:30.212Z",
"archetype": "regular",
"unseen": false,
- "last_read_post_number": 7,
- "unread": 32,
- "new_posts": 615,
+ "last_read_post_number": 75,
+ "unread": 0,
+ "new_posts": 16,
"pinned": false,
"unpinned": null,
"visible": true,
@@ -609,12 +401,12 @@
"archived": false,
"notification_level": 2,
"bookmarked": false,
- "liked": false,
+ "liked": true,
"thumbnails": null,
"tags": [],
- "like_count": 453,
- "views": 68547,
- "category_id": 9,
+ "like_count": 33,
+ "views": 946,
+ "category_id": 11,
"featured_link": null,
"has_accepted_answer": false,
"posters": [
@@ -622,90 +414,179 @@
"extras": null,
"description": "Original Poster",
"user": {
- "id": 11256,
- "username": "Mattias_Persson",
- "name": "Mattias Persson",
- "avatar_template": "/user_avatar/community.home-assistant.io/mattias_persson/{size}/14773_2.png"
+ "id": 26121,
+ "username": "helgemor",
+ "name": "Helge",
+ "avatar_template": "/user_avatar/community.home-assistant.io/helgemor/{size}/42574_2.png"
}
},
{
"extras": null,
"description": "Frequent Poster",
"user": {
- "id": 27634,
- "username": "Jason_hill",
- "name": "Jason Hill",
- "avatar_template": "/user_avatar/community.home-assistant.io/jason_hill/{size}/93218_2.png"
+ "id": 3204,
+ "username": "nickrout",
+ "name": "Nick Rout",
+ "avatar_template": "/user_avatar/community.home-assistant.io/nickrout/{size}/27020_2.png"
}
},
{
"extras": null,
"description": "Frequent Poster",
"user": {
- "id": 46782,
- "username": "Martin_Pejstrup",
- "name": "mpejstrup",
- "avatar_template": "/user_avatar/community.home-assistant.io/martin_pejstrup/{size}/78412_2.png"
+ "id": 28146,
+ "username": "123",
+ "name": "Taras",
+ "avatar_template": "/user_avatar/community.home-assistant.io/123/{size}/44349_2.png"
}
},
{
"extras": null,
"description": "Frequent Poster",
"user": {
- "id": 46841,
- "username": "spudje",
- "name": "",
- "avatar_template": "/letter_avatar/spudje/{size}/5_70a404e2c8e633b245e797a566d32dc7.png"
+ "id": 8361,
+ "username": "kanga_who",
+ "name": "Jason",
+ "avatar_template": "/user_avatar/community.home-assistant.io/kanga_who/{size}/46427_2.png"
}
},
{
"extras": "latest",
"description": "Most Recent Poster",
"user": {
- "id": 20924,
- "username": "Diego_Santos",
- "name": "Diego Santos",
- "avatar_template": "/user_avatar/community.home-assistant.io/diego_santos/{size}/29096_2.png"
+ "id": 44704,
+ "username": "joselito1",
+ "name": "jose litomans",
+ "avatar_template": "/user_avatar/community.home-assistant.io/joselito1/{size}/75914_2.png"
+ }
+ }
+ ]
+ },
+ {
+ "id": 130280,
+ "title": "Home Assistant Cast",
+ "fancy_title": "Home Assistant Cast",
+ "slug": "home-assistant-cast",
+ "posts_count": 282,
+ "reply_count": 206,
+ "highest_post_number": 289,
+ "image_url": null,
+ "created_at": "2019-08-06T15:59:00.183Z",
+ "last_posted_at": "2020-12-09T16:48:51.132Z",
+ "bumped": true,
+ "bumped_at": "2020-12-09T16:48:51.132Z",
+ "archetype": "regular",
+ "unseen": false,
+ "last_read_post_number": 88,
+ "unread": 0,
+ "new_posts": 201,
+ "pinned": false,
+ "unpinned": null,
+ "visible": true,
+ "closed": false,
+ "archived": false,
+ "notification_level": 3,
+ "bookmarked": false,
+ "liked": false,
+ "thumbnails": null,
+ "tags": [],
+ "like_count": 94,
+ "views": 29308,
+ "category_id": 30,
+ "featured_link": null,
+ "has_accepted_answer": false,
+ "posters": [
+ {
+ "extras": null,
+ "description": "Original Poster",
+ "user": {
+ "id": -1,
+ "username": "system",
+ "name": "system",
+ "avatar_template": "/user_avatar/community.home-assistant.io/system/{size}/13_2.png"
+ }
+ },
+ {
+ "extras": null,
+ "description": "Frequent Poster",
+ "user": {
+ "id": 11649,
+ "username": "DavidFW1960",
+ "name": "David",
+ "avatar_template": "/user_avatar/community.home-assistant.io/davidfw1960/{size}/66886_2.png"
+ }
+ },
+ {
+ "extras": null,
+ "description": "Frequent Poster",
+ "user": {
+ "id": 26084,
+ "username": "Yoinkz",
+ "name": "",
+ "avatar_template": "/letter_avatar/yoinkz/{size}/5_70a404e2c8e633b245e797a566d32dc7.png"
+ }
+ },
+ {
+ "extras": null,
+ "description": "Frequent Poster",
+ "user": {
+ "id": 3204,
+ "username": "nickrout",
+ "name": "Nick Rout",
+ "avatar_template": "/user_avatar/community.home-assistant.io/nickrout/{size}/27020_2.png"
+ }
+ },
+ {
+ "extras": "latest",
+ "description": "Most Recent Poster",
+ "user": {
+ "id": 45396,
+ "username": "Wetzel402",
+ "name": "Cody",
+ "avatar_template": "/user_avatar/community.home-assistant.io/wetzel402/{size}/76694_2.png"
}
}
]
}
],
- "tags": [],
- "id": 236133,
- "title": "Test Topic",
- "fancy_title": "Test Topic",
- "posts_count": 3,
- "created_at": "2020-10-16T12:20:12.580Z",
- "views": 13,
+ "tags": [
+ "blueprint",
+ "zha"
+ ],
+ "id": 253804,
+ "title": "ZHA - IKEA five button remote for lights",
+ "fancy_title": "ZHA - IKEA five button remote for lights",
+ "posts_count": 1,
+ "created_at": "2020-12-10T09:20:58.681Z",
+ "views": 4,
"reply_count": 0,
"like_count": 0,
- "last_posted_at": "2020-10-16T12:27:53.926Z",
- "visible": false,
+ "last_posted_at": "2020-12-10T09:20:58.974Z",
+ "visible": true,
"closed": false,
"archived": false,
"has_summary": false,
"archetype": "regular",
- "slug": "test-topic",
- "category_id": 1,
- "word_count": 37,
+ "slug": "zha-ikea-five-button-remote-for-lights",
+ "category_id": 53,
+ "word_count": 633,
"deleted_at": null,
- "user_id": 3,
+ "user_id": 10250,
"featured_link": null,
"pinned_globally": false,
"pinned_at": null,
"pinned_until": null,
- "image_url": null,
+ "image_url": "https://community-assets.home-assistant.io/original/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80.jpeg",
"draft": null,
- "draft_key": "topic_236133",
- "draft_sequence": 8,
- "posted": true,
+ "draft_key": "topic_253804",
+ "draft_sequence": 0,
+ "posted": false,
"unpinned": null,
"pinned": false,
"current_post_number": 1,
- "highest_post_number": 3,
- "last_read_post_number": 3,
- "last_read_post_id": 1144872,
+ "highest_post_number": 1,
+ "last_read_post_number": 1,
+ "last_read_post_id": 1216212,
"deleted_by": null,
"has_deleted": false,
"actions_summary": [
@@ -732,16 +613,24 @@
"bookmarked": false,
"topic_timer": null,
"private_topic_timer": null,
- "message_bus_last_id": 5,
+ "message_bus_last_id": 4,
"participant_count": 1,
"show_read_indicator": false,
- "thumbnails": null,
+ "thumbnails": [
+ {
+ "max_width": null,
+ "max_height": null,
+ "width": 1400,
+ "height": 1400,
+ "url": "https://community-assets.home-assistant.io/original/3X/0/1/0100d04d2debf34eb11abdfee0707624f3961f80.jpeg"
+ }
+ ],
"can_vote": false,
"vote_count": null,
"user_voted": false,
"details": {
- "notification_level": 3,
- "notifications_reason_id": 1,
+ "notification_level": 1,
+ "notifications_reason_id": null,
"can_move_posts": true,
"can_edit": true,
"can_delete": true,
@@ -756,11 +645,11 @@
"can_remove_self_id": 3,
"participants": [
{
- "id": 3,
- "username": "balloob",
- "name": "Paulus Schoutsen",
- "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png",
- "post_count": 3,
+ "id": 10250,
+ "username": "frenck",
+ "name": "Franck Nijhof",
+ "avatar_template": "/user_avatar/community.home-assistant.io/frenck/{size}/161777_2.png",
+ "post_count": 1,
"primary_group_name": null,
"primary_group_flair_url": null,
"primary_group_flair_color": null,
@@ -768,16 +657,16 @@
}
],
"created_by": {
- "id": 3,
- "username": "balloob",
- "name": "Paulus Schoutsen",
- "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png"
+ "id": 10250,
+ "username": "frenck",
+ "name": "Franck Nijhof",
+ "avatar_template": "/user_avatar/community.home-assistant.io/frenck/{size}/161777_2.png"
},
"last_poster": {
- "id": 3,
- "username": "balloob",
- "name": "Paulus Schoutsen",
- "avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png"
+ "id": 10250,
+ "username": "frenck",
+ "name": "Franck Nijhof",
+ "avatar_template": "/user_avatar/community.home-assistant.io/frenck/{size}/161777_2.png"
}
}
}
diff --git a/tests/fixtures/blueprint/github_gist.json b/tests/fixtures/blueprint/github_gist.json
new file mode 100644
index 00000000000..208e8b54a71
--- /dev/null
+++ b/tests/fixtures/blueprint/github_gist.json
@@ -0,0 +1,84 @@
+{
+ "url": "https://api.github.com/gists/e717ce85dd0d2f1bdcdfc884ea25a344",
+ "forks_url": "https://api.github.com/gists/e717ce85dd0d2f1bdcdfc884ea25a344/forks",
+ "commits_url": "https://api.github.com/gists/e717ce85dd0d2f1bdcdfc884ea25a344/commits",
+ "id": "e717ce85dd0d2f1bdcdfc884ea25a344",
+ "node_id": "MDQ6R2lzdGU3MTdjZTg1ZGQwZDJmMWJkY2RmYzg4NGVhMjVhMzQ0",
+ "git_pull_url": "https://gist.github.com/e717ce85dd0d2f1bdcdfc884ea25a344.git",
+ "git_push_url": "https://gist.github.com/e717ce85dd0d2f1bdcdfc884ea25a344.git",
+ "html_url": "https://gist.github.com/e717ce85dd0d2f1bdcdfc884ea25a344",
+ "files": {
+ "motion_light.yaml": {
+ "filename": "motion_light.yaml",
+ "type": "text/x-yaml",
+ "language": "YAML",
+ "raw_url": "https://gist.githubusercontent.com/balloob/e717ce85dd0d2f1bdcdfc884ea25a344/raw/d3cede19ffed75443934325177cd78d26b1700ad/motion_light.yaml",
+ "size": 803,
+ "truncated": false,
+ "content": "blueprint:\n name: Motion-activated Light\n domain: automation\n input:\n motion_entity:\n name: Motion Sensor\n selector:\n entity:\n domain: binary_sensor\n device_class: motion\n light_entity:\n name: Light\n selector:\n entity:\n domain: light\n\n# If motion is detected within the 120s delay,\n# we restart the script.\nmode: restart\nmax_exceeded: silent\n\ntrigger:\n platform: state\n entity_id: !input motion_entity\n from: \"off\"\n to: \"on\"\n\naction:\n - service: homeassistant.turn_on\n entity_id: !input light_entity\n - wait_for_trigger:\n platform: state\n entity_id: !input motion_entity\n from: \"on\"\n to: \"off\"\n - delay: 120\n - service: homeassistant.turn_off\n entity_id: !input light_entity\n"
+ }
+ },
+ "public": false,
+ "created_at": "2020-11-25T22:49:50Z",
+ "updated_at": "2020-11-25T22:49:51Z",
+ "description": "Example gist",
+ "comments": 0,
+ "user": null,
+ "comments_url": "https://api.github.com/gists/e717ce85dd0d2f1bdcdfc884ea25a344/comments",
+ "owner": {
+ "login": "balloob",
+ "id": 1444314,
+ "node_id": "MDQ6VXNlcjE0NDQzMTQ=",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/1444314?v=4",
+ "gravatar_id": "",
+ "url": "https://api.github.com/users/balloob",
+ "html_url": "https://github.com/balloob",
+ "followers_url": "https://api.github.com/users/balloob/followers",
+ "following_url": "https://api.github.com/users/balloob/following{/other_user}",
+ "gists_url": "https://api.github.com/users/balloob/gists{/gist_id}",
+ "starred_url": "https://api.github.com/users/balloob/starred{/owner}{/repo}",
+ "subscriptions_url": "https://api.github.com/users/balloob/subscriptions",
+ "organizations_url": "https://api.github.com/users/balloob/orgs",
+ "repos_url": "https://api.github.com/users/balloob/repos",
+ "events_url": "https://api.github.com/users/balloob/events{/privacy}",
+ "received_events_url": "https://api.github.com/users/balloob/received_events",
+ "type": "User",
+ "site_admin": false
+ },
+ "forks": [
+
+ ],
+ "history": [
+ {
+ "user": {
+ "login": "balloob",
+ "id": 1444314,
+ "node_id": "MDQ6VXNlcjE0NDQzMTQ=",
+ "avatar_url": "https://avatars3.githubusercontent.com/u/1444314?v=4",
+ "gravatar_id": "",
+ "url": "https://api.github.com/users/balloob",
+ "html_url": "https://github.com/balloob",
+ "followers_url": "https://api.github.com/users/balloob/followers",
+ "following_url": "https://api.github.com/users/balloob/following{/other_user}",
+ "gists_url": "https://api.github.com/users/balloob/gists{/gist_id}",
+ "starred_url": "https://api.github.com/users/balloob/starred{/owner}{/repo}",
+ "subscriptions_url": "https://api.github.com/users/balloob/subscriptions",
+ "organizations_url": "https://api.github.com/users/balloob/orgs",
+ "repos_url": "https://api.github.com/users/balloob/repos",
+ "events_url": "https://api.github.com/users/balloob/events{/privacy}",
+ "received_events_url": "https://api.github.com/users/balloob/received_events",
+ "type": "User",
+ "site_admin": false
+ },
+ "version": "0b1028d04209ad0cc7942d3dcf02f9b036cea21f",
+ "committed_at": "2020-11-25T22:49:50Z",
+ "change_status": {
+ "total": 38,
+ "additions": 38,
+ "deletions": 0
+ },
+ "url": "https://api.github.com/gists/e717ce85dd0d2f1bdcdfc884ea25a344/0b1028d04209ad0cc7942d3dcf02f9b036cea21f"
+ }
+ ],
+ "truncated": false
+}
diff --git a/tests/fixtures/homekit_controller/anker_eufycam.json b/tests/fixtures/homekit_controller/anker_eufycam.json
new file mode 100644
index 00000000000..b3ebfcf7c9f
--- /dev/null
+++ b/tests/fixtures/homekit_controller/anker_eufycam.json
@@ -0,0 +1,2073 @@
+[
+ {
+ "aid": 1,
+ "services": [
+ {
+ "iid": 1,
+ "type": "0000003E-0000-1000-8000-0026BB765291",
+ "primary": false,
+ "hidden": false,
+ "linked": [],
+ "characteristics": [
+ {
+ "iid": 2,
+ "type": "00000014-0000-1000-8000-0026BB765291",
+ "format": "bool",
+ "perms": [
+ "pw"
+ ]
+ },
+ {
+ "iid": 3,
+ "type": "00000020-0000-1000-8000-0026BB765291",
+ "format": "string",
+ "value": "Anker",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 4,
+ "type": "00000021-0000-1000-8000-0026BB765291",
+ "format": "string",
+ "value": "T8010",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 5,
+ "type": "00000023-0000-1000-8000-0026BB765291",
+ "format": "string",
+ "value": "eufy HomeBase2-0AAA",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 6,
+ "type": "00000030-0000-1000-8000-0026BB765291",
+ "format": "string",
+ "value": "A0000A000000000A",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 7,
+ "type": "00000052-0000-1000-8000-0026BB765291",
+ "format": "string",
+ "value": "2.1.6",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 8,
+ "type": "00000053-0000-1000-8000-0026BB765291",
+ "format": "string",
+ "value": "2.0.0",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 9,
+ "type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B",
+ "format": "string",
+ "value": "3.0;17A93g",
+ "perms": [
+ "pr",
+ "hd"
+ ],
+ "ev": false
+ }
+ ],
+ "stype": "accessory-information"
+ },
+ {
+ "iid": 19,
+ "type": "80CF79D6-9D29-4268-83F7-58FA0244B7CE",
+ "primary": false,
+ "hidden": false,
+ "linked": [],
+ "characteristics": [
+ {
+ "iid": 20,
+ "type": "B6704D2D-682B-4CB5-9150-AF94EFD18C22",
+ "format": "string",
+ "perms": [
+ "pw"
+ ],
+ "maxLen": 256
+ },
+ {
+ "iid": 21,
+ "type": "489F0737-E399-41C1-A38A-BC2C152DC88D",
+ "format": "string",
+ "value": "0|0",
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 22,
+ "type": "DAC539C4-2E71-4C5F-97BE-47A11B41DE4A",
+ "format": "string",
+ "value": "0",
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 23,
+ "type": "7BD15050-677E-446B-983F-CA276A96ECDF",
+ "format": "string",
+ "value": "0",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 24,
+ "type": "DBE912DF-223D-4038-8116-D0DFA1B6E3DF",
+ "format": "string",
+ "value": "T8010N2319490CEB",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ }
+ ],
+ "stype": "Unknown Service: 80CF79D6-9D29-4268-83F7-58FA0244B7CE"
+ },
+ {
+ "iid": 16,
+ "type": "000000A2-0000-1000-8000-0026BB765291",
+ "primary": false,
+ "hidden": false,
+ "linked": [],
+ "characteristics": [
+ {
+ "iid": 18,
+ "type": "00000037-0000-1000-8000-0026BB765291",
+ "format": "string",
+ "value": "1.1.0",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ }
+ ],
+ "stype": "service"
+ }
+ ]
+ },
+ {
+ "aid": 2,
+ "services": [
+ {
+ "iid": 1,
+ "type": "0000003E-0000-1000-8000-0026BB765291",
+ "primary": false,
+ "hidden": false,
+ "linked": [],
+ "characteristics": [
+ {
+ "iid": 2,
+ "type": "00000014-0000-1000-8000-0026BB765291",
+ "format": "bool",
+ "perms": [
+ "pw"
+ ]
+ },
+ {
+ "iid": 3,
+ "type": "00000020-0000-1000-8000-0026BB765291",
+ "format": "string",
+ "value": "Anker",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 4,
+ "type": "00000021-0000-1000-8000-0026BB765291",
+ "format": "string",
+ "value": "T8113",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 5,
+ "type": "00000023-0000-1000-8000-0026BB765291",
+ "format": "string",
+ "value": "eufyCam2-000A",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 6,
+ "type": "00000030-0000-1000-8000-0026BB765291",
+ "format": "string",
+ "value": "A0000A000000000B",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 7,
+ "type": "00000052-0000-1000-8000-0026BB765291",
+ "format": "string",
+ "value": "1.6.7",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 8,
+ "type": "00000053-0000-1000-8000-0026BB765291",
+ "format": "string",
+ "value": "1.0.0",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ }
+ ],
+ "stype": "accessory-information"
+ },
+ {
+ "iid": 48,
+ "type": "00000110-0000-1000-8000-0026BB765291",
+ "primary": true,
+ "hidden": false,
+ "linked": [],
+ "characteristics": [
+ {
+ "iid": 50,
+ "type": "000000B0-0000-1000-8000-0026BB765291",
+ "format": "uint8",
+ "value": 1,
+ "perms": [
+ "pr",
+ "pw",
+ "ev",
+ "tw"
+ ],
+ "ev": false,
+ "minValue": 0,
+ "maxValue": 1,
+ "minStep": 1
+ },
+ {
+ "iid": 51,
+ "type": "00000120-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "AQEA",
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 52,
+ "type": "00000114-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "AaoBAQACEQEBAQIBAAAAAgECAwEABAEAAwsBAoAHAgI4BAMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAgAFAgLAAwMBHgAAAwsBAgAEAgIAAwMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAkABAgLwAAMBHg==",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 53,
+ "type": "00000115-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "AQ4BAQICCQEBAQIBAAMBAQIBAA==",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 54,
+ "type": "00000116-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "AgEAAAACAQEAAAIBAg==",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 55,
+ "type": "00000118-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "",
+ "perms": [
+ "pr",
+ "pw"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 56,
+ "type": "00000117-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "",
+ "perms": [
+ "pr",
+ "pw"
+ ],
+ "ev": false
+ }
+ ],
+ "stype": "camera-rtp-stream-management"
+ },
+ {
+ "iid": 64,
+ "type": "00000110-0000-1000-8000-0026BB765291",
+ "primary": false,
+ "hidden": false,
+ "linked": [],
+ "characteristics": [
+ {
+ "iid": 66,
+ "type": "000000B0-0000-1000-8000-0026BB765291",
+ "format": "uint8",
+ "value": 1,
+ "perms": [
+ "pr",
+ "pw",
+ "ev",
+ "tw"
+ ],
+ "ev": false,
+ "minValue": 0,
+ "maxValue": 1,
+ "minStep": 1
+ },
+ {
+ "iid": 67,
+ "type": "00000120-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "AQEA",
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 68,
+ "type": "00000114-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "AYwBAQACEQEBAQIBAAAAAgECAwEABAEAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAgAEAgIAAwMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAkABAgLwAAMBHg==",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 69,
+ "type": "00000115-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "AQ4BAQICCQEBAQIBAAMBAQIBAA==",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 70,
+ "type": "00000116-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "AgEAAAACAQEAAAIBAg==",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 71,
+ "type": "00000118-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "",
+ "perms": [
+ "pr",
+ "pw"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 72,
+ "type": "00000117-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "",
+ "perms": [
+ "pr",
+ "pw"
+ ],
+ "ev": false
+ }
+ ],
+ "stype": "camera-rtp-stream-management"
+ },
+ {
+ "iid": 128,
+ "type": "204",
+ "primary": false,
+ "hidden": false,
+ "linked": [
+ 112,
+ 160
+ ],
+ "characteristics": [
+ {
+ "iid": 130,
+ "type": "000000B0-0000-1000-8000-0026BB765291",
+ "format": "uint8",
+ "value": 0,
+ "perms": [
+ "pr",
+ "pw",
+ "ev",
+ "tw"
+ ],
+ "ev": false,
+ "minValue": 0,
+ "maxValue": 1,
+ "minStep": 1
+ },
+ {
+ "iid": 131,
+ "type": "205",
+ "format": "tlv8",
+ "value": "AQQAAAAAAggBAAAAAAAAAAMLAQEAAgYBBKAPAAA=",
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 132,
+ "type": "206",
+ "format": "tlv8",
+ "value": "AaQBAQACCwEBAQIBAAAAAgECAwsBAoAHAgI4BAMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAoAHAgI4BAMBDwAAAwsBAgAFAgLQAgMBDwAAAwsBAoACAgJoAQMBDwAAAwsBAuABAgIOAQMBDwAAAwsBAkABAgK0AAMBDw==",
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 133,
+ "type": "207",
+ "format": "tlv8",
+ "value": "AQ4BAQECCQEBAQIBAAMBAQ==",
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 134,
+ "type": "209",
+ "format": "tlv8",
+ "value": "AR0BBAAAAAACCAEAAAAAAAAAAwsBAQACBgEEoA8AAAIkAQEAAhIBAQECAQIDBNAHAAAEBKAPAAADCwECgAcCAjgEAwEeAxQBAQECDwEBAQIBAAMBAQQEEAAAAA==",
+ "perms": [
+ "pr",
+ "pw",
+ "ev"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 135,
+ "type": "226",
+ "format": "uint8",
+ "value": 1,
+ "perms": [
+ "pr",
+ "pw",
+ "ev",
+ "tw"
+ ],
+ "ev": false,
+ "minValue": 0,
+ "maxValue": 1,
+ "minStep": 1
+ }
+ ],
+ "stype": "Unknown Service: 204"
+ },
+ {
+ "iid": 112,
+ "type": "00000129-0000-1000-8000-0026BB765291",
+ "primary": false,
+ "hidden": false,
+ "linked": [],
+ "characteristics": [
+ {
+ "iid": 114,
+ "type": "00000130-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "AQMBAQA=",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 115,
+ "type": "00000131-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "",
+ "perms": [
+ "pr",
+ "pw",
+ "wr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 116,
+ "type": "00000037-0000-1000-8000-0026BB765291",
+ "format": "string",
+ "value": "1.0",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ }
+ ],
+ "stype": "data-stream-transport-management"
+ },
+ {
+ "iid": 144,
+ "type": "21A",
+ "primary": false,
+ "hidden": false,
+ "linked": [],
+ "characteristics": [
+ {
+ "iid": 146,
+ "type": "223",
+ "format": "uint8",
+ "value": 1,
+ "perms": [
+ "pr",
+ "pw",
+ "ev"
+ ],
+ "ev": false,
+ "minValue": 0,
+ "maxValue": 1,
+ "minStep": 1
+ },
+ {
+ "iid": 147,
+ "type": "225",
+ "format": "uint8",
+ "value": 1,
+ "perms": [
+ "pr",
+ "pw",
+ "ev"
+ ],
+ "ev": false,
+ "minValue": 0,
+ "maxValue": 1,
+ "minStep": 1
+ },
+ {
+ "iid": 148,
+ "type": "21B",
+ "format": "uint8",
+ "value": 1,
+ "perms": [
+ "pr",
+ "pw",
+ "ev"
+ ],
+ "ev": false,
+ "minValue": 0,
+ "maxValue": 1,
+ "minStep": 1
+ },
+ {
+ "iid": 149,
+ "type": "21C",
+ "format": "uint8",
+ "value": 1,
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": false,
+ "minValue": 0,
+ "maxValue": 1,
+ "minStep": 1
+ },
+ {
+ "iid": 150,
+ "type": "21D",
+ "format": "uint8",
+ "value": 0,
+ "perms": [
+ "pr",
+ "pw",
+ "ev"
+ ],
+ "ev": false,
+ "minValue": 0,
+ "maxValue": 1,
+ "minStep": 1
+ },
+ {
+ "iid": 152,
+ "type": "0000011B-0000-1000-8000-0026BB765291",
+ "format": "bool",
+ "value": 1,
+ "perms": [
+ "pr",
+ "pw",
+ "ev"
+ ],
+ "ev": false
+ }
+ ],
+ "stype": "Unknown Service: 21A"
+ },
+ {
+ "iid": 80,
+ "type": "00000112-0000-1000-8000-0026BB765291",
+ "primary": false,
+ "hidden": false,
+ "linked": [],
+ "characteristics": [
+ {
+ "iid": 82,
+ "type": "00000023-0000-1000-8000-0026BB765291",
+ "format": "string",
+ "value": "Microphone",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 83,
+ "type": "0000011A-0000-1000-8000-0026BB765291",
+ "format": "bool",
+ "value": 0,
+ "perms": [
+ "pr",
+ "pw",
+ "ev"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 84,
+ "type": "00000119-0000-1000-8000-0026BB765291",
+ "format": "uint8",
+ "value": 100,
+ "perms": [
+ "pr",
+ "pw",
+ "ev"
+ ],
+ "ev": false,
+ "unit": "percentage",
+ "minValue": 0,
+ "maxValue": 100,
+ "minStep": 1
+ }
+ ],
+ "stype": "microphone"
+ },
+ {
+ "iid": 160,
+ "type": "00000085-0000-1000-8000-0026BB765291",
+ "primary": false,
+ "hidden": false,
+ "linked": [],
+ "characteristics": [
+ {
+ "iid": 162,
+ "type": "00000022-0000-1000-8000-0026BB765291",
+ "format": "bool",
+ "value": 0,
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": true
+ },
+ {
+ "iid": 163,
+ "type": "00000023-0000-1000-8000-0026BB765291",
+ "format": "string",
+ "value": "Motion Sensor",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 164,
+ "type": "00000075-0000-1000-8000-0026BB765291",
+ "format": "bool",
+ "value": 1,
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": false
+ }
+ ],
+ "stype": "motion"
+ },
+ {
+ "iid": 101,
+ "type": "00000096-0000-1000-8000-0026BB765291",
+ "primary": false,
+ "hidden": false,
+ "linked": [],
+ "characteristics": [
+ {
+ "iid": 102,
+ "type": "00000068-0000-1000-8000-0026BB765291",
+ "format": "uint8",
+ "value": 38,
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": true,
+ "unit": "percentage",
+ "minValue": 0,
+ "maxValue": 100,
+ "minStep": 1
+ },
+ {
+ "iid": 103,
+ "type": "0000008F-0000-1000-8000-0026BB765291",
+ "format": "uint8",
+ "value": 0,
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": true,
+ "minValue": 0,
+ "maxValue": 2,
+ "minStep": 1
+ },
+ {
+ "iid": 104,
+ "type": "00000079-0000-1000-8000-0026BB765291",
+ "format": "uint8",
+ "value": 0,
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": true,
+ "minValue": 0,
+ "maxValue": 1,
+ "minStep": 1
+ }
+ ],
+ "stype": "battery"
+ }
+ ]
+ },
+ {
+ "aid": 3,
+ "services": [
+ {
+ "iid": 1,
+ "type": "0000003E-0000-1000-8000-0026BB765291",
+ "primary": false,
+ "hidden": false,
+ "linked": [],
+ "characteristics": [
+ {
+ "iid": 2,
+ "type": "00000014-0000-1000-8000-0026BB765291",
+ "format": "bool",
+ "perms": [
+ "pw"
+ ]
+ },
+ {
+ "iid": 3,
+ "type": "00000020-0000-1000-8000-0026BB765291",
+ "format": "string",
+ "value": "Anker",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 4,
+ "type": "00000021-0000-1000-8000-0026BB765291",
+ "format": "string",
+ "value": "T8113",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 5,
+ "type": "00000023-0000-1000-8000-0026BB765291",
+ "format": "string",
+ "value": "eufyCam2-000A",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 6,
+ "type": "00000030-0000-1000-8000-0026BB765291",
+ "format": "string",
+ "value": "A0000A000000000C",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 7,
+ "type": "00000052-0000-1000-8000-0026BB765291",
+ "format": "string",
+ "value": "1.6.7",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 8,
+ "type": "00000053-0000-1000-8000-0026BB765291",
+ "format": "string",
+ "value": "1.0.0",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ }
+ ],
+ "stype": "accessory-information"
+ },
+ {
+ "iid": 48,
+ "type": "00000110-0000-1000-8000-0026BB765291",
+ "primary": true,
+ "hidden": false,
+ "linked": [],
+ "characteristics": [
+ {
+ "iid": 50,
+ "type": "000000B0-0000-1000-8000-0026BB765291",
+ "format": "uint8",
+ "value": 1,
+ "perms": [
+ "pr",
+ "pw",
+ "ev",
+ "tw"
+ ],
+ "ev": false,
+ "minValue": 0,
+ "maxValue": 1,
+ "minStep": 1
+ },
+ {
+ "iid": 51,
+ "type": "00000120-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "AQEA",
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 52,
+ "type": "00000114-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "AaoBAQACEQEBAQIBAAAAAgECAwEABAEAAwsBAoAHAgI4BAMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAgAFAgLAAwMBHgAAAwsBAgAEAgIAAwMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAkABAgLwAAMBHg==",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 53,
+ "type": "00000115-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "AQ4BAQICCQEBAQIBAAMBAQIBAA==",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 54,
+ "type": "00000116-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "AgEAAAACAQEAAAIBAg==",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 55,
+ "type": "00000118-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "",
+ "perms": [
+ "pr",
+ "pw"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 56,
+ "type": "00000117-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "",
+ "perms": [
+ "pr",
+ "pw"
+ ],
+ "ev": false
+ }
+ ],
+ "stype": "camera-rtp-stream-management"
+ },
+ {
+ "iid": 64,
+ "type": "00000110-0000-1000-8000-0026BB765291",
+ "primary": false,
+ "hidden": false,
+ "linked": [],
+ "characteristics": [
+ {
+ "iid": 66,
+ "type": "000000B0-0000-1000-8000-0026BB765291",
+ "format": "uint8",
+ "value": 1,
+ "perms": [
+ "pr",
+ "pw",
+ "ev",
+ "tw"
+ ],
+ "ev": false,
+ "minValue": 0,
+ "maxValue": 1,
+ "minStep": 1
+ },
+ {
+ "iid": 67,
+ "type": "00000120-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "AQEA",
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 68,
+ "type": "00000114-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "AYwBAQACEQEBAQIBAAAAAgECAwEABAEAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAgAEAgIAAwMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAkABAgLwAAMBHg==",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 69,
+ "type": "00000115-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "AQ4BAQICCQEBAQIBAAMBAQIBAA==",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 70,
+ "type": "00000116-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "AgEAAAACAQEAAAIBAg==",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 71,
+ "type": "00000118-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "",
+ "perms": [
+ "pr",
+ "pw"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 72,
+ "type": "00000117-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "",
+ "perms": [
+ "pr",
+ "pw"
+ ],
+ "ev": false
+ }
+ ],
+ "stype": "camera-rtp-stream-management"
+ },
+ {
+ "iid": 128,
+ "type": "204",
+ "primary": false,
+ "hidden": false,
+ "linked": [
+ 112,
+ 160
+ ],
+ "characteristics": [
+ {
+ "iid": 130,
+ "type": "000000B0-0000-1000-8000-0026BB765291",
+ "format": "uint8",
+ "value": 0,
+ "perms": [
+ "pr",
+ "pw",
+ "ev",
+ "tw"
+ ],
+ "ev": false,
+ "minValue": 0,
+ "maxValue": 1,
+ "minStep": 1
+ },
+ {
+ "iid": 131,
+ "type": "205",
+ "format": "tlv8",
+ "value": "AQQAAAAAAggBAAAAAAAAAAMLAQEAAgYBBKAPAAA=",
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 132,
+ "type": "206",
+ "format": "tlv8",
+ "value": "AaQBAQACCwEBAQIBAAAAAgECAwsBAoAHAgI4BAMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAoAHAgI4BAMBDwAAAwsBAgAFAgLQAgMBDwAAAwsBAoACAgJoAQMBDwAAAwsBAuABAgIOAQMBDwAAAwsBAkABAgK0AAMBDw==",
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 133,
+ "type": "207",
+ "format": "tlv8",
+ "value": "AQ4BAQECCQEBAQIBAAMBAQ==",
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 134,
+ "type": "209",
+ "format": "tlv8",
+ "value": "",
+ "perms": [
+ "pr",
+ "pw",
+ "ev"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 135,
+ "type": "226",
+ "format": "uint8",
+ "value": 1,
+ "perms": [
+ "pr",
+ "pw",
+ "ev",
+ "tw"
+ ],
+ "ev": false,
+ "minValue": 0,
+ "maxValue": 1,
+ "minStep": 1
+ }
+ ],
+ "stype": "Unknown Service: 204"
+ },
+ {
+ "iid": 112,
+ "type": "00000129-0000-1000-8000-0026BB765291",
+ "primary": false,
+ "hidden": false,
+ "linked": [],
+ "characteristics": [
+ {
+ "iid": 114,
+ "type": "00000130-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "AQMBAQA=",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 115,
+ "type": "00000131-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "",
+ "perms": [
+ "pr",
+ "pw",
+ "wr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 116,
+ "type": "00000037-0000-1000-8000-0026BB765291",
+ "format": "string",
+ "value": "1.0",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ }
+ ],
+ "stype": "data-stream-transport-management"
+ },
+ {
+ "iid": 144,
+ "type": "21A",
+ "primary": false,
+ "hidden": false,
+ "linked": [],
+ "characteristics": [
+ {
+ "iid": 146,
+ "type": "223",
+ "format": "uint8",
+ "value": 1,
+ "perms": [
+ "pr",
+ "pw",
+ "ev"
+ ],
+ "ev": false,
+ "minValue": 0,
+ "maxValue": 1,
+ "minStep": 1
+ },
+ {
+ "iid": 147,
+ "type": "225",
+ "format": "uint8",
+ "value": 1,
+ "perms": [
+ "pr",
+ "pw",
+ "ev"
+ ],
+ "ev": false,
+ "minValue": 0,
+ "maxValue": 1,
+ "minStep": 1
+ },
+ {
+ "iid": 148,
+ "type": "21B",
+ "format": "uint8",
+ "value": 1,
+ "perms": [
+ "pr",
+ "pw",
+ "ev"
+ ],
+ "ev": false,
+ "minValue": 0,
+ "maxValue": 1,
+ "minStep": 1
+ },
+ {
+ "iid": 149,
+ "type": "21C",
+ "format": "uint8",
+ "value": 1,
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": false,
+ "minValue": 0,
+ "maxValue": 1,
+ "minStep": 1
+ },
+ {
+ "iid": 150,
+ "type": "21D",
+ "format": "uint8",
+ "value": 0,
+ "perms": [
+ "pr",
+ "pw",
+ "ev"
+ ],
+ "ev": false,
+ "minValue": 0,
+ "maxValue": 1,
+ "minStep": 1
+ },
+ {
+ "iid": 152,
+ "type": "0000011B-0000-1000-8000-0026BB765291",
+ "format": "bool",
+ "value": null,
+ "perms": [
+ "pr",
+ "pw",
+ "ev"
+ ],
+ "ev": false
+ }
+ ],
+ "stype": "Unknown Service: 21A"
+ },
+ {
+ "iid": 80,
+ "type": "00000112-0000-1000-8000-0026BB765291",
+ "primary": false,
+ "hidden": false,
+ "linked": [],
+ "characteristics": [
+ {
+ "iid": 82,
+ "type": "00000023-0000-1000-8000-0026BB765291",
+ "format": "string",
+ "value": "Microphone",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 83,
+ "type": "0000011A-0000-1000-8000-0026BB765291",
+ "format": "bool",
+ "value": null,
+ "perms": [
+ "pr",
+ "pw",
+ "ev"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 84,
+ "type": "00000119-0000-1000-8000-0026BB765291",
+ "format": "uint8",
+ "value": null,
+ "perms": [
+ "pr",
+ "pw",
+ "ev"
+ ],
+ "ev": false,
+ "unit": "percentage",
+ "minValue": 0,
+ "maxValue": 100,
+ "minStep": 1
+ }
+ ],
+ "stype": "microphone"
+ },
+ {
+ "iid": 160,
+ "type": "00000085-0000-1000-8000-0026BB765291",
+ "primary": false,
+ "hidden": false,
+ "linked": [],
+ "characteristics": [
+ {
+ "iid": 162,
+ "type": "00000022-0000-1000-8000-0026BB765291",
+ "format": "bool",
+ "value": null,
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": true
+ },
+ {
+ "iid": 163,
+ "type": "00000023-0000-1000-8000-0026BB765291",
+ "format": "string",
+ "value": "Motion Sensor",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 164,
+ "type": "00000075-0000-1000-8000-0026BB765291",
+ "format": "bool",
+ "value": null,
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": false
+ }
+ ],
+ "stype": "motion"
+ },
+ {
+ "iid": 101,
+ "type": "00000096-0000-1000-8000-0026BB765291",
+ "primary": false,
+ "hidden": false,
+ "linked": [],
+ "characteristics": [
+ {
+ "iid": 102,
+ "type": "00000068-0000-1000-8000-0026BB765291",
+ "format": "uint8",
+ "value": null,
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": true,
+ "unit": "percentage",
+ "minValue": 0,
+ "maxValue": 100,
+ "minStep": 1
+ },
+ {
+ "iid": 103,
+ "type": "0000008F-0000-1000-8000-0026BB765291",
+ "format": "uint8",
+ "value": null,
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": true,
+ "minValue": 0,
+ "maxValue": 2,
+ "minStep": 1
+ },
+ {
+ "iid": 104,
+ "type": "00000079-0000-1000-8000-0026BB765291",
+ "format": "uint8",
+ "value": null,
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": true,
+ "minValue": 0,
+ "maxValue": 1,
+ "minStep": 1
+ }
+ ],
+ "stype": "battery"
+ }
+ ]
+ },
+ {
+ "aid": 4,
+ "services": [
+ {
+ "iid": 1,
+ "type": "0000003E-0000-1000-8000-0026BB765291",
+ "primary": false,
+ "hidden": false,
+ "linked": [],
+ "characteristics": [
+ {
+ "iid": 2,
+ "type": "00000014-0000-1000-8000-0026BB765291",
+ "format": "bool",
+ "perms": [
+ "pw"
+ ]
+ },
+ {
+ "iid": 3,
+ "type": "00000020-0000-1000-8000-0026BB765291",
+ "format": "string",
+ "value": "Anker",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 4,
+ "type": "00000021-0000-1000-8000-0026BB765291",
+ "format": "string",
+ "value": "T8113",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 5,
+ "type": "00000023-0000-1000-8000-0026BB765291",
+ "format": "string",
+ "value": "eufyCam2-0000",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 6,
+ "type": "00000030-0000-1000-8000-0026BB765291",
+ "format": "string",
+ "value": "A0000A000000000D",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 7,
+ "type": "00000052-0000-1000-8000-0026BB765291",
+ "format": "string",
+ "value": "1.6.7",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 8,
+ "type": "00000053-0000-1000-8000-0026BB765291",
+ "format": "string",
+ "value": "1.0.0",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ }
+ ],
+ "stype": "accessory-information"
+ },
+ {
+ "iid": 48,
+ "type": "00000110-0000-1000-8000-0026BB765291",
+ "primary": true,
+ "hidden": false,
+ "linked": [],
+ "characteristics": [
+ {
+ "iid": 50,
+ "type": "000000B0-0000-1000-8000-0026BB765291",
+ "format": "uint8",
+ "value": 1,
+ "perms": [
+ "pr",
+ "pw",
+ "ev",
+ "tw"
+ ],
+ "ev": false,
+ "minValue": 0,
+ "maxValue": 1,
+ "minStep": 1
+ },
+ {
+ "iid": 51,
+ "type": "00000120-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "AQEA",
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 52,
+ "type": "00000114-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "AaoBAQACEQEBAQIBAAAAAgECAwEABAEAAwsBAoAHAgI4BAMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAgAFAgLAAwMBHgAAAwsBAgAEAgIAAwMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAkABAgLwAAMBHg==",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 53,
+ "type": "00000115-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "AQ4BAQICCQEBAQIBAAMBAQIBAA==",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 54,
+ "type": "00000116-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "AgEAAAACAQEAAAIBAg==",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 55,
+ "type": "00000118-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "",
+ "perms": [
+ "pr",
+ "pw"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 56,
+ "type": "00000117-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "",
+ "perms": [
+ "pr",
+ "pw"
+ ],
+ "ev": false
+ }
+ ],
+ "stype": "camera-rtp-stream-management"
+ },
+ {
+ "iid": 64,
+ "type": "00000110-0000-1000-8000-0026BB765291",
+ "primary": false,
+ "hidden": false,
+ "linked": [],
+ "characteristics": [
+ {
+ "iid": 66,
+ "type": "000000B0-0000-1000-8000-0026BB765291",
+ "format": "uint8",
+ "value": 1,
+ "perms": [
+ "pr",
+ "pw",
+ "ev",
+ "tw"
+ ],
+ "ev": false,
+ "minValue": 0,
+ "maxValue": 1,
+ "minStep": 1
+ },
+ {
+ "iid": 67,
+ "type": "00000120-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "AQEA",
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 68,
+ "type": "00000114-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "AYwBAQACEQEBAQIBAAAAAgECAwEABAEAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAgAEAgIAAwMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAkABAgLwAAMBHg==",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 69,
+ "type": "00000115-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "AQ4BAQICCQEBAQIBAAMBAQIBAA==",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 70,
+ "type": "00000116-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "AgEAAAACAQEAAAIBAg==",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 71,
+ "type": "00000118-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "",
+ "perms": [
+ "pr",
+ "pw"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 72,
+ "type": "00000117-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "",
+ "perms": [
+ "pr",
+ "pw"
+ ],
+ "ev": false
+ }
+ ],
+ "stype": "camera-rtp-stream-management"
+ },
+ {
+ "iid": 128,
+ "type": "204",
+ "primary": false,
+ "hidden": false,
+ "linked": [
+ 112,
+ 160
+ ],
+ "characteristics": [
+ {
+ "iid": 130,
+ "type": "000000B0-0000-1000-8000-0026BB765291",
+ "format": "uint8",
+ "value": 1,
+ "perms": [
+ "pr",
+ "pw",
+ "ev",
+ "tw"
+ ],
+ "ev": false,
+ "minValue": 0,
+ "maxValue": 1,
+ "minStep": 1
+ },
+ {
+ "iid": 131,
+ "type": "205",
+ "format": "tlv8",
+ "value": "AQQAAAAAAggBAAAAAAAAAAMLAQEAAgYBBKAPAAA=",
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 132,
+ "type": "206",
+ "format": "tlv8",
+ "value": "AaQBAQACCwEBAQIBAAAAAgECAwsBAoAHAgI4BAMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAoAHAgI4BAMBDwAAAwsBAgAFAgLQAgMBDwAAAwsBAoACAgJoAQMBDwAAAwsBAuABAgIOAQMBDwAAAwsBAkABAgK0AAMBDw==",
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 133,
+ "type": "207",
+ "format": "tlv8",
+ "value": "AQ4BAQECCQEBAQIBAAMBAQ==",
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 134,
+ "type": "209",
+ "format": "tlv8",
+ "value": "AR0BBAAAAAACCAEAAAAAAAAAAwsBAQACBgEEoA8AAAIkAQEAAhIBAQECAQIDBNAHAAAEBKAPAAADCwECgAcCAjgEAwEeAxQBAQECDwEBAQIBAAMBAQQEEAAAAA==",
+ "perms": [
+ "pr",
+ "pw",
+ "ev"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 135,
+ "type": "226",
+ "format": "uint8",
+ "value": 1,
+ "perms": [
+ "pr",
+ "pw",
+ "ev",
+ "tw"
+ ],
+ "ev": false,
+ "minValue": 0,
+ "maxValue": 1,
+ "minStep": 1
+ }
+ ],
+ "stype": "Unknown Service: 204"
+ },
+ {
+ "iid": 112,
+ "type": "00000129-0000-1000-8000-0026BB765291",
+ "primary": false,
+ "hidden": false,
+ "linked": [],
+ "characteristics": [
+ {
+ "iid": 114,
+ "type": "00000130-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "AQMBAQA=",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 115,
+ "type": "00000131-0000-1000-8000-0026BB765291",
+ "format": "tlv8",
+ "value": "",
+ "perms": [
+ "pr",
+ "pw",
+ "wr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 116,
+ "type": "00000037-0000-1000-8000-0026BB765291",
+ "format": "string",
+ "value": "1.0",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ }
+ ],
+ "stype": "data-stream-transport-management"
+ },
+ {
+ "iid": 144,
+ "type": "21A",
+ "primary": false,
+ "hidden": false,
+ "linked": [],
+ "characteristics": [
+ {
+ "iid": 146,
+ "type": "223",
+ "format": "uint8",
+ "value": 1,
+ "perms": [
+ "pr",
+ "pw",
+ "ev"
+ ],
+ "ev": false,
+ "minValue": 0,
+ "maxValue": 1,
+ "minStep": 1
+ },
+ {
+ "iid": 147,
+ "type": "225",
+ "format": "uint8",
+ "value": 1,
+ "perms": [
+ "pr",
+ "pw",
+ "ev"
+ ],
+ "ev": false,
+ "minValue": 0,
+ "maxValue": 1,
+ "minStep": 1
+ },
+ {
+ "iid": 148,
+ "type": "21B",
+ "format": "uint8",
+ "value": 1,
+ "perms": [
+ "pr",
+ "pw",
+ "ev"
+ ],
+ "ev": false,
+ "minValue": 0,
+ "maxValue": 1,
+ "minStep": 1
+ },
+ {
+ "iid": 149,
+ "type": "21C",
+ "format": "uint8",
+ "value": 1,
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": false,
+ "minValue": 0,
+ "maxValue": 1,
+ "minStep": 1
+ },
+ {
+ "iid": 150,
+ "type": "21D",
+ "format": "uint8",
+ "value": 0,
+ "perms": [
+ "pr",
+ "pw",
+ "ev"
+ ],
+ "ev": false,
+ "minValue": 0,
+ "maxValue": 1,
+ "minStep": 1
+ },
+ {
+ "iid": 152,
+ "type": "0000011B-0000-1000-8000-0026BB765291",
+ "format": "bool",
+ "value": 1,
+ "perms": [
+ "pr",
+ "pw",
+ "ev"
+ ],
+ "ev": false
+ }
+ ],
+ "stype": "Unknown Service: 21A"
+ },
+ {
+ "iid": 80,
+ "type": "00000112-0000-1000-8000-0026BB765291",
+ "primary": false,
+ "hidden": false,
+ "linked": [],
+ "characteristics": [
+ {
+ "iid": 82,
+ "type": "00000023-0000-1000-8000-0026BB765291",
+ "format": "string",
+ "value": "Microphone",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 83,
+ "type": "0000011A-0000-1000-8000-0026BB765291",
+ "format": "bool",
+ "value": 0,
+ "perms": [
+ "pr",
+ "pw",
+ "ev"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 84,
+ "type": "00000119-0000-1000-8000-0026BB765291",
+ "format": "uint8",
+ "value": 50,
+ "perms": [
+ "pr",
+ "pw",
+ "ev"
+ ],
+ "ev": false,
+ "unit": "percentage",
+ "minValue": 0,
+ "maxValue": 100,
+ "minStep": 1
+ }
+ ],
+ "stype": "microphone"
+ },
+ {
+ "iid": 160,
+ "type": "00000085-0000-1000-8000-0026BB765291",
+ "primary": false,
+ "hidden": false,
+ "linked": [],
+ "characteristics": [
+ {
+ "iid": 162,
+ "type": "00000022-0000-1000-8000-0026BB765291",
+ "format": "bool",
+ "value": 0,
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": true
+ },
+ {
+ "iid": 163,
+ "type": "00000023-0000-1000-8000-0026BB765291",
+ "format": "string",
+ "value": "Motion Sensor",
+ "perms": [
+ "pr"
+ ],
+ "ev": false
+ },
+ {
+ "iid": 164,
+ "type": "00000075-0000-1000-8000-0026BB765291",
+ "format": "bool",
+ "value": 1,
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": false
+ }
+ ],
+ "stype": "motion"
+ },
+ {
+ "iid": 101,
+ "type": "00000096-0000-1000-8000-0026BB765291",
+ "primary": false,
+ "hidden": false,
+ "linked": [],
+ "characteristics": [
+ {
+ "iid": 102,
+ "type": "00000068-0000-1000-8000-0026BB765291",
+ "format": "uint8",
+ "value": 17,
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": true,
+ "unit": "percentage",
+ "minValue": 0,
+ "maxValue": 100,
+ "minStep": 1
+ },
+ {
+ "iid": 103,
+ "type": "0000008F-0000-1000-8000-0026BB765291",
+ "format": "uint8",
+ "value": 0,
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": true,
+ "minValue": 0,
+ "maxValue": 2,
+ "minStep": 1
+ },
+ {
+ "iid": 104,
+ "type": "00000079-0000-1000-8000-0026BB765291",
+ "format": "uint8",
+ "value": 0,
+ "perms": [
+ "pr",
+ "ev"
+ ],
+ "ev": true,
+ "minValue": 0,
+ "maxValue": 1,
+ "minStep": 1
+ }
+ ],
+ "stype": "battery"
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/tests/fixtures/ozw/light_network_dump.csv b/tests/fixtures/ozw/light_network_dump.csv
index 2f0cc7019dc..e9c0d8fb74b 100644
--- a/tests/fixtures/ozw/light_network_dump.csv
+++ b/tests/fixtures/ozw/light_network_dump.csv
@@ -225,3 +225,96 @@ OpenZWave/1/node/2/instance/1/commandclass/134/,{ "Instance": 1, "CommandC
OpenZWave/1/node/2/instance/1/commandclass/134/value/48332823/,{ "Label": "Library Version", "Value": "6", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 2, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 48332823, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617}
OpenZWave/1/node/2/instance/1/commandclass/134/value/281475025043479/,{ "Label": "Protocol Version", "Value": "3.67", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 2, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475025043479, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617}
OpenZWave/1/node/2/instance/1/commandclass/134/value/562950001754135/,{ "Label": "Application Version", "Value": "3.37", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 2, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950001754135, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1594407617}
+OpenZWave/1/node/12/,{ "NodeID": 12, "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/0086:0063:0103", "ZWAProductURL": "", "ProductPic": "images/aeotec/zw099.png", "Description": "Aeotec Smart Dimmer 6 is a low-cost Z-Wave Dimmer plug-in module specifically used to enable Z-Wave command and control (on/off/dim) of any plug-in tool. It can report immediate wattage consumption or kWh energy usage over a period of time. In the event of power failure, non-volatile memory retains all programmed information relating to the unit’s operating status. Its surface has a Smart RGB LED, which can be used for indicating the output load status or strength of the wireless signal. You can configure its indication colour according to your favour. The Smart Dimmer 6 is also a security Z-Wave device and supports Over The Air (OTA) feature for the products firmware upgrade.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2246/Aeon Labs Smart Dimmer 6 manual.pdf", "ProductPageURL": "", "InclusionHelp": "Turn the primary controller of Z-Wave network into inclusion mode, short press the product’s Action button that you can find on the product's housing.", "ExclusionHelp": "Turn the primary controller of Z-Wave network into exclusion mode, short press the product’s Action button that you can find on the product's housing.", "ResetHelp": "Press and hold the Action button that you can find on the product's housing for 20 seconds and then release. This procedure should only be used when the primary controller is inoperable.", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "Smart Dimmer 6", "ProductPicBase64": "iVBORw0KGgoAAAANSUhEUgAAAMgAAACiCAIAAACLVRX1AAAACXBIWXMAAA7EAAAOxAGVKw4bAAAgAElEQVR4nO1dWW8cx/Hv2fviklzepCRSsnzJVnzKARLkwfn7IQZywDmA5AskyFM+Q14C5BvkEwRIkDiJkYcYCiAHsOU4Vqw4tuzIpiRSssSbS3K55+zO/B/KbDWrqmt6l4cYh/VAzvZUV1d3V/2qpmemxwuCQO0TeZ4XhuF+SdsvOmStbM25q/HAh3FfFEhoQfqvUoo90K3KOpm19LEgWRMt0e2a5UgCZWArUk62OUoyGxofk5OtZRsKZQwdYutKK7ncNuaoXCgUmkCnvENArMN0wd7aOgQND7qJB45ziD7XRsYV21nlADOCDykL0lBAYjmVHbRsWgkq2RxUUFUglyZkrWwYo/UBcteK7UW3AURmMJvYT8TqmfbF20wHOFAFZGbT0E2vcJGP2JCoo4ZJMvGIBeQSkmWXomxyYkSJbVGRsXaBh8isxRTLnnKXJjfhyEwNC52SVbJNU+T0RZ6KbEIBYj1AV+jKF13YbE4vM+/jCDgqILR4lJHJXbf7huWOWOyBI5uQOrDUm0vRLARFpb0kTO4er3ZjtnmWzVaVBc/cI4lNgiDNRfkeRB2JHOuB0yGARA9NHGXoiqSIdSygvSdGtDDSC+XybnnkJlRUCtWVerY+dguT3VaRpXU7vHuZEXU0EauHvMTGY8ui0CWb2p28d9Xi3hVwl3Y4tC+N7hJhc1MhMXK0626v0Uw2FZV5CFemSAebKFkBl0uwwxR1cDCzF1HMVWFX7R1xOrJ5yX8v9vRGOMdSDqvhkUK7wjZ9lm3apVFUNzJl7Ir25QIW0d61ctchcuiEWMRW7yLHOoJeTlU6UCX3KBxlbFQUWvLoTSvk/zKzTZMDIqb1UFx5VxwAmORu7D1fFVJRPfAITbgw95xKUmbbICMJB4Fn3SrWVS7I5Fg9m3a3FffiQ/vuf5Ews3exe+HpVqsD7YIjHO4a0i9Y8n5MR4RiKiqICGf3F6s9zwOBNrFmeu6uj1OmuYeOIK0ie7HH5v5b6PNQCD/kdSy2frcXgHu/wESZn8zjqNXhUFcLaZGrdPu1XhWpQG9NHLnrwWP6YlAC/vVs6T2vz+5lScyFZ1/WymnFMAxbrVatVvN9v9PpgFvGYrFkMplMJlOpVDKZjMfj3WrbM9shCOlN/jFiRVAQBPV6fXt7u9lsNpvNVqsVhmEicd8hTQqCIAxDbWfa2lKpVDqdTqVS/wvZFZCUY2mSsxmXdayel2cOIWlAPEEQ1Gq1arXaarUajYbv+57nmZZkMrMHYGTKMLsgCADeEokEoBoYXDqdTqfTmUwG5LsviR1E3/dexaT/dcQKw7DdbtdqtXq9DpjUbrdjsVgikaB2ozhjci9EBgfwFgSBbjFlEBhcJpOJx+P/jTgnrbxr2serQoH2PSGw6eb7frVabTQarVar2WwGQRCLxSAx6s1cuq1CCynCaYLAqq1NG1w6nY7FYgd35btHUZ8jvOMCkuOp3hTad780Q3yz2dzY2KjX661WKwiCZDIZi8WUUvF4nM21WSA/CHQ3/VmvXJtWDqbW6XQ6nQ6Ymu/74B7xeBx6EYvF0um0XkLbL8X2VN1lsHrIsbpCOMfWu5IA/Wq1Wmtra+VyWSmVyWRgGlgIUd3ADMvgzikc65/xeDyVSmWz2Ww2m8vl0um0dgDzckHXarVa8XgczMs+Kpj2d6ZM+sLmWI1GY3l5eWNjI5vNZjIZRYIOsLE/hQMXTtuxzBCLxWRLMkELWZXnebFYLBaLhWGYzWbBfx4sdfFe4V7SKQHzZGku8hFPp9NZW1tbXFzMZDJ9fX2Km0VbiqN2p9gCj+2UKcHkoWqAJaXT6Vwul81mwZL0KIU715JgTKxMevsBsq58Pt/VMHYFTo7MXyjECsOw0WisrKysra2NjIzoNQKWUz4Q/vZ2CoQnk8l0Og2whK74AIo0JoExKe7GlLIbFthrLBYrFAp7Gso9U4RhmebZG5zsnRxBKwiCjY2Nzc3NjY2NiYmJyHBg63i3Nmf7qQ/i8ThEt1wul0gkzPE0MUlbkiYW45Fhsfp7ngcXj/IIoCqOzI5kfR6LLRTOCvzyKcXNMSsfDlBuAVNYrVaXlpY8zyuXy1NTU91exiJitbVFNH1AzS6dThcKBTPp0ZakA5zc5UjrF6hYLLqbi+P0yVaxq5YeiN5yLHfaizSWGTRvNBpLS0urq6uFQmF7e3tsbIxGwMiGur2SspWYIa+vry+dTsPPTqfTbrfb7bYZ4IQIYAKSyQbp48rKyubmJtwbiMVi/f39J06cmJqaMkE6DMNUKpXL5dz7tb8UjVjobKRp04qR4KcIDinD1qlkIN/3V1ZWlpaWqtVqLpdLJpMQcZAc2pZc0i2DSaBkLpfT6XO73YaVM5RsaeHyyEB8X1hYWFpaWllZWV9fh/mCijqGdjqdgYGBl156aXp62hTY399PW0GZmdlNVpmuoPQ+j4lYtpGixyxDV+RYkXXrIAjW1tYWFha2trba7XYikRgbG6tUKqOjo5/3KsqA9m5z7NlYLKaBKggCuEGElI+UX6lUFhYWlpeXAYnb7bZ5qWhSp9PR5a1Wy/f9V1555Utf+pJuCy42hV4cHCWo8SJiUwrTZlEhDckCdJkhmA3Hajd0KaU2Njbu3btXLpdhAdrzvIGBgXK5PDo6Co9ZUwdVZEYFBpTMsdNvw/hisZhKpZRSvu83m005DujCZrMJmLS8vLy8vFyv13VQC3du7wA46TvZkLp5nuf7frlchrZardavf/3r4eHhiYkJqO77Pujj2IWeiUE1YbcZTXvMrvZFglIqCIJbt24tLi7CsysgMJVKDQwMNBqNUqmkopBJhqVuUQ3VLRaL2WxWKdVqtVqtlj5lOgb87XQ6y8vLi4uLYEmbm5uw7qChyMzxwzBMp9NjY2OTk5OTk5Ojo6OQlZu+evfu3UuXLl27dq1er58+ffpnP/sZ6BYEwcDAwIGGFBvhHCsyv6EAY6uIys26yo4QaveEmaI+/vjjpaUl1OFSqbS9vT0+Pr4Xs+iWnzLn83lIaHzfbzQailAQBHfu3Jmfn19YWFhdXYU8Se0EMg1IcJBIJEql0uTk5NTU1MTExODgYDweZ0OH1icIgj//+c+vv/56PB7/6U9/+vjjjwNbPp/Xz+TIOG2LKghl6RSjWnB8f1mFWol5yvaTHX1hkugB5WELb9y4sbi4qOcDephIJPRdZDbIohJToHtwtDFoSqVSfX19nue12+1ms4nOrqysvP/++7Ozs77vo3UHeGam0+kA4E1MTAAsjYyM0NxI1tzzvG9+85uffPLJp59+evnyZTAspVS73U4mk6zaqARNjTmALpOFRCXQBFCztSEW6iQ7GcowcFtQZ8EPcW5sbNy5c4c2kc1mNzY2hoeHhWwGEYvwNptz7GZ/f38sFguCoNFomKcqlcobb7xx69YtffPb932wpCAIstnsxMTE1NTU5OTk2NgYXEjSzFXoi6kS/P3BD37w85///Pr16/V6HZIw/fw0W9Fmo2bgRg056pbwOJtljxEkCpzU2G1IRnmozCAIrl+/DiFDF0ITqVSqVqvBpKLqrKmZdQUwR62gEqRePp9PJpNhGMKjXfrUxx9/fPHiRbieaLVacAU3ODh48uTJEydOTExMDAwM0NsDjkZv6+D09PSZM2eWl5dv3rz5xBNPQLqGsEBoToAoQUOWGV8V0jiKEIWOLGttQtxh1bKBYhiGCwsLtVqNykkkEoDz+p1bl4BrI4pSbI8QxWKxfD7fbrfj8TisLIDm//jHPy5duhSPxyHMTU1NPfHEE2fOnOnr6xNSpUjdXE49//zzr7322tzc3Llz55RS6IXkyHyADVbK8DGbxyLJCXYchfyJFRpZHjnBqEWTf35+nsXkVCq1sbExOjoKk4cqCtmDzGaL16za6XS60+kkEgm4DAS2jz766K9//Suk5I888shXv/rV8fFx4LddoNDJo/hqswak0pNPPvm73/3u3r17cApCoc1b2J+OQycPEYNY1GzpiNiiiT4W0ilagjzDPLuxsVGtVukzuJ7nAWIppVAcdB8a2anYKyazd57n6ccTINIppcrl8p/+9KdGo1EoFF5++eUnn3ySDi8dItm2TLbIksnJyVQqtbKyAqdisVin0zEfyBF6Rw/YJtiZQgIZxDLNlg1ziIdSDxHa5hALCwv6eTdh+oXkmq2Ijt0PzJ9g7rFYrN1uazt47bXX1tfXS6XSD3/4w1OnTtnGQXEjLKACNXeBc3x8HN5Xy2QykGbp+6fdIjrbBFuOquy6KqSJji4369hATvMIsGRThWVQSq2traHeakdsNBrpdBpVERI+xU0MHZHIQv0TXsGIxWLwiphSanZ29sqVK319fa+88opsVbrLtpjI9kLGMN270dHRubm57e1tfXNJQCCKTzIgsSV0ihNaBHJ91qooG1vCdj4SxilDq9Wq1+vJZJJGh3g8vrm5OTY2RuOgskyMrVwwO5tTwgE8Uqd2EmTP8/7yl78EQfDlL3/50UcflRNzeXjRTxuzqZh5qlQq3bp1q1qtDg0NRVaPnHGXEjrFzHbcbMRlfR31TTlfycsxXpeXy2W0+KltCwKQrFVX5sWWC14LbwKaKx3Ly8tXrlx56KGHvv71r7ukRzSdoGhhk2PL/+BvsVhUStXrdW/nNpEyJkiJM24bUhaiKJsmV8SSpbAV6Sm2RPDdSqVC03Ygc8h6oz2aned5EAf1CuTf//73VCr13HPPoTUFlmwTw46M4JnI5eAnPOmP7izZgozjREeGF0TM5raKs25aE0EURSy2hB7QcdHl1WoVZSG0t5GuI1PIJS7Kwbx0gqUfa7l8+fLQ0NCzzz4rVDQJ9d3WLxbYqBCTDR5Kazabios8ZossNNK5sIUUVhQQRiyqeqQJy1DneMAea59DuO15HlxCC+rtCwlmp18WBQW2trYWFhaee+65sbExF5UoAPSWRLOhLZPJeJ6nF2xNTiE+2H5GVmH7y2/HzcIMIvdszGbparfXIga143NqtzsChrVaLbiRQmUiIfI09wZ4Og7qujdu3Egmk2fPnmUFsmPCimXP2nCdVoG/8NoqGJYwjxSWhGFkWzd50FkGsWTTNpthDyi/izewDL7v68050Cg0Gg1YpDH7LDRqm4/ezM7zPHjfRp+9fv16X1/fqVOn5B6xJTZPRq27OwA8zqDXbF106GHuaKF5Fu9BSp3GluLQcpoNsKLMtMnbIcQGDoduApqczWYTPRsZhiEdBZOEUzZOgcFELM/z5ubm+vr69KObFHLYEtR386cwjJGEjB6NoSJTIDRBJ4vtHaODigqokYiFSgSrpwxC06bD0TGCHItti+2niXY2mULXEKe3czNH92hpaWliYgKe9WMluOCNCVSOU8iKBd3Q62Xm4EdOgXzWBcbuIxYbrW0/FWe/7ohlnkK+qwv1cjYSAsSui0JFPXbmQGiir6ubPLSELTcNC8B1e3sbnvOknbIhjZZsyqd2T7vD9tokWKMJyco+qmUe0ElhC+mMoL/6wDXHEpBMAKRIBnawTMRCCGTaDa0ekvSTnTNaxeSPBLxw5zV2fbZSqSil4OlyZAEm/CAypxYZge4FnWMXLATbYk/RWsKkuIAZOx1hGOKnoc0hQMgsO5ZZHooLIcLMmZz6YRjaOlVDoK6MRpagf8K0af6NjY1kMgnr3ebIUL+3kWB5NrVNNnaoFbFdVAXNNaprHlMyq+u6JnNC6LyAlpSHFlLYFBSlDNpfNQZ4DldJVL7g64o4peMUou0bNzc3k8kkLEtSIaxYAUtkJeWzyEkEwGZ/slPGki0makqY5maGEgQzphQUcdhRo8inyBzLqpuXhBppZcgReGxnZR1sM4qe5KlUKvAetuI8xN0h0fjbdGajBDrr7RAKUkg+BRsabVB1YRbMs7sQywZRtsmIhB8bW1chTBGLjGxREBUJS0gsxQPIYMzyarUKO105ilVcR9AUsu3SinSmPSNjs0V5IRax8OOCZKiidQepI0JoTKkLKgczjWSwSXO041qtBnseu8gXtGKxv9VqVSqVjY2NXC43OTnpkXRTmGl3F9p3OrqGJSQWNjxXBGNoLWUYK00DKGCwAtHbNc1mM5FICPu8sUKgpNls6p12EbPneZVK5c0332y324ODgw8//LAj3HaFygdEidASU+UkQChRlsitCBiwWR3iMcuFVIONIIJW+oLA9lePhgw22jjgYQc6mLZscnV1dX19fWNjIwzDZ555Bj5aQRWoVCrvvPPOiRMnTp06ZXsvl2qLkmDFjb85JoiHepowcVQTJseiA2cbUJnTFrkFHqEKmi1FhomFHLZRc+YcfVrooD6AV5z1paJLRvLuu+/Oz89PT0/PzMxQgagXsMGk7axNW/NRNpcJZecicuJsmuCrQpOPDS4srph9ZmfXJooKESDQlKAhx6wuePM+ElW70+noJVMEV7YewV4g8IEdxWGMlu95HuwkIOOE2j28CIcctUIybb5t08S0Aeub0IqYcCSbcjDnSNcReov6tu9phGNSrHZ7i1IK1nLZKWctXgtJpVJorZU2qp8oZM+yc0G912WObDIFIWytz0OhHGX1T7M+Wy5URAfID5B+JjjZTqESlwSO9j8yvaNN0DGFY/0yBeqsTQJQPB4397q19dHbuT+DFDN/opEPyXKDDWzkKbYNly5nDQAO9nkdy1aRrcI6ja0tNi2VAVJGQVtn5bNsqDK3bzRPsceaIHrK0cDGJhOybISm7HGkGi61zBJm5d0GM7Rc2VEHMQggZxsd2lUUVpAXmrVsP9kS2gTiZPGGqtEVeTvfkqDS0AizbKwyNlhVZEDMctsUsLgrM5g/matCG8z0hjou4NEbUYu36WAy29RghVCBdIbYwKFlIiVN0jiEEEWRATTZZM8UYpPQKZdTcvChP3lX0B6A8Jx6BosiZkUbzAgYQBW1earLxChi3IKoyLM2i9THJikOS7QcMx8P7WT2LhIdgZnN+UwNWeVtnUIlkdWhkP+sHDsTAmIJFW2IJUdxpDc7RjLZYnS3kOkYKWRmxbmNaTEor7CBqxDHqdouEcYdybqNV045FlQwy6kPmSMixGPURLlcnp2dbTabxWKxWCz29/cXCgX0XRC255SQVjK0yNVpOTvlXcmhJDQn6MyWyDMo9IKdPqQJlRYpKtS7zdC/tOfuHhBp71qV99577969e+12G/ZQhL32U6lUJpPJ5/PDw8PKbsq2JlgFbIXCWTMMCUDbVSsmmwkqqK5Zjp6lprBN5w6Zi7LPrABgMnPk2S4QS1n8ySXisAaud9XWd3BND9Nb77NpCh1fuVwmz5Ji25pA/eoBX1k5VAf4CoG2LQGuzEJ4qpsiFlsRQZHJIChp65Tm6QKxaImtgUjEggO9GbpgDQiNlWFn7q27kA1rXWqxhawvsVOoLOOvlCqVSt/97neHhobMHQPNsWLV9n1fkCm3KHTNZYg0D3Ov0PQ/c2hYfLIVCiVamrnFo1ZreHh4dXU1Ho/D6Aj9FGyxB9ByJ2FkhWDRm0qFQuGpp56C5ya2t7epHNbBtBXSqUQkIJkiHoKqyOGLX3kHskVolkcupCPu7SxYm2ydTmd+fv4b3/jGzMzMRx99dPfuXdsQmKPmqNVRID1nGr1kg4NR0t+nUMRt2J6iDZ66Qnc617JV2Nh2PWJLvcHsvC3Gsxrb2MwDs8OmE8zMzCil9Iawkc2FYVitVufn57e3t+EDO2wtVv9uqQcJ7AjbciZE3u5FKZdWzPRU7UagrpS3qcdOJfqrzDehKazJ4ugpmsMiv2H5TZ52u72wsIBGn9UKnVpbW/v973//9NNPw7PnmUwGPnyVSqVSqRRcHMA3LPTLnHTI3MllNOTCZrOJPjeHsg7W5eTmkFbowXx326KzhpoWuq8LE0qMXEKspQxydUro3ipEt4mJiWq1an7S2Bxo2hM4C09X6hDQ6XRarRY8PgD77vm+D7tOgtklk0ltZ/F4HD4fzyqJGpJ53O31pZdegjfGNjY2UHXT1IR4QtVDEmzhTIiMJj/NPVz6dT/HQko7+oTA436WPjSSz+cfffTRP/7xj9/61rdYpKUd0N6plII4CN9h832/0+lsb29vb2/DZwQHBgaSyWSn06nX641GQ6cvnU4nlUo99thjQhdsvUAZiS1BpoXT09OQMw0ODup3vln5LuFSGQsfZgJjC4LdJjCOZPLff/6fGqkJgwg26CWeo0+YvaVvHwDk5PP5X/ziF4899hj7dWcUO7TySql6vd7pdGq1WqPRiMVimUxmYGBgYmJCP4GpP/gGo6+/Fw+fnXEdP0t3bMRired5vu9D/qTfUqT9UsRYbQ5mO5CNXiDbJNLZZ/kTZgWqNypngQ35Bx1HG6TDGwSI4cMPPwzD8Ny5c+Zw23quj2GT2ffff//UqVOFQmFoaMjzPB0EzR30TbcOwxA2SzI3JHJECFMNmlxGkpkj9gYnlBmNvEdWDdzFyprYgpgrYrHiWAeyeYkpnEqLxWIDAwNra2uNRsNcLIa5Z5tg7dXbeVKg2Ww+/vjj5i7ZQCY+qZ2bJDobC4IAdipjlRcchnbN3cKoTBvks2HENqpIsi3CoCouWZc7gbRoxKLVHAtdNDh//vz58+fhg2zVarW2Q9VqtVqtKrtD0CsXtfuDRJ7xJB2FhND4imkQBPJ2pvLI2DR0IdYC2FiviH24qCREGKEXe6cQdpsBEnIss0R2aJkNlWjh8Xg8n8/n83mTYWtr6+2336Z4IIyp3p7KxmCKgqtCYIP9PHogOR/SxCrDZkVhGPq+r7+ZwLKp3X6ljLlj8cwWRnqjfc6xbD9tFSPlCIVAtpzXPDYjGoDQjRs3BgYGYB3LnB4XEnJV9wyM4opNLC3c2tr65JNP2u12sVh85JFHhFerbTqj8WFxfe+0DzkWm2HI4MSyUf7IuI78WOCBPH1qaqrdbs/Ozm5tbcGXTmF1CrAwl8uZf/V1g9AL1ATSSlAbBTi5d5pqtdpvfvObWCw2PT0dj8fv3Llz5swZti6aS9qWmafuI1xFpuAYsUzI0UqbwZ52Bkm0tSSXu/tQZGAdGhr60Y9+FARBq9VqNpu1Wm1ra6tcLsNr7Ovr6+vr63Nzc9VqtdFoQLaey+UKhQLY2dTU1LPPPouiCetLQirdW9c0Xbp0aXFxsVAozM3NPfnkk5ubmxATI0fG1BBNVm9DLTDLcnYhFkobUQCiSaVLjuVC7ohla5fyx+PxbDabzWYHBgYmJydRVIIvgTcajc3NzfUdKpfLa2trc3NzyuLcMr7aIhFNPtjcQAtvt9vvvPOO53nwXapyuTw+Pl6r1fr7+wWMt+EoRQfKI5BjSLF1Bwh/CBPZuIvt25KzyIyE7T9LXSXFNga4vZPP54eGhnSU0X3vSlSkekKizdLs7Ozm5mY2m4WPtW5tbY2Pj7MPDrGhg5XP3it0IRSsXPSnTTitYyGnYd2RTVm69RKboo6A0TPtl5zeGvU8b25urtlsLi4uNhqNc+fOTU1N6SsPilLaDWzZrX6BNjI1FAbWPOWYj5rkuo5lQzJbxT1eg+hBodH5SJHLKLvkoKurqyAHLm+TyaS5sZE7mWaHCllmQT3FIZb7XOx60I/mUiYg0XJTA0enZznDMLxx48bVq1eDIBgeHh4ZGRkaGspkMojtCFqYS69tapt16/V6u93OZDLwmV39Wj1NbSO77+08QYk2c1NceJHTKTPyuEQh0/4Skdc1jsmW+3ybnJ7n+b7/q1/96re//S18kgmeY0kkEqVS6Sc/+YncBLV7ZfEwWwRXuyfedqzcPMpxEKgEeE8pl8vBIz0uW1vR1rUC2rDQZJnJE4IxNCm0okvvTPl4n/dDpjAMX3311YsXL05OTsLg+r7farXa7XatVtM8QnZsC83oWGCLrMv+RGqwPx1FhWEISyRwE1P41kukZCB4mUAjFot2kREwshW5orTP+yFQpVK5cePGyZMntXuFO3fx2IdJ0AWLJopYJiC5u42cT9ha74EQaDUajfX19WQyOTIyArhl3ikXQJGdOHj6yPxU0+HPLw6Fh0wrKyvnz59PpVKVSqXdbnd2KAxDuKEhoLeyr1sihq6iGIuOQhaiLJAQSSbzyZMnv//977/wwgsnT54slUrpdHp9fT0SupDzaDW0YSG2w6QeQyHNbAQ5gvx4PP7QQw8FQQCGBe9Dw0G4Q5pZyDrZkCTHKdtPZENIFFuLgqUpypa2m6K+973v5XI5eGgMsu+BgQG0t4eWKQwIECyA6VX7BxKO8GflHImNCDY5gvyRkZG7d++Ojo4C+APpp1mAx5w8PZS2awh3EsxdlikEJjnhEwqz2Sw8amF2PJKazebq6moymRweHjavAeEySH8o9MEY1oPKroDATZeWlsKdT2olEglwNYAr0yPptCHcsgU7OVAiZkQo0WFjsXwFR32AthuGof6MnjwjWkgQBDBu8Xi8Xq+br580m80wDOHL0PB87APIscwfdHDlvIQtsZGN8/nnn//b3/52/fp1MKz4DiUSib6+PjkBp3NmA1EXYJMBTJZgNsTajdrtG8J1rqOG1WoVMlH4urFptbVazfO8bDYL5ehzB47kPrMs7TKsyN72NhwyZzwef/HFF5999tk7d+5sbW1tb29XKpXt7e2tra1CoYAmRu0gWbetHxq5LECYONrzRSt8DkO/OGmKrdVqfX19urC3Udrj2PJf/7KRHC9kNhpTzAgyMDAwMDBgModhePHiRUUW99jWtRyaO6OmbQm1IgmyS1RF6ZQjqtlK2DgrXAGYb+Tq8nq9HgSBjowyXPUAS46mIu3dQMnG4O6mcjpiMnueh5ayZMnssVnLvacCMCPUNKWxI86ahcYqoV2EyvSqEO78pFIp9Cmezc1NpZT+OrVsWD3AkqOpJAQDpG7qmGO5+IELTzKZNLfvUbt9mv0sNCWKxxQCaRqEypEotgl9LJzqLedjEQvsKZlMwnMQsF9DGIawV0MZJiQAAA21SURBVA/Af88JlqyVy7B38YVVli0SQmzkpFwi0Ww2EfAoMSJT+WbqjaAiMgEyf5qtoGmG2waRywR7SQ2pa4E9wVWO3hDK87ylpaV4PK43Q9x3w3JU/v4r9i7J016ohwQunU7DplCKA5VuE5pINWwhVRHXQsLj8bhef6LybXAoK28LEZogr4JsAe5VQBOw99PExASURL73e0CEP3limz86KHIK3JUotXvC9HE6nbY1Cq8+w5U2Oiuo6nGPK2mSTR/1y+xRIpHQu1gp0abRgUCRzBp6zY0Rq9Xq6urq4OBgsVj0PK/T6ehFQXkGbTkA60isMgiPmReMbJGCZhi2nEPXtWEAW5H+zOVybJfCMASQoNuK2CK7HP5sFGkiQKlUCu4WuEtGE8Ymf9RFqRqQWmkju3nzZqfTmZ6ehirmNwqQQLNdeiyfEnqkaa9XhY7z1FtFWMdSBIrCMIS8XvhaLpuZodZZf6UVNZlwZfKkUim4xRmZ+aG+ywHXPBZ6YRb++9//jsfj8ES/t/uymtY6UOri0737mG85NtHf3w/vv1OUTiQS6F0DF3Sx2bdjuNf2rTdMA8pkMvAYmXs3HTkd5UAv7ty5c+/evb6+vlOnTukYvS8N9UDMp3uFbmtmG3RHxnJK6KwpHDZJa7VaKCjAi4HwVqqgqk1/gQ2FDKohnDJ34lNK5XI53/fhDp1Nn4Mgsy/tdvutt94Kw/D8+fPgirBjPpx1mRchaRbMA026/onf0kHqsihFERsddBv4hCytVCotLi7Si4BkMqlDT2S32f4rMX0xiabk5lVYGIb5fB52DKS5o22qbA3RurIn6MJ333337t276XT6mWeegVqwgmq2GDkvNsXkfAsJ91Dybl6GoAljswSXWbGZM/2J/up2x8bGFhYWTGalFNyx17NLu8B2iu0RAkuhI+ZPtKN/X19fq9XSKyORYyKc1RI8z6O5NmWDg6tXr8KrKBcuXNBXPOZHgelkuQQWx+CDrAUo4UXBj3lgdtLmBGyrtlrmTyrQ87zx8XG4AEQDpCeVjWWyMnukcPdTU57n9ff3+74P91JoE9T6kWvZapmzaItl9Xr98uXL169fb7VaIyMjL7zwApTDOz/KPrZso72VsOW71oFcInFkNuaeY9l8yDyIxWKnTp26efMmcjvbtT2S4KKGss+rTb7ezw0KBwcHfd9fX18X0IUKN1UVggNqWh+02+1r167961//2tzchJuG3/72t/WFjoYr2tPI0M8ikMDMTiLzaHJXRionYUIJ66ksWJ49e/b27dvmpvie58HVYrvdtl34RE6wY2pFNQyNzUshiYHXHzY2NtB+z7Ji9EBm1gqvrKxcu3Ztdna21Wrpp5C/853vDA4OAgNshOQILZFNd8vpmmMJERqxKeKLikM4hPDKbvVwkEwmz507d/XqVb2PKASjvr6+SqViPmzTMzl6pya0zh6Lxaampra2tpaWltD2cXtvSynleV6n07l9+/Z//vOf27dv12o1WJIFZxseHn755ZeHhoa0cHOTJiEVZgFFAHV3UaqrHIs1zMhC9pjmFjJ0nTx5cnV19fbt2/rhtU6nk8vllpaWBgcHTRPflyxKIB0KYald4+VDDz309ttv37p1Sy9O2i4skIYsLuqObG1tffjhh59++unS0lIsFoPN6OPxOCxtZLPZp5566sKFC+ZuR77vDw8PO6bC7tGpW1H4XqHJF3l1IyQ0LCciCmzILcyKTz/9dLvdvnv3rp5LM9GxpcA9k81GvZ3LUlhq13czz549e+nSpWvXrv3f//2fEv1eLoF2l5aW3n333bm5uc3Nzf7+/nQ6nc/n4asIkFzm8/lz5849/fTTfX19yui+7/tQoqWpqJmKPCt0BwkxJeDHZmxgYxPXLaejKBY1L1y4kMlkZmdnwUEhwTLTLKH/tthtVqR9p1iohw8ikeZ87LHHksnkrVu35ubmZmZmTGn00seWNvi+f/PmzatXr96+fbtYLKZSqeHh4SAI6vU6LOun0+kzZ8488sgjZ86c0dvRaN3a7XYymTT3vHDM5yLn0cVj0cThB/3Mn46GbFZHQykHZrV79GmLNAk4f/58Npv94IMP4JbOwMBAuVweGRlxka+4OQYeb+fSjPad1tUXZfqDUJ7nlUqls2fPzs/Pv/766z/+8Y+7egqqXC7Pzc3duXNnYWEBmi4UCrA9PSgDd2lOnz598uTJVCpFLyHDnRWQUqmE/Ecgc3jRmLMjoJTkn4hn19MBkbBh00xAeCRKDjGRHuZ53sMPP5zJZK5cuaKUisVi+oZPz8Ram7KPO7BBKNRbAXie95WvfGVpaenWrVtvvPHGiy++KHu/7/ufffbZ/Pz87du3t7e3PeNiE4Id7Ko6MzMzPT2tXxtEtqK1go9cjI6OdhVwEI9tymQ2G8/nW95QjWk3WJ0QILEVWURE5Up0C7McjldWVt5++21Ytkkmk+haTIbbfaFEItHf3z88PAwJdRiGrVbrl7/8ZbPZTKfTFy5c+NrXvqafzgCGMAxXV1c/++yzzz77bGFhQS/f610FPM8rFAonT548ffr0iRMncrkcGwfMgQqCoFqtZjIZuCpUnA/IMEPDgjx9ZgmLap/Xcn/vltLBTZsLbW5uvvnmm5lMplwuT0xMmKcOQSuIUIODg3BZCoX//Oc///CHP2QyGXh/ZmpqanBwMBaLNZvNSqVSLpf1apy+rlRKxWKx0dHR06dPT09Pj46OoqcXKenQ3Gw26/X6yMhI5BrH4dN9xKKWq0SLtgGPsiMWNXAaImn0UZxPQEmtVnvrrbfgHWLzaqg3ikzU1G6XhY9AjYyM6OfpwjB89dVX33//fTAs+HidPhUaK6ue52UyGcic4Ps/FNdt6sEuvdVqtVgsQuuKCx1sdkvjg0ACsJkMfENCA7Z8qFuevZAgH061Wq333ntvYWFhcHCQXYVnzSKyUZez8K728PCwCRhBEFy8ePHy5cvwCg283q2rwD4LMzMzMzMz4+Pjkbttmy222+1Wq1Wr1TqdDkRh9+fZD3qaKEmIZQMqJa6Y24I05ddnlQXkaAkrIQiCa9eu3bhxAz76xcqPLEQljg6dzWb7+/tLpRLAhhaysrJy5cqVe/fu+b6fTqfBDsbHxycmJvL5PMVm3SjqIFwlNBoN2JYtn88PDg6adowQTsAnW6Bgx4HqJowMG0/2lGMdKZqdnf3444+LxSLde5OSo7UJEoDg4xelUonNcswgSE+xQR8IvoQA9pRIJAqFQn9/f+Q3YI8U4atCWwrFV+ZWvATEYuvCTxb5FIFGAVY9z1tfX//ggw8ajQbay1q3gqzExZLkn3CbJZfLlUolOa7ZorA5YjrYNZvNbDbb19fX399v7p+mLJgkg5/7jFBRbNoXCeefI9ZBX9wd8sXj2trawsLC5uZmpVLpdDqxHfJ2kxLNzt3m0ul0Op0GUEGbc2hOoftwdwiScaVULpcrFovFYrH3/h8NwjkWzfMpSCguG6A5strtQLQu2yKLZGwCTg9Q60opSFDgI9DwvRP4fHyz2Ww2m3Dxz1qe6sbs4KtPhUKhWCyiOy3h7lV7TZ1OB55mhk+gFwoF+GgZHT0BjdgUCp2iDGYvhCtHJAQFJcUZya6/X5gcqweCVAbMDmxOE3w9Gp64QpanAOp3Gx+89wEffkqn0+j6FEYf1q5g8cn3fR3sHtTLygdKTCikmROUC3bNWjFiUxbwM0nAJ7OE9RhZlA3eKL+WD0GqYRAyO3jIDt5Z8DwvlUrBE38QHHU52BPAJHzMp1gswvOAMjDLqGwDeHOQKUqhkUe5NRsuTAZlIWZGDiHHOvrU2wiEO2vf9XpdG1wYhrBwBQew82UmkwF7+kKCE0v/6yZ1TAdE+7zHzTEdE9D970EAQalZaPuJOE2hpjQk3yS2Cltu42ThFmlrk0N7TYXQYxdRQhBAI2w7sA01Ox02tbtS1WVwbP1iex0RCsNDv8fkQkdTq2My6TjHOqYDoe5yrMO3wmO7/y+lWLg7bEMpjeI0eCtLCFckGzALBYE2aVS47QC1Lld0NFkqLVJPdqzkFtlWbLXc9adpmU1Vmkt12y46exwKj+lA6Hi54ZgOhPZqWMeAd0ws4XUsulJiFlIeVM5yKi7YR2YVNk7KZhNFNUQ8SCxSGB3QurS/NuFCE6aEyIFCPIJiVD7qC5UvjGHk3FGt7j/XcbwydEz7SLs+dG7zHsSASgRHp95AZVKBVILg6zaZrO/aHJc2bes4eypy3Cgz1Yf2Vx4uOnRsr9kR60ErVIX9aYr6r1x5P6ajTxHrWPRBRFRftmWTjfXLSP1YV6OiImuxWlHJtlqog2wXXEQhbBB6LSgvV9yX8sgqQvedEOuYjqk3woglAJjirNgGAzaHc/FXJKcHUawzRQb9rk45QohjE5RH0FnAexbaBWn7KAqxHSPWMR0IMTmWDRJY03YsjBRlcx1BuA1fTWksrpgHMv5ROTJMsiWoOVZbF8XU7tGTR8Y2VqxwYeRZUWyXkSjrM+89PwjPvk7Um6hDo33U8Oh39nAIf/KELi7Ql3DMs2wVZd8cQRDFCvzc/O3vJ/YsyqZwSF566arjNlHK8G9Wf8ps+yn019ZBW3WhFy6jSo91yV7dS9DsmP6XiV/HAkJhlY3ELAn5h5CjsE2wJZHNseXu+keK2osQR/2FFm05llCrq6ZdREVq9f9fHPLDsj8W4AAAAABJRU5ErkJggg==" }, "Event": "nodeQueriesComplete", "TimeStamp": 1605630092, "NodeManufacturerName": "AEON Labs", "NodeProductName": "ZW099 Smart Dimmer 6", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Multilevel Switch", "NodeGeneric": 17, "NodeSpecificString": "Multilevel Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x0086", "NodeProductType": "0x0103", "NodeProductID": "0x0063", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 2, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "Light Dimmer Switch", "NodeDeviceType": 1536, "NodeRole": 5, "NodeRoleString": "Always On Slave", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 4, 13, 16, 17, 18, 21, 25, 27, 28, 29, 30, 31 ], "Neighbors": [ 4, 13, 16, 17, 18, 21, 25, 27, 28, 29, 30, 31 ], "Neighbors": [ 4, 13, 16, 17, 18, 21, 25, 27, 28, 29, 30, 31 ]}
+OpenZWave/1/node/12/instance/1/,{ "Instance": 1, "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "CommandClassVersion": 1, "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/112/value/844425141682196/,{ "Label": "Current Overload Protection", "Value": { "List": [ { "Value": 0, "Label": "Deactivate Overload Protection (Default)" }, { "Value": 1, "Label": "Active Overload Protection" } ], "Selected": "Deactivate Overload Protection (Default)", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 12, "Genre": "Config", "Help": "Load will be closed when the Current overruns (US 15.5A, Others 16.2) for more than 2 minutes", "ValueIDKey": 844425141682196, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629157}
+OpenZWave/1/node/12/instance/1/commandclass/112/value/5629499745763348/,{ "Label": "Output Load Status", "Value": { "List": [ { "Value": 0, "Label": "Last status (Default)" }, { "Value": 1, "Label": "Always on" }, { "Value": 2, "Label": "Always off" } ], "Selected": "Last status (Default)", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 20, "Node": 12, "Genre": "Config", "Help": "Configure the output load status after re-power on.", "ValueIDKey": 5629499745763348, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629157}
+OpenZWave/1/node/12/instance/1/commandclass/112/value/22517998348402708/,{ "Label": "Notification status", "Value": { "List": [ { "Value": 0, "Label": "Nothing" }, { "Value": 1, "Label": "Hail" }, { "Value": 2, "Label": "Basic" } ], "Selected": "Nothing", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 80, "Node": 12, "Genre": "Config", "Help": "Defines the automated status notification of an associated device when status changes", "ValueIDKey": 22517998348402708, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629158}
+OpenZWave/1/node/12/instance/1/commandclass/112/value/22799473325113364/,{ "Label": "Configure the state of the LED", "Value": { "List": [ { "Value": 0, "Label": "The LED will follow the status (on/off) of its load. (Default)" }, { "Value": 1, "Label": "When the state of the Switch changes, the LED will follow the status (on/off) of its load, but the LED will turn off after 5 seconds." }, { "Value": 2, "Label": "Night Light Mode" } ], "Selected": "The LED will follow the status (on/off) of its load. (Default)", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 81, "Node": 12, "Genre": "Config", "Help": "Configure what the LED Ring displays during operations", "ValueIDKey": 22799473325113364, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629158}
+OpenZWave/1/node/12/instance/1/commandclass/112/value/23362423278534675/,{ "Label": "Night Light Color", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 16777215, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 83, "Node": 12, "Genre": "Config", "Help": "Configure the RGB Value when in Night Light Mode. Byte 1: Red Color Byte 2: Green Color Byte 3: Blue Color", "ValueIDKey": 23362423278534675, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629158}
+OpenZWave/1/node/12/instance/1/commandclass/112/value/23643898255245331/,{ "Label": "RGB Brightness in Energy Mode", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 16777215, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 84, "Node": 12, "Genre": "Config", "Help": "Configure the brightness level of RGB LED (0%-100%) when it is in Energy Mode/momentary indicate mode. Byte 1: Red Color Byte 2: Green Color Byte 3: Blue Color", "ValueIDKey": 23643898255245331, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629158}
+OpenZWave/1/node/12/instance/1/commandclass/112/value/25332748115509264/,{ "Label": "Enables/disables parameter 91/92", "Value": false, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 90, "Node": 12, "Genre": "Config", "Help": "Enable/disable Wattage threshold and percent.", "ValueIDKey": 25332748115509264, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629158}
+OpenZWave/1/node/12/instance/1/commandclass/112/value/25614223092219926/,{ "Label": "Minimum Change to send Report (Watt)", "Value": 25, "Units": "watts", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 32000, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 91, "Node": 12, "Genre": "Config", "Help": "The value represents the minimum change in wattage for a Report to be sent (default 25 W)", "ValueIDKey": 25614223092219926, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629159}
+OpenZWave/1/node/12/instance/1/commandclass/112/value/25895698068930577/,{ "Label": "Minimum Change to send Report (%)", "Value": 5, "Units": "%", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 92, "Node": 12, "Genre": "Config", "Help": "The value represents the minimum percentage change in wattage for a Report to be sent (Default 5)", "ValueIDKey": 25895698068930577, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629159}
+OpenZWave/1/node/12/instance/1/commandclass/112/value/28147497882615832/,{ "Label": "Default Group Reports", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 100, "Node": 12, "Genre": "Config", "Help": "Set report types for groups 1, 2 and 3 to default.", "ValueIDKey": 28147497882615832, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/112/value/28428972859326483/,{ "Label": "Report type sent in Reporting Group 1", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 101, "Node": 12, "Genre": "Config", "Help": "Defines the type of report sent for reporting group 1. 2 is multisensor report. 4 is meter report for watts. 8 is meter report for kilowatts. Value 1 (msb) Reserved Value 2 Reserved Value 3 Reserved Value 4 (lsb) bits 7-4 reserved bit 3 KWH bit 2 Watt bit 1 Current bit 0 Voltage", "ValueIDKey": 28428972859326483, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629159}
+OpenZWave/1/node/12/instance/1/commandclass/112/value/28710447836037139/,{ "Label": "Report type sent in Reporting Group 2", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 102, "Node": 12, "Genre": "Config", "Help": "Defines the type of report sent for reporting group 1. 2 is multisensor report. 4 is meter report for watts. 8 is meter report for kilowatts. Value 1 (msb) Reserved Value 2 Reserved Value 3 Reserved Value 4 (lsb) bits 7-4 reserved bit 3 KWH bit 2 Watt bit 1 Current bit 0 Voltage", "ValueIDKey": 28710447836037139, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629159}
+OpenZWave/1/node/12/instance/1/commandclass/112/value/28991922812747795/,{ "Label": "Report type sent in Reporting Group 3", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 103, "Node": 12, "Genre": "Config", "Help": "Defines the type of report sent for reporting group 1. 2 is multisensor report. 4 is meter report for watts. 8 is meter report for kilowatts. Value 1 (msb) Reserved Value 2 Reserved Value 3 Reserved Value 4 (lsb) bits 7-4 reserved bit 3 KWH bit 2 Watt bit 1 Current bit 0 Voltage", "ValueIDKey": 28991922812747795, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629159}
+OpenZWave/1/node/12/instance/1/commandclass/112/value/30962247649722392/,{ "Label": "Set 111 to 113 to default", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 110, "Node": 12, "Genre": "Config", "Help": "Set time interval for sending reports for groups 1, 2 and 3 to default.", "ValueIDKey": 30962247649722392, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/112/value/31243722626433043/,{ "Label": "Send Interval for Reporting Group 1", "Value": 3, "Units": "seconds", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": -1, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 111, "Node": 12, "Genre": "Config", "Help": "Defines the time interval when the defined report for group 1 is sent.", "ValueIDKey": 31243722626433043, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629160}
+OpenZWave/1/node/12/instance/1/commandclass/112/value/31525197603143699/,{ "Label": "Send Interval for Reporting Group 2", "Value": 600, "Units": "seconds", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": -1, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 112, "Node": 12, "Genre": "Config", "Help": "Defines the time interval when the defined report for group 2 is sent.", "ValueIDKey": 31525197603143699, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629160}
+OpenZWave/1/node/12/instance/1/commandclass/112/value/31806672579854355/,{ "Label": "Send Interval for Reporting Group 3", "Value": 600, "Units": "seconds", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": -1, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 113, "Node": 12, "Genre": "Config", "Help": "Defines the time interval when the defined report for group 3 is sent.", "ValueIDKey": 31806672579854355, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629160}
+OpenZWave/1/node/12/instance/1/commandclass/112/value/56294995553681428/,{ "Label": "Partner ID", "Value": { "List": [ { "Value": 0, "Label": "Aeon Labs Standard (Default)" }, { "Value": 1, "Label": "Others" } ], "Selected": "Aeon Labs Standard (Default)", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 200, "Node": 12, "Genre": "Config", "Help": "Partner ID", "ValueIDKey": 56294995553681428, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629160}
+OpenZWave/1/node/12/instance/1/commandclass/112/value/70931694342635540/,{ "Label": "Configuration Locked", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 252, "Node": 12, "Genre": "Config", "Help": "Enable/disable Configuration Locked", "ValueIDKey": 70931694342635540, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629160}
+OpenZWave/1/node/12/instance/1/commandclass/112/value/71494644296056854/,{ "Label": "Device tag", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 65535, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 254, "Node": 12, "Genre": "Config", "Help": "Device tag.", "ValueIDKey": 71494644296056854, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629161}
+OpenZWave/1/node/12/instance/1/commandclass/112/value/71776119272767512/,{ "Label": "Reset device", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 255, "Node": 12, "Genre": "Config", "Help": "Reset to the default configuration.", "ValueIDKey": 71776119272767512, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "CommandClassVersion": 2, "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/38/value/1407375098085395/,{ "Label": "Instance 1: Dimming Duration", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 5, "Node": 12, "Genre": "System", "Help": "Duration taken when changing the Level of a Device (Values above 7620 use the devices default duration)", "ValueIDKey": 1407375098085395, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605630293}
+OpenZWave/1/node/12/instance/1/commandclass/38/value/206143505/,{ "Label": "Instance 1: Level", "Value": 99, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 12, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 206143505, "ReadOnly": false, "WriteOnly": false, "Event": "valueRefreshed", "TimeStamp": 1605630090}
+OpenZWave/1/node/12/instance/1/commandclass/38/value/281475182854168/,{ "Label": "Instance 1: 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": 12, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475182854168, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/38/value/562950159564824/,{ "Label": "Instance 1: 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": 12, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950159564824, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/38/value/844425144664080/,{ "Label": "Instance 1: 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": 12, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425144664080, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/38/value/1125900121374737/,{ "Label": "Instance 1: 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": 12, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900121374737, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/39/,{ "Instance": 1, "CommandClassId": 39, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "CommandClassVersion": 1, "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/39/value/214548500/,{ "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": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "Index": 0, "Node": 12, "Genre": "System", "Help": "Switch All Devices On/Off", "ValueIDKey": 214548500, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629055}
+OpenZWave/1/node/12/instance/1/commandclass/51/,{ "Instance": 1, "CommandClassId": 51, "CommandClass": "COMMAND_CLASS_COLOR", "CommandClassVersion": 1, "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/51/value/562950168166419/,{ "Label": "Color Channels", "Value": 28, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 2, "Node": 12, "Genre": "System", "Help": "Color Capabilities of the device", "ValueIDKey": 562950168166419, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/51/value/206356503/,{ "Label": "Color", "Value": "#000000", "Units": "#RRGGBB", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 0, "Node": 12, "Genre": "User", "Help": "Color (in RGB format)", "ValueIDKey": 206356503, "ReadOnly": false, "WriteOnly": false, "Event": "valueRefreshed", "TimeStamp": 1605630092}
+OpenZWave/1/node/12/instance/1/commandclass/51/value/281475183067156/,{ "Label": "Color Index", "Value": { "List": [ { "Value": 0, "Label": "Off" }, { "Value": 1, "Label": "Cool White" }, { "Value": 2, "Label": "Warm White" }, { "Value": 3, "Label": "Red" }, { "Value": 4, "Label": "Lime" }, { "Value": 5, "Label": "Blue" }, { "Value": 6, "Label": "Yellow" }, { "Value": 7, "Label": "Cyan" }, { "Value": 8, "Label": "Magenta" }, { "Value": 9, "Label": "Silver" }, { "Value": 10, "Label": "Gray" }, { "Value": 11, "Label": "Maroon" }, { "Value": 12, "Label": "Olive" }, { "Value": 13, "Label": "Green" }, { "Value": 14, "Label": "Purple" }, { "Value": 15, "Label": "Teal" }, { "Value": 16, "Label": "Navy" }, { "Value": 17, "Label": "Custom" } ], "Selected": "Off", "Selected_id": 0 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 1, "Node": 12, "Genre": "User", "Help": "Preset Color", "ValueIDKey": 281475183067156, "ReadOnly": false, "WriteOnly": false, "Event": "valueRefreshed", "TimeStamp": 1605630092}
+OpenZWave/1/node/12/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "CommandClassVersion": 1, "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/94/value/215449617/,{ "Label": "Instance 1: 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": 12, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 215449617, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/94/value/281475192160278/,{ "Label": "Instance 1: InstallerIcon", "Value": 1536, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 12, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475192160278, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/94/value/562950168870934/,{ "Label": "Instance 1: UserIcon", "Value": 1536, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 12, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950168870934, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "CommandClassVersion": 2, "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/114/value/215777299/,{ "Label": "Loaded Config Revision", "Value": 5, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 12, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 215777299, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/114/value/281475192487955/,{ "Label": "Config File Revision", "Value": 5, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 12, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475192487955, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/114/value/562950169198611/,{ "Label": "Latest Available Config File Revision", "Value": 5, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 12, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950169198611, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/114/value/1125900122619927/,{ "Label": "Serial Number", "Value": "0a000100010106040700000108010000000000", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 12, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900122619927, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "CommandClassVersion": 1, "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/115/value/215793684/,{ "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": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 12, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 215793684, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629055}
+OpenZWave/1/node/12/instance/1/commandclass/115/value/281475192504337/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 12, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475192504337, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605629055}
+OpenZWave/1/node/12/instance/1/commandclass/115/value/562950169215000/,{ "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": 12, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950169215000, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/115/value/844425145925649/,{ "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": 12, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425145925649, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/115/value/1125900122636308/,{ "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": 12, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900122636308, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/115/value/1407375099346966/,{ "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": 12, "Genre": "System", "Help": "How Many Messages to send to the Node for the Test", "ValueIDKey": 1407375099346966, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/115/value/1688850076057624/,{ "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": 12, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850076057624, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/115/value/1970325052768280/,{ "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": 12, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325052768280, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/115/value/2251800029478932/,{ "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": 12, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800029478932, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/115/value/2533275006189590/,{ "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": 12, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275006189590, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/129/,{ "Instance": 1, "CommandClassId": 129, "CommandClass": "COMMAND_CLASS_CLOCK", "CommandClassVersion": 1, "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/129/value/207634452/,{ "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": "Tuesday", "Selected_id": 2 }, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLOCK", "Index": 0, "Node": 12, "Genre": "User", "Help": "Day of Week", "ValueIDKey": 207634452, "ReadOnly": false, "WriteOnly": false, "Event": "valueRefreshed", "TimeStamp": 1605630092}
+OpenZWave/1/node/12/instance/1/commandclass/129/value/281475184345105/,{ "Label": "Hour", "Value": 11, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLOCK", "Index": 1, "Node": 12, "Genre": "User", "Help": "Hour", "ValueIDKey": 281475184345105, "ReadOnly": false, "WriteOnly": false, "Event": "valueRefreshed", "TimeStamp": 1605630092}
+OpenZWave/1/node/12/instance/1/commandclass/129/value/562950161055761/,{ "Label": "Minute", "Value": 21, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CLOCK", "Index": 2, "Node": 12, "Genre": "User", "Help": "Minute", "ValueIDKey": 562950161055761, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605630092}
+OpenZWave/1/node/12/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "CommandClassVersion": 1, "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/134/value/216104983/,{ "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": 12, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 216104983, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/134/value/281475192815639/,{ "Label": "Protocol Version", "Value": "4.38", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 12, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475192815639, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/134/value/562950169526295/,{ "Label": "Application Version", "Value": "1.12", "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 12, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950169526295, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/50/,{ "Instance": 1, "CommandClassId": 50, "CommandClass": "COMMAND_CLASS_METER", "CommandClassVersion": 3, "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/50/value/206340114/,{ "Label": "Electric - kWh", "Value": 17.562999725341798, "Units": "kWh", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 0, "Node": 12, "Genre": "User", "Help": "", "ValueIDKey": 206340114, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605630091}
+OpenZWave/1/node/12/instance/1/commandclass/50/value/562950159761426/,{ "Label": "Electric - W", "Value": 9.6899995803833, "Units": "W", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 2, "Node": 12, "Genre": "User", "Help": "", "ValueIDKey": 562950159761426, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605630091}
+OpenZWave/1/node/12/instance/1/commandclass/50/value/1125900113182738/,{ "Label": "Electric - V", "Value": 123.04900360107422, "Units": "V", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 4, "Node": 12, "Genre": "User", "Help": "", "ValueIDKey": 1125900113182738, "ReadOnly": true, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605630091}
+OpenZWave/1/node/12/instance/1/commandclass/50/value/1407375089893394/,{ "Label": "Electric - A", "Value": 0.08299999684095383, "Units": "A", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 5, "Node": 12, "Genre": "User", "Help": "", "ValueIDKey": 1407375089893394, "ReadOnly": true, "WriteOnly": false, "Event": "valueRefreshed", "TimeStamp": 1605630091}
+OpenZWave/1/node/12/instance/1/commandclass/50/value/72057594244268048/,{ "Label": "Exporting", "Value": false, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 256, "Node": 12, "Genre": "User", "Help": "", "ValueIDKey": 72057594244268048, "ReadOnly": true, "WriteOnly": false, "Event": "valueRefreshed", "TimeStamp": 1605630091}
+OpenZWave/1/node/12/instance/1/commandclass/50/value/72339069229367320/,{ "Label": "Reset", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 257, "Node": 12, "Genre": "System", "Help": "", "ValueIDKey": 72339069229367320, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/43/,{ "Instance": 1, "CommandClassId": 43, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "CommandClassVersion": 1, "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/43/value/206225427/,{ "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": 12, "Genre": "User", "Help": "", "ValueIDKey": 206225427, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/43/value/281475182936083/,{ "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": 12, "Genre": "User", "Help": "", "ValueIDKey": 281475182936083, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/152/,{ "Instance": 1, "CommandClassId": 152, "CommandClass": "COMMAND_CLASS_SECURITY", "CommandClassVersion": 1, "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/1/commandclass/152/value/216399888/,{ "Label": "Instance 1: Secured", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SECURITY", "Index": 0, "Node": 12, "Genre": "System", "Help": "Is Communication with Device Encrypted", "ValueIDKey": 216399888, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/2/,{ "Instance": 2, "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/2/commandclass/38/,{ "Instance": 2, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "CommandClassVersion": 2, "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/2/commandclass/38/value/1407375098085411/,{ "Label": "Instance 2: Dimming Duration", "Value": 0, "Units": "", "ValueSet": true, "ValuePolled": false, "ChangeVerified": false, "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 5, "Node": 12, "Genre": "System", "Help": "Duration taken when changing the Level of a Device (Values above 7620 use the devices default duration)", "ValueIDKey": 1407375098085411, "ReadOnly": false, "WriteOnly": false, "Event": "valueChanged", "TimeStamp": 1605630295}
+OpenZWave/1/node/12/instance/2/commandclass/38/value/206143521/,{ "Label": "Instance 2: Level", "Value": 99, "Units": "", "ValueSet": true, "ValuePolled": true, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 12, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 206143521, "ReadOnly": false, "WriteOnly": false, "Event": "valueRefreshed", "TimeStamp": 1605630132}
+OpenZWave/1/node/12/instance/2/commandclass/38/value/281475182854184/,{ "Label": "Instance 2: Bright", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 12, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475182854184, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/2/commandclass/38/value/562950159564840/,{ "Label": "Instance 2: Dim", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Button", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 12, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950159564840, "ReadOnly": false, "WriteOnly": true, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/2/commandclass/38/value/844425144664096/,{ "Label": "Instance 2: Ignore Start Level", "Value": true, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 12, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425144664096, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/2/commandclass/38/value/1125900121374753/,{ "Label": "Instance 2: Start Level", "Value": 0, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 12, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900121374753, "ReadOnly": false, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/2/commandclass/94/,{ "Instance": 2, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "CommandClassVersion": 1, "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/2/commandclass/94/value/215449633/,{ "Label": "Instance 2: ZWave+ Version", "Value": 1, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 12, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 215449633, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/2/commandclass/94/value/281475192160294/,{ "Label": "Instance 2: InstallerIcon", "Value": 1536, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 2, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 12, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475192160294, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/2/commandclass/94/value/562950168870950/,{ "Label": "Instance 2: UserIcon", "Value": 1536, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 2, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 12, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950168870950, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/2/commandclass/152/,{ "Instance": 2, "CommandClassId": 152, "CommandClass": "COMMAND_CLASS_SECURITY", "CommandClassVersion": 1, "TimeStamp": 1605629028}
+OpenZWave/1/node/12/instance/2/commandclass/152/value/216399904/,{ "Label": "Instance 2: Secured", "Value": false, "Units": "", "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Min": 0, "Max": 0, "Type": "Bool", "Instance": 2, "CommandClass": "COMMAND_CLASS_SECURITY", "Index": 0, "Node": 12, "Genre": "System", "Help": "Is Communication with Device Encrypted", "ValueIDKey": 216399904, "ReadOnly": true, "WriteOnly": false, "Event": "valueAdded", "TimeStamp": 1605629028}
+OpenZWave/1/node/12/association/1/,{ "Name": "LifeLine", "Help": "", "MaxAssociations": 5, "Members": [ "1.1" ], "TimeStamp": 1605629028}
+OpenZWave/1/node/12/association/2/,{ "Name": "Retransmit Switch CC", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1605629035}
\ No newline at end of file
diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_all_devices.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_all_devices.json
index 61ebc4d9a6c..bcaf40b4196 100644
--- a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_all_devices.json
+++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_all_devices.json
@@ -1 +1 @@
-{"df4a4a8169904cdb9c03d61a21f42140": {"name": "Zone Lisa Bios", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "12493538af164a409c6a1c79e38afe1c"}, "b310b72a0e354bfab43089919b9a88bf": {"name": "Floor kraan", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "c50f167537524366a5af7aa3942feb1e"}, "a2c3583e0a6349358998b760cea82d2a": {"name": "Bios Cv Thermostatic Radiator ", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "12493538af164a409c6a1c79e38afe1c"}, "b59bcebaf94b499ea7d46e4a66fb62d8": {"name": "Zone Lisa WK", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "c50f167537524366a5af7aa3942feb1e"}, "fe799307f1624099878210aa0b9f1475": {"name": "Adam", "types": {"py/set": ["temperature", "thermostat", "home"]}, "class": "gateway", "location": "1f9dcf83fd4e4b66b72ff787957bfe5d"}, "d3da73bde12a47d5a6b8f9dad971f2ec": {"name": "Thermostatic Radiator Jessie", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "82fa13f017d240daa0d0ea1775420f24"}, "21f2b542c49845e6bb416884c55778d6": {"name": "Playstation Smart Plug", "types": {"py/set": ["plug", "power"]}, "class": "game_console", "location": "cd143c07248f491493cea0533bc3d669"}, "78d1126fc4c743db81b61c20e88342a7": {"name": "CV Pomp", "types": {"py/set": ["plug", "power"]}, "class": "central_heating_pump", "location": "c50f167537524366a5af7aa3942feb1e"}, "90986d591dcd426cae3ec3e8111ff730": {"name": "Adam", "types": {"py/set": ["temperature", "thermostat", "home"]}, "class": "heater_central", "location": "1f9dcf83fd4e4b66b72ff787957bfe5d"}, "cd0ddb54ef694e11ac18ed1cbce5dbbd": {"name": "NAS", "types": {"py/set": ["plug", "power"]}, "class": "vcr", "location": "cd143c07248f491493cea0533bc3d669"}, "4a810418d5394b3f82727340b91ba740": {"name": "USG Smart Plug", "types": {"py/set": ["plug", "power"]}, "class": "router", "location": "cd143c07248f491493cea0533bc3d669"}, "02cf28bfec924855854c544690a609ef": {"name": "NVR", "types": {"py/set": ["plug", "power"]}, "class": "vcr", "location": "cd143c07248f491493cea0533bc3d669"}, "a28f588dc4a049a483fd03a30361ad3a": {"name": "Fibaro HC2", "types": {"py/set": ["plug", "power"]}, "class": "settop", "location": "cd143c07248f491493cea0533bc3d669"}, "6a3bf693d05e48e0b460c815a4fdd09d": {"name": "Zone Thermostat Jessie", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "82fa13f017d240daa0d0ea1775420f24"}, "680423ff840043738f42cc7f1ff97a36": {"name": "Thermostatic Radiator Badkamer", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "08963fec7c53423ca5680aa4cb502c63"}, "f1fee6043d3642a9b0a65297455f008e": {"name": "Zone Thermostat Badkamer", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "08963fec7c53423ca5680aa4cb502c63"}, "675416a629f343c495449970e2ca37b5": {"name": "Ziggo Modem", "types": {"py/set": ["plug", "power"]}, "class": "router", "location": "cd143c07248f491493cea0533bc3d669"}, "e7693eb9582644e5b865dba8d4447cf1": {"name": "CV Kraan Garage", "types": {"py/set": ["thermostat"]}, "class": "thermostatic_radiator_valve", "location": "446ac08dd04d4eff8ac57489757b7314"}}
\ No newline at end of file
+{"df4a4a8169904cdb9c03d61a21f42140": {"name": "Zone Lisa Bios", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "12493538af164a409c6a1c79e38afe1c"}, "b310b72a0e354bfab43089919b9a88bf": {"name": "Floor kraan", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "c50f167537524366a5af7aa3942feb1e"}, "a2c3583e0a6349358998b760cea82d2a": {"name": "Bios Cv Thermostatic Radiator ", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "12493538af164a409c6a1c79e38afe1c"}, "b59bcebaf94b499ea7d46e4a66fb62d8": {"name": "Zone Lisa WK", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "c50f167537524366a5af7aa3942feb1e"}, "fe799307f1624099878210aa0b9f1475": {"name": "Adam", "types": {"py/set": ["home", "temperature", "thermostat"]}, "class": "gateway", "location": "1f9dcf83fd4e4b66b72ff787957bfe5d"}, "d3da73bde12a47d5a6b8f9dad971f2ec": {"name": "Thermostatic Radiator Jessie", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "82fa13f017d240daa0d0ea1775420f24"}, "21f2b542c49845e6bb416884c55778d6": {"name": "Playstation Smart Plug", "types": {"py/set": ["plug", "power"]}, "class": "game_console", "location": "cd143c07248f491493cea0533bc3d669"}, "78d1126fc4c743db81b61c20e88342a7": {"name": "CV Pomp", "types": {"py/set": ["plug", "power"]}, "class": "central_heating_pump", "location": "c50f167537524366a5af7aa3942feb1e"}, "90986d591dcd426cae3ec3e8111ff730": {"name": "Adam", "types": {"py/set": ["home", "temperature", "thermostat"]}, "class": "heater_central", "location": "1f9dcf83fd4e4b66b72ff787957bfe5d"}, "cd0ddb54ef694e11ac18ed1cbce5dbbd": {"name": "NAS", "types": {"py/set": ["plug", "power"]}, "class": "vcr", "location": "cd143c07248f491493cea0533bc3d669"}, "4a810418d5394b3f82727340b91ba740": {"name": "USG Smart Plug", "types": {"py/set": ["plug", "power"]}, "class": "router", "location": "cd143c07248f491493cea0533bc3d669"}, "02cf28bfec924855854c544690a609ef": {"name": "NVR", "types": {"py/set": ["plug", "power"]}, "class": "vcr", "location": "cd143c07248f491493cea0533bc3d669"}, "a28f588dc4a049a483fd03a30361ad3a": {"name": "Fibaro HC2", "types": {"py/set": ["plug", "power"]}, "class": "settop", "location": "cd143c07248f491493cea0533bc3d669"}, "6a3bf693d05e48e0b460c815a4fdd09d": {"name": "Zone Thermostat Jessie", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "82fa13f017d240daa0d0ea1775420f24"}, "680423ff840043738f42cc7f1ff97a36": {"name": "Thermostatic Radiator Badkamer", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "08963fec7c53423ca5680aa4cb502c63"}, "f1fee6043d3642a9b0a65297455f008e": {"name": "Zone Thermostat Badkamer", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "08963fec7c53423ca5680aa4cb502c63"}, "675416a629f343c495449970e2ca37b5": {"name": "Ziggo Modem", "types": {"py/set": ["plug", "power"]}, "class": "router", "location": "cd143c07248f491493cea0533bc3d669"}, "e7693eb9582644e5b865dba8d4447cf1": {"name": "CV Kraan Garage", "types": {"py/set": ["thermostat"]}, "class": "thermostatic_radiator_valve", "location": "446ac08dd04d4eff8ac57489757b7314"}}
\ No newline at end of file
diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/680423ff840043738f42cc7f1ff97a36.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/680423ff840043738f42cc7f1ff97a36.json
index 75bc62fbad4..6754cf63d2d 100644
--- a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/680423ff840043738f42cc7f1ff97a36.json
+++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/680423ff840043738f42cc7f1ff97a36.json
@@ -1 +1 @@
-{"setpoint": 14.0, "temperature": 19.1, "battery": 0.51, "valve_position": 0.0, "temperature_difference": -0.4}
\ No newline at end of file
+{"temperature": 19.1, "setpoint": 14.0, "battery": 0.51, "temperature_difference": -0.4, "valve_position": 0.0}
\ No newline at end of file
diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/6a3bf693d05e48e0b460c815a4fdd09d.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/6a3bf693d05e48e0b460c815a4fdd09d.json
index 41333f374e1..14d596fb315 100644
--- a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/6a3bf693d05e48e0b460c815a4fdd09d.json
+++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/6a3bf693d05e48e0b460c815a4fdd09d.json
@@ -1 +1 @@
-{"setpoint": 15.0, "temperature": 17.2, "battery": 0.37, "active_preset": "asleep", "presets": {"home": [20.0, 22.0], "no_frost": [10.0, 30.0], "away": [12.0, 25.0], "vacation": [11.0, 28.0], "asleep": [16.0, 24.0]}, "schedule_temperature": 15.0, "available_schedules": ["CV Jessie"], "selected_schedule": "CV Jessie", "last_used": "CV Jessie"}
\ No newline at end of file
+{"temperature": 17.2, "setpoint": 15.0, "battery": 0.37, "active_preset": "asleep", "presets": {"home": [20.0, 22.0], "no_frost": [10.0, 30.0], "away": [12.0, 25.0], "vacation": [11.0, 28.0], "asleep": [16.0, 24.0]}, "schedule_temperature": 16.5, "available_schedules": ["CV Jessie"], "selected_schedule": "CV Jessie", "last_used": "CV Jessie"}
\ No newline at end of file
diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/90986d591dcd426cae3ec3e8111ff730.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/90986d591dcd426cae3ec3e8111ff730.json
index 5e481d36b46..862a3159754 100644
--- a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/90986d591dcd426cae3ec3e8111ff730.json
+++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/90986d591dcd426cae3ec3e8111ff730.json
@@ -1 +1 @@
-{"water_temperature": 70.0, "intended_boiler_temperature": 70.0, "modulation_level": 0.01}
\ No newline at end of file
+{"water_temperature": 70.0, "intended_boiler_temperature": 70.0, "modulation_level": 0.01, "heating_state": true}
\ No newline at end of file
diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/a2c3583e0a6349358998b760cea82d2a.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/a2c3583e0a6349358998b760cea82d2a.json
index eef83a67a20..c3e1a35b292 100644
--- a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/a2c3583e0a6349358998b760cea82d2a.json
+++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/a2c3583e0a6349358998b760cea82d2a.json
@@ -1 +1 @@
-{"setpoint": 13.0, "temperature": 17.2, "battery": 0.62, "valve_position": 0.0, "temperature_difference": -0.2}
\ No newline at end of file
+{"temperature": 17.2, "setpoint": 13.0, "battery": 0.62, "temperature_difference": -0.2, "valve_position": 0.0}
\ No newline at end of file
diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b310b72a0e354bfab43089919b9a88bf.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b310b72a0e354bfab43089919b9a88bf.json
index 16da5f44ef5..8478716dc7b 100644
--- a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b310b72a0e354bfab43089919b9a88bf.json
+++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b310b72a0e354bfab43089919b9a88bf.json
@@ -1 +1 @@
-{"setpoint": 21.5, "temperature": 26.0, "valve_position": 1.0, "temperature_difference": 3.5}
\ No newline at end of file
+{"temperature": 26.0, "setpoint": 21.5, "temperature_difference": 3.5, "valve_position": 1.0}
\ No newline at end of file
diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b59bcebaf94b499ea7d46e4a66fb62d8.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b59bcebaf94b499ea7d46e4a66fb62d8.json
index 65fa0dd3d52..6d1a8d135a4 100644
--- a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b59bcebaf94b499ea7d46e4a66fb62d8.json
+++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b59bcebaf94b499ea7d46e4a66fb62d8.json
@@ -1 +1 @@
-{"setpoint": 21.5, "temperature": 20.9, "battery": 0.34, "active_preset": "home", "presets": {"vacation": [15.0, 28.0], "asleep": [18.0, 24.0], "no_frost": [12.0, 30.0], "away": [17.0, 25.0], "home": [21.5, 22.0]}, "schedule_temperature": 21.5, "available_schedules": ["GF7 Woonkamer"], "selected_schedule": "GF7 Woonkamer", "last_used": "GF7 Woonkamer"}
\ No newline at end of file
+{"temperature": 20.9, "setpoint": 21.5, "battery": 0.34, "active_preset": "home", "presets": {"vacation": [15.0, 28.0], "asleep": [18.0, 24.0], "no_frost": [12.0, 30.0], "away": [17.0, 25.0], "home": [21.5, 22.0]}, "schedule_temperature": 21.5, "available_schedules": ["GF7 Woonkamer"], "selected_schedule": "GF7 Woonkamer", "last_used": "GF7 Woonkamer"}
\ No newline at end of file
diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/d3da73bde12a47d5a6b8f9dad971f2ec.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/d3da73bde12a47d5a6b8f9dad971f2ec.json
index fd202e05586..b5a26000c7f 100644
--- a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/d3da73bde12a47d5a6b8f9dad971f2ec.json
+++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/d3da73bde12a47d5a6b8f9dad971f2ec.json
@@ -1 +1 @@
-{"setpoint": 15.0, "temperature": 17.1, "battery": 0.62, "valve_position": 0.0, "temperature_difference": 0.1}
\ No newline at end of file
+{"temperature": 17.1, "setpoint": 15.0, "battery": 0.62, "temperature_difference": 0.1, "valve_position": 0.0}
\ No newline at end of file
diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/df4a4a8169904cdb9c03d61a21f42140.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/df4a4a8169904cdb9c03d61a21f42140.json
index 12947c42ce0..f27c382fc0b 100644
--- a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/df4a4a8169904cdb9c03d61a21f42140.json
+++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/df4a4a8169904cdb9c03d61a21f42140.json
@@ -1 +1 @@
-{"setpoint": 13.0, "temperature": 16.5, "battery": 0.67, "active_preset": "away", "presets": {"home": [20.0, 22.0], "away": [12.0, 25.0], "vacation": [12.0, 28.0], "no_frost": [8.0, 30.0], "asleep": [15.0, 24.0]}, "schedule_temperature": null, "available_schedules": [], "selected_schedule": null, "last_used": null}
\ No newline at end of file
+{"temperature": 16.5, "setpoint": 13.0, "battery": 0.67, "active_preset": "away", "presets": {"home": [20.0, 22.0], "away": [12.0, 25.0], "vacation": [12.0, 28.0], "no_frost": [8.0, 30.0], "asleep": [15.0, 24.0]}, "schedule_temperature": null, "available_schedules": [], "selected_schedule": null, "last_used": null}
\ No newline at end of file
diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/e7693eb9582644e5b865dba8d4447cf1.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/e7693eb9582644e5b865dba8d4447cf1.json
index 151b4b41f70..610c019b686 100644
--- a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/e7693eb9582644e5b865dba8d4447cf1.json
+++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/e7693eb9582644e5b865dba8d4447cf1.json
@@ -1 +1 @@
-{"setpoint": 5.5, "temperature": 15.6, "battery": 0.68, "valve_position": 0.0, "temperature_difference": 0.0, "active_preset": "no_frost", "presets": {"home": [20.0, 22.0], "asleep": [17.0, 24.0], "away": [15.0, 25.0], "vacation": [15.0, 28.0], "no_frost": [10.0, 30.0]}, "schedule_temperature": null, "available_schedules": [], "selected_schedule": null, "last_used": null}
\ No newline at end of file
+{"temperature": 15.6, "setpoint": 5.5, "battery": 0.68, "temperature_difference": 0.0, "valve_position": 0.0, "active_preset": "no_frost", "presets": {"home": [20.0, 22.0], "asleep": [17.0, 24.0], "away": [15.0, 25.0], "vacation": [15.0, 28.0], "no_frost": [10.0, 30.0]}, "schedule_temperature": null, "available_schedules": [], "selected_schedule": null, "last_used": null}
\ No newline at end of file
diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/f1fee6043d3642a9b0a65297455f008e.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/f1fee6043d3642a9b0a65297455f008e.json
index 9934e109033..c4b5769e6d1 100644
--- a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/f1fee6043d3642a9b0a65297455f008e.json
+++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/f1fee6043d3642a9b0a65297455f008e.json
@@ -1 +1 @@
-{"setpoint": 14.0, "temperature": 18.9, "battery": 0.92, "active_preset": "away", "presets": {"asleep": [17.0, 24.0], "no_frost": [10.0, 30.0], "away": [14.0, 25.0], "home": [21.0, 22.0], "vacation": [12.0, 28.0]}, "schedule_temperature": 14.0, "available_schedules": ["Badkamer Schema"], "selected_schedule": "Badkamer Schema", "last_used": "Badkamer Schema"}
\ No newline at end of file
+{"temperature": 18.9, "setpoint": 14.0, "battery": 0.92, "active_preset": "away", "presets": {"asleep": [17.0, 24.0], "no_frost": [10.0, 30.0], "away": [14.0, 25.0], "home": [21.0, 22.0], "vacation": [12.0, 28.0]}, "schedule_temperature": 14.0, "available_schedules": ["Badkamer Schema"], "selected_schedule": "Badkamer Schema", "last_used": "Badkamer Schema"}
\ No newline at end of file
diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/notifications.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/notifications.json
new file mode 100644
index 00000000000..c229f64da04
--- /dev/null
+++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/notifications.json
@@ -0,0 +1 @@
+{"af82e4ccf9c548528166d38e560662a4": {"warning": "Node Plug (with MAC address 000D6F000D13CB01, in room 'n.a.') has been unreachable since 23:03 2020-01-18. Please check the connection and restart the device."}}
\ No newline at end of file
diff --git a/tests/fixtures/plugwise/anna_heatpump/get_all_devices.json b/tests/fixtures/plugwise/anna_heatpump/get_all_devices.json
index 4992a175b14..191f5b442b7 100644
--- a/tests/fixtures/plugwise/anna_heatpump/get_all_devices.json
+++ b/tests/fixtures/plugwise/anna_heatpump/get_all_devices.json
@@ -1 +1 @@
-{"1cbf783bb11e4a7c8a6843dee3a86927": {"name": "Anna", "types": {"py/set": ["temperature", "thermostat", "home"]}, "class": "heater_central", "location": "a57efe5f145f498c9be62a9b63626fbf"}, "015ae9ea3f964e668e490fa39da3870b": {"name": "Anna", "types": {"py/set": ["temperature", "thermostat", "home"]}, "class": "gateway", "location": "a57efe5f145f498c9be62a9b63626fbf"}, "3cb70739631c4d17a86b8b12e8a5161b": {"name": "Anna", "types": {"py/set": ["thermostat"]}, "class": "thermostat", "location": "c784ee9fdab44e1395b8dee7d7a497d5"}}
\ No newline at end of file
+{"1cbf783bb11e4a7c8a6843dee3a86927": {"name": "Anna", "types": {"py/set": ["home", "temperature", "thermostat"]}, "class": "heater_central", "location": "a57efe5f145f498c9be62a9b63626fbf"}, "015ae9ea3f964e668e490fa39da3870b": {"name": "Anna", "types": {"py/set": ["home", "temperature", "thermostat"]}, "class": "gateway", "location": "a57efe5f145f498c9be62a9b63626fbf"}, "3cb70739631c4d17a86b8b12e8a5161b": {"name": "Anna", "types": {"py/set": ["thermostat"]}, "class": "thermostat", "location": "c784ee9fdab44e1395b8dee7d7a497d5"}}
\ No newline at end of file
diff --git a/tests/fixtures/plugwise/anna_heatpump/get_device_data/1cbf783bb11e4a7c8a6843dee3a86927.json b/tests/fixtures/plugwise/anna_heatpump/get_device_data/1cbf783bb11e4a7c8a6843dee3a86927.json
index a8aea8e1357..ddf807303a2 100644
--- a/tests/fixtures/plugwise/anna_heatpump/get_device_data/1cbf783bb11e4a7c8a6843dee3a86927.json
+++ b/tests/fixtures/plugwise/anna_heatpump/get_device_data/1cbf783bb11e4a7c8a6843dee3a86927.json
@@ -1 +1 @@
-{"outdoor_temperature": 18.0, "heating_state": false, "dhw_state": false, "water_temperature": 29.1, "return_temperature": 25.1, "water_pressure": 1.57, "intended_boiler_temperature": 0.0, "modulation_level": 0.52, "cooling_state": false, "slave_boiler_state": false, "compressor_state": true, "flame_state": false}
\ No newline at end of file
+{"water_temperature": 29.1, "dhw_state": false, "intended_boiler_temperature": 0.0, "heating_state": false, "modulation_level": 0.52, "return_temperature": 25.1, "compressor_state": true, "cooling_state": false, "slave_boiler_state": false, "flame_state": false, "water_pressure": 1.57, "outdoor_temperature": 18.0}
\ No newline at end of file
diff --git a/tests/fixtures/plugwise/anna_heatpump/get_device_data/3cb70739631c4d17a86b8b12e8a5161b.json b/tests/fixtures/plugwise/anna_heatpump/get_device_data/3cb70739631c4d17a86b8b12e8a5161b.json
index 2a092e792d5..3177880705b 100644
--- a/tests/fixtures/plugwise/anna_heatpump/get_device_data/3cb70739631c4d17a86b8b12e8a5161b.json
+++ b/tests/fixtures/plugwise/anna_heatpump/get_device_data/3cb70739631c4d17a86b8b12e8a5161b.json
@@ -1 +1 @@
-{"setpoint": 21.0, "temperature": 23.3, "active_preset": "home", "presets": {"no_frost": [10.0, 30.0], "home": [21.0, 22.0], "away": [20.0, 25.0], "asleep": [20.5, 24.0], "vacation": [17.0, 28.0]}, "schedule_temperature": null, "available_schedules": ["standaard"], "selected_schedule": "standaard", "last_used": "standaard", "illuminance": 86.0}
\ No newline at end of file
+{"temperature": 23.3, "setpoint": 21.0, "active_preset": "home", "presets": {"no_frost": [10.0, 30.0], "home": [21.0, 22.0], "away": [20.0, 25.0], "asleep": [20.5, 24.0], "vacation": [17.0, 28.0]}, "schedule_temperature": null, "available_schedules": ["standaard"], "selected_schedule": "standaard", "last_used": "standaard", "illuminance": 86.0}
\ No newline at end of file
diff --git a/tests/fixtures/plugwise/anna_heatpump/notifications.json b/tests/fixtures/plugwise/anna_heatpump/notifications.json
new file mode 100644
index 00000000000..9e26dfeeb6e
--- /dev/null
+++ b/tests/fixtures/plugwise/anna_heatpump/notifications.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/tests/fixtures/plugwise/p1v3_full_option/get_all_devices.json b/tests/fixtures/plugwise/p1v3_full_option/get_all_devices.json
index e25fcb953c8..1feb33dd630 100644
--- a/tests/fixtures/plugwise/p1v3_full_option/get_all_devices.json
+++ b/tests/fixtures/plugwise/p1v3_full_option/get_all_devices.json
@@ -1 +1 @@
-{"e950c7d5e1ee407a858e2a8b5016c8b3": {"name": "P1", "types": {"py/set": ["power", "home"]}, "class": "gateway", "location": "cd3e822288064775a7c4afcdd70bdda2"}}
\ No newline at end of file
+{"e950c7d5e1ee407a858e2a8b5016c8b3": {"name": "P1", "types": {"py/set": ["home", "power"]}, "class": "gateway", "location": "cd3e822288064775a7c4afcdd70bdda2"}}
\ No newline at end of file
diff --git a/tests/fixtures/plugwise/p1v3_full_option/get_device_data/e950c7d5e1ee407a858e2a8b5016c8b3.json b/tests/fixtures/plugwise/p1v3_full_option/get_device_data/e950c7d5e1ee407a858e2a8b5016c8b3.json
index 36cb66c7902..fcbc1bbce33 100644
--- a/tests/fixtures/plugwise/p1v3_full_option/get_device_data/e950c7d5e1ee407a858e2a8b5016c8b3.json
+++ b/tests/fixtures/plugwise/p1v3_full_option/get_device_data/e950c7d5e1ee407a858e2a8b5016c8b3.json
@@ -1 +1 @@
-{"net_electricity_point": -2761.0, "electricity_consumed_peak_point": 0.0, "electricity_consumed_off_peak_point": 0.0, "net_electricity_cumulative": 442972.0, "electricity_consumed_peak_cumulative": 442932.0, "electricity_consumed_off_peak_cumulative": 551090.0, "net_electricity_interval": 0.0, "electricity_consumed_peak_interval": 0.0, "electricity_consumed_off_peak_interval": 0.0, "electricity_produced_peak_point": 2761.0, "electricity_produced_off_peak_point": 0.0, "electricity_produced_peak_cumulative": 396559.0, "electricity_produced_off_peak_cumulative": 154491.0, "electricity_produced_peak_interval": 0.0, "electricity_produced_off_peak_interval": 0.0, "gas_consumed_cumulative": 584.9, "gas_consumed_interval": 0.0}
\ No newline at end of file
+{"net_electricity_point": -2761.0, "electricity_consumed_peak_point": 0.0, "electricity_consumed_off_peak_point": 0.0, "net_electricity_cumulative": 442972.0, "electricity_consumed_peak_cumulative": 442932.0, "electricity_consumed_off_peak_cumulative": 551090.0, "net_electricity_interval": 0.0, "electricity_consumed_peak_interval": 0.0, "electricity_consumed_off_peak_interval": 0.0, "electricity_produced_peak_point": 2761.0, "electricity_produced_off_peak_point": 0.0, "electricity_produced_peak_cumulative": 396559.0, "electricity_produced_off_peak_cumulative": 154491.0, "electricity_produced_peak_interval": 0.0, "electricity_produced_off_peak_interval": 0.0, "gas_consumed_cumulative": 584.85, "gas_consumed_interval": 0.0}
\ No newline at end of file
diff --git a/tests/fixtures/plugwise/p1v3_full_option/notifications.json b/tests/fixtures/plugwise/p1v3_full_option/notifications.json
new file mode 100644
index 00000000000..9e26dfeeb6e
--- /dev/null
+++ b/tests/fixtures/plugwise/p1v3_full_option/notifications.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/tests/helpers/test_area_registry.py b/tests/helpers/test_area_registry.py
index f6a75fe3c30..4b6ca7da3fe 100644
--- a/tests/helpers/test_area_registry.py
+++ b/tests/helpers/test_area_registry.py
@@ -43,6 +43,7 @@ async def test_create_area(hass, registry, update_events):
"""Make sure that we can create an area."""
area = registry.async_create("mock")
+ assert area.id == "mock"
assert area.name == "mock"
assert len(registry.areas) == 1
@@ -68,6 +69,17 @@ async def test_create_area_with_name_already_in_use(hass, registry, update_event
assert len(update_events) == 1
+async def test_create_area_with_id_already_in_use(registry):
+ """Make sure that we can't create an area with a name already in use."""
+ area1 = registry.async_create("mock")
+
+ updated_area1 = registry.async_update(area1.id, "New Name")
+ assert updated_area1.id == area1.id
+
+ area2 = registry.async_create("mock")
+ assert area2.id == "mock_2"
+
+
async def test_delete_area(hass, registry, update_events):
"""Make sure that we can delete an area."""
area = registry.async_create("mock")
diff --git a/tests/helpers/test_check_config.py b/tests/helpers/test_check_config.py
index ffc05544694..7959cf66403 100644
--- a/tests/helpers/test_check_config.py
+++ b/tests/helpers/test_check_config.py
@@ -7,8 +7,8 @@ from homeassistant.helpers.check_config import (
async_check_ha_config_file,
)
-from tests.async_mock import patch
-from tests.common import patch_yaml_files
+from tests.async_mock import Mock, patch
+from tests.common import mock_platform, patch_yaml_files
_LOGGER = logging.getLogger(__name__)
@@ -37,7 +37,7 @@ def log_ha_config(conf):
_LOGGER.debug("error[%s] = %s", cnt, err)
-async def test_bad_core_config(hass, loop):
+async def test_bad_core_config(hass):
"""Test a bad core config setup."""
files = {YAML_CONFIG_FILE: BAD_CORE_CONFIG}
with patch("os.path.isfile", return_value=True), patch_yaml_files(files):
@@ -53,7 +53,7 @@ async def test_bad_core_config(hass, loop):
assert not res.errors
-async def test_config_platform_valid(hass, loop):
+async def test_config_platform_valid(hass):
"""Test a valid platform setup."""
files = {YAML_CONFIG_FILE: BASE_CONFIG + "light:\n platform: demo"}
with patch("os.path.isfile", return_value=True), patch_yaml_files(files):
@@ -65,7 +65,7 @@ async def test_config_platform_valid(hass, loop):
assert not res.errors
-async def test_component_platform_not_found(hass, loop):
+async def test_component_platform_not_found(hass):
"""Test errors if component or platform not found."""
# Make sure they don't exist
files = {YAML_CONFIG_FILE: BASE_CONFIG + "beer:"}
@@ -83,7 +83,7 @@ async def test_component_platform_not_found(hass, loop):
assert not res.errors
-async def test_component_platform_not_found_2(hass, loop):
+async def test_component_platform_not_found_2(hass):
"""Test errors if component or platform not found."""
# Make sure they don't exist
files = {YAML_CONFIG_FILE: BASE_CONFIG + "light:\n platform: beer"}
@@ -103,7 +103,7 @@ async def test_component_platform_not_found_2(hass, loop):
assert not res.errors
-async def test_package_invalid(hass, loop):
+async def test_package_invalid(hass):
"""Test a valid platform setup."""
files = {
YAML_CONFIG_FILE: BASE_CONFIG + (" packages:\n p1:\n" ' group: ["a"]')
@@ -121,7 +121,7 @@ async def test_package_invalid(hass, loop):
assert res.keys() == {"homeassistant"}
-async def test_bootstrap_error(hass, loop):
+async def test_bootstrap_error(hass):
"""Test a valid platform setup."""
files = {YAML_CONFIG_FILE: BASE_CONFIG + "automation: !include no.yaml"}
with patch("os.path.isfile", return_value=True), patch_yaml_files(files):
@@ -133,3 +133,62 @@ async def test_bootstrap_error(hass, loop):
# Only 1 error expected
res.errors.pop(0)
assert not res.errors
+
+
+async def test_automation_config_platform(hass):
+ """Test automation async config."""
+ files = {
+ YAML_CONFIG_FILE: BASE_CONFIG
+ + """
+automation:
+ use_blueprint:
+ path: test_event_service.yaml
+ input:
+ trigger_event: blueprint_event
+ service_to_call: test.automation
+input_datetime:
+""",
+ hass.config.path(
+ "blueprints/automation/test_event_service.yaml"
+ ): """
+blueprint:
+ name: "Call service based on event"
+ domain: automation
+ input:
+ trigger_event:
+ service_to_call:
+trigger:
+ platform: event
+ event_type: !input trigger_event
+action:
+ service: !input service_to_call
+""",
+ }
+ with patch("os.path.isfile", return_value=True), patch_yaml_files(files):
+ res = await async_check_ha_config_file(hass)
+ assert len(res.get("automation", [])) == 1
+ assert len(res.errors) == 0
+ assert "input_datetime" in res
+
+
+async def test_config_platform_raise(hass):
+ """Test bad config validation platform."""
+ mock_platform(
+ hass,
+ "bla.config",
+ Mock(async_validate_config=Mock(side_effect=Exception("Broken"))),
+ )
+ files = {
+ YAML_CONFIG_FILE: BASE_CONFIG
+ + """
+bla:
+ value: 1
+""",
+ }
+ with patch("os.path.isfile", return_value=True), patch_yaml_files(files):
+ res = await async_check_ha_config_file(hass)
+ assert len(res.errors) == 1
+ err = res.errors[0]
+ assert err.domain == "bla"
+ assert err.message == "Unexpected error calling config validator: Broken"
+ assert err.config == {"value": 1}
diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py
index 7ce71defb7e..157bbf3bc23 100644
--- a/tests/helpers/test_config_entry_oauth2_flow.py
+++ b/tests/helpers/test_config_entry_oauth2_flow.py
@@ -6,7 +6,6 @@ import time
import pytest
from homeassistant import config_entries, data_entry_flow, setup
-from homeassistant.config import async_process_ha_core_config
from homeassistant.helpers import config_entry_oauth2_flow
from homeassistant.helpers.network import NoURLAvailableError
@@ -146,14 +145,14 @@ async def test_abort_if_no_url_available(hass, flow_handler, local_impl):
async def test_abort_if_oauth_error(
- hass, flow_handler, local_impl, aiohttp_client, aioclient_mock, current_request
+ hass,
+ flow_handler,
+ local_impl,
+ aiohttp_client,
+ aioclient_mock,
+ current_request_with_host,
):
"""Check bad oauth token."""
- await async_process_ha_core_config(
- hass,
- {"external_url": "https://example.com"},
- )
-
flow_handler.async_register_implementation(hass, local_impl)
config_entry_oauth2_flow.async_register_implementation(
hass, TEST_DOMAIN, MockOAuth2Implementation()
@@ -171,7 +170,13 @@ async def test_abort_if_oauth_error(
result["flow_id"], user_input={"implementation": TEST_DOMAIN}
)
- state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]})
+ state = config_entry_oauth2_flow._encode_jwt(
+ hass,
+ {
+ "flow_id": result["flow_id"],
+ "redirect_uri": "https://example.com/auth/external/callback",
+ },
+ )
assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP
assert result["url"] == (
@@ -203,10 +208,6 @@ async def test_abort_if_oauth_error(
async def test_step_discovery(hass, flow_handler, local_impl):
"""Check flow triggers from discovery."""
- await async_process_ha_core_config(
- hass,
- {"external_url": "https://example.com"},
- )
flow_handler.async_register_implementation(hass, local_impl)
config_entry_oauth2_flow.async_register_implementation(
hass, TEST_DOMAIN, MockOAuth2Implementation()
@@ -222,11 +223,6 @@ async def test_step_discovery(hass, flow_handler, local_impl):
async def test_abort_discovered_multiple(hass, flow_handler, local_impl):
"""Test if aborts when discovered multiple times."""
- await async_process_ha_core_config(
- hass,
- {"external_url": "https://example.com"},
- )
-
flow_handler.async_register_implementation(hass, local_impl)
config_entry_oauth2_flow.async_register_implementation(
hass, TEST_DOMAIN, MockOAuth2Implementation()
@@ -249,10 +245,6 @@ async def test_abort_discovered_multiple(hass, flow_handler, local_impl):
async def test_abort_discovered_existing_entries(hass, flow_handler, local_impl):
"""Test if abort discovery when entries exists."""
- await async_process_ha_core_config(
- hass,
- {"external_url": "https://example.com"},
- )
flow_handler.async_register_implementation(hass, local_impl)
config_entry_oauth2_flow.async_register_implementation(
hass, TEST_DOMAIN, MockOAuth2Implementation()
@@ -273,14 +265,14 @@ async def test_abort_discovered_existing_entries(hass, flow_handler, local_impl)
async def test_full_flow(
- hass, flow_handler, local_impl, aiohttp_client, aioclient_mock, current_request
+ hass,
+ flow_handler,
+ local_impl,
+ aiohttp_client,
+ aioclient_mock,
+ current_request_with_host,
):
"""Check full flow."""
- await async_process_ha_core_config(
- hass,
- {"external_url": "https://example.com"},
- )
-
flow_handler.async_register_implementation(hass, local_impl)
config_entry_oauth2_flow.async_register_implementation(
hass, TEST_DOMAIN, MockOAuth2Implementation()
@@ -298,7 +290,13 @@ async def test_full_flow(
result["flow_id"], user_input={"implementation": TEST_DOMAIN}
)
- state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]})
+ state = config_entry_oauth2_flow._encode_jwt(
+ hass,
+ {
+ "flow_id": result["flow_id"],
+ "redirect_uri": "https://example.com/auth/external/callback",
+ },
+ )
assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP
assert result["url"] == (
diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py
index c829d4413f0..5d907408b61 100644
--- a/tests/helpers/test_config_validation.py
+++ b/tests/helpers/test_config_validation.py
@@ -179,7 +179,12 @@ def test_entity_domain():
"""Test entity domain validation."""
schema = vol.Schema(cv.entity_domain("sensor"))
- for value in ("invalid_entity", "cover.demo"):
+ for value in (
+ "invalid_entity",
+ "cover.demo",
+ "cover.demo,sensor.another_entity",
+ "",
+ ):
with pytest.raises(vol.MultipleInvalid):
schema(value)
@@ -693,114 +698,6 @@ def test_deprecated_with_replacement_key(caplog, schema):
assert test_data == output
-def test_deprecated_with_invalidation_version(caplog, schema, version):
- """
- Test deprecation behaves correctly with only an invalidation_version.
-
- Expected behavior:
- - Outputs the appropriate deprecation warning if key is detected
- - Processes schema without changing any values
- - No warning or difference in output if key is not provided
- - Once the invalidation_version is crossed, raises vol.Invalid if key
- is detected
- """
- deprecated_schema = vol.All(
- cv.deprecated("mars", invalidation_version="1.0.0"), schema
- )
-
- message = (
- "The 'mars' option is deprecated, "
- "please remove it from your configuration. "
- "This option will become invalid in version 1.0.0"
- )
-
- test_data = {"mars": True}
- output = deprecated_schema(test_data.copy())
- assert len(caplog.records) == 1
- assert message in caplog.text
- assert test_data == output
-
- caplog.clear()
- assert len(caplog.records) == 0
-
- test_data = {"venus": False}
- output = deprecated_schema(test_data.copy())
- assert len(caplog.records) == 0
- assert test_data == output
-
- invalidated_schema = vol.All(
- cv.deprecated("mars", invalidation_version="0.1.0"), schema
- )
- test_data = {"mars": True}
- with pytest.raises(vol.MultipleInvalid) as exc_info:
- invalidated_schema(test_data)
- assert str(exc_info.value) == (
- "The 'mars' option is deprecated, "
- "please remove it from your configuration. This option became "
- "invalid in version 0.1.0"
- )
-
-
-def test_deprecated_with_replacement_key_and_invalidation_version(
- caplog, schema, version
-):
- """
- Test deprecation behaves with a replacement key & invalidation_version.
-
- Expected behavior:
- - Outputs the appropriate deprecation warning if key is detected
- - Processes schema moving the value from key to replacement_key
- - Processes schema changing nothing if only replacement_key provided
- - No warning if only replacement_key provided
- - No warning or difference in output if neither key nor
- replacement_key are provided
- - Once the invalidation_version is crossed, raises vol.Invalid if key
- is detected
- """
- deprecated_schema = vol.All(
- cv.deprecated("mars", replacement_key="jupiter", invalidation_version="1.0.0"),
- schema,
- )
-
- warning = (
- "The 'mars' option is deprecated, "
- "please replace it with 'jupiter'. This option will become "
- "invalid in version 1.0.0"
- )
-
- test_data = {"mars": True}
- output = deprecated_schema(test_data.copy())
- assert len(caplog.records) == 1
- assert warning in caplog.text
- assert {"jupiter": True} == output
-
- caplog.clear()
- assert len(caplog.records) == 0
-
- test_data = {"jupiter": True}
- output = deprecated_schema(test_data.copy())
- assert len(caplog.records) == 0
- assert test_data == output
-
- test_data = {"venus": True}
- output = deprecated_schema(test_data.copy())
- assert len(caplog.records) == 0
- assert test_data == output
-
- invalidated_schema = vol.All(
- cv.deprecated("mars", replacement_key="jupiter", invalidation_version="0.1.0"),
- schema,
- )
- test_data = {"mars": True}
- with pytest.raises(vol.MultipleInvalid) as exc_info:
- invalidated_schema(test_data)
- assert str(exc_info.value) == (
- "The 'mars' option is deprecated, "
- "please replace it with 'jupiter'. This option became "
- "invalid in version 0.1.0"
- )
-
-
def test_deprecated_with_default(caplog, schema):
"""
Test deprecation behaves correctly with a default value.
@@ -887,69 +784,6 @@ def test_deprecated_with_replacement_key_and_default(caplog, schema):
assert {"jupiter": True} == output
-def test_deprecated_with_replacement_key_invalidation_version_default(
- caplog, schema, version
-):
- """
- Test deprecation with a replacement key, invalidation_version & default.
-
- Expected behavior:
- - Outputs the appropriate deprecation warning if key is detected
- - Processes schema moving the value from key to replacement_key
- - Processes schema changing nothing if only replacement_key provided
- - No warning if only replacement_key provided
- - No warning if neither key nor replacement_key are provided
- - Adds replacement_key with default value in this case
- - Once the invalidation_version is crossed, raises vol.Invalid if key
- is detected
- """
- deprecated_schema = vol.All(
- cv.deprecated(
- "mars",
- replacement_key="jupiter",
- invalidation_version="1.0.0",
- default=False,
- ),
- schema,
- )
-
- test_data = {"mars": True}
- output = deprecated_schema(test_data.copy())
- assert len(caplog.records) == 1
- assert (
- "The 'mars' option is deprecated, "
- "please replace it with 'jupiter'. This option will become "
- "invalid in version 1.0.0"
- ) in caplog.text
- assert {"jupiter": True} == output
-
- caplog.clear()
- assert len(caplog.records) == 0
-
- test_data = {"jupiter": True}
- output = deprecated_schema(test_data.copy())
- assert len(caplog.records) == 0
- assert test_data == output
-
- test_data = {"venus": True}
- output = deprecated_schema(test_data.copy())
- assert len(caplog.records) == 0
- assert {"venus": True, "jupiter": False} == output
-
- invalidated_schema = vol.All(
- cv.deprecated("mars", replacement_key="jupiter", invalidation_version="0.1.0"),
- schema,
- )
- test_data = {"mars": True}
- with pytest.raises(vol.MultipleInvalid) as exc_info:
- invalidated_schema(test_data)
- assert str(exc_info.value) == (
- "The 'mars' option is deprecated, "
- "please replace it with 'jupiter'. This option became "
- "invalid in version 0.1.0"
- )
-
-
def test_deprecated_cant_find_module():
"""Test if the current module cannot be inspected."""
with patch("inspect.getmodule", return_value=None):
@@ -957,7 +791,6 @@ def test_deprecated_cant_find_module():
cv.deprecated(
"mars",
replacement_key="jupiter",
- invalidation_version="1.0.0",
default=False,
)
diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py
index 85ff693f261..7fa787e023e 100644
--- a/tests/helpers/test_device_registry.py
+++ b/tests/helpers/test_device_registry.py
@@ -152,6 +152,7 @@ async def test_loading_from_storage(hass, hass_storage):
"entry_type": "service",
"area_id": "12345A",
"name_by_user": "Test Friendly Name",
+ "disabled_by": "user",
}
],
"deleted_devices": [
@@ -180,6 +181,7 @@ async def test_loading_from_storage(hass, hass_storage):
assert entry.area_id == "12345A"
assert entry.name_by_user == "Test Friendly Name"
assert entry.entry_type == "service"
+ assert entry.disabled_by == "user"
assert isinstance(entry.config_entries, set)
assert isinstance(entry.connections, set)
assert isinstance(entry.identifiers, set)
@@ -445,6 +447,7 @@ async def test_loading_saving_data(hass, registry):
manufacturer="manufacturer",
model="light",
via_device=("hue", "0123"),
+ disabled_by="user",
)
orig_light2 = registry.async_get_or_create(
@@ -581,6 +584,7 @@ async def test_update(registry):
name_by_user="Test Friendly Name",
new_identifiers=new_identifiers,
via_device_id="98765B",
+ disabled_by="user",
)
assert mock_save.call_count == 1
@@ -591,6 +595,7 @@ async def test_update(registry):
assert updated_entry.name_by_user == "Test Friendly Name"
assert updated_entry.identifiers == new_identifiers
assert updated_entry.via_device_id == "98765B"
+ assert updated_entry.disabled_by == "user"
assert registry.async_get_device({("hue", "456")}, {}) is None
assert registry.async_get_device({("bla", "123")}, {}) is None
diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py
index 336329396cc..19af3715160 100644
--- a/tests/helpers/test_entity_registry.py
+++ b/tests/helpers/test_entity_registry.py
@@ -9,7 +9,12 @@ from homeassistant.helpers import entity_registry
import tests.async_mock
from tests.async_mock import patch
-from tests.common import MockConfigEntry, flush_store, mock_registry
+from tests.common import (
+ MockConfigEntry,
+ flush_store,
+ mock_device_registry,
+ mock_registry,
+)
YAML__OPEN_PATH = "homeassistant.util.yaml.loader.open"
@@ -677,3 +682,115 @@ async def test_async_get_device_class_lookup(hass):
("sensor", "battery"): "sensor.vacuum_battery",
},
}
+
+
+async def test_remove_device_removes_entities(hass, registry):
+ """Test that we remove entities tied to a device."""
+ device_registry = mock_device_registry(hass)
+ config_entry = MockConfigEntry(domain="light")
+
+ device_entry = device_registry.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={("mac", "12:34:56:AB:CD:EF")},
+ )
+
+ entry = registry.async_get_or_create(
+ "light",
+ "hue",
+ "5678",
+ config_entry=config_entry,
+ device_id=device_entry.id,
+ )
+
+ assert registry.async_is_registered(entry.entity_id)
+
+ device_registry.async_remove_device(device_entry.id)
+ await hass.async_block_till_done()
+
+ assert not registry.async_is_registered(entry.entity_id)
+
+
+async def test_disable_device_disables_entities(hass, registry):
+ """Test that we disable entities tied to a device."""
+ device_registry = mock_device_registry(hass)
+ config_entry = MockConfigEntry(domain="light")
+ config_entry.add_to_hass(hass)
+
+ device_entry = device_registry.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={("mac", "12:34:56:AB:CD:EF")},
+ )
+
+ entry1 = registry.async_get_or_create(
+ "light",
+ "hue",
+ "5678",
+ config_entry=config_entry,
+ device_id=device_entry.id,
+ )
+ entry2 = registry.async_get_or_create(
+ "light",
+ "hue",
+ "ABCD",
+ config_entry=config_entry,
+ device_id=device_entry.id,
+ disabled_by="user",
+ )
+
+ assert not entry1.disabled
+ assert entry2.disabled
+
+ device_registry.async_update_device(device_entry.id, disabled_by="user")
+ await hass.async_block_till_done()
+
+ entry1 = registry.async_get(entry1.entity_id)
+ assert entry1.disabled
+ assert entry1.disabled_by == "device"
+ entry2 = registry.async_get(entry2.entity_id)
+ assert entry2.disabled
+ assert entry2.disabled_by == "user"
+
+ device_registry.async_update_device(device_entry.id, disabled_by=None)
+ await hass.async_block_till_done()
+
+ entry1 = registry.async_get(entry1.entity_id)
+ assert not entry1.disabled
+ entry2 = registry.async_get(entry2.entity_id)
+ assert entry2.disabled
+ assert entry2.disabled_by == "user"
+
+
+async def test_disabled_entities_excluded_from_entity_list(hass, registry):
+ """Test that disabled entities are exclduded from async_entries_for_device."""
+ device_registry = mock_device_registry(hass)
+ config_entry = MockConfigEntry(domain="light")
+
+ device_entry = device_registry.async_get_or_create(
+ config_entry_id=config_entry.entry_id,
+ connections={("mac", "12:34:56:AB:CD:EF")},
+ )
+
+ entry1 = registry.async_get_or_create(
+ "light",
+ "hue",
+ "5678",
+ config_entry=config_entry,
+ device_id=device_entry.id,
+ )
+
+ entry2 = registry.async_get_or_create(
+ "light",
+ "hue",
+ "ABCD",
+ config_entry=config_entry,
+ device_id=device_entry.id,
+ disabled_by="user",
+ )
+
+ entries = entity_registry.async_entries_for_device(registry, device_entry.id)
+ assert entries == [entry1]
+
+ entries = entity_registry.async_entries_for_device(
+ registry, device_entry.id, include_disabled_entities=True
+ )
+ assert entries == [entry1, entry2]
diff --git a/tests/helpers/test_network.py b/tests/helpers/test_network.py
index 495c9d511bd..3330c42d9fc 100644
--- a/tests/helpers/test_network.py
+++ b/tests/helpers/test_network.py
@@ -12,6 +12,7 @@ from homeassistant.helpers.network import (
_get_internal_url,
_get_request_host,
get_url,
+ is_internal_request,
)
from tests.async_mock import Mock, patch
@@ -500,7 +501,7 @@ async def test_get_url(hass: HomeAssistant):
with patch(
"homeassistant.helpers.network._get_request_host", return_value="example.com"
- ), patch("homeassistant.helpers.network.current_request"):
+ ), patch("homeassistant.components.http.current_request"):
assert get_url(hass, require_current_request=True) == "https://example.com"
assert (
get_url(hass, require_current_request=True, require_ssl=True)
@@ -512,7 +513,7 @@ async def test_get_url(hass: HomeAssistant):
with patch(
"homeassistant.helpers.network._get_request_host", return_value="example.local"
- ), patch("homeassistant.helpers.network.current_request"):
+ ), patch("homeassistant.components.http.current_request"):
assert get_url(hass, require_current_request=True) == "http://example.local"
with pytest.raises(NoURLAvailableError):
@@ -533,7 +534,7 @@ async def test_get_request_host(hass: HomeAssistant):
with pytest.raises(NoURLAvailableError):
_get_request_host()
- with patch("homeassistant.helpers.network.current_request") as mock_request_context:
+ with patch("homeassistant.components.http.current_request") as mock_request_context:
mock_request = Mock()
mock_request.url = "http://example.com:8123/test/request"
mock_request_context.get = Mock(return_value=mock_request)
@@ -860,3 +861,40 @@ async def test_get_current_request_url_with_known_host(
"homeassistant.helpers.network._get_request_host", return_value="unknown.local"
), pytest.raises(NoURLAvailableError):
get_url(hass, require_current_request=True)
+
+
+async def test_is_internal_request(hass: HomeAssistant):
+ """Test if accessing an instance on its internal URL."""
+ # Test with internal URL: http://example.local:8123
+ await async_process_ha_core_config(
+ hass,
+ {"internal_url": "http://example.local:8123"},
+ )
+
+ assert hass.config.internal_url == "http://example.local:8123"
+ assert not is_internal_request(hass)
+
+ with patch(
+ "homeassistant.helpers.network._get_request_host", return_value="example.local"
+ ):
+ assert is_internal_request(hass)
+
+ with patch(
+ "homeassistant.helpers.network._get_request_host",
+ return_value="no_match.example.local",
+ ):
+ assert not is_internal_request(hass)
+
+ # Test with internal URL: http://192.168.0.1:8123
+ await async_process_ha_core_config(
+ hass,
+ {"internal_url": "http://192.168.0.1:8123"},
+ )
+
+ assert hass.config.internal_url == "http://192.168.0.1:8123"
+ assert not is_internal_request(hass)
+
+ with patch(
+ "homeassistant.helpers.network._get_request_host", return_value="192.168.0.1"
+ ):
+ assert is_internal_request(hass)
diff --git a/tests/helpers/test_placeholder.py b/tests/helpers/test_placeholder.py
deleted file mode 100644
index d5978cd465a..00000000000
--- a/tests/helpers/test_placeholder.py
+++ /dev/null
@@ -1,29 +0,0 @@
-"""Test placeholders."""
-import pytest
-
-from homeassistant.helpers import placeholder
-from homeassistant.util.yaml import Placeholder
-
-
-def test_extract_placeholders():
- """Test extracting placeholders from data."""
- assert placeholder.extract_placeholders(Placeholder("hello")) == {"hello"}
- assert placeholder.extract_placeholders(
- {"info": [1, Placeholder("hello"), 2, Placeholder("world")]}
- ) == {"hello", "world"}
-
-
-def test_substitute():
- """Test we can substitute."""
- assert placeholder.substitute(Placeholder("hello"), {"hello": 5}) == 5
-
- with pytest.raises(placeholder.UndefinedSubstitution):
- placeholder.substitute(Placeholder("hello"), {})
-
- assert (
- placeholder.substitute(
- {"info": [1, Placeholder("hello"), 2, Placeholder("world")]},
- {"hello": 5, "world": 10},
- )
- == {"info": [1, 5, 2, 10]}
- )
diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py
index 8f9e3cec36c..92666335f28 100644
--- a/tests/helpers/test_script.py
+++ b/tests/helpers/test_script.py
@@ -1338,6 +1338,18 @@ async def test_referenced_entities(hass):
"service": "test.script",
"data": {"entity_id": "{{ 'light.service_template' }}"},
},
+ {
+ "service": "test.script",
+ "entity_id": "light.direct_entity_referenced",
+ },
+ {
+ "service": "test.script",
+ "target": {"entity_id": "light.entity_in_target"},
+ },
+ {
+ "service": "test.script",
+ "data_template": {"entity_id": "light.entity_in_data_template"},
+ },
{
"condition": "state",
"entity_id": "sensor.condition",
@@ -1357,6 +1369,9 @@ async def test_referenced_entities(hass):
"light.service_list",
"sensor.condition",
"scene.hello",
+ "light.direct_entity_referenced",
+ "light.entity_in_target",
+ "light.entity_in_data_template",
}
# Test we cache results.
assert script_obj.referenced_entities is script_obj.referenced_entities
@@ -1374,12 +1389,36 @@ async def test_referenced_devices(hass):
"device_id": "condition-dev-id",
"domain": "switch",
},
+ {
+ "service": "test.script",
+ "data": {"device_id": "data-string-id"},
+ },
+ {
+ "service": "test.script",
+ "data_template": {"device_id": "data-template-string-id"},
+ },
+ {
+ "service": "test.script",
+ "target": {"device_id": "target-string-id"},
+ },
+ {
+ "service": "test.script",
+ "target": {"device_id": ["target-list-id-1", "target-list-id-2"]},
+ },
]
),
"Test Name",
"test_domain",
)
- assert script_obj.referenced_devices == {"script-dev-id", "condition-dev-id"}
+ assert script_obj.referenced_devices == {
+ "script-dev-id",
+ "condition-dev-id",
+ "data-string-id",
+ "data-template-string-id",
+ "target-string-id",
+ "target-list-id-1",
+ "target-list-id-2",
+ }
# Test we cache results.
assert script_obj.referenced_devices is script_obj.referenced_devices
diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py
new file mode 100644
index 00000000000..86ee6078e87
--- /dev/null
+++ b/tests/helpers/test_selector.py
@@ -0,0 +1,171 @@
+"""Test selectors."""
+import pytest
+import voluptuous as vol
+
+from homeassistant.helpers import selector
+
+
+@pytest.mark.parametrize(
+ "schema",
+ (
+ {"device": None},
+ {"entity": None},
+ ),
+)
+def test_valid_base_schema(schema):
+ """Test base schema validation."""
+ selector.validate_selector(schema)
+
+
+@pytest.mark.parametrize(
+ "schema",
+ (
+ {},
+ {"non_existing": {}},
+ # Two keys
+ {"device": {}, "entity": {}},
+ ),
+)
+def test_invalid_base_schema(schema):
+ """Test base schema validation."""
+ with pytest.raises(vol.Invalid):
+ selector.validate_selector(schema)
+
+
+def test_validate_selector():
+ """Test return is the same as input."""
+ schema = {"device": {"manufacturer": "mock-manuf", "model": "mock-model"}}
+ assert schema == selector.validate_selector(schema)
+
+
+@pytest.mark.parametrize(
+ "schema",
+ (
+ {},
+ {"integration": "zha"},
+ {"manufacturer": "mock-manuf"},
+ {"model": "mock-model"},
+ {"manufacturer": "mock-manuf", "model": "mock-model"},
+ {"integration": "zha", "manufacturer": "mock-manuf", "model": "mock-model"},
+ {"entity": {"device_class": "motion"}},
+ {
+ "integration": "zha",
+ "manufacturer": "mock-manuf",
+ "model": "mock-model",
+ "entity": {"domain": "binary_sensor", "device_class": "motion"},
+ },
+ ),
+)
+def test_device_selector_schema(schema):
+ """Test device selector."""
+ selector.validate_selector({"device": schema})
+
+
+@pytest.mark.parametrize(
+ "schema",
+ (
+ {},
+ {"integration": "zha"},
+ {"domain": "light"},
+ {"device_class": "motion"},
+ {"integration": "zha", "domain": "light"},
+ {"integration": "zha", "domain": "binary_sensor", "device_class": "motion"},
+ ),
+)
+def test_entity_selector_schema(schema):
+ """Test entity selector."""
+ selector.validate_selector({"entity": schema})
+
+
+@pytest.mark.parametrize(
+ "schema",
+ (
+ {},
+ {"entity": {}},
+ {"entity": {"domain": "light"}},
+ {"entity": {"domain": "binary_sensor", "device_class": "motion"}},
+ {
+ "entity": {
+ "domain": "binary_sensor",
+ "device_class": "motion",
+ "integration": "demo",
+ }
+ },
+ {"device": {"integration": "demo", "model": "mock-model"}},
+ {
+ "entity": {"domain": "binary_sensor", "device_class": "motion"},
+ "device": {"integration": "demo", "model": "mock-model"},
+ },
+ ),
+)
+def test_area_selector_schema(schema):
+ """Test area selector."""
+ selector.validate_selector({"area": schema})
+
+
+@pytest.mark.parametrize(
+ "schema",
+ (
+ {"min": 10, "max": 50},
+ {"min": -100, "max": 100, "step": 5},
+ {"min": -20, "max": -10, "mode": "box"},
+ {"min": 0, "max": 100, "unit_of_measurement": "seconds", "mode": "slider"},
+ {"min": 10, "max": 1000, "mode": "slider", "step": 0.5},
+ ),
+)
+def test_number_selector_schema(schema):
+ """Test number selector."""
+ selector.validate_selector({"number": schema})
+
+
+@pytest.mark.parametrize(
+ "schema",
+ ({},),
+)
+def test_boolean_selector_schema(schema):
+ """Test boolean selector."""
+ selector.validate_selector({"boolean": schema})
+
+
+@pytest.mark.parametrize(
+ "schema",
+ ({},),
+)
+def test_time_selector_schema(schema):
+ """Test time selector."""
+ selector.validate_selector({"time": schema})
+
+
+@pytest.mark.parametrize(
+ "schema",
+ (
+ {},
+ {"entity": {}},
+ {"entity": {"domain": "light"}},
+ {"entity": {"domain": "binary_sensor", "device_class": "motion"}},
+ {
+ "entity": {
+ "domain": "binary_sensor",
+ "device_class": "motion",
+ "integration": "demo",
+ }
+ },
+ {"device": {"integration": "demo", "model": "mock-model"}},
+ {
+ "entity": {"domain": "binary_sensor", "device_class": "motion"},
+ "device": {"integration": "demo", "model": "mock-model"},
+ },
+ ),
+)
+def test_target_selector_schema(schema):
+ """Test target selector."""
+ selector.validate_selector({"target": schema})
+
+
+@pytest.mark.parametrize(
+ "schema",
+ ({},),
+)
+def test_action_selector_schema(schema):
+ """Test action sequence selector."""
+ selector.validate_selector({"action": schema})
diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py
index 6f2cd4ba130..a75593ddd40 100644
--- a/tests/helpers/test_service.py
+++ b/tests/helpers/test_service.py
@@ -93,7 +93,7 @@ def area_mock(hass):
hass.states.async_set("light.Kitchen", STATE_OFF)
device_in_area = dev_reg.DeviceEntry(area_id="test-area")
- device_no_area = dev_reg.DeviceEntry()
+ device_no_area = dev_reg.DeviceEntry(id="device-no-area-id")
device_diff_area = dev_reg.DeviceEntry(area_id="diff-area")
mock_device_registry(
@@ -175,18 +175,25 @@ class TestServiceHelpers(unittest.TestCase):
"entity_id": "hello.world",
"data": {
"hello": "{{ 'goodbye' }}",
- "data": {"value": "{{ 'complex' }}", "simple": "simple"},
+ "effect": {"value": "{{ 'complex' }}", "simple": "simple"},
},
"data_template": {"list": ["{{ 'list' }}", "2"]},
+ "target": {"area_id": "test-area-id", "entity_id": "will.be_overridden"},
}
service.call_from_config(self.hass, config)
self.hass.block_till_done()
- assert self.calls[0].data["hello"] == "goodbye"
- assert self.calls[0].data["data"]["value"] == "complex"
- assert self.calls[0].data["data"]["simple"] == "simple"
- assert self.calls[0].data["list"][0] == "list"
+ assert dict(self.calls[0].data) == {
+ "hello": "goodbye",
+ "effect": {
+ "value": "complex",
+ "simple": "simple",
+ },
+ "list": ["list", "2"],
+ "entity_id": ["hello.world"],
+ "area_id": ["test-area-id"],
+ }
def test_service_template_service_call(self):
"""Test legacy service_template call with templating."""
@@ -940,3 +947,53 @@ async def test_extract_from_service_area_id(hass, area_mock):
"light.diff_area",
"light.in_area",
]
+
+ call = ha.ServiceCall(
+ "light",
+ "turn_on",
+ {"area_id": ["test-area", "diff-area"], "device_id": "device-no-area-id"},
+ )
+ extracted = await service.async_extract_entities(hass, entities, call)
+ assert len(extracted) == 3
+ assert sorted(ent.entity_id for ent in extracted) == [
+ "light.diff_area",
+ "light.in_area",
+ "light.no_area",
+ ]
+
+
+async def test_entity_service_call_warn_referenced(hass, caplog):
+ """Test we only warn for referenced entities in entity_service_call."""
+ call = ha.ServiceCall(
+ "light",
+ "turn_on",
+ {
+ "area_id": "non-existent-area",
+ "entity_id": "non.existent",
+ "device_id": "non-existent-device",
+ },
+ )
+ await service.entity_service_call(hass, {}, "", call)
+ assert (
+ "Unable to find referenced areas non-existent-area, devices non-existent-device, entities non.existent"
+ in caplog.text
+ )
+
+
+async def test_async_extract_entities_warn_referenced(hass, caplog):
+ """Test we only warn for referenced entities in async_extract_entities."""
+ call = ha.ServiceCall(
+ "light",
+ "turn_on",
+ {
+ "area_id": "non-existent-area",
+ "entity_id": "non.existent",
+ "device_id": "non-existent-device",
+ },
+ )
+ extracted = await service.async_extract_entities(hass, {}, call)
+ assert len(extracted) == 0
+ assert (
+ "Unable to find referenced areas non-existent-area, devices non-existent-device, entities non.existent"
+ in caplog.text
+ )
diff --git a/tests/test_config.py b/tests/test_config.py
index bfda156f2b7..931b672d01b 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -1116,7 +1116,7 @@ async def test_component_config_exceptions(hass, caplog):
("non_existing", vol.Schema({"zone": int}), None),
("zone", vol.Schema({}), None),
("plex", vol.Schema(vol.All({"plex": {"host": str}})), "dict"),
- ("openuv", cv.deprecated("openuv", invalidation_version="0.115"), None),
+ ("openuv", cv.deprecated("openuv"), None),
],
)
def test_identify_config_schema(domain, schema, expected):
diff --git a/tests/test_core.py b/tests/test_core.py
index f08de049efa..541f75e6343 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -6,7 +6,6 @@ import functools
import logging
import os
from tempfile import TemporaryDirectory
-import unittest
import pytest
import pytz
@@ -33,12 +32,16 @@ from homeassistant.const import (
__version__,
)
import homeassistant.core as ha
-from homeassistant.exceptions import InvalidEntityFormatError, InvalidStateError
+from homeassistant.exceptions import (
+ InvalidEntityFormatError,
+ InvalidStateError,
+ ServiceNotFound,
+)
import homeassistant.util.dt as dt_util
from homeassistant.util.unit_system import METRIC_SYSTEM
from tests.async_mock import MagicMock, Mock, PropertyMock, patch
-from tests.common import async_mock_service, get_test_home_assistant
+from tests.common import async_capture_events, async_mock_service
PST = pytz.timezone("America/Los_Angeles")
@@ -151,22 +154,14 @@ def test_async_run_hass_job_delegates_non_async():
assert len(hass.async_add_hass_job.mock_calls) == 1
-def test_stage_shutdown():
+async def test_stage_shutdown(hass):
"""Simulate a shutdown, test calling stuff."""
- hass = get_test_home_assistant()
- test_stop = []
- test_final_write = []
- test_close = []
- test_all = []
+ test_stop = async_capture_events(hass, EVENT_HOMEASSISTANT_STOP)
+ test_final_write = async_capture_events(hass, EVENT_HOMEASSISTANT_FINAL_WRITE)
+ test_close = async_capture_events(hass, EVENT_HOMEASSISTANT_CLOSE)
+ test_all = async_capture_events(hass, MATCH_ALL)
- hass.bus.listen(EVENT_HOMEASSISTANT_STOP, lambda event: test_stop.append(event))
- hass.bus.listen(
- EVENT_HOMEASSISTANT_FINAL_WRITE, lambda event: test_final_write.append(event)
- )
- hass.bus.listen(EVENT_HOMEASSISTANT_CLOSE, lambda event: test_close.append(event))
- hass.bus.listen("*", lambda event: test_all.append(event))
-
- hass.stop()
+ await hass.async_stop()
assert len(test_stop) == 1
assert len(test_close) == 1
@@ -341,147 +336,139 @@ def test_state_as_dict():
assert state.as_dict() is state.as_dict()
-class TestEventBus(unittest.TestCase):
- """Test EventBus methods."""
+async def test_eventbus_add_remove_listener(hass):
+ """Test remove_listener method."""
+ old_count = len(hass.bus.async_listeners())
- # pylint: disable=invalid-name
- def setUp(self):
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- self.bus = self.hass.bus
+ def listener(_):
+ pass
- # pylint: disable=invalid-name
- def tearDown(self):
- """Stop down stuff we started."""
- self.hass.stop()
+ unsub = hass.bus.async_listen("test", listener)
- def test_add_remove_listener(self):
- """Test remove_listener method."""
- self.hass.allow_pool = False
- old_count = len(self.bus.listeners)
+ assert old_count + 1 == len(hass.bus.async_listeners())
- def listener(_):
- pass
+ # Remove listener
+ unsub()
+ assert old_count == len(hass.bus.async_listeners())
- unsub = self.bus.listen("test", listener)
+ # Should do nothing now
+ unsub()
- assert old_count + 1 == len(self.bus.listeners)
- # Remove listener
- unsub()
- assert old_count == len(self.bus.listeners)
+async def test_eventbus_unsubscribe_listener(hass):
+ """Test unsubscribe listener from returned function."""
+ calls = []
- # Should do nothing now
- unsub()
+ @ha.callback
+ def listener(event):
+ """Mock listener."""
+ calls.append(event)
- def test_unsubscribe_listener(self):
- """Test unsubscribe listener from returned function."""
- calls = []
+ unsub = hass.bus.async_listen("test", listener)
- @ha.callback
- def listener(event):
- """Mock listener."""
- calls.append(event)
+ hass.bus.async_fire("test")
+ await hass.async_block_till_done()
- unsub = self.bus.listen("test", listener)
+ assert len(calls) == 1
- self.bus.fire("test")
- self.hass.block_till_done()
+ unsub()
- assert len(calls) == 1
+ hass.bus.async_fire("event")
+ await hass.async_block_till_done()
- unsub()
+ assert len(calls) == 1
- self.bus.fire("event")
- self.hass.block_till_done()
- assert len(calls) == 1
+async def test_eventbus_listen_once_event_with_callback(hass):
+ """Test listen_once_event method."""
+ runs = []
- def test_listen_once_event_with_callback(self):
- """Test listen_once_event method."""
- runs = []
+ @ha.callback
+ def event_handler(event):
+ runs.append(event)
- @ha.callback
- def event_handler(event):
- runs.append(event)
+ hass.bus.async_listen_once("test_event", event_handler)
- self.bus.listen_once("test_event", event_handler)
+ hass.bus.async_fire("test_event")
+ # Second time it should not increase runs
+ hass.bus.async_fire("test_event")
- self.bus.fire("test_event")
- # Second time it should not increase runs
- self.bus.fire("test_event")
+ await hass.async_block_till_done()
+ assert len(runs) == 1
- self.hass.block_till_done()
- assert len(runs) == 1
- def test_listen_once_event_with_coroutine(self):
- """Test listen_once_event method."""
- runs = []
+async def test_eventbus_listen_once_event_with_coroutine(hass):
+ """Test listen_once_event method."""
+ runs = []
- async def event_handler(event):
- runs.append(event)
+ async def event_handler(event):
+ runs.append(event)
- self.bus.listen_once("test_event", event_handler)
+ hass.bus.async_listen_once("test_event", event_handler)
- self.bus.fire("test_event")
- # Second time it should not increase runs
- self.bus.fire("test_event")
+ hass.bus.async_fire("test_event")
+ # Second time it should not increase runs
+ hass.bus.async_fire("test_event")
- self.hass.block_till_done()
- assert len(runs) == 1
+ await hass.async_block_till_done()
+ assert len(runs) == 1
- def test_listen_once_event_with_thread(self):
- """Test listen_once_event method."""
- runs = []
- def event_handler(event):
- runs.append(event)
+async def test_eventbus_listen_once_event_with_thread(hass):
+ """Test listen_once_event method."""
+ runs = []
- self.bus.listen_once("test_event", event_handler)
+ def event_handler(event):
+ runs.append(event)
- self.bus.fire("test_event")
- # Second time it should not increase runs
- self.bus.fire("test_event")
+ hass.bus.async_listen_once("test_event", event_handler)
- self.hass.block_till_done()
- assert len(runs) == 1
+ hass.bus.async_fire("test_event")
+ # Second time it should not increase runs
+ hass.bus.async_fire("test_event")
- def test_thread_event_listener(self):
- """Test thread event listener."""
- thread_calls = []
+ await hass.async_block_till_done()
+ assert len(runs) == 1
- def thread_listener(event):
- thread_calls.append(event)
- self.bus.listen("test_thread", thread_listener)
- self.bus.fire("test_thread")
- self.hass.block_till_done()
- assert len(thread_calls) == 1
+async def test_eventbus_thread_event_listener(hass):
+ """Test thread event listener."""
+ thread_calls = []
- def test_callback_event_listener(self):
- """Test callback event listener."""
- callback_calls = []
+ def thread_listener(event):
+ thread_calls.append(event)
- @ha.callback
- def callback_listener(event):
- callback_calls.append(event)
+ hass.bus.async_listen("test_thread", thread_listener)
+ hass.bus.async_fire("test_thread")
+ await hass.async_block_till_done()
+ assert len(thread_calls) == 1
- self.bus.listen("test_callback", callback_listener)
- self.bus.fire("test_callback")
- self.hass.block_till_done()
- assert len(callback_calls) == 1
- def test_coroutine_event_listener(self):
- """Test coroutine event listener."""
- coroutine_calls = []
+async def test_eventbus_callback_event_listener(hass):
+ """Test callback event listener."""
+ callback_calls = []
- async def coroutine_listener(event):
- coroutine_calls.append(event)
+ @ha.callback
+ def callback_listener(event):
+ callback_calls.append(event)
- self.bus.listen("test_coroutine", coroutine_listener)
- self.bus.fire("test_coroutine")
- self.hass.block_till_done()
- assert len(coroutine_calls) == 1
+ hass.bus.async_listen("test_callback", callback_listener)
+ hass.bus.async_fire("test_callback")
+ await hass.async_block_till_done()
+ assert len(callback_calls) == 1
+
+
+async def test_eventbus_coroutine_event_listener(hass):
+ """Test coroutine event listener."""
+ coroutine_calls = []
+
+ async def coroutine_listener(event):
+ coroutine_calls.append(event)
+
+ hass.bus.async_listen("test_coroutine", coroutine_listener)
+ hass.bus.async_fire("test_coroutine")
+ await hass.async_block_till_done()
+ assert len(coroutine_calls) == 1
def test_state_init():
@@ -562,117 +549,92 @@ def test_state_repr():
)
-class TestStateMachine(unittest.TestCase):
- """Test State machine methods."""
+async def test_statemachine_is_state(hass):
+ """Test is_state method."""
+ hass.states.async_set("light.bowl", "on", {})
+ assert hass.states.is_state("light.Bowl", "on")
+ assert not hass.states.is_state("light.Bowl", "off")
+ assert not hass.states.is_state("light.Non_existing", "on")
- # pylint: disable=invalid-name
- def setUp(self):
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- self.states = self.hass.states
- self.states.set("light.Bowl", "on")
- self.states.set("switch.AC", "off")
- # pylint: disable=invalid-name
- def tearDown(self):
- """Stop down stuff we started."""
- self.hass.stop()
+async def test_statemachine_entity_ids(hass):
+ """Test get_entity_ids method."""
+ hass.states.async_set("light.bowl", "on", {})
+ hass.states.async_set("SWITCH.AC", "off", {})
+ ent_ids = hass.states.async_entity_ids()
+ assert len(ent_ids) == 2
+ assert "light.bowl" in ent_ids
+ assert "switch.ac" in ent_ids
- def test_is_state(self):
- """Test is_state method."""
- assert self.states.is_state("light.Bowl", "on")
- assert not self.states.is_state("light.Bowl", "off")
- assert not self.states.is_state("light.Non_existing", "on")
+ ent_ids = hass.states.async_entity_ids("light")
+ assert len(ent_ids) == 1
+ assert "light.bowl" in ent_ids
- def test_entity_ids(self):
- """Test get_entity_ids method."""
- ent_ids = self.states.entity_ids()
- assert len(ent_ids) == 2
- assert "light.bowl" in ent_ids
- assert "switch.ac" in ent_ids
+ states = sorted(state.entity_id for state in hass.states.async_all())
+ assert states == ["light.bowl", "switch.ac"]
- ent_ids = self.states.entity_ids("light")
- assert len(ent_ids) == 1
- assert "light.bowl" in ent_ids
- def test_all(self):
- """Test everything."""
- states = sorted(state.entity_id for state in self.states.all())
- assert ["light.bowl", "switch.ac"] == states
+async def test_statemachine_remove(hass):
+ """Test remove method."""
+ hass.states.async_set("light.bowl", "on", {})
+ events = async_capture_events(hass, EVENT_STATE_CHANGED)
- def test_remove(self):
- """Test remove method."""
- events = []
+ assert "light.bowl" in hass.states.async_entity_ids()
+ assert hass.states.async_remove("light.bowl")
+ await hass.async_block_till_done()
- @ha.callback
- def callback(event):
- events.append(event)
+ assert "light.bowl" not in hass.states.async_entity_ids()
+ assert len(events) == 1
+ assert events[0].data.get("entity_id") == "light.bowl"
+ assert events[0].data.get("old_state") is not None
+ assert events[0].data["old_state"].entity_id == "light.bowl"
+ assert events[0].data.get("new_state") is None
- self.hass.bus.listen(EVENT_STATE_CHANGED, callback)
+ # If it does not exist, we should get False
+ assert not hass.states.async_remove("light.Bowl")
+ await hass.async_block_till_done()
+ assert len(events) == 1
- assert "light.bowl" in self.states.entity_ids()
- assert self.states.remove("light.bowl")
- self.hass.block_till_done()
- assert "light.bowl" not in self.states.entity_ids()
- assert len(events) == 1
- assert events[0].data.get("entity_id") == "light.bowl"
- assert events[0].data.get("old_state") is not None
- assert events[0].data["old_state"].entity_id == "light.bowl"
- assert events[0].data.get("new_state") is None
+async def test_statemachine_case_insensitivty(hass):
+ """Test insensitivty."""
+ events = async_capture_events(hass, EVENT_STATE_CHANGED)
- # If it does not exist, we should get False
- assert not self.states.remove("light.Bowl")
- self.hass.block_till_done()
- assert len(events) == 1
+ hass.states.async_set("light.BOWL", "off")
+ await hass.async_block_till_done()
- def test_case_insensitivty(self):
- """Test insensitivty."""
- runs = []
+ assert hass.states.is_state("light.bowl", "off")
+ assert len(events) == 1
- @ha.callback
- def callback(event):
- runs.append(event)
- self.hass.bus.listen(EVENT_STATE_CHANGED, callback)
+async def test_statemachine_last_changed_not_updated_on_same_state(hass):
+ """Test to not update the existing, same state."""
+ hass.states.async_set("light.bowl", "on", {})
+ state = hass.states.get("light.Bowl")
- self.states.set("light.BOWL", "off")
- self.hass.block_till_done()
+ future = dt_util.utcnow() + timedelta(hours=10)
- assert self.states.is_state("light.bowl", "off")
- assert len(runs) == 1
+ with patch("homeassistant.util.dt.utcnow", return_value=future):
+ hass.states.async_set("light.Bowl", "on", {"attr": "triggers_change"})
+ await hass.async_block_till_done()
- def test_last_changed_not_updated_on_same_state(self):
- """Test to not update the existing, same state."""
- state = self.states.get("light.Bowl")
+ state2 = hass.states.get("light.Bowl")
+ assert state2 is not None
+ assert state.last_changed == state2.last_changed
- future = dt_util.utcnow() + timedelta(hours=10)
- with patch("homeassistant.util.dt.utcnow", return_value=future):
- self.states.set("light.Bowl", "on", {"attr": "triggers_change"})
- self.hass.block_till_done()
+async def test_statemachine_force_update(hass):
+ """Test force update option."""
+ hass.states.async_set("light.bowl", "on", {})
+ events = async_capture_events(hass, EVENT_STATE_CHANGED)
- state2 = self.states.get("light.Bowl")
- assert state2 is not None
- assert state.last_changed == state2.last_changed
+ hass.states.async_set("light.bowl", "on")
+ await hass.async_block_till_done()
+ assert len(events) == 0
- def test_force_update(self):
- """Test force update option."""
- events = []
-
- @ha.callback
- def callback(event):
- events.append(event)
-
- self.hass.bus.listen(EVENT_STATE_CHANGED, callback)
-
- self.states.set("light.bowl", "on")
- self.hass.block_till_done()
- assert len(events) == 0
-
- self.states.set("light.bowl", "on", None, True)
- self.hass.block_till_done()
- assert len(events) == 1
+ hass.states.async_set("light.bowl", "on", None, True)
+ await hass.async_block_till_done()
+ assert len(events) == 1
def test_service_call_repr():
@@ -687,202 +649,154 @@ def test_service_call_repr():
)
-class TestServiceRegistry(unittest.TestCase):
- """Test ServicerRegistry methods."""
+async def test_serviceregistry_has_service(hass):
+ """Test has_service method."""
+ hass.services.async_register("test_domain", "test_service", lambda call: None)
+ assert len(hass.services.async_services()) == 1
+ assert hass.services.has_service("tesT_domaiN", "tesT_servicE")
+ assert not hass.services.has_service("test_domain", "non_existing")
+ assert not hass.services.has_service("non_existing", "test_service")
- # pylint: disable=invalid-name
- def setUp(self):
- """Set up things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- self.services = self.hass.services
- @ha.callback
- def mock_service(call):
- pass
+async def test_serviceregistry_call_with_blocking_done_in_time(hass):
+ """Test call with blocking."""
+ registered_events = async_capture_events(hass, EVENT_SERVICE_REGISTERED)
+ calls = async_mock_service(hass, "test_domain", "register_calls")
+ await hass.async_block_till_done()
- self.services.register("Test_Domain", "TEST_SERVICE", mock_service)
+ assert len(registered_events) == 1
+ assert registered_events[0].data["domain"] == "test_domain"
+ assert registered_events[0].data["service"] == "register_calls"
- self.calls_register = []
+ assert await hass.services.async_call(
+ "test_domain", "REGISTER_CALLS", blocking=True
+ )
+ assert len(calls) == 1
- @ha.callback
- def mock_event_register(event):
- """Mock register event."""
- self.calls_register.append(event)
- self.hass.bus.listen(EVENT_SERVICE_REGISTERED, mock_event_register)
+async def test_serviceregistry_call_non_existing_with_blocking(hass):
+ """Test non-existing with blocking."""
+ with pytest.raises(ha.ServiceNotFound):
+ await hass.services.async_call("test_domain", "i_do_not_exist", blocking=True)
- # pylint: disable=invalid-name
- def tearDown(self):
- """Stop down stuff we started."""
- self.hass.stop()
- def test_has_service(self):
- """Test has_service method."""
- assert self.services.has_service("tesT_domaiN", "tesT_servicE")
- assert not self.services.has_service("test_domain", "non_existing")
- assert not self.services.has_service("non_existing", "test_service")
+async def test_serviceregistry_async_service(hass):
+ """Test registering and calling an async service."""
+ calls = []
- def test_services(self):
- """Test services."""
- assert len(self.services.services) == 1
+ async def service_handler(call):
+ """Service handler coroutine."""
+ calls.append(call)
- def test_call_with_blocking_done_in_time(self):
- """Test call with blocking."""
- calls = []
+ hass.services.async_register("test_domain", "register_calls", service_handler)
- @ha.callback
- def service_handler(call):
- """Service handler."""
- calls.append(call)
+ assert await hass.services.async_call(
+ "test_domain", "REGISTER_CALLS", blocking=True
+ )
+ assert len(calls) == 1
- self.services.register("test_domain", "register_calls", service_handler)
- self.hass.block_till_done()
- assert len(self.calls_register) == 1
- assert self.calls_register[-1].data["domain"] == "test_domain"
- assert self.calls_register[-1].data["service"] == "register_calls"
+async def test_serviceregistry_async_service_partial(hass):
+ """Test registering and calling an wrapped async service."""
+ calls = []
- assert self.services.call("test_domain", "REGISTER_CALLS", blocking=True)
- assert len(calls) == 1
+ async def service_handler(call):
+ """Service handler coroutine."""
+ calls.append(call)
- def test_call_non_existing_with_blocking(self):
- """Test non-existing with blocking."""
- with pytest.raises(ha.ServiceNotFound):
- self.services.call("test_domain", "i_do_not_exist", blocking=True)
+ hass.services.async_register(
+ "test_domain", "register_calls", functools.partial(service_handler)
+ )
+ await hass.async_block_till_done()
- def test_async_service(self):
- """Test registering and calling an async service."""
- calls = []
+ assert await hass.services.async_call(
+ "test_domain", "REGISTER_CALLS", blocking=True
+ )
+ assert len(calls) == 1
- async def service_handler(call):
- """Service handler coroutine."""
- calls.append(call)
- self.services.register("test_domain", "register_calls", service_handler)
- self.hass.block_till_done()
+async def test_serviceregistry_callback_service(hass):
+ """Test registering and calling an async service."""
+ calls = []
- assert len(self.calls_register) == 1
- assert self.calls_register[-1].data["domain"] == "test_domain"
- assert self.calls_register[-1].data["service"] == "register_calls"
+ @ha.callback
+ def service_handler(call):
+ """Service handler coroutine."""
+ calls.append(call)
- assert self.services.call("test_domain", "REGISTER_CALLS", blocking=True)
- self.hass.block_till_done()
- assert len(calls) == 1
+ hass.services.async_register("test_domain", "register_calls", service_handler)
- def test_async_service_partial(self):
- """Test registering and calling an wrapped async service."""
- calls = []
+ assert await hass.services.async_call(
+ "test_domain", "REGISTER_CALLS", blocking=True
+ )
+ assert len(calls) == 1
- async def service_handler(call):
- """Service handler coroutine."""
- calls.append(call)
- self.services.register(
- "test_domain", "register_calls", functools.partial(service_handler)
+async def test_serviceregistry_remove_service(hass):
+ """Test remove service."""
+ calls_remove = async_capture_events(hass, EVENT_SERVICE_REMOVED)
+
+ hass.services.async_register("test_domain", "test_service", lambda call: None)
+ assert hass.services.has_service("test_Domain", "test_Service")
+
+ hass.services.async_remove("test_Domain", "test_Service")
+ await hass.async_block_till_done()
+
+ assert not hass.services.has_service("test_Domain", "test_Service")
+ assert len(calls_remove) == 1
+ assert calls_remove[-1].data["domain"] == "test_domain"
+ assert calls_remove[-1].data["service"] == "test_service"
+
+
+async def test_serviceregistry_service_that_not_exists(hass):
+ """Test remove service that not exists."""
+ calls_remove = async_capture_events(hass, EVENT_SERVICE_REMOVED)
+ assert not hass.services.has_service("test_xxx", "test_yyy")
+ hass.services.async_remove("test_xxx", "test_yyy")
+ await hass.async_block_till_done()
+ assert len(calls_remove) == 0
+
+ with pytest.raises(ServiceNotFound):
+ await hass.services.async_call("test_do_not", "exist", {})
+
+
+async def test_serviceregistry_async_service_raise_exception(hass):
+ """Test registering and calling an async service raise exception."""
+
+ async def service_handler(_):
+ """Service handler coroutine."""
+ raise ValueError
+
+ hass.services.async_register("test_domain", "register_calls", service_handler)
+
+ with pytest.raises(ValueError):
+ assert await hass.services.async_call(
+ "test_domain", "REGISTER_CALLS", blocking=True
)
- self.hass.block_till_done()
- assert len(self.calls_register) == 1
- assert self.calls_register[-1].data["domain"] == "test_domain"
- assert self.calls_register[-1].data["service"] == "register_calls"
+ # Non-blocking service call never throw exception
+ await hass.services.async_call("test_domain", "REGISTER_CALLS", blocking=False)
+ await hass.async_block_till_done()
- assert self.services.call("test_domain", "REGISTER_CALLS", blocking=True)
- self.hass.block_till_done()
- assert len(calls) == 1
- def test_callback_service(self):
- """Test registering and calling an async service."""
- calls = []
+async def test_serviceregistry_callback_service_raise_exception(hass):
+ """Test registering and calling an callback service raise exception."""
- @ha.callback
- def service_handler(call):
- """Service handler coroutine."""
- calls.append(call)
+ @ha.callback
+ def service_handler(_):
+ """Service handler coroutine."""
+ raise ValueError
- self.services.register("test_domain", "register_calls", service_handler)
- self.hass.block_till_done()
+ hass.services.async_register("test_domain", "register_calls", service_handler)
- assert len(self.calls_register) == 1
- assert self.calls_register[-1].data["domain"] == "test_domain"
- assert self.calls_register[-1].data["service"] == "register_calls"
+ with pytest.raises(ValueError):
+ assert await hass.services.async_call(
+ "test_domain", "REGISTER_CALLS", blocking=True
+ )
- assert self.services.call("test_domain", "REGISTER_CALLS", blocking=True)
- self.hass.block_till_done()
- assert len(calls) == 1
-
- def test_remove_service(self):
- """Test remove service."""
- calls_remove = []
-
- @ha.callback
- def mock_event_remove(event):
- """Mock register event."""
- calls_remove.append(event)
-
- self.hass.bus.listen(EVENT_SERVICE_REMOVED, mock_event_remove)
-
- assert self.services.has_service("test_Domain", "test_Service")
-
- self.services.remove("test_Domain", "test_Service")
- self.hass.block_till_done()
-
- assert not self.services.has_service("test_Domain", "test_Service")
- assert len(calls_remove) == 1
- assert calls_remove[-1].data["domain"] == "test_domain"
- assert calls_remove[-1].data["service"] == "test_service"
-
- def test_remove_service_that_not_exists(self):
- """Test remove service that not exists."""
- calls_remove = []
-
- @ha.callback
- def mock_event_remove(event):
- """Mock register event."""
- calls_remove.append(event)
-
- self.hass.bus.listen(EVENT_SERVICE_REMOVED, mock_event_remove)
-
- assert not self.services.has_service("test_xxx", "test_yyy")
- self.services.remove("test_xxx", "test_yyy")
- self.hass.block_till_done()
- assert len(calls_remove) == 0
-
- def test_async_service_raise_exception(self):
- """Test registering and calling an async service raise exception."""
-
- async def service_handler(_):
- """Service handler coroutine."""
- raise ValueError
-
- self.services.register("test_domain", "register_calls", service_handler)
- self.hass.block_till_done()
-
- with pytest.raises(ValueError):
- assert self.services.call("test_domain", "REGISTER_CALLS", blocking=True)
- self.hass.block_till_done()
-
- # Non-blocking service call never throw exception
- self.services.call("test_domain", "REGISTER_CALLS", blocking=False)
- self.hass.block_till_done()
-
- def test_callback_service_raise_exception(self):
- """Test registering and calling an callback service raise exception."""
-
- @ha.callback
- def service_handler(_):
- """Service handler coroutine."""
- raise ValueError
-
- self.services.register("test_domain", "register_calls", service_handler)
- self.hass.block_till_done()
-
- with pytest.raises(ValueError):
- assert self.services.call("test_domain", "REGISTER_CALLS", blocking=True)
- self.hass.block_till_done()
-
- # Non-blocking service call never throw exception
- self.services.call("test_domain", "REGISTER_CALLS", blocking=False)
- self.hass.block_till_done()
+ # Non-blocking service call never throw exception
+ await hass.services.async_call("test_domain", "REGISTER_CALLS", blocking=False)
+ await hass.async_block_till_done()
def test_config_defaults():
diff --git a/tests/test_loader.py b/tests/test_loader.py
index 71a373a579d..c05240893de 100644
--- a/tests/test_loader.py
+++ b/tests/test_loader.py
@@ -1,9 +1,9 @@
"""Test to verify that we can load components."""
import pytest
+from homeassistant import core, loader
from homeassistant.components import http, hue
from homeassistant.components.hue import light as hue_light
-import homeassistant.loader as loader
from tests.async_mock import ANY, patch
from tests.common import MockModule, async_mock_service, mock_integration
@@ -83,6 +83,7 @@ async def test_helpers_wrapper(hass):
result = []
+ @core.callback
def discovery_callback(service, discovered):
"""Handle discovery callback."""
result.append(discovered)
@@ -149,7 +150,7 @@ async def test_get_integration_legacy(hass):
assert integration.get_platform("switch") is not None
-async def test_get_integration_custom_component(hass):
+async def test_get_integration_custom_component(hass, enable_custom_integrations):
"""Test resolving integration."""
integration = await loader.async_get_integration(hass, "test_package")
print(integration)
@@ -293,7 +294,7 @@ def _get_test_integration_with_zeroconf_matcher(hass, name, config_flow):
)
-async def test_get_custom_components(hass):
+async def test_get_custom_components(hass, enable_custom_integrations):
"""Verify that custom components are cached."""
test_1_integration = _get_test_integration(hass, "test_1", False)
test_2_integration = _get_test_integration(hass, "test_2", True)
diff --git a/tests/testing_config/blueprints/automation/in_folder/in_folder_blueprint.yaml b/tests/testing_config/blueprints/automation/in_folder/in_folder_blueprint.yaml
index c869e30c41e..baaaf3df1ea 100644
--- a/tests/testing_config/blueprints/automation/in_folder/in_folder_blueprint.yaml
+++ b/tests/testing_config/blueprints/automation/in_folder/in_folder_blueprint.yaml
@@ -4,5 +4,5 @@ blueprint:
input:
trigger:
action:
-trigger: !placeholder trigger
-action: !placeholder action
+trigger: !input trigger
+action: !input action
diff --git a/tests/testing_config/blueprints/automation/test_event_service.yaml b/tests/testing_config/blueprints/automation/test_event_service.yaml
index 0e9479cd8c3..ab067b004ac 100644
--- a/tests/testing_config/blueprints/automation/test_event_service.yaml
+++ b/tests/testing_config/blueprints/automation/test_event_service.yaml
@@ -6,6 +6,7 @@ blueprint:
service_to_call:
trigger:
platform: event
- event_type: !placeholder trigger_event
+ event_type: !input trigger_event
action:
- service: !placeholder service_to_call
+ service: !input service_to_call
+ entity_id: light.kitchen
diff --git a/tests/util/yaml/__init__.py b/tests/util/yaml/__init__.py
new file mode 100644
index 00000000000..5b5c1b8f15a
--- /dev/null
+++ b/tests/util/yaml/__init__.py
@@ -0,0 +1 @@
+"""Tests for YAML util."""
diff --git a/tests/util/test_yaml.py b/tests/util/yaml/test_init.py
similarity index 97%
rename from tests/util/test_yaml.py
rename to tests/util/yaml/test_init.py
index 2e9d1b471ac..1c5b9bd9fd8 100644
--- a/tests/util/test_yaml.py
+++ b/tests/util/yaml/test_init.py
@@ -463,18 +463,18 @@ def test_duplicate_key(caplog):
assert "contains duplicate key" in caplog.text
-def test_placeholder_class():
- """Test placeholder class."""
- placeholder = yaml_loader.Placeholder("hello")
- placeholder2 = yaml_loader.Placeholder("hello")
+def test_input_class():
+ """Test input class."""
+ input = yaml_loader.Input("hello")
+ input2 = yaml_loader.Input("hello")
- assert placeholder.name == "hello"
- assert placeholder == placeholder2
+ assert input.name == "hello"
+ assert input == input2
- assert len({placeholder, placeholder2}) == 1
+ assert len({input, input2}) == 1
-def test_placeholder():
- """Test loading placeholders."""
- data = {"hello": yaml.Placeholder("test_name")}
+def test_input():
+ """Test loading inputs."""
+ data = {"hello": yaml.Input("test_name")}
assert yaml.parse_yaml(yaml.dump(data)) == data
diff --git a/tests/util/yaml/test_input.py b/tests/util/yaml/test_input.py
new file mode 100644
index 00000000000..1c13d1b3684
--- /dev/null
+++ b/tests/util/yaml/test_input.py
@@ -0,0 +1,34 @@
+"""Test inputs."""
+import pytest
+
+from homeassistant.util.yaml import (
+ Input,
+ UndefinedSubstitution,
+ extract_inputs,
+ substitute,
+)
+
+
+def test_extract_inputs():
+ """Test extracting inputs from data."""
+ assert extract_inputs(Input("hello")) == {"hello"}
+ assert extract_inputs({"info": [1, Input("hello"), 2, Input("world")]}) == {
+ "hello",
+ "world",
+ }
+
+
+def test_substitute():
+ """Test we can substitute."""
+ assert substitute(Input("hello"), {"hello": 5}) == 5
+
+ with pytest.raises(UndefinedSubstitution):
+ substitute(Input("hello"), {})
+
+ assert (
+ substitute(
+ {"info": [1, Input("hello"), 2, Input("world")]},
+ {"hello": 5, "world": 10},
+ )
+ == {"info": [1, 5, 2, 10]}
+ )