From 4b057101c516fa878a248b3aefa9150bc28e626b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 30 Dec 2020 13:27:12 +0100 Subject: [PATCH 001/507] Bumped version to 2021.2.0dev0 (#44647) --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index de18fd4ed04..1a191441990 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,6 +1,6 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 -MINOR_VERSION = 1 +MINOR_VERSION = 2 PATCH_VERSION = "0.dev0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" From e2e07cf42e077ef0e8b21ed6dc21045f55ef2803 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 30 Dec 2020 13:33:13 +0100 Subject: [PATCH 002/507] Upgrade sendgrid to 6.4.8 (#44646) --- homeassistant/components/sendgrid/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sendgrid/manifest.json b/homeassistant/components/sendgrid/manifest.json index d09ec684e52..0b98f132c95 100644 --- a/homeassistant/components/sendgrid/manifest.json +++ b/homeassistant/components/sendgrid/manifest.json @@ -2,6 +2,6 @@ "domain": "sendgrid", "name": "SendGrid", "documentation": "https://www.home-assistant.io/integrations/sendgrid", - "requirements": ["sendgrid==6.4.6"], + "requirements": ["sendgrid==6.4.8"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 8477f615aab..8eeb5ee3aa3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1988,7 +1988,7 @@ schiene==0.23 scsgate==0.1.0 # homeassistant.components.sendgrid -sendgrid==6.4.6 +sendgrid==6.4.8 # homeassistant.components.sensehat sense-hat==2.2.0 From a6c83cc46a34084fdc4c0e7221b6ba493f82cbac Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 30 Dec 2020 09:11:08 -0500 Subject: [PATCH 003/507] Bump ZHA quirks version to 0.0.50 (#44650) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index ed2460614fc..ebca96da5fd 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,7 +7,7 @@ "bellows==0.21.0", "pyserial==3.5", "pyserial-asyncio==0.5", - "zha-quirks==0.0.49", + "zha-quirks==0.0.50", "zigpy-cc==0.5.2", "zigpy-deconz==0.11.0", "zigpy==0.28.2", diff --git a/requirements_all.txt b/requirements_all.txt index 8eeb5ee3aa3..07014bba992 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2339,7 +2339,7 @@ zengge==0.2 zeroconf==0.28.7 # homeassistant.components.zha -zha-quirks==0.0.49 +zha-quirks==0.0.50 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2e038049fad..ae89859695a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1144,7 +1144,7 @@ zeep[async]==4.0.0 zeroconf==0.28.7 # homeassistant.components.zha -zha-quirks==0.0.49 +zha-quirks==0.0.50 # homeassistant.components.zha zigpy-cc==0.5.2 From 16ddbb95f4b9d2480062ef98f7352b86c29dcfe5 Mon Sep 17 00:00:00 2001 From: zewelor Date: Wed, 30 Dec 2020 17:00:28 +0100 Subject: [PATCH 004/507] Add yeelight service to enable disable music mode (#44533) * Add service to enable / disable music mode * Black reformat * Update test * Fix tests * Revert consts cleanup * Use entity method as service call * Use ATTR for service call * Sort * Add tests * Fix isort * Fix print * Black --- homeassistant/components/yeelight/__init__.py | 1 + homeassistant/components/yeelight/light.py | 28 ++++++++-- .../components/yeelight/services.yaml | 9 ++++ tests/components/yeelight/test_light.py | 51 ++++++++++++++++--- 4 files changed, 77 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 324999c7124..86df66de31d 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -53,6 +53,7 @@ DATA_UNSUB_UPDATE_LISTENER = "unsub_update_listener" ATTR_COUNT = "count" ATTR_ACTION = "action" ATTR_TRANSITIONS = "transitions" +ATTR_MODE_MUSIC = "music_mode" ACTION_RECOVER = "recover" ACTION_STAY = "stay" diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 98b7f097636..c1d4f14c813 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -49,6 +49,7 @@ from . import ( ACTION_RECOVER, ATTR_ACTION, ATTR_COUNT, + ATTR_MODE_MUSIC, ATTR_TRANSITIONS, CONF_FLOW_PARAMS, CONF_MODE_MUSIC, @@ -77,6 +78,7 @@ SUPPORT_YEELIGHT_RGB = SUPPORT_YEELIGHT_WHITE_TEMP | SUPPORT_COLOR ATTR_MINUTES = "minutes" SERVICE_SET_MODE = "set_mode" +SERVICE_SET_MUSIC_MODE = "set_music_mode" SERVICE_START_FLOW = "start_flow" SERVICE_SET_COLOR_SCENE = "set_color_scene" SERVICE_SET_HSV_SCENE = "set_hsv_scene" @@ -175,6 +177,10 @@ SERVICE_SCHEMA_SET_MODE = { vol.Required(ATTR_MODE): vol.In([mode.name.lower() for mode in PowerMode]) } +SERVICE_SCHEMA_SET_MUSIC_MODE = { + vol.Required(ATTR_MODE_MUSIC): cv.boolean, +} + SERVICE_SCHEMA_START_FLOW = YEELIGHT_FLOW_TRANSITION_SCHEMA SERVICE_SCHEMA_SET_COLOR_SCENE = { @@ -404,6 +410,11 @@ def _async_setup_services(hass: HomeAssistant): SERVICE_SCHEMA_SET_AUTO_DELAY_OFF_SCENE, _async_set_auto_delay_off_scene, ) + platform.async_register_entity_service( + SERVICE_SET_MUSIC_MODE, + SERVICE_SCHEMA_SET_MUSIC_MODE, + "set_music_mode", + ) class YeelightGenericLight(YeelightEntity, LightEntity): @@ -550,7 +561,11 @@ class YeelightGenericLight(YeelightEntity, LightEntity): def device_state_attributes(self): """Return the device specific state attributes.""" - attributes = {"flowing": self.device.is_color_flow_enabled} + attributes = { + "flowing": self.device.is_color_flow_enabled, + "music_mode": self._bulb.music_mode, + } + if self.device.is_nightlight_supported: attributes["night_light"] = self.device.is_nightlight_enabled @@ -591,13 +606,18 @@ class YeelightGenericLight(YeelightEntity, LightEntity): return color_util.color_RGB_to_hs(red, green, blue) - def set_music_mode(self, mode) -> None: + def set_music_mode(self, music_mode) -> None: """Set the music mode on or off.""" - if mode: - self._bulb.start_music() + if music_mode: + try: + self._bulb.start_music() + except AssertionError as ex: + _LOGGER.error(ex) else: self._bulb.stop_music() + self.device.update() + @_cmd def set_brightness(self, brightness, duration) -> None: """Set bulb brightness.""" diff --git a/homeassistant/components/yeelight/services.yaml b/homeassistant/components/yeelight/services.yaml index 5e7f2419f16..b519d0c91d9 100644 --- a/homeassistant/components/yeelight/services.yaml +++ b/homeassistant/components/yeelight/services.yaml @@ -85,3 +85,12 @@ start_flow: transitions: description: Array of transitions, for desired effect. Examples https://yeelight.readthedocs.io/en/stable/flow.html example: '[{ "TemperatureTransition": [1900, 1000, 80] }, { "TemperatureTransition": [1900, 1000, 10] }]' +set_music_mode: + description: Enable or disable music_mode + fields: + entity_id: + description: Name of the light entity. + example: "light.yeelight" + music_mode: + description: Use true or false to enable / disable music_mode + example: true diff --git a/tests/components/yeelight/test_light.py b/tests/components/yeelight/test_light.py index 686ba6d8e82..115b825d863 100644 --- a/tests/components/yeelight/test_light.py +++ b/tests/components/yeelight/test_light.py @@ -31,6 +31,7 @@ from homeassistant.components.light import ( ) from homeassistant.components.yeelight import ( ATTR_COUNT, + ATTR_MODE_MUSIC, ATTR_TRANSITIONS, CONF_CUSTOM_EFFECTS, CONF_FLOW_PARAMS, @@ -72,6 +73,7 @@ from homeassistant.components.yeelight.light import ( SERVICE_SET_COLOR_TEMP_SCENE, SERVICE_SET_HSV_SCENE, SERVICE_SET_MODE, + SERVICE_SET_MUSIC_MODE, SERVICE_START_FLOW, SUPPORT_YEELIGHT, SUPPORT_YEELIGHT_RGB, @@ -135,7 +137,14 @@ async def test_services(hass: HomeAssistant, caplog): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - async def _async_test_service(service, data, method, payload=None, domain=DOMAIN): + async def _async_test_service( + service, + data, + method, + payload=None, + domain=DOMAIN, + failure_side_effect=BulbException, + ): err_count = len([x for x in caplog.records if x.levelno == logging.ERROR]) # success @@ -153,13 +162,14 @@ async def test_services(hass: HomeAssistant, caplog): ) # failure - mocked_method = MagicMock(side_effect=BulbException) - setattr(type(mocked_bulb), method, mocked_method) - await hass.services.async_call(domain, service, data, blocking=True) - assert ( - len([x for x in caplog.records if x.levelno == logging.ERROR]) - == err_count + 1 - ) + if failure_side_effect: + mocked_method = MagicMock(side_effect=failure_side_effect) + setattr(type(mocked_bulb), method, mocked_method) + await hass.services.async_call(domain, service, data, blocking=True) + assert ( + len([x for x in caplog.records if x.levelno == logging.ERROR]) + == err_count + 1 + ) # turn_on brightness = 100 @@ -283,6 +293,29 @@ async def test_services(hass: HomeAssistant, caplog): [SceneClass.AUTO_DELAY_OFF, 50, 1], ) + # set_music_mode failure enable + await _async_test_service( + SERVICE_SET_MUSIC_MODE, + {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_MODE_MUSIC: "true"}, + "start_music", + failure_side_effect=AssertionError, + ) + + # set_music_mode disable + await _async_test_service( + SERVICE_SET_MUSIC_MODE, + {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_MODE_MUSIC: "false"}, + "stop_music", + failure_side_effect=None, + ) + + # set_music_mode success enable + await _async_test_service( + SERVICE_SET_MUSIC_MODE, + {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_MODE_MUSIC: "true"}, + "start_music", + failure_side_effect=None, + ) # test _cmd wrapper error handler err_count = len([x for x in caplog.records if x.levelno == logging.ERROR]) type(mocked_bulb).turn_on = MagicMock() @@ -338,6 +371,7 @@ async def test_device_types(hass: HomeAssistant): target_properties["friendly_name"] = name target_properties["flowing"] = False target_properties["night_light"] = True + target_properties["music_mode"] = False assert dict(state.attributes) == target_properties await hass.config_entries.async_unload(config_entry.entry_id) @@ -365,6 +399,7 @@ async def test_device_types(hass: HomeAssistant): nightlight_properties["icon"] = "mdi:weather-night" nightlight_properties["flowing"] = False nightlight_properties["night_light"] = True + nightlight_properties["music_mode"] = False assert dict(state.attributes) == nightlight_properties await hass.config_entries.async_unload(config_entry.entry_id) From 2e62e0661bda16ccb4989c96ff98bec1e7e3c676 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 30 Dec 2020 17:12:51 +0100 Subject: [PATCH 005/507] Upgrade colorlog to 4.6.2 (#44652) --- homeassistant/scripts/check_config.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index b5745d533fb..07a6a54e402 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -17,7 +17,7 @@ import homeassistant.util.yaml.loader as yaml_loader # mypy: allow-untyped-calls, allow-untyped-defs -REQUIREMENTS = ("colorlog==4.5.0",) +REQUIREMENTS = ("colorlog==4.6.2",) _LOGGER = logging.getLogger(__name__) # pylint: disable=protected-access diff --git a/requirements_all.txt b/requirements_all.txt index 07014bba992..827d1213ad2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -428,7 +428,7 @@ coinbase==2.1.0 coinmarketcap==5.0.3 # homeassistant.scripts.check_config -colorlog==4.5.0 +colorlog==4.6.2 # homeassistant.components.color_extractor colorthief==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ae89859695a..65665a57f49 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -222,7 +222,7 @@ caldav==0.6.1 coinmarketcap==5.0.3 # homeassistant.scripts.check_config -colorlog==4.5.0 +colorlog==4.6.2 # homeassistant.components.color_extractor colorthief==0.2.1 From 15a4e1e1b3017f8ad68694b62f83b025a6189353 Mon Sep 17 00:00:00 2001 From: Daniel Lintott Date: Wed, 30 Dec 2020 18:15:27 +0000 Subject: [PATCH 006/507] Bump zm-py version to 0.5.2 (#44658) --- homeassistant/components/zoneminder/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zoneminder/manifest.json b/homeassistant/components/zoneminder/manifest.json index b3a87510e5a..039513f100e 100644 --- a/homeassistant/components/zoneminder/manifest.json +++ b/homeassistant/components/zoneminder/manifest.json @@ -2,6 +2,6 @@ "domain": "zoneminder", "name": "ZoneMinder", "documentation": "https://www.home-assistant.io/integrations/zoneminder", - "requirements": ["zm-py==0.4.0"], + "requirements": ["zm-py==0.5.2"], "codeowners": ["@rohankapoorcom"] } diff --git a/requirements_all.txt b/requirements_all.txt index 827d1213ad2..5c699a91ca9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2366,4 +2366,4 @@ zigpy-znp==0.3.0 zigpy==0.28.2 # homeassistant.components.zoneminder -zm-py==0.4.0 +zm-py==0.5.2 From e37bb513209f88fda6593b306bcd235dc298cc29 Mon Sep 17 00:00:00 2001 From: "J.P. Krauss" Date: Wed, 30 Dec 2020 11:25:57 -0800 Subject: [PATCH 007/507] Add AirNow Integration (#40091) --- .coveragerc | 2 + CODEOWNERS | 1 + homeassistant/components/airnow/__init__.py | 161 +++++++++++++++ .../components/airnow/config_flow.py | 110 +++++++++++ homeassistant/components/airnow/const.py | 21 ++ homeassistant/components/airnow/manifest.json | 12 ++ homeassistant/components/airnow/sensor.py | 118 +++++++++++ homeassistant/components/airnow/strings.json | 26 +++ .../components/airnow/translations/en.json | 27 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/airnow/__init__.py | 1 + tests/components/airnow/test_config_flow.py | 185 ++++++++++++++++++ 14 files changed, 671 insertions(+) create mode 100644 homeassistant/components/airnow/__init__.py create mode 100644 homeassistant/components/airnow/config_flow.py create mode 100644 homeassistant/components/airnow/const.py create mode 100644 homeassistant/components/airnow/manifest.json create mode 100644 homeassistant/components/airnow/sensor.py create mode 100644 homeassistant/components/airnow/strings.json create mode 100644 homeassistant/components/airnow/translations/en.json create mode 100644 tests/components/airnow/__init__.py create mode 100644 tests/components/airnow/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 9f2fdc80716..5778d541a68 100644 --- a/.coveragerc +++ b/.coveragerc @@ -29,6 +29,8 @@ omit = homeassistant/components/agent_dvr/camera.py homeassistant/components/agent_dvr/const.py homeassistant/components/agent_dvr/helpers.py + homeassistant/components/airnow/__init__.py + homeassistant/components/airnow/sensor.py homeassistant/components/airvisual/__init__.py homeassistant/components/airvisual/air_quality.py homeassistant/components/airvisual/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index a660d930128..7b874bb0ebf 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -26,6 +26,7 @@ homeassistant/components/adguard/* @frenck homeassistant/components/advantage_air/* @Bre77 homeassistant/components/agent_dvr/* @ispysoftware homeassistant/components/airly/* @bieniu +homeassistant/components/airnow/* @asymworks homeassistant/components/airvisual/* @bachya homeassistant/components/alarmdecoder/* @ajschmidt8 homeassistant/components/alexa/* @home-assistant/cloud @ochlocracy diff --git a/homeassistant/components/airnow/__init__.py b/homeassistant/components/airnow/__init__.py new file mode 100644 index 00000000000..5cbc87947f9 --- /dev/null +++ b/homeassistant/components/airnow/__init__.py @@ -0,0 +1,161 @@ +"""The AirNow integration.""" +import asyncio +import datetime +import logging + +from aiohttp.client_exceptions import ClientConnectorError +from pyairnow import WebServiceAPI +from pyairnow.conv import aqi_to_concentration +from pyairnow.errors import AirNowError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + ATTR_API_AQI, + ATTR_API_AQI_DESCRIPTION, + ATTR_API_AQI_LEVEL, + ATTR_API_AQI_PARAM, + ATTR_API_CAT_DESCRIPTION, + ATTR_API_CAT_LEVEL, + ATTR_API_CATEGORY, + ATTR_API_PM25, + ATTR_API_POLLUTANT, + ATTR_API_REPORT_DATE, + ATTR_API_REPORT_HOUR, + ATTR_API_STATE, + ATTR_API_STATION, + ATTR_API_STATION_LATITUDE, + ATTR_API_STATION_LONGITUDE, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) +PLATFORMS = ["sensor"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the AirNow component.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up AirNow from a config entry.""" + api_key = entry.data[CONF_API_KEY] + latitude = entry.data[CONF_LATITUDE] + longitude = entry.data[CONF_LONGITUDE] + distance = entry.data[CONF_RADIUS] + + # Reports are published hourly but update twice per hour + update_interval = datetime.timedelta(minutes=30) + + # Setup the Coordinator + session = async_get_clientsession(hass) + coordinator = AirNowDataUpdateCoordinator( + hass, session, api_key, latitude, longitude, distance, update_interval + ) + + # Sync with Coordinator + await coordinator.async_refresh() + if not coordinator.last_update_success: + raise ConfigEntryNotReady + + # Store Entity and Initialize Platforms + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][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): + """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 AirNowDataUpdateCoordinator(DataUpdateCoordinator): + """Define an object to hold Airly data.""" + + def __init__( + self, hass, session, api_key, latitude, longitude, distance, update_interval + ): + """Initialize.""" + self.latitude = latitude + self.longitude = longitude + self.distance = distance + + self.airnow = WebServiceAPI(api_key, session=session) + + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) + + async def _async_update_data(self): + """Update data via library.""" + data = {} + try: + obs = await self.airnow.observations.latLong( + self.latitude, + self.longitude, + distance=self.distance, + ) + + except (AirNowError, ClientConnectorError) as error: + raise UpdateFailed(error) from error + + if not obs: + raise UpdateFailed("No data was returned from AirNow") + + max_aqi = 0 + max_aqi_level = 0 + max_aqi_desc = "" + max_aqi_poll = "" + for obv in obs: + # Convert AQIs to Concentration + pollutant = obv[ATTR_API_AQI_PARAM] + concentration = aqi_to_concentration(obv[ATTR_API_AQI], pollutant) + data[obv[ATTR_API_AQI_PARAM]] = concentration + + # Overall AQI is the max of all pollutant AQIs + if obv[ATTR_API_AQI] > max_aqi: + max_aqi = obv[ATTR_API_AQI] + max_aqi_level = obv[ATTR_API_CATEGORY][ATTR_API_CAT_LEVEL] + max_aqi_desc = obv[ATTR_API_CATEGORY][ATTR_API_CAT_DESCRIPTION] + max_aqi_poll = pollutant + + # Copy other data from PM2.5 Value + if obv[ATTR_API_AQI_PARAM] == ATTR_API_PM25: + # Copy Report Details + data[ATTR_API_REPORT_DATE] = obv[ATTR_API_REPORT_DATE] + data[ATTR_API_REPORT_HOUR] = obv[ATTR_API_REPORT_HOUR] + + # Copy Station Details + data[ATTR_API_STATE] = obv[ATTR_API_STATE] + data[ATTR_API_STATION] = obv[ATTR_API_STATION] + data[ATTR_API_STATION_LATITUDE] = obv[ATTR_API_STATION_LATITUDE] + data[ATTR_API_STATION_LONGITUDE] = obv[ATTR_API_STATION_LONGITUDE] + + # Store Overall AQI + data[ATTR_API_AQI] = max_aqi + data[ATTR_API_AQI_LEVEL] = max_aqi_level + data[ATTR_API_AQI_DESCRIPTION] = max_aqi_desc + data[ATTR_API_POLLUTANT] = max_aqi_poll + + return data diff --git a/homeassistant/components/airnow/config_flow.py b/homeassistant/components/airnow/config_flow.py new file mode 100644 index 00000000000..6d53ac133ee --- /dev/null +++ b/homeassistant/components/airnow/config_flow.py @@ -0,0 +1,110 @@ +"""Config flow for AirNow integration.""" +import logging + +from pyairnow import WebServiceAPI +from pyairnow.errors import AirNowError, InvalidKeyError +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv + +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +async def validate_input(hass: core.HomeAssistant, data): + """ + Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + session = async_get_clientsession(hass) + client = WebServiceAPI(data[CONF_API_KEY], session=session) + + lat = data[CONF_LATITUDE] + lng = data[CONF_LONGITUDE] + distance = data[CONF_RADIUS] + + # Check that the provided latitude/longitude provide a response + try: + test_data = await client.observations.latLong(lat, lng, distance=distance) + + except InvalidKeyError as exc: + raise InvalidAuth from exc + except AirNowError as exc: + raise CannotConnect from exc + + if not test_data: + raise InvalidLocation + + # Validation Succeeded + return True + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for AirNow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + # Set a unique id based on latitude/longitude + await self.async_set_unique_id( + f"{user_input[CONF_LATITUDE]}-{user_input[CONF_LONGITUDE]}" + ) + self._abort_if_unique_id_configured() + + try: + # Validate inputs + await validate_input(self.hass, user_input) + + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except InvalidLocation: + errors["base"] = "invalid_location" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + # Create Entry + return self.async_create_entry( + title=f"AirNow Sensor at {user_input[CONF_LATITUDE]}, {user_input[CONF_LONGITUDE]}", + data=user_input, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_API_KEY): str, + vol.Optional( + CONF_LATITUDE, default=self.hass.config.latitude + ): cv.latitude, + vol.Optional( + CONF_LONGITUDE, default=self.hass.config.longitude + ): cv.longitude, + vol.Optional(CONF_RADIUS, default=150): int, + } + ), + errors=errors, + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" + + +class InvalidLocation(exceptions.HomeAssistantError): + """Error to indicate the location is invalid.""" diff --git a/homeassistant/components/airnow/const.py b/homeassistant/components/airnow/const.py new file mode 100644 index 00000000000..67a9289efc5 --- /dev/null +++ b/homeassistant/components/airnow/const.py @@ -0,0 +1,21 @@ +"""Constants for the AirNow integration.""" +ATTR_API_AQI = "AQI" +ATTR_API_AQI_LEVEL = "Category.Number" +ATTR_API_AQI_DESCRIPTION = "Category.Name" +ATTR_API_AQI_PARAM = "ParameterName" +ATTR_API_CATEGORY = "Category" +ATTR_API_CAT_LEVEL = "Number" +ATTR_API_CAT_DESCRIPTION = "Name" +ATTR_API_O3 = "O3" +ATTR_API_PM25 = "PM2.5" +ATTR_API_POLLUTANT = "Pollutant" +ATTR_API_REPORT_DATE = "HourObserved" +ATTR_API_REPORT_HOUR = "DateObserved" +ATTR_API_STATE = "StateCode" +ATTR_API_STATION = "ReportingArea" +ATTR_API_STATION_LATITUDE = "Latitude" +ATTR_API_STATION_LONGITUDE = "Longitude" +DEFAULT_NAME = "AirNow" +DOMAIN = "airnow" +SENSOR_AQI_ATTR_DESCR = "description" +SENSOR_AQI_ATTR_LEVEL = "level" diff --git a/homeassistant/components/airnow/manifest.json b/homeassistant/components/airnow/manifest.json new file mode 100644 index 00000000000..fee89ae4fff --- /dev/null +++ b/homeassistant/components/airnow/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "airnow", + "name": "AirNow", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/airnow", + "requirements": [ + "pyairnow==1.1.0" + ], + "codeowners": [ + "@asymworks" + ] +} diff --git a/homeassistant/components/airnow/sensor.py b/homeassistant/components/airnow/sensor.py new file mode 100644 index 00000000000..fed6def2b36 --- /dev/null +++ b/homeassistant/components/airnow/sensor.py @@ -0,0 +1,118 @@ +"""Support for the AirNow sensor service.""" +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_DEVICE_CLASS, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, +) +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ( + ATTR_API_AQI, + ATTR_API_AQI_DESCRIPTION, + ATTR_API_AQI_LEVEL, + ATTR_API_O3, + ATTR_API_PM25, + DOMAIN, + SENSOR_AQI_ATTR_DESCR, + SENSOR_AQI_ATTR_LEVEL, +) + +ATTRIBUTION = "Data provided by AirNow" + +ATTR_ICON = "icon" +ATTR_LABEL = "label" +ATTR_UNIT = "unit" + +PARALLEL_UPDATES = 1 + +SENSOR_TYPES = { + ATTR_API_AQI: { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:blur", + ATTR_LABEL: ATTR_API_AQI, + ATTR_UNIT: "aqi", + }, + ATTR_API_PM25: { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:blur", + ATTR_LABEL: ATTR_API_PM25, + ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + }, + ATTR_API_O3: { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:blur", + ATTR_LABEL: ATTR_API_O3, + ATTR_UNIT: CONCENTRATION_PARTS_PER_MILLION, + }, +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up AirNow sensor entities based on a config entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + + sensors = [] + for sensor in SENSOR_TYPES: + sensors.append(AirNowSensor(coordinator, sensor)) + + async_add_entities(sensors, False) + + +class AirNowSensor(CoordinatorEntity): + """Define an AirNow sensor.""" + + def __init__(self, coordinator, kind): + """Initialize.""" + super().__init__(coordinator) + self.kind = kind + self._device_class = None + self._state = None + self._icon = None + self._unit_of_measurement = None + self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} + + @property + def name(self): + """Return the name.""" + return f"AirNow {SENSOR_TYPES[self.kind][ATTR_LABEL]}" + + @property + def state(self): + """Return the state.""" + self._state = self.coordinator.data[self.kind] + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self.kind == ATTR_API_AQI: + self._attrs[SENSOR_AQI_ATTR_DESCR] = self.coordinator.data[ + ATTR_API_AQI_DESCRIPTION + ] + self._attrs[SENSOR_AQI_ATTR_LEVEL] = self.coordinator.data[ + ATTR_API_AQI_LEVEL + ] + + return self._attrs + + @property + def icon(self): + """Return the icon.""" + self._icon = SENSOR_TYPES[self.kind][ATTR_ICON] + return self._icon + + @property + def device_class(self): + """Return the device_class.""" + return SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS] + + @property + def unique_id(self): + """Return a unique_id for this entity.""" + return f"{self.coordinator.latitude}-{self.coordinator.longitude}-{self.kind.lower()}" + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return SENSOR_TYPES[self.kind][ATTR_UNIT] diff --git a/homeassistant/components/airnow/strings.json b/homeassistant/components/airnow/strings.json new file mode 100644 index 00000000000..a73ad6d179c --- /dev/null +++ b/homeassistant/components/airnow/strings.json @@ -0,0 +1,26 @@ +{ + "title": "AirNow", + "config": { + "step": { + "user": { + "title": "AirNow", + "description": "Set up AirNow air quality integration. To generate API key go to https://docs.airnowapi.org/account/request/", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]", + "radius": "Station Radius (miles; optional)" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "invalid_location": "No results found for that location", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/airnow/translations/en.json b/homeassistant/components/airnow/translations/en.json new file mode 100644 index 00000000000..5c5259c74e2 --- /dev/null +++ b/homeassistant/components/airnow/translations/en.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "invalid_location": "No results found for that location", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Name of the Entity", + "radius": "Station Radius (miles; optional)" + }, + "description": "Set up AirNow air quality integration. To generate API key go to https://docs.airnowapi.org/account/request/", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 9e204e91da5..b94b4deee94 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -13,6 +13,7 @@ FLOWS = [ "advantage_air", "agent_dvr", "airly", + "airnow", "airvisual", "alarmdecoder", "almond", diff --git a/requirements_all.txt b/requirements_all.txt index 5c699a91ca9..d601cbdac34 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1258,6 +1258,9 @@ pyaehw4a1==0.3.9 # homeassistant.components.aftership pyaftership==0.1.2 +# homeassistant.components.airnow +pyairnow==1.1.0 + # homeassistant.components.airvisual pyairvisual==5.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 65665a57f49..06b8f504fe4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -633,6 +633,9 @@ py_nextbusnext==0.1.4 # homeassistant.components.hisense_aehw4a1 pyaehw4a1==0.3.9 +# homeassistant.components.airnow +pyairnow==1.1.0 + # homeassistant.components.airvisual pyairvisual==5.0.4 diff --git a/tests/components/airnow/__init__.py b/tests/components/airnow/__init__.py new file mode 100644 index 00000000000..d7fc1922ee8 --- /dev/null +++ b/tests/components/airnow/__init__.py @@ -0,0 +1 @@ +"""Tests for the AirNow integration.""" diff --git a/tests/components/airnow/test_config_flow.py b/tests/components/airnow/test_config_flow.py new file mode 100644 index 00000000000..2db3c22795f --- /dev/null +++ b/tests/components/airnow/test_config_flow.py @@ -0,0 +1,185 @@ +"""Test the AirNow config flow.""" +from pyairnow.errors import AirNowError, InvalidKeyError + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.airnow.const import DOMAIN +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS + +from tests.async_mock import patch +from tests.common import MockConfigEntry + +CONFIG = { + CONF_API_KEY: "abc123", + CONF_LATITUDE: 34.053718, + CONF_LONGITUDE: -118.244842, + CONF_RADIUS: 75, +} + +# Mock AirNow Response +MOCK_RESPONSE = [ + { + "DateObserved": "2020-12-20", + "HourObserved": 15, + "LocalTimeZone": "PST", + "ReportingArea": "Central LA CO", + "StateCode": "CA", + "Latitude": 34.0663, + "Longitude": -118.2266, + "ParameterName": "O3", + "AQI": 44, + "Category": { + "Number": 1, + "Name": "Good", + }, + }, + { + "DateObserved": "2020-12-20", + "HourObserved": 15, + "LocalTimeZone": "PST", + "ReportingArea": "Central LA CO", + "StateCode": "CA", + "Latitude": 34.0663, + "Longitude": -118.2266, + "ParameterName": "PM2.5", + "AQI": 37, + "Category": { + "Number": 1, + "Name": "Good", + }, + }, + { + "DateObserved": "2020-12-20", + "HourObserved": 15, + "LocalTimeZone": "PST", + "ReportingArea": "Central LA CO", + "StateCode": "CA", + "Latitude": 34.0663, + "Longitude": -118.2266, + "ParameterName": "PM10", + "AQI": 11, + "Category": { + "Number": 1, + "Name": "Good", + }, + }, +] + + +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"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch("pyairnow.WebServiceAPI._get", return_value=MOCK_RESPONSE,), patch( + "homeassistant.components.airnow.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.airnow.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + CONFIG, + ) + + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["data"] == CONFIG + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "pyairnow.WebServiceAPI._get", + side_effect=InvalidKeyError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + CONFIG, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_invalid_location(hass): + """Test we handle invalid location.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("pyairnow.WebServiceAPI._get", return_value={}): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + CONFIG, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_location"} + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "pyairnow.WebServiceAPI._get", + side_effect=AirNowError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + CONFIG, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_unexpected(hass): + """Test we handle an unexpected error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.airnow.config_flow.validate_input", + side_effect=RuntimeError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + CONFIG, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} + + +async def test_entry_already_exists(hass): + """Test that the form aborts if the Lat/Lng is already configured.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_id = f"{CONFIG[CONF_LATITUDE]}-{CONFIG[CONF_LONGITUDE]}" + mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=mock_id) + mock_entry.add_to_hass(hass) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + CONFIG, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "already_configured" From b1bb0d12c96d9677c1b002709eb66e5e39c23a4b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 30 Dec 2020 23:06:30 +0100 Subject: [PATCH 008/507] Upgrade vsure to 1.6.1 (#44657) --- homeassistant/components/verisure/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/verisure/manifest.json b/homeassistant/components/verisure/manifest.json index 6260f4a9ffc..22c5e0c2362 100644 --- a/homeassistant/components/verisure/manifest.json +++ b/homeassistant/components/verisure/manifest.json @@ -2,6 +2,6 @@ "domain": "verisure", "name": "Verisure", "documentation": "https://www.home-assistant.io/integrations/verisure", - "requirements": ["jsonpath==0.82", "vsure==1.5.4"], + "requirements": ["jsonpath==0.82", "vsure==1.6.1"], "codeowners": ["@frenck"] } diff --git a/requirements_all.txt b/requirements_all.txt index d601cbdac34..5afd476066c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2254,7 +2254,7 @@ volkszaehler==0.1.3 volvooncall==0.8.12 # homeassistant.components.verisure -vsure==1.5.4 +vsure==1.6.1 # homeassistant.components.vasttrafik vtjp==0.1.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 06b8f504fe4..ab388851105 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1104,7 +1104,7 @@ uvcclient==0.11.0 vilfo-api-client==0.3.2 # homeassistant.components.verisure -vsure==1.5.4 +vsure==1.6.1 # homeassistant.components.vultr vultr==0.1.2 From e2964ca878595fefc74b52f66875bea383da09c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 30 Dec 2020 12:10:42 -1000 Subject: [PATCH 009/507] Update py-august to 0.25.2 to fix august token refreshes (#40109) * Update py-august to 0.26.0 to fix august token refreshes * bump version --- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index c2c383468f6..67649b7edba 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["py-august==0.25.0"], + "requirements": ["py-august==0.25.2"], "dependencies": ["configurator"], "codeowners": ["@bdraco"], "config_flow": true diff --git a/requirements_all.txt b/requirements_all.txt index 5afd476066c..de0e56698a2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1192,7 +1192,7 @@ pushover_complete==1.1.1 pwmled==1.6.7 # homeassistant.components.august -py-august==0.25.0 +py-august==0.25.2 # homeassistant.components.canary py-canary==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ab388851105..b05ee72863d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -597,7 +597,7 @@ pure-python-adb[async]==0.3.0.dev0 pushbullet.py==0.11.0 # homeassistant.components.august -py-august==0.25.0 +py-august==0.25.2 # homeassistant.components.canary py-canary==0.5.0 From da66a4e933c41d376c00716c0c77b057042716c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 31 Dec 2020 01:02:14 +0200 Subject: [PATCH 010/507] Device automation config error message improvements (#44656) Refs #44654, #44655 --- homeassistant/components/deconz/device_trigger.py | 10 +++++++++- homeassistant/components/sensor/device_condition.py | 5 ++++- homeassistant/components/sensor/device_trigger.py | 5 ++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 869a6ef3594..9fc6e956963 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -417,7 +417,15 @@ async def async_validate_trigger_config(hass, config): or device.model not in REMOTES or trigger not in REMOTES[device.model] ): - raise InvalidDeviceAutomationConfig + if not device: + raise InvalidDeviceAutomationConfig( + f"deCONZ trigger {trigger} device with id " + f"{config[CONF_DEVICE_ID]} not found" + ) + raise InvalidDeviceAutomationConfig( + f"deCONZ trigger {trigger} is not valid for device " + f"{device} ({config[CONF_DEVICE_ID]})" + ) return config diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index d58381a2f40..a9d44f2f860 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -169,7 +169,10 @@ async def async_get_condition_capabilities(hass, config): ) if not state or not unit_of_measurement: - raise InvalidDeviceAutomationConfig + raise InvalidDeviceAutomationConfig( + "No state or unit of measurement found for " + f"condition entity {config[CONF_ENTITY_ID]}" + ) return { "extra_fields": vol.Schema( diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 77f6afd9acf..86dda53cd2b 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -168,7 +168,10 @@ async def async_get_trigger_capabilities(hass, config): ) if not state or not unit_of_measurement: - raise InvalidDeviceAutomationConfig + raise InvalidDeviceAutomationConfig( + "No state or unit of measurement found for " + f"trigger entity {config[CONF_ENTITY_ID]}" + ) return { "extra_fields": vol.Schema( From 687f90e164690cc3484cad25b825c1c32382393d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 31 Dec 2020 00:02:56 +0100 Subject: [PATCH 011/507] Add motion binary sensor (#44445) --- homeassistant/components/shelly/binary_sensor.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index 53038352d4d..d53f089054a 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -3,6 +3,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_GAS, DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, DEVICE_CLASS_OPENING, DEVICE_CLASS_POWER, DEVICE_CLASS_PROBLEM, @@ -70,6 +71,9 @@ SENSORS = { default_enabled=False, removal_condition=is_momentary_input, ), + ("sensor", "motion"): BlockAttributeDescription( + name="Motion", device_class=DEVICE_CLASS_MOTION + ), } REST_SENSORS = { From b290a8b5a17845eeb3d6251f7350b57d2c57d8ab Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Wed, 30 Dec 2020 23:39:14 +0000 Subject: [PATCH 012/507] always sync unit_of_measurement (#44670) --- homeassistant/components/utility_meter/sensor.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 9a4ed9e7782..6b25ec7d123 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -145,13 +145,7 @@ class UtilityMeterSensor(RestoreEntity): ): return - if ( - self._unit_of_measurement is None - and new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is not None - ): - self._unit_of_measurement = new_state.attributes.get( - ATTR_UNIT_OF_MEASUREMENT - ) + self._unit_of_measurement = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) try: diff = Decimal(new_state.state) - Decimal(old_state.state) From c7bf7b32a27bb5ba2b82e2286d87cde521f120ac Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 31 Dec 2020 01:06:26 +0100 Subject: [PATCH 013/507] Zeroconf lowercase (#44675) --- .../components/brother/manifest.json | 2 +- homeassistant/components/zeroconf/__init__.py | 36 +++++++++++-------- homeassistant/generated/zeroconf.py | 2 +- script/hassfest/manifest.py | 20 +++++++++-- tests/components/zeroconf/test_init.py | 8 +++-- 5 files changed, 48 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index 0e534147cb1..9bb9ba00261 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/brother", "codeowners": ["@bieniu"], "requirements": ["brother==0.1.20"], - "zeroconf": [{"type": "_printer._tcp.local.", "name":"Brother*"}], + "zeroconf": [{ "type": "_printer._tcp.local.", "name": "brother*" }], "config_flow": true, "quality_scale": "platinum" } diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 68300adbcfe..fdf4b98faf8 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -284,22 +284,30 @@ async def _async_start_zeroconf_browser(hass, zeroconf): # likely bad homekit data return + if "name" in info: + lowercase_name = info["name"].lower() + else: + lowercase_name = None + + if "macaddress" in info.get("properties", {}): + uppercase_mac = info["properties"]["macaddress"].upper() + else: + uppercase_mac = None + for entry in zeroconf_types[service_type]: if len(entry) > 1: - if "macaddress" in entry: - if "properties" not in info: - continue - if "macaddress" not in info["properties"]: - continue - if not fnmatch.fnmatch( - info["properties"]["macaddress"], entry["macaddress"] - ): - continue - if "name" in entry: - if "name" not in info: - continue - if not fnmatch.fnmatch(info["name"], entry["name"]): - continue + if ( + uppercase_mac is not None + and "macaddress" in entry + and not fnmatch.fnmatch(uppercase_mac, entry["macaddress"]) + ): + continue + if ( + lowercase_name is not None + and "name" in entry + and not fnmatch.fnmatch(lowercase_name, entry["name"]) + ): + continue hass.add_job( hass.config_entries.flow.async_init( diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 57b6e6cb123..49527666f53 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -116,7 +116,7 @@ ZEROCONF = { "_printer._tcp.local.": [ { "domain": "brother", - "name": "Brother*" + "name": "brother*" } ], "_spotify-connect._tcp.local.": [ diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 389e380af85..7500483ec53 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -33,6 +33,22 @@ def documentation_url(value: str) -> str: return value +def verify_lowercase(value: str): + """Verify a value is lowercase.""" + if value.lower() != value: + raise vol.Invalid("Value needs to be lowercase") + + return value + + +def verify_uppercase(value: str): + """Verify a value is uppercase.""" + if value.upper() != value: + raise vol.Invalid("Value needs to be uppercase") + + return value + + MANIFEST_SCHEMA = vol.Schema( { vol.Required("domain"): str, @@ -45,8 +61,8 @@ MANIFEST_SCHEMA = vol.Schema( vol.Schema( { vol.Required("type"): str, - vol.Optional("macaddress"): str, - vol.Optional("name"): str, + vol.Optional("macaddress"): vol.All(str, verify_uppercase), + vol.Optional("name"): vol.All(str, verify_lowercase), } ), ) diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 8767953b363..6b79c552911 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -242,13 +242,17 @@ async def test_zeroconf_match(hass, mock_zeroconf): handlers[0]( zeroconf, "_http._tcp.local.", - "shelly108._http._tcp.local.", + "Shelly108._http._tcp.local.", ServiceStateChange.Added, ) with patch.dict( zc_gen.ZEROCONF, - {"_http._tcp.local.": [{"domain": "shelly", "name": "shelly*"}]}, + { + "_http._tcp.local.": [ + {"domain": "shelly", "name": "shelly*", "macaddress": "FFAADD*"} + ] + }, clear=True, ), patch.object( hass.config_entries.flow, "async_init" From 1428c403ba8ad399fb2d257e194f60d1f6fbed34 Mon Sep 17 00:00:00 2001 From: Mark Allanson Date: Thu, 31 Dec 2020 00:16:53 +0000 Subject: [PATCH 014/507] Upgrade canary integration to use py-canary 0.5.1 (#44645) Fixes #35569 --- homeassistant/components/canary/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/canary/manifest.json b/homeassistant/components/canary/manifest.json index b4598d64087..af6b0ce54ba 100644 --- a/homeassistant/components/canary/manifest.json +++ b/homeassistant/components/canary/manifest.json @@ -2,7 +2,7 @@ "domain": "canary", "name": "Canary", "documentation": "https://www.home-assistant.io/integrations/canary", - "requirements": ["py-canary==0.5.0"], + "requirements": ["py-canary==0.5.1"], "dependencies": ["ffmpeg"], "codeowners": [], "config_flow": true diff --git a/requirements_all.txt b/requirements_all.txt index de0e56698a2..e059b744dc8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1195,7 +1195,7 @@ pwmled==1.6.7 py-august==0.25.2 # homeassistant.components.canary -py-canary==0.5.0 +py-canary==0.5.1 # homeassistant.components.cpuspeed py-cpuinfo==7.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b05ee72863d..7a89791218a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -600,7 +600,7 @@ pushbullet.py==0.11.0 py-august==0.25.2 # homeassistant.components.canary -py-canary==0.5.0 +py-canary==0.5.1 # homeassistant.components.melissa py-melissa-climate==2.1.4 From 4bde0640d671bf230878aaa53d6f64e028c76bba Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 31 Dec 2020 01:18:58 +0100 Subject: [PATCH 015/507] Bump pytradfri to 7.0.6 (#44661) --- homeassistant/components/tradfri/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index 57f58f05993..5c6bf76a169 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -3,7 +3,7 @@ "name": "IKEA TRÅDFRI", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tradfri", - "requirements": ["pytradfri[async]==7.0.5"], + "requirements": ["pytradfri[async]==7.0.6"], "homekit": { "models": ["TRADFRI"] }, diff --git a/requirements_all.txt b/requirements_all.txt index e059b744dc8..363d1cd4e6d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1855,7 +1855,7 @@ pytraccar==0.9.0 pytrackr==0.0.5 # homeassistant.components.tradfri -pytradfri[async]==7.0.5 +pytradfri[async]==7.0.6 # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7a89791218a..ce59860b3f6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -915,7 +915,7 @@ pytile==4.0.0 pytraccar==0.9.0 # homeassistant.components.tradfri -pytradfri[async]==7.0.5 +pytradfri[async]==7.0.6 # homeassistant.components.vera pyvera==0.3.11 From 408da3600b3f6e97bb9a0c548aee0fb3c6e7393f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 31 Dec 2020 12:00:43 +0100 Subject: [PATCH 016/507] Upgrade feedparser to 6.0.2 (#44683) --- homeassistant/components/feedreader/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/feedreader/manifest.json b/homeassistant/components/feedreader/manifest.json index 30413d10e43..d1bc9cdb524 100644 --- a/homeassistant/components/feedreader/manifest.json +++ b/homeassistant/components/feedreader/manifest.json @@ -2,6 +2,6 @@ "domain": "feedreader", "name": "Feedreader", "documentation": "https://www.home-assistant.io/integrations/feedreader", - "requirements": ["feedparser-homeassistant==5.2.2.dev1"], + "requirements": ["feedparser==6.0.2"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 363d1cd4e6d..8fd3200d722 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -584,7 +584,7 @@ evohome-async==0.3.5.post1 fastdotcom==0.0.3 # homeassistant.components.feedreader -feedparser-homeassistant==5.2.2.dev1 +feedparser==6.0.2 # homeassistant.components.fibaro fiblary3==0.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ce59860b3f6..a3ad62796b0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -299,7 +299,7 @@ ephem==3.7.7.0 epson-projector==0.2.3 # homeassistant.components.feedreader -feedparser-homeassistant==5.2.2.dev1 +feedparser==6.0.2 # homeassistant.components.homekit fnvhash==0.1.0 From 64dd748330c5f889da26670b60a3c44582037bb0 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 31 Dec 2020 08:07:15 -0500 Subject: [PATCH 017/507] Bump up ZHA dependencies (#44680) - zigpy == 0.29.0 - zigpy_deconz == 0.11.1 - zha-quirks == 0.0.51 --- homeassistant/components/zha/manifest.json | 6 +++--- requirements_all.txt | 6 +++--- requirements_test_all.txt | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index ebca96da5fd..54fceda03a6 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,10 +7,10 @@ "bellows==0.21.0", "pyserial==3.5", "pyserial-asyncio==0.5", - "zha-quirks==0.0.50", + "zha-quirks==0.0.51", "zigpy-cc==0.5.2", - "zigpy-deconz==0.11.0", - "zigpy==0.28.2", + "zigpy-deconz==0.11.1", + "zigpy==0.29.0", "zigpy-xbee==0.13.0", "zigpy-zigate==0.7.3", "zigpy-znp==0.3.0" diff --git a/requirements_all.txt b/requirements_all.txt index 8fd3200d722..9470222ebef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2342,7 +2342,7 @@ zengge==0.2 zeroconf==0.28.7 # homeassistant.components.zha -zha-quirks==0.0.50 +zha-quirks==0.0.51 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2354,7 +2354,7 @@ ziggo-mediabox-xl==1.1.0 zigpy-cc==0.5.2 # homeassistant.components.zha -zigpy-deconz==0.11.0 +zigpy-deconz==0.11.1 # homeassistant.components.zha zigpy-xbee==0.13.0 @@ -2366,7 +2366,7 @@ zigpy-zigate==0.7.3 zigpy-znp==0.3.0 # homeassistant.components.zha -zigpy==0.28.2 +zigpy==0.29.0 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a3ad62796b0..d82be8f0411 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1147,13 +1147,13 @@ zeep[async]==4.0.0 zeroconf==0.28.7 # homeassistant.components.zha -zha-quirks==0.0.50 +zha-quirks==0.0.51 # homeassistant.components.zha zigpy-cc==0.5.2 # homeassistant.components.zha -zigpy-deconz==0.11.0 +zigpy-deconz==0.11.1 # homeassistant.components.zha zigpy-xbee==0.13.0 @@ -1165,4 +1165,4 @@ zigpy-zigate==0.7.3 zigpy-znp==0.3.0 # homeassistant.components.zha -zigpy==0.28.2 +zigpy==0.29.0 From cdda5900e588194d14b8840342eef88821cd39cc Mon Sep 17 00:00:00 2001 From: Mike Keesey Date: Thu, 31 Dec 2020 10:48:36 -0700 Subject: [PATCH 018/507] Upgrade pubnubsub-handler to 1.0.9 (#44542) This resolves an error thrown on shutdown of the wink component --- homeassistant/components/wink/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/wink/manifest.json b/homeassistant/components/wink/manifest.json index a1bae648292..7d357d88e55 100644 --- a/homeassistant/components/wink/manifest.json +++ b/homeassistant/components/wink/manifest.json @@ -2,7 +2,7 @@ "domain": "wink", "name": "Wink", "documentation": "https://www.home-assistant.io/integrations/wink", - "requirements": ["pubnubsub-handler==1.0.8", "python-wink==1.10.5"], + "requirements": ["pubnubsub-handler==1.0.9", "python-wink==1.10.5"], "dependencies": ["configurator", "http"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 9470222ebef..eed13d62c14 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1174,7 +1174,7 @@ psutil==5.8.0 ptvsd==4.3.2 # homeassistant.components.wink -pubnubsub-handler==1.0.8 +pubnubsub-handler==1.0.9 # homeassistant.components.pulseaudio_loopback pulsectl==20.2.4 From 1c8fbc7e6a3efe62f5ac3d609e62e1fb50265162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 31 Dec 2020 20:14:07 +0200 Subject: [PATCH 019/507] Upgrade codespell to 2.0.0 (#44695) * Upgrade codespell to 2.0.0 * Fix newly found spelling errors --- .pre-commit-config.yaml | 2 +- homeassistant/components/rflink/__init__.py | 2 +- homeassistant/helpers/service.py | 6 ++++-- requirements_test_pre_commit.txt | 2 +- tests/components/homematicip_cloud/conftest.py | 2 +- tests/components/homematicip_cloud/test_climate.py | 2 +- tests/test_loader.py | 2 +- 7 files changed, 10 insertions(+), 8 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c96a990433a..11cd4aa7731 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,7 +13,7 @@ repos: - --quiet files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$ - repo: https://github.com/codespell-project/codespell - rev: v1.17.1 + rev: v2.0.0 hooks: - id: codespell args: diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py index 68b6d841654..116b2464213 100644 --- a/homeassistant/components/rflink/__init__.py +++ b/homeassistant/components/rflink/__init__.py @@ -520,7 +520,7 @@ class RflinkCommand(RflinkDevice): if self._wait_ack: # Puts command on outgoing buffer then waits for Rflink to confirm - # the command has been send out in the ether. + # the command has been sent out. await self._protocol.send_command_ack(self._device_id, cmd) else: # Puts command on outgoing buffer and returns straight away. diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 25a88bb59cb..c95f942c6dc 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -128,13 +128,15 @@ async def async_call_from_config( ) -> None: """Call a service based on a config hash.""" try: - parms = async_prepare_call_from_config(hass, config, variables, validate_config) + params = async_prepare_call_from_config( + hass, config, variables, validate_config + ) except HomeAssistantError as ex: if blocking: raise _LOGGER.error(ex) else: - await hass.services.async_call(*parms, blocking, context) + await hass.services.async_call(*params, blocking, context) @ha.callback diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index e479f5e9ac1..953c7d75394 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -2,7 +2,7 @@ bandit==1.7.0 black==20.8b1 -codespell==1.17.1 +codespell==2.0.0 flake8-docstrings==1.5.0 flake8==3.8.4 isort==5.5.3 diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py index 9764ee74e22..8bf792c259f 100644 --- a/tests/components/homematicip_cloud/conftest.py +++ b/tests/components/homematicip_cloud/conftest.py @@ -46,7 +46,7 @@ def mock_connection_fixture() -> AsyncConnection: @pytest.fixture(name="hmip_config_entry") def hmip_config_entry_fixture() -> config_entries.ConfigEntry: - """Create a mock config entriy for homematic ip cloud.""" + """Create a mock config entry for homematic ip cloud.""" entry_data = { HMIPC_HAPID: HAPID, HMIPC_AUTHTOKEN: AUTH_TOKEN, diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py index 6d5c3fb6060..dc850fac026 100644 --- a/tests/components/homematicip_cloud/test_climate.py +++ b/tests/components/homematicip_cloud/test_climate.py @@ -129,7 +129,7 @@ async def test_hmip_heating_group_heat(hass, default_mock_hap_factory): ha_state = hass.states.get(entity_id) assert ha_state.attributes[ATTR_PRESET_MODE] == "STD" - # Not required for hmip, but a posiblity to send no temperature. + # Not required for hmip, but a possibility to send no temperature. await hass.services.async_call( "climate", "set_temperature", diff --git a/tests/test_loader.py b/tests/test_loader.py index c05240893de..64c8c5764cf 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -97,7 +97,7 @@ async def test_helpers_wrapper(hass): async def test_custom_component_name(hass): - """Test the name attribte of custom components.""" + """Test the name attribute of custom components.""" integration = await loader.async_get_integration(hass, "test_standalone") int_comp = integration.get_component() assert int_comp.__name__ == "custom_components.test_standalone" From fe9a254017dd11c40c1f5be71b0dff527430c372 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 31 Dec 2020 10:22:24 -0800 Subject: [PATCH 020/507] Fix legacy nest api binary_sensor initialization (#44674) --- .../components/nest/binary_sensor.py | 2 +- .../components/nest/legacy/binary_sensor.py | 2 +- tests/components/nest/test_init_legacy.py | 87 +++++++++++++++++++ 3 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 tests/components/nest/test_init_legacy.py diff --git a/homeassistant/components/nest/binary_sensor.py b/homeassistant/components/nest/binary_sensor.py index dc58dd2856f..d49ec8535cc 100644 --- a/homeassistant/components/nest/binary_sensor.py +++ b/homeassistant/components/nest/binary_sensor.py @@ -4,7 +4,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType from .const import DATA_SDM -from .legacy.sensor import async_setup_legacy_entry +from .legacy.binary_sensor import async_setup_legacy_entry async def async_setup_entry( diff --git a/homeassistant/components/nest/legacy/binary_sensor.py b/homeassistant/components/nest/legacy/binary_sensor.py index 4470bd14676..32c30f747d2 100644 --- a/homeassistant/components/nest/legacy/binary_sensor.py +++ b/homeassistant/components/nest/legacy/binary_sensor.py @@ -61,7 +61,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """ -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_legacy_entry(hass, entry, async_add_entities): """Set up a Nest binary sensor based on a config entry.""" nest = hass.data[DATA_NEST] diff --git a/tests/components/nest/test_init_legacy.py b/tests/components/nest/test_init_legacy.py new file mode 100644 index 00000000000..f85fcdaa749 --- /dev/null +++ b/tests/components/nest/test_init_legacy.py @@ -0,0 +1,87 @@ +"""Test basic initialization for the Legacy Nest API using mocks for the Nest python library.""" + +import time + +from homeassistant.setup import async_setup_component + +from tests.async_mock import MagicMock, PropertyMock, patch +from tests.common import MockConfigEntry + +DOMAIN = "nest" + +CONFIG = { + "nest": { + "client_id": "some-client-id", + "client_secret": "some-client-secret", + }, +} + +CONFIG_ENTRY_DATA = { + "auth_implementation": "local", + "tokens": { + "expires_at": time.time() + 86400, + "access_token": { + "token": "some-token", + }, + }, +} + + +def make_thermostat(): + """Make a mock thermostat with dummy values.""" + device = MagicMock() + type(device).device_id = PropertyMock(return_value="a.b.c.d.e.f.g") + type(device).name = PropertyMock(return_value="My Thermostat") + type(device).name_long = PropertyMock(return_value="My Thermostat") + type(device).serial = PropertyMock(return_value="serial-number") + type(device).mode = "off" + type(device).hvac_state = "off" + type(device).target = PropertyMock(return_value=31.0) + type(device).temperature = PropertyMock(return_value=30.1) + type(device).min_temperature = PropertyMock(return_value=10.0) + type(device).max_temperature = PropertyMock(return_value=50.0) + type(device).humidity = PropertyMock(return_value=40.4) + type(device).software_version = PropertyMock(return_value="a.b.c") + return device + + +async def test_thermostat(hass): + """Test simple initialization for thermostat entities.""" + + thermostat = make_thermostat() + + structure = MagicMock() + type(structure).name = PropertyMock(return_value="My Room") + type(structure).thermostats = PropertyMock(return_value=[thermostat]) + type(structure).eta = PropertyMock(return_value="away") + + nest = MagicMock() + type(nest).structures = PropertyMock(return_value=[structure]) + + config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA) + config_entry.add_to_hass(hass) + with patch("homeassistant.components.nest.legacy.Nest", return_value=nest), patch( + "homeassistant.components.nest.legacy.sensor._VALID_SENSOR_TYPES", + ["humidity", "temperature"], + ), patch( + "homeassistant.components.nest.legacy.binary_sensor._VALID_BINARY_SENSOR_TYPES", + {"fan": None}, + ): + assert await async_setup_component(hass, DOMAIN, CONFIG) + await hass.async_block_till_done() + + climate = hass.states.get("climate.my_thermostat") + assert climate is not None + assert climate.state == "off" + + temperature = hass.states.get("sensor.my_thermostat_temperature") + assert temperature is not None + assert temperature.state == "-1.1" + + humidity = hass.states.get("sensor.my_thermostat_humidity") + assert humidity is not None + assert humidity.state == "40.4" + + fan = hass.states.get("binary_sensor.my_thermostat_fan") + assert fan is not None + assert fan.state == "on" From f1dff973dcbde1f64fbe9857da2782977259f751 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 31 Dec 2020 14:04:12 -0800 Subject: [PATCH 021/507] Fix broken test test_auto_purge in recorder (#44687) * Fix failing test due to edge-of-2021 bug * Rewrite test_auto_purge to make the intent more clear --- tests/components/recorder/test_init.py | 45 +++++++++++++++++++++----- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index fcda7b0bb67..41c1f52b993 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -23,7 +23,7 @@ from homeassistant.util import dt as dt_util from .common import wait_recording_done from tests.async_mock import patch -from tests.common import async_fire_time_changed, get_test_home_assistant +from tests.common import fire_time_changed, get_test_home_assistant def test_saving_state(hass, hass_recorder): @@ -351,8 +351,15 @@ async def test_defaults_set(hass): assert recorder_config["purge_keep_days"] == 10 +def run_tasks_at_time(hass, test_time): + """Advance the clock and wait for any callbacks to finish.""" + fire_time_changed(hass, test_time) + hass.block_till_done() + hass.data[DATA_INSTANCE].block_till_done() + + def test_auto_purge(hass_recorder): - """Test saving and restoring a state.""" + """Test periodic purge alarm scheduling.""" hass = hass_recorder() original_tz = dt_util.DEFAULT_TIME_ZONE @@ -360,18 +367,40 @@ def test_auto_purge(hass_recorder): tz = dt_util.get_time_zone("Europe/Copenhagen") dt_util.set_default_time_zone(tz) + # Purging is schedule to happen at 4:12am every day. Exercise this behavior + # by firing alarms and advancing the clock around this time. Pick an arbitrary + # year in the future to avoid boundary conditions relative to the current date. + # + # The clock is started at 4:15am then advanced forward below now = dt_util.utcnow() - test_time = tz.localize(datetime(now.year + 1, 1, 1, 4, 12, 0)) - async_fire_time_changed(hass, test_time) + test_time = tz.localize(datetime(now.year + 2, 1, 1, 4, 15, 0)) + run_tasks_at_time(hass, test_time) with patch( "homeassistant.components.recorder.purge.purge_old_data", return_value=True ) as purge_old_data: - for delta in (-1, 0, 1): - async_fire_time_changed(hass, test_time + timedelta(seconds=delta)) - hass.block_till_done() - hass.data[DATA_INSTANCE].block_till_done() + # Advance one day, and the purge task should run + test_time = test_time + timedelta(days=1) + run_tasks_at_time(hass, test_time) + assert len(purge_old_data.mock_calls) == 1 + purge_old_data.reset_mock() + + # Advance one day, and the purge task should run again + test_time = test_time + timedelta(days=1) + run_tasks_at_time(hass, test_time) + assert len(purge_old_data.mock_calls) == 1 + + purge_old_data.reset_mock() + + # Advance less than one full day. The alarm should not yet fire. + test_time = test_time + timedelta(hours=23) + run_tasks_at_time(hass, test_time) + assert len(purge_old_data.mock_calls) == 0 + + # Advance to the next day and fire the alarm again + test_time = test_time + timedelta(hours=1) + run_tasks_at_time(hass, test_time) assert len(purge_old_data.mock_calls) == 1 dt_util.set_default_time_zone(original_tz) From 74b480e9d472117e9c4e0067a0f6c362369ddd59 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Thu, 31 Dec 2020 17:44:04 -0500 Subject: [PATCH 022/507] Bump elkm1-lib to 0.8.10 (#44714) --- homeassistant/components/elkm1/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/elkm1/manifest.json b/homeassistant/components/elkm1/manifest.json index 769e5c37dd7..2077890d3d2 100644 --- a/homeassistant/components/elkm1/manifest.json +++ b/homeassistant/components/elkm1/manifest.json @@ -2,7 +2,7 @@ "domain": "elkm1", "name": "Elk-M1 Control", "documentation": "https://www.home-assistant.io/integrations/elkm1", - "requirements": ["elkm1-lib==0.8.8"], + "requirements": ["elkm1-lib==0.8.10"], "codeowners": ["@gwww", "@bdraco"], "config_flow": true } diff --git a/requirements_all.txt b/requirements_all.txt index eed13d62c14..4f9fedf31fe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -535,7 +535,7 @@ elgato==1.0.0 eliqonline==1.2.2 # homeassistant.components.elkm1 -elkm1-lib==0.8.8 +elkm1-lib==0.8.10 # homeassistant.components.mobile_app emoji==0.5.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d82be8f0411..7fdcea45a33 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -281,7 +281,7 @@ eebrightbox==0.0.4 elgato==1.0.0 # homeassistant.components.elkm1 -elkm1-lib==0.8.8 +elkm1-lib==0.8.10 # homeassistant.components.mobile_app emoji==0.5.4 From 61f137c7c65f41117ea719b13f64d36eff467674 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 1 Jan 2021 00:38:38 +0100 Subject: [PATCH 023/507] Upgrade pytz to >=2020.5 (#44702) --- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7eb736c0037..e25dcbbb1f9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ paho-mqtt==1.5.1 pillow==7.2.0 pip>=8.0.3,<20.3 python-slugify==4.0.1 -pytz>=2020.1 +pytz>=2020.5 pyyaml==5.3.1 requests==2.25.0 ruamel.yaml==0.15.100 diff --git a/requirements.txt b/requirements.txt index cbe339fd835..a4c749e4c54 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ PyJWT==1.7.1 cryptography==3.2 pip>=8.0.3,<20.3 python-slugify==4.0.1 -pytz>=2020.1 +pytz>=2020.5 pyyaml==5.3.1 requests==2.25.0 ruamel.yaml==0.15.100 diff --git a/setup.py b/setup.py index c9acb4d82d7..e553dc0b1f4 100755 --- a/setup.py +++ b/setup.py @@ -47,7 +47,7 @@ REQUIRES = [ "cryptography==3.2", "pip>=8.0.3,<20.3", "python-slugify==4.0.1", - "pytz>=2020.1", + "pytz>=2020.5", "pyyaml==5.3.1", "requests==2.25.0", "ruamel.yaml==0.15.100", From edee0682baa1788f63e81612e9bf0edaace11564 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Thu, 31 Dec 2020 18:48:44 -0500 Subject: [PATCH 024/507] Bump upb-lib to 0.4.12 (#44721) --- homeassistant/components/upb/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/upb/manifest.json b/homeassistant/components/upb/manifest.json index 1f170030df6..9ad43117225 100644 --- a/homeassistant/components/upb/manifest.json +++ b/homeassistant/components/upb/manifest.json @@ -2,7 +2,7 @@ "domain": "upb", "name": "Universal Powerline Bus (UPB)", "documentation": "https://www.home-assistant.io/integrations/upb", - "requirements": ["upb_lib==0.4.11"], + "requirements": ["upb_lib==0.4.12"], "codeowners": ["@gwww"], "config_flow": true } diff --git a/requirements_all.txt b/requirements_all.txt index 4f9fedf31fe..5de8b5b76c0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2223,7 +2223,7 @@ uEagle==0.0.2 unifiled==0.11 # homeassistant.components.upb -upb_lib==0.4.11 +upb_lib==0.4.12 # homeassistant.components.upcloud upcloud-api==0.4.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7fdcea45a33..98e79441bbc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1088,7 +1088,7 @@ twilio==6.32.0 twinkly-client==0.0.2 # homeassistant.components.upb -upb_lib==0.4.11 +upb_lib==0.4.12 # homeassistant.components.upcloud upcloud-api==0.4.5 From 12b7b2098ded472ca46d39ea256a79d989d90be8 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 1 Jan 2021 01:28:15 +0100 Subject: [PATCH 025/507] Upgrade sqlalchemy to 1.3.22 (#44698) --- homeassistant/components/recorder/manifest.json | 2 +- homeassistant/components/sql/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 6d2311c76e7..67d3bdd0f5b 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -2,7 +2,7 @@ "domain": "recorder", "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", - "requirements": ["sqlalchemy==1.3.20"], + "requirements": ["sqlalchemy==1.3.22"], "codeowners": [], "quality_scale": "internal" } diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index 459124820ff..3b21d32b110 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -2,6 +2,6 @@ "domain": "sql", "name": "SQL", "documentation": "https://www.home-assistant.io/integrations/sql", - "requirements": ["sqlalchemy==1.3.20"], + "requirements": ["sqlalchemy==1.3.22"], "codeowners": ["@dgomes"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e25dcbbb1f9..33e3ecd61cb 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -26,7 +26,7 @@ pytz>=2020.5 pyyaml==5.3.1 requests==2.25.0 ruamel.yaml==0.15.100 -sqlalchemy==1.3.20 +sqlalchemy==1.3.22 voluptuous-serialize==2.4.0 voluptuous==0.12.1 yarl==1.4.2 diff --git a/requirements_all.txt b/requirements_all.txt index 5de8b5b76c0..c1d1340095c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2094,7 +2094,7 @@ spotipy==2.16.1 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.20 +sqlalchemy==1.3.22 # homeassistant.components.srp_energy srpenergy==1.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 98e79441bbc..1f1c0eff147 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1031,7 +1031,7 @@ spotipy==2.16.1 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.20 +sqlalchemy==1.3.22 # homeassistant.components.srp_energy srpenergy==1.3.2 From 41ebfcdc9e01384f82bba7580ba7ee7bc3777404 Mon Sep 17 00:00:00 2001 From: Oncleben31 Date: Fri, 1 Jan 2021 01:51:03 +0100 Subject: [PATCH 026/507] =?UTF-8?q?Add=20device=20info=20to=20M=C3=A9t?= =?UTF-8?q?=C3=A9o-France=20(#44457)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homeassistant/components/meteo_france/const.py | 2 ++ homeassistant/components/meteo_france/sensor.py | 13 +++++++++++++ homeassistant/components/meteo_france/weather.py | 13 +++++++++++++ 3 files changed, 28 insertions(+) diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py index d642d3c6e0f..a2e9eeb2799 100644 --- a/homeassistant/components/meteo_france/const.py +++ b/homeassistant/components/meteo_france/const.py @@ -36,6 +36,8 @@ COORDINATOR_RAIN = "coordinator_rain" COORDINATOR_ALERT = "coordinator_alert" UNDO_UPDATE_LISTENER = "undo_update_listener" ATTRIBUTION = "Data provided by Météo-France" +MODEL = "Météo-France mobile API" +MANUFACTURER = "Météo-France" CONF_CITY = "city" FORECAST_MODE_HOURLY = "hourly" diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index 8e6b036202f..201cca7ae9d 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -29,6 +29,8 @@ from .const import ( ENTITY_ICON, ENTITY_NAME, ENTITY_UNIT, + MANUFACTURER, + MODEL, SENSOR_TYPES, ) @@ -94,6 +96,17 @@ class MeteoFranceSensor(CoordinatorEntity): """Return the name.""" return self._name + @property + def device_info(self): + """Return the device info.""" + return { + "identifiers": {(DOMAIN, self.platform.config_entry.unique_id)}, + "name": self.coordinator.name, + "manufacturer": MANUFACTURER, + "model": MODEL, + "entry_type": "service", + } + @property def state(self): """Return the state.""" diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index ffb468574b8..09e062cc715 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -28,6 +28,8 @@ from .const import ( DOMAIN, FORECAST_MODE_DAILY, FORECAST_MODE_HOURLY, + MANUFACTURER, + MODEL, ) _LOGGER = logging.getLogger(__name__) @@ -83,6 +85,17 @@ class MeteoFranceWeather(CoordinatorEntity, WeatherEntity): """Return the name of the sensor.""" return self._city_name + @property + def device_info(self): + """Return the device info.""" + return { + "identifiers": {(DOMAIN, self.platform.config_entry.unique_id)}, + "name": self.coordinator.name, + "manufacturer": MANUFACTURER, + "model": MODEL, + "entry_type": "service", + } + @property def condition(self): """Return the current condition.""" From db6bd22fc98e53a91e5b4967a7f4d3a44ba0cc88 Mon Sep 17 00:00:00 2001 From: hung2kgithub <73251414+hung2kgithub@users.noreply.github.com> Date: Fri, 1 Jan 2021 08:59:42 +0800 Subject: [PATCH 027/507] Add Chinese (Hong Kong) to Google Cloud TTS (#44689) --- homeassistant/components/google_cloud/tts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index 69b276fc75f..6ffa3a9acd1 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -25,6 +25,7 @@ CONF_TEXT_TYPE = "text_type" SUPPORTED_LANGUAGES = [ "ar-XA", "bn-IN", + "yue-HK", "cmn-CN", "cmn-TW", "cs-CZ", From 787027958d17fb095d1347e46c057e8d62ec3586 Mon Sep 17 00:00:00 2001 From: Jason Cronquist Date: Thu, 31 Dec 2020 19:15:39 -0700 Subject: [PATCH 028/507] Use the async_call context in result of call_service (#44458) --- homeassistant/components/websocket_api/commands.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 80ea945834b..fb9ffea2904 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -132,15 +132,16 @@ async def handle_call_service(hass, connection, msg): blocking = False try: + context = connection.context(msg) await hass.services.async_call( msg["domain"], msg["service"], msg.get("service_data"), blocking, - connection.context(msg), + context, ) connection.send_message( - messages.result_message(msg["id"], {"context": connection.context(msg)}) + messages.result_message(msg["id"], {"context": context}) ) except ServiceNotFound as err: if err.domain == msg["domain"] and err.service == msg["service"]: From 99eed915d68750bb5ba61b12522535d3d67a2a7d Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 1 Jan 2021 06:35:42 +0100 Subject: [PATCH 029/507] Upgrade slixmpp to 1.6.0 (#44693) --- homeassistant/components/xmpp/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xmpp/manifest.json b/homeassistant/components/xmpp/manifest.json index 2453ce61b05..b56d43b9c9c 100644 --- a/homeassistant/components/xmpp/manifest.json +++ b/homeassistant/components/xmpp/manifest.json @@ -2,6 +2,6 @@ "domain": "xmpp", "name": "Jabber (XMPP)", "documentation": "https://www.home-assistant.io/integrations/xmpp", - "requirements": ["slixmpp==1.5.2"], + "requirements": ["slixmpp==1.6.0"], "codeowners": ["@fabaff", "@flowolf"] } diff --git a/requirements_all.txt b/requirements_all.txt index c1d1340095c..d3ee43ca8d9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2034,7 +2034,7 @@ slackclient==2.5.0 sleepyq==0.8.1 # homeassistant.components.xmpp -slixmpp==1.5.2 +slixmpp==1.6.0 # homeassistant.components.smart_meter_texas smart-meter-texas==0.4.0 From 5e0eea21d4f64e3bb6bf56d480b556aeb13fcd48 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 1 Jan 2021 07:02:59 +0100 Subject: [PATCH 030/507] Upgrade pyowm to 3.1.1 (#44706) --- homeassistant/components/openweathermap/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/openweathermap/manifest.json b/homeassistant/components/openweathermap/manifest.json index 4ebdaf44c9e..e355e2e4752 100644 --- a/homeassistant/components/openweathermap/manifest.json +++ b/homeassistant/components/openweathermap/manifest.json @@ -3,6 +3,6 @@ "name": "OpenWeatherMap", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/openweathermap", - "requirements": ["pyowm==3.1.0"], + "requirements": ["pyowm==3.1.1"], "codeowners": ["@fabaff", "@freekode", "@nzapponi"] } diff --git a/requirements_all.txt b/requirements_all.txt index d3ee43ca8d9..f04f2c7f3d8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1598,7 +1598,7 @@ pyotgw==1.0b1 pyotp==2.3.0 # homeassistant.components.openweathermap -pyowm==3.1.0 +pyowm==3.1.1 # homeassistant.components.onewire pyownet==0.10.0.post1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1f1c0eff147..c0698e79f5a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -811,7 +811,7 @@ pyotgw==1.0b1 pyotp==2.3.0 # homeassistant.components.openweathermap -pyowm==3.1.0 +pyowm==3.1.1 # homeassistant.components.onewire pyownet==0.10.0.post1 From 681f76b99dd37f331f97ef969fd570fd52384319 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 1 Jan 2021 01:40:08 -1000 Subject: [PATCH 031/507] Fix rest notify GET without params configured (#44723) --- homeassistant/components/rest/notify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/rest/notify.py b/homeassistant/components/rest/notify.py index 3e4f97d5bc7..15b871cde5c 100644 --- a/homeassistant/components/rest/notify.py +++ b/homeassistant/components/rest/notify.py @@ -197,7 +197,7 @@ class RestNotificationService(BaseNotificationService): response = requests.get( self._resource, headers=self._headers, - params=self._params.update(data), + params={**self._params, **data} if self._params else data, timeout=10, auth=self._auth, verify=self._verify_ssl, From 94825b3e158b16ab8e89fbddd0c732b9ed90e6e6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 1 Jan 2021 01:44:53 -1000 Subject: [PATCH 032/507] Do not restore unavailable state for august locks (#44722) --- homeassistant/components/august/activity.py | 19 ++++++++++--------- homeassistant/components/august/sensor.py | 6 +++--- tests/components/august/test_lock.py | 3 +-- tests/components/august/test_sensor.py | 4 ++-- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/august/activity.py b/homeassistant/components/august/activity.py index c7a7d68d959..d972fbf5281 100644 --- a/homeassistant/components/august/activity.py +++ b/homeassistant/components/august/activity.py @@ -11,7 +11,7 @@ from .subscriber import AugustSubscriberMixin _LOGGER = logging.getLogger(__name__) ACTIVITY_STREAM_FETCH_LIMIT = 10 -ACTIVITY_CATCH_UP_FETCH_LIMIT = 1000 +ACTIVITY_CATCH_UP_FETCH_LIMIT = 2500 class ActivityStream(AugustSubscriberMixin): @@ -102,11 +102,14 @@ class ActivityStream(AugustSubscriberMixin): def _process_newer_device_activities(self, activities): updated_device_ids = set() for activity in activities: - self._latest_activities_by_id_type.setdefault(activity.device_id, {}) + device_id = activity.device_id + activity_type = activity.activity_type - lastest_activity = self._latest_activities_by_id_type[ - activity.device_id - ].get(activity.activity_type) + self._latest_activities_by_id_type.setdefault(device_id, {}) + + lastest_activity = self._latest_activities_by_id_type[device_id].get( + activity_type + ) # Ignore activities that are older than the latest one if ( @@ -115,10 +118,8 @@ class ActivityStream(AugustSubscriberMixin): ): continue - self._latest_activities_by_id_type[activity.device_id][ - activity.activity_type - ] = activity + self._latest_activities_by_id_type[device_id][activity_type] = activity - updated_device_ids.add(activity.device_id) + updated_device_ids.add(device_id) return updated_device_ids diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py index c3f5f05ceef..6004a07f605 100644 --- a/homeassistant/components/august/sensor.py +++ b/homeassistant/components/august/sensor.py @@ -4,7 +4,7 @@ import logging from august.activity import ActivityType from homeassistant.components.sensor import DEVICE_CLASS_BATTERY -from homeassistant.const import ATTR_ENTITY_PICTURE, PERCENTAGE +from homeassistant.const import ATTR_ENTITY_PICTURE, PERCENTAGE, STATE_UNAVAILABLE from homeassistant.core import callback from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_registry import async_get_registry @@ -157,8 +157,8 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, Entity): self._device_id, [ActivityType.LOCK_OPERATION] ) + self._available = True if lock_activity is not None: - self._available = True self._state = lock_activity.operated_by self._operated_remote = lock_activity.operated_remote self._operated_keypad = lock_activity.operated_keypad @@ -193,7 +193,7 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, Entity): await super().async_added_to_hass() last_state = await self.async_get_last_state() - if not last_state: + if not last_state or last_state.state == STATE_UNAVAILABLE: return self._state = last_state.state diff --git a/tests/components/august/test_lock.py b/tests/components/august/test_lock.py index f36a5e3f180..8baefa68271 100644 --- a/tests/components/august/test_lock.py +++ b/tests/components/august/test_lock.py @@ -6,7 +6,6 @@ from homeassistant.const import ( SERVICE_LOCK, SERVICE_UNLOCK, STATE_LOCKED, - STATE_UNAVAILABLE, STATE_UNKNOWN, STATE_UNLOCKED, ) @@ -98,7 +97,7 @@ async def test_one_lock_operation(hass): assert lock_operator_sensor assert ( hass.states.get("sensor.online_with_doorsense_name_operator").state - == STATE_UNAVAILABLE + == STATE_UNKNOWN ) diff --git a/tests/components/august/test_sensor.py b/tests/components/august/test_sensor.py index 51e00b9d09f..fb7ddfde979 100644 --- a/tests/components/august/test_sensor.py +++ b/tests/components/august/test_sensor.py @@ -1,6 +1,6 @@ """The sensor tests for the august platform.""" -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, STATE_UNAVAILABLE +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, STATE_UNKNOWN from tests.components.august.mocks import ( _create_august_with_devices, @@ -120,7 +120,7 @@ async def test_create_lock_with_low_battery_linked_keypad(hass): ) assert ( hass.states.get("sensor.a6697750d607098bae8d6baa11ef8063_name_operator").state - == STATE_UNAVAILABLE + == STATE_UNKNOWN ) From 7415dacec93e04e2c7fc4f4b82686bc45508e492 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 1 Jan 2021 12:51:27 +0100 Subject: [PATCH 033/507] Add Python 3.9 to CI (#41373) --- .github/workflows/ci.yaml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a33cd59f227..c39185f3073 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -585,7 +585,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.8] + python-version: [3.7, 3.8, 3.9] container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub @@ -609,6 +609,10 @@ jobs: Create full Python ${{ matrix.python-version }} virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' run: | + # Temporary addition of cmake, needed to build some Python 3.9 packages + apt-get update + apt-get -y install cmake + python -m venv venv . venv/bin/activate pip install -U "pip<20.3" setuptools wheel @@ -692,7 +696,7 @@ jobs: strategy: matrix: group: [1, 2, 3, 4] - python-version: [3.7, 3.8] + python-version: [3.7, 3.8, 3.9] name: >- Run tests Python ${{ matrix.python-version }} (group ${{ matrix.group }}) container: homeassistant/ci-azure:${{ matrix.python-version }} From 2ef25e7414fdff80f0e600e41620aa04ab3babe8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 1 Jan 2021 02:03:34 -1000 Subject: [PATCH 034/507] Fix script wait templates with now/utcnow (#44717) --- homeassistant/helpers/event.py | 6 +++--- homeassistant/helpers/script.py | 15 +++++++++++---- tests/helpers/test_event.py | 27 +++++++++++++++++++++++++++ tests/helpers/test_script.py | 26 ++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 661e1a11b56..f06ac8aca3f 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -715,9 +715,9 @@ def async_track_template( hass.async_run_hass_job( job, - event.data.get("entity_id"), - event.data.get("old_state"), - event.data.get("new_state"), + event and event.data.get("entity_id"), + event and event.data.get("old_state"), + event and event.data.get("new_state"), ) info = async_track_template_result( diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 48a662e3a81..77c842a27fe 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -62,7 +62,11 @@ from homeassistant.core import ( callback, ) 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.event import ( + TrackTemplate, + async_call_later, + async_track_template_result, +) from homeassistant.helpers.script_variables import ScriptVariables from homeassistant.helpers.trigger import ( async_initialize_triggers, @@ -355,7 +359,7 @@ class _ScriptRun: return @callback - def async_script_wait(entity_id, from_s, to_s): + def _async_script_wait(event, updates): """Handle script after template condition is true.""" self._variables["wait"] = { "remaining": to_context.remaining if to_context else delay, @@ -364,9 +368,12 @@ class _ScriptRun: done.set() to_context = None - unsub = async_track_template( - self._hass, wait_template, async_script_wait, self._variables + info = async_track_template_result( + self._hass, + [TrackTemplate(wait_template, self._variables)], + _async_script_wait, ) + unsub = info.async_remove self._changed() done = asyncio.Event() diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 1b9ddc52191..9c7ddb09f85 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -908,6 +908,33 @@ async def test_track_template_error_can_recover(hass, caplog): assert "UndefinedError" not in caplog.text +async def test_track_template_time_change(hass, caplog): + """Test tracking template with time change.""" + template_error = Template("{{ utcnow().minute % 2 == 0 }}", hass) + calls = [] + + @ha.callback + def error_callback(entity_id, old_state, new_state): + calls.append((entity_id, old_state, new_state)) + + start_time = dt_util.utcnow() + timedelta(hours=24) + time_that_will_not_match_right_away = start_time.replace(minute=1, second=0) + with patch( + "homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away + ): + async_track_template(hass, template_error, error_callback) + await hass.async_block_till_done() + assert not calls + + first_time = start_time.replace(minute=2, second=0) + with patch("homeassistant.util.dt.utcnow", return_value=first_time): + async_fire_time_changed(hass, first_time) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0] == (None, None, None) + + async def test_track_template_result(hass): """Test tracking template.""" specific_runs = [] diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 92666335f28..c81ed681d42 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -780,6 +780,32 @@ async def test_wait_template_variables_in(hass): assert not script_obj.is_running +async def test_wait_template_with_utcnow(hass): + """Test the wait template with utcnow.""" + sequence = cv.SCRIPT_SCHEMA({"wait_template": "{{ utcnow().hours == 12 }}"}) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + wait_started_flag = async_watch_for_action(script_obj, "wait") + start_time = dt_util.utcnow() + timedelta(hours=24) + + try: + hass.async_create_task(script_obj.async_run(context=Context())) + async_fire_time_changed(hass, start_time.replace(hour=5)) + assert not script_obj.is_running + async_fire_time_changed(hass, start_time.replace(hour=12)) + + await asyncio.wait_for(wait_started_flag.wait(), 1) + + assert script_obj.is_running + except (AssertionError, asyncio.TimeoutError): + await script_obj.async_stop() + raise + else: + async_fire_time_changed(hass, start_time.replace(hour=3)) + await hass.async_block_till_done() + + assert not script_obj.is_running + + @pytest.mark.parametrize("mode", ["no_timeout", "timeout_finish", "timeout_not_finish"]) @pytest.mark.parametrize("action_type", ["template", "trigger"]) async def test_wait_variables_out(hass, mode, action_type): From c4b11322c85472afd2a4b502f2a66d8d2a1443a1 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 1 Jan 2021 13:07:07 +0100 Subject: [PATCH 035/507] Updated certifi to >=2020.12.5 (#44701) --- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 33e3ecd61cb..bb2c4c8636b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ astral==1.10.1 async_timeout==3.0.1 attrs==19.3.0 bcrypt==3.1.7 -certifi>=2020.6.20 +certifi>=2020.12.5 ciso8601==2.1.3 cryptography==3.2 defusedxml==0.6.0 diff --git a/requirements.txt b/requirements.txt index a4c749e4c54..7492e794172 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ astral==1.10.1 async_timeout==3.0.1 attrs==19.3.0 bcrypt==3.1.7 -certifi>=2020.6.20 +certifi>=2020.12.5 ciso8601==2.1.3 httpx==0.16.1 importlib-metadata==1.6.0;python_version<'3.8' diff --git a/setup.py b/setup.py index e553dc0b1f4..59ea906344c 100755 --- a/setup.py +++ b/setup.py @@ -37,7 +37,7 @@ REQUIRES = [ "async_timeout==3.0.1", "attrs==19.3.0", "bcrypt==3.1.7", - "certifi>=2020.6.20", + "certifi>=2020.12.5", "ciso8601==2.1.3", "httpx==0.16.1", "importlib-metadata==1.6.0;python_version<'3.8'", From 051f6c0e72a7ff4a20d4175777c607e789e66aa5 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 1 Jan 2021 04:31:18 -0800 Subject: [PATCH 036/507] Increase test coverage for Nest SDM integration (#44718) --- .coveragerc | 5 --- tests/components/nest/test_events.py | 38 +++++++++++++++++ tests/components/nest/test_init_sdm.py | 57 +++++++++++++++++++++++--- 3 files changed, 90 insertions(+), 10 deletions(-) diff --git a/.coveragerc b/.coveragerc index 5778d541a68..ccbdaa9c6fa 100644 --- a/.coveragerc +++ b/.coveragerc @@ -580,13 +580,8 @@ omit = homeassistant/components/neato/vacuum.py homeassistant/components/nederlandse_spoorwegen/sensor.py homeassistant/components/nello/lock.py - homeassistant/components/nest/__init__.py homeassistant/components/nest/api.py - homeassistant/components/nest/binary_sensor.py - homeassistant/components/nest/camera.py - homeassistant/components/nest/climate.py homeassistant/components/nest/legacy/* - homeassistant/components/nest/sensor.py homeassistant/components/netatmo/__init__.py homeassistant/components/netatmo/api.py homeassistant/components/netatmo/camera.py diff --git a/tests/components/nest/test_events.py b/tests/components/nest/test_events.py index 7295d134087..692507d6ff9 100644 --- a/tests/components/nest/test_events.py +++ b/tests/components/nest/test_events.py @@ -253,3 +253,41 @@ async def test_unknown_event(hass): await hass.async_block_till_done() assert len(events) == 0 + + +async def test_unknown_device_id(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("sdm.devices.events.DoorbellChime.Chime", "invalid-device-id") + ) + await hass.async_block_till_done() + + assert len(events) == 0 + + +async def test_event_message_without_device_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"), + ) + timestamp = utcnow() + event = EventMessage( + { + "eventId": "some-event-id", + "timestamp": timestamp.isoformat(timespec="seconds"), + }, + auth=None, + ) + await subscriber.async_receive_event(event) + await hass.async_block_till_done() + + assert len(events) == 0 diff --git a/tests/components/nest/test_init_sdm.py b/tests/components/nest/test_init_sdm.py index cb17f81d18a..e49f3b89f27 100644 --- a/tests/components/nest/test_init_sdm.py +++ b/tests/components/nest/test_init_sdm.py @@ -7,11 +7,12 @@ and failure modes. import logging -from google_nest_sdm.exceptions import GoogleNestException +from google_nest_sdm.exceptions import AuthException, GoogleNestException from homeassistant.components.nest import DOMAIN from homeassistant.config_entries import ( ENTRY_STATE_LOADED, + ENTRY_STATE_NOT_LOADED, ENTRY_STATE_SETUP_ERROR, ENTRY_STATE_SETUP_RETRY, ) @@ -42,7 +43,7 @@ async def async_setup_sdm(hass, config=CONFIG): with patch( "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation" ): - await async_setup_component(hass, DOMAIN, config) + return await async_setup_component(hass, DOMAIN, config) async def test_setup_configuration_failure(hass, caplog): @@ -50,7 +51,8 @@ async def test_setup_configuration_failure(hass, caplog): config = CONFIG.copy() config[DOMAIN]["subscriber_id"] = "invalid-subscriber-format" - await async_setup_sdm(hass, config) + result = await async_setup_sdm(hass, config) + assert result entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 @@ -67,7 +69,8 @@ async def test_setup_susbcriber_failure(hass, caplog): "homeassistant.components.nest.GoogleNestSubscriber.start_async", side_effect=GoogleNestException(), ), caplog.at_level(logging.ERROR, logger="homeassistant.components.nest"): - await async_setup_sdm(hass) + result = await async_setup_sdm(hass) + assert result assert "Subscriber error:" in caplog.text entries = hass.config_entries.async_entries(DOMAIN) @@ -81,10 +84,54 @@ async def test_setup_device_manager_failure(hass, caplog): "homeassistant.components.nest.GoogleNestSubscriber.async_get_device_manager", side_effect=GoogleNestException(), ), caplog.at_level(logging.ERROR, logger="homeassistant.components.nest"): - await async_setup_sdm(hass) + result = await async_setup_sdm(hass) + assert result assert len(caplog.messages) == 1 assert "Device manager error:" in caplog.text entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 assert entries[0].state == ENTRY_STATE_SETUP_RETRY + + +async def test_subscriber_auth_failure(hass, caplog): + """Test configuration error.""" + with patch( + "homeassistant.components.nest.GoogleNestSubscriber.start_async", + side_effect=AuthException(), + ): + result = await async_setup_sdm(hass, CONFIG) + assert result + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state == ENTRY_STATE_SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["step_id"] == "reauth_confirm" + + +async def test_setup_missing_subscriber_id(hass, caplog): + """Test successful setup.""" + config = CONFIG + del config[DOMAIN]["subscriber_id"] + with caplog.at_level(logging.ERROR, logger="homeassistant.components.nest"): + result = await async_setup_sdm(hass, config) + assert not result + assert "Configuration option" in caplog.text + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state == ENTRY_STATE_NOT_LOADED + + +async def test_empty_config(hass, caplog): + """Test successful setup.""" + with caplog.at_level(logging.ERROR, logger="homeassistant.components.nest"): + result = await async_setup_component(hass, DOMAIN, {}) + assert result + assert not caplog.records + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 0 From b651f63ef036016ca223ddafd7c7188a3d67f7e6 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 1 Jan 2021 06:35:05 -0600 Subject: [PATCH 037/507] Suppress vizio logging API call failures to prevent no-op logs (#44388) --- .../components/vizio/media_player.py | 43 +++++++++++-------- tests/components/vizio/test_config_flow.py | 1 + tests/components/vizio/test_media_player.py | 27 ++++++++++-- 3 files changed, 50 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index 61c9ca54854..4c06c89692a 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -184,10 +184,10 @@ class VizioDevice(MediaPlayerEntity): async def async_update(self) -> None: """Retrieve latest state of the device.""" if not self._model: - self._model = await self._device.get_model_name() + self._model = await self._device.get_model_name(log_api_exception=False) if not self._sw_version: - self._sw_version = await self._device.get_version() + self._sw_version = await self._device.get_version(log_api_exception=False) is_on = await self._device.get_power_state(log_api_exception=False) @@ -236,7 +236,9 @@ class VizioDevice(MediaPlayerEntity): if not self._available_sound_modes: self._available_sound_modes = ( await self._device.get_setting_options( - VIZIO_AUDIO_SETTINGS, VIZIO_SOUND_MODE + VIZIO_AUDIO_SETTINGS, + VIZIO_SOUND_MODE, + log_api_exception=False, ) ) else: @@ -306,6 +308,7 @@ class VizioDevice(MediaPlayerEntity): setting_type, setting_name, new_value, + log_api_exception=False, ) async def async_added_to_hass(self) -> None: @@ -453,52 +456,58 @@ class VizioDevice(MediaPlayerEntity): """Select sound mode.""" if sound_mode in self._available_sound_modes: await self._device.set_setting( - VIZIO_AUDIO_SETTINGS, VIZIO_SOUND_MODE, sound_mode + VIZIO_AUDIO_SETTINGS, + VIZIO_SOUND_MODE, + sound_mode, + log_api_exception=False, ) async def async_turn_on(self) -> None: """Turn the device on.""" - await self._device.pow_on() + await self._device.pow_on(log_api_exception=False) async def async_turn_off(self) -> None: """Turn the device off.""" - await self._device.pow_off() + await self._device.pow_off(log_api_exception=False) async def async_mute_volume(self, mute: bool) -> None: """Mute the volume.""" if mute: - await self._device.mute_on() + await self._device.mute_on(log_api_exception=False) self._is_volume_muted = True else: - await self._device.mute_off() + await self._device.mute_off(log_api_exception=False) self._is_volume_muted = False async def async_media_previous_track(self) -> None: """Send previous channel command.""" - await self._device.ch_down() + await self._device.ch_down(log_api_exception=False) async def async_media_next_track(self) -> None: """Send next channel command.""" - await self._device.ch_up() + await self._device.ch_up(log_api_exception=False) async def async_select_source(self, source: str) -> None: """Select input source.""" if source in self._available_inputs: - await self._device.set_input(source) + await self._device.set_input(source, log_api_exception=False) elif source in self._get_additional_app_names(): await self._device.launch_app_config( **next( app["config"] for app in self._additional_app_configs if app["name"] == source - ) + ), + log_api_exception=False, ) elif source in self._available_apps: - await self._device.launch_app(source, self._all_apps) + await self._device.launch_app( + source, self._all_apps, log_api_exception=False + ) async def async_volume_up(self) -> None: """Increase volume of the device.""" - await self._device.vol_up(num=self._volume_step) + await self._device.vol_up(num=self._volume_step, log_api_exception=False) if self._volume_level is not None: self._volume_level = min( @@ -507,7 +516,7 @@ class VizioDevice(MediaPlayerEntity): async def async_volume_down(self) -> None: """Decrease volume of the device.""" - await self._device.vol_down(num=self._volume_step) + await self._device.vol_down(num=self._volume_step, log_api_exception=False) if self._volume_level is not None: self._volume_level = max( @@ -519,10 +528,10 @@ class VizioDevice(MediaPlayerEntity): if self._volume_level is not None: if volume > self._volume_level: num = int(self._max_volume * (volume - self._volume_level)) - await self._device.vol_up(num=num) + await self._device.vol_up(num=num, log_api_exception=False) self._volume_level = volume elif volume < self._volume_level: num = int(self._max_volume * (self._volume_level - volume)) - await self._device.vol_down(num=num) + await self._device.vol_down(num=num, log_api_exception=False) self._volume_level = volume diff --git a/tests/components/vizio/test_config_flow.py b/tests/components/vizio/test_config_flow.py index e966188afd2..5f33aa2be4a 100644 --- a/tests/components/vizio/test_config_flow.py +++ b/tests/components/vizio/test_config_flow.py @@ -858,6 +858,7 @@ async def test_zeroconf_ignore( async def test_zeroconf_no_unique_id( hass: HomeAssistantType, + vizio_guess_device_type: pytest.fixture, vizio_no_unique_id: pytest.fixture, ) -> None: """Test zeroconf discovery aborts when unique_id is None.""" diff --git a/tests/components/vizio/test_media_player.py b/tests/components/vizio/test_media_player.py index 996d46e08a7..0d11ec2289c 100644 --- a/tests/components/vizio/test_media_player.py +++ b/tests/components/vizio/test_media_player.py @@ -40,6 +40,7 @@ from homeassistant.components.vizio.const import ( CONF_ADDITIONAL_CONFIGS, CONF_APPS, CONF_VOLUME_STEP, + DEFAULT_VOLUME_STEP, DOMAIN, SERVICE_UPDATE_SETTING, VIZIO_SCHEMA, @@ -259,6 +260,7 @@ async def _test_service( **kwargs, ) -> None: """Test generic Vizio media player entity service.""" + kwargs["log_api_exception"] = False service_data = {ATTR_ENTITY_ID: ENTITY_ID} if additional_service_data: service_data.update(additional_service_data) @@ -378,13 +380,27 @@ async def test_services( {ATTR_INPUT_SOURCE: "USB"}, "USB", ) - await _test_service(hass, MP_DOMAIN, "vol_up", SERVICE_VOLUME_UP, None) - await _test_service(hass, MP_DOMAIN, "vol_down", SERVICE_VOLUME_DOWN, None) await _test_service( - hass, MP_DOMAIN, "vol_up", SERVICE_VOLUME_SET, {ATTR_MEDIA_VOLUME_LEVEL: 1} + hass, MP_DOMAIN, "vol_up", SERVICE_VOLUME_UP, None, num=DEFAULT_VOLUME_STEP ) await _test_service( - hass, MP_DOMAIN, "vol_down", SERVICE_VOLUME_SET, {ATTR_MEDIA_VOLUME_LEVEL: 0} + hass, MP_DOMAIN, "vol_down", SERVICE_VOLUME_DOWN, None, num=DEFAULT_VOLUME_STEP + ) + await _test_service( + hass, + MP_DOMAIN, + "vol_up", + SERVICE_VOLUME_SET, + {ATTR_MEDIA_VOLUME_LEVEL: 1}, + num=(100 - 15), + ) + await _test_service( + hass, + MP_DOMAIN, + "vol_down", + SERVICE_VOLUME_SET, + {ATTR_MEDIA_VOLUME_LEVEL: 0}, + num=(15 - 0), ) await _test_service(hass, MP_DOMAIN, "ch_up", SERVICE_MEDIA_NEXT_TRACK, None) await _test_service(hass, MP_DOMAIN, "ch_down", SERVICE_MEDIA_PREVIOUS_TRACK, None) @@ -394,6 +410,9 @@ async def test_services( "set_setting", SERVICE_SELECT_SOUND_MODE, {ATTR_SOUND_MODE: "Music"}, + "audio", + "eq", + "Music", ) # Test that the update_setting service does config validation/transformation correctly await _test_service( From 2f486543dfa3855afe886530352fb08d1f8815c9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 1 Jan 2021 13:47:01 +0100 Subject: [PATCH 038/507] Drop Python 3.7 support (#43805) --- .github/workflows/ci.yaml | 35 ++++++++----------- azure-pipelines-ci.yml | 6 +--- .../components/arcam_fmj/__init__.py | 2 -- .../components/system_log/__init__.py | 3 -- homeassistant/const.py | 6 ++-- homeassistant/util/logging.py | 2 -- 6 files changed, 18 insertions(+), 36 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c39185f3073..7c1c8bac6b6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,7 +11,7 @@ on: env: CACHE_VERSION: 1 - DEFAULT_PYTHON: 3.7 + DEFAULT_PYTHON: 3.8 PRE_COMMIT_HOME: ~/.cache/pre-commit jobs: @@ -521,13 +521,12 @@ jobs: needs: prepare-tests strategy: matrix: - python-version: [3.7] + python-version: [3.8] container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub uses: actions/checkout@v2 - - name: - Restore full Python ${{ matrix.python-version }} virtual environment + - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2 with: @@ -585,13 +584,12 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.8, 3.9] + python-version: [3.8, 3.9] container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub uses: actions/checkout@v2 - - name: - Restore full Python ${{ matrix.python-version }} virtual environment + - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2 with: @@ -605,8 +603,7 @@ jobs: ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ matrix.python-version }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('requirements_all.txt') }} ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ matrix.python-version }}-${{ hashFiles('requirements_test.txt') }} ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ matrix.python-version }}- - - name: - Create full Python ${{ matrix.python-version }} virtual environment + - name: Create full Python ${{ matrix.python-version }} virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' run: | # Temporary addition of cmake, needed to build some Python 3.9 packages @@ -626,13 +623,12 @@ jobs: needs: prepare-tests strategy: matrix: - python-version: [3.7] + python-version: [3.8] container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub uses: actions/checkout@v2 - - name: - Restore full Python ${{ matrix.python-version }} virtual environment + - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2 with: @@ -661,13 +657,12 @@ jobs: needs: prepare-tests strategy: matrix: - python-version: [3.7] + python-version: [3.8] container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub uses: actions/checkout@v2 - - name: - Restore full Python ${{ matrix.python-version }} virtual environment + - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2 with: @@ -696,15 +691,14 @@ jobs: strategy: matrix: group: [1, 2, 3, 4] - python-version: [3.7, 3.8, 3.9] + python-version: [3.8, 3.9] name: >- Run tests Python ${{ matrix.python-version }} (group ${{ matrix.group }}) container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub uses: actions/checkout@v2 - - name: - Restore full Python ${{ matrix.python-version }} virtual environment + - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2 with: @@ -759,13 +753,12 @@ jobs: needs: pytest strategy: matrix: - python-version: [3.7] + python-version: [3.8] container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub uses: actions/checkout@v2 - - name: - Restore full Python ${{ matrix.python-version }} virtual environment + - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2 with: diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 613e386b249..cda5943ecd0 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -14,8 +14,6 @@ pr: resources: containers: - - container: 37 - image: homeassistant/ci-azure:3.7 - container: 38 image: homeassistant/ci-azure:3.8 repositories: @@ -25,7 +23,7 @@ resources: endpoint: "home-assistant" variables: - name: PythonMain - value: "37" + value: "38" - name: versionHadolint value: "v1.17.6" @@ -150,8 +148,6 @@ stages: strategy: maxParallel: 3 matrix: - Python37: - python.container: "37" Python38: python.container: "38" container: $[ variables['python.container'] ] diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index 0175dfd6586..686e7c2de16 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -108,8 +108,6 @@ async def _run_client(hass, client, interval): await asyncio.sleep(interval) except asyncio.TimeoutError: continue - except asyncio.CancelledError: - raise except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception, aborting arcam client") return diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index bb255ba8bf3..9c868724d9b 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -1,5 +1,4 @@ """Support for system log.""" -import asyncio from collections import OrderedDict, deque import logging import queue @@ -165,8 +164,6 @@ class LogErrorQueueHandler(logging.handlers.QueueHandler): """Emit a log record.""" try: self.enqueue(record) - except asyncio.CancelledError: - raise except Exception: # pylint: disable=broad-except self.handleError(record) diff --git a/homeassistant/const.py b/homeassistant/const.py index 1a191441990..15ea6b7b00d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -4,10 +4,10 @@ MINOR_VERSION = 2 PATCH_VERSION = "0.dev0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" -REQUIRED_PYTHON_VER = (3, 7, 1) +REQUIRED_PYTHON_VER = (3, 8, 0) # Truthy date string triggers showing related deprecation warning messages. -REQUIRED_NEXT_PYTHON_VER = (3, 8, 0) -REQUIRED_NEXT_PYTHON_DATE = "December 7, 2020" +REQUIRED_NEXT_PYTHON_VER = (3, 9, 0) +REQUIRED_NEXT_PYTHON_DATE = "" # Format for platform files PLATFORM_FORMAT = "{platform}.{domain}" diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index 2f202165550..feef339a200 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -34,8 +34,6 @@ class HomeAssistantQueueHandler(logging.handlers.QueueHandler): """Emit a log record.""" try: self.enqueue(record) - except asyncio.CancelledError: - raise except Exception: # pylint: disable=broad-except self.handleError(record) From 168b3ae6afb8cb01c01e8985e02a9e912ae5da08 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 1 Jan 2021 13:48:24 +0100 Subject: [PATCH 039/507] Upgrade alpha-vantage to 2.3.1 (#44705) --- homeassistant/components/alpha_vantage/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alpha_vantage/manifest.json b/homeassistant/components/alpha_vantage/manifest.json index 28103980869..5ff3122668d 100644 --- a/homeassistant/components/alpha_vantage/manifest.json +++ b/homeassistant/components/alpha_vantage/manifest.json @@ -2,6 +2,6 @@ "domain": "alpha_vantage", "name": "Alpha Vantage", "documentation": "https://www.home-assistant.io/integrations/alpha_vantage", - "requirements": ["alpha_vantage==2.2.0"], + "requirements": ["alpha_vantage==2.3.1"], "codeowners": ["@fabaff"] } diff --git a/requirements_all.txt b/requirements_all.txt index f04f2c7f3d8..9db1587aa1c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -236,7 +236,7 @@ airly==1.0.0 aladdin_connect==0.3 # homeassistant.components.alpha_vantage -alpha_vantage==2.2.0 +alpha_vantage==2.3.1 # homeassistant.components.ambiclimate ambiclimate==0.2.1 From ddfc3d6d8e8cffb29900350905827de9c443897e Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 1 Jan 2021 13:49:57 +0100 Subject: [PATCH 040/507] Upgrade volkszaehler to 0.2.1 (#44703) --- homeassistant/components/volkszaehler/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/volkszaehler/manifest.json b/homeassistant/components/volkszaehler/manifest.json index b133550b327..937c589bdf4 100644 --- a/homeassistant/components/volkszaehler/manifest.json +++ b/homeassistant/components/volkszaehler/manifest.json @@ -2,6 +2,6 @@ "domain": "volkszaehler", "name": "Volkszaehler", "documentation": "https://www.home-assistant.io/integrations/volkszaehler", - "requirements": ["volkszaehler==0.1.3"], + "requirements": ["volkszaehler==0.2.1"], "codeowners": ["@fabaff"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9db1587aa1c..73b6b15b765 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2248,7 +2248,7 @@ venstarcolortouch==0.13 vilfo-api-client==0.3.2 # homeassistant.components.volkszaehler -volkszaehler==0.1.3 +volkszaehler==0.2.1 # homeassistant.components.volvooncall volvooncall==0.8.12 From f0e96f739bff73bb107dff15f1c58d124bd97200 Mon Sep 17 00:00:00 2001 From: Clifford Roche Date: Fri, 1 Jan 2021 07:58:38 -0500 Subject: [PATCH 041/507] Add turn_on and turn_off to gree climate component (#43207) --- homeassistant/components/gree/climate.py | 16 ++++++- tests/components/gree/test_climate.py | 58 +++++++++++++++++++++--- 2 files changed, 67 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/gree/climate.py b/homeassistant/components/gree/climate.py index 6a33e3341b0..17d2045ee90 100644 --- a/homeassistant/components/gree/climate.py +++ b/homeassistant/components/gree/climate.py @@ -193,7 +193,7 @@ class GreeClimateEntity(CoordinatorEntity, ClimateEntity): return HVAC_MODES.get(self.coordinator.device.mode) - async def async_set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode) -> None: """Set new target hvac mode.""" if hvac_mode not in self.hvac_modes: raise ValueError(f"Invalid hvac_mode: {hvac_mode}") @@ -217,6 +217,20 @@ class GreeClimateEntity(CoordinatorEntity, ClimateEntity): await self.coordinator.push_state_update() self.async_write_ha_state() + async def async_turn_on(self) -> None: + """Turn on the device.""" + _LOGGER.debug("Turning on HVAC for device %s", self._name) + + self._device.power = True + await self._push_state_update() + + async def async_turn_off(self) -> None: + """Turn off the device.""" + _LOGGER.debug("Turning off HVAC for device %s", self._name) + + self._device.power = False + await self._push_state_update() + @property def hvac_modes(self) -> List[str]: """Return the HVAC modes support by the device.""" diff --git a/tests/components/gree/test_climate.py b/tests/components/gree/test_climate.py index 534168fa78e..cfb689589bf 100644 --- a/tests/components/gree/test_climate.py +++ b/tests/components/gree/test_climate.py @@ -51,6 +51,8 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, STATE_UNAVAILABLE, ) from homeassistant.setup import async_setup_component @@ -244,14 +246,14 @@ async def test_send_power_on(hass, discovery, device, mock_now): assert await hass.services.async_call( DOMAIN, - SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVAC_MODE_AUTO}, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_ID}, blocking=True, ) state = hass.states.get(ENTITY_ID) assert state is not None - assert state.state == HVAC_MODE_AUTO + assert state.state != HVAC_MODE_OFF async def test_send_power_on_device_timeout(hass, discovery, device, mock_now): @@ -262,14 +264,58 @@ async def test_send_power_on_device_timeout(hass, discovery, device, mock_now): assert await hass.services.async_call( DOMAIN, - SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVAC_MODE_AUTO}, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_ID}, blocking=True, ) state = hass.states.get(ENTITY_ID) assert state is not None - assert state.state == HVAC_MODE_AUTO + assert state.state != HVAC_MODE_OFF + + +async def test_send_power_off(hass, discovery, device, mock_now): + """Test for sending power off command to the device.""" + await async_setup_gree(hass) + + next_update = mock_now + timedelta(minutes=5) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + assert await hass.services.async_call( + DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + + state = hass.states.get(ENTITY_ID) + assert state is not None + assert state.state == HVAC_MODE_OFF + + +async def test_send_power_off_device_timeout(hass, discovery, device, mock_now): + """Test for sending power off command to the device with a device timeout.""" + device().push_state_update.side_effect = DeviceTimeoutError + + await async_setup_gree(hass) + + next_update = mock_now + timedelta(minutes=5) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + assert await hass.services.async_call( + DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + + state = hass.states.get(ENTITY_ID) + assert state is not None + assert state.state == HVAC_MODE_OFF async def test_send_target_temperature(hass, discovery, device, mock_now): From 1f0a6b178e47e19b3a0bfa4c1733d40c4959b190 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 1 Jan 2021 15:11:25 +0100 Subject: [PATCH 042/507] Fix Gree climate turn on/off (#44731) --- homeassistant/components/gree/climate.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/gree/climate.py b/homeassistant/components/gree/climate.py index 17d2045ee90..8d0170fbe50 100644 --- a/homeassistant/components/gree/climate.py +++ b/homeassistant/components/gree/climate.py @@ -221,15 +221,17 @@ class GreeClimateEntity(CoordinatorEntity, ClimateEntity): """Turn on the device.""" _LOGGER.debug("Turning on HVAC for device %s", self._name) - self._device.power = True - await self._push_state_update() + self.coordinator.device.power = True + await self.coordinator.push_state_update() + self.async_write_ha_state() async def async_turn_off(self) -> None: """Turn off the device.""" _LOGGER.debug("Turning off HVAC for device %s", self._name) - self._device.power = False - await self._push_state_update() + self.coordinator.device.power = False + await self.coordinator.push_state_update() + self.async_write_ha_state() @property def hvac_modes(self) -> List[str]: From 176415b04511b91293d0babd8e2831551e0b2129 Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 1 Jan 2021 07:40:28 -0700 Subject: [PATCH 043/507] Fix AccuWeather condition mapping (#44710) --- homeassistant/components/accuweather/const.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/accuweather/const.py b/homeassistant/components/accuweather/const.py index cbccc3a462d..e8dbe921d77 100644 --- a/homeassistant/components/accuweather/const.py +++ b/homeassistant/components/accuweather/const.py @@ -52,12 +52,12 @@ CONDITION_CLASSES = { 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_PARTLYCLOUDY: [3, 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_SUNNY: [1, 2, 5], ATTR_CONDITION_WINDY: [32], } From 52e1aad0083997f8f7ac4f4e9dfdc5ebf46514ab Mon Sep 17 00:00:00 2001 From: Sian Date: Sat, 2 Jan 2021 01:36:36 +1030 Subject: [PATCH 044/507] Correct Dyson climate fan auto mode (#44569) Co-authored-by: Justin Gauthier --- homeassistant/components/dyson/climate.py | 8 ++++++-- tests/components/dyson/test_climate.py | 3 +-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dyson/climate.py b/homeassistant/components/dyson/climate.py index d2c23f46093..a71c124c633 100644 --- a/homeassistant/components/dyson/climate.py +++ b/homeassistant/components/dyson/climate.py @@ -2,6 +2,7 @@ import logging from libpurecool.const import ( + AutoMode, FanPower, FanSpeed, FanState, @@ -333,7 +334,10 @@ class DysonPureHotCoolEntity(ClimateEntity): @property def fan_mode(self): """Return the fan setting.""" - if self._device.state.fan_state == FanState.FAN_OFF.value: + if ( + self._device.state.auto_mode != AutoMode.AUTO_ON.value + and self._device.state.fan_state == FanState.FAN_OFF.value + ): return FAN_OFF return SPEED_MAP[self._device.state.speed] @@ -368,7 +372,7 @@ class DysonPureHotCoolEntity(ClimateEntity): elif fan_mode == FAN_HIGH: self._device.set_fan_speed(FanSpeed.FAN_SPEED_10) elif fan_mode == FAN_AUTO: - self._device.set_fan_speed(FanSpeed.FAN_SPEED_AUTO) + self._device.enable_auto_mode() def set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" diff --git a/tests/components/dyson/test_climate.py b/tests/components/dyson/test_climate.py index 77105dc73db..c4e4c91087c 100644 --- a/tests/components/dyson/test_climate.py +++ b/tests/components/dyson/test_climate.py @@ -677,8 +677,7 @@ async def test_purehotcool_set_fan_mode(devices, login, hass): {ATTR_ENTITY_ID: "climate.living_room", ATTR_FAN_MODE: FAN_AUTO}, True, ) - assert device.set_fan_speed.call_count == 4 - device.set_fan_speed.assert_called_with(FanSpeed.FAN_SPEED_AUTO) + assert device.enable_auto_mode.call_count == 1 @patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) From e2e79aba4e36802bd4608a23f58739ebdec9a3cc Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Fri, 1 Jan 2021 17:20:55 +0100 Subject: [PATCH 045/507] Update pyhomematic to 0.1.71 (#44732) --- homeassistant/components/homematic/const.py | 6 ++++++ homeassistant/components/homematic/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematic/const.py b/homeassistant/components/homematic/const.py index cd474428113..a6ff19a6eea 100644 --- a/homeassistant/components/homematic/const.py +++ b/homeassistant/components/homematic/const.py @@ -46,6 +46,7 @@ HM_DEVICE_TYPES = { "Switch", "SwitchPowermeter", "IOSwitch", + "IOSwitchNoInhibit", "IPSwitch", "RFSiren", "IPSwitchPowermeter", @@ -115,6 +116,10 @@ HM_DEVICE_TYPES = { "IPRemoteMotionV2", "HBUNISenWEA", "PresenceIPW", + "IPRainSensor", + "ValveBox", + "IPKeyBlind", + "IPKeyBlindTilt", ], DISCOVER_CLIMATE: [ "Thermostat", @@ -158,6 +163,7 @@ HM_DEVICE_TYPES = { "IPWInputDevice", "IPWMotionDection", "IPAlarmSensor", + "IPRainSensor", ], DISCOVER_COVER: [ "Blind", diff --git a/homeassistant/components/homematic/manifest.json b/homeassistant/components/homematic/manifest.json index 63e33a60c53..36414b606f9 100644 --- a/homeassistant/components/homematic/manifest.json +++ b/homeassistant/components/homematic/manifest.json @@ -2,6 +2,6 @@ "domain": "homematic", "name": "Homematic", "documentation": "https://www.home-assistant.io/integrations/homematic", - "requirements": ["pyhomematic==0.1.70"], + "requirements": ["pyhomematic==0.1.71"], "codeowners": ["@pvizeli", "@danielperna84"] } diff --git a/requirements_all.txt b/requirements_all.txt index 73b6b15b765..aeee2ef26b6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1431,7 +1431,7 @@ pyhik==0.2.8 pyhiveapi==0.2.20.2 # homeassistant.components.homematic -pyhomematic==0.1.70 +pyhomematic==0.1.71 # homeassistant.components.homeworks pyhomeworks==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c0698e79f5a..890aafc7441 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -719,7 +719,7 @@ pyhaversion==3.4.2 pyheos==0.7.2 # homeassistant.components.homematic -pyhomematic==0.1.70 +pyhomematic==0.1.71 # homeassistant.components.icloud pyicloud==0.9.7 From ea10f96bf702b13e3730d521644acc4c9be33288 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 1 Jan 2021 17:58:13 +0100 Subject: [PATCH 046/507] Upgrade sentry-sdk to 0.19.5 (#44738) --- homeassistant/components/sentry/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index be07586cebd..da5294b9258 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.4"], + "requirements": ["sentry-sdk==0.19.5"], "codeowners": ["@dcramer", "@frenck"] } diff --git a/requirements_all.txt b/requirements_all.txt index aeee2ef26b6..84879879540 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2001,7 +2001,7 @@ sense-hat==2.2.0 sense_energy==0.8.1 # homeassistant.components.sentry -sentry-sdk==0.19.4 +sentry-sdk==0.19.5 # homeassistant.components.sharkiq sharkiqpy==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 890aafc7441..b971ad550a0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -982,7 +982,7 @@ samsungtvws==1.4.0 sense_energy==0.8.1 # homeassistant.components.sentry -sentry-sdk==0.19.4 +sentry-sdk==0.19.5 # homeassistant.components.sharkiq sharkiqpy==0.1.8 From 5c634ac8bbd25492926b4a5319906244764c0bdf Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 1 Jan 2021 17:59:05 +0100 Subject: [PATCH 047/507] Upgrade debugpy to 1.2.1 (#44737) --- homeassistant/components/debugpy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/debugpy/manifest.json b/homeassistant/components/debugpy/manifest.json index 27b110b1f68..67af8fc553b 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.2.0"], + "requirements": ["debugpy==1.2.1"], "codeowners": ["@frenck"], "quality_scale": "internal" } diff --git a/requirements_all.txt b/requirements_all.txt index 84879879540..402374f9a63 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -460,7 +460,7 @@ datadog==0.15.0 datapoint==0.9.5 # homeassistant.components.debugpy -debugpy==1.2.0 +debugpy==1.2.1 # homeassistant.components.decora # decora==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b971ad550a0..c7da734d3e9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -245,7 +245,7 @@ datadog==0.15.0 datapoint==0.9.5 # homeassistant.components.debugpy -debugpy==1.2.0 +debugpy==1.2.1 # homeassistant.components.ihc # homeassistant.components.namecheapdns From 70d2c371311e40ed09df4918e80e0828c47b0431 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 1 Jan 2021 18:10:28 +0100 Subject: [PATCH 048/507] Upgrade pre-commit to 2.9.3 (#44740) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index e4553a2498b..f9bf5c679d5 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.9.2 +pre-commit==2.9.3 pylint==2.6.0 astroid==2.4.2 pipdeptree==1.0.0 From c4fbfc25e3bb0a3d072d9acd203891c5b5f52b75 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 1 Jan 2021 18:39:59 +0100 Subject: [PATCH 049/507] Bump H11 library to support non RFC line endings (#44735) --- homeassistant/package_constraints.txt | 3 +++ script/gen_requirements_all.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index bb2c4c8636b..d273d730b97 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -37,6 +37,9 @@ pycryptodome>=3.6.6 # Constrain urllib3 to ensure we deal with CVE-2019-11236 & CVE-2019-11324 urllib3>=1.24.3 +# Constrain H11 to ensure we get a new enough version to support non-rfc line endings +h11>=0.12.0 + # Constrain httplib2 to protect against CVE-2020-11078 httplib2>=0.18.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index f627346c67b..130fd2cc245 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -65,6 +65,9 @@ pycryptodome>=3.6.6 # Constrain urllib3 to ensure we deal with CVE-2019-11236 & CVE-2019-11324 urllib3>=1.24.3 +# Constrain H11 to ensure we get a new enough version to support non-rfc line endings +h11>=0.12.0 + # Constrain httplib2 to protect against CVE-2020-11078 httplib2>=0.18.0 From 661eb0338afa961088018b46e95f4aa6477e24f0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 1 Jan 2021 08:28:20 -1000 Subject: [PATCH 050/507] Fix templates for rest notify (#44724) --- homeassistant/components/rest/notify.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/rest/notify.py b/homeassistant/components/rest/notify.py index 15b871cde5c..f15df428640 100644 --- a/homeassistant/components/rest/notify.py +++ b/homeassistant/components/rest/notify.py @@ -30,6 +30,7 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import setup_reload_service +from homeassistant.helpers.template import Template from . import DOMAIN, PLATFORMS @@ -56,8 +57,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_TARGET_PARAMETER_NAME): cv.string, vol.Optional(CONF_TITLE_PARAMETER_NAME): cv.string, - vol.Optional(CONF_DATA): dict, - vol.Optional(CONF_DATA_TEMPLATE): {cv.match_all: cv.template_complex}, + vol.Optional(CONF_DATA): vol.All(dict, cv.template_complex), + vol.Optional(CONF_DATA_TEMPLATE): vol.All(dict, cv.template_complex), vol.Optional(CONF_AUTHENTICATION): vol.In( [HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION] ), @@ -155,9 +156,7 @@ class RestNotificationService(BaseNotificationService): # integrations, so just return the first target in the list. data[self._target_param_name] = kwargs[ATTR_TARGET][0] - if self._data: - data.update(self._data) - elif self._data_template: + if self._data_template or self._data: kwargs[ATTR_MESSAGE] = message def _data_template_creator(value): @@ -168,10 +167,15 @@ class RestNotificationService(BaseNotificationService): return { key: _data_template_creator(item) for key, item in value.items() } + if not isinstance(value, Template): + return value value.hass = self._hass return value.async_render(kwargs, parse_result=False) - data.update(_data_template_creator(self._data_template)) + if self._data: + data.update(_data_template_creator(self._data)) + if self._data_template: + data.update(_data_template_creator(self._data_template)) if self._method == "POST": response = requests.post( From c7fa98211d7a561beaa6c8363c6647e792b3e735 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 1 Jan 2021 19:48:33 +0100 Subject: [PATCH 051/507] Bump locationsharinglib to 4.1.5 (#44742) --- homeassistant/components/google_maps/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_maps/manifest.json b/homeassistant/components/google_maps/manifest.json index 62791c212f9..435e01fb026 100644 --- a/homeassistant/components/google_maps/manifest.json +++ b/homeassistant/components/google_maps/manifest.json @@ -2,6 +2,6 @@ "domain": "google_maps", "name": "Google Maps", "documentation": "https://www.home-assistant.io/integrations/google_maps", - "requirements": ["locationsharinglib==4.1.0"], + "requirements": ["locationsharinglib==4.1.5"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 402374f9a63..940d2b96a89 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -889,7 +889,7 @@ linode-api==4.1.9b1 lmnotify==0.0.4 # homeassistant.components.google_maps -locationsharinglib==4.1.0 +locationsharinglib==4.1.5 # homeassistant.components.logi_circle logi_circle==0.2.2 From 2b0556520b687edd502f9665543fbf1e622f5f63 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 1 Jan 2021 20:54:43 +0100 Subject: [PATCH 052/507] Catch Shelly zeroconf types with uppercase too (#44672) Co-authored-by: Franck Nijhof --- homeassistant/components/shelly/config_flow.py | 3 --- tests/components/shelly/test_config_flow.py | 11 ----------- 2 files changed, 14 deletions(-) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 261c1898ca9..b3dd7bb80fe 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -138,9 +138,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf(self, zeroconf_info): """Handle zeroconf discovery.""" - if not zeroconf_info.get("name", "").startswith("shelly"): - return self.async_abort(reason="not_shelly") - try: self.info = info = await self._async_get_info(zeroconf_info["host"]) except HTTP_CONNECT_ERRORS: diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 71c971757c1..2850be11450 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -488,14 +488,3 @@ async def test_zeroconf_require_auth(hass): } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_zeroconf_not_shelly(hass): - """Test we filter out non-shelly devices.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - data={"host": "1.1.1.1", "name": "notshelly"}, - context={"source": config_entries.SOURCE_ZEROCONF}, - ) - assert result["type"] == "abort" - assert result["reason"] == "not_shelly" From 65cf2fcb6f782da89af215c159b220e4c18a0b45 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 1 Jan 2021 22:31:56 +0100 Subject: [PATCH 053/507] Drop asynctest (#44746) --- requirements_test.txt | 1 - .../config_flow/tests/test_config_flow.py | 4 ++-- .../config_flow_oauth2/tests/test_config_flow.py | 4 ++-- tests/async_mock.py | 9 --------- tests/auth/mfa_modules/test_notify.py | 2 +- tests/auth/mfa_modules/test_totp.py | 2 +- tests/auth/providers/test_command_line.py | 3 +-- tests/auth/providers/test_homeassistant.py | 3 +-- tests/auth/providers/test_insecure_example.py | 3 +-- tests/auth/test_auth_store.py | 3 +-- tests/auth/test_init.py | 2 +- tests/common.py | 3 +-- tests/components/abode/common.py | 3 ++- tests/components/abode/test_alarm_control_panel.py | 4 ++-- tests/components/abode/test_camera.py | 4 ++-- tests/components/abode/test_config_flow.py | 3 ++- tests/components/abode/test_cover.py | 4 ++-- tests/components/abode/test_init.py | 4 ++-- tests/components/abode/test_light.py | 4 ++-- tests/components/abode/test_lock.py | 4 ++-- tests/components/abode/test_switch.py | 4 ++-- tests/components/accuweather/__init__.py | 2 +- tests/components/accuweather/test_config_flow.py | 2 +- tests/components/accuweather/test_init.py | 3 ++- tests/components/accuweather/test_sensor.py | 2 +- tests/components/accuweather/test_system_health.py | 2 +- tests/components/accuweather/test_weather.py | 2 +- tests/components/acmeda/test_config_flow.py | 3 ++- tests/components/adguard/test_config_flow.py | 3 ++- tests/components/advantage_air/test_config_flow.py | 3 ++- tests/components/airly/test_system_health.py | 2 +- tests/components/airnow/test_config_flow.py | 3 ++- tests/components/airvisual/test_config_flow.py | 3 ++- tests/components/alarmdecoder/test_config_flow.py | 3 ++- tests/components/alexa/test_capabilities.py | 3 ++- tests/components/alexa/test_entities.py | 4 ++-- tests/components/alexa/test_smart_home.py | 3 ++- tests/components/almond/test_config_flow.py | 2 +- tests/components/almond/test_init.py | 2 +- tests/components/ambiclimate/test_config_flow.py | 4 ++-- tests/components/androidtv/patchers.py | 2 +- tests/components/androidtv/test_media_player.py | 2 +- tests/components/apache_kafka/test_init.py | 3 +-- tests/components/api/test_init.py | 2 +- tests/components/apns/test_notify.py | 2 +- tests/components/apple_tv/conftest.py | 4 ++-- tests/components/apple_tv/test_config_flow.py | 3 ++- tests/components/apprise/test_notify.py | 4 ++-- tests/components/aprs/test_device_tracker.py | 3 ++- tests/components/arcam_fmj/conftest.py | 3 ++- tests/components/arcam_fmj/test_config_flow.py | 3 ++- tests/components/arcam_fmj/test_media_player.py | 3 +-- tests/components/arlo/test_sensor.py | 3 +-- tests/components/asuswrt/test_device_tracker.py | 4 ++-- tests/components/asuswrt/test_sensor.py | 4 ++-- tests/components/atag/test_climate.py | 3 ++- tests/components/atag/test_config_flow.py | 3 ++- tests/components/atag/test_init.py | 3 ++- tests/components/atag/test_water_heater.py | 3 ++- tests/components/august/mocks.py | 5 +++-- tests/components/august/test_camera.py | 3 ++- tests/components/august/test_config_flow.py | 3 ++- tests/components/august/test_gateway.py | 3 ++- tests/components/august/test_init.py | 2 +- tests/components/aurora/test_config_flow.py | 3 ++- tests/components/auth/test_indieauth.py | 2 +- tests/components/auth/test_init.py | 2 +- tests/components/auth/test_login_flow.py | 3 ++- tests/components/automation/test_blueprint.py | 2 +- tests/components/automation/test_init.py | 2 +- tests/components/awair/test_config_flow.py | 3 ++- tests/components/awair/test_sensor.py | 3 ++- tests/components/aws/test_init.py | 4 ++-- tests/components/axis/test_camera.py | 4 ++-- tests/components/axis/test_config_flow.py | 3 ++- tests/components/axis/test_device.py | 2 +- tests/components/axis/test_init.py | 3 ++- tests/components/axis/test_light.py | 3 +-- tests/components/axis/test_switch.py | 3 +-- tests/components/azure_devops/test_config_flow.py | 3 ++- tests/components/azure_event_hub/test_init.py | 3 +-- tests/components/bayesian/test_binary_sensor.py | 3 +-- .../binary_sensor/test_device_condition.py | 2 +- tests/components/blebox/conftest.py | 2 +- tests/components/blebox/test_air_quality.py | 3 +-- tests/components/blebox/test_climate.py | 3 +-- tests/components/blebox/test_config_flow.py | 4 ++-- tests/components/blebox/test_cover.py | 3 +-- tests/components/blebox/test_light.py | 3 +-- tests/components/blebox/test_sensor.py | 3 +-- tests/components/blebox/test_switch.py | 3 +-- tests/components/blink/test_config_flow.py | 3 ++- tests/components/blueprint/conftest.py | 4 ++-- tests/components/blueprint/test_models.py | 3 +-- tests/components/blueprint/test_websocket_api.py | 3 +-- .../bluetooth_le_tracker/test_device_tracker.py | 2 +- .../bmw_connected_drive/test_config_flow.py | 3 ++- tests/components/bond/common.py | 2 +- tests/components/bond/test_config_flow.py | 2 +- tests/components/braviatv/test_config_flow.py | 3 ++- tests/components/broadlink/__init__.py | 3 ++- tests/components/broadlink/test_config_flow.py | 3 +-- tests/components/broadlink/test_device.py | 3 ++- tests/components/broadlink/test_remote.py | 2 +- tests/components/brother/__init__.py | 2 +- tests/components/brother/test_config_flow.py | 2 +- tests/components/brother/test_init.py | 3 ++- tests/components/brother/test_sensor.py | 2 +- tests/components/caldav/test_calendar.py | 3 +-- tests/components/camera/test_init.py | 2 +- tests/components/canary/__init__.py | 3 +-- tests/components/canary/conftest.py | 4 ++-- .../components/canary/test_alarm_control_panel.py | 3 ++- tests/components/canary/test_config_flow.py | 4 ++-- tests/components/canary/test_init.py | 4 ++-- tests/components/canary/test_sensor.py | 2 +- tests/components/cast/test_home_assistant_cast.py | 3 ++- tests/components/cast/test_init.py | 4 ++-- tests/components/cast/test_media_player.py | 2 +- tests/components/cert_expiry/test_config_flow.py | 2 +- tests/components/cert_expiry/test_init.py | 2 +- tests/components/cert_expiry/test_sensors.py | 2 +- tests/components/climate/test_init.py | 2 +- tests/components/cloud/__init__.py | 4 ++-- tests/components/cloud/conftest.py | 4 ++-- tests/components/cloud/test_account_link.py | 2 +- tests/components/cloud/test_alexa_config.py | 2 +- tests/components/cloud/test_binary_sensor.py | 4 ++-- tests/components/cloud/test_client.py | 2 +- tests/components/cloud/test_google_config.py | 3 ++- tests/components/cloud/test_http_api.py | 2 +- tests/components/cloud/test_init.py | 4 ++-- tests/components/cloud/test_prefs.py | 4 ++-- tests/components/cloud/test_system_health.py | 2 +- tests/components/cloudflare/__init__.py | 2 +- tests/components/cloudflare/conftest.py | 4 ++-- tests/components/coinmarketcap/test_sensor.py | 2 +- tests/components/color_extractor/test_service.py | 2 +- tests/components/command_line/test_cover.py | 3 +-- tests/components/command_line/test_notify.py | 2 +- tests/components/command_line/test_sensor.py | 2 +- tests/components/config/test_automation.py | 2 +- tests/components/config/test_config_entries.py | 2 +- tests/components/config/test_core.py | 4 ++-- tests/components/config/test_customize.py | 3 +-- tests/components/config/test_group.py | 3 +-- tests/components/config/test_init.py | 3 ++- tests/components/config/test_scene.py | 3 +-- tests/components/config/test_script.py | 4 ++-- tests/components/config/test_zwave.py | 2 +- tests/components/conftest.py | 4 ++-- tests/components/control4/test_config_flow.py | 2 +- tests/components/coolmaster/test_config_flow.py | 4 ++-- tests/components/coronavirus/conftest.py | 4 ++-- tests/components/daikin/test_config_flow.py | 2 +- tests/components/darksky/test_sensor.py | 2 +- tests/components/darksky/test_weather.py | 2 +- tests/components/datadog/test_init.py | 2 +- tests/components/debugpy/test_init.py | 4 ++-- tests/components/deconz/test_binary_sensor.py | 3 +-- tests/components/deconz/test_climate.py | 3 +-- tests/components/deconz/test_config_flow.py | 3 +-- tests/components/deconz/test_cover.py | 3 +-- tests/components/deconz/test_fan.py | 3 +-- tests/components/deconz/test_gateway.py | 2 +- tests/components/deconz/test_init.py | 3 +-- tests/components/deconz/test_light.py | 3 +-- tests/components/deconz/test_lock.py | 3 +-- tests/components/deconz/test_scene.py | 3 +-- tests/components/deconz/test_services.py | 3 +-- tests/components/deconz/test_switch.py | 3 +-- tests/components/default_config/test_init.py | 3 ++- tests/components/demo/test_camera.py | 4 ++-- tests/components/demo/test_geo_location.py | 3 ++- tests/components/demo/test_media_player.py | 4 ++-- tests/components/demo/test_notify.py | 2 +- tests/components/denonavr/test_config_flow.py | 3 ++- tests/components/derivative/test_sensor.py | 3 +-- .../device_sun_light_trigger/test_init.py | 2 +- tests/components/device_tracker/test_init.py | 2 +- .../devolo_home_control/test_config_flow.py | 3 ++- tests/components/dexcom/__init__.py | 2 +- tests/components/dexcom/test_config_flow.py | 3 ++- tests/components/dexcom/test_init.py | 3 ++- tests/components/dexcom/test_sensor.py | 3 ++- tests/components/directv/test_config_flow.py | 3 ++- tests/components/directv/test_media_player.py | 2 +- tests/components/directv/test_remote.py | 3 ++- tests/components/discovery/test_init.py | 3 +-- tests/components/doorbird/test_config_flow.py | 2 +- tests/components/dsmr/conftest.py | 3 +-- tests/components/dsmr/test_config_flow.py | 2 +- tests/components/dsmr/test_sensor.py | 2 +- tests/components/dunehd/test_config_flow.py | 3 ++- tests/components/dynalite/common.py | 3 ++- tests/components/dynalite/test_bridge.py | 3 ++- tests/components/dynalite/test_config_flow.py | 3 ++- tests/components/dynalite/test_init.py | 3 ++- tests/components/dyson/test_air_quality.py | 3 +-- tests/components/dyson/test_climate.py | 3 +-- tests/components/dyson/test_fan.py | 2 +- tests/components/dyson/test_sensor.py | 2 +- tests/components/eafm/conftest.py | 4 ++-- tests/components/eafm/test_config_flow.py | 4 ++-- .../components/ee_brightbox/test_device_tracker.py | 3 +-- tests/components/elkm1/test_config_flow.py | 4 ++-- tests/components/emulated_hue/test_hue_api.py | 2 +- tests/components/emulated_hue/test_init.py | 4 ++-- tests/components/emulated_kasa/test_init.py | 3 +-- tests/components/emulated_roku/test_binding.py | 4 ++-- tests/components/emulated_roku/test_init.py | 4 ++-- tests/components/enocean/test_config_flow.py | 3 ++- tests/components/epson/test_config_flow.py | 4 ++-- tests/components/esphome/test_config_flow.py | 2 +- tests/components/facebox/test_image_processing.py | 4 ++-- tests/components/fail2ban/test_sensor.py | 3 ++- tests/components/feedreader/test_init.py | 2 +- tests/components/ffmpeg/test_init.py | 3 ++- tests/components/fido/test_sensor.py | 2 +- tests/components/file/test_notify.py | 2 +- tests/components/file/test_sensor.py | 3 ++- tests/components/filesize/test_sensor.py | 3 +-- tests/components/filter/test_sensor.py | 2 +- .../components/fireservicerota/test_config_flow.py | 3 ++- tests/components/firmata/test_config_flow.py | 4 ++-- .../components/flick_electric/test_config_flow.py | 2 +- tests/components/flo/test_config_flow.py | 3 +-- tests/components/flume/test_config_flow.py | 4 ++-- tests/components/flunearyou/test_config_flow.py | 3 ++- tests/components/flux/test_switch.py | 3 ++- tests/components/folder_watcher/test_init.py | 3 +-- tests/components/foobot/test_sensor.py | 2 +- tests/components/forked_daapd/test_config_flow.py | 3 ++- tests/components/forked_daapd/test_media_player.py | 3 ++- tests/components/freebox/conftest.py | 4 ++-- tests/components/freebox/test_config_flow.py | 3 ++- tests/components/fritzbox/__init__.py | 4 ++-- tests/components/fritzbox/conftest.py | 4 ++-- tests/components/fritzbox/test_binary_sensor.py | 2 +- tests/components/fritzbox/test_climate.py | 2 +- tests/components/fritzbox/test_config_flow.py | 3 +-- tests/components/fritzbox/test_init.py | 3 ++- tests/components/fritzbox/test_sensor.py | 2 +- tests/components/fritzbox/test_switch.py | 2 +- tests/components/frontend/test_init.py | 2 +- .../components/garmin_connect/test_config_flow.py | 3 ++- tests/components/gdacs/__init__.py | 2 +- tests/components/gdacs/test_config_flow.py | 3 +-- tests/components/gdacs/test_geo_location.py | 2 +- tests/components/gdacs/test_init.py | 4 ++-- tests/components/gdacs/test_sensor.py | 3 ++- tests/components/generic/test_camera.py | 3 +-- .../components/generic_thermostat/test_climate.py | 2 +- .../geo_json_events/test_geo_location.py | 3 ++- tests/components/geo_rss_events/test_sensor.py | 3 ++- tests/components/geofency/test_init.py | 6 +++--- tests/components/geonetnz_quakes/__init__.py | 2 +- .../components/geonetnz_quakes/test_config_flow.py | 3 +-- .../geonetnz_quakes/test_geo_location.py | 2 +- tests/components/geonetnz_quakes/test_init.py | 4 ++-- tests/components/geonetnz_quakes/test_sensor.py | 2 +- tests/components/geonetnz_volcano/__init__.py | 2 +- .../geonetnz_volcano/test_config_flow.py | 3 +-- tests/components/geonetnz_volcano/test_init.py | 4 ++-- tests/components/geonetnz_volcano/test_sensor.py | 3 ++- tests/components/gios/__init__.py | 2 +- tests/components/gios/test_air_quality.py | 2 +- tests/components/gios/test_config_flow.py | 2 +- tests/components/gios/test_init.py | 3 ++- tests/components/goalzero/__init__.py | 4 ++-- tests/components/goalzero/test_config_flow.py | 3 ++- tests/components/gogogate2/test_config_flow.py | 3 ++- tests/components/gogogate2/test_cover.py | 2 +- tests/components/gogogate2/test_init.py | 3 ++- tests/components/google/conftest.py | 4 ++-- tests/components/google/test_calendar.py | 2 +- tests/components/google/test_init.py | 4 ++-- tests/components/google_assistant/__init__.py | 4 ++-- tests/components/google_assistant/test_helpers.py | 2 +- tests/components/google_assistant/test_http.py | 3 +-- .../google_assistant/test_report_state.py | 3 ++- .../components/google_assistant/test_smart_home.py | 3 ++- tests/components/google_assistant/test_trait.py | 2 +- tests/components/google_pubsub/test_init.py | 3 +-- tests/components/google_translate/test_tts.py | 2 +- tests/components/google_wifi/test_sensor.py | 2 +- tests/components/gpslogger/test_init.py | 4 ++-- tests/components/graphite/test_init.py | 2 +- tests/components/gree/common.py | 2 +- tests/components/gree/conftest.py | 4 ++-- tests/components/gree/test_climate.py | 2 +- tests/components/gree/test_init.py | 3 ++- tests/components/griddy/test_config_flow.py | 3 +-- tests/components/griddy/test_sensor.py | 2 +- tests/components/group/test_init.py | 2 +- tests/components/group/test_light.py | 7 +++---- tests/components/group/test_notify.py | 3 +-- tests/components/group/test_reproduce_state.py | 3 +-- tests/components/guardian/conftest.py | 4 ++-- tests/components/guardian/test_config_flow.py | 3 ++- tests/components/hangouts/test_config_flow.py | 4 ++-- tests/components/harmony/test_config_flow.py | 3 ++- tests/components/hassio/conftest.py | 3 +-- tests/components/hassio/test_addon_panel.py | 4 ++-- tests/components/hassio/test_auth.py | 4 ++-- tests/components/hassio/test_discovery.py | 4 ++-- tests/components/hassio/test_http.py | 3 +-- tests/components/hassio/test_init.py | 3 +-- tests/components/hassio/test_system_health.py | 2 +- tests/components/hddtemp/test_sensor.py | 2 +- tests/components/heos/conftest.py | 2 +- tests/components/heos/test_config_flow.py | 3 +-- tests/components/heos/test_init.py | 3 +-- tests/components/here_travel_time/test_sensor.py | 2 +- tests/components/hisense_aehw4a1/test_init.py | 4 ++-- tests/components/history/test_init.py | 2 +- tests/components/history_stats/test_sensor.py | 2 +- tests/components/hlk_sw16/test_config_flow.py | 3 +-- tests/components/home_connect/test_config_flow.py | 4 ++-- tests/components/homeassistant/test_init.py | 2 +- tests/components/homeassistant/test_scene.py | 3 ++- .../homeassistant/triggers/test_homeassistant.py | 3 ++- .../homeassistant/triggers/test_numeric_state.py | 2 +- .../homeassistant/triggers/test_state.py | 2 +- .../components/homeassistant/triggers/test_time.py | 2 +- .../homeassistant/triggers/test_time_pattern.py | 2 +- tests/components/homekit/common.py | 2 +- tests/components/homekit/conftest.py | 4 ++-- tests/components/homekit/test_accessories.py | 2 +- tests/components/homekit/test_aidmanager.py | 2 +- tests/components/homekit/test_config_flow.py | 3 ++- tests/components/homekit/test_get_accessories.py | 4 ++-- tests/components/homekit/test_homekit.py | 2 +- tests/components/homekit/test_img_util.py | 4 ++-- tests/components/homekit/test_init.py | 3 ++- tests/components/homekit/test_type_cameras.py | 3 +-- tests/components/homekit/test_type_fans.py | 2 +- tests/components/homekit/test_type_thermostats.py | 3 ++- tests/components/homekit/util.py | 3 ++- tests/components/homekit_controller/conftest.py | 4 ++-- .../homekit_controller/test_config_flow.py | 14 +++++++------- tests/components/homematicip_cloud/conftest.py | 3 ++- tests/components/homematicip_cloud/helper.py | 2 +- .../homematicip_cloud/test_config_flow.py | 3 ++- tests/components/homematicip_cloud/test_device.py | 4 ++-- tests/components/homematicip_cloud/test_hap.py | 4 ++-- tests/components/homematicip_cloud/test_init.py | 3 ++- tests/components/html5/test_notify.py | 3 +-- tests/components/http/test_auth.py | 3 +-- tests/components/http/test_ban.py | 2 +- tests/components/http/test_cors.py | 3 +-- tests/components/http/test_data_validator.py | 4 ++-- tests/components/http/test_init.py | 3 +-- tests/components/http/test_view.py | 4 ++-- tests/components/huawei_lte/test_config_flow.py | 3 ++- tests/components/hue/conftest.py | 2 +- tests/components/hue/test_bridge.py | 4 ++-- tests/components/hue/test_config_flow.py | 2 +- tests/components/hue/test_init.py | 3 +-- tests/components/hue/test_init_multiple_bridges.py | 4 ++-- tests/components/hue/test_light.py | 3 +-- tests/components/hue/test_sensor_base.py | 3 +-- .../hunterdouglas_powerview/test_config_flow.py | 2 +- .../components/hvv_departures/test_config_flow.py | 2 +- tests/components/hyperion/__init__.py | 2 +- tests/components/hyperion/test_config_flow.py | 2 +- tests/components/hyperion/test_light.py | 3 +-- tests/components/icloud/conftest.py | 4 ++-- tests/components/icloud/test_config_flow.py | 3 ++- tests/components/ifttt/test_init.py | 4 ++-- .../components/ign_sismologia/test_geo_location.py | 2 +- tests/components/image/test_init.py | 3 +-- tests/components/image_processing/test_init.py | 3 ++- tests/components/influxdb/test_init.py | 3 +-- tests/components/influxdb/test_sensor.py | 2 +- tests/components/input_boolean/test_init.py | 2 +- tests/components/input_datetime/test_init.py | 2 +- tests/components/input_number/test_init.py | 5 +++-- tests/components/input_select/test_init.py | 5 +++-- tests/components/input_text/test_init.py | 5 +++-- tests/components/insteon/mock_devices.py | 4 ++-- tests/components/insteon/test_config_flow.py | 3 ++- tests/components/insteon/test_init.py | 2 +- tests/components/integration/test_sensor.py | 3 +-- tests/components/ipma/test_config_flow.py | 3 ++- tests/components/ipma/test_weather.py | 2 +- tests/components/ipp/test_config_flow.py | 3 ++- tests/components/ipp/test_sensor.py | 2 +- tests/components/iqvia/test_config_flow.py | 3 ++- .../islamic_prayer_times/test_config_flow.py | 3 ++- tests/components/islamic_prayer_times/test_init.py | 2 +- .../components/islamic_prayer_times/test_sensor.py | 3 ++- tests/components/isy994/test_config_flow.py | 3 ++- tests/components/izone/test_config_flow.py | 4 ++-- tests/components/jewish_calendar/__init__.py | 3 +-- tests/components/juicenet/test_config_flow.py | 4 ++-- tests/components/kira/test_init.py | 3 +-- tests/components/kira/test_remote.py | 2 +- tests/components/kira/test_sensor.py | 2 +- tests/components/kodi/__init__.py | 3 ++- tests/components/kodi/test_config_flow.py | 3 ++- tests/components/kodi/test_init.py | 4 ++-- tests/components/konnected/test_config_flow.py | 3 ++- tests/components/konnected/test_init.py | 3 ++- tests/components/konnected/test_panel.py | 2 +- tests/components/kulersky/test_config_flow.py | 4 ++-- tests/components/kulersky/test_light.py | 2 +- tests/components/lastfm/test_sensor.py | 4 ++-- tests/components/light/conftest.py | 4 ++-- tests/components/light/test_device_condition.py | 2 +- tests/components/locative/test_init.py | 4 ++-- tests/components/logbook/test_init.py | 2 +- tests/components/logentries/test_init.py | 4 ++-- tests/components/logger/test_init.py | 3 +-- tests/components/logi_circle/test_config_flow.py | 3 +-- tests/components/lovelace/test_dashboard.py | 3 ++- tests/components/lovelace/test_resources.py | 3 +-- tests/components/lovelace/test_system_health.py | 3 ++- tests/components/luftdaten/test_config_flow.py | 2 +- tests/components/luftdaten/test_init.py | 4 ++-- tests/components/lutron_caseta/test_config_flow.py | 3 ++- .../components/manual/test_alarm_control_panel.py | 2 +- .../manual_mqtt/test_alarm_control_panel.py | 2 +- tests/components/marytts/test_tts.py | 2 +- tests/components/media_player/test_init.py | 3 +-- tests/components/media_source/test_init.py | 4 ++-- tests/components/melcloud/test_config_flow.py | 2 +- tests/components/melissa/test_climate.py | 2 +- tests/components/melissa/test_init.py | 4 ++-- tests/components/met/__init__.py | 3 ++- tests/components/met/conftest.py | 4 ++-- tests/components/met/test_config_flow.py | 3 ++- tests/components/meteo_france/conftest.py | 4 ++-- tests/components/meteo_france/test_config_flow.py | 3 ++- tests/components/metoffice/conftest.py | 4 ++-- tests/components/metoffice/test_config_flow.py | 2 +- tests/components/metoffice/test_sensor.py | 2 +- tests/components/metoffice/test_weather.py | 2 +- tests/components/mfi/test_sensor.py | 4 ++-- tests/components/mfi/test_switch.py | 4 ++-- tests/components/mhz19/test_sensor.py | 3 ++- tests/components/microsoft_face/test_init.py | 2 +- tests/components/mikrotik/test_config_flow.py | 2 +- tests/components/mikrotik/test_hub.py | 3 ++- tests/components/mikrotik/test_init.py | 3 ++- tests/components/mill/test_config_flow.py | 3 ++- tests/components/min_max/test_sensor.py | 3 +-- .../minecraft_server/test_config_flow.py | 2 +- tests/components/minio/test_minio.py | 2 +- tests/components/mobile_app/test_webhook.py | 3 ++- tests/components/mochad/test_light.py | 4 ++-- tests/components/mochad/test_switch.py | 4 ++-- tests/components/modbus/conftest.py | 2 +- tests/components/monoprice/test_config_flow.py | 3 ++- tests/components/monoprice/test_media_player.py | 2 +- tests/components/moon/test_sensor.py | 3 +-- tests/components/motion_blinds/test_config_flow.py | 3 +-- tests/components/mqtt/test_alarm_control_panel.py | 2 +- tests/components/mqtt/test_binary_sensor.py | 2 +- tests/components/mqtt/test_camera.py | 2 +- tests/components/mqtt/test_climate.py | 2 +- tests/components/mqtt/test_common.py | 2 +- tests/components/mqtt/test_config_flow.py | 3 ++- tests/components/mqtt/test_cover.py | 3 ++- tests/components/mqtt/test_device_tracker.py | 3 ++- tests/components/mqtt/test_discovery.py | 2 +- tests/components/mqtt/test_fan.py | 3 ++- tests/components/mqtt/test_init.py | 2 +- tests/components/mqtt/test_legacy_vacuum.py | 2 +- tests/components/mqtt/test_light.py | 2 +- tests/components/mqtt/test_light_json.py | 2 +- tests/components/mqtt/test_light_template.py | 3 ++- tests/components/mqtt/test_lock.py | 3 ++- tests/components/mqtt/test_scene.py | 3 +-- tests/components/mqtt/test_sensor.py | 2 +- tests/components/mqtt/test_state_vacuum.py | 2 +- tests/components/mqtt/test_subscription.py | 3 ++- tests/components/mqtt/test_switch.py | 2 +- tests/components/mqtt/test_tag.py | 2 +- tests/components/mqtt/test_trigger.py | 3 ++- tests/components/mqtt_eventstream/test_init.py | 2 +- tests/components/mqtt_json/test_device_tracker.py | 2 +- tests/components/mqtt_room/test_sensor.py | 2 +- tests/components/mqtt_statestream/test_init.py | 3 ++- tests/components/myq/test_config_flow.py | 3 ++- tests/components/myq/util.py | 2 +- tests/components/mythicbeastsdns/test_init.py | 3 +-- tests/components/neato/test_config_flow.py | 3 ++- tests/components/ness_alarm/test_init.py | 3 +-- tests/components/nest/camera_sdm_test.py | 2 +- tests/components/nest/common.py | 2 +- tests/components/nest/test_config_flow_legacy.py | 3 +-- tests/components/nest/test_config_flow_sdm.py | 4 ++-- tests/components/nest/test_init_legacy.py | 2 +- tests/components/nest/test_init_sdm.py | 2 +- tests/components/netatmo/test_config_flow.py | 3 ++- tests/components/nexia/test_config_flow.py | 4 ++-- tests/components/nexia/util.py | 2 +- tests/components/nextbus/test_sensor.py | 2 +- tests/components/nightscout/__init__.py | 2 +- tests/components/nightscout/test_config_flow.py | 3 ++- tests/components/nightscout/test_init.py | 3 ++- tests/components/notion/test_config_flow.py | 3 ++- tests/components/nsw_fuel_station/test_sensor.py | 3 ++- .../test_geo_location.py | 2 +- tests/components/nuheat/mocks.py | 4 ++-- tests/components/nuheat/test_climate.py | 2 +- tests/components/nuheat/test_config_flow.py | 4 ++-- tests/components/nuheat/test_init.py | 4 ++-- tests/components/number/test_init.py | 4 ++-- tests/components/nut/test_config_flow.py | 3 ++- tests/components/nut/util.py | 2 +- tests/components/nws/conftest.py | 3 ++- tests/components/nws/test_config_flow.py | 4 ++-- tests/components/nws/test_weather.py | 2 +- tests/components/nzbget/__init__.py | 2 +- tests/components/nzbget/conftest.py | 4 ++-- tests/components/nzbget/test_config_flow.py | 3 ++- tests/components/nzbget/test_init.py | 3 ++- tests/components/nzbget/test_sensor.py | 3 +-- tests/components/omnilogic/test_config_flow.py | 3 ++- tests/components/onboarding/test_views.py | 2 +- tests/components/onewire/__init__.py | 3 ++- tests/components/onewire/test_binary_sensor.py | 2 +- tests/components/onewire/test_config_flow.py | 4 ++-- tests/components/onewire/test_entity_owserver.py | 3 ++- tests/components/onewire/test_entity_sysbus.py | 3 ++- tests/components/onewire/test_init.py | 3 ++- tests/components/onewire/test_sensor.py | 3 ++- tests/components/onewire/test_switch.py | 2 +- tests/components/onvif/test_config_flow.py | 3 ++- .../openalpr_cloud/test_image_processing.py | 2 +- .../openalpr_local/test_image_processing.py | 3 ++- tests/components/openerz/test_sensor.py | 4 ++-- tests/components/opentherm_gw/test_config_flow.py | 2 +- tests/components/openuv/test_config_flow.py | 3 ++- .../components/openweathermap/test_config_flow.py | 3 ++- tests/components/ovo_energy/test_config_flow.py | 3 ++- tests/components/owntracks/test_config_flow.py | 3 ++- tests/components/owntracks/test_device_tracker.py | 2 +- tests/components/owntracks/test_helper.py | 4 ++-- tests/components/ozw/common.py | 2 +- tests/components/ozw/conftest.py | 2 +- tests/components/ozw/test_config_flow.py | 3 ++- tests/components/ozw/test_init.py | 3 ++- tests/components/ozw/test_websocket_api.py | 4 ++-- .../components/panasonic_viera/test_config_flow.py | 3 ++- tests/components/panasonic_viera/test_init.py | 3 ++- tests/components/panel_custom/test_init.py | 4 ++-- tests/components/person/test_init.py | 2 +- tests/components/pi_hole/__init__.py | 4 ++-- tests/components/pi_hole/test_config_flow.py | 3 +-- tests/components/pi_hole/test_init.py | 2 +- tests/components/pilight/test_init.py | 2 +- tests/components/ping/test_binary_sensor.py | 3 +-- tests/components/plex/conftest.py | 3 ++- tests/components/plex/test_config_flow.py | 2 +- tests/components/plex/test_init.py | 2 +- tests/components/plex/test_media_players.py | 4 ++-- tests/components/plex/test_playback.py | 3 ++- tests/components/plex/test_server.py | 3 +-- tests/components/plex/test_services.py | 3 ++- tests/components/plugwise/conftest.py | 2 +- tests/components/plugwise/test_config_flow.py | 3 ++- tests/components/plum_lightpad/test_config_flow.py | 3 ++- tests/components/plum_lightpad/test_init.py | 3 ++- tests/components/point/test_config_flow.py | 3 +-- tests/components/poolsense/test_config_flow.py | 4 ++-- tests/components/powerwall/mocks.py | 2 +- tests/components/powerwall/test_binary_sensor.py | 4 ++-- tests/components/powerwall/test_config_flow.py | 4 ++-- tests/components/powerwall/test_sensor.py | 4 ++-- tests/components/profiler/test_config_flow.py | 3 ++- tests/components/profiler/test_init.py | 2 +- tests/components/progettihwsw/test_config_flow.py | 3 ++- tests/components/prometheus/test_init.py | 3 +-- tests/components/ps4/conftest.py | 4 ++-- tests/components/ps4/test_config_flow.py | 3 ++- tests/components/ps4/test_init.py | 3 ++- tests/components/ps4/test_media_player.py | 3 ++- tests/components/ptvsd/test_ptvsd.py | 4 ++-- tests/components/pushbullet/test_notify.py | 2 +- .../pvpc_hourly_pricing/test_config_flow.py | 2 +- .../components/pvpc_hourly_pricing/test_sensor.py | 2 +- tests/components/python_script/test_init.py | 2 +- tests/components/qld_bushfire/test_geo_location.py | 2 +- tests/components/qwikswitch/test_init.py | 2 +- tests/components/rachio/test_config_flow.py | 3 ++- tests/components/radarr/test_sensor.py | 4 ++-- tests/components/rainmachine/test_config_flow.py | 3 ++- tests/components/random/test_binary_sensor.py | 4 ++-- .../components/recollect_waste/test_config_flow.py | 3 ++- tests/components/recorder/test_init.py | 2 +- tests/components/recorder/test_migrate.py | 5 +++-- tests/components/recorder/test_purge.py | 3 +-- tests/components/recorder/test_util.py | 2 +- tests/components/reddit/test_sensor.py | 3 +-- tests/components/remember_the_milk/test_init.py | 4 ++-- tests/components/remote/test_device_condition.py | 2 +- tests/components/rest/test_binary_sensor.py | 3 +-- tests/components/rest/test_notify.py | 3 +-- tests/components/rest/test_sensor.py | 3 +-- tests/components/rflink/test_binary_sensor.py | 2 +- tests/components/rflink/test_init.py | 4 ++-- tests/components/rfxtrx/conftest.py | 2 +- tests/components/rfxtrx/test_config_flow.py | 2 +- tests/components/rfxtrx/test_init.py | 3 ++- tests/components/ring/common.py | 3 ++- tests/components/ring/test_binary_sensor.py | 3 +-- tests/components/ring/test_config_flow.py | 4 ++-- tests/components/risco/test_alarm_control_panel.py | 3 ++- tests/components/risco/test_binary_sensor.py | 3 ++- tests/components/risco/test_config_flow.py | 3 ++- tests/components/risco/test_sensor.py | 3 ++- tests/components/risco/util.py | 3 ++- tests/components/rmvtransport/test_sensor.py | 3 +-- tests/components/roku/test_config_flow.py | 3 ++- tests/components/roku/test_init.py | 3 ++- tests/components/roku/test_media_player.py | 2 +- tests/components/roku/test_remote.py | 3 ++- tests/components/roomba/test_config_flow.py | 3 ++- tests/components/roon/test_config_flow.py | 3 ++- tests/components/rpi_power/test_binary_sensor.py | 2 +- tests/components/rpi_power/test_config_flow.py | 3 ++- tests/components/ruckus_unleashed/__init__.py | 3 ++- .../ruckus_unleashed/test_config_flow.py | 2 +- .../ruckus_unleashed/test_device_tracker.py | 2 +- tests/components/ruckus_unleashed/test_init.py | 3 ++- tests/components/samsungtv/test_config_flow.py | 4 ++-- tests/components/samsungtv/test_init.py | 4 ++-- tests/components/samsungtv/test_media_player.py | 2 +- tests/components/script/test_init.py | 2 +- tests/components/season/test_sensor.py | 3 +-- tests/components/sense/test_config_flow.py | 4 ++-- tests/components/sentry/test_config_flow.py | 2 +- tests/components/sentry/test_init.py | 2 +- tests/components/seventeentrack/test_sensor.py | 2 +- tests/components/sharkiq/test_config_flow.py | 3 ++- tests/components/sharkiq/test_vacuum.py | 2 +- tests/components/shell_command/test_init.py | 3 +-- tests/components/shelly/conftest.py | 4 ++-- tests/components/shelly/test_config_flow.py | 2 +- tests/components/shopping_list/conftest.py | 3 ++- tests/components/signal_messenger/test_notify.py | 3 +-- tests/components/simplisafe/test_config_flow.py | 3 ++- tests/components/slack/test_notify.py | 4 +--- tests/components/sleepiq/test_binary_sensor.py | 3 ++- tests/components/sleepiq/test_init.py | 3 ++- tests/components/sleepiq/test_sensor.py | 3 ++- tests/components/smappee/test_config_flow.py | 3 ++- tests/components/smappee/test_init.py | 3 ++- .../smart_meter_texas/test_config_flow.py | 2 +- tests/components/smart_meter_texas/test_init.py | 4 ++-- tests/components/smart_meter_texas/test_sensor.py | 4 ++-- tests/components/smarthab/test_config_flow.py | 4 ++-- tests/components/smartthings/conftest.py | 2 +- tests/components/smartthings/test_config_flow.py | 2 +- tests/components/smartthings/test_init.py | 2 +- tests/components/smartthings/test_smartapp.py | 2 +- tests/components/smhi/common.py | 2 +- tests/components/smhi/test_config_flow.py | 4 ++-- tests/components/smhi/test_init.py | 4 ++-- tests/components/smhi/test_weather.py | 2 +- tests/components/smtp/test_notify.py | 3 +-- tests/components/solaredge/test_config_flow.py | 3 ++- tests/components/solarlog/test_config_flow.py | 3 ++- tests/components/soma/test_config_flow.py | 3 ++- tests/components/somfy/test_config_flow.py | 2 +- tests/components/sonarr/__init__.py | 2 +- tests/components/sonarr/test_config_flow.py | 3 ++- tests/components/sonarr/test_init.py | 3 ++- tests/components/sonarr/test_sensor.py | 2 +- tests/components/songpal/__init__.py | 4 ++-- tests/components/songpal/test_config_flow.py | 2 +- tests/components/songpal/test_init.py | 3 ++- tests/components/songpal/test_media_player.py | 2 +- tests/components/sonos/conftest.py | 3 ++- tests/components/soundtouch/test_media_player.py | 4 ++-- .../components/speedtestdotnet/test_config_flow.py | 2 +- tests/components/speedtestdotnet/test_init.py | 3 ++- tests/components/speedtestdotnet/test_sensor.py | 3 ++- tests/components/spider/test_config_flow.py | 3 ++- tests/components/spotify/test_config_flow.py | 3 ++- tests/components/squeezebox/test_config_flow.py | 3 ++- tests/components/srp_energy/__init__.py | 3 ++- tests/components/srp_energy/test_config_flow.py | 4 ++-- tests/components/srp_energy/test_sensor.py | 4 ++-- tests/components/statistics/test_sensor.py | 2 +- tests/components/statsd/test_init.py | 3 +-- tests/components/stream/test_hls.py | 2 +- tests/components/stream/test_init.py | 4 ++-- tests/components/stream/test_recorder.py | 2 +- tests/components/sun/test_init.py | 3 +-- tests/components/sun/test_trigger.py | 2 +- tests/components/surepetcare/__init__.py | 4 ++-- tests/components/surepetcare/conftest.py | 4 ++-- tests/components/switch/test_device_condition.py | 2 +- tests/components/switcher_kis/conftest.py | 3 +-- tests/components/syncthru/test_config_flow.py | 2 +- tests/components/synology_dsm/conftest.py | 4 ++-- tests/components/synology_dsm/test_config_flow.py | 3 ++- tests/components/synology_dsm/test_init.py | 3 ++- tests/components/system_health/test_init.py | 2 +- tests/components/system_log/test_init.py | 3 +-- tests/components/tado/test_config_flow.py | 3 ++- tests/components/tag/test_init.py | 4 ++-- tests/components/tasmota/conftest.py | 3 ++- tests/components/tasmota/test_binary_sensor.py | 2 +- tests/components/tasmota/test_common.py | 2 +- tests/components/tasmota/test_cover.py | 2 +- tests/components/tasmota/test_device_trigger.py | 2 +- tests/components/tasmota/test_discovery.py | 2 +- tests/components/tasmota/test_fan.py | 2 +- tests/components/tasmota/test_init.py | 2 +- tests/components/tasmota/test_light.py | 2 +- tests/components/tasmota/test_mixins.py | 2 +- tests/components/tasmota/test_sensor.py | 2 +- tests/components/tasmota/test_switch.py | 2 +- tests/components/tcp/test_binary_sensor.py | 2 +- tests/components/tcp/test_sensor.py | 2 +- tests/components/telegram/test_notify.py | 3 +-- tests/components/template/test_binary_sensor.py | 2 +- tests/components/template/test_sensor.py | 2 +- tests/components/template/test_trigger.py | 2 +- tests/components/tesla/test_config_flow.py | 3 ++- tests/components/tibber/test_config_flow.py | 3 ++- tests/components/tile/test_config_flow.py | 3 ++- tests/components/time_date/test_sensor.py | 4 ++-- tests/components/timer/test_init.py | 2 +- tests/components/tod/test_binary_sensor.py | 2 +- tests/components/toon/test_config_flow.py | 3 ++- tests/components/totalconnect/common.py | 3 ++- .../totalconnect/test_alarm_control_panel.py | 4 ++-- tests/components/totalconnect/test_config_flow.py | 3 ++- tests/components/tplink/test_light.py | 2 +- tests/components/traccar/test_init.py | 4 ++-- tests/components/tradfri/conftest.py | 3 ++- tests/components/tradfri/test_config_flow.py | 3 ++- tests/components/tradfri/test_init.py | 3 ++- tests/components/tradfri/test_light.py | 2 +- tests/components/transmission/test_config_flow.py | 2 +- tests/components/transport_nsw/test_sensor.py | 4 ++-- tests/components/trend/test_binary_sensor.py | 2 +- tests/components/tts/test_init.py | 3 ++- tests/components/tts/test_notify.py | 3 ++- tests/components/tuya/test_config_flow.py | 3 ++- tests/components/twilio/test_init.py | 4 ++-- tests/components/twinkly/test_config_flow.py | 3 ++- tests/components/twinkly/test_init.py | 2 +- tests/components/twinkly/test_twinkly.py | 2 +- tests/components/twitch/test_twitch.py | 4 ++-- tests/components/uk_transport/test_sensor.py | 2 +- tests/components/unifi/conftest.py | 4 ++-- tests/components/unifi/test_config_flow.py | 3 ++- tests/components/unifi/test_controller.py | 2 +- tests/components/unifi/test_init.py | 3 +-- .../components/unifi_direct/test_device_tracker.py | 2 +- tests/components/universal/test_media_player.py | 2 +- tests/components/upb/test_config_flow.py | 4 ++-- tests/components/updater/test_init.py | 3 ++- tests/components/upnp/test_config_flow.py | 2 +- tests/components/upnp/test_init.py | 3 ++- .../usgs_earthquakes_feed/test_geo_location.py | 2 +- tests/components/utility_meter/test_init.py | 3 +-- tests/components/utility_meter/test_sensor.py | 2 +- tests/components/velbus/test_config_flow.py | 3 ++- tests/components/vera/common.py | 2 +- tests/components/vera/conftest.py | 3 ++- tests/components/vera/test_binary_sensor.py | 4 ++-- tests/components/vera/test_climate.py | 4 ++-- tests/components/vera/test_common.py | 2 +- tests/components/vera/test_config_flow.py | 3 ++- tests/components/vera/test_cover.py | 4 ++-- tests/components/vera/test_init.py | 3 ++- tests/components/vera/test_light.py | 4 ++-- tests/components/vera/test_lock.py | 4 ++-- tests/components/vera/test_scene.py | 4 ++-- tests/components/vera/test_sensor.py | 3 +-- tests/components/vera/test_switch.py | 4 ++-- tests/components/verisure/test_ethernet_status.py | 3 +-- tests/components/verisure/test_lock.py | 3 +-- tests/components/version/test_sensor.py | 4 ++-- tests/components/vesync/test_config_flow.py | 3 ++- tests/components/vilfo/test_config_flow.py | 4 ++-- tests/components/vizio/conftest.py | 4 ++-- tests/components/vizio/test_media_player.py | 2 +- tests/components/volumio/test_config_flow.py | 3 ++- tests/components/vultr/test_binary_sensor.py | 2 +- tests/components/vultr/test_init.py | 2 +- tests/components/vultr/test_sensor.py | 2 +- tests/components/vultr/test_switch.py | 2 +- tests/components/wake_on_lan/test_init.py | 4 ++-- tests/components/wake_on_lan/test_switch.py | 2 +- tests/components/webhook/test_trigger.py | 3 ++- tests/components/webostv/test_media_player.py | 4 ++-- tests/components/websocket_api/test_http.py | 2 +- tests/components/websocket_api/test_init.py | 4 ++-- tests/components/wemo/conftest.py | 3 +-- tests/components/wemo/entity_test_helpers.py | 3 +-- tests/components/wemo/test_init.py | 2 +- tests/components/wemo/test_light_bridge.py | 4 ++-- tests/components/wiffi/test_config_flow.py | 2 +- tests/components/wilight/test_config_flow.py | 3 ++- tests/components/wilight/test_init.py | 3 ++- tests/components/wilight/test_light.py | 3 ++- tests/components/withings/common.py | 2 +- tests/components/withings/test_common.py | 2 +- tests/components/withings/test_init.py | 3 ++- tests/components/wled/test_config_flow.py | 3 ++- tests/components/wled/test_init.py | 3 ++- tests/components/wled/test_light.py | 2 +- tests/components/wled/test_sensor.py | 2 +- tests/components/wled/test_switch.py | 3 ++- tests/components/wolflink/test_config_flow.py | 3 ++- tests/components/workday/test_binary_sensor.py | 2 +- tests/components/xbox/test_config_flow.py | 3 ++- tests/components/xiaomi/test_device_tracker.py | 3 +-- tests/components/xiaomi_aqara/test_config_flow.py | 3 +-- tests/components/xiaomi_miio/test_config_flow.py | 4 ++-- tests/components/xiaomi_miio/test_vacuum.py | 3 +-- tests/components/yamaha/test_media_player.py | 4 ++-- .../test_yandex_transport_sensor.py | 2 +- tests/components/yeelight/__init__.py | 4 ++-- tests/components/yeelight/test_binary_sensor.py | 4 ++-- tests/components/yeelight/test_config_flow.py | 3 ++- tests/components/yeelight/test_init.py | 3 +-- tests/components/yeelight/test_light.py | 2 +- tests/components/zeroconf/test_init.py | 4 ++-- tests/components/zeroconf/test_usage.py | 4 ++-- tests/components/zerproc/test_config_flow.py | 4 ++-- tests/components/zerproc/test_light.py | 3 ++- tests/components/zha/common.py | 3 +-- tests/components/zha/conftest.py | 3 ++- tests/components/zha/test_api.py | 3 +-- tests/components/zha/test_channels.py | 2 +- tests/components/zha/test_climate.py | 4 ++-- tests/components/zha/test_config_flow.py | 2 +- tests/components/zha/test_cover.py | 2 +- tests/components/zha/test_device.py | 2 +- tests/components/zha/test_discover.py | 3 +-- tests/components/zha/test_fan.py | 4 ++-- tests/components/zha/test_init.py | 3 ++- tests/components/zha/test_light.py | 2 +- tests/components/zha/test_number.py | 3 ++- tests/components/zodiac/test_sensor.py | 3 +-- tests/components/zone/test_init.py | 3 ++- tests/components/zwave/conftest.py | 3 ++- tests/components/zwave/test_binary_sensor.py | 2 +- tests/components/zwave/test_cover.py | 3 ++- tests/components/zwave/test_init.py | 2 +- tests/components/zwave/test_light.py | 3 ++- tests/components/zwave/test_lock.py | 3 ++- tests/components/zwave/test_node_entity.py | 3 ++- tests/components/zwave/test_switch.py | 3 ++- tests/conftest.py | 2 +- tests/helpers/test_aiohttp_client.py | 3 +-- tests/helpers/test_area_registry.py | 4 ++-- tests/helpers/test_check_config.py | 2 +- tests/helpers/test_condition.py | 3 +-- tests/helpers/test_config_entry_flow.py | 3 ++- tests/helpers/test_config_entry_oauth2_flow.py | 2 +- tests/helpers/test_config_validation.py | 3 +-- tests/helpers/test_debounce.py | 4 ++-- tests/helpers/test_deprecation.py | 4 ++-- tests/helpers/test_device_registry.py | 2 +- tests/helpers/test_entity.py | 2 +- tests/helpers/test_entity_component.py | 2 +- tests/helpers/test_entity_platform.py | 2 +- tests/helpers/test_entity_registry.py | 6 +++--- tests/helpers/test_event.py | 2 +- tests/helpers/test_frame.py | 4 ++-- tests/helpers/test_httpx_client.py | 4 ++-- tests/helpers/test_instance_id.py | 2 +- tests/helpers/test_integration_platform.py | 3 ++- tests/helpers/test_network.py | 3 ++- tests/helpers/test_reload.py | 2 +- tests/helpers/test_restore_state.py | 3 +-- tests/helpers/test_script.py | 2 +- tests/helpers/test_service.py | 2 +- tests/helpers/test_singleton.py | 4 ++-- tests/helpers/test_state.py | 2 +- tests/helpers/test_storage.py | 2 +- tests/helpers/test_storage_remove.py | 2 +- tests/helpers/test_sun.py | 3 +-- tests/helpers/test_template.py | 3 +-- tests/helpers/test_translation.py | 3 +-- tests/helpers/test_update_coordinator.py | 2 +- tests/mock/zwave.py | 4 ++-- tests/scripts/test_auth.py | 3 ++- tests/scripts/test_check_config.py | 2 +- tests/scripts/test_init.py | 4 ++-- tests/test_bootstrap.py | 3 +-- tests/test_config.py | 2 +- tests/test_config_entries.py | 2 +- tests/test_core.py | 2 +- tests/test_loader.py | 3 ++- tests/test_main.py | 4 ++-- tests/test_requirements.py | 2 +- tests/test_setup.py | 2 +- tests/util/test_async.py | 3 +-- tests/util/test_init.py | 3 +-- tests/util/test_json.py | 3 +-- tests/util/test_location.py | 3 ++- tests/util/test_logging.py | 3 +-- tests/util/test_package.py | 3 +-- tests/util/yaml/test_init.py | 2 +- 906 files changed, 1360 insertions(+), 1254 deletions(-) delete mode 100644 tests/async_mock.py diff --git a/requirements_test.txt b/requirements_test.txt index f9bf5c679d5..3bd4ff8c479 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -4,7 +4,6 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt -asynctest==0.13.0 codecov==2.1.10 coverage==5.3 jsonpickle==1.4.1 diff --git a/script/scaffold/templates/config_flow/tests/test_config_flow.py b/script/scaffold/templates/config_flow/tests/test_config_flow.py index a69bea2abd8..04eab6e683c 100644 --- a/script/scaffold/templates/config_flow/tests/test_config_flow.py +++ b/script/scaffold/templates/config_flow/tests/test_config_flow.py @@ -1,10 +1,10 @@ """Test the NEW_NAME config flow.""" +from unittest.mock import patch + from homeassistant import config_entries, setup from homeassistant.components.NEW_DOMAIN.config_flow import CannotConnect, InvalidAuth from homeassistant.components.NEW_DOMAIN.const import DOMAIN -from tests.async_mock import patch - async def test_form(hass): """Test we get the form.""" 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 ed974601646..dd0fc3446b3 100644 --- a/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py +++ b/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py @@ -1,4 +1,6 @@ """Test the NEW_NAME config flow.""" +from unittest.mock import patch + from homeassistant import config_entries, setup from homeassistant.components.NEW_DOMAIN.const import ( DOMAIN, @@ -7,8 +9,6 @@ from homeassistant.components.NEW_DOMAIN.const import ( ) from homeassistant.helpers import config_entry_oauth2_flow -from tests.async_mock import patch - CLIENT_ID = "1234" CLIENT_SECRET = "5678" diff --git a/tests/async_mock.py b/tests/async_mock.py deleted file mode 100644 index 8257ddd3b3b..00000000000 --- a/tests/async_mock.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Mock utilities that are async aware.""" -import sys - -if sys.version_info[:2] < (3, 8): - from asynctest.mock import * # noqa - - AsyncMock = CoroutineMock # noqa: F405 -else: - from unittest.mock import * # noqa diff --git a/tests/auth/mfa_modules/test_notify.py b/tests/auth/mfa_modules/test_notify.py index 65ee5d5d0c5..c79d76baf4f 100644 --- a/tests/auth/mfa_modules/test_notify.py +++ b/tests/auth/mfa_modules/test_notify.py @@ -1,12 +1,12 @@ """Test the HMAC-based One Time Password (MFA) auth module.""" import asyncio +from unittest.mock import patch from homeassistant import data_entry_flow from homeassistant.auth import auth_manager_from_config, models as auth_models from homeassistant.auth.mfa_modules import auth_mfa_module_from_config from homeassistant.components.notify import NOTIFY_SERVICE_SCHEMA -from tests.async_mock import patch from tests.common import MockUser, async_mock_service MOCK_CODE = "123456" diff --git a/tests/auth/mfa_modules/test_totp.py b/tests/auth/mfa_modules/test_totp.py index b14b20297eb..d0a4f3cf3ac 100644 --- a/tests/auth/mfa_modules/test_totp.py +++ b/tests/auth/mfa_modules/test_totp.py @@ -1,11 +1,11 @@ """Test the Time-based One Time Password (MFA) auth module.""" import asyncio +from unittest.mock import patch from homeassistant import data_entry_flow from homeassistant.auth import auth_manager_from_config, models as auth_models from homeassistant.auth.mfa_modules import auth_mfa_module_from_config -from tests.async_mock import patch from tests.common import MockUser MOCK_CODE = "123456" diff --git a/tests/auth/providers/test_command_line.py b/tests/auth/providers/test_command_line.py index 3915950cedb..e437ca9e331 100644 --- a/tests/auth/providers/test_command_line.py +++ b/tests/auth/providers/test_command_line.py @@ -1,6 +1,7 @@ """Tests for the command_line auth provider.""" import os +from unittest.mock import AsyncMock import uuid import pytest @@ -10,8 +11,6 @@ from homeassistant.auth import AuthManager, auth_store, models as auth_models from homeassistant.auth.providers import command_line from homeassistant.const import CONF_TYPE -from tests.async_mock import AsyncMock - @pytest.fixture def store(hass): diff --git a/tests/auth/providers/test_homeassistant.py b/tests/auth/providers/test_homeassistant.py index c804b237e8b..62093df7210 100644 --- a/tests/auth/providers/test_homeassistant.py +++ b/tests/auth/providers/test_homeassistant.py @@ -1,5 +1,6 @@ """Test the Home Assistant local auth provider.""" import asyncio +from unittest.mock import Mock, patch import pytest import voluptuous as vol @@ -11,8 +12,6 @@ from homeassistant.auth.providers import ( homeassistant as hass_auth, ) -from tests.async_mock import Mock, patch - @pytest.fixture def data(hass): diff --git a/tests/auth/providers/test_insecure_example.py b/tests/auth/providers/test_insecure_example.py index c2b16cbafab..235f9a4735f 100644 --- a/tests/auth/providers/test_insecure_example.py +++ b/tests/auth/providers/test_insecure_example.py @@ -1,4 +1,5 @@ """Tests for the insecure example auth provider.""" +from unittest.mock import AsyncMock import uuid import pytest @@ -6,8 +7,6 @@ import pytest from homeassistant.auth import AuthManager, auth_store, models as auth_models from homeassistant.auth.providers import insecure_example -from tests.async_mock import AsyncMock - @pytest.fixture def store(hass): diff --git a/tests/auth/test_auth_store.py b/tests/auth/test_auth_store.py index 78ab9829ab6..4ab0fc4a360 100644 --- a/tests/auth/test_auth_store.py +++ b/tests/auth/test_auth_store.py @@ -1,10 +1,9 @@ """Tests for the auth store.""" import asyncio +from unittest.mock import patch from homeassistant.auth import auth_store -from tests.async_mock import patch - async def test_loading_no_group_data_format(hass, hass_storage): """Test we correctly load old data without any groups.""" diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py index f303a59179b..edcd01d51e1 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -1,5 +1,6 @@ """Tests for the Home Assistant auth module.""" from datetime import timedelta +from unittest.mock import Mock, patch import jwt import pytest @@ -11,7 +12,6 @@ from homeassistant.auth.const import MFA_SESSION_EXPIRATION from homeassistant.core import callback from homeassistant.util import dt as dt_util -from tests.async_mock import Mock, patch from tests.common import CLIENT_ID, MockUser, ensure_auth_manager_loaded, flush_store diff --git a/tests/common.py b/tests/common.py index ce07f5ab615..2621f2f4b15 100644 --- a/tests/common.py +++ b/tests/common.py @@ -12,6 +12,7 @@ import os import pathlib import threading import time +from unittest.mock import AsyncMock, Mock, patch import uuid from aiohttp.test_utils import unused_port as get_test_instance_port # noqa @@ -60,8 +61,6 @@ import homeassistant.util.dt as date_util from homeassistant.util.unit_system import METRIC_SYSTEM import homeassistant.util.yaml.loader as yaml_loader -from tests.async_mock import AsyncMock, Mock, patch - _LOGGER = logging.getLogger(__name__) INSTANCES = [] CLIENT_ID = "https://example.com/app" diff --git a/tests/components/abode/common.py b/tests/components/abode/common.py index 157a5441bb1..aabc732daa2 100644 --- a/tests/components/abode/common.py +++ b/tests/components/abode/common.py @@ -1,9 +1,10 @@ """Common methods used across tests for Abode.""" +from unittest.mock import patch + from homeassistant.components.abode import DOMAIN as ABODE_DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/abode/test_alarm_control_panel.py b/tests/components/abode/test_alarm_control_panel.py index b45599c408e..63ae20441f5 100644 --- a/tests/components/abode/test_alarm_control_panel.py +++ b/tests/components/abode/test_alarm_control_panel.py @@ -1,4 +1,6 @@ """Tests for the Abode alarm control panel device.""" +from unittest.mock import PropertyMock, patch + import abodepy.helpers.constants as CONST from homeassistant.components.abode import ATTR_DEVICE_ID @@ -17,8 +19,6 @@ from homeassistant.const import ( from .common import setup_platform -from tests.async_mock import PropertyMock, patch - DEVICE_ID = "alarm_control_panel.abode_alarm" diff --git a/tests/components/abode/test_camera.py b/tests/components/abode/test_camera.py index 9db03d90222..06540955464 100644 --- a/tests/components/abode/test_camera.py +++ b/tests/components/abode/test_camera.py @@ -1,12 +1,12 @@ """Tests for the Abode camera device.""" +from unittest.mock import patch + from homeassistant.components.abode.const import DOMAIN as ABODE_DOMAIN from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN from homeassistant.const import ATTR_ENTITY_ID, STATE_IDLE from .common import setup_platform -from tests.async_mock import patch - async def test_entity_registry(hass): """Tests that the devices are registered in the entity registry.""" diff --git a/tests/components/abode/test_config_flow.py b/tests/components/abode/test_config_flow.py index f1445db340f..026735ed536 100644 --- a/tests/components/abode/test_config_flow.py +++ b/tests/components/abode/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for the Abode config flow.""" +from unittest.mock import patch + from abodepy.exceptions import AbodeAuthenticationException from abodepy.helpers.errors import MFA_CODE_REQUIRED @@ -13,7 +15,6 @@ from homeassistant.const import ( HTTP_INTERNAL_SERVER_ERROR, ) -from tests.async_mock import patch from tests.common import MockConfigEntry CONF_POLLING = "polling" diff --git a/tests/components/abode/test_cover.py b/tests/components/abode/test_cover.py index b166ec5464a..bb1b8fceffb 100644 --- a/tests/components/abode/test_cover.py +++ b/tests/components/abode/test_cover.py @@ -1,4 +1,6 @@ """Tests for the Abode cover device.""" +from unittest.mock import patch + from homeassistant.components.abode import ATTR_DEVICE_ID from homeassistant.components.cover import DOMAIN as COVER_DOMAIN from homeassistant.const import ( @@ -11,8 +13,6 @@ from homeassistant.const import ( from .common import setup_platform -from tests.async_mock import patch - DEVICE_ID = "cover.garage_door" diff --git a/tests/components/abode/test_init.py b/tests/components/abode/test_init.py index 68f7ce9dd03..b4f3dbd736b 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 unittest.mock import patch + from abodepy.exceptions import AbodeAuthenticationException from homeassistant.components.abode import ( @@ -12,8 +14,6 @@ from homeassistant.const import CONF_USERNAME, HTTP_BAD_REQUEST from .common import setup_platform -from tests.async_mock import patch - async def test_change_settings(hass): """Test change_setting service.""" diff --git a/tests/components/abode/test_light.py b/tests/components/abode/test_light.py index 6506746783c..f0eee4b209b 100644 --- a/tests/components/abode/test_light.py +++ b/tests/components/abode/test_light.py @@ -1,4 +1,6 @@ """Tests for the Abode light device.""" +from unittest.mock import patch + from homeassistant.components.abode import ATTR_DEVICE_ID from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -17,8 +19,6 @@ from homeassistant.const import ( from .common import setup_platform -from tests.async_mock import patch - DEVICE_ID = "light.living_room_lamp" diff --git a/tests/components/abode/test_lock.py b/tests/components/abode/test_lock.py index 6850eebe0ce..45e17861d33 100644 --- a/tests/components/abode/test_lock.py +++ b/tests/components/abode/test_lock.py @@ -1,4 +1,6 @@ """Tests for the Abode lock device.""" +from unittest.mock import patch + from homeassistant.components.abode import ATTR_DEVICE_ID from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.const import ( @@ -11,8 +13,6 @@ from homeassistant.const import ( from .common import setup_platform -from tests.async_mock import patch - DEVICE_ID = "lock.test_lock" diff --git a/tests/components/abode/test_switch.py b/tests/components/abode/test_switch.py index 5c480b33225..3ec9648d87d 100644 --- a/tests/components/abode/test_switch.py +++ b/tests/components/abode/test_switch.py @@ -1,4 +1,6 @@ """Tests for the Abode switch device.""" +from unittest.mock import patch + from homeassistant.components.abode import ( DOMAIN as ABODE_DOMAIN, SERVICE_TRIGGER_AUTOMATION, @@ -14,8 +16,6 @@ from homeassistant.const import ( from .common import setup_platform -from tests.async_mock import patch - AUTOMATION_ID = "switch.test_automation" AUTOMATION_UID = "47fae27488f74f55b964a81a066c3a01" DEVICE_ID = "switch.test_switch" diff --git a/tests/components/accuweather/__init__.py b/tests/components/accuweather/__init__.py index 28e53d1e2fc..d78eac4269b 100644 --- a/tests/components/accuweather/__init__.py +++ b/tests/components/accuweather/__init__.py @@ -1,9 +1,9 @@ """Tests for AccuWeather.""" import json +from unittest.mock import patch from homeassistant.components.accuweather.const import DOMAIN -from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture diff --git a/tests/components/accuweather/test_config_flow.py b/tests/components/accuweather/test_config_flow.py index 89159d7c1bf..1d9feecda3c 100644 --- a/tests/components/accuweather/test_config_flow.py +++ b/tests/components/accuweather/test_config_flow.py @@ -1,5 +1,6 @@ """Define tests for the AccuWeather config flow.""" import json +from unittest.mock import patch from accuweather import ApiError, InvalidApiKeyError, RequestsExceededError @@ -8,7 +9,6 @@ from homeassistant.components.accuweather.const import CONF_FORECAST, DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME -from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture VALID_CONFIG = { diff --git a/tests/components/accuweather/test_init.py b/tests/components/accuweather/test_init.py index 0a54132fd68..3e480d57278 100644 --- a/tests/components/accuweather/test_init.py +++ b/tests/components/accuweather/test_init.py @@ -1,4 +1,6 @@ """Test init of AccuWeather integration.""" +from unittest.mock import patch + from homeassistant.components.accuweather.const import DOMAIN from homeassistant.config_entries import ( ENTRY_STATE_LOADED, @@ -7,7 +9,6 @@ from homeassistant.config_entries import ( ) from homeassistant.const import STATE_UNAVAILABLE -from tests.async_mock import patch from tests.common import MockConfigEntry from tests.components.accuweather import init_integration diff --git a/tests/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py index 185f6024886..361422883d4 100644 --- a/tests/components/accuweather/test_sensor.py +++ b/tests/components/accuweather/test_sensor.py @@ -1,6 +1,7 @@ """Test sensor of AccuWeather integration.""" from datetime import timedelta import json +from unittest.mock import patch from homeassistant.components.accuweather.const import ATTRIBUTION, DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -24,7 +25,6 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from tests.async_mock import patch from tests.common import async_fire_time_changed, load_fixture from tests.components.accuweather import init_integration diff --git a/tests/components/accuweather/test_system_health.py b/tests/components/accuweather/test_system_health.py index cf8c931e123..749f516e44c 100644 --- a/tests/components/accuweather/test_system_health.py +++ b/tests/components/accuweather/test_system_health.py @@ -1,12 +1,12 @@ """Test AccuWeather system health.""" import asyncio +from unittest.mock import Mock 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 diff --git a/tests/components/accuweather/test_weather.py b/tests/components/accuweather/test_weather.py index 692b2ae243f..0c1559ef0d6 100644 --- a/tests/components/accuweather/test_weather.py +++ b/tests/components/accuweather/test_weather.py @@ -1,6 +1,7 @@ """Test weather of AccuWeather integration.""" from datetime import timedelta import json +from unittest.mock import patch from homeassistant.components.accuweather.const import ATTRIBUTION from homeassistant.components.weather import ( @@ -25,7 +26,6 @@ from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ENTITY_ID, STATE_UNAVAILA from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from tests.async_mock import patch from tests.common import async_fire_time_changed, load_fixture from tests.components.accuweather import init_integration diff --git a/tests/components/acmeda/test_config_flow.py b/tests/components/acmeda/test_config_flow.py index 2aacedc3680..269a72cd839 100644 --- a/tests/components/acmeda/test_config_flow.py +++ b/tests/components/acmeda/test_config_flow.py @@ -1,4 +1,6 @@ """Define tests for the Acmeda config flow.""" +from unittest.mock import patch + import aiopulse import pytest @@ -7,7 +9,6 @@ from homeassistant.components.acmeda.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST -from tests.async_mock import patch from tests.common import MockConfigEntry DUMMY_HOST1 = "127.0.0.1" diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py index 36263335dac..06fe235741f 100644 --- a/tests/components/adguard/test_config_flow.py +++ b/tests/components/adguard/test_config_flow.py @@ -1,5 +1,7 @@ """Tests for the AdGuard Home config flow.""" +from unittest.mock import patch + import aiohttp from homeassistant import config_entries, data_entry_flow @@ -15,7 +17,6 @@ from homeassistant.const import ( CONTENT_TYPE_JSON, ) -from tests.async_mock import patch from tests.common import MockConfigEntry FIXTURE_USER_INPUT = { diff --git a/tests/components/advantage_air/test_config_flow.py b/tests/components/advantage_air/test_config_flow.py index 248ee25858b..a8e219fff89 100644 --- a/tests/components/advantage_air/test_config_flow.py +++ b/tests/components/advantage_air/test_config_flow.py @@ -1,9 +1,10 @@ """Test the Advantage Air config flow.""" +from unittest.mock import patch + from homeassistant import config_entries, data_entry_flow from homeassistant.components.advantage_air.const import DOMAIN -from tests.async_mock import patch from tests.components.advantage_air import TEST_SYSTEM_DATA, TEST_SYSTEM_URL, USER_INPUT diff --git a/tests/components/airly/test_system_health.py b/tests/components/airly/test_system_health.py index b1f8119e880..02ee67ae452 100644 --- a/tests/components/airly/test_system_health.py +++ b/tests/components/airly/test_system_health.py @@ -1,12 +1,12 @@ """Test Airly system health.""" import asyncio +from unittest.mock import Mock 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 diff --git a/tests/components/airnow/test_config_flow.py b/tests/components/airnow/test_config_flow.py index 2db3c22795f..f7533b7f5ac 100644 --- a/tests/components/airnow/test_config_flow.py +++ b/tests/components/airnow/test_config_flow.py @@ -1,11 +1,12 @@ """Test the AirNow config flow.""" +from unittest.mock import patch + from pyairnow.errors import AirNowError, InvalidKeyError from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.airnow.const import DOMAIN from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS -from tests.async_mock import patch from tests.common import MockConfigEntry CONFIG = { diff --git a/tests/components/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py index 3eb22360c5c..4e550d94b09 100644 --- a/tests/components/airvisual/test_config_flow.py +++ b/tests/components/airvisual/test_config_flow.py @@ -1,4 +1,6 @@ """Define tests for the AirVisual config flow.""" +from unittest.mock import patch + from pyairvisual.errors import InvalidKeyError, NodeProError from homeassistant import data_entry_flow @@ -20,7 +22,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/alarmdecoder/test_config_flow.py b/tests/components/alarmdecoder/test_config_flow.py index b858671fb54..52d1686691c 100644 --- a/tests/components/alarmdecoder/test_config_flow.py +++ b/tests/components/alarmdecoder/test_config_flow.py @@ -1,4 +1,6 @@ """Test the AlarmDecoder config flow.""" +from unittest.mock import patch + from alarmdecoder.util import NoDeviceError import pytest @@ -29,7 +31,6 @@ from homeassistant.components.binary_sensor import DEVICE_CLASS_WINDOW from homeassistant.const import CONF_HOST, CONF_PORT, CONF_PROTOCOL from homeassistant.core import HomeAssistant -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 2fcc3a236e3..0bdbac70d7d 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -1,4 +1,6 @@ """Test Alexa capabilities.""" +from unittest.mock import patch + import pytest from homeassistant.components.alexa import smart_home @@ -33,7 +35,6 @@ from . import ( reported_properties, ) -from tests.async_mock import patch from tests.common import async_mock_service diff --git a/tests/components/alexa/test_entities.py b/tests/components/alexa/test_entities.py index 6b48c313fcc..c1769bc8d06 100644 --- a/tests/components/alexa/test_entities.py +++ b/tests/components/alexa/test_entities.py @@ -1,10 +1,10 @@ """Test Alexa entity representation.""" +from unittest.mock import patch + from homeassistant.components.alexa import smart_home from . import DEFAULT_CONFIG, get_new_request -from tests.async_mock import patch - async def test_unsupported_domain(hass): """Discovery ignores entities of unknown domains.""" diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 9a7f5760270..05a60c86ae0 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1,5 +1,7 @@ """Test for smart home alexa support.""" +from unittest.mock import patch + import pytest from homeassistant.components.alexa import messages, smart_home @@ -40,7 +42,6 @@ from . import ( reported_properties, ) -from tests.async_mock import patch from tests.common import async_mock_service diff --git a/tests/components/almond/test_config_flow.py b/tests/components/almond/test_config_flow.py index afcad55bf2a..8f6b68e47ee 100644 --- a/tests/components/almond/test_config_flow.py +++ b/tests/components/almond/test_config_flow.py @@ -1,5 +1,6 @@ """Test the Almond config flow.""" import asyncio +from unittest.mock import patch from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.almond import config_flow @@ -7,7 +8,6 @@ from homeassistant.components.almond.const import DOMAIN from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow -from tests.async_mock import patch from tests.common import MockConfigEntry CLIENT_ID_VALUE = "1234" diff --git a/tests/components/almond/test_init.py b/tests/components/almond/test_init.py index 35a641bac01..fb74bdfa7f5 100644 --- a/tests/components/almond/test_init.py +++ b/tests/components/almond/test_init.py @@ -1,5 +1,6 @@ """Tests for Almond set up.""" from time import time +from unittest.mock import patch import pytest @@ -10,7 +11,6 @@ from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/ambiclimate/test_config_flow.py b/tests/components/ambiclimate/test_config_flow.py index 04c714ebb5f..b87c2171815 100644 --- a/tests/components/ambiclimate/test_config_flow.py +++ b/tests/components/ambiclimate/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for the Ambiclimate config flow.""" +from unittest.mock import AsyncMock, patch + import ambiclimate from homeassistant import data_entry_flow @@ -7,8 +9,6 @@ from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.setup import async_setup_component from homeassistant.util import aiohttp -from tests.async_mock import AsyncMock, patch - async def init_config_flow(hass): """Init a configuration flow.""" diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py index b5b6bbd0a46..b8ee4aaa2cd 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -1,6 +1,6 @@ """Define patches used for androidtv tests.""" -from tests.async_mock import mock_open, patch +from unittest.mock import mock_open, patch KEY_PYTHON = "python" KEY_SERVER = "server" diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index f6d189cfbc2..a9a803741b1 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -2,6 +2,7 @@ import base64 import copy import logging +from unittest.mock import patch from androidtv.constants import APPS as ANDROIDTV_APPS from androidtv.exceptions import LockNotAcquiredException @@ -58,7 +59,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.components.androidtv import patchers SHELL_RESPONSE_OFF = "" diff --git a/tests/components/apache_kafka/test_init.py b/tests/components/apache_kafka/test_init.py index ea62bb0569d..7e793bce96a 100644 --- a/tests/components/apache_kafka/test_init.py +++ b/tests/components/apache_kafka/test_init.py @@ -2,6 +2,7 @@ from asyncio import AbstractEventLoop from dataclasses import dataclass from typing import Callable, Type +from unittest.mock import patch import pytest @@ -9,8 +10,6 @@ import homeassistant.components.apache_kafka as apache_kafka from homeassistant.const import STATE_ON from homeassistant.setup import async_setup_component -from tests.async_mock import patch - APACHE_KAFKA_PATH = "homeassistant.components.apache_kafka" PRODUCER_PATH = f"{APACHE_KAFKA_PATH}.AIOKafkaProducer" MIN_CONFIG = { diff --git a/tests/components/api/test_init.py b/tests/components/api/test_init.py index e54af925dec..678a8096af5 100644 --- a/tests/components/api/test_init.py +++ b/tests/components/api/test_init.py @@ -1,6 +1,7 @@ """The tests for the Home Assistant API component.""" # pylint: disable=protected-access import json +from unittest.mock import patch from aiohttp import web import pytest @@ -11,7 +12,6 @@ from homeassistant.bootstrap import DATA_LOGGING import homeassistant.core as ha from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import async_mock_service diff --git a/tests/components/apns/test_notify.py b/tests/components/apns/test_notify.py index 6aa327a3943..22d0a30ab16 100644 --- a/tests/components/apns/test_notify.py +++ b/tests/components/apns/test_notify.py @@ -1,5 +1,6 @@ """The tests for the APNS component.""" import io +from unittest.mock import Mock, mock_open, patch from apns2.errors import Unregistered import pytest @@ -10,7 +11,6 @@ import homeassistant.components.notify as notify from homeassistant.core import State from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, mock_open, patch from tests.common import assert_setup_component CONFIG = { diff --git a/tests/components/apple_tv/conftest.py b/tests/components/apple_tv/conftest.py index 50b57e073d9..db543007fb2 100644 --- a/tests/components/apple_tv/conftest.py +++ b/tests/components/apple_tv/conftest.py @@ -1,12 +1,12 @@ """Fixtures for component.""" +from unittest.mock import patch + 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(): diff --git a/tests/components/apple_tv/test_config_flow.py b/tests/components/apple_tv/test_config_flow.py index 50344dc3c05..c55bef8edfd 100644 --- a/tests/components/apple_tv/test_config_flow.py +++ b/tests/components/apple_tv/test_config_flow.py @@ -1,5 +1,7 @@ """Test config flow.""" +from unittest.mock import patch + from pyatv import exceptions from pyatv.const import Protocol import pytest @@ -7,7 +9,6 @@ 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 = { diff --git a/tests/components/apprise/test_notify.py b/tests/components/apprise/test_notify.py index 125971016cb..8135f4e8e2c 100644 --- a/tests/components/apprise/test_notify.py +++ b/tests/components/apprise/test_notify.py @@ -1,7 +1,7 @@ """The tests for the apprise notification platform.""" -from homeassistant.setup import async_setup_component +from unittest.mock import MagicMock, patch -from tests.async_mock import MagicMock, patch +from homeassistant.setup import async_setup_component BASE_COMPONENT = "notify" diff --git a/tests/components/aprs/test_device_tracker.py b/tests/components/aprs/test_device_tracker.py index 95cdf4befec..dc0cf09f28d 100644 --- a/tests/components/aprs/test_device_tracker.py +++ b/tests/components/aprs/test_device_tracker.py @@ -1,10 +1,11 @@ """Test APRS device tracker.""" +from unittest.mock import Mock, patch + import aprslib import homeassistant.components.aprs.device_tracker as device_tracker from homeassistant.const import EVENT_HOMEASSISTANT_START -from tests.async_mock import Mock, patch from tests.common import get_test_home_assistant DEFAULT_PORT = 14580 diff --git a/tests/components/arcam_fmj/conftest.py b/tests/components/arcam_fmj/conftest.py index dfdc9e434f2..2ef0df9511e 100644 --- a/tests/components/arcam_fmj/conftest.py +++ b/tests/components/arcam_fmj/conftest.py @@ -1,4 +1,6 @@ """Tests for the arcam_fmj component.""" +from unittest.mock import Mock, patch + from arcam.fmj.client import Client from arcam.fmj.state import State import pytest @@ -7,7 +9,6 @@ from homeassistant.components.arcam_fmj.const import DEFAULT_NAME from homeassistant.components.arcam_fmj.media_player import ArcamFmj from homeassistant.const import CONF_HOST, CONF_PORT -from tests.async_mock import Mock, patch from tests.common import MockConfigEntry MOCK_HOST = "127.0.0.1" diff --git a/tests/components/arcam_fmj/test_config_flow.py b/tests/components/arcam_fmj/test_config_flow.py index 7de4b83723e..6c86b2bbf96 100644 --- a/tests/components/arcam_fmj/test_config_flow.py +++ b/tests/components/arcam_fmj/test_config_flow.py @@ -1,5 +1,7 @@ """Tests for the Arcam FMJ config flow module.""" +from unittest.mock import AsyncMock, patch + from arcam.fmj.client import ConnectionFailed import pytest @@ -19,7 +21,6 @@ from .conftest import ( MOCK_UUID, ) -from tests.async_mock import AsyncMock, patch from tests.common import MockConfigEntry MOCK_UPNP_DEVICE = f""" diff --git a/tests/components/arcam_fmj/test_media_player.py b/tests/components/arcam_fmj/test_media_player.py index 91117cff0a2..05a070aada2 100644 --- a/tests/components/arcam_fmj/test_media_player.py +++ b/tests/components/arcam_fmj/test_media_player.py @@ -1,5 +1,6 @@ """Tests for arcam fmj receivers.""" from math import isclose +from unittest.mock import ANY, MagicMock, Mock, PropertyMock, patch from arcam.fmj import DecodeMode2CH, DecodeModeMCH, IncomingAudioFormat, SourceCodes import pytest @@ -13,8 +14,6 @@ from homeassistant.const import ATTR_ENTITY_ID from .conftest import MOCK_HOST, MOCK_NAME, MOCK_PORT, MOCK_UUID -from tests.async_mock import ANY, MagicMock, Mock, PropertyMock, patch - MOCK_TURN_ON = { "service": "switch.turn_on", "data": {"entity_id": "switch.test"}, diff --git a/tests/components/arlo/test_sensor.py b/tests/components/arlo/test_sensor.py index 85a1d1e315a..5d729a5a658 100644 --- a/tests/components/arlo/test_sensor.py +++ b/tests/components/arlo/test_sensor.py @@ -1,5 +1,6 @@ """The tests for the Netgear Arlo sensors.""" from collections import namedtuple +from unittest.mock import patch import pytest @@ -11,8 +12,6 @@ from homeassistant.const import ( PERCENTAGE, ) -from tests.async_mock import patch - def _get_named_tuple(input_dict): return namedtuple("Struct", input_dict.keys())(*input_dict.values()) diff --git a/tests/components/asuswrt/test_device_tracker.py b/tests/components/asuswrt/test_device_tracker.py index ed733b54d25..941b0c340d6 100644 --- a/tests/components/asuswrt/test_device_tracker.py +++ b/tests/components/asuswrt/test_device_tracker.py @@ -1,5 +1,7 @@ """The tests for the ASUSWRT device tracker platform.""" +from unittest.mock import AsyncMock, patch + from homeassistant.components.asuswrt import ( CONF_DNSMASQ, CONF_INTERFACE, @@ -9,8 +11,6 @@ from homeassistant.components.asuswrt import ( from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock, patch - async def test_password_or_pub_key_required(hass): """Test creating an AsusWRT scanner without a pass or pubkey.""" diff --git a/tests/components/asuswrt/test_sensor.py b/tests/components/asuswrt/test_sensor.py index 7c929992473..69c70c409d5 100644 --- a/tests/components/asuswrt/test_sensor.py +++ b/tests/components/asuswrt/test_sensor.py @@ -1,5 +1,7 @@ """The tests for the AsusWrt sensor platform.""" +from unittest.mock import AsyncMock, patch + from aioasuswrt.asuswrt import Device from homeassistant.components import sensor @@ -16,8 +18,6 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock, patch - VALID_CONFIG_ROUTER_SSH = { DOMAIN: { CONF_DNSMASQ: "/", diff --git a/tests/components/atag/test_climate.py b/tests/components/atag/test_climate.py index 48418050d2d..3d511821baf 100644 --- a/tests/components/atag/test_climate.py +++ b/tests/components/atag/test_climate.py @@ -1,5 +1,7 @@ """Tests for the Atag climate platform.""" +from unittest.mock import PropertyMock, patch + from homeassistant.components.atag import CLIMATE, DOMAIN from homeassistant.components.climate import ( ATTR_HVAC_ACTION, @@ -19,7 +21,6 @@ from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.async_mock import PropertyMock, patch from tests.components.atag import UID, init_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/atag/test_config_flow.py b/tests/components/atag/test_config_flow.py index 59d8d99670c..81375792c71 100644 --- a/tests/components/atag/test_config_flow.py +++ b/tests/components/atag/test_config_flow.py @@ -1,11 +1,12 @@ """Tests for the Atag config flow.""" +from unittest.mock import PropertyMock, patch + from pyatag import errors from homeassistant import config_entries, data_entry_flow from homeassistant.components.atag import DOMAIN from homeassistant.core import HomeAssistant -from tests.async_mock import PropertyMock, patch from tests.components.atag import ( PAIR_REPLY, RECEIVE_REPLY, diff --git a/tests/components/atag/test_init.py b/tests/components/atag/test_init.py index 9f7ae9cb4ed..b86de8a8be5 100644 --- a/tests/components/atag/test_init.py +++ b/tests/components/atag/test_init.py @@ -1,11 +1,12 @@ """Tests for the ATAG integration.""" +from unittest.mock import patch + import aiohttp from homeassistant.components.atag import DOMAIN from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY from homeassistant.core import HomeAssistant -from tests.async_mock import patch from tests.components.atag import init_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/atag/test_water_heater.py b/tests/components/atag/test_water_heater.py index 0d717db70bc..5eb219fa3bc 100644 --- a/tests/components/atag/test_water_heater.py +++ b/tests/components/atag/test_water_heater.py @@ -1,11 +1,12 @@ """Tests for the Atag water heater platform.""" +from unittest.mock import patch + from homeassistant.components.atag import DOMAIN, WATER_HEATER from homeassistant.components.water_heater import SERVICE_SET_TEMPERATURE from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE from homeassistant.core import HomeAssistant -from tests.async_mock import patch from tests.components.atag import UID, init_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py index 93b64ebbd3f..e02e1ec59dd 100644 --- a/tests/components/august/mocks.py +++ b/tests/components/august/mocks.py @@ -3,6 +3,9 @@ import json import os import time +# from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch + from august.activity import ( ACTIVITY_ACTIONS_DOOR_OPERATION, ACTIVITY_ACTIONS_DOORBELL_DING, @@ -27,8 +30,6 @@ from homeassistant.components.august import ( ) from homeassistant.setup import async_setup_component -# from tests.async_mock import AsyncMock -from tests.async_mock import AsyncMock, MagicMock, PropertyMock, patch from tests.common import load_fixture diff --git a/tests/components/august/test_camera.py b/tests/components/august/test_camera.py index 3ec1b2d608c..151f7972e1e 100644 --- a/tests/components/august/test_camera.py +++ b/tests/components/august/test_camera.py @@ -1,8 +1,9 @@ """The camera tests for the august platform.""" +from unittest.mock import patch + from homeassistant.const import STATE_IDLE -from tests.async_mock import patch from tests.components.august.mocks import ( _create_august_with_devices, _mock_doorbell_from_fixture, diff --git a/tests/components/august/test_config_flow.py b/tests/components/august/test_config_flow.py index 0f9a8ebbd2f..c1e7c9bb3c5 100644 --- a/tests/components/august/test_config_flow.py +++ b/tests/components/august/test_config_flow.py @@ -1,4 +1,6 @@ """Test the August config flow.""" +from unittest.mock import patch + from august.authenticator import ValidationResult from homeassistant import config_entries, setup @@ -16,7 +18,6 @@ from homeassistant.components.august.exceptions import ( ) from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/august/test_gateway.py b/tests/components/august/test_gateway.py index c1aa0723baa..ced07360008 100644 --- a/tests/components/august/test_gateway.py +++ b/tests/components/august/test_gateway.py @@ -1,10 +1,11 @@ """The gateway tests for the august platform.""" +from unittest.mock import MagicMock, patch + from august.authenticator_common import AuthenticationState from homeassistant.components.august.const import DOMAIN from homeassistant.components.august.gateway import AugustGateway -from tests.async_mock import MagicMock, patch from tests.components.august.mocks import _mock_august_authentication, _mock_get_config diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py index f954ff83c25..e881ac09c97 100644 --- a/tests/components/august/test_init.py +++ b/tests/components/august/test_init.py @@ -1,5 +1,6 @@ """The tests for the august platform.""" import asyncio +from unittest.mock import patch from aiohttp import ClientResponseError from august.authenticator_common import AuthenticationState @@ -31,7 +32,6 @@ from homeassistant.const import ( from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import MockConfigEntry from tests.components.august.mocks import ( _create_august_with_devices, diff --git a/tests/components/aurora/test_config_flow.py b/tests/components/aurora/test_config_flow.py index 2f4b457a9dd..b9e0496f668 100644 --- a/tests/components/aurora/test_config_flow.py +++ b/tests/components/aurora/test_config_flow.py @@ -1,9 +1,10 @@ """Test the Aurora config flow.""" +from unittest.mock import patch + 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 = { diff --git a/tests/components/auth/test_indieauth.py b/tests/components/auth/test_indieauth.py index e8aabce4678..4cf7402725d 100644 --- a/tests/components/auth/test_indieauth.py +++ b/tests/components/auth/test_indieauth.py @@ -1,11 +1,11 @@ """Tests for the client validator.""" import asyncio +from unittest.mock import patch import pytest from homeassistant.components.auth import indieauth -from tests.async_mock import patch from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index 3d799fe0078..2c9a39c6fb6 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -1,5 +1,6 @@ """Integration tests for the auth component.""" from datetime import timedelta +from unittest.mock import patch from homeassistant.auth.models import Credentials from homeassistant.components import auth @@ -9,7 +10,6 @@ from homeassistant.util.dt import utcnow from . import async_setup_auth -from tests.async_mock import patch from tests.common import CLIENT_ID, CLIENT_REDIRECT_URI, MockUser diff --git a/tests/components/auth/test_login_flow.py b/tests/components/auth/test_login_flow.py index f2629a27bb9..e6e5281d601 100644 --- a/tests/components/auth/test_login_flow.py +++ b/tests/components/auth/test_login_flow.py @@ -1,7 +1,8 @@ """Tests for the login flow.""" +from unittest.mock import patch + from . import async_setup_auth -from tests.async_mock import patch from tests.common import CLIENT_ID, CLIENT_REDIRECT_URI diff --git a/tests/components/automation/test_blueprint.py b/tests/components/automation/test_blueprint.py index 56062af17b7..1a6ccec7a8e 100644 --- a/tests/components/automation/test_blueprint.py +++ b/tests/components/automation/test_blueprint.py @@ -3,6 +3,7 @@ import asyncio import contextlib from datetime import timedelta import pathlib +from unittest.mock import patch from homeassistant.components import automation from homeassistant.components.blueprint import models @@ -10,7 +11,6 @@ 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" diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 5f258fc28b7..244f37ecb9c 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1,5 +1,6 @@ """The tests for the automation component.""" import asyncio +from unittest.mock import Mock, patch import pytest @@ -28,7 +29,6 @@ from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import Mock, patch from tests.common import assert_setup_component, async_mock_service, mock_restore_cache from tests.components.logbook.test_init import MockLazyEventPartialState diff --git a/tests/components/awair/test_config_flow.py b/tests/components/awair/test_config_flow.py index 6dcbc7eac9e..92d92dd5a63 100644 --- a/tests/components/awair/test_config_flow.py +++ b/tests/components/awair/test_config_flow.py @@ -1,5 +1,7 @@ """Define tests for the Awair config flow.""" +from unittest.mock import patch + from python_awair.exceptions import AuthError, AwairError from homeassistant import data_entry_flow @@ -9,7 +11,6 @@ from homeassistant.const import CONF_ACCESS_TOKEN from .const import CONFIG, DEVICES_FIXTURE, NO_DEVICES_FIXTURE, UNIQUE_ID, USER_FIXTURE -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/awair/test_sensor.py b/tests/components/awair/test_sensor.py index 3b013fad29c..0fcbab99a3a 100644 --- a/tests/components/awair/test_sensor.py +++ b/tests/components/awair/test_sensor.py @@ -1,5 +1,7 @@ """Tests for the Awair sensor platform.""" +from unittest.mock import patch + from homeassistant.components.awair.const import ( API_CO2, API_HUMID, @@ -40,7 +42,6 @@ from .const import ( USER_FIXTURE, ) -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/aws/test_init.py b/tests/components/aws/test_init.py index 045ad2ff609..e50c0aa546b 100644 --- a/tests/components/aws/test_init.py +++ b/tests/components/aws/test_init.py @@ -1,9 +1,9 @@ """Tests for the aws component config and setup.""" +from unittest.mock import AsyncMock, MagicMock, patch as async_patch + from homeassistant.components import aws from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock, MagicMock, patch as async_patch - class MockAioSession: """Mock AioSession.""" diff --git a/tests/components/axis/test_camera.py b/tests/components/axis/test_camera.py index 01c5a677e38..9a5872b4f2d 100644 --- a/tests/components/axis/test_camera.py +++ b/tests/components/axis/test_camera.py @@ -1,5 +1,7 @@ """Axis camera platform tests.""" +from unittest.mock import patch + from homeassistant.components import camera from homeassistant.components.axis.const import ( CONF_STREAM_PROFILE, @@ -11,8 +13,6 @@ from homeassistant.setup import async_setup_component from .test_device import ENTRY_OPTIONS, NAME, setup_axis_integration -from tests.async_mock import patch - async def test_platform_manually_configured(hass): """Test that nothing happens when platform is manually configured.""" diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index b9dceec7477..a557c1144e5 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -1,4 +1,6 @@ """Test Axis config flow.""" +from unittest.mock import patch + from homeassistant import data_entry_flow from homeassistant.components.axis import config_flow from homeassistant.components.axis.const import ( @@ -25,7 +27,6 @@ from homeassistant.data_entry_flow import ( from .test_device import MAC, MODEL, NAME, setup_axis_integration, vapix_request -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index b7612a01c1d..ec9313e3cd5 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -1,6 +1,7 @@ """Test Axis device.""" from copy import deepcopy from unittest import mock +from unittest.mock import AsyncMock, Mock, patch import axis as axislib from axis.api_discovery import URL as API_DISCOVERY_URL @@ -40,7 +41,6 @@ from homeassistant.const import ( STATE_ON, ) -from tests.async_mock import AsyncMock, Mock, patch from tests.common import MockConfigEntry, async_fire_mqtt_message MAC = "00408C12345" diff --git a/tests/components/axis/test_init.py b/tests/components/axis/test_init.py index cf5253d4675..345dfac4d11 100644 --- a/tests/components/axis/test_init.py +++ b/tests/components/axis/test_init.py @@ -1,4 +1,6 @@ """Test Axis component setup process.""" +from unittest.mock import AsyncMock, Mock, patch + from homeassistant.components import axis from homeassistant.components.axis.const import CONF_MODEL, DOMAIN as AXIS_DOMAIN from homeassistant.const import ( @@ -14,7 +16,6 @@ from homeassistant.setup import async_setup_component from .test_device import MAC, setup_axis_integration -from tests.async_mock import AsyncMock, Mock, patch from tests.common import MockConfigEntry diff --git a/tests/components/axis/test_light.py b/tests/components/axis/test_light.py index 06612daa313..05b58c04565 100644 --- a/tests/components/axis/test_light.py +++ b/tests/components/axis/test_light.py @@ -1,6 +1,7 @@ """Axis light platform tests.""" from copy import deepcopy +from unittest.mock import patch from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN from homeassistant.components.light import ATTR_BRIGHTNESS, DOMAIN as LIGHT_DOMAIN @@ -15,8 +16,6 @@ from homeassistant.setup import async_setup_component from .test_device import API_DISCOVERY_RESPONSE, NAME, setup_axis_integration -from tests.async_mock import patch - API_DISCOVERY_LIGHT_CONTROL = { "id": "light-control", "version": "1.1", diff --git a/tests/components/axis/test_switch.py b/tests/components/axis/test_switch.py index fbcf0624fc9..2f8cde777b5 100644 --- a/tests/components/axis/test_switch.py +++ b/tests/components/axis/test_switch.py @@ -1,6 +1,7 @@ """Axis switch platform tests.""" from copy import deepcopy +from unittest.mock import AsyncMock, patch from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN @@ -20,8 +21,6 @@ from .test_device import ( setup_axis_integration, ) -from tests.async_mock import AsyncMock, patch - EVENTS = [ { "operation": "Initialized", diff --git a/tests/components/azure_devops/test_config_flow.py b/tests/components/azure_devops/test_config_flow.py index daad5992350..4cda9076b98 100644 --- a/tests/components/azure_devops/test_config_flow.py +++ b/tests/components/azure_devops/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Azure DevOps config flow.""" +from unittest.mock import patch + from aioazuredevops.core import DevOpsProject import aiohttp @@ -11,7 +13,6 @@ from homeassistant.components.azure_devops.const import ( ) from homeassistant.core import HomeAssistant -from tests.async_mock import patch from tests.common import MockConfigEntry FIXTURE_REAUTH_INPUT = {CONF_PAT: "abc123"} diff --git a/tests/components/azure_event_hub/test_init.py b/tests/components/azure_event_hub/test_init.py index 42bfefbcb3c..dd588ad7499 100644 --- a/tests/components/azure_event_hub/test_init.py +++ b/tests/components/azure_event_hub/test_init.py @@ -1,5 +1,6 @@ """The tests for the Azure Event Hub component.""" from dataclasses import dataclass +from unittest.mock import MagicMock, patch import pytest @@ -7,8 +8,6 @@ import homeassistant.components.azure_event_hub as azure_event_hub from homeassistant.const import STATE_ON from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, patch - AZURE_EVENT_HUB_PATH = "homeassistant.components.azure_event_hub" PRODUCER_PATH = f"{AZURE_EVENT_HUB_PATH}.EventHubProducerClient" MIN_CONFIG = { diff --git a/tests/components/bayesian/test_binary_sensor.py b/tests/components/bayesian/test_binary_sensor.py index 2be01679777..01f2664ea67 100644 --- a/tests/components/bayesian/test_binary_sensor.py +++ b/tests/components/bayesian/test_binary_sensor.py @@ -1,6 +1,7 @@ """The test for the bayesian sensor platform.""" import json from os import path +from unittest.mock import patch from homeassistant import config as hass_config from homeassistant.components.bayesian import DOMAIN, binary_sensor as bayesian @@ -18,8 +19,6 @@ from homeassistant.const import ( from homeassistant.core import Context, callback from homeassistant.setup import async_setup_component -from tests.async_mock import patch - async def test_load_values_when_added_to_hass(hass): """Test that sensor initializes with observations of relevant entities.""" diff --git a/tests/components/binary_sensor/test_device_condition.py b/tests/components/binary_sensor/test_device_condition.py index 7a7e80e36a3..f25b529d426 100644 --- a/tests/components/binary_sensor/test_device_condition.py +++ b/tests/components/binary_sensor/test_device_condition.py @@ -1,5 +1,6 @@ """The test for binary_sensor device automation.""" from datetime import timedelta +from unittest.mock import patch import pytest @@ -11,7 +12,6 @@ from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import ( MockConfigEntry, async_get_device_automation_capabilities, diff --git a/tests/components/blebox/conftest.py b/tests/components/blebox/conftest.py index e43fb9fc4b4..3b12c682f3f 100644 --- a/tests/components/blebox/conftest.py +++ b/tests/components/blebox/conftest.py @@ -1,5 +1,6 @@ """PyTest fixtures and test helpers.""" from unittest import mock +from unittest.mock import AsyncMock, PropertyMock, patch import blebox_uniapi import pytest @@ -8,7 +9,6 @@ from homeassistant.components.blebox.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock, PropertyMock, patch from tests.common import MockConfigEntry from tests.components.light.conftest import mock_light_profiles # noqa diff --git a/tests/components/blebox/test_air_quality.py b/tests/components/blebox/test_air_quality.py index 3467c94411c..4f1f6dff671 100644 --- a/tests/components/blebox/test_air_quality.py +++ b/tests/components/blebox/test_air_quality.py @@ -1,6 +1,7 @@ """Blebox air_quality tests.""" import logging +from unittest.mock import AsyncMock, PropertyMock import blebox_uniapi import pytest @@ -10,8 +11,6 @@ from homeassistant.const import ATTR_ICON, STATE_UNKNOWN from .conftest import async_setup_entity, mock_feature -from tests.async_mock import AsyncMock, PropertyMock - @pytest.fixture(name="airsensor") def airsensor_fixture(): diff --git a/tests/components/blebox/test_climate.py b/tests/components/blebox/test_climate.py index c36c93a7f98..baaa5a5009e 100644 --- a/tests/components/blebox/test_climate.py +++ b/tests/components/blebox/test_climate.py @@ -1,6 +1,7 @@ """BleBox climate entities tests.""" import logging +from unittest.mock import AsyncMock, PropertyMock import blebox_uniapi import pytest @@ -30,8 +31,6 @@ from homeassistant.const import ( from .conftest import async_setup_entity, mock_feature -from tests.async_mock import AsyncMock, PropertyMock - @pytest.fixture(name="saunabox") def saunabox_fixture(): diff --git a/tests/components/blebox/test_config_flow.py b/tests/components/blebox/test_config_flow.py index a7200b05b28..965c707d2af 100644 --- a/tests/components/blebox/test_config_flow.py +++ b/tests/components/blebox/test_config_flow.py @@ -1,5 +1,7 @@ """Test Home Assistant config flow for BleBox devices.""" +from unittest.mock import DEFAULT, AsyncMock, PropertyMock, patch + import blebox_uniapi import pytest @@ -9,8 +11,6 @@ from homeassistant.setup import async_setup_component from .conftest import mock_config, mock_only_feature, setup_product_mock -from tests.async_mock import DEFAULT, AsyncMock, PropertyMock, patch - def create_valid_feature_mock(path="homeassistant.components.blebox.Products"): """Return a valid, complete BleBox feature mock.""" diff --git a/tests/components/blebox/test_cover.py b/tests/components/blebox/test_cover.py index cd060445bf7..a5d3a8f705b 100644 --- a/tests/components/blebox/test_cover.py +++ b/tests/components/blebox/test_cover.py @@ -1,6 +1,7 @@ """BleBox cover entities tests.""" import logging +from unittest.mock import AsyncMock, PropertyMock import blebox_uniapi import pytest @@ -32,8 +33,6 @@ from homeassistant.const import ( from .conftest import async_setup_entity, mock_feature -from tests.async_mock import AsyncMock, PropertyMock - ALL_COVER_FIXTURES = ["gatecontroller", "shutterbox", "gatebox"] FIXTURES_SUPPORTING_STOP = ["gatecontroller", "shutterbox"] diff --git a/tests/components/blebox/test_light.py b/tests/components/blebox/test_light.py index 48af534545c..5d9e5709e4d 100644 --- a/tests/components/blebox/test_light.py +++ b/tests/components/blebox/test_light.py @@ -1,6 +1,7 @@ """BleBox light entities tests.""" import logging +from unittest.mock import AsyncMock, PropertyMock import blebox_uniapi import pytest @@ -24,8 +25,6 @@ from homeassistant.util import color from .conftest import async_setup_entity, mock_feature -from tests.async_mock import AsyncMock, PropertyMock - ALL_LIGHT_FIXTURES = ["dimmer", "wlightbox_s", "wlightbox"] diff --git a/tests/components/blebox/test_sensor.py b/tests/components/blebox/test_sensor.py index a19e628181c..aeb726cc726 100644 --- a/tests/components/blebox/test_sensor.py +++ b/tests/components/blebox/test_sensor.py @@ -1,6 +1,7 @@ """Blebox sensors tests.""" import logging +from unittest.mock import AsyncMock, PropertyMock import blebox_uniapi import pytest @@ -15,8 +16,6 @@ from homeassistant.const import ( from .conftest import async_setup_entity, mock_feature -from tests.async_mock import AsyncMock, PropertyMock - @pytest.fixture(name="tempsensor") def tempsensor_fixture(): diff --git a/tests/components/blebox/test_switch.py b/tests/components/blebox/test_switch.py index 43d7d811acc..e2bc1240510 100644 --- a/tests/components/blebox/test_switch.py +++ b/tests/components/blebox/test_switch.py @@ -1,6 +1,7 @@ """Blebox switch tests.""" import logging +from unittest.mock import AsyncMock, PropertyMock import blebox_uniapi import pytest @@ -22,8 +23,6 @@ from .conftest import ( setup_product_mock, ) -from tests.async_mock import AsyncMock, PropertyMock - @pytest.fixture(name="switchbox") def switchbox_fixture(): diff --git a/tests/components/blink/test_config_flow.py b/tests/components/blink/test_config_flow.py index 36e3fbd95ea..91264997769 100644 --- a/tests/components/blink/test_config_flow.py +++ b/tests/components/blink/test_config_flow.py @@ -1,11 +1,12 @@ """Test the Blink config flow.""" +from unittest.mock import Mock, patch + from blinkpy.auth import LoginError from blinkpy.blinkpy import BlinkSetupError from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.blink import DOMAIN -from tests.async_mock import Mock, patch from tests.common import MockConfigEntry diff --git a/tests/components/blueprint/conftest.py b/tests/components/blueprint/conftest.py index c8110ddaf08..ec76451065c 100644 --- a/tests/components/blueprint/conftest.py +++ b/tests/components/blueprint/conftest.py @@ -1,8 +1,8 @@ """Blueprints conftest.""" -import pytest +from unittest.mock import patch -from tests.async_mock import patch +import pytest @pytest.fixture(autouse=True) diff --git a/tests/components/blueprint/test_models.py b/tests/components/blueprint/test_models.py index 6e15bd952a4..ba8914c3b1d 100644 --- a/tests/components/blueprint/test_models.py +++ b/tests/components/blueprint/test_models.py @@ -1,13 +1,12 @@ """Test blueprint models.""" import logging +from unittest.mock import patch import pytest from homeassistant.components.blueprint import errors, models from homeassistant.util.yaml import Input -from tests.async_mock import patch - @pytest.fixture def blueprint_1(): diff --git a/tests/components/blueprint/test_websocket_api.py b/tests/components/blueprint/test_websocket_api.py index bb08414b6e8..6dfd445c634 100644 --- a/tests/components/blueprint/test_websocket_api.py +++ b/tests/components/blueprint/test_websocket_api.py @@ -1,12 +1,11 @@ """Test websocket API.""" from pathlib import Path +from unittest.mock import Mock, patch import pytest from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, patch - @pytest.fixture(autouse=True) async def setup_bp(hass): diff --git a/tests/components/bluetooth_le_tracker/test_device_tracker.py b/tests/components/bluetooth_le_tracker/test_device_tracker.py index af4df463339..308371c9aaa 100644 --- a/tests/components/bluetooth_le_tracker/test_device_tracker.py +++ b/tests/components/bluetooth_le_tracker/test_device_tracker.py @@ -1,6 +1,7 @@ """Test Bluetooth LE device tracker.""" from datetime import timedelta +from unittest.mock import patch from homeassistant.components.bluetooth_le_tracker import device_tracker from homeassistant.components.device_tracker.const import ( @@ -12,7 +13,6 @@ from homeassistant.const import CONF_PLATFORM from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util, slugify -from tests.async_mock import patch from tests.common import async_fire_time_changed diff --git a/tests/components/bmw_connected_drive/test_config_flow.py b/tests/components/bmw_connected_drive/test_config_flow.py index ae32feec7b1..52433f2f58f 100644 --- a/tests/components/bmw_connected_drive/test_config_flow.py +++ b/tests/components/bmw_connected_drive/test_config_flow.py @@ -1,4 +1,6 @@ """Test the for the BMW Connected Drive config flow.""" +from unittest.mock import patch + from homeassistant import config_entries, data_entry_flow from homeassistant.components.bmw_connected_drive.config_flow import DOMAIN from homeassistant.components.bmw_connected_drive.const import ( @@ -8,7 +10,6 @@ from homeassistant.components.bmw_connected_drive.const import ( ) from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import patch from tests.common import MockConfigEntry FIXTURE_USER_INPUT = { diff --git a/tests/components/bond/common.py b/tests/components/bond/common.py index d5e4a0b9418..9aaaf9a249d 100644 --- a/tests/components/bond/common.py +++ b/tests/components/bond/common.py @@ -3,6 +3,7 @@ from asyncio import TimeoutError as AsyncIOTimeoutError from contextlib import nullcontext from datetime import timedelta from typing import Any, Dict, Optional +from unittest.mock import patch from homeassistant import core from homeassistant.components.bond.const import DOMAIN as BOND_DOMAIN @@ -10,7 +11,6 @@ from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, STATE_UNAVAILABLE from homeassistant.setup import async_setup_component from homeassistant.util import utcnow -from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/bond/test_config_flow.py b/tests/components/bond/test_config_flow.py index b87891a1896..dba6c590641 100644 --- a/tests/components/bond/test_config_flow.py +++ b/tests/components/bond/test_config_flow.py @@ -1,5 +1,6 @@ """Test the Bond config flow.""" from typing import Any, Dict +from unittest.mock import Mock, patch from aiohttp import ClientConnectionError, ClientResponseError @@ -9,7 +10,6 @@ from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST from .common import patch_bond_device_ids, patch_bond_version -from tests.async_mock import Mock, patch from tests.common import MockConfigEntry diff --git a/tests/components/braviatv/test_config_flow.py b/tests/components/braviatv/test_config_flow.py index c6f76105cfc..cbe87f14839 100644 --- a/tests/components/braviatv/test_config_flow.py +++ b/tests/components/braviatv/test_config_flow.py @@ -1,4 +1,6 @@ """Define tests for the Bravia TV config flow.""" +from unittest.mock import patch + from bravia_tv.braviarc import NoIPControl from homeassistant import data_entry_flow @@ -6,7 +8,6 @@ from homeassistant.components.braviatv.const import CONF_IGNORED_SOURCES, DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN -from tests.async_mock import patch from tests.common import MockConfigEntry BRAVIA_SYSTEM_INFO = { diff --git a/tests/components/broadlink/__init__.py b/tests/components/broadlink/__init__.py index 86756c922f1..7185c605f5c 100644 --- a/tests/components/broadlink/__init__.py +++ b/tests/components/broadlink/__init__.py @@ -1,7 +1,8 @@ """Tests for the Broadlink integration.""" +from unittest.mock import MagicMock, patch + from homeassistant.components.broadlink.const import DOMAIN -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry # Do not edit/remove. Adding is ok. diff --git a/tests/components/broadlink/test_config_flow.py b/tests/components/broadlink/test_config_flow.py index c19d831cf3a..30f19c178b7 100644 --- a/tests/components/broadlink/test_config_flow.py +++ b/tests/components/broadlink/test_config_flow.py @@ -1,6 +1,7 @@ """Test the Broadlink config flow.""" import errno import socket +from unittest.mock import call, patch import broadlink.exceptions as blke import pytest @@ -10,8 +11,6 @@ from homeassistant.components.broadlink.const import DOMAIN from . import get_device -from tests.async_mock import call, patch - DEVICE_DISCOVERY = "homeassistant.components.broadlink.config_flow.blk.discover" DEVICE_FACTORY = "homeassistant.components.broadlink.config_flow.blk.gendevice" diff --git a/tests/components/broadlink/test_device.py b/tests/components/broadlink/test_device.py index d267243aeb9..a105ba26553 100644 --- a/tests/components/broadlink/test_device.py +++ b/tests/components/broadlink/test_device.py @@ -1,4 +1,6 @@ """Tests for Broadlink devices.""" +from unittest.mock import patch + import broadlink.exceptions as blke from homeassistant.components.broadlink.const import DOMAIN @@ -13,7 +15,6 @@ from homeassistant.helpers.entity_registry import async_entries_for_device from . import get_device -from tests.async_mock import patch from tests.common import mock_device_registry, mock_registry diff --git a/tests/components/broadlink/test_remote.py b/tests/components/broadlink/test_remote.py index 941dfc4d3ce..a1be8b364a3 100644 --- a/tests/components/broadlink/test_remote.py +++ b/tests/components/broadlink/test_remote.py @@ -1,5 +1,6 @@ """Tests for Broadlink remotes.""" from base64 import b64decode +from unittest.mock import call from homeassistant.components.broadlink.const import DOMAIN, REMOTE_DOMAIN from homeassistant.components.remote import ( @@ -12,7 +13,6 @@ from homeassistant.helpers.entity_registry import async_entries_for_device from . import get_device -from tests.async_mock import call from tests.common import mock_device_registry, mock_registry REMOTE_DEVICES = ["Entrance", "Living Room", "Office", "Garage"] diff --git a/tests/components/brother/__init__.py b/tests/components/brother/__init__.py index b24ef97705b..b4706d56fba 100644 --- a/tests/components/brother/__init__.py +++ b/tests/components/brother/__init__.py @@ -1,10 +1,10 @@ """Tests for Brother Printer integration.""" import json +from unittest.mock import patch from homeassistant.components.brother.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_TYPE -from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture diff --git a/tests/components/brother/test_config_flow.py b/tests/components/brother/test_config_flow.py index 153c07deeac..d681ac9c988 100644 --- a/tests/components/brother/test_config_flow.py +++ b/tests/components/brother/test_config_flow.py @@ -1,5 +1,6 @@ """Define tests for the Brother Printer config flow.""" import json +from unittest.mock import patch from brother import SnmpError, UnsupportedModel @@ -8,7 +9,6 @@ from homeassistant.components.brother.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_TYPE -from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture CONFIG = {CONF_HOST: "localhost", CONF_TYPE: "laser"} diff --git a/tests/components/brother/test_init.py b/tests/components/brother/test_init.py index 04c3c130fc9..7b85586ce28 100644 --- a/tests/components/brother/test_init.py +++ b/tests/components/brother/test_init.py @@ -1,4 +1,6 @@ """Test init of Brother integration.""" +from unittest.mock import patch + from homeassistant.components.brother.const import DOMAIN from homeassistant.config_entries import ( ENTRY_STATE_LOADED, @@ -7,7 +9,6 @@ from homeassistant.config_entries import ( ) from homeassistant.const import CONF_HOST, CONF_TYPE, STATE_UNAVAILABLE -from tests.async_mock import patch from tests.common import MockConfigEntry from tests.components.brother import init_integration diff --git a/tests/components/brother/test_sensor.py b/tests/components/brother/test_sensor.py index 3f9ee9394b7..b386b0753b7 100644 --- a/tests/components/brother/test_sensor.py +++ b/tests/components/brother/test_sensor.py @@ -1,6 +1,7 @@ """Test sensor of Brother integration.""" from datetime import datetime, timedelta import json +from unittest.mock import Mock, patch from homeassistant.components.brother.const import DOMAIN, UNIT_PAGES from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -16,7 +17,6 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component from homeassistant.util.dt import UTC, utcnow -from tests.async_mock import Mock, patch from tests.common import async_fire_time_changed, load_fixture from tests.components.brother import init_integration diff --git a/tests/components/caldav/test_calendar.py b/tests/components/caldav/test_calendar.py index 1b6c21c7358..3e380b44de4 100644 --- a/tests/components/caldav/test_calendar.py +++ b/tests/components/caldav/test_calendar.py @@ -1,5 +1,6 @@ """The tests for the webdav calendar component.""" import datetime +from unittest.mock import MagicMock, Mock, patch from caldav.objects import Event import pytest @@ -8,8 +9,6 @@ from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component from homeassistant.util import dt -from tests.async_mock import MagicMock, Mock, patch - # pylint: disable=redefined-outer-name DEVICE_DATA = {"name": "Private Calendar", "device_id": "Private Calendar"} diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 966adc97b67..2c2d744deb9 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -2,6 +2,7 @@ import asyncio import base64 import io +from unittest.mock import Mock, PropertyMock, mock_open, patch import pytest @@ -14,7 +15,6 @@ from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, PropertyMock, mock_open, patch from tests.components.camera import common diff --git a/tests/components/canary/__init__.py b/tests/components/canary/__init__.py index 9d0e488d516..27cec31b9e9 100644 --- a/tests/components/canary/__init__.py +++ b/tests/components/canary/__init__.py @@ -1,5 +1,5 @@ """Tests for the Canary integration.""" -from unittest.mock import MagicMock, PropertyMock +from unittest.mock import MagicMock, PropertyMock, patch from canary.api import SensorType @@ -12,7 +12,6 @@ from homeassistant.components.canary.const import ( from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from homeassistant.helpers.typing import HomeAssistantType -from tests.async_mock import patch from tests.common import MockConfigEntry ENTRY_CONFIG = { diff --git a/tests/components/canary/conftest.py b/tests/components/canary/conftest.py index ed0e7f80f8d..26db0dfefcf 100644 --- a/tests/components/canary/conftest.py +++ b/tests/components/canary/conftest.py @@ -1,9 +1,9 @@ """Define fixtures available for all tests.""" +from unittest.mock import MagicMock, patch + from canary.api import Api from pytest import fixture -from tests.async_mock import MagicMock, patch - @fixture(autouse=True) def mock_ffmpeg(hass): diff --git a/tests/components/canary/test_alarm_control_panel.py b/tests/components/canary/test_alarm_control_panel.py index 930fd9613e0..a21284ec376 100644 --- a/tests/components/canary/test_alarm_control_panel.py +++ b/tests/components/canary/test_alarm_control_panel.py @@ -1,4 +1,6 @@ """The tests for the Canary alarm_control_panel platform.""" +from unittest.mock import PropertyMock, patch + from canary.api import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, LOCATION_MODE_NIGHT from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN @@ -18,7 +20,6 @@ from homeassistant.setup import async_setup_component from . import mock_device, mock_location, mock_mode -from tests.async_mock import PropertyMock, patch from tests.common import mock_registry diff --git a/tests/components/canary/test_config_flow.py b/tests/components/canary/test_config_flow.py index e02217dc67e..d194ae21185 100644 --- a/tests/components/canary/test_config_flow.py +++ b/tests/components/canary/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Canary config flow.""" +from unittest.mock import patch + from requests import ConnectTimeout, HTTPError from homeassistant.components.canary.const import ( @@ -18,8 +20,6 @@ from homeassistant.setup import async_setup_component from . import USER_INPUT, _patch_async_setup, _patch_async_setup_entry, init_integration -from tests.async_mock import patch - async def test_user_form(hass, canary_config_flow): """Test we get the user initiated form.""" diff --git a/tests/components/canary/test_init.py b/tests/components/canary/test_init.py index f548a007505..a767eb0ec51 100644 --- a/tests/components/canary/test_init.py +++ b/tests/components/canary/test_init.py @@ -1,4 +1,6 @@ """The tests for the Canary component.""" +from unittest.mock import patch + from requests import ConnectTimeout from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN @@ -13,8 +15,6 @@ from homeassistant.setup import async_setup_component from . import YAML_CONFIG, init_integration -from tests.async_mock import patch - async def test_import_from_yaml(hass, canary) -> None: """Test import from YAML.""" diff --git a/tests/components/canary/test_sensor.py b/tests/components/canary/test_sensor.py index d32741d3705..3f44f047d1c 100644 --- a/tests/components/canary/test_sensor.py +++ b/tests/components/canary/test_sensor.py @@ -1,5 +1,6 @@ """The tests for the Canary sensor platform.""" from datetime import timedelta +from unittest.mock import patch from homeassistant.components.canary.const import DOMAIN, MANUFACTURER from homeassistant.components.canary.sensor import ( @@ -23,7 +24,6 @@ from homeassistant.util.dt import utcnow from . import mock_device, mock_location, mock_reading -from tests.async_mock import patch from tests.common import async_fire_time_changed, mock_device_registry, mock_registry diff --git a/tests/components/cast/test_home_assistant_cast.py b/tests/components/cast/test_home_assistant_cast.py index 8ddb6e82eda..3fd0e921ca6 100644 --- a/tests/components/cast/test_home_assistant_cast.py +++ b/tests/components/cast/test_home_assistant_cast.py @@ -1,10 +1,11 @@ """Test Home Assistant Cast.""" +from unittest.mock import patch + from homeassistant import config_entries from homeassistant.components.cast import home_assistant_cast from homeassistant.config import async_process_ha_core_config -from tests.async_mock import patch from tests.common import MockConfigEntry, async_mock_signal diff --git a/tests/components/cast/test_init.py b/tests/components/cast/test_init.py index 24be4d53ee6..d364256b703 100644 --- a/tests/components/cast/test_init.py +++ b/tests/components/cast/test_init.py @@ -1,11 +1,11 @@ """Tests for the Cast config flow.""" +from unittest.mock import patch + from homeassistant import config_entries, data_entry_flow from homeassistant.components import cast from homeassistant.setup import async_setup_component -from tests.async_mock import patch - async def test_creating_entry_sets_up_media_player(hass): """Test setting up Cast loads the media player.""" diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 4f75e93faef..c04dc87ad11 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -2,6 +2,7 @@ # pylint: disable=protected-access import json from typing import Optional +from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch from uuid import UUID import attr @@ -16,7 +17,6 @@ from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component -from tests.async_mock import ANY, AsyncMock, MagicMock, Mock, patch from tests.common import MockConfigEntry, assert_setup_component from tests.components.media_player import common diff --git a/tests/components/cert_expiry/test_config_flow.py b/tests/components/cert_expiry/test_config_flow.py index e5d90e12d13..ed51ebf70a4 100644 --- a/tests/components/cert_expiry/test_config_flow.py +++ b/tests/components/cert_expiry/test_config_flow.py @@ -1,6 +1,7 @@ """Tests for the Cert Expiry config flow.""" import socket import ssl +from unittest.mock import patch from homeassistant import data_entry_flow from homeassistant.components.cert_expiry.const import DEFAULT_PORT, DOMAIN @@ -9,7 +10,6 @@ from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from .const import HOST, PORT from .helpers import future_timestamp -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/cert_expiry/test_init.py b/tests/components/cert_expiry/test_init.py index 6aa8568a9d1..ea31ba50ea0 100644 --- a/tests/components/cert_expiry/test_init.py +++ b/tests/components/cert_expiry/test_init.py @@ -1,5 +1,6 @@ """Tests for Cert Expiry setup.""" from datetime import timedelta +from unittest.mock import patch from homeassistant.components.cert_expiry.const import DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -11,7 +12,6 @@ import homeassistant.util.dt as dt_util from .const import HOST, PORT from .helpers import future_timestamp, static_datetime -from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/cert_expiry/test_sensors.py b/tests/components/cert_expiry/test_sensors.py index 4a78f02b39c..375b676eaf8 100644 --- a/tests/components/cert_expiry/test_sensors.py +++ b/tests/components/cert_expiry/test_sensors.py @@ -2,6 +2,7 @@ from datetime import timedelta import socket import ssl +from unittest.mock import patch from homeassistant.components.cert_expiry.const import DOMAIN from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY @@ -11,7 +12,6 @@ from homeassistant.util.dt import utcnow from .const import HOST, PORT from .helpers import future_timestamp, static_datetime -from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/climate/test_init.py b/tests/components/climate/test_init.py index 89c12b4c517..8113c1e343a 100644 --- a/tests/components/climate/test_init.py +++ b/tests/components/climate/test_init.py @@ -1,5 +1,6 @@ """The tests for the climate component.""" from typing import List +from unittest.mock import MagicMock import pytest import voluptuous as vol @@ -12,7 +13,6 @@ from homeassistant.components.climate import ( ClimateEntity, ) -from tests.async_mock import MagicMock from tests.common import async_mock_service diff --git a/tests/components/cloud/__init__.py b/tests/components/cloud/__init__.py index da7c6ff13d0..8613c6408fe 100644 --- a/tests/components/cloud/__init__.py +++ b/tests/components/cloud/__init__.py @@ -1,11 +1,11 @@ """Tests for the cloud component.""" +from unittest.mock import AsyncMock, patch + from homeassistant.components import cloud from homeassistant.components.cloud import const from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock, patch - async def mock_cloud(hass, config=None): """Mock cloud.""" diff --git a/tests/components/cloud/conftest.py b/tests/components/cloud/conftest.py index 02d9b4c41aa..4755d470418 100644 --- a/tests/components/cloud/conftest.py +++ b/tests/components/cloud/conftest.py @@ -1,4 +1,6 @@ """Fixtures for cloud tests.""" +from unittest.mock import patch + import jwt import pytest @@ -6,8 +8,6 @@ from homeassistant.components.cloud import const, prefs from . import mock_cloud, mock_cloud_prefs -from tests.async_mock import patch - @pytest.fixture(autouse=True) def mock_user_data(): diff --git a/tests/components/cloud/test_account_link.py b/tests/components/cloud/test_account_link.py index 1580969b0a5..62225597939 100644 --- a/tests/components/cloud/test_account_link.py +++ b/tests/components/cloud/test_account_link.py @@ -2,6 +2,7 @@ import asyncio import logging from time import time +from unittest.mock import AsyncMock, Mock, patch import pytest @@ -10,7 +11,6 @@ from homeassistant.components.cloud import account_link from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.util.dt import utcnow -from tests.async_mock import AsyncMock, Mock, patch from tests.common import async_fire_time_changed, mock_platform TEST_DOMAIN = "oauth2_test" diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index e54a5dcde01..7286ece2c53 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -1,11 +1,11 @@ """Test Alexa config.""" import contextlib +from unittest.mock import AsyncMock, Mock, patch from homeassistant.components.cloud import ALEXA_SCHEMA, alexa_config from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED from homeassistant.util.dt import utcnow -from tests.async_mock import AsyncMock, Mock, patch from tests.common import async_fire_time_changed diff --git a/tests/components/cloud/test_binary_sensor.py b/tests/components/cloud/test_binary_sensor.py index 6a2d76dc403..c9c9d53981e 100644 --- a/tests/components/cloud/test_binary_sensor.py +++ b/tests/components/cloud/test_binary_sensor.py @@ -1,9 +1,9 @@ """Tests for the cloud binary sensor.""" +from unittest.mock import Mock, patch + from homeassistant.components.cloud.const import DISPATCHER_REMOTE_UPDATE from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, patch - async def test_remote_connection_sensor(hass): """Test the remote connection sensor.""" diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index 752394b5d0f..dfea8f80cee 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -1,5 +1,6 @@ """Test the cloud.iot module.""" from datetime import timedelta +from unittest.mock import AsyncMock, MagicMock, Mock, patch import aiohttp from aiohttp import web @@ -15,7 +16,6 @@ from homeassistant.util import dt as dt_util from . import mock_cloud, mock_cloud_prefs -from tests.async_mock import AsyncMock, MagicMock, Mock, patch from tests.common import async_fire_time_changed from tests.components.alexa import test_smart_home as test_alexa diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index 78207605830..f58ea1a415b 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -1,4 +1,6 @@ """Test the Cloud Google Config.""" +from unittest.mock import AsyncMock, Mock, patch + import pytest from homeassistant.components.cloud import GACTIONS_SCHEMA @@ -9,7 +11,6 @@ from homeassistant.core import CoreState, State from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED from homeassistant.util.dt import utcnow -from tests.async_mock import AsyncMock, Mock, patch from tests.common import async_fire_time_changed diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index a3c33b31ebb..047a69184ba 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -1,6 +1,7 @@ """Tests for the HTTP API for the cloud component.""" import asyncio from ipaddress import ip_network +from unittest.mock import AsyncMock, MagicMock, Mock, patch import aiohttp from hass_nabucasa import thingtalk @@ -19,7 +20,6 @@ from homeassistant.core import State from . import mock_cloud, mock_cloud_prefs -from tests.async_mock import AsyncMock, MagicMock, Mock, patch from tests.components.google_assistant import MockConfig SUBSCRIPTION_INFO_URL = "https://api-test.hass.io/subscription_info" diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index e174b080102..7202c8a0b39 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -1,5 +1,7 @@ """Test the cloud component.""" +from unittest.mock import patch + import pytest from homeassistant.components import cloud @@ -10,8 +12,6 @@ from homeassistant.core import Context from homeassistant.exceptions import Unauthorized from homeassistant.setup import async_setup_component -from tests.async_mock import patch - async def test_constructor_loads_info_from_config(hass): """Test non-dev mode loads info from SERVERS constant.""" diff --git a/tests/components/cloud/test_prefs.py b/tests/components/cloud/test_prefs.py index 4f2d5d6d661..d1b6f9ed867 100644 --- a/tests/components/cloud/test_prefs.py +++ b/tests/components/cloud/test_prefs.py @@ -1,9 +1,9 @@ """Test Cloud preferences.""" +from unittest.mock import patch + from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components.cloud.prefs import STORAGE_KEY, CloudPreferences -from tests.async_mock import patch - async def test_set_username(hass): """Test we clear config if we set different username.""" diff --git a/tests/components/cloud/test_system_health.py b/tests/components/cloud/test_system_health.py index b69ab462ddb..65ffd859f33 100644 --- a/tests/components/cloud/test_system_health.py +++ b/tests/components/cloud/test_system_health.py @@ -1,12 +1,12 @@ """Test cloud system health.""" import asyncio +from unittest.mock import Mock from aiohttp import ClientError from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from tests.async_mock import Mock from tests.common import get_system_health_info diff --git a/tests/components/cloudflare/__init__.py b/tests/components/cloudflare/__init__.py index 68eed3afd87..c72a9cd84b0 100644 --- a/tests/components/cloudflare/__init__.py +++ b/tests/components/cloudflare/__init__.py @@ -1,12 +1,12 @@ """Tests for the Cloudflare integration.""" from typing import List +from unittest.mock import AsyncMock, patch from pycfdns import CFRecord from homeassistant.components.cloudflare.const import CONF_RECORDS, DOMAIN from homeassistant.const import CONF_API_TOKEN, CONF_ZONE -from tests.async_mock import AsyncMock, patch from tests.common import MockConfigEntry ENTRY_CONFIG = { diff --git a/tests/components/cloudflare/conftest.py b/tests/components/cloudflare/conftest.py index 1ee381de104..99ca7af26f4 100644 --- a/tests/components/cloudflare/conftest.py +++ b/tests/components/cloudflare/conftest.py @@ -1,10 +1,10 @@ """Define fixtures available for all tests.""" +from unittest.mock import patch + from pytest import fixture from . import _get_mock_cfupdate -from tests.async_mock import patch - @fixture def cfupdate(hass): diff --git a/tests/components/coinmarketcap/test_sensor.py b/tests/components/coinmarketcap/test_sensor.py index 30fc0f0e1f7..369a006f568 100644 --- a/tests/components/coinmarketcap/test_sensor.py +++ b/tests/components/coinmarketcap/test_sensor.py @@ -1,12 +1,12 @@ """Tests for the CoinMarketCap sensor platform.""" import json +from unittest.mock import patch import pytest from homeassistant.components.sensor import DOMAIN from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import assert_setup_component, load_fixture VALID_CONFIG = { diff --git a/tests/components/color_extractor/test_service.py b/tests/components/color_extractor/test_service.py index 31b623f1c76..2a1592fe73f 100644 --- a/tests/components/color_extractor/test_service.py +++ b/tests/components/color_extractor/test_service.py @@ -1,6 +1,7 @@ """Tests for color_extractor component service calls.""" import base64 import io +from unittest.mock import Mock, mock_open, patch import aiohttp import pytest @@ -23,7 +24,6 @@ from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component import homeassistant.util.color as color_util -from tests.async_mock import Mock, mock_open, patch from tests.common import load_fixture LIGHT_ENTITY = "light.kitchen_lights" diff --git a/tests/components/command_line/test_cover.py b/tests/components/command_line/test_cover.py index 508b1e1fb41..ee692413bcd 100644 --- a/tests/components/command_line/test_cover.py +++ b/tests/components/command_line/test_cover.py @@ -3,6 +3,7 @@ import os from os import path import tempfile from unittest import mock +from unittest.mock import patch import pytest @@ -18,8 +19,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import patch - @pytest.fixture def rs(hass): diff --git a/tests/components/command_line/test_notify.py b/tests/components/command_line/test_notify.py index 8509bc785da..3dcb521cfd2 100644 --- a/tests/components/command_line/test_notify.py +++ b/tests/components/command_line/test_notify.py @@ -2,11 +2,11 @@ import os import tempfile import unittest +from unittest.mock import patch import homeassistant.components.notify as notify from homeassistant.setup import async_setup_component, setup_component -from tests.async_mock import patch from tests.common import assert_setup_component, get_test_home_assistant diff --git a/tests/components/command_line/test_sensor.py b/tests/components/command_line/test_sensor.py index 0a6e29ca00c..042c9acf432 100644 --- a/tests/components/command_line/test_sensor.py +++ b/tests/components/command_line/test_sensor.py @@ -1,10 +1,10 @@ """The tests for the Command line sensor platform.""" import unittest +from unittest.mock import patch from homeassistant.components.command_line import sensor as command_line from homeassistant.helpers.template import Template -from tests.async_mock import patch from tests.common import get_test_home_assistant diff --git a/tests/components/config/test_automation.py b/tests/components/config/test_automation.py index 347ac96f892..00c89edeef0 100644 --- a/tests/components/config/test_automation.py +++ b/tests/components/config/test_automation.py @@ -1,10 +1,10 @@ """Test Automation config panel.""" import json +from unittest.mock import patch 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 diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 11299f4108b..6873bc8311a 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -1,6 +1,7 @@ """Test config entries API.""" from collections import OrderedDict +from unittest.mock import AsyncMock, patch import pytest import voluptuous as vol @@ -12,7 +13,6 @@ from homeassistant.core import callback from homeassistant.generated import config_flows from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock, patch from tests.common import ( MockConfigEntry, MockModule, diff --git a/tests/components/config/test_core.py b/tests/components/config/test_core.py index 72e655dbb66..361fceab565 100644 --- a/tests/components/config/test_core.py +++ b/tests/components/config/test_core.py @@ -1,4 +1,6 @@ """Test hassbian config.""" +from unittest.mock import patch + import pytest from homeassistant.bootstrap import async_setup_component @@ -7,8 +9,6 @@ from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.const import CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL from homeassistant.util import dt as dt_util, location -from tests.async_mock import patch - ORIG_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE diff --git a/tests/components/config/test_customize.py b/tests/components/config/test_customize.py index ca3bcc98c7d..aac18bc379e 100644 --- a/tests/components/config/test_customize.py +++ b/tests/components/config/test_customize.py @@ -1,12 +1,11 @@ """Test Customize config panel.""" import json +from unittest.mock import patch from homeassistant.bootstrap import async_setup_component from homeassistant.components import config from homeassistant.config import DATA_CUSTOMIZE -from tests.async_mock import patch - async def test_get_entity(hass, hass_client): """Test getting entity.""" diff --git a/tests/components/config/test_group.py b/tests/components/config/test_group.py index 98ad2041713..c4b7cf25800 100644 --- a/tests/components/config/test_group.py +++ b/tests/components/config/test_group.py @@ -1,11 +1,10 @@ """Test Group config panel.""" import json +from unittest.mock import AsyncMock, patch from homeassistant.bootstrap import async_setup_component from homeassistant.components import config -from tests.async_mock import AsyncMock, patch - VIEW_NAME = "api:config:group:config" diff --git a/tests/components/config/test_init.py b/tests/components/config/test_init.py index 6dd16fef7ec..dd3e294bac3 100644 --- a/tests/components/config/test_init.py +++ b/tests/components/config/test_init.py @@ -1,10 +1,11 @@ """Test config init.""" +from unittest.mock import patch + from homeassistant.components import config from homeassistant.const import EVENT_COMPONENT_LOADED from homeassistant.setup import ATTR_COMPONENT, async_setup_component -from tests.async_mock import patch from tests.common import mock_component diff --git a/tests/components/config/test_scene.py b/tests/components/config/test_scene.py index dcaa950f342..bdb2a2e3f10 100644 --- a/tests/components/config/test_scene.py +++ b/tests/components/config/test_scene.py @@ -1,12 +1,11 @@ """Test Automation config panel.""" import json +from unittest.mock import patch from homeassistant.bootstrap import async_setup_component from homeassistant.components import config from homeassistant.util.yaml import dump -from tests.async_mock import patch - async def test_update_scene(hass, hass_client): """Test updating a scene.""" diff --git a/tests/components/config/test_script.py b/tests/components/config/test_script.py index 4dc906e92f3..0026729766c 100644 --- a/tests/components/config/test_script.py +++ b/tests/components/config/test_script.py @@ -1,9 +1,9 @@ """Tests for config/script.""" +from unittest.mock import patch + from homeassistant.bootstrap import async_setup_component from homeassistant.components import config -from tests.async_mock import patch - async def test_delete_script(hass, hass_client): """Test deleting a script.""" diff --git a/tests/components/config/test_zwave.py b/tests/components/config/test_zwave.py index 8b83583d1f5..2f15a167c92 100644 --- a/tests/components/config/test_zwave.py +++ b/tests/components/config/test_zwave.py @@ -1,5 +1,6 @@ """Test Z-Wave config panel.""" import json +from unittest.mock import MagicMock, patch import pytest @@ -8,7 +9,6 @@ from homeassistant.components import config from homeassistant.components.zwave import DATA_NETWORK, const from homeassistant.const import HTTP_NOT_FOUND -from tests.async_mock import MagicMock, patch from tests.mock.zwave import MockEntityValues, MockNode, MockValue VIEW_NAME = "api:config:zwave:device_config" diff --git a/tests/components/conftest.py b/tests/components/conftest.py index acfbdeb8629..3b1781ba510 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -1,10 +1,10 @@ """Fixtures for component testing.""" +from unittest.mock import patch + import pytest from homeassistant.components import zeroconf -from tests.async_mock import patch - zeroconf.orig_install_multiple_zeroconf_catcher = ( zeroconf.install_multiple_zeroconf_catcher ) diff --git a/tests/components/control4/test_config_flow.py b/tests/components/control4/test_config_flow.py index 8c68039920d..48ff8201166 100644 --- a/tests/components/control4/test_config_flow.py +++ b/tests/components/control4/test_config_flow.py @@ -1,5 +1,6 @@ """Test the Control4 config flow.""" import datetime +from unittest.mock import AsyncMock, patch from pyControl4.account import C4Account from pyControl4.director import C4Director @@ -14,7 +15,6 @@ from homeassistant.const import ( CONF_USERNAME, ) -from tests.async_mock import AsyncMock, patch from tests.common import MockConfigEntry diff --git a/tests/components/coolmaster/test_config_flow.py b/tests/components/coolmaster/test_config_flow.py index 27e44949585..3dd0f27ecdf 100644 --- a/tests/components/coolmaster/test_config_flow.py +++ b/tests/components/coolmaster/test_config_flow.py @@ -1,9 +1,9 @@ """Test the Coolmaster config flow.""" +from unittest.mock import patch + from homeassistant import config_entries from homeassistant.components.coolmaster.const import AVAILABLE_MODES, DOMAIN -from tests.async_mock import patch - def _flow_data(): options = {"host": "1.1.1.1"} diff --git a/tests/components/coronavirus/conftest.py b/tests/components/coronavirus/conftest.py index bbe5a463802..57128268fd7 100644 --- a/tests/components/coronavirus/conftest.py +++ b/tests/components/coronavirus/conftest.py @@ -1,8 +1,8 @@ """Test helpers.""" -import pytest +from unittest.mock import Mock, patch -from tests.async_mock import Mock, patch +import pytest @pytest.fixture(autouse=True) diff --git a/tests/components/daikin/test_config_flow.py b/tests/components/daikin/test_config_flow.py index 3e4ddb6d3fd..a7165b2cb9b 100644 --- a/tests/components/daikin/test_config_flow.py +++ b/tests/components/daikin/test_config_flow.py @@ -1,6 +1,7 @@ # pylint: disable=redefined-outer-name """Tests for the Daikin config flow.""" import asyncio +from unittest.mock import PropertyMock, patch from aiohttp import ClientError from aiohttp.web_exceptions import HTTPForbidden @@ -20,7 +21,6 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_FORM, ) -from tests.async_mock import PropertyMock, patch from tests.common import MockConfigEntry MAC = "AABBCCDDEEFF" diff --git a/tests/components/darksky/test_sensor.py b/tests/components/darksky/test_sensor.py index be1e9849452..02bc392cd68 100644 --- a/tests/components/darksky/test_sensor.py +++ b/tests/components/darksky/test_sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import re import unittest +from unittest.mock import MagicMock, patch import forecastio from requests.exceptions import HTTPError @@ -10,7 +11,6 @@ import requests_mock from homeassistant.components.darksky import sensor as darksky from homeassistant.setup import setup_component -from tests.async_mock import MagicMock, patch from tests.common import get_test_home_assistant, load_fixture VALID_CONFIG_MINIMAL = { diff --git a/tests/components/darksky/test_weather.py b/tests/components/darksky/test_weather.py index 1a2a2e156d9..9a1b3912b87 100644 --- a/tests/components/darksky/test_weather.py +++ b/tests/components/darksky/test_weather.py @@ -1,6 +1,7 @@ """The tests for the Dark Sky weather component.""" import re import unittest +from unittest.mock import patch import forecastio from requests.exceptions import ConnectionError @@ -10,7 +11,6 @@ from homeassistant.components import weather from homeassistant.setup import setup_component from homeassistant.util.unit_system import METRIC_SYSTEM -from tests.async_mock import patch from tests.common import get_test_home_assistant, load_fixture diff --git a/tests/components/datadog/test_init.py b/tests/components/datadog/test_init.py index f46ad027442..087e0f4b884 100644 --- a/tests/components/datadog/test_init.py +++ b/tests/components/datadog/test_init.py @@ -1,5 +1,6 @@ """The tests for the Datadog component.""" from unittest import mock +from unittest.mock import MagicMock, patch import homeassistant.components.datadog as datadog from homeassistant.const import ( @@ -11,7 +12,6 @@ from homeassistant.const import ( import homeassistant.core as ha from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, patch from tests.common import assert_setup_component diff --git a/tests/components/debugpy/test_init.py b/tests/components/debugpy/test_init.py index 86be0d788f6..97d08e13bfc 100644 --- a/tests/components/debugpy/test_init.py +++ b/tests/components/debugpy/test_init.py @@ -1,4 +1,6 @@ """Tests for the Remote Python Debugger integration.""" +from unittest.mock import patch + import pytest from homeassistant.components.debugpy import ( @@ -12,8 +14,6 @@ from homeassistant.components.debugpy import ( from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.async_mock import patch - @pytest.fixture def mock_debugpy(): diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 78a4f1e937d..3611e30f665 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -1,6 +1,7 @@ """deCONZ binary sensor platform tests.""" from copy import deepcopy +from unittest.mock import patch from homeassistant.components.binary_sensor import ( DEVICE_CLASS_MOTION, @@ -21,8 +22,6 @@ 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", diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index 319675cf6f7..4d68ba2a6a7 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -1,6 +1,7 @@ """deCONZ climate platform tests.""" from copy import deepcopy +from unittest.mock import patch import pytest @@ -43,8 +44,6 @@ 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": "Thermostat id", diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index d922dffb623..6a8066e98fb 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -1,6 +1,7 @@ """Tests for deCONZ config flow.""" import asyncio +from unittest.mock import patch import pydeconz @@ -31,8 +32,6 @@ from homeassistant.data_entry_flow import ( from .test_gateway import API_KEY, BRIDGEID, setup_deconz_integration -from tests.async_mock import patch - BAD_BRIDGEID = "0000000000000000" diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index 374d3683a6e..5314a41b315 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -1,6 +1,7 @@ """deCONZ cover platform tests.""" from copy import deepcopy +from unittest.mock import patch from homeassistant.components.cover import ( ATTR_CURRENT_TILT_POSITION, @@ -23,8 +24,6 @@ from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration -from tests.async_mock import patch - COVERS = { "1": { "id": "Level controllable cover id", diff --git a/tests/components/deconz/test_fan.py b/tests/components/deconz/test_fan.py index 6806071dd75..b9c154a2791 100644 --- a/tests/components/deconz/test_fan.py +++ b/tests/components/deconz/test_fan.py @@ -1,6 +1,7 @@ """deCONZ fan platform tests.""" from copy import deepcopy +from unittest.mock import patch import pytest @@ -22,8 +23,6 @@ from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration -from tests.async_mock import patch - FANS = { "1": { "etag": "432f3de28965052961a99e3c5494daf4", diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index 12666cdb692..023d4d32da5 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -1,6 +1,7 @@ """Test deCONZ gateway.""" from copy import deepcopy +from unittest.mock import Mock, patch import pydeconz import pytest @@ -31,7 +32,6 @@ from homeassistant.config_entries import CONN_CLASS_LOCAL_PUSH, SOURCE_SSDP from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.helpers.dispatcher import async_dispatcher_connect -from tests.async_mock import Mock, patch from tests.common import MockConfigEntry API_KEY = "1234567890ABCDEF" diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index ae7ce5b2a39..d408d764d0e 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -2,6 +2,7 @@ import asyncio from copy import deepcopy +from unittest.mock import patch from homeassistant.components.deconz import ( DeconzGateway, @@ -13,8 +14,6 @@ from homeassistant.components.deconz.gateway import get_gateway_from_config_entr from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration -from tests.async_mock import patch - ENTRY1_HOST = "1.2.3.4" ENTRY1_PORT = 80 ENTRY1_API_KEY = "1234567890ABCDEF" diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 18a135a5e05..28d9b4e33fb 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -1,6 +1,7 @@ """deCONZ light platform tests.""" from copy import deepcopy +from unittest.mock import patch from homeassistant.components.deconz.const import ( CONF_ALLOW_DECONZ_GROUPS, @@ -33,8 +34,6 @@ from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration -from tests.async_mock import patch - GROUPS = { "1": { "id": "Light group id", diff --git a/tests/components/deconz/test_lock.py b/tests/components/deconz/test_lock.py index 2acfb440b13..7e9b8233778 100644 --- a/tests/components/deconz/test_lock.py +++ b/tests/components/deconz/test_lock.py @@ -1,6 +1,7 @@ """deCONZ lock platform tests.""" from copy import deepcopy +from unittest.mock import patch from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN from homeassistant.components.deconz.gateway import get_gateway_from_config_entry @@ -14,8 +15,6 @@ from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration -from tests.async_mock import patch - LOCKS = { "1": { "etag": "5c2ec06cde4bd654aef3a555fcd8ad12", diff --git a/tests/components/deconz/test_scene.py b/tests/components/deconz/test_scene.py index c4a92538815..ca8df2c0425 100644 --- a/tests/components/deconz/test_scene.py +++ b/tests/components/deconz/test_scene.py @@ -1,6 +1,7 @@ """deCONZ scene platform tests.""" from copy import deepcopy +from unittest.mock import patch from homeassistant.components.deconz import DOMAIN as DECONZ_DOMAIN from homeassistant.components.deconz.gateway import get_gateway_from_config_entry @@ -10,8 +11,6 @@ from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration -from tests.async_mock import patch - GROUPS = { "1": { "id": "Light group id", diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py index 26524553dbe..faa1d3485bb 100644 --- a/tests/components/deconz/test_services.py +++ b/tests/components/deconz/test_services.py @@ -1,6 +1,7 @@ """deCONZ service tests.""" from copy import deepcopy +from unittest.mock import Mock, patch import pytest import voluptuous as vol @@ -26,8 +27,6 @@ from homeassistant.helpers.entity_registry import async_entries_for_config_entry from .test_gateway import BRIDGEID, DECONZ_WEB_REQUEST, setup_deconz_integration -from tests.async_mock import Mock, patch - GROUP = { "1": { "id": "Group 1 id", diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py index 4f23be3d1e3..e42e89d903e 100644 --- a/tests/components/deconz/test_switch.py +++ b/tests/components/deconz/test_switch.py @@ -1,6 +1,7 @@ """deCONZ switch platform tests.""" from copy import deepcopy +from unittest.mock import patch from homeassistant.components.deconz import DOMAIN as DECONZ_DOMAIN from homeassistant.components.deconz.gateway import get_gateway_from_config_entry @@ -14,8 +15,6 @@ from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration -from tests.async_mock import patch - POWER_PLUGS = { "1": { "id": "On off switch id", diff --git a/tests/components/default_config/test_init.py b/tests/components/default_config/test_init.py index a4a5898982b..9830d944471 100644 --- a/tests/components/default_config/test_init.py +++ b/tests/components/default_config/test_init.py @@ -1,9 +1,10 @@ """Test the default_config init.""" +from unittest.mock import patch + 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 diff --git a/tests/components/demo/test_camera.py b/tests/components/demo/test_camera.py index e62d6db0464..8e73a0beff6 100644 --- a/tests/components/demo/test_camera.py +++ b/tests/components/demo/test_camera.py @@ -1,4 +1,6 @@ """The tests for local file camera component.""" +from unittest.mock import patch + import pytest from homeassistant.components.camera import ( @@ -16,8 +18,6 @@ from homeassistant.const import ATTR_ENTITY_ID from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component -from tests.async_mock import patch - ENTITY_CAMERA = "camera.demo_camera" diff --git a/tests/components/demo/test_geo_location.py b/tests/components/demo/test_geo_location.py index 564d27e7131..c3c233c39be 100644 --- a/tests/components/demo/test_geo_location.py +++ b/tests/components/demo/test_geo_location.py @@ -1,5 +1,7 @@ """The tests for the demo platform.""" +from unittest.mock import patch + import pytest from homeassistant.components import geo_location @@ -16,7 +18,6 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import assert_setup_component, async_fire_time_changed CONFIG = {geo_location.DOMAIN: [{"platform": "demo"}]} diff --git a/tests/components/demo/test_media_player.py b/tests/components/demo/test_media_player.py index 340416f4586..a32a99bbc63 100644 --- a/tests/components/demo/test_media_player.py +++ b/tests/components/demo/test_media_player.py @@ -1,4 +1,6 @@ """The tests for the Demo Media player platform.""" +from unittest.mock import patch + import pytest import voluptuous as vol @@ -14,8 +16,6 @@ from homeassistant.const import ( from homeassistant.helpers.aiohttp_client import DATA_CLIENTSESSION from homeassistant.setup import async_setup_component -from tests.async_mock import patch - TEST_ENTITY_ID = "media_player.walkman" diff --git a/tests/components/demo/test_notify.py b/tests/components/demo/test_notify.py index 893b9d57e65..7c7f83312dd 100644 --- a/tests/components/demo/test_notify.py +++ b/tests/components/demo/test_notify.py @@ -1,6 +1,7 @@ """The tests for the notify demo platform.""" import logging +from unittest.mock import patch import pytest import voluptuous as vol @@ -11,7 +12,6 @@ from homeassistant.core import callback from homeassistant.helpers import discovery from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import assert_setup_component CONFIG = {notify.DOMAIN: {"platform": "demo"}} diff --git a/tests/components/denonavr/test_config_flow.py b/tests/components/denonavr/test_config_flow.py index b8ea6016131..67d1a4e10db 100644 --- a/tests/components/denonavr/test_config_flow.py +++ b/tests/components/denonavr/test_config_flow.py @@ -1,4 +1,6 @@ """Test the DenonAVR config flow.""" +from unittest.mock import patch + import pytest from homeassistant import config_entries, data_entry_flow @@ -15,7 +17,6 @@ from homeassistant.components.denonavr.config_flow import ( ) from homeassistant.const import CONF_HOST, CONF_MAC -from tests.async_mock import patch from tests.common import MockConfigEntry TEST_HOST = "1.2.3.4" diff --git a/tests/components/derivative/test_sensor.py b/tests/components/derivative/test_sensor.py index 58090f1587d..03861a30c47 100644 --- a/tests/components/derivative/test_sensor.py +++ b/tests/components/derivative/test_sensor.py @@ -1,12 +1,11 @@ """The tests for the derivative sensor platform.""" from datetime import timedelta +from unittest.mock import patch from homeassistant.const import POWER_WATT, TIME_HOURS, TIME_MINUTES, TIME_SECONDS from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch - async def test_state(hass): """Test derivative sensor state.""" diff --git a/tests/components/device_sun_light_trigger/test_init.py b/tests/components/device_sun_light_trigger/test_init.py index 4879be9d18c..7d478e7d8d1 100644 --- a/tests/components/device_sun_light_trigger/test_init.py +++ b/tests/components/device_sun_light_trigger/test_init.py @@ -1,6 +1,7 @@ """The tests device sun light trigger component.""" # pylint: disable=protected-access from datetime import datetime +from unittest.mock import patch import pytest @@ -24,7 +25,6 @@ from homeassistant.core import CoreState from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from tests.async_mock import patch from tests.common import async_fire_time_changed diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 9424ed229b5..c7aba405ccd 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -3,6 +3,7 @@ from datetime import datetime, timedelta import json import logging import os +from unittest.mock import Mock, call, patch import pytest @@ -27,7 +28,6 @@ from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import Mock, call, patch from tests.common import ( assert_setup_component, async_fire_time_changed, diff --git a/tests/components/devolo_home_control/test_config_flow.py b/tests/components/devolo_home_control/test_config_flow.py index 060188d65aa..dd856d2e6b5 100644 --- a/tests/components/devolo_home_control/test_config_flow.py +++ b/tests/components/devolo_home_control/test_config_flow.py @@ -1,9 +1,10 @@ """Test the devolo_home_control config flow.""" +from unittest.mock import patch + from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.devolo_home_control.const import DOMAIN from homeassistant.config_entries import SOURCE_USER -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/dexcom/__init__.py b/tests/components/dexcom/__init__.py index 47f6238c7c1..16c6f4b4d45 100644 --- a/tests/components/dexcom/__init__.py +++ b/tests/components/dexcom/__init__.py @@ -1,13 +1,13 @@ """Tests for the Dexcom integration.""" import json +from unittest.mock import patch from pydexcom import GlucoseReading from homeassistant.components.dexcom.const import CONF_SERVER, DOMAIN, SERVER_US from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture CONFIG = { diff --git a/tests/components/dexcom/test_config_flow.py b/tests/components/dexcom/test_config_flow.py index c79e7ca0075..2e1dfbcdee5 100644 --- a/tests/components/dexcom/test_config_flow.py +++ b/tests/components/dexcom/test_config_flow.py @@ -1,11 +1,12 @@ """Test the Dexcom config flow.""" +from unittest.mock import patch + from pydexcom import AccountError, SessionError from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.dexcom.const import DOMAIN, MG_DL, MMOL_L from homeassistant.const import CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME -from tests.async_mock import patch from tests.common import MockConfigEntry from tests.components.dexcom import CONFIG diff --git a/tests/components/dexcom/test_init.py b/tests/components/dexcom/test_init.py index 2cb3ad3bf79..a155450bf26 100644 --- a/tests/components/dexcom/test_init.py +++ b/tests/components/dexcom/test_init.py @@ -1,10 +1,11 @@ """Test the Dexcom config flow.""" +from unittest.mock import patch + from pydexcom import AccountError, SessionError from homeassistant.components.dexcom.const import DOMAIN from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED -from tests.async_mock import patch from tests.common import MockConfigEntry from tests.components.dexcom import CONFIG, init_integration diff --git a/tests/components/dexcom/test_sensor.py b/tests/components/dexcom/test_sensor.py index c9e00398140..15de72e9c95 100644 --- a/tests/components/dexcom/test_sensor.py +++ b/tests/components/dexcom/test_sensor.py @@ -1,5 +1,7 @@ """The sensor tests for the griddy platform.""" +from unittest.mock import patch + from pydexcom import SessionError from homeassistant.components.dexcom.const import MMOL_L @@ -9,7 +11,6 @@ from homeassistant.const import ( STATE_UNKNOWN, ) -from tests.async_mock import patch from tests.components.dexcom import GLUCOSE_READING, init_integration diff --git a/tests/components/directv/test_config_flow.py b/tests/components/directv/test_config_flow.py index 09b76cfb550..3372d48aadb 100644 --- a/tests/components/directv/test_config_flow.py +++ b/tests/components/directv/test_config_flow.py @@ -1,4 +1,6 @@ """Test the DirecTV config flow.""" +from unittest.mock import patch + from aiohttp import ClientError as HTTPClientError from homeassistant.components.directv.const import CONF_RECEIVER_ID, DOMAIN @@ -12,7 +14,6 @@ from homeassistant.data_entry_flow import ( ) from homeassistant.helpers.typing import HomeAssistantType -from tests.async_mock import patch from tests.components.directv import ( HOST, MOCK_SSDP_DISCOVERY_INFO, diff --git a/tests/components/directv/test_media_player.py b/tests/components/directv/test_media_player.py index 11aaad707b7..506cf62a44d 100644 --- a/tests/components/directv/test_media_player.py +++ b/tests/components/directv/test_media_player.py @@ -1,6 +1,7 @@ """The tests for the DirecTV Media player platform.""" from datetime import datetime, timedelta from typing import Optional +from unittest.mock import patch from pytest import fixture @@ -55,7 +56,6 @@ from homeassistant.const import ( from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import dt as dt_util -from tests.async_mock import patch from tests.components.directv import setup_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/directv/test_remote.py b/tests/components/directv/test_remote.py index b00f62c0e0c..33521958747 100644 --- a/tests/components/directv/test_remote.py +++ b/tests/components/directv/test_remote.py @@ -1,4 +1,6 @@ """The tests for the DirecTV remote platform.""" +from unittest.mock import patch + from homeassistant.components.remote import ( ATTR_COMMAND, DOMAIN as REMOTE_DOMAIN, @@ -7,7 +9,6 @@ from homeassistant.components.remote import ( from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.helpers.typing import HomeAssistantType -from tests.async_mock import patch from tests.components.directv import setup_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/discovery/test_init.py b/tests/components/discovery/test_init.py index 8003f83d996..fd66e59ef21 100644 --- a/tests/components/discovery/test_init.py +++ b/tests/components/discovery/test_init.py @@ -1,5 +1,5 @@ """The tests for the discovery component.""" -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch import pytest @@ -9,7 +9,6 @@ from homeassistant.components import discovery from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.util.dt import utcnow -from tests.async_mock import patch from tests.common import async_fire_time_changed, mock_coro # One might consider to "mock" services, but it's easy enough to just use diff --git a/tests/components/doorbird/test_config_flow.py b/tests/components/doorbird/test_config_flow.py index c52388b886c..e94f73239f1 100644 --- a/tests/components/doorbird/test_config_flow.py +++ b/tests/components/doorbird/test_config_flow.py @@ -1,4 +1,5 @@ """Test the DoorBird config flow.""" +from unittest.mock import MagicMock, patch import urllib from homeassistant import config_entries, data_entry_flow, setup @@ -6,7 +7,6 @@ from homeassistant.components.doorbird import CONF_CUSTOM_URL, CONF_TOKEN from homeassistant.components.doorbird.const import CONF_EVENTS, DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry, init_recorder_component VALID_CONFIG = { diff --git a/tests/components/dsmr/conftest.py b/tests/components/dsmr/conftest.py index d57828fdfa4..ab7b3a4d479 100644 --- a/tests/components/dsmr/conftest.py +++ b/tests/components/dsmr/conftest.py @@ -1,5 +1,6 @@ """Common test tools.""" import asyncio +from unittest.mock import MagicMock, patch from dsmr_parser.clients.protocol import DSMRProtocol from dsmr_parser.obis_references import ( @@ -10,8 +11,6 @@ from dsmr_parser.obis_references import ( from dsmr_parser.objects import CosemObject import pytest -from tests.async_mock import MagicMock, patch - @pytest.fixture async def dsmr_connection_fixture(hass): diff --git a/tests/components/dsmr/test_config_flow.py b/tests/components/dsmr/test_config_flow.py index 9ae49419bf4..edb3810e24f 100644 --- a/tests/components/dsmr/test_config_flow.py +++ b/tests/components/dsmr/test_config_flow.py @@ -1,13 +1,13 @@ """Test the DSMR config flow.""" import asyncio from itertools import chain, repeat +from unittest.mock import DEFAULT, AsyncMock, patch import serial from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.dsmr import DOMAIN -from tests.async_mock import DEFAULT, AsyncMock, patch from tests.common import MockConfigEntry SERIAL_DATA = {"serial_id": "12345678", "serial_id_gas": "123456789"} diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 76a9a5bb070..dde66c6bfb7 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -9,6 +9,7 @@ import asyncio import datetime from decimal import Decimal from itertools import chain, repeat +from unittest.mock import DEFAULT, MagicMock from homeassistant.components.dsmr.const import DOMAIN from homeassistant.components.dsmr.sensor import DerivativeDSMREntity @@ -20,7 +21,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import DEFAULT, MagicMock from tests.common import MockConfigEntry, patch diff --git a/tests/components/dunehd/test_config_flow.py b/tests/components/dunehd/test_config_flow.py index f2d30b2a8dd..b6e57a71668 100644 --- a/tests/components/dunehd/test_config_flow.py +++ b/tests/components/dunehd/test_config_flow.py @@ -1,10 +1,11 @@ """Define tests for the Dune HD config flow.""" +from unittest.mock import patch + from homeassistant import data_entry_flow from homeassistant.components.dunehd.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_HOST -from tests.async_mock import patch from tests.common import MockConfigEntry CONFIG_HOSTNAME = {CONF_HOST: "dunehd-host"} diff --git a/tests/components/dynalite/common.py b/tests/components/dynalite/common.py index f72e3f481b6..48ec378689e 100644 --- a/tests/components/dynalite/common.py +++ b/tests/components/dynalite/common.py @@ -1,8 +1,9 @@ """Common functions for tests.""" +from unittest.mock import AsyncMock, Mock, call, patch + from homeassistant.components import dynalite from homeassistant.helpers import entity_registry -from tests.async_mock import AsyncMock, Mock, call, patch from tests.common import MockConfigEntry ATTR_SERVICE = "service" diff --git a/tests/components/dynalite/test_bridge.py b/tests/components/dynalite/test_bridge.py index 8f3210bbcf8..363a9671f59 100644 --- a/tests/components/dynalite/test_bridge.py +++ b/tests/components/dynalite/test_bridge.py @@ -1,6 +1,8 @@ """Test Dynalite bridge.""" +from unittest.mock import AsyncMock, Mock, patch + from dynalite_devices_lib.dynalite_devices import ( CONF_AREA as dyn_CONF_AREA, CONF_PRESET as dyn_CONF_PRESET, @@ -18,7 +20,6 @@ from homeassistant.components.dynalite.const import ( ) from homeassistant.helpers.dispatcher import async_dispatcher_connect -from tests.async_mock import AsyncMock, Mock, patch from tests.common import MockConfigEntry diff --git a/tests/components/dynalite/test_config_flow.py b/tests/components/dynalite/test_config_flow.py index 11b4d6b524c..e21c82d7c20 100644 --- a/tests/components/dynalite/test_config_flow.py +++ b/tests/components/dynalite/test_config_flow.py @@ -1,11 +1,12 @@ """Test Dynalite config flow.""" +from unittest.mock import AsyncMock, patch + import pytest from homeassistant import config_entries from homeassistant.components import dynalite -from tests.async_mock import AsyncMock, patch from tests.common import MockConfigEntry diff --git a/tests/components/dynalite/test_init.py b/tests/components/dynalite/test_init.py index faa75aadef8..d231f82d2f8 100644 --- a/tests/components/dynalite/test_init.py +++ b/tests/components/dynalite/test_init.py @@ -1,6 +1,8 @@ """Test Dynalite __init__.""" +from unittest.mock import call, patch + import pytest from voluptuous import MultipleInvalid @@ -8,7 +10,6 @@ import homeassistant.components.dynalite.const as dynalite from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_ROOM from homeassistant.setup import async_setup_component -from tests.async_mock import call, patch from tests.common import MockConfigEntry diff --git a/tests/components/dyson/test_air_quality.py b/tests/components/dyson/test_air_quality.py index ab11a1ad897..06050fb8014 100644 --- a/tests/components/dyson/test_air_quality.py +++ b/tests/components/dyson/test_air_quality.py @@ -1,6 +1,7 @@ """Test the Dyson air quality component.""" import json from unittest import mock +from unittest.mock import patch from libpurecool.dyson_pure_cool import DysonPureCool from libpurecool.dyson_pure_state_v2 import DysonEnvironmentalSensorV2State @@ -18,8 +19,6 @@ from homeassistant.setup import async_setup_component from .common import load_mock_device -from tests.async_mock import patch - def _get_dyson_purecool_device(): """Return a valid device as provided by the Dyson web services.""" diff --git a/tests/components/dyson/test_climate.py b/tests/components/dyson/test_climate.py index c4e4c91087c..484af9d48e8 100644 --- a/tests/components/dyson/test_climate.py +++ b/tests/components/dyson/test_climate.py @@ -1,5 +1,6 @@ """Test the Dyson fan component.""" import json +from unittest.mock import Mock, call, patch from libpurecool.const import ( FanPower, @@ -61,8 +62,6 @@ from homeassistant.setup import async_setup_component from .common import load_mock_device -from tests.async_mock import Mock, call, patch - class MockDysonState(DysonPureHotCoolState): """Mock Dyson state.""" diff --git a/tests/components/dyson/test_fan.py b/tests/components/dyson/test_fan.py index 11770e1f133..8fa75e818ef 100644 --- a/tests/components/dyson/test_fan.py +++ b/tests/components/dyson/test_fan.py @@ -2,6 +2,7 @@ import json import unittest from unittest import mock +from unittest.mock import patch from libpurecool.const import FanMode, FanSpeed, NightMode, Oscillation from libpurecool.dyson_pure_cool import DysonPureCool @@ -27,7 +28,6 @@ from homeassistant.setup import async_setup_component from .common import load_mock_device -from tests.async_mock import patch from tests.common import get_test_home_assistant diff --git a/tests/components/dyson/test_sensor.py b/tests/components/dyson/test_sensor.py index 4acc52743bb..7ad6ab6c2f2 100644 --- a/tests/components/dyson/test_sensor.py +++ b/tests/components/dyson/test_sensor.py @@ -1,6 +1,7 @@ """Test the Dyson sensor(s) component.""" import unittest from unittest import mock +from unittest.mock import patch from libpurecool.dyson_pure_cool import DysonPureCool from libpurecool.dyson_pure_cool_link import DysonPureCoolLink @@ -20,7 +21,6 @@ from homeassistant.setup import async_setup_component from .common import load_mock_device -from tests.async_mock import patch from tests.common import get_test_home_assistant diff --git a/tests/components/eafm/conftest.py b/tests/components/eafm/conftest.py index 7233257f2eb..69bb2258486 100644 --- a/tests/components/eafm/conftest.py +++ b/tests/components/eafm/conftest.py @@ -1,8 +1,8 @@ """eafm fixtures.""" -import pytest +from unittest.mock import patch -from tests.async_mock import patch +import pytest @pytest.fixture() diff --git a/tests/components/eafm/test_config_flow.py b/tests/components/eafm/test_config_flow.py index cd71767104f..8a1e2bd89fc 100644 --- a/tests/components/eafm/test_config_flow.py +++ b/tests/components/eafm/test_config_flow.py @@ -1,11 +1,11 @@ """Tests for eafm config flow.""" +from unittest.mock import patch + import pytest from voluptuous.error import MultipleInvalid from homeassistant.components.eafm import const -from tests.async_mock import patch - async def test_flow_no_discovered_stations(hass, mock_get_stations): """Test config flow discovers no station.""" diff --git a/tests/components/ee_brightbox/test_device_tracker.py b/tests/components/ee_brightbox/test_device_tracker.py index 64f24ec289c..06c3ce0cd1d 100644 --- a/tests/components/ee_brightbox/test_device_tracker.py +++ b/tests/components/ee_brightbox/test_device_tracker.py @@ -1,5 +1,6 @@ """Tests for the EE BrightBox device scanner.""" from datetime import datetime +from unittest.mock import patch from eebrightbox import EEBrightBoxException import pytest @@ -8,8 +9,6 @@ from homeassistant.components.device_tracker import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_PLATFORM from homeassistant.setup import async_setup_component -from tests.async_mock import patch - def _configure_mock_get_devices(eebrightbox_mock): eebrightbox_instance = eebrightbox_mock.return_value diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py index 346d0b65ad2..d73bbe53e9a 100644 --- a/tests/components/elkm1/test_config_flow.py +++ b/tests/components/elkm1/test_config_flow.py @@ -1,10 +1,10 @@ """Test the Elk-M1 Control config flow.""" +from unittest.mock import MagicMock, patch + from homeassistant import config_entries, setup from homeassistant.components.elkm1.const import DOMAIN -from tests.async_mock import MagicMock, patch - def mock_elk(invalid_auth=None, sync_complete=None): """Mock m1lib Elk.""" diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 576a464c86a..4d8079b9db9 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -3,6 +3,7 @@ import asyncio from datetime import timedelta from ipaddress import ip_address import json +from unittest.mock import patch from aiohttp.hdrs import CONTENT_TYPE import pytest @@ -50,7 +51,6 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import async_fire_time_changed, get_test_instance_port HTTP_SERVER_PORT = get_test_instance_port() diff --git a/tests/components/emulated_hue/test_init.py b/tests/components/emulated_hue/test_init.py index b1cf2aacb1b..6fa6d969539 100644 --- a/tests/components/emulated_hue/test_init.py +++ b/tests/components/emulated_hue/test_init.py @@ -1,7 +1,7 @@ """Test the Emulated Hue component.""" -from homeassistant.components.emulated_hue import Config +from unittest.mock import MagicMock, Mock, patch -from tests.async_mock import MagicMock, Mock, patch +from homeassistant.components.emulated_hue import Config def test_config_google_home_entity_id_to_number(): diff --git a/tests/components/emulated_kasa/test_init.py b/tests/components/emulated_kasa/test_init.py index 10ccb4d68a5..60f4f5be1db 100644 --- a/tests/components/emulated_kasa/test_init.py +++ b/tests/components/emulated_kasa/test_init.py @@ -1,5 +1,6 @@ """Tests for emulated_kasa library bindings.""" import math +from unittest.mock import AsyncMock, Mock, patch from homeassistant.components import emulated_kasa from homeassistant.components.emulated_kasa.const import ( @@ -29,8 +30,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock, Mock, patch - ENTITY_SWITCH = "switch.ac" ENTITY_SWITCH_NAME = "A/C" ENTITY_SWITCH_POWER = 400.0 diff --git a/tests/components/emulated_roku/test_binding.py b/tests/components/emulated_roku/test_binding.py index 5ff29194adf..5afee6f6cc3 100644 --- a/tests/components/emulated_roku/test_binding.py +++ b/tests/components/emulated_roku/test_binding.py @@ -1,4 +1,6 @@ """Tests for emulated_roku library bindings.""" +from unittest.mock import AsyncMock, Mock, patch + from homeassistant.components.emulated_roku.binding import ( ATTR_APP_ID, ATTR_COMMAND_TYPE, @@ -12,8 +14,6 @@ from homeassistant.components.emulated_roku.binding import ( EmulatedRoku, ) -from tests.async_mock import AsyncMock, Mock, patch - async def test_events_fired_properly(hass): """Test that events are fired correctly.""" diff --git a/tests/components/emulated_roku/test_init.py b/tests/components/emulated_roku/test_init.py index 92952a5d840..8f256ee4c79 100644 --- a/tests/components/emulated_roku/test_init.py +++ b/tests/components/emulated_roku/test_init.py @@ -1,9 +1,9 @@ """Test emulated_roku component setup process.""" +from unittest.mock import AsyncMock, Mock, patch + from homeassistant.components import emulated_roku from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock, Mock, patch - async def test_config_required_fields(hass): """Test that configuration is successful with required fields.""" diff --git a/tests/components/enocean/test_config_flow.py b/tests/components/enocean/test_config_flow.py index b815d48694a..4cea2a4fb9b 100644 --- a/tests/components/enocean/test_config_flow.py +++ b/tests/components/enocean/test_config_flow.py @@ -1,10 +1,11 @@ """Tests for EnOcean config flow.""" +from unittest.mock import Mock, patch + from homeassistant import data_entry_flow from homeassistant.components.enocean.config_flow import EnOceanFlowHandler from homeassistant.components.enocean.const import DOMAIN from homeassistant.const import CONF_DEVICE -from tests.async_mock import Mock, patch from tests.common import MockConfigEntry DONGLE_VALIDATE_PATH_METHOD = "homeassistant.components.enocean.dongle.validate_path" diff --git a/tests/components/epson/test_config_flow.py b/tests/components/epson/test_config_flow.py index 6faaa285a2a..4a0b9f9675f 100644 --- a/tests/components/epson/test_config_flow.py +++ b/tests/components/epson/test_config_flow.py @@ -1,10 +1,10 @@ """Test the epson config flow.""" +from unittest.mock import patch + from homeassistant import config_entries, setup from homeassistant.components.epson.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, STATE_UNAVAILABLE -from tests.async_mock import patch - async def test_form(hass): """Test we get the form.""" diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 41c3d6cf528..f3afce0d43b 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -1,5 +1,6 @@ """Test config flow.""" from collections import namedtuple +from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -11,7 +12,6 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_FORM, ) -from tests.async_mock import AsyncMock, MagicMock, patch from tests.common import MockConfigEntry MockDeviceInfo = namedtuple("DeviceInfo", ["uses_password", "name"]) diff --git a/tests/components/facebox/test_image_processing.py b/tests/components/facebox/test_image_processing.py index 00ce7af0d01..b0e482a89f5 100644 --- a/tests/components/facebox/test_image_processing.py +++ b/tests/components/facebox/test_image_processing.py @@ -1,4 +1,6 @@ """The tests for the facebox component.""" +from unittest.mock import Mock, mock_open, patch + import pytest import requests import requests_mock @@ -21,8 +23,6 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, mock_open, patch - MOCK_IP = "192.168.0.1" MOCK_PORT = "8080" diff --git a/tests/components/fail2ban/test_sensor.py b/tests/components/fail2ban/test_sensor.py index 09daee423aa..e43064c54ab 100644 --- a/tests/components/fail2ban/test_sensor.py +++ b/tests/components/fail2ban/test_sensor.py @@ -1,4 +1,6 @@ """The tests for local file sensor platform.""" +from unittest.mock import Mock, mock_open, patch + from homeassistant.components.fail2ban.sensor import ( STATE_ALL_BANS, STATE_CURRENT_BANS, @@ -7,7 +9,6 @@ from homeassistant.components.fail2ban.sensor import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, mock_open, patch from tests.common import assert_setup_component diff --git a/tests/components/feedreader/test_init.py b/tests/components/feedreader/test_init.py index b50ef5f6619..307ba577de3 100644 --- a/tests/components/feedreader/test_init.py +++ b/tests/components/feedreader/test_init.py @@ -6,6 +6,7 @@ from os.path import exists import time import unittest from unittest import mock +from unittest.mock import patch from homeassistant.components import feedreader from homeassistant.components.feedreader import ( @@ -21,7 +22,6 @@ from homeassistant.const import CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START from homeassistant.core import callback from homeassistant.setup import setup_component -from tests.async_mock import patch from tests.common import get_test_home_assistant, load_fixture _LOGGER = getLogger(__name__) diff --git a/tests/components/ffmpeg/test_init.py b/tests/components/ffmpeg/test_init.py index 4187fe561cc..3c6a2fbb92d 100644 --- a/tests/components/ffmpeg/test_init.py +++ b/tests/components/ffmpeg/test_init.py @@ -1,4 +1,6 @@ """The tests for Home Assistant ffmpeg.""" +from unittest.mock import MagicMock + import homeassistant.components.ffmpeg as ffmpeg from homeassistant.components.ffmpeg import ( DOMAIN, @@ -10,7 +12,6 @@ from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import callback from homeassistant.setup import async_setup_component, setup_component -from tests.async_mock import MagicMock from tests.common import assert_setup_component, get_test_home_assistant diff --git a/tests/components/fido/test_sensor.py b/tests/components/fido/test_sensor.py index 551a59f0788..3baaf2e350c 100644 --- a/tests/components/fido/test_sensor.py +++ b/tests/components/fido/test_sensor.py @@ -1,12 +1,12 @@ """The test for the fido sensor platform.""" import logging +from unittest.mock import MagicMock, patch from pyfido.client import PyFidoError from homeassistant.bootstrap import async_setup_component from homeassistant.components.fido import sensor as fido -from tests.async_mock import MagicMock, patch from tests.common import assert_setup_component CONTRACT = "123456789" diff --git a/tests/components/file/test_notify.py b/tests/components/file/test_notify.py index 5bb387ea1e3..d2db5d9e8a8 100644 --- a/tests/components/file/test_notify.py +++ b/tests/components/file/test_notify.py @@ -1,5 +1,6 @@ """The tests for the notify file platform.""" import os +from unittest.mock import call, mock_open, patch import pytest @@ -8,7 +9,6 @@ from homeassistant.components.notify import ATTR_TITLE_DEFAULT from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import call, mock_open, patch from tests.common import assert_setup_component diff --git a/tests/components/file/test_sensor.py b/tests/components/file/test_sensor.py index 31370334f92..99e08362ab7 100644 --- a/tests/components/file/test_sensor.py +++ b/tests/components/file/test_sensor.py @@ -1,10 +1,11 @@ """The tests for local file sensor platform.""" +from unittest.mock import Mock, mock_open, patch + import pytest from homeassistant.const import STATE_UNKNOWN from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, mock_open, patch from tests.common import mock_registry diff --git a/tests/components/filesize/test_sensor.py b/tests/components/filesize/test_sensor.py index 60f6d273ea4..5649a678e3a 100644 --- a/tests/components/filesize/test_sensor.py +++ b/tests/components/filesize/test_sensor.py @@ -1,5 +1,6 @@ """The tests for the filesize sensor.""" import os +from unittest.mock import patch import pytest @@ -9,8 +10,6 @@ from homeassistant.components.filesize.sensor import CONF_FILE_PATHS from homeassistant.const import SERVICE_RELOAD from homeassistant.setup import async_setup_component -from tests.async_mock import patch - TEST_DIR = os.path.join(os.path.dirname(__file__)) TEST_FILE = os.path.join(TEST_DIR, "mock_file_test_filesize.txt") diff --git a/tests/components/filter/test_sensor.py b/tests/components/filter/test_sensor.py index 1c3b4b0d672..417c84e8ea4 100644 --- a/tests/components/filter/test_sensor.py +++ b/tests/components/filter/test_sensor.py @@ -1,6 +1,7 @@ """The test for the data filter sensor platform.""" from datetime import timedelta from os import path +from unittest.mock import patch from pytest import fixture @@ -20,7 +21,6 @@ import homeassistant.core as ha from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import assert_setup_component, async_init_recorder_component diff --git a/tests/components/fireservicerota/test_config_flow.py b/tests/components/fireservicerota/test_config_flow.py index 8ccaae5fbbc..69f37bc3ad4 100644 --- a/tests/components/fireservicerota/test_config_flow.py +++ b/tests/components/fireservicerota/test_config_flow.py @@ -1,11 +1,12 @@ """Test the FireServiceRota config flow.""" +from unittest.mock import patch + 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 = { diff --git a/tests/components/firmata/test_config_flow.py b/tests/components/firmata/test_config_flow.py index e77f219e320..91db94052cc 100644 --- a/tests/components/firmata/test_config_flow.py +++ b/tests/components/firmata/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Firmata config flow.""" +from unittest.mock import patch + from pymata_express.pymata_express_serial import serial from homeassistant import config_entries, setup @@ -6,8 +8,6 @@ from homeassistant.components.firmata.const import CONF_SERIAL_PORT, DOMAIN from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant -from tests.async_mock import patch - async def test_import_cannot_connect_pymata(hass: HomeAssistant) -> None: """Test we fail with an invalid board.""" diff --git a/tests/components/flick_electric/test_config_flow.py b/tests/components/flick_electric/test_config_flow.py index f18daed875f..1890ea9448a 100644 --- a/tests/components/flick_electric/test_config_flow.py +++ b/tests/components/flick_electric/test_config_flow.py @@ -1,5 +1,6 @@ """Test the Flick Electric config flow.""" import asyncio +from unittest.mock import patch from pyflick.authentication import AuthException @@ -7,7 +8,6 @@ from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.flick_electric.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import patch from tests.common import MockConfigEntry CONF = {CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"} diff --git a/tests/components/flo/test_config_flow.py b/tests/components/flo/test_config_flow.py index edc9705b7cd..d26051bbcb2 100644 --- a/tests/components/flo/test_config_flow.py +++ b/tests/components/flo/test_config_flow.py @@ -1,6 +1,7 @@ """Test the flo config flow.""" import json import time +from unittest.mock import patch from homeassistant import config_entries, setup from homeassistant.components.flo.const import DOMAIN @@ -8,8 +9,6 @@ from homeassistant.const import CONTENT_TYPE_JSON from .common import TEST_EMAIL_ADDRESS, TEST_PASSWORD, TEST_TOKEN, TEST_USER_ID -from tests.async_mock import patch - async def test_form(hass, aioclient_mock_fixture): """Test we get the form.""" diff --git a/tests/components/flume/test_config_flow.py b/tests/components/flume/test_config_flow.py index 0afea0a9742..9ae0889d52c 100644 --- a/tests/components/flume/test_config_flow.py +++ b/tests/components/flume/test_config_flow.py @@ -1,4 +1,6 @@ """Test the flume config flow.""" +from unittest.mock import MagicMock, patch + import requests.exceptions from homeassistant import config_entries, setup @@ -10,8 +12,6 @@ from homeassistant.const import ( CONF_USERNAME, ) -from tests.async_mock import MagicMock, patch - def _get_mocked_flume_device_list(): flume_device_list_mock = MagicMock() diff --git a/tests/components/flunearyou/test_config_flow.py b/tests/components/flunearyou/test_config_flow.py index 3681768ccdf..fbed4d2b426 100644 --- a/tests/components/flunearyou/test_config_flow.py +++ b/tests/components/flunearyou/test_config_flow.py @@ -1,4 +1,6 @@ """Define tests for the flunearyou config flow.""" +from unittest.mock import patch + from pyflunearyou.errors import FluNearYouError from homeassistant import data_entry_flow @@ -6,7 +8,6 @@ from homeassistant.components.flunearyou import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/flux/test_switch.py b/tests/components/flux/test_switch.py index be1fcf4c5ee..0d4e4f0595e 100644 --- a/tests/components/flux/test_switch.py +++ b/tests/components/flux/test_switch.py @@ -1,4 +1,6 @@ """The tests for the Flux switch platform.""" +from unittest.mock import patch + import pytest from homeassistant.components import light, switch @@ -13,7 +15,6 @@ from homeassistant.core import State from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import ( assert_setup_component, async_fire_time_changed, diff --git a/tests/components/folder_watcher/test_init.py b/tests/components/folder_watcher/test_init.py index 1123a5907fb..b0a522cb7fc 100644 --- a/tests/components/folder_watcher/test_init.py +++ b/tests/components/folder_watcher/test_init.py @@ -1,11 +1,10 @@ """The tests for the folder_watcher component.""" import os +from unittest.mock import Mock, patch from homeassistant.components import folder_watcher from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, patch - async def test_invalid_path_setup(hass): """Test that an invalid path is not set up.""" diff --git a/tests/components/foobot/test_sensor.py b/tests/components/foobot/test_sensor.py index 9b8a81c96d5..f817b38c98b 100644 --- a/tests/components/foobot/test_sensor.py +++ b/tests/components/foobot/test_sensor.py @@ -2,6 +2,7 @@ import asyncio import re +from unittest.mock import MagicMock import pytest @@ -19,7 +20,6 @@ from homeassistant.const import ( from homeassistant.exceptions import PlatformNotReady from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock from tests.common import load_fixture VALID_CONFIG = { diff --git a/tests/components/forked_daapd/test_config_flow.py b/tests/components/forked_daapd/test_config_flow.py index f655e727667..843aa12a759 100644 --- a/tests/components/forked_daapd/test_config_flow.py +++ b/tests/components/forked_daapd/test_config_flow.py @@ -1,4 +1,6 @@ """The config flow tests for the forked_daapd media player platform.""" +from unittest.mock import AsyncMock, patch + import pytest from homeassistant import data_entry_flow @@ -16,7 +18,6 @@ from homeassistant.config_entries import ( ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT -from tests.async_mock import AsyncMock, patch from tests.common import MockConfigEntry SAMPLE_CONFIG = { diff --git a/tests/components/forked_daapd/test_media_player.py b/tests/components/forked_daapd/test_media_player.py index 15755949062..149cbdae4e2 100644 --- a/tests/components/forked_daapd/test_media_player.py +++ b/tests/components/forked_daapd/test_media_player.py @@ -1,5 +1,7 @@ """The media player tests for the forked_daapd media player platform.""" +from unittest.mock import patch + import pytest from homeassistant.components.forked_daapd.const import ( @@ -65,7 +67,6 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) -from tests.async_mock import patch from tests.common import MockConfigEntry, async_mock_signal TEST_MASTER_ENTITY_NAME = "media_player.forked_daapd_server" diff --git a/tests/components/freebox/conftest.py b/tests/components/freebox/conftest.py index 7581b03ce72..e813469cbbf 100644 --- a/tests/components/freebox/conftest.py +++ b/tests/components/freebox/conftest.py @@ -1,7 +1,7 @@ """Test helpers for Freebox.""" -import pytest +from unittest.mock import patch -from tests.async_mock import patch +import pytest @pytest.fixture(autouse=True) diff --git a/tests/components/freebox/test_config_flow.py b/tests/components/freebox/test_config_flow.py index addb1762df0..f7150df7efc 100644 --- a/tests/components/freebox/test_config_flow.py +++ b/tests/components/freebox/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for the Freebox config flow.""" +from unittest.mock import AsyncMock, patch + from aiofreepybox.exceptions import ( AuthorizationError, HttpRequestError, @@ -11,7 +13,6 @@ from homeassistant.components.freebox.const import DOMAIN from homeassistant.config_entries import SOURCE_DISCOVERY, SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_PORT -from tests.async_mock import AsyncMock, patch from tests.common import MockConfigEntry HOST = "myrouter.freeboxos.fr" diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index 066b9a30cb3..f19e05b84df 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -1,9 +1,9 @@ """Tests for the AVM Fritz!Box integration.""" +from unittest.mock import Mock + from homeassistant.components.fritzbox.const import DOMAIN from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import Mock - MOCK_CONFIG = { DOMAIN: { CONF_DEVICES: [ diff --git a/tests/components/fritzbox/conftest.py b/tests/components/fritzbox/conftest.py index 7dcee138382..591c1037525 100644 --- a/tests/components/fritzbox/conftest.py +++ b/tests/components/fritzbox/conftest.py @@ -1,7 +1,7 @@ """Fixtures for the AVM Fritz!Box integration.""" -import pytest +from unittest.mock import Mock, patch -from tests.async_mock import Mock, patch +import pytest @pytest.fixture(name="fritz") diff --git a/tests/components/fritzbox/test_binary_sensor.py b/tests/components/fritzbox/test_binary_sensor.py index b3157a3be33..89c1dea1704 100644 --- a/tests/components/fritzbox/test_binary_sensor.py +++ b/tests/components/fritzbox/test_binary_sensor.py @@ -1,6 +1,7 @@ """Tests for AVM Fritz!Box binary sensor component.""" from datetime import timedelta from unittest import mock +from unittest.mock import Mock from requests.exceptions import HTTPError @@ -18,7 +19,6 @@ import homeassistant.util.dt as dt_util from . import MOCK_CONFIG, FritzDeviceBinarySensorMock -from tests.async_mock import Mock from tests.common import async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake_name" diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index 519e3afa31a..627eae5da91 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -1,5 +1,6 @@ """Tests for AVM Fritz!Box climate component.""" from datetime import timedelta +from unittest.mock import Mock, call from requests.exceptions import HTTPError @@ -41,7 +42,6 @@ import homeassistant.util.dt as dt_util from . import MOCK_CONFIG, FritzDeviceClimateMock -from tests.async_mock import Mock, call from tests.common import async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake_name" diff --git a/tests/components/fritzbox/test_config_flow.py b/tests/components/fritzbox/test_config_flow.py index 41396735441..31a9f89ce48 100644 --- a/tests/components/fritzbox/test_config_flow.py +++ b/tests/components/fritzbox/test_config_flow.py @@ -1,5 +1,6 @@ """Tests for AVM Fritz!Box config flow.""" from unittest import mock +from unittest.mock import Mock, patch from pyfritzhome import LoginError import pytest @@ -16,8 +17,6 @@ from homeassistant.helpers.typing import HomeAssistantType from . import MOCK_CONFIG -from tests.async_mock import Mock, patch - MOCK_USER_DATA = MOCK_CONFIG[DOMAIN][CONF_DEVICES][0] MOCK_SSDP_DATA = { ATTR_SSDP_LOCATION: "https://fake_host:12345/test", diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index 55dab3626db..11067c1aa51 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -1,4 +1,6 @@ """Tests for the AVM Fritz!Box integration.""" +from unittest.mock import Mock, call + from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED @@ -8,7 +10,6 @@ from homeassistant.setup import async_setup_component from . import MOCK_CONFIG, FritzDeviceSwitchMock -from tests.async_mock import Mock, call from tests.common import MockConfigEntry diff --git a/tests/components/fritzbox/test_sensor.py b/tests/components/fritzbox/test_sensor.py index 7f97e8abfb1..6dde22f074e 100644 --- a/tests/components/fritzbox/test_sensor.py +++ b/tests/components/fritzbox/test_sensor.py @@ -1,5 +1,6 @@ """Tests for AVM Fritz!Box sensor component.""" from datetime import timedelta +from unittest.mock import Mock from requests.exceptions import HTTPError @@ -20,7 +21,6 @@ import homeassistant.util.dt as dt_util from . import MOCK_CONFIG, FritzDeviceSensorMock -from tests.async_mock import Mock from tests.common import async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake_name" diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index c9e05b2d481..1c0f7b3f37a 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -1,5 +1,6 @@ """Tests for AVM Fritz!Box switch component.""" from datetime import timedelta +from unittest.mock import Mock from requests.exceptions import HTTPError @@ -28,7 +29,6 @@ import homeassistant.util.dt as dt_util from . import MOCK_CONFIG, FritzDeviceSwitchMock -from tests.async_mock import Mock from tests.common import async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake_name" diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index 7802ee60e8c..5ae8d707cb1 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -1,6 +1,7 @@ """The tests for Home Assistant frontend.""" from datetime import timedelta import re +from unittest.mock import patch import pytest @@ -19,7 +20,6 @@ from homeassistant.loader import async_get_integration from homeassistant.setup import async_setup_component from homeassistant.util import dt -from tests.async_mock import patch from tests.common import async_capture_events, async_fire_time_changed CONFIG_THEMES = { diff --git a/tests/components/garmin_connect/test_config_flow.py b/tests/components/garmin_connect/test_config_flow.py index 93b24269441..75146570d55 100644 --- a/tests/components/garmin_connect/test_config_flow.py +++ b/tests/components/garmin_connect/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Garmin Connect config flow.""" +from unittest.mock import patch + from garminconnect import ( GarminConnectAuthenticationError, GarminConnectConnectionError, @@ -10,7 +12,6 @@ from homeassistant import data_entry_flow from homeassistant.components.garmin_connect.const import DOMAIN from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import patch from tests.common import MockConfigEntry MOCK_CONF = { diff --git a/tests/components/gdacs/__init__.py b/tests/components/gdacs/__init__.py index 648ab08507b..6e61b86dbb7 100644 --- a/tests/components/gdacs/__init__.py +++ b/tests/components/gdacs/__init__.py @@ -1,5 +1,5 @@ """Tests for the GDACS component.""" -from tests.async_mock import MagicMock +from unittest.mock import MagicMock def _generate_mock_feed_entry( diff --git a/tests/components/gdacs/test_config_flow.py b/tests/components/gdacs/test_config_flow.py index 10e4312eb38..e2ecd3902d5 100644 --- a/tests/components/gdacs/test_config_flow.py +++ b/tests/components/gdacs/test_config_flow.py @@ -1,5 +1,6 @@ """Define tests for the GDACS config flow.""" from datetime import timedelta +from unittest.mock import patch import pytest @@ -12,8 +13,6 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, ) -from tests.async_mock import patch - @pytest.fixture(name="gdacs_setup", autouse=True) def gdacs_setup_fixture(): diff --git a/tests/components/gdacs/test_geo_location.py b/tests/components/gdacs/test_geo_location.py index 4185a7f656e..7dc23eaa5d8 100644 --- a/tests/components/gdacs/test_geo_location.py +++ b/tests/components/gdacs/test_geo_location.py @@ -1,5 +1,6 @@ """The tests for the GDACS Feed integration.""" import datetime +from unittest.mock import patch from homeassistant.components import gdacs from homeassistant.components.gdacs import DEFAULT_SCAN_INTERVAL, DOMAIN, FEED @@ -33,7 +34,6 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM -from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.gdacs import _generate_mock_feed_entry diff --git a/tests/components/gdacs/test_init.py b/tests/components/gdacs/test_init.py index c0ac83ebcc2..cf78faf729b 100644 --- a/tests/components/gdacs/test_init.py +++ b/tests/components/gdacs/test_init.py @@ -1,7 +1,7 @@ """Define tests for the GDACS general setup.""" -from homeassistant.components.gdacs import DOMAIN, FEED +from unittest.mock import patch -from tests.async_mock import patch +from homeassistant.components.gdacs import DOMAIN, FEED async def test_component_unload_config_entry(hass, config_entry): diff --git a/tests/components/gdacs/test_sensor.py b/tests/components/gdacs/test_sensor.py index b123021a7e3..ac53d88478b 100644 --- a/tests/components/gdacs/test_sensor.py +++ b/tests/components/gdacs/test_sensor.py @@ -1,4 +1,6 @@ """The tests for the GDACS Feed integration.""" +from unittest.mock import patch + from homeassistant.components import gdacs from homeassistant.components.gdacs import DEFAULT_SCAN_INTERVAL from homeassistant.components.gdacs.sensor import ( @@ -18,7 +20,6 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.gdacs import _generate_mock_feed_entry diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index 7be1670dd4c..9a147995541 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -1,6 +1,7 @@ """The tests for generic camera component.""" import asyncio from os import path +from unittest.mock import patch from homeassistant import config as hass_config from homeassistant.components.generic import DOMAIN @@ -12,8 +13,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import patch - async def test_fetching_url(aioclient_mock, hass, hass_client): """Test that it fetches the given url.""" diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index 71c6f41282b..201ed0130ff 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -1,6 +1,7 @@ """The tests for the generic_thermostat.""" import datetime from os import path +from unittest.mock import patch import pytest import pytz @@ -37,7 +38,6 @@ from homeassistant.core import DOMAIN as HASS_DOMAIN, CoreState, State, callback from homeassistant.setup import async_setup_component from homeassistant.util.unit_system import METRIC_SYSTEM -from tests.async_mock import patch from tests.common import ( assert_setup_component, async_fire_time_changed, diff --git a/tests/components/geo_json_events/test_geo_location.py b/tests/components/geo_json_events/test_geo_location.py index d0505cc68d9..75f41bb93c0 100644 --- a/tests/components/geo_json_events/test_geo_location.py +++ b/tests/components/geo_json_events/test_geo_location.py @@ -1,4 +1,6 @@ """The tests for the geojson platform.""" +from unittest.mock import MagicMock, call, patch + from homeassistant.components import geo_location from homeassistant.components.geo_json_events.geo_location import ( ATTR_EXTERNAL_ID, @@ -21,7 +23,6 @@ from homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import MagicMock, call, patch from tests.common import assert_setup_component, async_fire_time_changed URL = "http://geo.json.local/geo_json_events.json" diff --git a/tests/components/geo_rss_events/test_sensor.py b/tests/components/geo_rss_events/test_sensor.py index 8c55b8ad4dd..ead81e5c5a8 100644 --- a/tests/components/geo_rss_events/test_sensor.py +++ b/tests/components/geo_rss_events/test_sensor.py @@ -1,4 +1,6 @@ """The test for the geo rss events sensor platform.""" +from unittest.mock import MagicMock, patch + import pytest from homeassistant.components import sensor @@ -12,7 +14,6 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import MagicMock, patch from tests.common import assert_setup_component, async_fire_time_changed URL = "http://geo.rss.local/geo_rss_events.xml" diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py index 21b6830e7f4..b87b201a144 100644 --- a/tests/components/geofency/test_init.py +++ b/tests/components/geofency/test_init.py @@ -1,4 +1,7 @@ """The tests for the Geofency device tracker platform.""" +# pylint: disable=redefined-outer-name +from unittest.mock import patch + import pytest from homeassistant import data_entry_flow @@ -16,9 +19,6 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component from homeassistant.util import slugify -# pylint: disable=redefined-outer-name -from tests.async_mock import patch - HOME_LATITUDE = 37.239622 HOME_LONGITUDE = -115.815811 diff --git a/tests/components/geonetnz_quakes/__init__.py b/tests/components/geonetnz_quakes/__init__.py index 82cb62b3939..424c6372ea8 100644 --- a/tests/components/geonetnz_quakes/__init__.py +++ b/tests/components/geonetnz_quakes/__init__.py @@ -1,5 +1,5 @@ """Tests for the geonetnz_quakes component.""" -from tests.async_mock import MagicMock +from unittest.mock import MagicMock def _generate_mock_feed_entry( diff --git a/tests/components/geonetnz_quakes/test_config_flow.py b/tests/components/geonetnz_quakes/test_config_flow.py index a4b1d9c792b..d362e9cdf0f 100644 --- a/tests/components/geonetnz_quakes/test_config_flow.py +++ b/tests/components/geonetnz_quakes/test_config_flow.py @@ -1,5 +1,6 @@ """Define tests for the GeoNet NZ Quakes config flow.""" from datetime import timedelta +from unittest.mock import patch from homeassistant import data_entry_flow from homeassistant.components.geonetnz_quakes import ( @@ -15,8 +16,6 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM, ) -from tests.async_mock import patch - async def test_duplicate_error(hass, config_entry): """Test that errors are shown when duplicates are added.""" diff --git a/tests/components/geonetnz_quakes/test_geo_location.py b/tests/components/geonetnz_quakes/test_geo_location.py index 2622cd100b3..b0e54e89929 100644 --- a/tests/components/geonetnz_quakes/test_geo_location.py +++ b/tests/components/geonetnz_quakes/test_geo_location.py @@ -1,5 +1,6 @@ """The tests for the GeoNet NZ Quakes Feed integration.""" import datetime +from unittest.mock import patch from homeassistant.components import geonetnz_quakes from homeassistant.components.geo_location import ATTR_SOURCE @@ -29,7 +30,6 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM -from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.geonetnz_quakes import _generate_mock_feed_entry diff --git a/tests/components/geonetnz_quakes/test_init.py b/tests/components/geonetnz_quakes/test_init.py index 87f2f2a7947..e8a1dc1e380 100644 --- a/tests/components/geonetnz_quakes/test_init.py +++ b/tests/components/geonetnz_quakes/test_init.py @@ -1,7 +1,7 @@ """Define tests for the GeoNet NZ Quakes general setup.""" -from homeassistant.components.geonetnz_quakes import DOMAIN, FEED +from unittest.mock import patch -from tests.async_mock import patch +from homeassistant.components.geonetnz_quakes import DOMAIN, FEED async def test_component_unload_config_entry(hass, config_entry): diff --git a/tests/components/geonetnz_quakes/test_sensor.py b/tests/components/geonetnz_quakes/test_sensor.py index 5a6e675471a..8226fd91898 100644 --- a/tests/components/geonetnz_quakes/test_sensor.py +++ b/tests/components/geonetnz_quakes/test_sensor.py @@ -1,5 +1,6 @@ """The tests for the GeoNet NZ Quakes Feed integration.""" import datetime +from unittest.mock import patch from homeassistant.components import geonetnz_quakes from homeassistant.components.geonetnz_quakes import DEFAULT_SCAN_INTERVAL @@ -20,7 +21,6 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.geonetnz_quakes import _generate_mock_feed_entry diff --git a/tests/components/geonetnz_volcano/__init__.py b/tests/components/geonetnz_volcano/__init__.py index 023cab46ec8..708b69e0031 100644 --- a/tests/components/geonetnz_volcano/__init__.py +++ b/tests/components/geonetnz_volcano/__init__.py @@ -1,5 +1,5 @@ """The tests for the GeoNet NZ Volcano Feed integration.""" -from tests.async_mock import MagicMock +from unittest.mock import MagicMock def _generate_mock_feed_entry( diff --git a/tests/components/geonetnz_volcano/test_config_flow.py b/tests/components/geonetnz_volcano/test_config_flow.py index 21d74bb5e96..92c25e00927 100644 --- a/tests/components/geonetnz_volcano/test_config_flow.py +++ b/tests/components/geonetnz_volcano/test_config_flow.py @@ -1,5 +1,6 @@ """Define tests for the GeoNet NZ Volcano config flow.""" from datetime import timedelta +from unittest.mock import patch from homeassistant import data_entry_flow from homeassistant.components.geonetnz_volcano import config_flow @@ -11,8 +12,6 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM, ) -from tests.async_mock import patch - async def test_duplicate_error(hass, config_entry): """Test that errors are shown when duplicates are added.""" diff --git a/tests/components/geonetnz_volcano/test_init.py b/tests/components/geonetnz_volcano/test_init.py index 4edf8f452fe..42915f7feaa 100644 --- a/tests/components/geonetnz_volcano/test_init.py +++ b/tests/components/geonetnz_volcano/test_init.py @@ -1,7 +1,7 @@ """Define tests for the GeoNet NZ Volcano general setup.""" -from homeassistant.components.geonetnz_volcano import DOMAIN, FEED +from unittest.mock import AsyncMock, patch -from tests.async_mock import AsyncMock, patch +from homeassistant.components.geonetnz_volcano import DOMAIN, FEED async def test_component_unload_config_entry(hass, config_entry): diff --git a/tests/components/geonetnz_volcano/test_sensor.py b/tests/components/geonetnz_volcano/test_sensor.py index 22157c241ac..824fc059ace 100644 --- a/tests/components/geonetnz_volcano/test_sensor.py +++ b/tests/components/geonetnz_volcano/test_sensor.py @@ -1,4 +1,6 @@ """The tests for the GeoNet NZ Volcano Feed integration.""" +from unittest.mock import AsyncMock, patch + from homeassistant.components import geonetnz_volcano from homeassistant.components.geo_location import ATTR_DISTANCE from homeassistant.components.geonetnz_volcano import DEFAULT_SCAN_INTERVAL @@ -21,7 +23,6 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM -from tests.async_mock import AsyncMock, patch from tests.common import async_fire_time_changed from tests.components.geonetnz_volcano import _generate_mock_feed_entry diff --git a/tests/components/gios/__init__.py b/tests/components/gios/__init__.py index 920461a6ae1..6b1aa982c71 100644 --- a/tests/components/gios/__init__.py +++ b/tests/components/gios/__init__.py @@ -1,9 +1,9 @@ """Tests for GIOS.""" import json +from unittest.mock import patch from homeassistant.components.gios.const import DOMAIN -from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture STATIONS = [ diff --git a/tests/components/gios/test_air_quality.py b/tests/components/gios/test_air_quality.py index 9a5b1be6a20..21a1abf637a 100644 --- a/tests/components/gios/test_air_quality.py +++ b/tests/components/gios/test_air_quality.py @@ -1,6 +1,7 @@ """Test air_quality of GIOS integration.""" from datetime import timedelta import json +from unittest.mock import patch from gios import ApiError @@ -24,7 +25,6 @@ from homeassistant.const import ( ) from homeassistant.util.dt import utcnow -from tests.async_mock import patch from tests.common import async_fire_time_changed, load_fixture from tests.components.gios import init_integration diff --git a/tests/components/gios/test_config_flow.py b/tests/components/gios/test_config_flow.py index 24ada20aded..830b3a198a5 100644 --- a/tests/components/gios/test_config_flow.py +++ b/tests/components/gios/test_config_flow.py @@ -1,5 +1,6 @@ """Define tests for the GIOS config flow.""" import json +from unittest.mock import patch from gios import ApiError @@ -8,7 +9,6 @@ from homeassistant.components.gios import config_flow from homeassistant.components.gios.const import CONF_STATION_ID from homeassistant.const import CONF_NAME -from tests.async_mock import patch from tests.common import load_fixture from tests.components.gios import STATIONS diff --git a/tests/components/gios/test_init.py b/tests/components/gios/test_init.py index 0846ddfb4ca..344afe4e047 100644 --- a/tests/components/gios/test_init.py +++ b/tests/components/gios/test_init.py @@ -1,4 +1,6 @@ """Test init of GIOS integration.""" +from unittest.mock import patch + from homeassistant.components.gios.const import DOMAIN from homeassistant.config_entries import ( ENTRY_STATE_LOADED, @@ -7,7 +9,6 @@ from homeassistant.config_entries import ( ) from homeassistant.const import STATE_UNAVAILABLE -from tests.async_mock import patch from tests.common import MockConfigEntry from tests.components.gios import init_integration diff --git a/tests/components/goalzero/__init__.py b/tests/components/goalzero/__init__.py index 0ba2a912766..fb531dfca4b 100644 --- a/tests/components/goalzero/__init__.py +++ b/tests/components/goalzero/__init__.py @@ -1,8 +1,8 @@ """Tests for the Goal Zero Yeti integration.""" -from homeassistant.const import CONF_HOST, CONF_NAME +from unittest.mock import AsyncMock, patch -from tests.async_mock import AsyncMock, patch +from homeassistant.const import CONF_HOST, CONF_NAME HOST = "1.2.3.4" NAME = "Yeti" diff --git a/tests/components/goalzero/test_config_flow.py b/tests/components/goalzero/test_config_flow.py index 906a84d7882..10ef02bfcff 100644 --- a/tests/components/goalzero/test_config_flow.py +++ b/tests/components/goalzero/test_config_flow.py @@ -1,4 +1,6 @@ """Test Goal Zero Yeti config flow.""" +from unittest.mock import patch + from goalzero import exceptions from homeassistant.components.goalzero.const import DOMAIN @@ -19,7 +21,6 @@ from . import ( _patch_config_flow_yeti, ) -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/gogogate2/test_config_flow.py b/tests/components/gogogate2/test_config_flow.py index 667c0330d80..ba8a119553c 100644 --- a/tests/components/gogogate2/test_config_flow.py +++ b/tests/components/gogogate2/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for the GogoGate2 component.""" +from unittest.mock import MagicMock, patch + from gogogate2_api import GogoGate2Api from gogogate2_api.common import ApiError from gogogate2_api.const import GogoGate2ApiErrorCode @@ -19,7 +21,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry MOCK_MAC_ADDR = "AA:BB:CC:DD:EE:FF" diff --git a/tests/components/gogogate2/test_cover.py b/tests/components/gogogate2/test_cover.py index 91bffca56ce..7a944d3f7f1 100644 --- a/tests/components/gogogate2/test_cover.py +++ b/tests/components/gogogate2/test_cover.py @@ -1,5 +1,6 @@ """Tests for the GogoGate2 component.""" from datetime import timedelta +from unittest.mock import MagicMock, patch from gogogate2_api import GogoGate2Api, ISmartGateApi from gogogate2_api.common import ( @@ -49,7 +50,6 @@ from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry, async_fire_time_changed, mock_device_registry diff --git a/tests/components/gogogate2/test_init.py b/tests/components/gogogate2/test_init.py index af8678300d1..d76108a9221 100644 --- a/tests/components/gogogate2/test_init.py +++ b/tests/components/gogogate2/test_init.py @@ -1,4 +1,6 @@ """Tests for the GogoGate2 component.""" +from unittest.mock import MagicMock, patch + from gogogate2_api import GogoGate2Api import pytest @@ -15,7 +17,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index 6ec75ad53f6..20cb13130ec 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -1,7 +1,7 @@ """Test configuration and mocks for the google integration.""" -import pytest +from unittest.mock import patch -from tests.async_mock import patch +import pytest TEST_CALENDAR = { "id": "qwertyuiopasdfghjklzxcvbnm@import.calendar.google.com", diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 92f03396965..ad7b6b12001 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -1,5 +1,6 @@ """The tests for the google calendar platform.""" import copy +from unittest.mock import Mock, patch import httplib2 import pytest @@ -22,7 +23,6 @@ from homeassistant.setup import async_setup_component from homeassistant.util import slugify import homeassistant.util.dt as dt_util -from tests.async_mock import Mock, patch from tests.common import async_mock_service GOOGLE_CONFIG = {CONF_CLIENT_ID: "client_id", CONF_CLIENT_SECRET: "client_secret"} diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index e3412a01f5e..d90efa29f6c 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -1,12 +1,12 @@ """The tests for the Google Calendar component.""" +from unittest.mock import patch + import pytest import homeassistant.components.google as google from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.setup import async_setup_component -from tests.async_mock import patch - @pytest.fixture(name="google_setup") def mock_google_setup(hass): diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index bbc5b92615c..5927ee83d25 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -1,7 +1,7 @@ """Tests for the Google Assistant integration.""" -from homeassistant.components.google_assistant import helpers +from unittest.mock import MagicMock -from tests.async_mock import MagicMock +from homeassistant.components.google_assistant import helpers def mock_google_config_store(agent_user_ids=None): diff --git a/tests/components/google_assistant/test_helpers.py b/tests/components/google_assistant/test_helpers.py index 87e3cac657c..abf2773d67e 100644 --- a/tests/components/google_assistant/test_helpers.py +++ b/tests/components/google_assistant/test_helpers.py @@ -1,5 +1,6 @@ """Test Google Assistant helpers.""" from datetime import timedelta +from unittest.mock import Mock, call, patch import pytest @@ -15,7 +16,6 @@ from homeassistant.util import dt from . import MockConfig -from tests.async_mock import Mock, call, patch from tests.common import ( async_capture_events, async_fire_time_changed, diff --git a/tests/components/google_assistant/test_http.py b/tests/components/google_assistant/test_http.py index 4b9461e6304..69a8242b7cc 100644 --- a/tests/components/google_assistant/test_http.py +++ b/tests/components/google_assistant/test_http.py @@ -1,5 +1,6 @@ """Test Google http services.""" from datetime import datetime, timedelta, timezone +from unittest.mock import ANY, patch from homeassistant.components.google_assistant import GOOGLE_ASSISTANT_SCHEMA from homeassistant.components.google_assistant.const import ( @@ -12,8 +13,6 @@ from homeassistant.components.google_assistant.http import ( _get_homegraph_token, ) -from tests.async_mock import ANY, patch - DUMMY_CONFIG = GOOGLE_ASSISTANT_SCHEMA( { "project_id": "1234", diff --git a/tests/components/google_assistant/test_report_state.py b/tests/components/google_assistant/test_report_state.py index cd967e6c82e..4ab206edb90 100644 --- a/tests/components/google_assistant/test_report_state.py +++ b/tests/components/google_assistant/test_report_state.py @@ -1,10 +1,11 @@ """Test Google report state.""" +from unittest.mock import AsyncMock, patch + from homeassistant.components.google_assistant import error, report_state from homeassistant.util.dt import utcnow from . import BASIC_CONFIG -from tests.async_mock import AsyncMock, patch from tests.common import async_fire_time_changed diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index ebe34c2aa95..5739370b187 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -1,4 +1,6 @@ """Test Google Smart Home.""" +from unittest.mock import patch + import pytest from homeassistant.components import camera @@ -28,7 +30,6 @@ from homeassistant.setup import async_setup_component from . import BASIC_CONFIG, MockConfig -from tests.async_mock import patch from tests.common import mock_area_registry, mock_device_registry, mock_registry REQ_ID = "ff36a3cc-ec34-11e6-b1a0-64510650abcf" diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 4946416e1c8..b7034f927b4 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1,5 +1,6 @@ """Tests for the Google Assistant traits.""" from datetime import datetime, timedelta +from unittest.mock import patch import pytest @@ -53,7 +54,6 @@ from homeassistant.util import color from . import BASIC_CONFIG, MockConfig -from tests.async_mock import patch from tests.common import async_mock_service REQ_ID = "ff36a3cc-ec34-11e6-b1a0-64510650abcf" diff --git a/tests/components/google_pubsub/test_init.py b/tests/components/google_pubsub/test_init.py index 96be5b3ed62..c174e454701 100644 --- a/tests/components/google_pubsub/test_init.py +++ b/tests/components/google_pubsub/test_init.py @@ -1,6 +1,7 @@ """The tests for the Google Pub/Sub component.""" from dataclasses import dataclass from datetime import datetime +import unittest.mock as mock import pytest @@ -10,8 +11,6 @@ from homeassistant.const import EVENT_STATE_CHANGED from homeassistant.core import split_entity_id from homeassistant.setup import async_setup_component -import tests.async_mock as mock - GOOGLE_PUBSUB_PATH = "homeassistant.components.google_pubsub" diff --git a/tests/components/google_translate/test_tts.py b/tests/components/google_translate/test_tts.py index 79c303fd2ff..5690591ccd2 100644 --- a/tests/components/google_translate/test_tts.py +++ b/tests/components/google_translate/test_tts.py @@ -1,6 +1,7 @@ """The tests for the Google speech platform.""" import os import shutil +from unittest.mock import patch from gtts import gTTSError import pytest @@ -14,7 +15,6 @@ import homeassistant.components.tts as tts from homeassistant.config import async_process_ha_core_config from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import async_mock_service from tests.components.tts.test_init import mutagen_mock # noqa: F401 diff --git a/tests/components/google_wifi/test_sensor.py b/tests/components/google_wifi/test_sensor.py index a9e09c8b66d..06ad5e0c3ea 100644 --- a/tests/components/google_wifi/test_sensor.py +++ b/tests/components/google_wifi/test_sensor.py @@ -1,12 +1,12 @@ """The tests for the Google Wifi platform.""" from datetime import datetime, timedelta +from unittest.mock import Mock, patch import homeassistant.components.google_wifi.sensor as google_wifi from homeassistant.const import STATE_UNKNOWN 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, async_fire_time_changed NAME = "foo" diff --git a/tests/components/gpslogger/test_init.py b/tests/components/gpslogger/test_init.py index 39dc05303b3..d30f57f0f33 100644 --- a/tests/components/gpslogger/test_init.py +++ b/tests/components/gpslogger/test_init.py @@ -1,4 +1,6 @@ """The tests the for GPSLogger device tracker platform.""" +from unittest.mock import patch + import pytest from homeassistant import data_entry_flow @@ -15,8 +17,6 @@ from homeassistant.const import ( from homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component -from tests.async_mock import patch - HOME_LATITUDE = 37.239622 HOME_LONGITUDE = -115.815811 diff --git a/tests/components/graphite/test_init.py b/tests/components/graphite/test_init.py index 19b8c165f37..88be3723936 100644 --- a/tests/components/graphite/test_init.py +++ b/tests/components/graphite/test_init.py @@ -2,6 +2,7 @@ import socket import unittest from unittest import mock +from unittest.mock import patch import homeassistant.components.graphite as graphite from homeassistant.const import ( @@ -14,7 +15,6 @@ from homeassistant.const import ( import homeassistant.core as ha from homeassistant.setup import setup_component -from tests.async_mock import patch from tests.common import get_test_home_assistant diff --git a/tests/components/gree/common.py b/tests/components/gree/common.py index 8894730c8ca..d9fcfba39ce 100644 --- a/tests/components/gree/common.py +++ b/tests/components/gree/common.py @@ -1,5 +1,5 @@ """Common helpers for gree test cases.""" -from tests.async_mock import AsyncMock, Mock +from unittest.mock import AsyncMock, Mock def build_device_info_mock( diff --git a/tests/components/gree/conftest.py b/tests/components/gree/conftest.py index b102f0f36fe..bc9a6451dce 100644 --- a/tests/components/gree/conftest.py +++ b/tests/components/gree/conftest.py @@ -1,10 +1,10 @@ """Pytest module configuration.""" +from unittest.mock import AsyncMock, patch + import pytest from .common import build_device_info_mock, build_device_mock -from tests.async_mock import AsyncMock, patch - @pytest.fixture(name="discovery") def discovery_fixture(): diff --git a/tests/components/gree/test_climate.py b/tests/components/gree/test_climate.py index cfb689589bf..d85976c2410 100644 --- a/tests/components/gree/test_climate.py +++ b/tests/components/gree/test_climate.py @@ -1,5 +1,6 @@ """Tests for gree component.""" from datetime import timedelta +from unittest.mock import DEFAULT as DEFAULT_MOCK, AsyncMock, patch from greeclimate.device import HorizontalSwing, VerticalSwing from greeclimate.exceptions import DeviceNotBoundError, DeviceTimeoutError @@ -60,7 +61,6 @@ import homeassistant.util.dt as dt_util from .common import build_device_mock -from tests.async_mock import DEFAULT as DEFAULT_MOCK, AsyncMock, patch from tests.common import MockConfigEntry, async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake_device_1" diff --git a/tests/components/gree/test_init.py b/tests/components/gree/test_init.py index ef693c9538a..bf999ee9e6f 100644 --- a/tests/components/gree/test_init.py +++ b/tests/components/gree/test_init.py @@ -1,10 +1,11 @@ """Tests for the Gree Integration.""" +from unittest.mock import patch + from homeassistant.components.gree.const import DOMAIN as GREE_DOMAIN from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/griddy/test_config_flow.py b/tests/components/griddy/test_config_flow.py index 0b2656dcf09..cfc2b23a8ed 100644 --- a/tests/components/griddy/test_config_flow.py +++ b/tests/components/griddy/test_config_flow.py @@ -1,11 +1,10 @@ """Test the Griddy Power config flow.""" import asyncio +from unittest.mock import MagicMock, patch from homeassistant import config_entries, setup from homeassistant.components.griddy.const import DOMAIN -from tests.async_mock import MagicMock, patch - async def test_form(hass): """Test we get the form.""" diff --git a/tests/components/griddy/test_sensor.py b/tests/components/griddy/test_sensor.py index ae3d0c3be84..46f8d238c49 100644 --- a/tests/components/griddy/test_sensor.py +++ b/tests/components/griddy/test_sensor.py @@ -1,13 +1,13 @@ """The sensor tests for the griddy platform.""" import json import os +from unittest.mock import patch from griddypower.async_api import GriddyPriceData from homeassistant.components.griddy import CONF_LOADZONE, DOMAIN from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import load_fixture diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index 77f137c6ee1..627e3c5bbe0 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -1,6 +1,7 @@ """The tests for the Group components.""" # pylint: disable=protected-access from collections import OrderedDict +from unittest.mock import patch import homeassistant.components.group as group from homeassistant.const import ( @@ -19,7 +20,6 @@ from homeassistant.core import CoreState from homeassistant.helpers.event import TRACK_STATE_CHANGE_CALLBACKS from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import assert_setup_component from tests.components.group import common diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index 8ce9f4bf9f3..136da458f66 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -1,5 +1,7 @@ """The tests for the Group Light platform.""" from os import path +import unittest.mock +from unittest.mock import MagicMock, patch from homeassistant import config as hass_config from homeassistant.components.group import DOMAIN, SERVICE_RELOAD @@ -31,9 +33,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -import tests.async_mock -from tests.async_mock import MagicMock, patch - async def test_default_state(hass): """Test light group default state.""" @@ -603,7 +602,7 @@ async def test_invalid_service_calls(hass): grouped_light = add_entities.call_args[0][0][0] grouped_light.hass = hass - with tests.async_mock.patch.object(hass.services, "async_call") as mock_call: + with unittest.mock.patch.object(hass.services, "async_call") as mock_call: await grouped_light.async_turn_on(brightness=150, four_oh_four="404") data = {ATTR_ENTITY_ID: ["light.test1", "light.test2"], ATTR_BRIGHTNESS: 150} mock_call.assert_called_once_with( diff --git a/tests/components/group/test_notify.py b/tests/components/group/test_notify.py index 05a23fa4e7a..a0f210c68a2 100644 --- a/tests/components/group/test_notify.py +++ b/tests/components/group/test_notify.py @@ -1,5 +1,6 @@ """The tests for the notify.group platform.""" from os import path +from unittest.mock import MagicMock, patch from homeassistant import config as hass_config import homeassistant.components.demo.notify as demo @@ -8,8 +9,6 @@ import homeassistant.components.group.notify as group import homeassistant.components.notify as notify from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, patch - async def test_send_message_with_data(hass): """Test sending a message with to a notify group.""" diff --git a/tests/components/group/test_reproduce_state.py b/tests/components/group/test_reproduce_state.py index 13422cd0826..58bbc94876e 100644 --- a/tests/components/group/test_reproduce_state.py +++ b/tests/components/group/test_reproduce_state.py @@ -1,12 +1,11 @@ """The tests for reproduction of state.""" from asyncio import Future +from unittest.mock import patch from homeassistant.components.group.reproduce_state import async_reproduce_states from homeassistant.core import Context, State -from tests.async_mock import patch - async def test_reproduce_group(hass): """Test reproduce_state with group.""" diff --git a/tests/components/guardian/conftest.py b/tests/components/guardian/conftest.py index cfa9174ef57..1a83222c575 100644 --- a/tests/components/guardian/conftest.py +++ b/tests/components/guardian/conftest.py @@ -1,7 +1,7 @@ """Define fixtures for Elexa Guardian tests.""" -import pytest +from unittest.mock import patch -from tests.async_mock import patch +import pytest @pytest.fixture() diff --git a/tests/components/guardian/test_config_flow.py b/tests/components/guardian/test_config_flow.py index db0cf877d37..e9b53b4e629 100644 --- a/tests/components/guardian/test_config_flow.py +++ b/tests/components/guardian/test_config_flow.py @@ -1,4 +1,6 @@ """Define tests for the Elexa Guardian config flow.""" +from unittest.mock import patch + from aioguardian.errors import GuardianError from homeassistant import data_entry_flow @@ -10,7 +12,6 @@ from homeassistant.components.guardian.config_flow import ( from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/hangouts/test_config_flow.py b/tests/components/hangouts/test_config_flow.py index 9cdb5799951..93f909d3bd4 100644 --- a/tests/components/hangouts/test_config_flow.py +++ b/tests/components/hangouts/test_config_flow.py @@ -1,11 +1,11 @@ """Tests for the Google Hangouts config flow.""" +from unittest.mock import patch + from homeassistant import data_entry_flow from homeassistant.components.hangouts import config_flow from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from tests.async_mock import patch - EMAIL = "test@test.com" PASSWORD = "1232456" diff --git a/tests/components/harmony/test_config_flow.py b/tests/components/harmony/test_config_flow.py index 994188eb62a..f43c9f6b478 100644 --- a/tests/components/harmony/test_config_flow.py +++ b/tests/components/harmony/test_config_flow.py @@ -1,10 +1,11 @@ """Test the Logitech Harmony Hub config flow.""" +from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch + from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.harmony.config_flow import CannotConnect from homeassistant.components.harmony.const import DOMAIN, PREVIOUS_ACTIVE_ACTIVITY from homeassistant.const import CONF_HOST, CONF_NAME -from tests.async_mock import AsyncMock, MagicMock, PropertyMock, patch from tests.common import MockConfigEntry diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index f52a825ca29..1442d133f1e 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -1,5 +1,6 @@ """Fixtures for Hass.io.""" import os +from unittest.mock import Mock, patch import pytest @@ -9,8 +10,6 @@ from homeassistant.setup import async_setup_component from . import HASSIO_TOKEN -from tests.async_mock import Mock, patch - @pytest.fixture def hassio_env(): diff --git a/tests/components/hassio/test_addon_panel.py b/tests/components/hassio/test_addon_panel.py index 9b11fd6dfc2..3f6db4dc430 100644 --- a/tests/components/hassio/test_addon_panel.py +++ b/tests/components/hassio/test_addon_panel.py @@ -1,10 +1,10 @@ """Test add-on panel.""" +from unittest.mock import patch + import pytest from homeassistant.setup import async_setup_component -from tests.async_mock import patch - @pytest.fixture(autouse=True) def mock_all(aioclient_mock): diff --git a/tests/components/hassio/test_auth.py b/tests/components/hassio/test_auth.py index c5ac9df74b7..5f27c06a190 100644 --- a/tests/components/hassio/test_auth.py +++ b/tests/components/hassio/test_auth.py @@ -1,8 +1,8 @@ """The tests for the hassio component.""" -from homeassistant.auth.providers.homeassistant import InvalidAuth +from unittest.mock import Mock, patch -from tests.async_mock import Mock, patch +from homeassistant.auth.providers.homeassistant import InvalidAuth async def test_auth_success(hass, hassio_client_supervisor): diff --git a/tests/components/hassio/test_discovery.py b/tests/components/hassio/test_discovery.py index 3bb97a6662e..c23ee40de6e 100644 --- a/tests/components/hassio/test_discovery.py +++ b/tests/components/hassio/test_discovery.py @@ -1,10 +1,10 @@ """Test config flow.""" +from unittest.mock import Mock, patch + from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, patch - async def test_hassio_discovery_startup(hass, aioclient_mock, hassio_client): """Test startup and discovery after event.""" diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index ea087cdc620..2ec964d8e8b 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -1,12 +1,11 @@ """The tests for the hassio component.""" import asyncio +from unittest.mock import patch import pytest from homeassistant.components.hassio.http import _need_auth -from tests.async_mock import patch - async def test_forward_request(hassio_client, aioclient_mock): """Test fetching normal path.""" diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 214551bc3b7..7ed24dca457 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -1,5 +1,6 @@ """The tests for the hassio component.""" import os +from unittest.mock import patch import pytest @@ -8,8 +9,6 @@ from homeassistant.components import frontend from homeassistant.components.hassio import STORAGE_KEY from homeassistant.setup import async_setup_component -from tests.async_mock import patch - MOCK_ENVIRON = {"HASSIO": "127.0.0.1", "HASSIO_TOKEN": "abcdefgh"} diff --git a/tests/components/hassio/test_system_health.py b/tests/components/hassio/test_system_health.py index cd6cb2d939f..88eb7ea20f8 100644 --- a/tests/components/hassio/test_system_health.py +++ b/tests/components/hassio/test_system_health.py @@ -1,6 +1,7 @@ """Test hassio system health.""" import asyncio import os +from unittest.mock import patch from aiohttp import ClientError @@ -8,7 +9,6 @@ from homeassistant.setup import async_setup_component from .test_init import MOCK_ENVIRON -from tests.async_mock import patch from tests.common import get_system_health_info diff --git a/tests/components/hddtemp/test_sensor.py b/tests/components/hddtemp/test_sensor.py index 4062d737ea2..43350c45570 100644 --- a/tests/components/hddtemp/test_sensor.py +++ b/tests/components/hddtemp/test_sensor.py @@ -1,11 +1,11 @@ """The tests for the hddtemp platform.""" import socket import unittest +from unittest.mock import patch from homeassistant.const import TEMP_CELSIUS from homeassistant.setup import setup_component -from tests.async_mock import patch from tests.common import get_test_home_assistant VALID_CONFIG_MINIMAL = {"sensor": {"platform": "hddtemp"}} diff --git a/tests/components/heos/conftest.py b/tests/components/heos/conftest.py index 86be36e8188..fa7615e2de8 100644 --- a/tests/components/heos/conftest.py +++ b/tests/components/heos/conftest.py @@ -1,5 +1,6 @@ """Configuration for HEOS tests.""" from typing import Dict, Sequence +from unittest.mock import Mock, patch as patch from pyheos import Dispatcher, Heos, HeosPlayer, HeosSource, InputSource, const import pytest @@ -8,7 +9,6 @@ from homeassistant.components import ssdp from homeassistant.components.heos import DOMAIN from homeassistant.const import CONF_HOST -from tests.async_mock import Mock, patch as patch from tests.common import MockConfigEntry diff --git a/tests/components/heos/test_config_flow.py b/tests/components/heos/test_config_flow.py index bcc2ed67b29..e62578e5108 100644 --- a/tests/components/heos/test_config_flow.py +++ b/tests/components/heos/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Heos config flow module.""" +from unittest.mock import patch from urllib.parse import urlparse from pyheos import HeosError @@ -10,8 +11,6 @@ from homeassistant.components.heos.const import DATA_DISCOVERED_HOSTS, DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP from homeassistant.const import CONF_HOST -from tests.async_mock import patch - async def test_flow_aborts_already_setup(hass, config_entry): """Test flow aborts when entry already setup.""" diff --git a/tests/components/heos/test_init.py b/tests/components/heos/test_init.py index a32ea5dd08c..6edbf7d8543 100644 --- a/tests/components/heos/test_init.py +++ b/tests/components/heos/test_init.py @@ -1,5 +1,6 @@ """Tests for the init module.""" import asyncio +from unittest.mock import Mock, patch from pyheos import CommandFailedError, HeosError, const import pytest @@ -19,8 +20,6 @@ from homeassistant.const import CONF_HOST from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, patch - async def test_async_setup_creates_entry(hass, config): """Test component setup creates entry from config.""" diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index 386dbbdf0ee..b2fcef715f1 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -1,5 +1,6 @@ """The test for the here_travel_time sensor platform.""" import logging +from unittest.mock import patch import urllib import herepy @@ -42,7 +43,6 @@ from homeassistant.const import ATTR_ICON, EVENT_HOMEASSISTANT_START from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import async_fire_time_changed, load_fixture DOMAIN = "sensor" diff --git a/tests/components/hisense_aehw4a1/test_init.py b/tests/components/hisense_aehw4a1/test_init.py index bf3c9d2c6a4..4add153ee94 100644 --- a/tests/components/hisense_aehw4a1/test_init.py +++ b/tests/components/hisense_aehw4a1/test_init.py @@ -1,12 +1,12 @@ """Tests for the Hisense AEH-W4A1 init file.""" +from unittest.mock import patch + from pyaehw4a1 import exceptions from homeassistant import config_entries, data_entry_flow from homeassistant.components import hisense_aehw4a1 from homeassistant.setup import async_setup_component -from tests.async_mock import patch - async def test_creating_entry_sets_up_climate_discovery(hass): """Test setting up Hisense AEH-W4A1 loads the climate component.""" diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 0b35d5194fb..87e18305b8a 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -4,6 +4,7 @@ from copy import copy from datetime import timedelta import json import unittest +from unittest.mock import patch, sentinel from homeassistant.components import history, recorder from homeassistant.components.recorder.models import process_timestamp @@ -12,7 +13,6 @@ from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component, setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch, sentinel from tests.common import ( get_test_home_assistant, init_recorder_component, diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index db6d7476912..62e3959f4ad 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -3,6 +3,7 @@ from datetime import datetime, timedelta from os import path import unittest +from unittest.mock import patch import pytest import pytz @@ -16,7 +17,6 @@ from homeassistant.helpers.template import Template from homeassistant.setup import async_setup_component, setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import get_test_home_assistant, init_recorder_component diff --git a/tests/components/hlk_sw16/test_config_flow.py b/tests/components/hlk_sw16/test_config_flow.py index ea637c805cd..6a13fae70dc 100644 --- a/tests/components/hlk_sw16/test_config_flow.py +++ b/tests/components/hlk_sw16/test_config_flow.py @@ -1,11 +1,10 @@ """Test the Hi-Link HLK-SW16 config flow.""" import asyncio +from unittest.mock import patch from homeassistant import config_entries, setup from homeassistant.components.hlk_sw16.const import DOMAIN -from tests.async_mock import patch - class MockSW16Client: """Class to mock the SW16Client client.""" diff --git a/tests/components/home_connect/test_config_flow.py b/tests/components/home_connect/test_config_flow.py index 5c94f8b3362..2852dc4fb57 100644 --- a/tests/components/home_connect/test_config_flow.py +++ b/tests/components/home_connect/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Home Connect config flow.""" +from unittest.mock import patch + from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.home_connect.const import ( DOMAIN, @@ -8,8 +10,6 @@ from homeassistant.components.home_connect.const import ( from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow -from tests.async_mock import patch - CLIENT_ID = "1234" CLIENT_SECRET = "5678" diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py index ca2f116a06a..ef830c7ee77 100644 --- a/tests/components/homeassistant/test_init.py +++ b/tests/components/homeassistant/test_init.py @@ -2,6 +2,7 @@ # pylint: disable=protected-access import asyncio import unittest +from unittest.mock import Mock, patch import pytest import voluptuous as vol @@ -32,7 +33,6 @@ from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.helpers import entity from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, patch from tests.common import ( async_capture_events, async_mock_service, diff --git a/tests/components/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py index 8f47d891f9f..30985432718 100644 --- a/tests/components/homeassistant/test_scene.py +++ b/tests/components/homeassistant/test_scene.py @@ -1,4 +1,6 @@ """Test Home Assistant scenes.""" +from unittest.mock import patch + import pytest import voluptuous as vol @@ -6,7 +8,6 @@ from homeassistant.components.homeassistant import scene as ha_scene from homeassistant.components.homeassistant.scene import EVENT_SCENE_RELOADED from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import async_mock_service diff --git a/tests/components/homeassistant/triggers/test_homeassistant.py b/tests/components/homeassistant/triggers/test_homeassistant.py index 9272c3620af..7ff7e566db0 100644 --- a/tests/components/homeassistant/triggers/test_homeassistant.py +++ b/tests/components/homeassistant/triggers/test_homeassistant.py @@ -1,9 +1,10 @@ """The tests for the Event automation.""" +from unittest.mock import AsyncMock, patch + import homeassistant.components.automation as automation from homeassistant.core import CoreState from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock, patch from tests.common import async_mock_service diff --git a/tests/components/homeassistant/triggers/test_numeric_state.py b/tests/components/homeassistant/triggers/test_numeric_state.py index 326990e12c6..d63cb970f80 100644 --- a/tests/components/homeassistant/triggers/test_numeric_state.py +++ b/tests/components/homeassistant/triggers/test_numeric_state.py @@ -1,5 +1,6 @@ """The tests for numeric state automation.""" from datetime import timedelta +from unittest.mock import patch import pytest import voluptuous as vol @@ -13,7 +14,6 @@ from homeassistant.core import Context from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import ( assert_setup_component, async_fire_time_changed, diff --git a/tests/components/homeassistant/triggers/test_state.py b/tests/components/homeassistant/triggers/test_state.py index 61fa991e0f4..dd98dbc429c 100644 --- a/tests/components/homeassistant/triggers/test_state.py +++ b/tests/components/homeassistant/triggers/test_state.py @@ -1,5 +1,6 @@ """The test for state automation.""" from datetime import timedelta +from unittest.mock import patch import pytest @@ -10,7 +11,6 @@ from homeassistant.core import Context from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import ( assert_setup_component, async_fire_time_changed, diff --git a/tests/components/homeassistant/triggers/test_time.py b/tests/components/homeassistant/triggers/test_time.py index a37be71102d..5805aa07fe3 100644 --- a/tests/components/homeassistant/triggers/test_time.py +++ b/tests/components/homeassistant/triggers/test_time.py @@ -1,5 +1,6 @@ """The tests for the time automation.""" from datetime import timedelta +from unittest.mock import Mock, patch import pytest import voluptuous as vol @@ -10,7 +11,6 @@ from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, SERVICE_TURN_ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import Mock, patch from tests.common import ( assert_setup_component, async_fire_time_changed, diff --git a/tests/components/homeassistant/triggers/test_time_pattern.py b/tests/components/homeassistant/triggers/test_time_pattern.py index f428bcf29bc..147bb388fed 100644 --- a/tests/components/homeassistant/triggers/test_time_pattern.py +++ b/tests/components/homeassistant/triggers/test_time_pattern.py @@ -1,5 +1,6 @@ """The tests for the time_pattern automation.""" from datetime import timedelta +from unittest.mock import patch import pytest import voluptuous as vol @@ -10,7 +11,6 @@ from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_O from homeassistant.setup import async_setup_component 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 diff --git a/tests/components/homekit/common.py b/tests/components/homekit/common.py index 4ad5f60551d..20aa0e04c2b 100644 --- a/tests/components/homekit/common.py +++ b/tests/components/homekit/common.py @@ -1,5 +1,5 @@ """Collection of fixtures and functions for the HomeKit tests.""" -from tests.async_mock import Mock, patch +from unittest.mock import Mock, patch EMPTY_8_6_JPEG = b"empty_8_6" diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py index 0cb31e1b701..ac51c4e6368 100644 --- a/tests/components/homekit/conftest.py +++ b/tests/components/homekit/conftest.py @@ -1,12 +1,12 @@ """HomeKit session fixtures.""" +from unittest.mock import patch + from pyhap.accessory_driver import AccessoryDriver import pytest from homeassistant.components.homekit.const import EVENT_HOMEKIT_CHANGED from homeassistant.core import callback as ha_callback -from tests.async_mock import patch - @pytest.fixture def hk_driver(loop): diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 91f02522126..886123062c4 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -3,6 +3,7 @@ This includes tests for all mock object types. """ from datetime import timedelta +from unittest.mock import Mock, patch import pytest @@ -46,7 +47,6 @@ from homeassistant.const import ( from homeassistant.helpers.event import TRACK_STATE_CHANGE_CALLBACKS import homeassistant.util.dt as dt_util -from tests.async_mock import Mock, patch from tests.common import async_fire_time_changed, async_mock_service diff --git a/tests/components/homekit/test_aidmanager.py b/tests/components/homekit/test_aidmanager.py index b7c12e86443..df1bb14dd9e 100644 --- a/tests/components/homekit/test_aidmanager.py +++ b/tests/components/homekit/test_aidmanager.py @@ -1,5 +1,6 @@ """Tests for the HomeKit AID manager.""" import os +from unittest.mock import patch from fnvhash import fnv1a_32 import pytest @@ -12,7 +13,6 @@ from homeassistant.components.homekit.aidmanager import ( from homeassistant.helpers import device_registry from homeassistant.helpers.storage import STORAGE_DIR -from tests.async_mock import patch from tests.common import MockConfigEntry, mock_device_registry, mock_registry diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index 59d65977066..a32de91cebd 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -1,4 +1,6 @@ """Test the HomeKit config flow.""" +from unittest.mock import patch + import pytest from homeassistant import config_entries, data_entry_flow, setup @@ -6,7 +8,6 @@ from homeassistant.components.homekit.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_NAME, CONF_PORT -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index ea91733fdab..314d0516223 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -1,4 +1,6 @@ """Package to test the get_accessory method.""" +from unittest.mock import Mock, patch + import pytest import homeassistant.components.climate as climate @@ -31,8 +33,6 @@ from homeassistant.const import ( ) from homeassistant.core import State -from tests.async_mock import Mock, patch - def test_not_supported(caplog): """Test if none is returned if entity isn't supported.""" diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 8b79c3f6c58..0f9b458cdde 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -1,6 +1,7 @@ """Tests for the HomeKit component.""" import os from typing import Dict +from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch from pyhap.accessory import Accessory import pytest @@ -66,7 +67,6 @@ from homeassistant.util import json as json_util from .util import PATH_HOMEKIT, async_init_entry, async_init_integration -from tests.async_mock import ANY, AsyncMock, MagicMock, Mock, patch from tests.common import MockConfigEntry, mock_device_registry, mock_registry from tests.components.homekit.common import patch_debounce diff --git a/tests/components/homekit/test_img_util.py b/tests/components/homekit/test_img_util.py index 728bb8847ff..45af8e6b1e6 100644 --- a/tests/components/homekit/test_img_util.py +++ b/tests/components/homekit/test_img_util.py @@ -1,4 +1,6 @@ """Test HomeKit img_util module.""" +from unittest.mock import patch + from homeassistant.components.camera import Image from homeassistant.components.homekit.img_util import ( TurboJPEGSingleton, @@ -7,8 +9,6 @@ from homeassistant.components.homekit.img_util import ( from .common import EMPTY_8_6_JPEG, mock_turbo_jpeg -from tests.async_mock import patch - EMPTY_16_12_JPEG = b"empty_16_12" diff --git a/tests/components/homekit/test_init.py b/tests/components/homekit/test_init.py index 72670667fa7..6643ae9ae18 100644 --- a/tests/components/homekit/test_init.py +++ b/tests/components/homekit/test_init.py @@ -1,4 +1,6 @@ """Test HomeKit initialization.""" +from unittest.mock import patch + from homeassistant.components import logbook from homeassistant.components.homekit.const import ( ATTR_DISPLAY_NAME, @@ -9,7 +11,6 @@ from homeassistant.components.homekit.const import ( from homeassistant.const import ATTR_ENTITY_ID, ATTR_SERVICE from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.components.logbook.test_init import MockLazyEventPartialState diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py index 6386b1d8e69..804e03a4e6c 100644 --- a/tests/components/homekit/test_type_cameras.py +++ b/tests/components/homekit/test_type_cameras.py @@ -1,5 +1,6 @@ """Test different accessory types: Camera.""" +from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch from uuid import UUID from pyhap.accessory_driver import AccessoryDriver @@ -34,8 +35,6 @@ from homeassistant.setup import async_setup_component from .common import mock_turbo_jpeg -from tests.async_mock import AsyncMock, MagicMock, PropertyMock, patch - MOCK_START_STREAM_TLV = "ARUCAQEBEDMD1QMXzEaatnKSQ2pxovYCNAEBAAIJAQECAgECAwEAAwsBAgAFAgLQAgMBHgQXAQFjAgQ768/RAwIrAQQEAAAAPwUCYgUDLAEBAwIMAQEBAgEAAwECBAEUAxYBAW4CBCzq28sDAhgABAQAAKBABgENBAEA" MOCK_END_POINTS_TLV = "ARAzA9UDF8xGmrZykkNqcaL2AgEAAxoBAQACDTE5Mi4xNjguMjA4LjUDAi7IBAKkxwQlAQEAAhDN0+Y0tZ4jzoO0ske9UsjpAw6D76oVXnoi7DbawIG4CwUlAQEAAhCyGcROB8P7vFRDzNF2xrK1Aw6NdcLugju9yCfkWVSaVAYEDoAsAAcEpxV8AA==" MOCK_START_STREAM_SESSION_UUID = UUID("3303d503-17cc-469a-b672-92436a71a2f6") diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index a25567b7004..bc1bac11844 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -1,5 +1,6 @@ """Test different accessory types: Fans.""" from collections import namedtuple +from unittest.mock import Mock from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE import pytest @@ -32,7 +33,6 @@ from homeassistant.const import ( from homeassistant.core import CoreState from homeassistant.helpers import entity_registry -from tests.async_mock import Mock from tests.common import async_mock_service from tests.components.homekit.common import patch_debounce diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index acb45bca85f..ce17cf7ea07 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -1,4 +1,6 @@ """Test different accessory types: Thermostats.""" +from unittest.mock import patch + from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE import pytest @@ -61,7 +63,6 @@ from homeassistant.const import ( from homeassistant.core import CoreState from homeassistant.helpers import entity_registry -from tests.async_mock import patch from tests.common import async_mock_service diff --git a/tests/components/homekit/util.py b/tests/components/homekit/util.py index a9def6d02f7..8555be00aa7 100644 --- a/tests/components/homekit/util.py +++ b/tests/components/homekit/util.py @@ -1,10 +1,11 @@ """Test util for the homekit integration.""" +from unittest.mock import patch + from homeassistant.components.homekit.const import DOMAIN from homeassistant.const import CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant -from tests.async_mock import patch from tests.common import MockConfigEntry PATH_HOMEKIT = "homeassistant.components.homekit" diff --git a/tests/components/homekit_controller/conftest.py b/tests/components/homekit_controller/conftest.py index cb75fc205e2..26adb25df21 100644 --- a/tests/components/homekit_controller/conftest.py +++ b/tests/components/homekit_controller/conftest.py @@ -1,13 +1,13 @@ """HomeKit controller session fixtures.""" import datetime from unittest import mock +import unittest.mock from aiohomekit.testing import FakeController import pytest import homeassistant.util.dt as dt_util -import tests.async_mock from tests.components.light.conftest import mock_light_profiles # noqa @@ -25,5 +25,5 @@ def utcnow(request): def controller(hass): """Replace aiohomekit.Controller with an instance of aiohomekit.testing.FakeController.""" instance = FakeController() - with tests.async_mock.patch("aiohomekit.Controller", return_value=instance): + with unittest.mock.patch("aiohomekit.Controller", return_value=instance): yield instance diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 72a8133159d..9cc785f85fb 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -1,5 +1,7 @@ """Tests for homekit_controller config flow.""" from unittest import mock +import unittest.mock +from unittest.mock import patch import aiohomekit from aiohomekit.model import Accessories, Accessory @@ -10,8 +12,6 @@ import pytest from homeassistant.components.homekit_controller import config_flow from homeassistant.helpers import device_registry -import tests.async_mock -from tests.async_mock import patch from tests.common import MockConfigEntry, mock_device_registry PAIRING_START_FORM_ERRORS = [ @@ -71,15 +71,15 @@ def _setup_flow_handler(hass, pairing=None): flow.hass = hass flow.context = {} - finish_pairing = tests.async_mock.AsyncMock(return_value=pairing) + finish_pairing = unittest.mock.AsyncMock(return_value=pairing) discovery = mock.Mock() discovery.device_id = "00:00:00:00:00:00" - discovery.start_pairing = tests.async_mock.AsyncMock(return_value=finish_pairing) + discovery.start_pairing = unittest.mock.AsyncMock(return_value=finish_pairing) flow.controller = mock.Mock() flow.controller.pairings = {} - flow.controller.find_ip_by_device_id = tests.async_mock.AsyncMock( + flow.controller.find_ip_by_device_id = unittest.mock.AsyncMock( return_value=discovery ) @@ -475,7 +475,7 @@ async def test_pair_abort_errors_on_finish(hass, controller, exception, expected # User initiates pairing - this triggers the device to show a pairing code # and then HA to show a pairing form - finish_pairing = tests.async_mock.AsyncMock(side_effect=exception("error")) + finish_pairing = unittest.mock.AsyncMock(side_effect=exception("error")) with patch.object(device, "start_pairing", return_value=finish_pairing): result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -515,7 +515,7 @@ async def test_pair_form_errors_on_finish(hass, controller, exception, expected) # User initiates pairing - this triggers the device to show a pairing code # and then HA to show a pairing form - finish_pairing = tests.async_mock.AsyncMock(side_effect=exception("error")) + finish_pairing = unittest.mock.AsyncMock(side_effect=exception("error")) with patch.object(device, "start_pairing", return_value=finish_pairing): result = await hass.config_entries.flow.async_configure(result["flow_id"]) diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py index 8bf792c259f..b05683d2361 100644 --- a/tests/components/homematicip_cloud/conftest.py +++ b/tests/components/homematicip_cloud/conftest.py @@ -1,4 +1,6 @@ """Initializer helpers for HomematicIP fake server.""" +from unittest.mock import AsyncMock, MagicMock, Mock, patch + from homematicip.aio.auth import AsyncAuth from homematicip.aio.connection import AsyncConnection from homematicip.aio.home import AsyncHome @@ -22,7 +24,6 @@ from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .helper import AUTH_TOKEN, HAPID, HAPPIN, HomeFactory -from tests.async_mock import AsyncMock, MagicMock, Mock, patch from tests.common import MockConfigEntry from tests.components.light.conftest import mock_light_profiles # noqa diff --git a/tests/components/homematicip_cloud/helper.py b/tests/components/homematicip_cloud/helper.py index ca7d8862756..8da5e4861c0 100644 --- a/tests/components/homematicip_cloud/helper.py +++ b/tests/components/homematicip_cloud/helper.py @@ -1,5 +1,6 @@ """Helper for HomematicIP Cloud Tests.""" import json +from unittest.mock import Mock, patch from homematicip.aio.class_maps import ( TYPE_CLASS_MAP, @@ -21,7 +22,6 @@ from homeassistant.components.homematicip_cloud.hap import HomematicipHAP from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, patch from tests.common import load_fixture HAPID = "3014F7110000000000000001" diff --git a/tests/components/homematicip_cloud/test_config_flow.py b/tests/components/homematicip_cloud/test_config_flow.py index e9ecab2dbfb..0b573e66b1d 100644 --- a/tests/components/homematicip_cloud/test_config_flow.py +++ b/tests/components/homematicip_cloud/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for HomematicIP Cloud config flow.""" +from unittest.mock import patch + from homeassistant.components.homematicip_cloud.const import ( DOMAIN as HMIPC_DOMAIN, HMIPC_AUTHTOKEN, @@ -7,7 +9,6 @@ from homeassistant.components.homematicip_cloud.const import ( HMIPC_PIN, ) -from tests.async_mock import patch from tests.common import MockConfigEntry DEFAULT_CONFIG = {HMIPC_HAPID: "ABC123", HMIPC_PIN: "123", HMIPC_NAME: "hmip"} diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 0e69a67cdbf..850b9e7ad82 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -1,4 +1,6 @@ """Common tests for HomematicIP devices.""" +from unittest.mock import patch + from homematicip.base.enums import EventType from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN @@ -13,8 +15,6 @@ from .helper import ( get_and_check_entity_basics, ) -from tests.async_mock import patch - async def test_hmip_load_all_supported_devices(hass, default_mock_hap_factory): """Ensure that all supported devices could be loaded.""" diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index 2a4833553d2..6c95017a635 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -1,5 +1,7 @@ """Test HomematicIP Cloud accesspoint.""" +from unittest.mock import Mock, patch + from homematicip.aio.auth import AsyncAuth from homematicip.base.base_connection import HmipConnectionError import pytest @@ -21,8 +23,6 @@ from homeassistant.exceptions import ConfigEntryNotReady from .helper import HAPID, HAPPIN -from tests.async_mock import Mock, patch - async def test_auth_setup(hass): """Test auth setup for client registration.""" diff --git a/tests/components/homematicip_cloud/test_init.py b/tests/components/homematicip_cloud/test_init.py index 46059f12d00..250cba81637 100644 --- a/tests/components/homematicip_cloud/test_init.py +++ b/tests/components/homematicip_cloud/test_init.py @@ -1,5 +1,7 @@ """Test HomematicIP Cloud setup process.""" +from unittest.mock import AsyncMock, Mock, patch + from homematicip.base.base_connection import HmipConnectionError from homeassistant.components.homematicip_cloud.const import ( @@ -20,7 +22,6 @@ from homeassistant.config_entries import ( from homeassistant.const import CONF_NAME from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock, Mock, patch from tests.common import MockConfigEntry diff --git a/tests/components/html5/test_notify.py b/tests/components/html5/test_notify.py index d3faa761435..4a62eb76c27 100644 --- a/tests/components/html5/test_notify.py +++ b/tests/components/html5/test_notify.py @@ -1,5 +1,6 @@ """Test HTML5 notify platform.""" import json +from unittest.mock import MagicMock, mock_open, patch from aiohttp.hdrs import AUTHORIZATION @@ -8,8 +9,6 @@ from homeassistant.const import HTTP_INTERNAL_SERVER_ERROR from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, mock_open, patch - CONFIG_FILE = "file.conf" VAPID_CONF = { diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index e3274ddfa7d..71c01630a67 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -1,6 +1,7 @@ """The tests for the Home Assistant HTTP component.""" from datetime import timedelta from ipaddress import ip_network +from unittest.mock import patch from aiohttp import BasicAuth, web from aiohttp.web_exceptions import HTTPUnauthorized @@ -14,8 +15,6 @@ from homeassistant.setup import async_setup_component from . import HTTP_HEADER_HA_AUTH, mock_real_ip -from tests.async_mock import patch - API_PASSWORD = "test-password" # Don't add 127.0.0.1/::1 as trusted, as it may interfere with other test cases diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index 993ec708c18..76f5f94a2ed 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -2,6 +2,7 @@ # pylint: disable=protected-access from ipaddress import ip_address import os +from unittest.mock import Mock, mock_open, patch from aiohttp import web from aiohttp.web_exceptions import HTTPUnauthorized @@ -23,7 +24,6 @@ from homeassistant.setup import async_setup_component from . import mock_real_ip -from tests.async_mock import Mock, mock_open, patch from tests.common import async_mock_service SUPERVISOR_IP = "1.2.3.4" diff --git a/tests/components/http/test_cors.py b/tests/components/http/test_cors.py index 191cdb0ba49..04447191fd5 100644 --- a/tests/components/http/test_cors.py +++ b/tests/components/http/test_cors.py @@ -1,5 +1,6 @@ """Test cors for the HTTP component.""" from pathlib import Path +from unittest.mock import patch from aiohttp import web from aiohttp.hdrs import ( @@ -18,8 +19,6 @@ from homeassistant.setup import async_setup_component from . import HTTP_HEADER_HA_AUTH -from tests.async_mock import patch - TRUSTED_ORIGIN = "https://home-assistant.io" diff --git a/tests/components/http/test_data_validator.py b/tests/components/http/test_data_validator.py index c7b5ed42ccd..a6e812ccdfe 100644 --- a/tests/components/http/test_data_validator.py +++ b/tests/components/http/test_data_validator.py @@ -1,12 +1,12 @@ """Test data validator decorator.""" +from unittest.mock import Mock + from aiohttp import web import voluptuous as vol from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator -from tests.async_mock import Mock - async def get_client(aiohttp_client, validator): """Generate a client that hits a view decorated with validator.""" diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 2c95d03a9ef..3dd587cd7a4 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -1,6 +1,7 @@ """The tests for the Home Assistant HTTP component.""" from ipaddress import ip_network import logging +from unittest.mock import Mock, patch import pytest @@ -8,8 +9,6 @@ import homeassistant.components.http as http from homeassistant.setup import async_setup_component from homeassistant.util.ssl import server_context_intermediate, server_context_modern -from tests.async_mock import Mock, patch - @pytest.fixture def mock_stack(): diff --git a/tests/components/http/test_view.py b/tests/components/http/test_view.py index 045f0837983..522344461cb 100644 --- a/tests/components/http/test_view.py +++ b/tests/components/http/test_view.py @@ -1,4 +1,6 @@ """Tests for Home Assistant View.""" +from unittest.mock import AsyncMock, Mock + from aiohttp.web_exceptions import ( HTTPBadRequest, HTTPInternalServerError, @@ -13,8 +15,6 @@ from homeassistant.components.http.view import ( ) from homeassistant.exceptions import ServiceNotFound, Unauthorized -from tests.async_mock import AsyncMock, Mock - @pytest.fixture def mock_request(): diff --git a/tests/components/huawei_lte/test_config_flow.py b/tests/components/huawei_lte/test_config_flow.py index 2547aa3f01f..baffcef3476 100644 --- a/tests/components/huawei_lte/test_config_flow.py +++ b/tests/components/huawei_lte/test_config_flow.py @@ -1,5 +1,7 @@ """Tests for the Huawei LTE config flow.""" +from unittest.mock import patch + from huawei_lte_api.enums.client import ResponseCodeEnum from huawei_lte_api.enums.user import LoginErrorEnum, LoginStateEnum, PasswordTypeEnum import pytest @@ -17,7 +19,6 @@ from homeassistant.const import ( CONF_USERNAME, ) -from tests.async_mock import patch from tests.common import MockConfigEntry FIXTURE_USER_INPUT = { diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py index 55b6443bdfa..fc42babbb35 100644 --- a/tests/components/hue/conftest.py +++ b/tests/components/hue/conftest.py @@ -1,5 +1,6 @@ """Test helpers for Hue.""" from collections import deque +from unittest.mock import AsyncMock, Mock, patch from aiohue.groups import Groups from aiohue.lights import Lights @@ -11,7 +12,6 @@ from homeassistant import config_entries from homeassistant.components import hue from homeassistant.components.hue import sensor_base as hue_sensor_base -from tests.async_mock import AsyncMock, Mock, patch from tests.components.light.conftest import mock_light_profiles # noqa diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index 06dd459723d..3e6465d6bc8 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -1,4 +1,6 @@ """Test Hue bridge.""" +from unittest.mock import AsyncMock, Mock, patch + import pytest from homeassistant import config_entries @@ -10,8 +12,6 @@ from homeassistant.components.hue.const import ( ) from homeassistant.exceptions import ConfigEntryNotReady -from tests.async_mock import AsyncMock, Mock, patch - async def test_bridge_setup(hass): """Test a successful setup.""" diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index a551e623e63..c7dc83183ae 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -1,5 +1,6 @@ """Tests for Philips Hue config flow.""" import asyncio +from unittest.mock import AsyncMock, Mock, patch from aiohttp import client_exceptions import aiohue @@ -11,7 +12,6 @@ from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.hue import config_flow, const -from tests.async_mock import AsyncMock, Mock, patch from tests.common import MockConfigEntry diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index 70a6c3b8756..1f6ba83e2ca 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -1,5 +1,5 @@ """Test Hue setup process.""" -from unittest.mock import Mock +from unittest.mock import AsyncMock, Mock, patch import pytest @@ -7,7 +7,6 @@ from homeassistant import config_entries from homeassistant.components import hue from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock, patch from tests.common import MockConfigEntry diff --git a/tests/components/hue/test_init_multiple_bridges.py b/tests/components/hue/test_init_multiple_bridges.py index 32be7677398..19b4da44a4d 100644 --- a/tests/components/hue/test_init_multiple_bridges.py +++ b/tests/components/hue/test_init_multiple_bridges.py @@ -1,5 +1,7 @@ """Test Hue init with multiple bridges.""" +from unittest.mock import Mock, patch + from aiohue.groups import Groups from aiohue.lights import Lights from aiohue.scenes import Scenes @@ -11,8 +13,6 @@ from homeassistant.components import hue from homeassistant.components.hue import sensor_base as hue_sensor_base from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, patch - async def setup_component(hass): """Hue component.""" diff --git a/tests/components/hue/test_light.py b/tests/components/hue/test_light.py index 2029c32c82f..629a9a4c98b 100644 --- a/tests/components/hue/test_light.py +++ b/tests/components/hue/test_light.py @@ -1,5 +1,6 @@ """Philips Hue lights platform tests.""" import asyncio +from unittest.mock import Mock import aiohue @@ -8,8 +9,6 @@ from homeassistant.components import hue from homeassistant.components.hue import light as hue_light from homeassistant.util import color -from tests.async_mock import Mock - HUE_LIGHT_NS = "homeassistant.components.light.hue." GROUP_RESPONSE = { "1": { diff --git a/tests/components/hue/test_sensor_base.py b/tests/components/hue/test_sensor_base.py index fcf3513bc7c..eb7ece241c3 100644 --- a/tests/components/hue/test_sensor_base.py +++ b/tests/components/hue/test_sensor_base.py @@ -1,5 +1,6 @@ """Philips Hue sensors platform tests.""" import asyncio +from unittest.mock import Mock import aiohue @@ -7,8 +8,6 @@ from homeassistant.components.hue.hue_event import CONF_HUE_EVENT from .conftest import create_mock_bridge, setup_bridge_for_sensors as setup_bridge -from tests.async_mock import Mock - PRESENCE_SENSOR_1_PRESENT = { "state": {"presence": True, "lastupdated": "2019-01-01T01:00:00"}, "swupdate": {"state": "noupdates", "lastinstall": "2019-01-01T00:00:00"}, diff --git a/tests/components/hunterdouglas_powerview/test_config_flow.py b/tests/components/hunterdouglas_powerview/test_config_flow.py index 84457a14a5e..dbcb5d719c9 100644 --- a/tests/components/hunterdouglas_powerview/test_config_flow.py +++ b/tests/components/hunterdouglas_powerview/test_config_flow.py @@ -1,11 +1,11 @@ """Test the Logitech Harmony Hub config flow.""" import asyncio import json +from unittest.mock import AsyncMock, MagicMock, patch from homeassistant import config_entries, setup from homeassistant.components.hunterdouglas_powerview.const import DOMAIN -from tests.async_mock import AsyncMock, MagicMock, patch from tests.common import MockConfigEntry, load_fixture diff --git a/tests/components/hvv_departures/test_config_flow.py b/tests/components/hvv_departures/test_config_flow.py index bd3e955d2d8..a6a927afd2e 100644 --- a/tests/components/hvv_departures/test_config_flow.py +++ b/tests/components/hvv_departures/test_config_flow.py @@ -1,5 +1,6 @@ """Test the HVV Departures config flow.""" import json +from unittest.mock import patch from pygti.exceptions import CannotConnect, InvalidAuth @@ -13,7 +14,6 @@ from homeassistant.components.hvv_departures.const import ( from homeassistant.config_entries import CONN_CLASS_CLOUD_POLL, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_OFFSET, CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture FIXTURE_INIT = json.loads(load_fixture("hvv_departures/init.json")) diff --git a/tests/components/hyperion/__init__.py b/tests/components/hyperion/__init__.py index 31a6c49eeb3..abf9f28a10b 100644 --- a/tests/components/hyperion/__init__.py +++ b/tests/components/hyperion/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations import logging from types import TracebackType from typing import Any, Dict, Optional, Type +from unittest.mock import AsyncMock, Mock, patch # type: ignore[attr-defined] from hyperion import const @@ -13,7 +14,6 @@ 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" diff --git a/tests/components/hyperion/test_config_flow.py b/tests/components/hyperion/test_config_flow.py index 481b7957849..c420fe2fe17 100644 --- a/tests/components/hyperion/test_config_flow.py +++ b/tests/components/hyperion/test_config_flow.py @@ -2,6 +2,7 @@ import logging from typing import Any, Dict, Optional +from unittest.mock import AsyncMock, patch # type: ignore[attr-defined] from hyperion import const @@ -43,7 +44,6 @@ from . import ( create_mock_client, ) -from tests.async_mock import AsyncMock, patch # type: ignore[attr-defined] from tests.common import MockConfigEntry _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/hyperion/test_light.py b/tests/components/hyperion/test_light.py index 4636a9ad59c..ea13aaab5ae 100644 --- a/tests/components/hyperion/test_light.py +++ b/tests/components/hyperion/test_light.py @@ -2,6 +2,7 @@ import logging from types import MappingProxyType from typing import Any, Optional +from unittest.mock import AsyncMock, call, patch # type: ignore[attr-defined] from hyperion import const @@ -55,8 +56,6 @@ from . import ( setup_test_config_entry, ) -from tests.async_mock import AsyncMock, call, patch # type: ignore[attr-defined] - _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/icloud/conftest.py b/tests/components/icloud/conftest.py index 2ed9006cdb6..2230cc2ea32 100644 --- a/tests/components/icloud/conftest.py +++ b/tests/components/icloud/conftest.py @@ -1,7 +1,7 @@ """Configure iCloud tests.""" -import pytest +from unittest.mock import patch -from tests.async_mock import patch +import pytest @pytest.fixture(name="icloud_bypass_setup", autouse=True) diff --git a/tests/components/icloud/test_config_flow.py b/tests/components/icloud/test_config_flow.py index 5ed46a6136f..a774e61f3ec 100644 --- a/tests/components/icloud/test_config_flow.py +++ b/tests/components/icloud/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for the iCloud config flow.""" +from unittest.mock import MagicMock, Mock, patch + from pyicloud.exceptions import PyiCloudFailedLoginException import pytest @@ -20,7 +22,6 @@ from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_US from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers.typing import HomeAssistantType -from tests.async_mock import MagicMock, Mock, patch from tests.common import MockConfigEntry USERNAME = "username@me.com" diff --git a/tests/components/ifttt/test_init.py b/tests/components/ifttt/test_init.py index e5fd8841a2b..d10df2492d4 100644 --- a/tests/components/ifttt/test_init.py +++ b/tests/components/ifttt/test_init.py @@ -1,10 +1,10 @@ """Test the init file of IFTTT.""" +from unittest.mock import patch + from homeassistant import data_entry_flow from homeassistant.components import ifttt from homeassistant.core import callback -from tests.async_mock import patch - async def test_config_flow_registers_webhook(hass, aiohttp_client): """Test setting up IFTTT and sending webhook.""" diff --git a/tests/components/ign_sismologia/test_geo_location.py b/tests/components/ign_sismologia/test_geo_location.py index 5695f26c5aa..1ad5ae2a2b2 100644 --- a/tests/components/ign_sismologia/test_geo_location.py +++ b/tests/components/ign_sismologia/test_geo_location.py @@ -1,5 +1,6 @@ """The tests for the IGN Sismologia (Earthquakes) Feed platform.""" import datetime +from unittest.mock import MagicMock, call, patch from homeassistant.components import geo_location from homeassistant.components.geo_location import ATTR_SOURCE @@ -28,7 +29,6 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import MagicMock, call, patch from tests.common import assert_setup_component, async_fire_time_changed CONFIG = {geo_location.DOMAIN: [{"platform": "ign_sismologia", CONF_RADIUS: 200}]} diff --git a/tests/components/image/test_init.py b/tests/components/image/test_init.py index 277e6f07149..ab73bb71286 100644 --- a/tests/components/image/test_init.py +++ b/tests/components/image/test_init.py @@ -1,6 +1,7 @@ """Test that we can upload images.""" import pathlib import tempfile +from unittest.mock import patch from aiohttp import ClientSession, ClientWebSocketResponse @@ -8,8 +9,6 @@ from homeassistant.components.websocket_api import const as ws_const from homeassistant.setup import async_setup_component from homeassistant.util import dt as util_dt -from tests.async_mock import patch - async def test_upload_image(hass, hass_client, hass_ws_client): """Test we can upload an image.""" diff --git a/tests/components/image_processing/test_init.py b/tests/components/image_processing/test_init.py index c006a262e85..3e6a3cc960f 100644 --- a/tests/components/image_processing/test_init.py +++ b/tests/components/image_processing/test_init.py @@ -1,4 +1,6 @@ """The tests for the image_processing component.""" +from unittest.mock import PropertyMock, patch + import homeassistant.components.http as http import homeassistant.components.image_processing as ip from homeassistant.const import ATTR_ENTITY_PICTURE @@ -6,7 +8,6 @@ from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import setup_component -from tests.async_mock import PropertyMock, patch from tests.common import ( assert_setup_component, get_test_home_assistant, diff --git a/tests/components/influxdb/test_init.py b/tests/components/influxdb/test_init.py index da405e3975b..db22b5c5236 100644 --- a/tests/components/influxdb/test_init.py +++ b/tests/components/influxdb/test_init.py @@ -1,6 +1,7 @@ """The tests for the InfluxDB component.""" from dataclasses import dataclass import datetime +from unittest.mock import MagicMock, Mock, call, patch import pytest @@ -16,8 +17,6 @@ from homeassistant.const import ( from homeassistant.core import split_entity_id from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, Mock, call, patch - INFLUX_PATH = "homeassistant.components.influxdb" INFLUX_CLIENT_PATH = f"{INFLUX_PATH}.InfluxDBClient" BASE_V1_CONFIG = {} diff --git a/tests/components/influxdb/test_sensor.py b/tests/components/influxdb/test_sensor.py index 633f9e891f8..57983d14aba 100644 --- a/tests/components/influxdb/test_sensor.py +++ b/tests/components/influxdb/test_sensor.py @@ -2,6 +2,7 @@ from dataclasses import dataclass from datetime import timedelta from typing import Dict, List, Type +from unittest.mock import MagicMock, patch from influxdb.exceptions import InfluxDBClientError, InfluxDBServerError from influxdb_client.rest import ApiException @@ -24,7 +25,6 @@ from homeassistant.helpers.entity_platform import PLATFORM_NOT_READY_BASE_WAIT_T from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from tests.async_mock import MagicMock, patch from tests.common import async_fire_time_changed INFLUXDB_PATH = "homeassistant.components.influxdb" diff --git a/tests/components/input_boolean/test_init.py b/tests/components/input_boolean/test_init.py index 9911ced28dd..88562678436 100644 --- a/tests/components/input_boolean/test_init.py +++ b/tests/components/input_boolean/test_init.py @@ -1,6 +1,7 @@ """The tests for the input_boolean component.""" # pylint: disable=protected-access import logging +from unittest.mock import patch import pytest @@ -22,7 +23,6 @@ from homeassistant.core import Context, CoreState, State from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import mock_component, mock_restore_cache _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index a336ef82363..f6cff819791 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -1,6 +1,7 @@ """Tests for the Input slider component.""" # pylint: disable=protected-access import datetime +from unittest.mock import patch import pytest import voluptuous as vol @@ -31,7 +32,6 @@ from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from tests.async_mock import patch from tests.common import mock_restore_cache INITIAL_DATE = "2020-01-10" diff --git a/tests/components/input_number/test_init.py b/tests/components/input_number/test_init.py index 8971439de74..28b9d27d23f 100644 --- a/tests/components/input_number/test_init.py +++ b/tests/components/input_number/test_init.py @@ -1,4 +1,7 @@ """The tests for the Input number component.""" +# pylint: disable=protected-access +from unittest.mock import patch + import pytest import voluptuous as vol @@ -21,8 +24,6 @@ from homeassistant.exceptions import Unauthorized from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component -# pylint: disable=protected-access -from tests.async_mock import patch from tests.common import mock_restore_cache diff --git a/tests/components/input_select/test_init.py b/tests/components/input_select/test_init.py index 6f83de340f3..5c470ca5bfc 100644 --- a/tests/components/input_select/test_init.py +++ b/tests/components/input_select/test_init.py @@ -1,4 +1,7 @@ """The tests for the Input select component.""" +# pylint: disable=protected-access +from unittest.mock import patch + import pytest from homeassistant.components.input_select import ( @@ -25,8 +28,6 @@ from homeassistant.helpers import entity_registry from homeassistant.loader import bind_hass from homeassistant.setup import async_setup_component -# pylint: disable=protected-access -from tests.async_mock import patch from tests.common import mock_restore_cache diff --git a/tests/components/input_text/test_init.py b/tests/components/input_text/test_init.py index ed89ccd7087..cc226dc1d87 100644 --- a/tests/components/input_text/test_init.py +++ b/tests/components/input_text/test_init.py @@ -1,4 +1,7 @@ """The tests for the Input text component.""" +# pylint: disable=protected-access +from unittest.mock import patch + import pytest from homeassistant.components.input_text import ( @@ -26,8 +29,6 @@ from homeassistant.helpers import entity_registry from homeassistant.loader import bind_hass from homeassistant.setup import async_setup_component -# pylint: disable=protected-access -from tests.async_mock import patch from tests.common import mock_restore_cache TEST_VAL_MIN = 2 diff --git a/tests/components/insteon/mock_devices.py b/tests/components/insteon/mock_devices.py index f2afd6083a3..7ffb0672161 100644 --- a/tests/components/insteon/mock_devices.py +++ b/tests/components/insteon/mock_devices.py @@ -1,4 +1,6 @@ """Mock devices object to test Insteon.""" +from unittest.mock import AsyncMock, MagicMock + from pyinsteon.address import Address from pyinsteon.device_types import ( GeneralController_MiniRemote_4, @@ -6,8 +8,6 @@ from pyinsteon.device_types import ( SwitchedLightingControl_SwitchLinc, ) -from tests.async_mock import AsyncMock, MagicMock - class MockSwitchLinc(SwitchedLightingControl_SwitchLinc): """Mock SwitchLinc device.""" diff --git a/tests/components/insteon/test_config_flow.py b/tests/components/insteon/test_config_flow.py index 4e060b0d840..f1940b1eb39 100644 --- a/tests/components/insteon/test_config_flow.py +++ b/tests/components/insteon/test_config_flow.py @@ -1,5 +1,7 @@ """Test the config flow for the Insteon integration.""" +from unittest.mock import patch + from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.insteon.config_flow import ( HUB1, @@ -50,7 +52,6 @@ from .const import ( PATCH_CONNECTION, ) -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/insteon/test_init.py b/tests/components/insteon/test_init.py index 73620df6776..01546453868 100644 --- a/tests/components/insteon/test_init.py +++ b/tests/components/insteon/test_init.py @@ -1,5 +1,6 @@ """Test the init file for the Insteon component.""" import asyncio +from unittest.mock import patch from pyinsteon.address import Address @@ -40,7 +41,6 @@ from .const import ( ) from .mock_devices import MockDevices -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index c36a718ffb4..3afa5c14c22 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -1,12 +1,11 @@ """The tests for the integration sensor platform.""" from datetime import timedelta +from unittest.mock import patch from homeassistant.const import ENERGY_KILO_WATT_HOUR, POWER_WATT, TIME_SECONDS from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch - async def test_state(hass): """Test integration sensor state.""" diff --git a/tests/components/ipma/test_config_flow.py b/tests/components/ipma/test_config_flow.py index 98c1667b2f6..493bcfe28cf 100644 --- a/tests/components/ipma/test_config_flow.py +++ b/tests/components/ipma/test_config_flow.py @@ -1,5 +1,7 @@ """Tests for IPMA config flow.""" +from unittest.mock import Mock, patch + from homeassistant.components.ipma import DOMAIN, config_flow from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE from homeassistant.helpers import entity_registry @@ -7,7 +9,6 @@ from homeassistant.setup import async_setup_component from .test_weather import MockLocation -from tests.async_mock import Mock, patch from tests.common import MockConfigEntry, mock_registry diff --git a/tests/components/ipma/test_weather.py b/tests/components/ipma/test_weather.py index 02e46816bbc..7ed1c4d3723 100644 --- a/tests/components/ipma/test_weather.py +++ b/tests/components/ipma/test_weather.py @@ -1,5 +1,6 @@ """The tests for the IPMA weather component.""" from collections import namedtuple +from unittest.mock import patch from homeassistant.components import weather from homeassistant.components.weather import ( @@ -21,7 +22,6 @@ from homeassistant.components.weather import ( from homeassistant.setup import async_setup_component from homeassistant.util.dt import now -from tests.async_mock import patch from tests.common import MockConfigEntry TEST_CONFIG = { diff --git a/tests/components/ipp/test_config_flow.py b/tests/components/ipp/test_config_flow.py index 24ead324243..140570c3c54 100644 --- a/tests/components/ipp/test_config_flow.py +++ b/tests/components/ipp/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for the IPP config flow.""" +from unittest.mock import patch + from homeassistant.components.ipp.const import CONF_BASE_PATH, CONF_UUID, DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SSL @@ -17,7 +19,6 @@ from . import ( mock_connection, ) -from tests.async_mock import patch from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/ipp/test_sensor.py b/tests/components/ipp/test_sensor.py index 1817a66f630..69143faec64 100644 --- a/tests/components/ipp/test_sensor.py +++ b/tests/components/ipp/test_sensor.py @@ -1,5 +1,6 @@ """Tests for the IPP sensor platform.""" from datetime import datetime +from unittest.mock import patch from homeassistant.components.ipp.const import DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -7,7 +8,6 @@ from homeassistant.const import ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util -from tests.async_mock import patch from tests.components.ipp import init_integration, mock_connection from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/iqvia/test_config_flow.py b/tests/components/iqvia/test_config_flow.py index 209f88cf895..5cc71302bc5 100644 --- a/tests/components/iqvia/test_config_flow.py +++ b/tests/components/iqvia/test_config_flow.py @@ -1,9 +1,10 @@ """Define tests for the IQVIA config flow.""" +from unittest.mock import patch + from homeassistant import data_entry_flow from homeassistant.components.iqvia import CONF_ZIP_CODE, DOMAIN from homeassistant.config_entries import SOURCE_USER -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/islamic_prayer_times/test_config_flow.py b/tests/components/islamic_prayer_times/test_config_flow.py index 3b966c9e861..b7a942e4f14 100644 --- a/tests/components/islamic_prayer_times/test_config_flow.py +++ b/tests/components/islamic_prayer_times/test_config_flow.py @@ -1,11 +1,12 @@ """Tests for Islamic Prayer Times config flow.""" +from unittest.mock import patch + import pytest from homeassistant import data_entry_flow from homeassistant.components import islamic_prayer_times from homeassistant.components.islamic_prayer_times.const import CONF_CALC_METHOD, DOMAIN -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/islamic_prayer_times/test_init.py b/tests/components/islamic_prayer_times/test_init.py index fd0de854056..850edc4b76d 100644 --- a/tests/components/islamic_prayer_times/test_init.py +++ b/tests/components/islamic_prayer_times/test_init.py @@ -1,6 +1,7 @@ """Tests for Islamic Prayer Times init.""" from datetime import timedelta +from unittest.mock import patch from prayer_times_calculator.exceptions import InvalidResponseError @@ -16,7 +17,6 @@ from . import ( PRAYER_TIMES_TIMESTAMPS, ) -from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/islamic_prayer_times/test_sensor.py b/tests/components/islamic_prayer_times/test_sensor.py index 13b69207cde..da89436a7e0 100644 --- a/tests/components/islamic_prayer_times/test_sensor.py +++ b/tests/components/islamic_prayer_times/test_sensor.py @@ -1,10 +1,11 @@ """The tests for the Islamic prayer times sensor platform.""" +from unittest.mock import patch + from homeassistant.components import islamic_prayer_times import homeassistant.util.dt as dt_util from . import NOW, PRAYER_TIMES, PRAYER_TIMES_TIMESTAMPS -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/isy994/test_config_flow.py b/tests/components/isy994/test_config_flow.py index 2fe19a0a9fd..c2236006b39 100644 --- a/tests/components/isy994/test_config_flow.py +++ b/tests/components/isy994/test_config_flow.py @@ -1,5 +1,7 @@ """Test the Universal Devices ISY994 config flow.""" +from unittest.mock import patch + from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components import ssdp from homeassistant.components.isy994.config_flow import CannotConnect @@ -17,7 +19,6 @@ from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers.typing import HomeAssistantType -from tests.async_mock import patch from tests.common import MockConfigEntry MOCK_HOSTNAME = "1.1.1.1" diff --git a/tests/components/izone/test_config_flow.py b/tests/components/izone/test_config_flow.py index df5ec18db8a..a548e59930b 100644 --- a/tests/components/izone/test_config_flow.py +++ b/tests/components/izone/test_config_flow.py @@ -1,12 +1,12 @@ """Tests for iZone.""" +from unittest.mock import Mock, patch + import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components.izone.const import DISPATCH_CONTROLLER_DISCOVERED, IZONE -from tests.async_mock import Mock, patch - @pytest.fixture def mock_disco(): diff --git a/tests/components/jewish_calendar/__init__.py b/tests/components/jewish_calendar/__init__.py index c5fc2683923..2d42458cf1b 100644 --- a/tests/components/jewish_calendar/__init__.py +++ b/tests/components/jewish_calendar/__init__.py @@ -2,12 +2,11 @@ from collections import namedtuple from contextlib import contextmanager from datetime import datetime +from unittest.mock import patch from homeassistant.components import jewish_calendar import homeassistant.util.dt as dt_util -from tests.async_mock import patch - _LatLng = namedtuple("_LatLng", ["lat", "lng"]) HDATE_DEFAULT_ALTITUDE = 754 diff --git a/tests/components/juicenet/test_config_flow.py b/tests/components/juicenet/test_config_flow.py index 3a7d2f06c43..817ae04aa79 100644 --- a/tests/components/juicenet/test_config_flow.py +++ b/tests/components/juicenet/test_config_flow.py @@ -1,4 +1,6 @@ """Test the JuiceNet config flow.""" +from unittest.mock import MagicMock, patch + import aiohttp from pyjuicenet import TokenError @@ -6,8 +8,6 @@ from homeassistant import config_entries, setup from homeassistant.components.juicenet.const import DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN -from tests.async_mock import MagicMock, patch - def _mock_juicenet_return_value(get_devices=None): juicenet_mock = MagicMock() diff --git a/tests/components/kira/test_init.py b/tests/components/kira/test_init.py index db8eb6b2456..93f3ee4d82a 100644 --- a/tests/components/kira/test_init.py +++ b/tests/components/kira/test_init.py @@ -3,14 +3,13 @@ import os import shutil import tempfile +from unittest.mock import MagicMock, patch import pytest import homeassistant.components.kira as kira from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, patch - TEST_CONFIG = { kira.DOMAIN: { "sensors": [ diff --git a/tests/components/kira/test_remote.py b/tests/components/kira/test_remote.py index c946823474c..e91cbaca891 100644 --- a/tests/components/kira/test_remote.py +++ b/tests/components/kira/test_remote.py @@ -1,9 +1,9 @@ """The tests for Kira sensor platform.""" import unittest +from unittest.mock import MagicMock from homeassistant.components.kira import remote as kira -from tests.async_mock import MagicMock from tests.common import get_test_home_assistant SERVICE_SEND_COMMAND = "send_command" diff --git a/tests/components/kira/test_sensor.py b/tests/components/kira/test_sensor.py index 21572a8735b..cd4bee60ae6 100644 --- a/tests/components/kira/test_sensor.py +++ b/tests/components/kira/test_sensor.py @@ -1,9 +1,9 @@ """The tests for Kira sensor platform.""" import unittest +from unittest.mock import MagicMock from homeassistant.components.kira import sensor as kira -from tests.async_mock import MagicMock from tests.common import get_test_home_assistant TEST_CONFIG = {kira.DOMAIN: {"sensors": [{"host": "127.0.0.1", "port": 17324}]}} diff --git a/tests/components/kodi/__init__.py b/tests/components/kodi/__init__.py index 31c9dff14ac..bef576dd6bf 100644 --- a/tests/components/kodi/__init__.py +++ b/tests/components/kodi/__init__.py @@ -1,4 +1,6 @@ """Tests for the Kodi integration.""" +from unittest.mock import patch + from homeassistant.components.kodi.const import CONF_WS_PORT, DOMAIN from homeassistant.const import ( CONF_HOST, @@ -11,7 +13,6 @@ from homeassistant.const import ( from .util import MockConnection -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/kodi/test_config_flow.py b/tests/components/kodi/test_config_flow.py index 9e892033786..cba567e5bb5 100644 --- a/tests/components/kodi/test_config_flow.py +++ b/tests/components/kodi/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Kodi config flow.""" +from unittest.mock import AsyncMock, PropertyMock, patch + import pytest from homeassistant import config_entries @@ -21,7 +23,6 @@ from .util import ( get_kodi_connection, ) -from tests.async_mock import AsyncMock, PropertyMock, patch from tests.common import MockConfigEntry diff --git a/tests/components/kodi/test_init.py b/tests/components/kodi/test_init.py index b272e005012..aa206270d35 100644 --- a/tests/components/kodi/test_init.py +++ b/tests/components/kodi/test_init.py @@ -1,11 +1,11 @@ """Test the Kodi integration init.""" +from unittest.mock import patch + from homeassistant.components.kodi.const import DOMAIN from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED from . import init_integration -from tests.async_mock import patch - async def test_unload_entry(hass): """Test successful unload of entry.""" diff --git a/tests/components/konnected/test_config_flow.py b/tests/components/konnected/test_config_flow.py index dfd4600f1fb..4b5cc602f99 100644 --- a/tests/components/konnected/test_config_flow.py +++ b/tests/components/konnected/test_config_flow.py @@ -1,10 +1,11 @@ """Tests for Konnected Alarm Panel config flow.""" +from unittest.mock import patch + import pytest from homeassistant.components import konnected from homeassistant.components.konnected import config_flow -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/konnected/test_init.py b/tests/components/konnected/test_init.py index c198812a82b..91d6633cf1d 100644 --- a/tests/components/konnected/test_init.py +++ b/tests/components/konnected/test_init.py @@ -1,4 +1,6 @@ """Test Konnected setup process.""" +from unittest.mock import patch + import pytest from homeassistant.components import konnected @@ -7,7 +9,6 @@ from homeassistant.config import async_process_ha_core_config from homeassistant.const import HTTP_NOT_FOUND from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/konnected/test_panel.py b/tests/components/konnected/test_panel.py index 21351cb6be3..38507aa973c 100644 --- a/tests/components/konnected/test_panel.py +++ b/tests/components/konnected/test_panel.py @@ -1,5 +1,6 @@ """Test Konnected setup process.""" from datetime import timedelta +from unittest.mock import patch import pytest @@ -7,7 +8,6 @@ from homeassistant.components.konnected import config_flow, panel from homeassistant.setup import async_setup_component from homeassistant.util import utcnow -from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/kulersky/test_config_flow.py b/tests/components/kulersky/test_config_flow.py index 59e3188fd7e..24f3f9a010e 100644 --- a/tests/components/kulersky/test_config_flow.py +++ b/tests/components/kulersky/test_config_flow.py @@ -1,11 +1,11 @@ """Test the Kuler Sky config flow.""" +from unittest.mock import patch + 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.""" diff --git a/tests/components/kulersky/test_light.py b/tests/components/kulersky/test_light.py index 5403f7cedde..fd5db92908b 100644 --- a/tests/components/kulersky/test_light.py +++ b/tests/components/kulersky/test_light.py @@ -1,5 +1,6 @@ """Test the Kuler Sky lights.""" import asyncio +from unittest.mock import MagicMock, patch import pykulersky import pytest @@ -27,7 +28,6 @@ from homeassistant.const import ( ) import homeassistant.util.dt as dt_util -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/lastfm/test_sensor.py b/tests/components/lastfm/test_sensor.py index 82c789415bf..af7a177edb8 100644 --- a/tests/components/lastfm/test_sensor.py +++ b/tests/components/lastfm/test_sensor.py @@ -1,4 +1,6 @@ """Tests for the lastfm sensor.""" +from unittest.mock import patch + from pylast import Track import pytest @@ -6,8 +8,6 @@ from homeassistant.components import sensor from homeassistant.components.lastfm.sensor import STATE_NOT_SCROBBLING from homeassistant.setup import async_setup_component -from tests.async_mock import patch - class MockUser: """Mock user object for pylast.""" diff --git a/tests/components/light/conftest.py b/tests/components/light/conftest.py index 67af99a3d6c..4ce72f441c2 100644 --- a/tests/components/light/conftest.py +++ b/tests/components/light/conftest.py @@ -1,11 +1,11 @@ """Light conftest.""" +from unittest.mock import AsyncMock, patch + import pytest from homeassistant.components.light import Profiles -from tests.async_mock import AsyncMock, patch - @pytest.fixture(autouse=True) def mock_light_profiles(): diff --git a/tests/components/light/test_device_condition.py b/tests/components/light/test_device_condition.py index eea443b7d34..2a877478b1e 100644 --- a/tests/components/light/test_device_condition.py +++ b/tests/components/light/test_device_condition.py @@ -1,5 +1,6 @@ """The test for light device automation.""" from datetime import timedelta +from unittest.mock import patch import pytest @@ -10,7 +11,6 @@ from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import ( MockConfigEntry, async_get_device_automation_capabilities, diff --git a/tests/components/locative/test_init.py b/tests/components/locative/test_init.py index 2fa68778f7e..d183cb9814a 100644 --- a/tests/components/locative/test_init.py +++ b/tests/components/locative/test_init.py @@ -1,4 +1,6 @@ """The tests the for Locative device tracker platform.""" +from unittest.mock import patch + import pytest from homeassistant import data_entry_flow @@ -10,8 +12,6 @@ from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY from homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component -from tests.async_mock import patch - # pylint: disable=redefined-outer-name diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 36d47115acc..8360759d564 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -3,6 +3,7 @@ import collections from datetime import datetime, timedelta import json +from unittest.mock import Mock, patch import pytest import voluptuous as vol @@ -36,7 +37,6 @@ from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component, setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import Mock, patch from tests.common import get_test_home_assistant, init_recorder_component, mock_platform from tests.components.recorder.common import trigger_db_commit diff --git a/tests/components/logentries/test_init.py b/tests/components/logentries/test_init.py index 22cbfb41c76..3ae25d521cb 100644 --- a/tests/components/logentries/test_init.py +++ b/tests/components/logentries/test_init.py @@ -1,13 +1,13 @@ """The tests for the Logentries component.""" +from unittest.mock import MagicMock, call, patch + import pytest import homeassistant.components.logentries as logentries from homeassistant.const import EVENT_STATE_CHANGED, STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, call, patch - async def test_setup_config_full(hass): """Test setup with all data.""" diff --git a/tests/components/logger/test_init.py b/tests/components/logger/test_init.py index 571df44bce5..abb8c9b0de8 100644 --- a/tests/components/logger/test_init.py +++ b/tests/components/logger/test_init.py @@ -1,6 +1,7 @@ """The tests for the Logger component.""" from collections import defaultdict import logging +from unittest.mock import Mock, patch import pytest @@ -8,8 +9,6 @@ from homeassistant.components import logger from homeassistant.components.logger import LOGSEVERITY from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, patch - HASS_NS = "unused.homeassistant" COMPONENTS_NS = f"{HASS_NS}.components" ZONE_NS = f"{COMPONENTS_NS}.zone" diff --git a/tests/components/logi_circle/test_config_flow.py b/tests/components/logi_circle/test_config_flow.py index c743cce7519..28335b93ad9 100644 --- a/tests/components/logi_circle/test_config_flow.py +++ b/tests/components/logi_circle/test_config_flow.py @@ -1,6 +1,6 @@ """Tests for Logi Circle config flow.""" import asyncio -from unittest.mock import Mock, patch +from unittest.mock import AsyncMock, Mock, patch import pytest @@ -13,7 +13,6 @@ from homeassistant.components.logi_circle.config_flow import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock from tests.common import mock_coro diff --git a/tests/components/lovelace/test_dashboard.py b/tests/components/lovelace/test_dashboard.py index b5157dd7c46..dd87e2bc275 100644 --- a/tests/components/lovelace/test_dashboard.py +++ b/tests/components/lovelace/test_dashboard.py @@ -1,11 +1,12 @@ """Test the Lovelace initialization.""" +from unittest.mock import patch + import pytest from homeassistant.components import frontend from homeassistant.components.lovelace import const, dashboard from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import assert_setup_component, async_capture_events diff --git a/tests/components/lovelace/test_resources.py b/tests/components/lovelace/test_resources.py index d32dc9388f1..86c0a052776 100644 --- a/tests/components/lovelace/test_resources.py +++ b/tests/components/lovelace/test_resources.py @@ -1,12 +1,11 @@ """Test Lovelace resources.""" import copy +from unittest.mock import patch import uuid from homeassistant.components.lovelace import dashboard, resources from homeassistant.setup import async_setup_component -from tests.async_mock import patch - RESOURCE_EXAMPLES = [ {"type": "js", "url": "/local/bla.js"}, {"type": "css", "url": "/local/bla.css"}, diff --git a/tests/components/lovelace/test_system_health.py b/tests/components/lovelace/test_system_health.py index b81915083d2..780a0678940 100644 --- a/tests/components/lovelace/test_system_health.py +++ b/tests/components/lovelace/test_system_health.py @@ -1,8 +1,9 @@ """Tests for Lovelace system health.""" +from unittest.mock import patch + from homeassistant.components.lovelace import dashboard from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import get_system_health_info diff --git a/tests/components/luftdaten/test_config_flow.py b/tests/components/luftdaten/test_config_flow.py index f3ee1f6c0f7..4ea55e26aee 100644 --- a/tests/components/luftdaten/test_config_flow.py +++ b/tests/components/luftdaten/test_config_flow.py @@ -1,12 +1,12 @@ """Define tests for the Luftdaten config flow.""" from datetime import timedelta +from unittest.mock import patch from homeassistant import data_entry_flow from homeassistant.components.luftdaten import DOMAIN, config_flow from homeassistant.components.luftdaten.const import CONF_SENSOR_ID from homeassistant.const import CONF_SCAN_INTERVAL, CONF_SHOW_ON_MAP -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/luftdaten/test_init.py b/tests/components/luftdaten/test_init.py index a8ea57cbb6b..ebe5f73669e 100644 --- a/tests/components/luftdaten/test_init.py +++ b/tests/components/luftdaten/test_init.py @@ -1,11 +1,11 @@ """Test the Luftdaten component setup.""" +from unittest.mock import patch + from homeassistant.components import luftdaten from homeassistant.components.luftdaten.const import CONF_SENSOR_ID, DOMAIN from homeassistant.const import CONF_SCAN_INTERVAL, CONF_SHOW_ON_MAP from homeassistant.setup import async_setup_component -from tests.async_mock import patch - async def test_config_with_sensor_passed_to_config_entry(hass): """Test that configured options for a sensor are loaded.""" diff --git a/tests/components/lutron_caseta/test_config_flow.py b/tests/components/lutron_caseta/test_config_flow.py index 4ec5f14237c..69f96ca1133 100644 --- a/tests/components/lutron_caseta/test_config_flow.py +++ b/tests/components/lutron_caseta/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Lutron Caseta config flow.""" +from unittest.mock import AsyncMock, patch + from pylutron_caseta.smartbridge import Smartbridge from homeassistant import config_entries, data_entry_flow @@ -13,7 +15,6 @@ from homeassistant.components.lutron_caseta.const import ( ) from homeassistant.const import CONF_HOST -from tests.async_mock import AsyncMock, patch from tests.common import MockConfigEntry diff --git a/tests/components/manual/test_alarm_control_panel.py b/tests/components/manual/test_alarm_control_panel.py index 9cf76ebbc8f..ee3d97e52d7 100644 --- a/tests/components/manual/test_alarm_control_panel.py +++ b/tests/components/manual/test_alarm_control_panel.py @@ -1,5 +1,6 @@ """The tests for the manual Alarm Control Panel component.""" from datetime import timedelta +from unittest.mock import MagicMock, patch from homeassistant.components import alarm_control_panel from homeassistant.components.demo import alarm_control_panel as demo @@ -17,7 +18,6 @@ from homeassistant.core import CoreState, State from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import MagicMock, patch from tests.common import async_fire_time_changed, mock_component, mock_restore_cache from tests.components.alarm_control_panel import common diff --git a/tests/components/manual_mqtt/test_alarm_control_panel.py b/tests/components/manual_mqtt/test_alarm_control_panel.py index 87a887b0751..9a98af127ea 100644 --- a/tests/components/manual_mqtt/test_alarm_control_panel.py +++ b/tests/components/manual_mqtt/test_alarm_control_panel.py @@ -1,5 +1,6 @@ """The tests for the manual_mqtt Alarm Control Panel component.""" from datetime import timedelta +from unittest.mock import patch from homeassistant.components import alarm_control_panel from homeassistant.const import ( @@ -13,7 +14,6 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import ( assert_setup_component, async_fire_mqtt_message, diff --git a/tests/components/marytts/test_tts.py b/tests/components/marytts/test_tts.py index ce2092dd607..6a09ff405c8 100644 --- a/tests/components/marytts/test_tts.py +++ b/tests/components/marytts/test_tts.py @@ -2,6 +2,7 @@ import asyncio import os import shutil +from unittest.mock import patch from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_ID, @@ -12,7 +13,6 @@ import homeassistant.components.tts as tts from homeassistant.config import async_process_ha_core_config from homeassistant.setup import setup_component -from tests.async_mock import patch from tests.common import assert_setup_component, get_test_home_assistant, mock_service diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index 9434fb1a411..98b54ace01f 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -1,12 +1,11 @@ """Test the base functions of the media player.""" import base64 +from unittest.mock import patch from homeassistant.components import media_player from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.setup import async_setup_component -from tests.async_mock import patch - async def test_get_image(hass, hass_ws_client, caplog): """Test get image via WS command.""" diff --git a/tests/components/media_source/test_init.py b/tests/components/media_source/test_init.py index a891fb0d11d..0dda9f67fbe 100644 --- a/tests/components/media_source/test_init.py +++ b/tests/components/media_source/test_init.py @@ -1,4 +1,6 @@ """Test Media Source initialization.""" +from unittest.mock import patch + import pytest from homeassistant.components import media_source @@ -8,8 +10,6 @@ from homeassistant.components.media_source import const from homeassistant.components.media_source.error import Unresolvable from homeassistant.setup import async_setup_component -from tests.async_mock import patch - async def test_is_media_source_id(): """Test media source validation.""" diff --git a/tests/components/melcloud/test_config_flow.py b/tests/components/melcloud/test_config_flow.py index 1fca3ac877e..dbf7a455791 100644 --- a/tests/components/melcloud/test_config_flow.py +++ b/tests/components/melcloud/test_config_flow.py @@ -1,5 +1,6 @@ """Test the MELCloud config flow.""" import asyncio +from unittest.mock import patch from aiohttp import ClientError, ClientResponseError import pymelcloud @@ -9,7 +10,6 @@ from homeassistant import config_entries from homeassistant.components.melcloud.const import DOMAIN from homeassistant.const import HTTP_FORBIDDEN, HTTP_INTERNAL_SERVER_ERROR -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/melissa/test_climate.py b/tests/components/melissa/test_climate.py index 874cc29ab7a..ccb5f951fa2 100644 --- a/tests/components/melissa/test_climate.py +++ b/tests/components/melissa/test_climate.py @@ -1,5 +1,6 @@ """Test for Melissa climate component.""" import json +from unittest.mock import AsyncMock, Mock, patch from homeassistant.components.climate.const import ( HVAC_MODE_COOL, @@ -15,7 +16,6 @@ from homeassistant.components.melissa import DATA_MELISSA, climate as melissa from homeassistant.components.melissa.climate import MelissaClimate from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from tests.async_mock import AsyncMock, Mock, patch from tests.common import load_fixture _SERIAL = "12345678" diff --git a/tests/components/melissa/test_init.py b/tests/components/melissa/test_init.py index b9e09a5d769..8ac48cbfd5d 100644 --- a/tests/components/melissa/test_init.py +++ b/tests/components/melissa/test_init.py @@ -1,7 +1,7 @@ """The test for the Melissa Climate component.""" -from homeassistant.components import melissa +from unittest.mock import AsyncMock, patch -from tests.async_mock import AsyncMock, patch +from homeassistant.components import melissa VALID_CONFIG = {"melissa": {"username": "********", "password": "********"}} diff --git a/tests/components/met/__init__.py b/tests/components/met/__init__.py index c238fec4cb7..13b186f3b47 100644 --- a/tests/components/met/__init__.py +++ b/tests/components/met/__init__.py @@ -1,8 +1,9 @@ """Tests for Met.no.""" +from unittest.mock import patch + from homeassistant.components.met.const import DOMAIN from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/met/conftest.py b/tests/components/met/conftest.py index 164a8498465..e6b975023d1 100644 --- a/tests/components/met/conftest.py +++ b/tests/components/met/conftest.py @@ -1,7 +1,7 @@ """Fixtures for Met weather testing.""" -import pytest +from unittest.mock import AsyncMock, patch -from tests.async_mock import AsyncMock, patch +import pytest @pytest.fixture diff --git a/tests/components/met/test_config_flow.py b/tests/components/met/test_config_flow.py index 52959428917..622475e8376 100644 --- a/tests/components/met/test_config_flow.py +++ b/tests/components/met/test_config_flow.py @@ -1,10 +1,11 @@ """Tests for Met.no config flow.""" +from unittest.mock import patch + import pytest from homeassistant.components.met.const import DOMAIN, HOME_LOCATION_NAME from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/meteo_france/conftest.py b/tests/components/meteo_france/conftest.py index 06a65b6ba87..0b62a03a53c 100644 --- a/tests/components/meteo_france/conftest.py +++ b/tests/components/meteo_france/conftest.py @@ -1,7 +1,7 @@ """Meteo-France generic test utils.""" -import pytest +from unittest.mock import patch -from tests.async_mock import patch +import pytest @pytest.fixture(autouse=True) diff --git a/tests/components/meteo_france/test_config_flow.py b/tests/components/meteo_france/test_config_flow.py index 6c1d9312f91..0fbe1b5e135 100644 --- a/tests/components/meteo_france/test_config_flow.py +++ b/tests/components/meteo_france/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for the Meteo-France config flow.""" +from unittest.mock import patch + from meteofrance_api.model import Place import pytest @@ -13,7 +15,6 @@ from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE from homeassistant.helpers.typing import HomeAssistantType -from tests.async_mock import patch from tests.common import MockConfigEntry CITY_1_POSTAL = "74220" diff --git a/tests/components/metoffice/conftest.py b/tests/components/metoffice/conftest.py index 9538c7a8668..78d7d2c2eee 100644 --- a/tests/components/metoffice/conftest.py +++ b/tests/components/metoffice/conftest.py @@ -1,9 +1,9 @@ """Fixtures for Met Office weather integration tests.""" +from unittest.mock import patch + from datapoint.exceptions import APIException import pytest -from tests.async_mock import patch - @pytest.fixture() def mock_simple_manager_fail(): diff --git a/tests/components/metoffice/test_config_flow.py b/tests/components/metoffice/test_config_flow.py index 3f248704fa1..f0023b0d8d5 100644 --- a/tests/components/metoffice/test_config_flow.py +++ b/tests/components/metoffice/test_config_flow.py @@ -1,5 +1,6 @@ """Test the National Weather Service (NWS) config flow.""" import json +from unittest.mock import patch from homeassistant import config_entries, setup from homeassistant.components.metoffice.const import DOMAIN @@ -12,7 +13,6 @@ from .const import ( TEST_SITE_NAME_WAVERTREE, ) -from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture diff --git a/tests/components/metoffice/test_sensor.py b/tests/components/metoffice/test_sensor.py index 43dbf3f75e0..43f460056f9 100644 --- a/tests/components/metoffice/test_sensor.py +++ b/tests/components/metoffice/test_sensor.py @@ -1,5 +1,6 @@ """The tests for the Met Office sensor component.""" import json +from unittest.mock import patch from homeassistant.components.metoffice.const import ATTRIBUTION, DOMAIN @@ -15,7 +16,6 @@ from .const import ( WAVERTREE_SENSOR_RESULTS, ) -from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py index f1530021fcf..18edbc4a972 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -1,6 +1,7 @@ """The tests for the Met Office sensor component.""" from datetime import timedelta import json +from unittest.mock import patch from homeassistant.components.metoffice.const import DOMAIN from homeassistant.const import STATE_UNAVAILABLE @@ -13,7 +14,6 @@ from .const import ( WAVERTREE_SENSOR_RESULTS, ) -from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture diff --git a/tests/components/mfi/test_sensor.py b/tests/components/mfi/test_sensor.py index ad23039237c..610aa91cd4c 100644 --- a/tests/components/mfi/test_sensor.py +++ b/tests/components/mfi/test_sensor.py @@ -1,4 +1,6 @@ """The tests for the mFi sensor platform.""" +import unittest.mock as mock + from mficlient.client import FailedToLogin import pytest import requests @@ -8,8 +10,6 @@ import homeassistant.components.sensor as sensor_component from homeassistant.const import TEMP_CELSIUS from homeassistant.setup import async_setup_component -import tests.async_mock as mock - PLATFORM = mfi COMPONENT = sensor_component THING = "sensor" diff --git a/tests/components/mfi/test_switch.py b/tests/components/mfi/test_switch.py index b11dcdccb6e..0409a4f387a 100644 --- a/tests/components/mfi/test_switch.py +++ b/tests/components/mfi/test_switch.py @@ -1,12 +1,12 @@ """The tests for the mFi switch platform.""" +import unittest.mock as mock + import pytest import homeassistant.components.mfi.switch as mfi import homeassistant.components.switch as switch_component from homeassistant.setup import async_setup_component -import tests.async_mock as mock - PLATFORM = mfi COMPONENT = switch_component THING = "switch" diff --git a/tests/components/mhz19/test_sensor.py b/tests/components/mhz19/test_sensor.py index 6305f86cedc..5b6e1e9fa37 100644 --- a/tests/components/mhz19/test_sensor.py +++ b/tests/components/mhz19/test_sensor.py @@ -1,4 +1,6 @@ """Tests for MH-Z19 sensor.""" +from unittest.mock import DEFAULT, Mock, patch + from pmsensor import co2sensor from pmsensor.co2sensor import read_mh_z19_with_temperature @@ -11,7 +13,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import DEFAULT, Mock, patch from tests.common import assert_setup_component diff --git a/tests/components/microsoft_face/test_init.py b/tests/components/microsoft_face/test_init.py index abd35eca3e1..191c59e556f 100644 --- a/tests/components/microsoft_face/test_init.py +++ b/tests/components/microsoft_face/test_init.py @@ -1,5 +1,6 @@ """The tests for the microsoft face platform.""" import asyncio +from unittest.mock import patch from homeassistant.components import camera, microsoft_face as mf from homeassistant.components.microsoft_face import ( @@ -17,7 +18,6 @@ from homeassistant.components.microsoft_face import ( from homeassistant.const import ATTR_NAME from homeassistant.setup import setup_component -from tests.async_mock import patch from tests.common import assert_setup_component, get_test_home_assistant, load_fixture diff --git a/tests/components/mikrotik/test_config_flow.py b/tests/components/mikrotik/test_config_flow.py index ad3a27d724a..c884315f400 100644 --- a/tests/components/mikrotik/test_config_flow.py +++ b/tests/components/mikrotik/test_config_flow.py @@ -1,5 +1,6 @@ """Test Mikrotik setup process.""" from datetime import timedelta +from unittest.mock import patch import librouteros import pytest @@ -15,7 +16,6 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) -from tests.async_mock import patch from tests.common import MockConfigEntry DEMO_USER_INPUT = { diff --git a/tests/components/mikrotik/test_hub.py b/tests/components/mikrotik/test_hub.py index ecfb9add717..27c53786519 100644 --- a/tests/components/mikrotik/test_hub.py +++ b/tests/components/mikrotik/test_hub.py @@ -1,4 +1,6 @@ """Test Mikrotik hub.""" +from unittest.mock import patch + import librouteros from homeassistant import config_entries @@ -6,7 +8,6 @@ from homeassistant.components import mikrotik from . import ARP_DATA, DHCP_DATA, MOCK_DATA, MOCK_OPTIONS, WIRELESS_DATA -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/mikrotik/test_init.py b/tests/components/mikrotik/test_init.py index 30df96e89a0..281b70e36be 100644 --- a/tests/components/mikrotik/test_init.py +++ b/tests/components/mikrotik/test_init.py @@ -1,10 +1,11 @@ """Test Mikrotik setup process.""" +from unittest.mock import AsyncMock, Mock, patch + from homeassistant.components import mikrotik from homeassistant.setup import async_setup_component from . import MOCK_DATA -from tests.async_mock import AsyncMock, Mock, patch from tests.common import MockConfigEntry diff --git a/tests/components/mill/test_config_flow.py b/tests/components/mill/test_config_flow.py index ff95af44ff5..ee565d32211 100644 --- a/tests/components/mill/test_config_flow.py +++ b/tests/components/mill/test_config_flow.py @@ -1,10 +1,11 @@ """Tests for Mill config flow.""" +from unittest.mock import patch + import pytest from homeassistant.components.mill.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/min_max/test_sensor.py b/tests/components/min_max/test_sensor.py index 0d3a2e8fdcd..93360d57786 100644 --- a/tests/components/min_max/test_sensor.py +++ b/tests/components/min_max/test_sensor.py @@ -1,6 +1,7 @@ """The test for the min/max sensor platform.""" from os import path import statistics +from unittest.mock import patch from homeassistant import config as hass_config from homeassistant.components.min_max import DOMAIN @@ -15,8 +16,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import patch - VALUES = [17, 20, 15.3] COUNT = len(VALUES) MIN_VALUE = min(VALUES) diff --git a/tests/components/minecraft_server/test_config_flow.py b/tests/components/minecraft_server/test_config_flow.py index 2f8ae5ff0bf..2f3b7781ecf 100644 --- a/tests/components/minecraft_server/test_config_flow.py +++ b/tests/components/minecraft_server/test_config_flow.py @@ -1,6 +1,7 @@ """Test the Minecraft Server config flow.""" import asyncio +from unittest.mock import patch import aiodns from mcstatus.pinger import PingResponse @@ -19,7 +20,6 @@ from homeassistant.data_entry_flow import ( ) from homeassistant.helpers.typing import HomeAssistantType -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/minio/test_minio.py b/tests/components/minio/test_minio.py index 8cde32f7870..de66ee2fafa 100644 --- a/tests/components/minio/test_minio.py +++ b/tests/components/minio/test_minio.py @@ -1,6 +1,7 @@ """Tests for Minio Hass related code.""" import asyncio import json +from unittest.mock import MagicMock, call, patch import pytest @@ -18,7 +19,6 @@ from homeassistant.components.minio import ( from homeassistant.core import callback from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, call, patch from tests.components.minio.common import TEST_EVENT diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index b56b56f239a..831c8250d7a 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -1,4 +1,6 @@ """Webhook tests for mobile_app.""" +from unittest.mock import patch + import pytest from homeassistant.components.camera import SUPPORT_STREAM as CAMERA_SUPPORT_STREAM @@ -11,7 +13,6 @@ from homeassistant.setup import async_setup_component from .const import CALL_SERVICE, FIRE_EVENT, REGISTER_CLEARTEXT, RENDER_TEMPLATE, UPDATE -from tests.async_mock import patch from tests.common import async_mock_service diff --git a/tests/components/mochad/test_light.py b/tests/components/mochad/test_light.py index 9d484b2db65..0cb1b022a60 100644 --- a/tests/components/mochad/test_light.py +++ b/tests/components/mochad/test_light.py @@ -1,13 +1,13 @@ """The tests for the mochad light platform.""" +import unittest.mock as mock + import pytest from homeassistant.components import light from homeassistant.components.mochad import light as mochad from homeassistant.setup import async_setup_component -import tests.async_mock as mock - @pytest.fixture(autouse=True) def pymochad_mock(): diff --git a/tests/components/mochad/test_switch.py b/tests/components/mochad/test_switch.py index e8d58233c40..218248c3442 100644 --- a/tests/components/mochad/test_switch.py +++ b/tests/components/mochad/test_switch.py @@ -1,12 +1,12 @@ """The tests for the mochad switch platform.""" +import unittest.mock as mock + import pytest from homeassistant.components import switch from homeassistant.components.mochad import switch as mochad from homeassistant.setup import async_setup_component -import tests.async_mock as mock - @pytest.fixture(autouse=True) def pymochad_mock(): diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index ce6e6585512..e3a707b7fc9 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -1,5 +1,6 @@ """The tests for the Modbus sensor component.""" from unittest import mock +from unittest.mock import patch import pytest @@ -14,7 +15,6 @@ from homeassistant.const import CONF_PLATFORM, CONF_SCAN_INTERVAL from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import async_fire_time_changed diff --git a/tests/components/monoprice/test_config_flow.py b/tests/components/monoprice/test_config_flow.py index f3530bb2c75..0b954cb6e34 100644 --- a/tests/components/monoprice/test_config_flow.py +++ b/tests/components/monoprice/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Monoprice 6-Zone Amplifier config flow.""" +from unittest.mock import patch + from serial import SerialException from homeassistant import config_entries, data_entry_flow, setup @@ -11,7 +13,6 @@ from homeassistant.components.monoprice.const import ( ) from homeassistant.const import CONF_PORT -from tests.async_mock import patch from tests.common import MockConfigEntry CONFIG = { diff --git a/tests/components/monoprice/test_media_player.py b/tests/components/monoprice/test_media_player.py index 41a33fd095b..d7b505b5279 100644 --- a/tests/components/monoprice/test_media_player.py +++ b/tests/components/monoprice/test_media_player.py @@ -1,5 +1,6 @@ """The tests for Monoprice Media player platform.""" from collections import defaultdict +from unittest.mock import patch from serial import SerialException @@ -34,7 +35,6 @@ from homeassistant.const import ( ) from homeassistant.helpers.entity_component import async_update_entity -from tests.async_mock import patch from tests.common import MockConfigEntry MOCK_CONFIG = {CONF_PORT: "fake port", CONF_SOURCES: {"1": "one", "3": "three"}} diff --git a/tests/components/moon/test_sensor.py b/tests/components/moon/test_sensor.py index 234a8a1e34d..8a0269ba9fe 100644 --- a/tests/components/moon/test_sensor.py +++ b/tests/components/moon/test_sensor.py @@ -1,5 +1,6 @@ """The test for the moon sensor platform.""" from datetime import datetime +from unittest.mock import patch from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, @@ -9,8 +10,6 @@ from homeassistant.const import ATTR_ENTITY_ID from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch - DAY1 = datetime(2017, 1, 1, 1, tzinfo=dt_util.UTC) DAY2 = datetime(2017, 1, 18, 1, tzinfo=dt_util.UTC) diff --git a/tests/components/motion_blinds/test_config_flow.py b/tests/components/motion_blinds/test_config_flow.py index 4a25026959c..18592421249 100644 --- a/tests/components/motion_blinds/test_config_flow.py +++ b/tests/components/motion_blinds/test_config_flow.py @@ -1,5 +1,6 @@ """Test the Motion Blinds config flow.""" import socket +from unittest.mock import Mock, patch import pytest @@ -8,8 +9,6 @@ from homeassistant.components.motion_blinds.config_flow import DEFAULT_GATEWAY_N from homeassistant.components.motion_blinds.const import DOMAIN from homeassistant.const import CONF_API_KEY, CONF_HOST -from tests.async_mock import Mock, patch - TEST_HOST = "1.2.3.4" TEST_HOST2 = "5.6.7.8" TEST_API_KEY = "12ab345c-d67e-8f" diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index 524448e5839..cfb59ba27a7 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -1,6 +1,7 @@ """The tests the MQTT alarm control panel component.""" import copy import json +from unittest.mock import patch import pytest @@ -43,7 +44,6 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.async_mock import patch from tests.common import assert_setup_component, async_fire_mqtt_message from tests.components.alarm_control_panel import common diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index d4c837de023..bf88b7901e1 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -2,6 +2,7 @@ import copy from datetime import datetime, timedelta import json +from unittest.mock import patch import pytest @@ -40,7 +41,6 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.async_mock import patch from tests.common import async_fire_mqtt_message, async_fire_time_changed DEFAULT_CONFIG = { diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index 1f3acee119e..13c15796cfd 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -1,5 +1,6 @@ """The tests for mqtt camera component.""" import json +from unittest.mock import patch import pytest @@ -30,7 +31,6 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.async_mock import patch from tests.common import async_fire_mqtt_message DEFAULT_CONFIG = { diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 0a9c1dc6104..97657e9e3e6 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -1,6 +1,7 @@ """The tests for the mqtt climate component.""" import copy import json +from unittest.mock import call, patch import pytest import voluptuous as vol @@ -49,7 +50,6 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.async_mock import call, patch from tests.common import async_fire_mqtt_message from tests.components.climate import common diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index e706bc47418..18320a8c467 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -2,6 +2,7 @@ import copy from datetime import datetime import json +from unittest.mock import ANY, patch from homeassistant.components import mqtt from homeassistant.components.mqtt import debug_info @@ -10,7 +11,6 @@ from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNAVAILABLE from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component -from tests.async_mock import ANY, patch from tests.common import async_fire_mqtt_message, mock_registry DEFAULT_CONFIG_DEVICE_INFO_ID = { diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 41dc4745528..b41a446a8c0 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -1,5 +1,7 @@ """Test config flow.""" +from unittest.mock import patch + import pytest import voluptuous as vol @@ -7,7 +9,6 @@ from homeassistant import data_entry_flow from homeassistant.components import mqtt from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 629d9674b22..019f0e19911 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -1,4 +1,6 @@ """The tests for the MQTT cover platform.""" +from unittest.mock import patch + import pytest from homeassistant.components import cover @@ -53,7 +55,6 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.async_mock import patch from tests.common import async_fire_mqtt_message DEFAULT_CONFIG = { diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py index 32f826422a8..c85fcef7dc4 100644 --- a/tests/components/mqtt/test_device_tracker.py +++ b/tests/components/mqtt/test_device_tracker.py @@ -1,11 +1,12 @@ """The tests for the MQTT device tracker platform.""" +from unittest.mock import patch + import pytest from homeassistant.components.device_tracker.const import DOMAIN, SOURCE_TYPE_BLUETOOTH from homeassistant.const import CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import async_fire_mqtt_message diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index e1365418483..7f22e0eef6f 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -1,6 +1,7 @@ """The tests for the MQTT discovery.""" from pathlib import Path import re +from unittest.mock import AsyncMock, patch import pytest @@ -13,7 +14,6 @@ from homeassistant.components.mqtt.abbreviations import ( from homeassistant.components.mqtt.discovery import ALREADY_DISCOVERED, async_start from homeassistant.const import STATE_OFF, STATE_ON -from tests.async_mock import AsyncMock, patch from tests.common import ( async_fire_mqtt_message, mock_device_registry, diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index e1801c5c15a..d52b67fd3ed 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -1,4 +1,6 @@ """Test MQTT fans.""" +from unittest.mock import patch + import pytest from homeassistant.components import fan @@ -34,7 +36,6 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.async_mock import patch from tests.common import async_fire_mqtt_message from tests.components.fan import common diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 82a88de918b..bb698a24d7e 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -3,6 +3,7 @@ import asyncio from datetime import datetime, timedelta import json import ssl +from unittest.mock import AsyncMock, MagicMock, call, mock_open, patch import pytest import voluptuous as vol @@ -21,7 +22,6 @@ from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from tests.async_mock import AsyncMock, MagicMock, call, mock_open, patch from tests.common import ( MockConfigEntry, async_fire_mqtt_message, diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index aacea4e345e..db25e66c2c2 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -1,6 +1,7 @@ """The tests for the Legacy Mqtt vacuum platform.""" from copy import deepcopy import json +from unittest.mock import patch import pytest @@ -46,7 +47,6 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.async_mock import patch from tests.common import async_fire_mqtt_message from tests.components.vacuum import common diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 9e044380555..933f49ff823 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -155,6 +155,7 @@ light: """ import json from os import path +from unittest.mock import call, patch import pytest @@ -188,7 +189,6 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.async_mock import call, patch from tests.common import assert_setup_component, async_fire_mqtt_message from tests.components.light import common diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index e41575968da..1c9eed0e404 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -88,6 +88,7 @@ light: brightness_scale: 99 """ import json +from unittest.mock import call, patch import pytest @@ -125,7 +126,6 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.async_mock import call, patch from tests.common import async_fire_mqtt_message from tests.components.light import common diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index 6767375a50e..733a39ce252 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -26,6 +26,8 @@ If your light doesn't support white value feature, omit `white_value_template`. If your light doesn't support RGB feature, omit `(red|green|blue)_template`. """ +from unittest.mock import patch + import pytest from homeassistant.components import light @@ -62,7 +64,6 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.async_mock import patch from tests.common import assert_setup_component, async_fire_mqtt_message from tests.components.light import common diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index cd37543d94e..754f60f49b2 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -1,4 +1,6 @@ """The tests for the MQTT lock platform.""" +from unittest.mock import patch + import pytest from homeassistant.components.lock import ( @@ -35,7 +37,6 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.async_mock import patch from tests.common import async_fire_mqtt_message DEFAULT_CONFIG = { diff --git a/tests/components/mqtt/test_scene.py b/tests/components/mqtt/test_scene.py index 0e3341bd15f..9a233e19fd8 100644 --- a/tests/components/mqtt/test_scene.py +++ b/tests/components/mqtt/test_scene.py @@ -1,6 +1,7 @@ """The tests for the MQTT scene platform.""" import copy import json +from unittest.mock import patch import pytest @@ -21,8 +22,6 @@ from .test_common import ( help_test_unique_id, ) -from tests.async_mock import patch - DEFAULT_CONFIG = { scene.DOMAIN: { "platform": "mqtt", diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 7e92579753b..7449d127dc1 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -2,6 +2,7 @@ import copy from datetime import datetime, timedelta import json +from unittest.mock import patch import pytest @@ -42,7 +43,6 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.async_mock import patch from tests.common import async_fire_mqtt_message, async_fire_time_changed DEFAULT_CONFIG = { diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index fe410821395..e18b0b05835 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -1,6 +1,7 @@ """The tests for the State vacuum Mqtt platform.""" from copy import deepcopy import json +from unittest.mock import patch import pytest @@ -56,7 +57,6 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.async_mock import patch from tests.common import async_fire_mqtt_message from tests.components.vacuum import common diff --git a/tests/components/mqtt/test_subscription.py b/tests/components/mqtt/test_subscription.py index f1ed26e89cc..36d8946be0b 100644 --- a/tests/components/mqtt/test_subscription.py +++ b/tests/components/mqtt/test_subscription.py @@ -1,11 +1,12 @@ """The tests for the MQTT subscription component.""" +from unittest.mock import ANY + from homeassistant.components.mqtt.subscription import ( async_subscribe_topics, async_unsubscribe_topics, ) from homeassistant.core import callback -from tests.async_mock import ANY from tests.common import async_fire_mqtt_message diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index 4d9c6dc2c77..607e4468a5f 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -1,6 +1,7 @@ """The tests for the MQTT switch platform.""" import copy import json +from unittest.mock import patch import pytest @@ -33,7 +34,6 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.async_mock import patch from tests.common import async_fire_mqtt_message from tests.components.switch import common diff --git a/tests/components/mqtt/test_tag.py b/tests/components/mqtt/test_tag.py index bed05ac6384..8ade3146455 100644 --- a/tests/components/mqtt/test_tag.py +++ b/tests/components/mqtt/test_tag.py @@ -1,10 +1,10 @@ """The tests for MQTT tag scanner.""" import copy import json +from unittest.mock import ANY, patch import pytest -from tests.async_mock import ANY, patch from tests.common import ( async_fire_mqtt_message, async_get_device_automations, diff --git a/tests/components/mqtt/test_trigger.py b/tests/components/mqtt/test_trigger.py index a26ecadb6bd..b27af2b9bd0 100644 --- a/tests/components/mqtt/test_trigger.py +++ b/tests/components/mqtt/test_trigger.py @@ -1,11 +1,12 @@ """The tests for the MQTT automation.""" +from unittest.mock import ANY + import pytest import homeassistant.components.automation as automation from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF 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 diff --git a/tests/components/mqtt_eventstream/test_init.py b/tests/components/mqtt_eventstream/test_init.py index 87ac4696a31..da37489a130 100644 --- a/tests/components/mqtt_eventstream/test_init.py +++ b/tests/components/mqtt_eventstream/test_init.py @@ -1,5 +1,6 @@ """The tests for the MQTT eventstream component.""" import json +from unittest.mock import ANY, patch import homeassistant.components.mqtt_eventstream as eventstream from homeassistant.const import EVENT_STATE_CHANGED @@ -8,7 +9,6 @@ from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import ANY, patch from tests.common import ( async_fire_mqtt_message, async_fire_time_changed, diff --git a/tests/components/mqtt_json/test_device_tracker.py b/tests/components/mqtt_json/test_device_tracker.py index 89b37e28b52..d17484cc5e9 100644 --- a/tests/components/mqtt_json/test_device_tracker.py +++ b/tests/components/mqtt_json/test_device_tracker.py @@ -2,6 +2,7 @@ import json import logging import os +from unittest.mock import patch import pytest @@ -12,7 +13,6 @@ from homeassistant.components.device_tracker.legacy import ( from homeassistant.const import CONF_PLATFORM from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import async_fire_mqtt_message LOCATION_MESSAGE = { diff --git a/tests/components/mqtt_room/test_sensor.py b/tests/components/mqtt_room/test_sensor.py index e17fbb4847d..ca5f9420dc5 100644 --- a/tests/components/mqtt_room/test_sensor.py +++ b/tests/components/mqtt_room/test_sensor.py @@ -1,6 +1,7 @@ """The tests for the MQTT room presence sensor.""" import datetime import json +from unittest.mock import patch from homeassistant.components.mqtt import CONF_QOS, CONF_STATE_TOPIC, DEFAULT_QOS import homeassistant.components.sensor as sensor @@ -8,7 +9,6 @@ from homeassistant.const import CONF_NAME, CONF_PLATFORM from homeassistant.setup import async_setup_component from homeassistant.util import dt -from tests.async_mock import patch from tests.common import async_fire_mqtt_message DEVICE_ID = "123TESTMAC" diff --git a/tests/components/mqtt_statestream/test_init.py b/tests/components/mqtt_statestream/test_init.py index cea4b492f3e..d7bfcfe4f2e 100644 --- a/tests/components/mqtt_statestream/test_init.py +++ b/tests/components/mqtt_statestream/test_init.py @@ -1,9 +1,10 @@ """The tests for the MQTT statestream component.""" +from unittest.mock import ANY, call + import homeassistant.components.mqtt_statestream as statestream from homeassistant.core import State from homeassistant.setup import async_setup_component -from tests.async_mock import ANY, call from tests.common import mock_state_change_event diff --git a/tests/components/myq/test_config_flow.py b/tests/components/myq/test_config_flow.py index e2c43e8ce5c..edf37895fd3 100644 --- a/tests/components/myq/test_config_flow.py +++ b/tests/components/myq/test_config_flow.py @@ -1,11 +1,12 @@ """Test the MyQ config flow.""" +from unittest.mock import patch + from pymyq.errors import InvalidCredentialsError, MyQError from homeassistant import config_entries, setup from homeassistant.components.myq.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/myq/util.py b/tests/components/myq/util.py index 61c19325d57..84e85723918 100644 --- a/tests/components/myq/util.py +++ b/tests/components/myq/util.py @@ -1,12 +1,12 @@ """Tests for the myq integration.""" import json +from unittest.mock import patch from homeassistant.components.myq.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture diff --git a/tests/components/mythicbeastsdns/test_init.py b/tests/components/mythicbeastsdns/test_init.py index e8efac2c01d..01c9b253e4f 100644 --- a/tests/components/mythicbeastsdns/test_init.py +++ b/tests/components/mythicbeastsdns/test_init.py @@ -1,11 +1,10 @@ """Test the Mythic Beasts DNS component.""" import logging +from unittest.mock import patch from homeassistant.components import mythicbeastsdns from homeassistant.setup import async_setup_component -from tests.async_mock import patch - _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/neato/test_config_flow.py b/tests/components/neato/test_config_flow.py index 6954eb1b7af..7c7e25f2e0c 100644 --- a/tests/components/neato/test_config_flow.py +++ b/tests/components/neato/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Neato Botvac config flow.""" +from unittest.mock import patch + from pybotvac.neato import Neato from homeassistant import config_entries, data_entry_flow, setup @@ -6,7 +8,6 @@ from homeassistant.components.neato.const import NEATO_DOMAIN from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.typing import HomeAssistantType -from tests.async_mock import patch from tests.common import MockConfigEntry CLIENT_ID = "1234" diff --git a/tests/components/ness_alarm/test_init.py b/tests/components/ness_alarm/test_init.py index f959b5345dd..507a786c3d1 100644 --- a/tests/components/ness_alarm/test_init.py +++ b/tests/components/ness_alarm/test_init.py @@ -1,5 +1,6 @@ """Tests for the ness_alarm component.""" from enum import Enum +from unittest.mock import MagicMock, patch import pytest @@ -31,8 +32,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, patch - VALID_CONFIG = { DOMAIN: { CONF_HOST: "alarm.local", diff --git a/tests/components/nest/camera_sdm_test.py b/tests/components/nest/camera_sdm_test.py index 69b413ba51a..07a21eb2b68 100644 --- a/tests/components/nest/camera_sdm_test.py +++ b/tests/components/nest/camera_sdm_test.py @@ -6,6 +6,7 @@ pubsub subscriber. """ import datetime +from unittest.mock import patch import aiohttp from google_nest_sdm.device import Device @@ -18,7 +19,6 @@ from homeassistant.util.dt import utcnow from .common import async_setup_sdm_platform -from tests.async_mock import patch from tests.common import async_fire_time_changed PLATFORM = "camera" diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index 65a37563911..2721cd08c19 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -2,6 +2,7 @@ import time from typing import Awaitable, Callable +from unittest.mock import patch from google_nest_sdm.device_manager import DeviceManager from google_nest_sdm.event import EventMessage @@ -10,7 +11,6 @@ from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber from homeassistant.components.nest import DOMAIN from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import MockConfigEntry CONFIG = { diff --git a/tests/components/nest/test_config_flow_legacy.py b/tests/components/nest/test_config_flow_legacy.py index 23e01cf239a..ed4df2c7d84 100644 --- a/tests/components/nest/test_config_flow_legacy.py +++ b/tests/components/nest/test_config_flow_legacy.py @@ -1,12 +1,11 @@ """Tests for the Nest config flow.""" import asyncio -from unittest.mock import Mock, patch +from unittest.mock import AsyncMock, Mock, patch from homeassistant import data_entry_flow from homeassistant.components.nest import DOMAIN, config_flow from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock from tests.common import mock_coro diff --git a/tests/components/nest/test_config_flow_sdm.py b/tests/components/nest/test_config_flow_sdm.py index aad5621935e..f8c9c69698a 100644 --- a/tests/components/nest/test_config_flow_sdm.py +++ b/tests/components/nest/test_config_flow_sdm.py @@ -1,5 +1,7 @@ """Test the Google Nest Device Access config flow.""" +from unittest.mock import patch + import pytest from homeassistant import config_entries, setup @@ -9,8 +11,6 @@ from homeassistant.helpers import config_entry_oauth2_flow from .common import MockConfigEntry -from tests.async_mock import patch - CLIENT_ID = "1234" CLIENT_SECRET = "5678" PROJECT_ID = "project-id-4321" diff --git a/tests/components/nest/test_init_legacy.py b/tests/components/nest/test_init_legacy.py index f85fcdaa749..3a78877a235 100644 --- a/tests/components/nest/test_init_legacy.py +++ b/tests/components/nest/test_init_legacy.py @@ -1,10 +1,10 @@ """Test basic initialization for the Legacy Nest API using mocks for the Nest python library.""" import time +from unittest.mock import MagicMock, PropertyMock, patch from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, PropertyMock, patch from tests.common import MockConfigEntry DOMAIN = "nest" diff --git a/tests/components/nest/test_init_sdm.py b/tests/components/nest/test_init_sdm.py index e49f3b89f27..ee2a7f4f242 100644 --- a/tests/components/nest/test_init_sdm.py +++ b/tests/components/nest/test_init_sdm.py @@ -6,6 +6,7 @@ and failure modes. """ import logging +from unittest.mock import patch from google_nest_sdm.exceptions import AuthException, GoogleNestException @@ -20,7 +21,6 @@ from homeassistant.setup import async_setup_component from .common import CONFIG, CONFIG_ENTRY_DATA, async_setup_sdm_platform -from tests.async_mock import patch from tests.common import MockConfigEntry PLATFORM = "sensor" diff --git a/tests/components/netatmo/test_config_flow.py b/tests/components/netatmo/test_config_flow.py index 74a5d8dcc92..03c751aae96 100644 --- a/tests/components/netatmo/test_config_flow.py +++ b/tests/components/netatmo/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Netatmo config flow.""" +from unittest.mock import patch + from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.netatmo import config_flow from homeassistant.components.netatmo.const import ( @@ -11,7 +13,6 @@ from homeassistant.components.netatmo.const import ( from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow -from tests.async_mock import patch from tests.common import MockConfigEntry CLIENT_ID = "1234" diff --git a/tests/components/nexia/test_config_flow.py b/tests/components/nexia/test_config_flow.py index 81536f5beea..d0433eb27f7 100644 --- a/tests/components/nexia/test_config_flow.py +++ b/tests/components/nexia/test_config_flow.py @@ -1,12 +1,12 @@ """Test the nexia config flow.""" +from unittest.mock import MagicMock, patch + from requests.exceptions import ConnectTimeout, HTTPError from homeassistant import config_entries, setup from homeassistant.components.nexia.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import MagicMock, patch - async def test_form(hass): """Test we get the form.""" diff --git a/tests/components/nexia/util.py b/tests/components/nexia/util.py index 7d34f0894e0..8e132941994 100644 --- a/tests/components/nexia/util.py +++ b/tests/components/nexia/util.py @@ -1,4 +1,5 @@ """Tests for the nexia integration.""" +from unittest.mock import patch import uuid from nexia.home import NexiaHome @@ -8,7 +9,6 @@ from homeassistant.components.nexia.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture diff --git a/tests/components/nextbus/test_sensor.py b/tests/components/nextbus/test_sensor.py index 4e7f0af8526..016afed2b0f 100644 --- a/tests/components/nextbus/test_sensor.py +++ b/tests/components/nextbus/test_sensor.py @@ -1,5 +1,6 @@ """The tests for the nexbus sensor component.""" from copy import deepcopy +from unittest.mock import patch import pytest @@ -7,7 +8,6 @@ import homeassistant.components.nextbus.sensor as nextbus import homeassistant.components.sensor as sensor from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import assert_setup_component VALID_AGENCY = "sf-muni" diff --git a/tests/components/nightscout/__init__.py b/tests/components/nightscout/__init__.py index c7f15068d1d..6c1a34ebe41 100644 --- a/tests/components/nightscout/__init__.py +++ b/tests/components/nightscout/__init__.py @@ -1,5 +1,6 @@ """Tests for the Nightscout integration.""" import json +from unittest.mock import patch from aiohttp import ClientConnectionError from py_nightscout.models import SGV, ServerStatus @@ -7,7 +8,6 @@ from py_nightscout.models import SGV, ServerStatus from homeassistant.components.nightscout.const import DOMAIN from homeassistant.const import CONF_URL -from tests.async_mock import patch from tests.common import MockConfigEntry GLUCOSE_READINGS = [ diff --git a/tests/components/nightscout/test_config_flow.py b/tests/components/nightscout/test_config_flow.py index 71983f1b29d..9a86e14b4e5 100644 --- a/tests/components/nightscout/test_config_flow.py +++ b/tests/components/nightscout/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Nightscout config flow.""" +from unittest.mock import patch + from aiohttp import ClientConnectionError, ClientResponseError from homeassistant import config_entries, data_entry_flow, setup @@ -6,7 +8,6 @@ from homeassistant.components.nightscout.const import DOMAIN from homeassistant.components.nightscout.utils import hash_from_url from homeassistant.const import CONF_URL -from tests.async_mock import patch from tests.common import MockConfigEntry from tests.components.nightscout import ( GLUCOSE_READINGS, diff --git a/tests/components/nightscout/test_init.py b/tests/components/nightscout/test_init.py index d81559ceba7..88ca141b999 100644 --- a/tests/components/nightscout/test_init.py +++ b/tests/components/nightscout/test_init.py @@ -1,4 +1,6 @@ """Test the Nightscout config flow.""" +from unittest.mock import patch + from aiohttp import ClientError from homeassistant.components.nightscout.const import DOMAIN @@ -9,7 +11,6 @@ from homeassistant.config_entries import ( ) from homeassistant.const import CONF_URL -from tests.async_mock import patch from tests.common import MockConfigEntry from tests.components.nightscout import init_integration diff --git a/tests/components/notion/test_config_flow.py b/tests/components/notion/test_config_flow.py index 728a8d40a52..d9ed37d516c 100644 --- a/tests/components/notion/test_config_flow.py +++ b/tests/components/notion/test_config_flow.py @@ -1,4 +1,6 @@ """Define tests for the Notion config flow.""" +from unittest.mock import AsyncMock, patch + import aionotion import pytest @@ -7,7 +9,6 @@ from homeassistant.components.notion import DOMAIN, config_flow from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import AsyncMock, patch from tests.common import MockConfigEntry diff --git a/tests/components/nsw_fuel_station/test_sensor.py b/tests/components/nsw_fuel_station/test_sensor.py index e99a7bfc079..28ff7a7ed95 100644 --- a/tests/components/nsw_fuel_station/test_sensor.py +++ b/tests/components/nsw_fuel_station/test_sensor.py @@ -1,8 +1,9 @@ """The tests for the NSW Fuel Station sensor platform.""" +from unittest.mock import patch + from homeassistant.components import sensor from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import assert_setup_component VALID_CONFIG = { diff --git a/tests/components/nsw_rural_fire_service_feed/test_geo_location.py b/tests/components/nsw_rural_fire_service_feed/test_geo_location.py index b8923d854ee..d8719578957 100644 --- a/tests/components/nsw_rural_fire_service_feed/test_geo_location.py +++ b/tests/components/nsw_rural_fire_service_feed/test_geo_location.py @@ -1,5 +1,6 @@ """The tests for the NSW Rural Fire Service Feeds platform.""" import datetime +from unittest.mock import ANY, MagicMock, call, patch from aio_geojson_nsw_rfs_incidents import NswRuralFireServiceIncidentsFeed @@ -35,7 +36,6 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import ANY, MagicMock, call, patch from tests.common import assert_setup_component, async_fire_time_changed CONFIG = { diff --git a/tests/components/nuheat/mocks.py b/tests/components/nuheat/mocks.py index ccfb031f043..de87f72fffb 100644 --- a/tests/components/nuheat/mocks.py +++ b/tests/components/nuheat/mocks.py @@ -1,11 +1,11 @@ """The test for the NuHeat thermostat module.""" +from unittest.mock import MagicMock, Mock + from nuheat.config import SCHEDULE_HOLD, SCHEDULE_RUN, SCHEDULE_TEMPORARY_HOLD from homeassistant.components.nuheat.const import DOMAIN from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import MagicMock, Mock - def _get_mock_thermostat_run(): serial_number = "12345" diff --git a/tests/components/nuheat/test_climate.py b/tests/components/nuheat/test_climate.py index 453f0dab110..e5fc6246841 100644 --- a/tests/components/nuheat/test_climate.py +++ b/tests/components/nuheat/test_climate.py @@ -1,5 +1,6 @@ """The test for the NuHeat thermostat module.""" from datetime import timedelta +from unittest.mock import patch from homeassistant.components.nuheat.const import DOMAIN from homeassistant.const import ATTR_ENTITY_ID @@ -15,7 +16,6 @@ from .mocks import ( _mock_get_config, ) -from tests.async_mock import patch from tests.common import async_fire_time_changed diff --git a/tests/components/nuheat/test_config_flow.py b/tests/components/nuheat/test_config_flow.py index 5b2259faea8..246c7130c94 100644 --- a/tests/components/nuheat/test_config_flow.py +++ b/tests/components/nuheat/test_config_flow.py @@ -1,4 +1,6 @@ """Test the NuHeat config flow.""" +from unittest.mock import MagicMock, patch + import requests from homeassistant import config_entries, setup @@ -7,8 +9,6 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, HTTP_INTERNAL_SERV from .mocks import _get_mock_thermostat_run -from tests.async_mock import MagicMock, patch - async def test_form_user(hass): """Test we get the form with user source.""" diff --git a/tests/components/nuheat/test_init.py b/tests/components/nuheat/test_init.py index 8cd08f2edd5..dde4fd8c787 100644 --- a/tests/components/nuheat/test_init.py +++ b/tests/components/nuheat/test_init.py @@ -1,11 +1,11 @@ """NuHeat component tests.""" +from unittest.mock import patch + from homeassistant.components.nuheat.const import DOMAIN from homeassistant.setup import async_setup_component from .mocks import _get_mock_nuheat -from tests.async_mock import patch - VALID_CONFIG = { "nuheat": {"username": "warm", "password": "feet", "devices": "thermostat123"} } diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index 58e090db20f..f1154581fdc 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -1,7 +1,7 @@ """The tests for the Number component.""" -from homeassistant.components.number import NumberEntity +from unittest.mock import MagicMock -from tests.async_mock import MagicMock +from homeassistant.components.number import NumberEntity class MockDefaultNumberEntity(NumberEntity): diff --git a/tests/components/nut/test_config_flow.py b/tests/components/nut/test_config_flow.py index e003ecd796b..01df325dd2c 100644 --- a/tests/components/nut/test_config_flow.py +++ b/tests/components/nut/test_config_flow.py @@ -1,12 +1,13 @@ """Test the Network UPS Tools (NUT) config flow.""" +from unittest.mock import patch + from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.nut.const import DOMAIN from homeassistant.const import CONF_RESOURCES, CONF_SCAN_INTERVAL from .util import _get_mock_pynutclient -from tests.async_mock import patch from tests.common import MockConfigEntry VALID_CONFIG = { diff --git a/tests/components/nut/util.py b/tests/components/nut/util.py index 772667b6b50..4e7506a9db1 100644 --- a/tests/components/nut/util.py +++ b/tests/components/nut/util.py @@ -1,12 +1,12 @@ """Tests for the nut integration.""" import json +from unittest.mock import MagicMock, patch from homeassistant.components.nut.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PORT, CONF_RESOURCES from homeassistant.core import HomeAssistant -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry, load_fixture diff --git a/tests/components/nws/conftest.py b/tests/components/nws/conftest.py index 74f84eb200c..d01201bb484 100644 --- a/tests/components/nws/conftest.py +++ b/tests/components/nws/conftest.py @@ -1,7 +1,8 @@ """Fixtures for National Weather Service tests.""" +from unittest.mock import AsyncMock, patch + import pytest -from tests.async_mock import AsyncMock, patch from tests.components.nws.const import DEFAULT_FORECAST, DEFAULT_OBSERVATION diff --git a/tests/components/nws/test_config_flow.py b/tests/components/nws/test_config_flow.py index 2ea5f36a379..81be7360e87 100644 --- a/tests/components/nws/test_config_flow.py +++ b/tests/components/nws/test_config_flow.py @@ -1,11 +1,11 @@ """Test the National Weather Service (NWS) config flow.""" +from unittest.mock import patch + import aiohttp from homeassistant import config_entries, setup from homeassistant.components.nws.const import DOMAIN -from tests.async_mock import patch - async def test_form(hass, mock_simple_nws_config): """Test we get the form.""" diff --git a/tests/components/nws/test_weather.py b/tests/components/nws/test_weather.py index bd8d81a4b0f..c7cb5b81c2a 100644 --- a/tests/components/nws/test_weather.py +++ b/tests/components/nws/test_weather.py @@ -1,5 +1,6 @@ """Tests for the NWS weather component.""" from datetime import timedelta +from unittest.mock import patch import aiohttp import pytest @@ -15,7 +16,6 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM -from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_time_changed from tests.components.nws.const import ( EXPECTED_FORECAST_IMPERIAL, diff --git a/tests/components/nzbget/__init__.py b/tests/components/nzbget/__init__.py index 27dc47a6df4..9993bdaff1e 100644 --- a/tests/components/nzbget/__init__.py +++ b/tests/components/nzbget/__init__.py @@ -1,5 +1,6 @@ """Tests for the NZBGet integration.""" from datetime import timedelta +from unittest.mock import patch from homeassistant.components.nzbget.const import DOMAIN from homeassistant.const import ( @@ -13,7 +14,6 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) -from tests.async_mock import patch from tests.common import MockConfigEntry ENTRY_CONFIG = { diff --git a/tests/components/nzbget/conftest.py b/tests/components/nzbget/conftest.py index 5855253b1d1..c9138cf59d3 100644 --- a/tests/components/nzbget/conftest.py +++ b/tests/components/nzbget/conftest.py @@ -1,10 +1,10 @@ """Define fixtures available for all tests.""" +from unittest.mock import MagicMock, patch + from pytest import fixture from . import MOCK_HISTORY, MOCK_STATUS, MOCK_VERSION -from tests.async_mock import MagicMock, patch - @fixture def nzbget_api(hass): diff --git a/tests/components/nzbget/test_config_flow.py b/tests/components/nzbget/test_config_flow.py index a58d1faa766..68488c376f6 100644 --- a/tests/components/nzbget/test_config_flow.py +++ b/tests/components/nzbget/test_config_flow.py @@ -1,4 +1,6 @@ """Test the NZBGet config flow.""" +from unittest.mock import patch + from pynzbgetapi import NZBGetAPIException from homeassistant.components.nzbget.const import DOMAIN @@ -21,7 +23,6 @@ from . import ( _patch_version, ) -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/nzbget/test_init.py b/tests/components/nzbget/test_init.py index d24a33d1f5b..2dcdab5754e 100644 --- a/tests/components/nzbget/test_init.py +++ b/tests/components/nzbget/test_init.py @@ -1,4 +1,6 @@ """Test the NZBGet config flow.""" +from unittest.mock import patch + from pynzbgetapi import NZBGetAPIException from homeassistant.components.nzbget.const import DOMAIN @@ -20,7 +22,6 @@ from . import ( init_integration, ) -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/nzbget/test_sensor.py b/tests/components/nzbget/test_sensor.py index c9f1ea71f0c..f5954bc7ee0 100644 --- a/tests/components/nzbget/test_sensor.py +++ b/tests/components/nzbget/test_sensor.py @@ -1,5 +1,6 @@ """Test the NZBGet sensors.""" from datetime import timedelta +from unittest.mock import patch from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, @@ -11,8 +12,6 @@ from homeassistant.util import dt as dt_util from . import init_integration -from tests.async_mock import patch - async def test_sensors(hass, nzbget_api) -> None: """Test the creation and values of the sensors.""" diff --git a/tests/components/omnilogic/test_config_flow.py b/tests/components/omnilogic/test_config_flow.py index 6243fe10efb..acf2df88610 100644 --- a/tests/components/omnilogic/test_config_flow.py +++ b/tests/components/omnilogic/test_config_flow.py @@ -1,10 +1,11 @@ """Test the Omnilogic config flow.""" +from unittest.mock import patch + from omnilogic import LoginException, OmniLogicException from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.omnilogic.const import DOMAIN -from tests.async_mock import patch from tests.common import MockConfigEntry DATA = {"username": "test-username", "password": "test-password"} diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 73845aba7b2..4fa6b8da78a 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -1,6 +1,7 @@ """Test the onboarding views.""" import asyncio import os +from unittest.mock import patch import pytest @@ -11,7 +12,6 @@ from homeassistant.setup import async_setup_component from . import mock_storage -from tests.async_mock import patch from tests.common import CLIENT_ID, CLIENT_REDIRECT_URI, register_auth_provider from tests.components.met.conftest import mock_weather # noqa: F401 diff --git a/tests/components/onewire/__init__.py b/tests/components/onewire/__init__.py index 39a3c438cf9..716e73747f1 100644 --- a/tests/components/onewire/__init__.py +++ b/tests/components/onewire/__init__.py @@ -1,5 +1,7 @@ """Tests for 1-Wire integration.""" +from unittest.mock import patch + from homeassistant.components.onewire.const import ( CONF_MOUNT_DIR, CONF_NAMES, @@ -11,7 +13,6 @@ from homeassistant.components.onewire.const import ( from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/onewire/test_binary_sensor.py b/tests/components/onewire/test_binary_sensor.py index be740b13fb5..aee1641c24f 100644 --- a/tests/components/onewire/test_binary_sensor.py +++ b/tests/components/onewire/test_binary_sensor.py @@ -1,5 +1,6 @@ """Tests for 1-Wire devices connected on OWServer.""" import copy +from unittest.mock import patch from pyownet.protocol import Error as ProtocolError import pytest @@ -11,7 +12,6 @@ from homeassistant.setup import async_setup_component from . import setup_onewire_patched_owserver_integration -from tests.async_mock import patch from tests.common import mock_registry MOCK_DEVICE_SENSORS = { diff --git a/tests/components/onewire/test_config_flow.py b/tests/components/onewire/test_config_flow.py index ba0ae090ed2..ea0b5e85dda 100644 --- a/tests/components/onewire/test_config_flow.py +++ b/tests/components/onewire/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for 1-Wire config flow.""" +from unittest.mock import patch + from pyownet import protocol from homeassistant.components.onewire.const import ( @@ -19,8 +21,6 @@ from homeassistant.data_entry_flow import ( from . import setup_onewire_owserver_integration, setup_onewire_sysbus_integration -from tests.async_mock import patch - async def test_user_owserver(hass): """Test OWServer user flow.""" diff --git a/tests/components/onewire/test_entity_owserver.py b/tests/components/onewire/test_entity_owserver.py index aee84f9fe2b..76ab50419d2 100644 --- a/tests/components/onewire/test_entity_owserver.py +++ b/tests/components/onewire/test_entity_owserver.py @@ -1,4 +1,6 @@ """Tests for 1-Wire devices connected on OWServer.""" +from unittest.mock import patch + from pyownet.protocol import Error as ProtocolError import pytest @@ -30,7 +32,6 @@ from homeassistant.setup import async_setup_component from . import setup_onewire_patched_owserver_integration -from tests.async_mock import patch from tests.common import mock_device_registry, mock_registry MOCK_DEVICE_SENSORS = { diff --git a/tests/components/onewire/test_entity_sysbus.py b/tests/components/onewire/test_entity_sysbus.py index 3ec46c60837..437fb1bb7d6 100644 --- a/tests/components/onewire/test_entity_sysbus.py +++ b/tests/components/onewire/test_entity_sysbus.py @@ -1,4 +1,6 @@ """Tests for 1-Wire devices connected on SysBus.""" +from unittest.mock import patch + from pi1wire import InvalidCRCException, UnsupportResponseException import pytest @@ -7,7 +9,6 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import mock_device_registry, mock_registry MOCK_CONFIG = { diff --git a/tests/components/onewire/test_init.py b/tests/components/onewire/test_init.py index 8edb5fa0178..38e97206698 100644 --- a/tests/components/onewire/test_init.py +++ b/tests/components/onewire/test_init.py @@ -1,4 +1,6 @@ """Tests for 1-Wire config flow.""" +from unittest.mock import patch + from pyownet.protocol import ConnError, OwnetError from homeassistant.components.onewire.const import CONF_TYPE_OWSERVER, DOMAIN @@ -12,7 +14,6 @@ from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE from . import setup_onewire_owserver_integration, setup_onewire_sysbus_integration -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/onewire/test_sensor.py b/tests/components/onewire/test_sensor.py index ad9580f34ed..9e91da01b21 100644 --- a/tests/components/onewire/test_sensor.py +++ b/tests/components/onewire/test_sensor.py @@ -1,4 +1,6 @@ """Tests for 1-Wire sensor platform.""" +from unittest.mock import patch + from pyownet.protocol import Error as ProtocolError import pytest @@ -8,7 +10,6 @@ from homeassistant.setup import async_setup_component from . import setup_onewire_patched_owserver_integration -from tests.async_mock import patch from tests.common import assert_setup_component, mock_registry MOCK_COUPLERS = { diff --git a/tests/components/onewire/test_switch.py b/tests/components/onewire/test_switch.py index 0c70ad3c9fc..1c778d4e264 100644 --- a/tests/components/onewire/test_switch.py +++ b/tests/components/onewire/test_switch.py @@ -1,5 +1,6 @@ """Tests for 1-Wire devices connected on OWServer.""" import copy +from unittest.mock import patch from pyownet.protocol import Error as ProtocolError import pytest @@ -11,7 +12,6 @@ from homeassistant.setup import async_setup_component from . import setup_onewire_patched_owserver_integration -from tests.async_mock import patch from tests.common import mock_registry MOCK_DEVICE_SENSORS = { diff --git a/tests/components/onvif/test_config_flow.py b/tests/components/onvif/test_config_flow.py index b8be96a123c..1802d211348 100644 --- a/tests/components/onvif/test_config_flow.py +++ b/tests/components/onvif/test_config_flow.py @@ -1,11 +1,12 @@ """Test ONVIF config flow.""" +from unittest.mock import AsyncMock, MagicMock, patch + from onvif.exceptions import ONVIFError from zeep.exceptions import Fault from homeassistant import config_entries, data_entry_flow from homeassistant.components.onvif import config_flow -from tests.async_mock import AsyncMock, MagicMock, patch from tests.common import MockConfigEntry URN = "urn:uuid:123456789" diff --git a/tests/components/openalpr_cloud/test_image_processing.py b/tests/components/openalpr_cloud/test_image_processing.py index 7f285d150b7..e1e9192e1de 100644 --- a/tests/components/openalpr_cloud/test_image_processing.py +++ b/tests/components/openalpr_cloud/test_image_processing.py @@ -1,12 +1,12 @@ """The tests for the openalpr cloud platform.""" import asyncio +from unittest.mock import PropertyMock, patch from homeassistant.components import camera, image_processing as ip from homeassistant.components.openalpr_cloud.image_processing import OPENALPR_API_URL from homeassistant.core import callback from homeassistant.setup import setup_component -from tests.async_mock import PropertyMock, patch from tests.common import assert_setup_component, get_test_home_assistant, load_fixture from tests.components.image_processing import common diff --git a/tests/components/openalpr_local/test_image_processing.py b/tests/components/openalpr_local/test_image_processing.py index d98c27490e8..0cde6e0454b 100644 --- a/tests/components/openalpr_local/test_image_processing.py +++ b/tests/components/openalpr_local/test_image_processing.py @@ -1,10 +1,11 @@ """The tests for the openalpr local platform.""" +from unittest.mock import MagicMock, PropertyMock, patch + import homeassistant.components.image_processing as ip from homeassistant.const import ATTR_ENTITY_PICTURE from homeassistant.core import callback from homeassistant.setup import setup_component -from tests.async_mock import MagicMock, PropertyMock, patch from tests.common import assert_setup_component, get_test_home_assistant, load_fixture from tests.components.image_processing import common diff --git a/tests/components/openerz/test_sensor.py b/tests/components/openerz/test_sensor.py index e616ea4fe4e..24a0f0610af 100644 --- a/tests/components/openerz/test_sensor.py +++ b/tests/components/openerz/test_sensor.py @@ -1,9 +1,9 @@ """Tests for OpenERZ component.""" +from unittest.mock import MagicMock, patch + from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, patch - MOCK_CONFIG = { "sensor": { "platform": "openerz", diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py index e0f4c6eda99..4d811b9f985 100644 --- a/tests/components/opentherm_gw/test_config_flow.py +++ b/tests/components/opentherm_gw/test_config_flow.py @@ -1,5 +1,6 @@ """Test the Opentherm Gateway config flow.""" import asyncio +from unittest.mock import patch from pyotgw.vars import OTGW, OTGW_ABOUT from serial import SerialException @@ -12,7 +13,6 @@ from homeassistant.components.opentherm_gw.const import ( ) from homeassistant.const import CONF_DEVICE, CONF_ID, CONF_NAME, PRECISION_HALVES -from tests.async_mock import patch from tests.common import MockConfigEntry MINIMAL_STATUS = {OTGW: {OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}} diff --git a/tests/components/openuv/test_config_flow.py b/tests/components/openuv/test_config_flow.py index 9c07322acca..83626c2d9f6 100644 --- a/tests/components/openuv/test_config_flow.py +++ b/tests/components/openuv/test_config_flow.py @@ -1,4 +1,6 @@ """Define tests for the OpenUV config flow.""" +from unittest.mock import patch + from pyopenuv.errors import InvalidApiKeyError import pytest @@ -12,7 +14,6 @@ from homeassistant.const import ( CONF_LONGITUDE, ) -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/openweathermap/test_config_flow.py b/tests/components/openweathermap/test_config_flow.py index c4d6be156bf..daa38bc1dc7 100644 --- a/tests/components/openweathermap/test_config_flow.py +++ b/tests/components/openweathermap/test_config_flow.py @@ -1,4 +1,6 @@ """Define tests for the OpenWeatherMap config flow.""" +from unittest.mock import MagicMock, patch + from pyowm.commons.exceptions import APIRequestError, UnauthorizedError from homeassistant import data_entry_flow @@ -17,7 +19,6 @@ from homeassistant.const import ( CONF_NAME, ) -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry CONFIG = { diff --git a/tests/components/ovo_energy/test_config_flow.py b/tests/components/ovo_energy/test_config_flow.py index a7f6ea9b9f2..ccf485211aa 100644 --- a/tests/components/ovo_energy/test_config_flow.py +++ b/tests/components/ovo_energy/test_config_flow.py @@ -1,4 +1,6 @@ """Test the OVO Energy config flow.""" +from unittest.mock import patch + import aiohttp from homeassistant import config_entries, data_entry_flow @@ -6,7 +8,6 @@ from homeassistant.components.ovo_energy.const import DOMAIN 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"} diff --git a/tests/components/owntracks/test_config_flow.py b/tests/components/owntracks/test_config_flow.py index 2290e3e17a4..d6ac059ce26 100644 --- a/tests/components/owntracks/test_config_flow.py +++ b/tests/components/owntracks/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for OwnTracks config flow.""" +from unittest.mock import patch + import pytest from homeassistant import data_entry_flow @@ -9,7 +11,6 @@ from homeassistant.config import async_process_ha_core_config from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import MockConfigEntry CONF_WEBHOOK_URL = "webhook_url" diff --git a/tests/components/owntracks/test_device_tracker.py b/tests/components/owntracks/test_device_tracker.py index 9f3694637f3..c21361c5fff 100644 --- a/tests/components/owntracks/test_device_tracker.py +++ b/tests/components/owntracks/test_device_tracker.py @@ -1,5 +1,6 @@ """The tests for the Owntracks device tracker.""" import json +from unittest.mock import patch import pytest @@ -7,7 +8,6 @@ from homeassistant.components import owntracks from homeassistant.const import STATE_NOT_HOME from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_mqtt_message, mock_coro USER = "greg" diff --git a/tests/components/owntracks/test_helper.py b/tests/components/owntracks/test_helper.py index 6d5139caa14..2c06ac0c4e7 100644 --- a/tests/components/owntracks/test_helper.py +++ b/tests/components/owntracks/test_helper.py @@ -1,10 +1,10 @@ """Test the owntracks_http platform.""" +from unittest.mock import patch + import pytest from homeassistant.components.owntracks import helper -from tests.async_mock import patch - @pytest.fixture(name="nacl_imported") def mock_nacl_imported(): diff --git a/tests/components/ozw/common.py b/tests/components/ozw/common.py index 7a78d11a445..6b44d364413 100644 --- a/tests/components/ozw/common.py +++ b/tests/components/ozw/common.py @@ -1,10 +1,10 @@ """Helpers for tests.""" import json +from unittest.mock import Mock, patch from homeassistant import config_entries from homeassistant.components.ozw.const import DOMAIN -from tests.async_mock import Mock, patch from tests.common import MockConfigEntry diff --git a/tests/components/ozw/conftest.py b/tests/components/ozw/conftest.py index d3f8288658c..b2bd6486d0f 100644 --- a/tests/components/ozw/conftest.py +++ b/tests/components/ozw/conftest.py @@ -1,11 +1,11 @@ """Helpers for tests.""" import json +from unittest.mock import patch import pytest from .common import MQTTMessage -from tests.async_mock import patch from tests.common import load_fixture from tests.components.light.conftest import mock_light_profiles # noqa diff --git a/tests/components/ozw/test_config_flow.py b/tests/components/ozw/test_config_flow.py index e86232adc65..c7ff2512ca7 100644 --- a/tests/components/ozw/test_config_flow.py +++ b/tests/components/ozw/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Z-Wave over MQTT config flow.""" +from unittest.mock import patch + import pytest from homeassistant import config_entries, setup @@ -6,7 +8,6 @@ from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.ozw.config_flow import TITLE from homeassistant.components.ozw.const import DOMAIN -from tests.async_mock import patch from tests.common import MockConfigEntry ADDON_DISCOVERY_INFO = { diff --git a/tests/components/ozw/test_init.py b/tests/components/ozw/test_init.py index efc38fa63c2..ac7ad59f3cb 100644 --- a/tests/components/ozw/test_init.py +++ b/tests/components/ozw/test_init.py @@ -1,11 +1,12 @@ """Test integration initialization.""" +from unittest.mock import patch + from homeassistant import config_entries from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.ozw import DOMAIN, PLATFORMS, const from .common import setup_ozw -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/ozw/test_websocket_api.py b/tests/components/ozw/test_websocket_api.py index 37c6d184c8e..383d3425ffb 100644 --- a/tests/components/ozw/test_websocket_api.py +++ b/tests/components/ozw/test_websocket_api.py @@ -1,4 +1,6 @@ """Test OpenZWave Websocket API.""" +from unittest.mock import patch + from openzwavemqtt.const import ( ATTR_CODE_SLOT, ATTR_LABEL, @@ -40,8 +42,6 @@ from homeassistant.components.websocket_api.const import ( from .common import MQTTMessage, setup_ozw -from tests.async_mock import patch - async def test_websocket_api(hass, generic_data, hass_ws_client): """Test the ozw websocket api.""" diff --git a/tests/components/panasonic_viera/test_config_flow.py b/tests/components/panasonic_viera/test_config_flow.py index 15e6b73202d..e099862604a 100644 --- a/tests/components/panasonic_viera/test_config_flow.py +++ b/tests/components/panasonic_viera/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Panasonic Viera config flow.""" +from unittest.mock import Mock, patch + from panasonic_viera import TV_TYPE_ENCRYPTED, TV_TYPE_NONENCRYPTED, SOAPError import pytest @@ -21,7 +23,6 @@ from homeassistant.components.panasonic_viera.const import ( ) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PIN, CONF_PORT -from tests.async_mock import Mock, patch from tests.common import MockConfigEntry diff --git a/tests/components/panasonic_viera/test_init.py b/tests/components/panasonic_viera/test_init.py index 3ac6b7e12da..8f95043f4fa 100644 --- a/tests/components/panasonic_viera/test_init.py +++ b/tests/components/panasonic_viera/test_init.py @@ -1,4 +1,6 @@ """Test the Panasonic Viera setup process.""" +from unittest.mock import Mock, patch + from homeassistant.components.panasonic_viera.const import ( ATTR_DEVICE_INFO, ATTR_FRIENDLY_NAME, @@ -18,7 +20,6 @@ from homeassistant.config_entries import ENTRY_STATE_NOT_LOADED from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, patch from tests.common import MockConfigEntry MOCK_CONFIG_DATA = { diff --git a/tests/components/panel_custom/test_init.py b/tests/components/panel_custom/test_init.py index ddcb4079ef7..bf67ca23e11 100644 --- a/tests/components/panel_custom/test_init.py +++ b/tests/components/panel_custom/test_init.py @@ -1,9 +1,9 @@ """The tests for the panel_custom component.""" +from unittest.mock import Mock, patch + from homeassistant import setup from homeassistant.components import frontend -from tests.async_mock import Mock, patch - async def test_webcomponent_custom_path_not_found(hass): """Test if a web component is found in config panels dir.""" diff --git a/tests/components/person/test_init.py b/tests/components/person/test_init.py index 64aa583e2f5..86ec71c1452 100644 --- a/tests/components/person/test_init.py +++ b/tests/components/person/test_init.py @@ -1,5 +1,6 @@ """The tests for the person component.""" import logging +from unittest.mock import patch import pytest @@ -24,7 +25,6 @@ from homeassistant.core import Context, CoreState, State from homeassistant.helpers import collection, entity_registry from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import mock_component, mock_restore_cache DEVICE_TRACKER = "device_tracker.test_tracker" diff --git a/tests/components/pi_hole/__init__.py b/tests/components/pi_hole/__init__.py index f487f413363..f2a040f615a 100644 --- a/tests/components/pi_hole/__init__.py +++ b/tests/components/pi_hole/__init__.py @@ -1,4 +1,6 @@ """Tests for the pi_hole component.""" +from unittest.mock import AsyncMock, MagicMock, patch + from hole.exceptions import HoleError from homeassistant.components.pi_hole.const import CONF_LOCATION @@ -11,8 +13,6 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) -from tests.async_mock import AsyncMock, MagicMock, patch - ZERO_DATA = { "ads_blocked_today": 0, "ads_percentage_today": 0, diff --git a/tests/components/pi_hole/test_config_flow.py b/tests/components/pi_hole/test_config_flow.py index 714f211d4f8..28589ab0193 100644 --- a/tests/components/pi_hole/test_config_flow.py +++ b/tests/components/pi_hole/test_config_flow.py @@ -1,5 +1,6 @@ """Test pi_hole config flow.""" import logging +from unittest.mock import patch from homeassistant.components.pi_hole.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER @@ -17,8 +18,6 @@ from . import ( _patch_config_flow_hole, ) -from tests.async_mock import patch - def _flow_next(hass, flow_id): return next( diff --git a/tests/components/pi_hole/test_init.py b/tests/components/pi_hole/test_init.py index 1f3e2451895..ef462270954 100644 --- a/tests/components/pi_hole/test_init.py +++ b/tests/components/pi_hole/test_init.py @@ -1,5 +1,6 @@ """Test pi_hole component.""" import logging +from unittest.mock import AsyncMock from hole.exceptions import HoleError @@ -29,7 +30,6 @@ from . import ( _patch_init_hole, ) -from tests.async_mock import AsyncMock from tests.common import MockConfigEntry diff --git a/tests/components/pilight/test_init.py b/tests/components/pilight/test_init.py index 25ccb3dcf36..b69e03058bf 100644 --- a/tests/components/pilight/test_init.py +++ b/tests/components/pilight/test_init.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging import socket +from unittest.mock import patch from voluptuous import MultipleInvalid @@ -9,7 +10,6 @@ from homeassistant.components import pilight from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from tests.async_mock import patch from tests.common import assert_setup_component, async_fire_time_changed _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/ping/test_binary_sensor.py b/tests/components/ping/test_binary_sensor.py index ae6362939a7..a9af91c9f6f 100644 --- a/tests/components/ping/test_binary_sensor.py +++ b/tests/components/ping/test_binary_sensor.py @@ -1,12 +1,11 @@ """The test for the ping binary_sensor platform.""" from os import path +from unittest.mock import patch from homeassistant import config as hass_config, setup from homeassistant.components.ping import DOMAIN from homeassistant.const import SERVICE_RELOAD -from tests.async_mock import patch - async def test_reload(hass): """Verify we can reload trend sensors.""" diff --git a/tests/components/plex/conftest.py b/tests/components/plex/conftest.py index 1838df32a05..50fcf3eb64d 100644 --- a/tests/components/plex/conftest.py +++ b/tests/components/plex/conftest.py @@ -1,4 +1,6 @@ """Fixtures for Plex tests.""" +from unittest.mock import patch + import pytest from homeassistant.components.plex.const import DOMAIN @@ -7,7 +9,6 @@ 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 from tests.common import MockConfigEntry diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index d8c010ceb92..13754a725db 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -1,6 +1,7 @@ """Tests for Plex config flow.""" import copy import ssl +from unittest.mock import patch import plexapi.exceptions import requests.exceptions @@ -45,7 +46,6 @@ from .mock_classes import ( MockResource, ) -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index a1a159010ef..404f7c167a5 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -2,6 +2,7 @@ import copy from datetime import timedelta import ssl +from unittest.mock import patch import plexapi import requests @@ -20,7 +21,6 @@ from .const import DEFAULT_DATA, DEFAULT_OPTIONS from .helpers import trigger_plex_update, wait_for_debouncer from .mock_classes import MockGDM, MockPlexAccount, MockPlexServer -from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/plex/test_media_players.py b/tests/components/plex/test_media_players.py index a4bda5467e2..3586cbc87bb 100644 --- a/tests/components/plex/test_media_players.py +++ b/tests/components/plex/test_media_players.py @@ -1,10 +1,10 @@ """Tests for Plex media_players.""" +from unittest.mock import patch + from plexapi.exceptions import NotFound from homeassistant.components.plex.const import DOMAIN, SERVERS -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.""" diff --git a/tests/components/plex/test_playback.py b/tests/components/plex/test_playback.py index 5119286d3b8..89c3f0253be 100644 --- a/tests/components/plex/test_playback.py +++ b/tests/components/plex/test_playback.py @@ -1,4 +1,6 @@ """Tests for Plex player playback methods/services.""" +from unittest.mock import patch + from plexapi.exceptions import NotFound from homeassistant.components.media_player.const import ( @@ -17,7 +19,6 @@ from homeassistant.exceptions import HomeAssistantError from .const import DEFAULT_OPTIONS, SECONDARY_DATA -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/plex/test_server.py b/tests/components/plex/test_server.py index 99a324786f6..2f8619834df 100644 --- a/tests/components/plex/test_server.py +++ b/tests/components/plex/test_server.py @@ -1,5 +1,6 @@ """Tests for Plex server.""" import copy +from unittest.mock import patch from plexapi.exceptions import BadRequest, NotFound from requests.exceptions import ConnectionError, RequestException @@ -39,8 +40,6 @@ from .mock_classes import ( MockPlexShow, ) -from tests.async_mock import patch - async def test_new_users_available(hass, entry, mock_websocket, setup_plex_server): """Test setting up when new users available on Plex server.""" diff --git a/tests/components/plex/test_services.py b/tests/components/plex/test_services.py index a3f4d4c833a..06654a736c7 100644 --- a/tests/components/plex/test_services.py +++ b/tests/components/plex/test_services.py @@ -1,4 +1,6 @@ """Tests for various Plex services.""" +from unittest.mock import patch + from homeassistant.components.plex.const import ( CONF_SERVER, CONF_SERVER_IDENTIFIER, @@ -18,7 +20,6 @@ from homeassistant.const import ( from .const import MOCK_SERVERS, MOCK_TOKEN from .mock_classes import MockPlexLibrarySection -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/plugwise/conftest.py b/tests/components/plugwise/conftest.py index ae934c565bc..615cfc55eeb 100644 --- a/tests/components/plugwise/conftest.py +++ b/tests/components/plugwise/conftest.py @@ -2,6 +2,7 @@ from functools import partial import re +from unittest.mock import AsyncMock, Mock, patch import jsonpickle from plugwise.exceptions import ( @@ -12,7 +13,6 @@ from plugwise.exceptions import ( ) import pytest -from tests.async_mock import AsyncMock, Mock, patch from tests.common import load_fixture from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index fc0e5f9e69f..382e7bc1a52 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Plugwise config flow.""" +from unittest.mock import MagicMock, patch + from plugwise.exceptions import ( ConnectionFailedError, InvalidAuthentication, @@ -22,7 +24,6 @@ from homeassistant.const import ( CONF_USERNAME, ) -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry TEST_HOST = "1.1.1.1" diff --git a/tests/components/plum_lightpad/test_config_flow.py b/tests/components/plum_lightpad/test_config_flow.py index f2ac756c4a3..29539b5886f 100644 --- a/tests/components/plum_lightpad/test_config_flow.py +++ b/tests/components/plum_lightpad/test_config_flow.py @@ -1,10 +1,11 @@ """Test the Plum Lightpad config flow.""" +from unittest.mock import patch + from requests.exceptions import ConnectTimeout from homeassistant import config_entries, setup from homeassistant.components.plum_lightpad.const import DOMAIN -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/plum_lightpad/test_init.py b/tests/components/plum_lightpad/test_init.py index 139ab786d1d..f4452dce880 100644 --- a/tests/components/plum_lightpad/test_init.py +++ b/tests/components/plum_lightpad/test_init.py @@ -1,4 +1,6 @@ """Tests for the Plum Lightpad config flow.""" +from unittest.mock import Mock, patch + from aiohttp import ContentTypeError from requests.exceptions import HTTPError @@ -6,7 +8,6 @@ from homeassistant.components.plum_lightpad.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, patch from tests.common import MockConfigEntry diff --git a/tests/components/point/test_config_flow.py b/tests/components/point/test_config_flow.py index 67817b308ce..93ea18f21b6 100644 --- a/tests/components/point/test_config_flow.py +++ b/tests/components/point/test_config_flow.py @@ -1,5 +1,6 @@ """Tests for the Point config flow.""" import asyncio +from unittest.mock import AsyncMock, patch import pytest @@ -7,8 +8,6 @@ from homeassistant import data_entry_flow from homeassistant.components.point import DOMAIN, config_flow from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET -from tests.async_mock import AsyncMock, patch - def init_config_flow(hass, side_effect=None): """Init a configuration flow.""" diff --git a/tests/components/poolsense/test_config_flow.py b/tests/components/poolsense/test_config_flow.py index 8ea05339c84..ca32a21758e 100644 --- a/tests/components/poolsense/test_config_flow.py +++ b/tests/components/poolsense/test_config_flow.py @@ -1,11 +1,11 @@ """Test the PoolSense config flow.""" +from unittest.mock import patch + from homeassistant import data_entry_flow from homeassistant.components.poolsense.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from tests.async_mock import patch - async def test_show_form(hass): """Test that the form is served with no input.""" diff --git a/tests/components/powerwall/mocks.py b/tests/components/powerwall/mocks.py index 6beb31c293a..8de8f5bfa4b 100644 --- a/tests/components/powerwall/mocks.py +++ b/tests/components/powerwall/mocks.py @@ -2,6 +2,7 @@ import json import os +from unittest.mock import MagicMock, Mock from tesla_powerwall import ( DeviceType, @@ -16,7 +17,6 @@ from tesla_powerwall import ( from homeassistant.components.powerwall.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS -from tests.async_mock import MagicMock, Mock from tests.common import load_fixture diff --git a/tests/components/powerwall/test_binary_sensor.py b/tests/components/powerwall/test_binary_sensor.py index caf7519f598..ae56addfbf8 100644 --- a/tests/components/powerwall/test_binary_sensor.py +++ b/tests/components/powerwall/test_binary_sensor.py @@ -1,13 +1,13 @@ """The binary sensor tests for the powerwall platform.""" +from unittest.mock import patch + from homeassistant.components.powerwall.const import DOMAIN from homeassistant.const import STATE_ON from homeassistant.setup import async_setup_component from .mocks import _mock_get_config, _mock_powerwall_with_fixtures -from tests.async_mock import patch - async def test_sensors(hass): """Test creation of the binary sensors.""" diff --git a/tests/components/powerwall/test_config_flow.py b/tests/components/powerwall/test_config_flow.py index 4f1a587b31b..c7aa4782ea3 100644 --- a/tests/components/powerwall/test_config_flow.py +++ b/tests/components/powerwall/test_config_flow.py @@ -1,5 +1,7 @@ """Test the Powerwall config flow.""" +from unittest.mock import patch + from tesla_powerwall import MissingAttributeError, PowerwallUnreachableError from homeassistant import config_entries, setup @@ -8,8 +10,6 @@ from homeassistant.const import CONF_IP_ADDRESS from .mocks import _mock_powerwall_side_effect, _mock_powerwall_site_name -from tests.async_mock import patch - async def test_form_source_user(hass): """Test we get config flow setup form as a user.""" diff --git a/tests/components/powerwall/test_sensor.py b/tests/components/powerwall/test_sensor.py index 0be7c12c5a8..104ed0cbbf3 100644 --- a/tests/components/powerwall/test_sensor.py +++ b/tests/components/powerwall/test_sensor.py @@ -1,13 +1,13 @@ """The sensor tests for the powerwall platform.""" +from unittest.mock import patch + from homeassistant.components.powerwall.const import DOMAIN from homeassistant.const import PERCENTAGE from homeassistant.setup import async_setup_component from .mocks import _mock_get_config, _mock_powerwall_with_fixtures -from tests.async_mock import patch - async def test_sensors(hass): """Test creation of the sensors.""" diff --git a/tests/components/profiler/test_config_flow.py b/tests/components/profiler/test_config_flow.py index dc2ddff14d9..d3b2b473012 100644 --- a/tests/components/profiler/test_config_flow.py +++ b/tests/components/profiler/test_config_flow.py @@ -1,8 +1,9 @@ """Test the Profiler config flow.""" +from unittest.mock import patch + from homeassistant import config_entries, setup from homeassistant.components.profiler.const import DOMAIN -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/profiler/test_init.py b/tests/components/profiler/test_init.py index 0ccda904eb6..efed6ef6126 100644 --- a/tests/components/profiler/test_init.py +++ b/tests/components/profiler/test_init.py @@ -1,6 +1,7 @@ """Test the Profiler config flow.""" from datetime import timedelta import os +from unittest.mock import patch from homeassistant import setup from homeassistant.components.profiler import ( @@ -16,7 +17,6 @@ from homeassistant.components.profiler import ( from homeassistant.components.profiler.const import DOMAIN import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/progettihwsw/test_config_flow.py b/tests/components/progettihwsw/test_config_flow.py index 7a0dbd692c0..883c1acd33b 100644 --- a/tests/components/progettihwsw/test_config_flow.py +++ b/tests/components/progettihwsw/test_config_flow.py @@ -1,4 +1,6 @@ """Test the ProgettiHWSW Automation config flow.""" +from unittest.mock import patch + from homeassistant import config_entries, setup from homeassistant.components.progettihwsw.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PORT @@ -8,7 +10,6 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_FORM, ) -from tests.async_mock import patch from tests.common import MockConfigEntry mock_value_step_user = { diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index f187429b151..bea42cc0888 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -1,6 +1,7 @@ """The tests for the Prometheus exporter.""" from dataclasses import dataclass import datetime +import unittest.mock as mock import pytest @@ -19,8 +20,6 @@ from homeassistant.core import split_entity_id from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -import tests.async_mock as mock - PROMETHEUS_PATH = "homeassistant.components.prometheus" diff --git a/tests/components/ps4/conftest.py b/tests/components/ps4/conftest.py index 42002404db2..821c58d596d 100644 --- a/tests/components/ps4/conftest.py +++ b/tests/components/ps4/conftest.py @@ -1,7 +1,7 @@ """Test configuration for PS4.""" -import pytest +from unittest.mock import patch -from tests.async_mock import patch +import pytest @pytest.fixture diff --git a/tests/components/ps4/test_config_flow.py b/tests/components/ps4/test_config_flow.py index 8ef11335199..bcae74c19fb 100644 --- a/tests/components/ps4/test_config_flow.py +++ b/tests/components/ps4/test_config_flow.py @@ -1,4 +1,6 @@ """Define tests for the PlayStation 4 config flow.""" +from unittest.mock import patch + from pyps4_2ndscreen.errors import CredentialTimeout import pytest @@ -21,7 +23,6 @@ from homeassistant.const import ( ) from homeassistant.util import location -from tests.async_mock import patch from tests.common import MockConfigEntry MOCK_TITLE = "PlayStation 4" diff --git a/tests/components/ps4/test_init.py b/tests/components/ps4/test_init.py index 75a983cc97a..da241022938 100644 --- a/tests/components/ps4/test_init.py +++ b/tests/components/ps4/test_init.py @@ -1,4 +1,6 @@ """Tests for the PS4 Integration.""" +from unittest.mock import MagicMock, patch + from homeassistant import config_entries, data_entry_flow from homeassistant.components import ps4 from homeassistant.components.media_player.const import ( @@ -27,7 +29,6 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component from homeassistant.util import location -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry, mock_registry MOCK_HOST = "192.168.0.1" diff --git a/tests/components/ps4/test_media_player.py b/tests/components/ps4/test_media_player.py index 8a03f13beda..48fb27ab6bc 100644 --- a/tests/components/ps4/test_media_player.py +++ b/tests/components/ps4/test_media_player.py @@ -1,4 +1,6 @@ """Tests for the PS4 media player platform.""" +from unittest.mock import MagicMock, patch + from pyps4_2ndscreen.credential import get_ddp_message from pyps4_2ndscreen.ddp import DEFAULT_UDP_PORT from pyps4_2ndscreen.media_art import TYPE_APP as PS_TYPE_APP @@ -36,7 +38,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry, mock_device_registry, mock_registry MOCK_CREDS = "123412341234abcd12341234abcd12341234abcd12341234abcd12341234abcd" diff --git a/tests/components/ptvsd/test_ptvsd.py b/tests/components/ptvsd/test_ptvsd.py index 93e1bb540db..b3e408833dc 100644 --- a/tests/components/ptvsd/test_ptvsd.py +++ b/tests/components/ptvsd/test_ptvsd.py @@ -1,13 +1,13 @@ """Tests for PTVSD Debugger.""" +from unittest.mock import AsyncMock, patch + from pytest import mark from homeassistant.bootstrap import _async_set_up_integrations import homeassistant.components.ptvsd as ptvsd_component from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock, patch - @mark.skip("causes code cover to fail") async def test_ptvsd(hass): diff --git a/tests/components/pushbullet/test_notify.py b/tests/components/pushbullet/test_notify.py index 12bfc686d17..3eec106019c 100644 --- a/tests/components/pushbullet/test_notify.py +++ b/tests/components/pushbullet/test_notify.py @@ -1,5 +1,6 @@ """The tests for the pushbullet notification platform.""" import json +from unittest.mock import patch from pushbullet import PushBullet import pytest @@ -7,7 +8,6 @@ import pytest import homeassistant.components.notify as notify from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import assert_setup_component, load_fixture diff --git a/tests/components/pvpc_hourly_pricing/test_config_flow.py b/tests/components/pvpc_hourly_pricing/test_config_flow.py index 5f72875c26a..ad321181ec4 100644 --- a/tests/components/pvpc_hourly_pricing/test_config_flow.py +++ b/tests/components/pvpc_hourly_pricing/test_config_flow.py @@ -1,5 +1,6 @@ """Tests for the pvpc_hourly_pricing config_flow.""" from datetime import datetime +from unittest.mock import patch from pytz import timezone @@ -10,7 +11,6 @@ from homeassistant.helpers import entity_registry from .conftest import check_valid_state -from tests.async_mock import patch from tests.common import date_util from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/pvpc_hourly_pricing/test_sensor.py b/tests/components/pvpc_hourly_pricing/test_sensor.py index ca3dec1e891..2045ba52671 100644 --- a/tests/components/pvpc_hourly_pricing/test_sensor.py +++ b/tests/components/pvpc_hourly_pricing/test_sensor.py @@ -1,6 +1,7 @@ """Tests for the pvpc_hourly_pricing sensor component.""" from datetime import datetime, timedelta import logging +from unittest.mock import patch from pytz import timezone @@ -11,7 +12,6 @@ from homeassistant.setup import async_setup_component from .conftest import check_valid_state -from tests.async_mock import patch from tests.common import date_util from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/python_script/test_init.py b/tests/components/python_script/test_init.py index bcd846889fa..b58bd6eb469 100644 --- a/tests/components/python_script/test_init.py +++ b/tests/components/python_script/test_init.py @@ -1,11 +1,11 @@ """Test the python_script component.""" import logging +from unittest.mock import mock_open, patch from homeassistant.components.python_script import DOMAIN, FOLDER, execute from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.setup import async_setup_component -from tests.async_mock import mock_open, patch from tests.common import patch_yaml_files diff --git a/tests/components/qld_bushfire/test_geo_location.py b/tests/components/qld_bushfire/test_geo_location.py index 743d967c953..58974c55b45 100644 --- a/tests/components/qld_bushfire/test_geo_location.py +++ b/tests/components/qld_bushfire/test_geo_location.py @@ -1,5 +1,6 @@ """The tests for the Queensland Bushfire Alert Feed platform.""" import datetime +from unittest.mock import MagicMock, call, patch from homeassistant.components import geo_location from homeassistant.components.geo_location import ATTR_SOURCE @@ -27,7 +28,6 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import MagicMock, call, patch from tests.common import assert_setup_component, async_fire_time_changed CONFIG = {geo_location.DOMAIN: [{"platform": "qld_bushfire", CONF_RADIUS: 200}]} diff --git a/tests/components/qwikswitch/test_init.py b/tests/components/qwikswitch/test_init.py index 7d92d40f987..74e600b50e4 100644 --- a/tests/components/qwikswitch/test_init.py +++ b/tests/components/qwikswitch/test_init.py @@ -1,5 +1,6 @@ """Test qwikswitch sensors.""" import asyncio +from unittest.mock import Mock from aiohttp.client_exceptions import ClientError import pytest @@ -8,7 +9,6 @@ from yarl import URL from homeassistant.components.qwikswitch import DOMAIN as QWIKSWITCH from homeassistant.setup import async_setup_component -from tests.async_mock import Mock from tests.test_util.aiohttp import AiohttpClientMockResponse, MockLongPollSideEffect diff --git a/tests/components/rachio/test_config_flow.py b/tests/components/rachio/test_config_flow.py index 75d671262a1..6b0fc2e69cb 100644 --- a/tests/components/rachio/test_config_flow.py +++ b/tests/components/rachio/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Rachio config flow.""" +from unittest.mock import MagicMock, patch + from homeassistant import config_entries, setup from homeassistant.components.rachio.const import ( CONF_CUSTOM_URL, @@ -7,7 +9,6 @@ from homeassistant.components.rachio.const import ( ) from homeassistant.const import CONF_API_KEY -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry diff --git a/tests/components/radarr/test_sensor.py b/tests/components/radarr/test_sensor.py index aa6a2a02679..514b8f1b817 100644 --- a/tests/components/radarr/test_sensor.py +++ b/tests/components/radarr/test_sensor.py @@ -1,11 +1,11 @@ """The tests for the Radarr platform.""" +from unittest.mock import patch + import pytest from homeassistant.const import DATA_GIGABYTES from homeassistant.setup import async_setup_component -from tests.async_mock import patch - def mocked_exception(*args, **kwargs): """Mock exception thrown by requests.get.""" diff --git a/tests/components/rainmachine/test_config_flow.py b/tests/components/rainmachine/test_config_flow.py index 7fb30d0043d..e79874831fe 100644 --- a/tests/components/rainmachine/test_config_flow.py +++ b/tests/components/rainmachine/test_config_flow.py @@ -1,4 +1,6 @@ """Define tests for the OpenUV config flow.""" +from unittest.mock import patch + from regenmaschine.errors import RainMachineError from homeassistant import data_entry_flow @@ -6,7 +8,6 @@ from homeassistant.components.rainmachine import CONF_ZONE_RUN_TIME, DOMAIN, con from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/random/test_binary_sensor.py b/tests/components/random/test_binary_sensor.py index 85a8e32018c..682c6db58d8 100644 --- a/tests/components/random/test_binary_sensor.py +++ b/tests/components/random/test_binary_sensor.py @@ -1,7 +1,7 @@ """The test for the Random binary sensor platform.""" -from homeassistant.setup import async_setup_component +from unittest.mock import patch -from tests.async_mock import patch +from homeassistant.setup import async_setup_component async def test_random_binary_sensor_on(hass): diff --git a/tests/components/recollect_waste/test_config_flow.py b/tests/components/recollect_waste/test_config_flow.py index ca3fbe8be56..cabcb1a8f9e 100644 --- a/tests/components/recollect_waste/test_config_flow.py +++ b/tests/components/recollect_waste/test_config_flow.py @@ -1,4 +1,6 @@ """Define tests for the ReCollect Waste config flow.""" +from unittest.mock import patch + from aiorecollect.errors import RecollectError from homeassistant import data_entry_flow @@ -10,7 +12,6 @@ from homeassistant.components.recollect_waste import ( from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_FRIENDLY_NAME -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 41c1f52b993..d4092d709c0 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -1,6 +1,7 @@ """The tests for the Recorder component.""" # pylint: disable=protected-access from datetime import datetime, timedelta +from unittest.mock import patch from sqlalchemy.exc import OperationalError @@ -22,7 +23,6 @@ from homeassistant.util import dt as dt_util from .common import wait_recording_done -from tests.async_mock import patch from tests.common import fire_time_changed, get_test_home_assistant diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index d3cf69fc994..d10dad43d75 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -1,4 +1,7 @@ """The tests for the Recorder component.""" +# pylint: disable=protected-access +from unittest.mock import call, patch + import pytest from sqlalchemy import create_engine from sqlalchemy.pool import StaticPool @@ -6,8 +9,6 @@ from sqlalchemy.pool import StaticPool from homeassistant.bootstrap import async_setup_component from homeassistant.components.recorder import const, migration, models -# pylint: disable=protected-access -from tests.async_mock import call, patch from tests.components.recorder import models_original diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index 9cb07819e79..791bd84b11b 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -1,6 +1,7 @@ """Test data purging.""" from datetime import datetime, timedelta import json +from unittest.mock import patch from homeassistant.components import recorder from homeassistant.components.recorder.const import DATA_INSTANCE @@ -11,8 +12,6 @@ from homeassistant.util import dt as dt_util from .common import wait_recording_done -from tests.async_mock import patch - def test_purge_old_states(hass, hass_recorder): """Test deleting old states.""" diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 23ab7ff929d..a4109648d2f 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -2,6 +2,7 @@ from datetime import timedelta import os import sqlite3 +from unittest.mock import MagicMock, patch import pytest @@ -11,7 +12,6 @@ from homeassistant.util import dt as dt_util from .common import wait_recording_done -from tests.async_mock import MagicMock, patch from tests.common import get_test_home_assistant, init_recorder_component diff --git a/tests/components/reddit/test_sensor.py b/tests/components/reddit/test_sensor.py index c49abd4038a..21767792afd 100644 --- a/tests/components/reddit/test_sensor.py +++ b/tests/components/reddit/test_sensor.py @@ -1,5 +1,6 @@ """The tests for the Reddit platform.""" import copy +from unittest.mock import patch from homeassistant.components.reddit.sensor import ( ATTR_BODY, @@ -23,8 +24,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import patch - VALID_CONFIG = { "sensor": { "platform": DOMAIN, diff --git a/tests/components/remember_the_milk/test_init.py b/tests/components/remember_the_milk/test_init.py index e03475c9131..2f3511aeaa8 100644 --- a/tests/components/remember_the_milk/test_init.py +++ b/tests/components/remember_the_milk/test_init.py @@ -1,10 +1,10 @@ """Tests for the Remember The Milk component.""" +from unittest.mock import Mock, mock_open, patch + import homeassistant.components.remember_the_milk as rtm from .const import JSON_STRING, PROFILE, TOKEN -from tests.async_mock import Mock, mock_open, patch - def test_create_new(hass): """Test creating a new config file.""" diff --git a/tests/components/remote/test_device_condition.py b/tests/components/remote/test_device_condition.py index e5a9bc3a9c9..c6a2b3f0c52 100644 --- a/tests/components/remote/test_device_condition.py +++ b/tests/components/remote/test_device_condition.py @@ -1,5 +1,6 @@ """The test for remote device automation.""" from datetime import timedelta +from unittest.mock import patch import pytest @@ -10,7 +11,6 @@ from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import ( MockConfigEntry, async_get_device_automation_capabilities, diff --git a/tests/components/rest/test_binary_sensor.py b/tests/components/rest/test_binary_sensor.py index 2638477ef79..9adb04ea40c 100644 --- a/tests/components/rest/test_binary_sensor.py +++ b/tests/components/rest/test_binary_sensor.py @@ -2,6 +2,7 @@ import asyncio from os import path +from unittest.mock import patch import httpx import respx @@ -18,8 +19,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import patch - async def test_setup_missing_basic_config(hass): """Test setup with configuration missing required entries.""" diff --git a/tests/components/rest/test_notify.py b/tests/components/rest/test_notify.py index 49f3876b97c..aa3e40c2dd4 100644 --- a/tests/components/rest/test_notify.py +++ b/tests/components/rest/test_notify.py @@ -1,5 +1,6 @@ """The tests for the rest.notify platform.""" from os import path +from unittest.mock import patch from homeassistant import config as hass_config import homeassistant.components.notify as notify @@ -7,8 +8,6 @@ from homeassistant.components.rest import DOMAIN from homeassistant.const import SERVICE_RELOAD from homeassistant.setup import async_setup_component -from tests.async_mock import patch - async def test_reload_notify(hass): """Verify we can reload the notify service.""" diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index 16d3f8ba0ac..58309cd7532 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -1,6 +1,7 @@ """The tests for the REST sensor platform.""" import asyncio from os import path +from unittest.mock import patch import httpx import respx @@ -18,8 +19,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import patch - async def test_setup_missing_config(hass): """Test setup with configuration missing required entries.""" diff --git a/tests/components/rflink/test_binary_sensor.py b/tests/components/rflink/test_binary_sensor.py index e20d2554f97..118a3689fc7 100644 --- a/tests/components/rflink/test_binary_sensor.py +++ b/tests/components/rflink/test_binary_sensor.py @@ -5,6 +5,7 @@ Test setup of rflink sensor component/platform. Verify manual and automatic sensor creation. """ from datetime import timedelta +from unittest.mock import patch from homeassistant.components.rflink import CONF_RECONNECT_INTERVAL from homeassistant.const import ( @@ -16,7 +17,6 @@ from homeassistant.const import ( import homeassistant.core as ha import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.rflink.test_init import mock_rflink diff --git a/tests/components/rflink/test_init.py b/tests/components/rflink/test_init.py index 2ae3724ee66..7ba90286e62 100644 --- a/tests/components/rflink/test_init.py +++ b/tests/components/rflink/test_init.py @@ -1,5 +1,7 @@ """Common functions for RFLink component tests and generic platform tests.""" +from unittest.mock import Mock + import pytest from voluptuous.error import MultipleInvalid @@ -15,8 +17,6 @@ from homeassistant.components.rflink import ( ) from homeassistant.const import ATTR_ENTITY_ID, SERVICE_STOP_COVER, SERVICE_TURN_OFF -from tests.async_mock import Mock - async def mock_rflink( hass, config, domain, monkeypatch, failures=None, failcommand=False diff --git a/tests/components/rfxtrx/conftest.py b/tests/components/rfxtrx/conftest.py index 563fb362e5a..ee695bee9dd 100644 --- a/tests/components/rfxtrx/conftest.py +++ b/tests/components/rfxtrx/conftest.py @@ -1,5 +1,6 @@ """Common test tools.""" from datetime import timedelta +from unittest.mock import patch import pytest @@ -7,7 +8,6 @@ from homeassistant.components import rfxtrx from homeassistant.components.rfxtrx import DOMAIN from homeassistant.util.dt import utcnow -from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_time_changed from tests.components.light.conftest import mock_light_profiles # noqa diff --git a/tests/components/rfxtrx/test_config_flow.py b/tests/components/rfxtrx/test_config_flow.py index 04545a1a422..5d4f5edaf2a 100644 --- a/tests/components/rfxtrx/test_config_flow.py +++ b/tests/components/rfxtrx/test_config_flow.py @@ -1,5 +1,6 @@ """Test the Tado config flow.""" import os +from unittest.mock import MagicMock, patch, sentinel import serial.tools.list_ports @@ -13,7 +14,6 @@ from homeassistant.helpers.entity_registry import ( async_get_registry as async_get_entity_registry, ) -from tests.async_mock import MagicMock, patch, sentinel from tests.common import MockConfigEntry diff --git a/tests/components/rfxtrx/test_init.py b/tests/components/rfxtrx/test_init.py index 037b08b7cc6..c2e1870638f 100644 --- a/tests/components/rfxtrx/test_init.py +++ b/tests/components/rfxtrx/test_init.py @@ -1,11 +1,12 @@ """The tests for the Rfxtrx component.""" +from unittest.mock import call + from homeassistant.components.rfxtrx import DOMAIN from homeassistant.components.rfxtrx.const import EVENT_RFXTRX_EVENT from homeassistant.core import callback from homeassistant.setup import async_setup_component -from tests.async_mock import call from tests.common import MockConfigEntry from tests.components.rfxtrx.conftest import create_rfx_test_cfg diff --git a/tests/components/ring/common.py b/tests/components/ring/common.py index 39b5c339677..93a6e4f91e0 100644 --- a/tests/components/ring/common.py +++ b/tests/components/ring/common.py @@ -1,8 +1,9 @@ """Common methods used across the tests for ring devices.""" +from unittest.mock import patch + from homeassistant.components.ring import DOMAIN from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/ring/test_binary_sensor.py b/tests/components/ring/test_binary_sensor.py index 8edaf2c229b..0b73c739503 100644 --- a/tests/components/ring/test_binary_sensor.py +++ b/tests/components/ring/test_binary_sensor.py @@ -1,10 +1,9 @@ """The tests for the Ring binary sensor platform.""" from time import time +from unittest.mock import patch from .common import setup_platform -from tests.async_mock import patch - async def test_binary_sensor(hass, requests_mock): """Test the Ring binary sensors.""" diff --git a/tests/components/ring/test_config_flow.py b/tests/components/ring/test_config_flow.py index fc2b490f560..85ca4ffb558 100644 --- a/tests/components/ring/test_config_flow.py +++ b/tests/components/ring/test_config_flow.py @@ -1,10 +1,10 @@ """Test the Ring config flow.""" +from unittest.mock import Mock, patch + from homeassistant import config_entries, setup from homeassistant.components.ring import DOMAIN from homeassistant.components.ring.config_flow import InvalidAuth -from tests.async_mock import Mock, patch - async def test_form(hass): """Test we get the form.""" diff --git a/tests/components/risco/test_alarm_control_panel.py b/tests/components/risco/test_alarm_control_panel.py index bf7c971df54..5bde97f8abd 100644 --- a/tests/components/risco/test_alarm_control_panel.py +++ b/tests/components/risco/test_alarm_control_panel.py @@ -1,4 +1,6 @@ """Tests for the Risco alarm control panel device.""" +from unittest.mock import MagicMock, PropertyMock, patch + import pytest from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN @@ -29,7 +31,6 @@ from homeassistant.helpers.entity_component import async_update_entity from .util import TEST_CONFIG, TEST_SITE_UUID, setup_risco -from tests.async_mock import MagicMock, PropertyMock, patch from tests.common import MockConfigEntry FIRST_ENTITY_ID = "alarm_control_panel.risco_test_site_name_partition_0" diff --git a/tests/components/risco/test_binary_sensor.py b/tests/components/risco/test_binary_sensor.py index 50b9c43c5c3..2f750ff6d35 100644 --- a/tests/components/risco/test_binary_sensor.py +++ b/tests/components/risco/test_binary_sensor.py @@ -1,4 +1,6 @@ """Tests for the Risco binary sensors.""" +from unittest.mock import PropertyMock, patch + from homeassistant.components.risco import CannotConnectError, UnauthorizedError from homeassistant.components.risco.const import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON @@ -7,7 +9,6 @@ from homeassistant.helpers.entity_component import async_update_entity from .util import TEST_CONFIG, TEST_SITE_UUID, setup_risco from .util import two_zone_alarm # noqa: F401 -from tests.async_mock import PropertyMock, patch from tests.common import MockConfigEntry FIRST_ENTITY_ID = "binary_sensor.zone_0" diff --git a/tests/components/risco/test_config_flow.py b/tests/components/risco/test_config_flow.py index ba14a52553e..cfb1a410960 100644 --- a/tests/components/risco/test_config_flow.py +++ b/tests/components/risco/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Risco config flow.""" +from unittest.mock import PropertyMock, patch + import pytest import voluptuous as vol @@ -9,7 +11,6 @@ from homeassistant.components.risco.config_flow import ( ) from homeassistant.components.risco.const import DOMAIN -from tests.async_mock import PropertyMock, patch from tests.common import MockConfigEntry TEST_SITE_NAME = "test-site-name" diff --git a/tests/components/risco/test_sensor.py b/tests/components/risco/test_sensor.py index acb92088478..09726727901 100644 --- a/tests/components/risco/test_sensor.py +++ b/tests/components/risco/test_sensor.py @@ -1,4 +1,6 @@ """Tests for the Risco event sensors.""" +from unittest.mock import MagicMock, patch + from homeassistant.components.risco import ( LAST_EVENT_TIMESTAMP_KEY, CannotConnectError, @@ -9,7 +11,6 @@ from homeassistant.components.risco.const import DOMAIN, EVENTS_COORDINATOR from .util import TEST_CONFIG, setup_risco from .util import two_zone_alarm # noqa: F401 -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry ENTITY_IDS = { diff --git a/tests/components/risco/util.py b/tests/components/risco/util.py index 704c12fb846..009f8c22fb1 100644 --- a/tests/components/risco/util.py +++ b/tests/components/risco/util.py @@ -1,10 +1,11 @@ """Utilities for Risco tests.""" +from unittest.mock import MagicMock, PropertyMock, patch + from pytest import fixture from homeassistant.components.risco.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_PIN, CONF_USERNAME -from tests.async_mock import MagicMock, PropertyMock, patch from tests.common import MockConfigEntry TEST_CONFIG = { diff --git a/tests/components/rmvtransport/test_sensor.py b/tests/components/rmvtransport/test_sensor.py index 935368f03b1..a1022d2f4b0 100644 --- a/tests/components/rmvtransport/test_sensor.py +++ b/tests/components/rmvtransport/test_sensor.py @@ -1,10 +1,9 @@ """The tests for the rmvtransport platform.""" import datetime +from unittest.mock import patch from homeassistant.setup import async_setup_component -from tests.async_mock import patch - VALID_CONFIG_MINIMAL = { "sensor": {"platform": "rmvtransport", "next_departure": [{"station": "3000010"}]} } diff --git a/tests/components/roku/test_config_flow.py b/tests/components/roku/test_config_flow.py index 16e4a434dc3..ed1b042e328 100644 --- a/tests/components/roku/test_config_flow.py +++ b/tests/components/roku/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Roku config flow.""" +from unittest.mock import patch + from homeassistant.components.roku.const import DOMAIN from homeassistant.config_entries import SOURCE_HOMEKIT, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE @@ -10,7 +12,6 @@ from homeassistant.data_entry_flow import ( from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.components.roku import ( HOMEKIT_HOST, HOST, diff --git a/tests/components/roku/test_init.py b/tests/components/roku/test_init.py index b929a48ee25..a5f16c6071f 100644 --- a/tests/components/roku/test_init.py +++ b/tests/components/roku/test_init.py @@ -1,4 +1,6 @@ """Tests for the Roku integration.""" +from unittest.mock import patch + from homeassistant.components.roku.const import DOMAIN from homeassistant.config_entries import ( ENTRY_STATE_LOADED, @@ -7,7 +9,6 @@ from homeassistant.config_entries import ( ) from homeassistant.helpers.typing import HomeAssistantType -from tests.async_mock import patch from tests.components.roku import setup_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index 23dd9dbc6c8..5e9e4c43d54 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -1,5 +1,6 @@ """Tests for the Roku Media Player platform.""" from datetime import timedelta +from unittest.mock import patch from rokuecp import RokuError @@ -62,7 +63,6 @@ from homeassistant.const import ( from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import dt as dt_util -from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.roku import UPNP_SERIAL, setup_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/roku/test_remote.py b/tests/components/roku/test_remote.py index 96426e5b10a..4122e0af1d1 100644 --- a/tests/components/roku/test_remote.py +++ b/tests/components/roku/test_remote.py @@ -1,4 +1,6 @@ """The tests for the Roku remote platform.""" +from unittest.mock import patch + from homeassistant.components.remote import ( ATTR_COMMAND, DOMAIN as REMOTE_DOMAIN, @@ -7,7 +9,6 @@ from homeassistant.components.remote import ( from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.helpers.typing import HomeAssistantType -from tests.async_mock import patch from tests.components.roku import UPNP_SERIAL, setup_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/roomba/test_config_flow.py b/tests/components/roomba/test_config_flow.py index 253250d7d49..54ef229ec49 100644 --- a/tests/components/roomba/test_config_flow.py +++ b/tests/components/roomba/test_config_flow.py @@ -1,4 +1,6 @@ """Test the iRobot Roomba config flow.""" +from unittest.mock import MagicMock, PropertyMock, patch + from roombapy import RoombaConnectionError from homeassistant import config_entries, data_entry_flow, setup @@ -10,7 +12,6 @@ from homeassistant.components.roomba.const import ( ) from homeassistant.const import CONF_HOST, CONF_PASSWORD -from tests.async_mock import MagicMock, PropertyMock, patch from tests.common import MockConfigEntry VALID_CONFIG = {CONF_HOST: "1.2.3.4", CONF_BLID: "blid", CONF_PASSWORD: "password"} diff --git a/tests/components/roon/test_config_flow.py b/tests/components/roon/test_config_flow.py index 7ffac08d9f6..6fcfc048d4b 100644 --- a/tests/components/roon/test_config_flow.py +++ b/tests/components/roon/test_config_flow.py @@ -1,9 +1,10 @@ """Test the roon config flow.""" +from unittest.mock import patch + from homeassistant import config_entries, setup from homeassistant.components.roon.const import DOMAIN from homeassistant.const import CONF_HOST -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/rpi_power/test_binary_sensor.py b/tests/components/rpi_power/test_binary_sensor.py index 873f654aa3b..13d452a2902 100644 --- a/tests/components/rpi_power/test_binary_sensor.py +++ b/tests/components/rpi_power/test_binary_sensor.py @@ -1,6 +1,7 @@ """Tests for rpi_power binary sensor.""" from datetime import timedelta import logging +from unittest.mock import MagicMock from homeassistant.components.rpi_power.binary_sensor import ( DESCRIPTION_NORMALIZED, @@ -11,7 +12,6 @@ from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from tests.async_mock import MagicMock from tests.common import MockConfigEntry, async_fire_time_changed, patch ENTITY_ID = "binary_sensor.rpi_power_status" diff --git a/tests/components/rpi_power/test_config_flow.py b/tests/components/rpi_power/test_config_flow.py index 090b6a6a793..7e302b51512 100644 --- a/tests/components/rpi_power/test_config_flow.py +++ b/tests/components/rpi_power/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for rpi_power config flow.""" +from unittest.mock import MagicMock + from homeassistant.components.rpi_power.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant @@ -8,7 +10,6 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_FORM, ) -from tests.async_mock import MagicMock from tests.common import patch MODULE = "homeassistant.components.rpi_power.config_flow.new_under_voltage" diff --git a/tests/components/ruckus_unleashed/__init__.py b/tests/components/ruckus_unleashed/__init__.py index 7efbfc03457..eff80a0387a 100644 --- a/tests/components/ruckus_unleashed/__init__.py +++ b/tests/components/ruckus_unleashed/__init__.py @@ -1,4 +1,6 @@ """Tests for the Ruckus Unleashed integration.""" +from unittest.mock import patch + from homeassistant.components.ruckus_unleashed import DOMAIN from homeassistant.components.ruckus_unleashed.const import ( API_ACCESS_POINT, @@ -15,7 +17,6 @@ from homeassistant.components.ruckus_unleashed.const import ( ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import patch from tests.common import MockConfigEntry DEFAULT_TITLE = "Ruckus Mesh" diff --git a/tests/components/ruckus_unleashed/test_config_flow.py b/tests/components/ruckus_unleashed/test_config_flow.py index 39112dd44aa..a11943bff00 100644 --- a/tests/components/ruckus_unleashed/test_config_flow.py +++ b/tests/components/ruckus_unleashed/test_config_flow.py @@ -1,5 +1,6 @@ """Test the Ruckus Unleashed config flow.""" from datetime import timedelta +from unittest.mock import patch from pyruckus.exceptions import AuthenticationError @@ -7,7 +8,6 @@ from homeassistant import config_entries from homeassistant.components.ruckus_unleashed.const import DOMAIN from homeassistant.util import utcnow -from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.ruckus_unleashed import CONFIG, DEFAULT_SYSTEM_INFO, DEFAULT_TITLE diff --git a/tests/components/ruckus_unleashed/test_device_tracker.py b/tests/components/ruckus_unleashed/test_device_tracker.py index 38736b0117f..37bae441abf 100644 --- a/tests/components/ruckus_unleashed/test_device_tracker.py +++ b/tests/components/ruckus_unleashed/test_device_tracker.py @@ -1,5 +1,6 @@ """The sensor tests for the Ruckus Unleashed platform.""" from datetime import timedelta +from unittest.mock import patch from homeassistant.components.ruckus_unleashed import API_MAC, DOMAIN from homeassistant.components.ruckus_unleashed.const import API_AP, API_ID, API_NAME @@ -8,7 +9,6 @@ from homeassistant.helpers import entity_registry from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.util import utcnow -from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.ruckus_unleashed import ( DEFAULT_AP_INFO, diff --git a/tests/components/ruckus_unleashed/test_init.py b/tests/components/ruckus_unleashed/test_init.py index 5c694a5ee24..7b379a51011 100644 --- a/tests/components/ruckus_unleashed/test_init.py +++ b/tests/components/ruckus_unleashed/test_init.py @@ -1,4 +1,6 @@ """Test the Ruckus Unleashed config flow.""" +from unittest.mock import patch + from pyruckus.exceptions import AuthenticationError from homeassistant.components.ruckus_unleashed import ( @@ -19,7 +21,6 @@ from homeassistant.config_entries import ( ) from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from tests.async_mock import patch from tests.components.ruckus_unleashed import ( DEFAULT_AP_INFO, DEFAULT_SYSTEM_INFO, diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index f8a362e9842..ea78ecacb3e 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for Samsung TV config flow.""" +from unittest.mock import DEFAULT as DEFAULT_MOCK, Mock, PropertyMock, call, patch + import pytest from samsungctl.exceptions import AccessDenied, UnhandledResponse from samsungtvws.exceptions import ConnectionFailure @@ -18,8 +20,6 @@ from homeassistant.components.ssdp import ( ) from homeassistant.const import CONF_HOST, CONF_ID, CONF_METHOD, CONF_NAME, CONF_TOKEN -from tests.async_mock import DEFAULT as DEFAULT_MOCK, Mock, PropertyMock, call, patch - MOCK_USER_DATA = {CONF_HOST: "fake_host", CONF_NAME: "fake_name"} MOCK_SSDP_DATA = { ATTR_SSDP_LOCATION: "https://fake_host:12345/test", diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index 5ef47cb3106..bb19f120cf6 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -1,4 +1,6 @@ """Tests for the Samsung TV Integration.""" +from unittest.mock import Mock, call, patch + import pytest from homeassistant.components.media_player.const import DOMAIN, SUPPORT_TURN_ON @@ -16,8 +18,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, call, patch - ENTITY_ID = f"{DOMAIN}.fake_name" MOCK_CONFIG = { SAMSUNGTV_DOMAIN: [ diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index cc9a0f37ec8..6415f02cdf5 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -2,6 +2,7 @@ import asyncio from datetime import timedelta import logging +from unittest.mock import DEFAULT as DEFAULT_MOCK, Mock, PropertyMock, call, patch import pytest from samsungctl import exceptions @@ -52,7 +53,6 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import DEFAULT as DEFAULT_MOCK, Mock, PropertyMock, call, patch from tests.common import MockConfigEntry, async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake" diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index 068b0b91a2c..95703949525 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -2,6 +2,7 @@ # pylint: disable=protected-access import asyncio import unittest +from unittest.mock import Mock, patch import pytest @@ -23,7 +24,6 @@ from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.loader import bind_hass from homeassistant.setup import async_setup_component, setup_component -from tests.async_mock import Mock, patch from tests.common import async_mock_service, get_test_home_assistant from tests.components.logbook.test_init import MockLazyEventPartialState diff --git a/tests/components/season/test_sensor.py b/tests/components/season/test_sensor.py index 943036e9c00..d1c6cfb0b9f 100644 --- a/tests/components/season/test_sensor.py +++ b/tests/components/season/test_sensor.py @@ -1,5 +1,6 @@ """The tests for the Season sensor platform.""" from datetime import datetime +from unittest.mock import patch import pytest @@ -14,8 +15,6 @@ from homeassistant.components.season.sensor import ( from homeassistant.const import STATE_UNKNOWN from homeassistant.setup import async_setup_component -from tests.async_mock import patch - HEMISPHERE_NORTHERN = { "homeassistant": {"latitude": "48.864716", "longitude": "2.349014"}, "sensor": {"platform": "season", "type": "astronomical"}, diff --git a/tests/components/sense/test_config_flow.py b/tests/components/sense/test_config_flow.py index 44bd9c7265c..41cfdc017dd 100644 --- a/tests/components/sense/test_config_flow.py +++ b/tests/components/sense/test_config_flow.py @@ -1,11 +1,11 @@ """Test the Sense config flow.""" +from unittest.mock import patch + from sense_energy import SenseAPITimeoutException, SenseAuthenticationException from homeassistant import config_entries, setup from homeassistant.components.sense.const import DOMAIN -from tests.async_mock import patch - async def test_form(hass): """Test we get the form.""" diff --git a/tests/components/sentry/test_config_flow.py b/tests/components/sentry/test_config_flow.py index 9ae572123b5..82a1a70ec8b 100644 --- a/tests/components/sentry/test_config_flow.py +++ b/tests/components/sentry/test_config_flow.py @@ -1,5 +1,6 @@ """Test the sentry config flow.""" import logging +from unittest.mock import patch from sentry_sdk.utils import BadDsn @@ -18,7 +19,6 @@ from homeassistant.config_entries import SOURCE_USER from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/sentry/test_init.py b/tests/components/sentry/test_init.py index 95bda9738b8..e920437b2f7 100644 --- a/tests/components/sentry/test_init.py +++ b/tests/components/sentry/test_init.py @@ -1,5 +1,6 @@ """Tests for Sentry integration.""" import logging +from unittest.mock import MagicMock, Mock, patch import pytest @@ -17,7 +18,6 @@ from homeassistant.components.sentry.const import ( from homeassistant.const import __version__ as current_version from homeassistant.core import HomeAssistant -from tests.async_mock import MagicMock, Mock, patch from tests.common import MockConfigEntry diff --git a/tests/components/seventeentrack/test_sensor.py b/tests/components/seventeentrack/test_sensor.py index 798620e2af7..6519b435c0a 100644 --- a/tests/components/seventeentrack/test_sensor.py +++ b/tests/components/seventeentrack/test_sensor.py @@ -1,6 +1,7 @@ """Tests for the seventeentrack sensor.""" import datetime from typing import Union +from unittest.mock import MagicMock, patch from py17track.package import Package import pytest @@ -13,7 +14,6 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.setup import async_setup_component from homeassistant.util import utcnow -from tests.async_mock import MagicMock, patch from tests.common import async_fire_time_changed VALID_CONFIG_MINIMAL = { diff --git a/tests/components/sharkiq/test_config_flow.py b/tests/components/sharkiq/test_config_flow.py index 3183f6fdee2..890efbf1679 100644 --- a/tests/components/sharkiq/test_config_flow.py +++ b/tests/components/sharkiq/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Shark IQ config flow.""" +from unittest.mock import patch + import aiohttp import pytest from sharkiqpy import AylaApi, SharkIqAuthError @@ -9,7 +11,6 @@ from homeassistant.core import HomeAssistant from .const import CONFIG, TEST_PASSWORD, TEST_USERNAME, UNIQUE_ID -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/sharkiq/test_vacuum.py b/tests/components/sharkiq/test_vacuum.py index c548b59f5ba..a894869a723 100644 --- a/tests/components/sharkiq/test_vacuum.py +++ b/tests/components/sharkiq/test_vacuum.py @@ -2,6 +2,7 @@ from copy import deepcopy import enum from typing import Any, Iterable, List, Optional +from unittest.mock import patch import pytest from sharkiqpy import AylaApi, SharkIqAuthError, SharkIqNotAuthedError, SharkIqVacuum @@ -56,7 +57,6 @@ from .const import ( TEST_USERNAME, ) -from tests.async_mock import patch from tests.common import MockConfigEntry VAC_ENTITY_ID = f"vacuum.{SHARK_DEVICE_DICT['product_name'].lower()}" diff --git a/tests/components/shell_command/test_init.py b/tests/components/shell_command/test_init.py index f5ad37cc617..928c186bc11 100644 --- a/tests/components/shell_command/test_init.py +++ b/tests/components/shell_command/test_init.py @@ -3,12 +3,11 @@ import os import tempfile from typing import Tuple +from unittest.mock import MagicMock, patch from homeassistant.components import shell_command from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, patch - def mock_process_creator(error: bool = False): """Mock a coroutine that creates a process when yielded.""" diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 3f97c0ef317..eb19813dc95 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -1,7 +1,7 @@ """Test configuration for Shelly.""" -import pytest +from unittest.mock import patch -from tests.async_mock import patch +import pytest @pytest.fixture(autouse=True) diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 2850be11450..ca62e41abd6 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -1,5 +1,6 @@ """Test the Shelly config flow.""" import asyncio +from unittest.mock import AsyncMock, Mock, patch import aiohttp import aioshelly @@ -8,7 +9,6 @@ import pytest from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.shelly.const import DOMAIN -from tests.async_mock import AsyncMock, Mock, patch from tests.common import MockConfigEntry MOCK_SETTINGS = { diff --git a/tests/components/shopping_list/conftest.py b/tests/components/shopping_list/conftest.py index 8307a6845b1..596a8c87cd3 100644 --- a/tests/components/shopping_list/conftest.py +++ b/tests/components/shopping_list/conftest.py @@ -1,9 +1,10 @@ """Shopping list test helpers.""" +from unittest.mock import patch + import pytest from homeassistant.components.shopping_list import intent as sl_intent -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/signal_messenger/test_notify.py b/tests/components/signal_messenger/test_notify.py index 407068e6f3e..4bb6cbdd197 100644 --- a/tests/components/signal_messenger/test_notify.py +++ b/tests/components/signal_messenger/test_notify.py @@ -3,6 +3,7 @@ import os import tempfile import unittest +from unittest.mock import patch from pysignalclirestapi import SignalCliRestApi import requests_mock @@ -10,8 +11,6 @@ import requests_mock import homeassistant.components.signal_messenger.notify as signalmessenger from homeassistant.setup import async_setup_component -from tests.async_mock import patch - BASE_COMPONENT = "notify" diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index ec7ad592f15..8f9d3a9897c 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -1,4 +1,6 @@ """Define tests for the SimpliSafe config flow.""" +from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch + from simplipy.errors import ( InvalidCredentialsError, PendingAuthorizationError, @@ -10,7 +12,6 @@ from homeassistant.components.simplisafe import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME -from tests.async_mock import AsyncMock, MagicMock, PropertyMock, patch from tests.common import MockConfigEntry diff --git a/tests/components/slack/test_notify.py b/tests/components/slack/test_notify.py index bfd78b20900..7f974b557fe 100644 --- a/tests/components/slack/test_notify.py +++ b/tests/components/slack/test_notify.py @@ -1,10 +1,8 @@ """Test slack notifications.""" -from unittest.mock import Mock +from unittest.mock import AsyncMock, Mock from homeassistant.components.slack.notify import SlackNotificationService -from tests.async_mock import AsyncMock - async def test_message_includes_default_emoji(): """Tests that default icon is used when no message icon is given.""" diff --git a/tests/components/sleepiq/test_binary_sensor.py b/tests/components/sleepiq/test_binary_sensor.py index c1577a21cd2..c158554b278 100644 --- a/tests/components/sleepiq/test_binary_sensor.py +++ b/tests/components/sleepiq/test_binary_sensor.py @@ -1,8 +1,9 @@ """The tests for SleepIQ binary sensor platform.""" +from unittest.mock import MagicMock + from homeassistant.components.sleepiq import binary_sensor as sleepiq from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock from tests.components.sleepiq.test_init import mock_responses CONFIG = {"username": "foo", "password": "bar"} diff --git a/tests/components/sleepiq/test_init.py b/tests/components/sleepiq/test_init.py index 70aaca2e17d..a5e8e43ae07 100644 --- a/tests/components/sleepiq/test_init.py +++ b/tests/components/sleepiq/test_init.py @@ -1,8 +1,9 @@ """The tests for the SleepIQ component.""" +from unittest.mock import MagicMock, patch + from homeassistant import setup import homeassistant.components.sleepiq as sleepiq -from tests.async_mock import MagicMock, patch from tests.common import load_fixture CONFIG = {"sleepiq": {"username": "foo", "password": "bar"}} diff --git a/tests/components/sleepiq/test_sensor.py b/tests/components/sleepiq/test_sensor.py index 53ee4de973f..559e808f554 100644 --- a/tests/components/sleepiq/test_sensor.py +++ b/tests/components/sleepiq/test_sensor.py @@ -1,8 +1,9 @@ """The tests for SleepIQ sensor platform.""" +from unittest.mock import MagicMock + import homeassistant.components.sleepiq.sensor as sleepiq from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock from tests.components.sleepiq.test_init import mock_responses CONFIG = {"username": "foo", "password": "bar"} diff --git a/tests/components/smappee/test_config_flow.py b/tests/components/smappee/test_config_flow.py index 55d063c2b1c..cba962d3e44 100644 --- a/tests/components/smappee/test_config_flow.py +++ b/tests/components/smappee/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Smappee component config flow module.""" +from unittest.mock import patch + from homeassistant import data_entry_flow, setup from homeassistant.components.smappee.const import ( CONF_HOSTNAME, @@ -12,7 +14,6 @@ from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow -from tests.async_mock import patch from tests.common import MockConfigEntry CLIENT_ID = "1234" diff --git a/tests/components/smappee/test_init.py b/tests/components/smappee/test_init.py index 9a81441e8b3..a3cd9897d9c 100644 --- a/tests/components/smappee/test_init.py +++ b/tests/components/smappee/test_init.py @@ -1,8 +1,9 @@ """Tests for the Smappee component init module.""" +from unittest.mock import patch + from homeassistant.components.smappee.const import DOMAIN from homeassistant.config_entries import SOURCE_ZEROCONF -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/smart_meter_texas/test_config_flow.py b/tests/components/smart_meter_texas/test_config_flow.py index 4908a50e57d..d0108f2ee09 100644 --- a/tests/components/smart_meter_texas/test_config_flow.py +++ b/tests/components/smart_meter_texas/test_config_flow.py @@ -1,5 +1,6 @@ """Test the Smart Meter Texas config flow.""" import asyncio +from unittest.mock import patch from aiohttp import ClientError import pytest @@ -12,7 +13,6 @@ from homeassistant import config_entries, setup from homeassistant.components.smart_meter_texas.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import patch from tests.common import MockConfigEntry TEST_LOGIN = {CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"} diff --git a/tests/components/smart_meter_texas/test_init.py b/tests/components/smart_meter_texas/test_init.py index 861425601ec..7db4113e3cf 100644 --- a/tests/components/smart_meter_texas/test_init.py +++ b/tests/components/smart_meter_texas/test_init.py @@ -1,4 +1,6 @@ """Test the Smart Meter Texas module.""" +from unittest.mock import patch + from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, SERVICE_UPDATE_ENTITY, @@ -15,8 +17,6 @@ from homeassistant.setup import async_setup_component from .conftest import TEST_ENTITY_ID, setup_integration -from tests.async_mock import patch - async def test_setup_with_no_config(hass): """Test that no config is successful.""" diff --git a/tests/components/smart_meter_texas/test_sensor.py b/tests/components/smart_meter_texas/test_sensor.py index 104da011d90..774a369c83a 100644 --- a/tests/components/smart_meter_texas/test_sensor.py +++ b/tests/components/smart_meter_texas/test_sensor.py @@ -1,4 +1,6 @@ """Test the Smart Meter Texas sensor entity.""" +from unittest.mock import patch + from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, SERVICE_UPDATE_ENTITY, @@ -13,8 +15,6 @@ from homeassistant.setup import async_setup_component from .conftest import TEST_ENTITY_ID, refresh_data, setup_integration -from tests.async_mock import patch - async def test_sensor(hass, config_entry, aioclient_mock): """Test that the sensor is setup.""" diff --git a/tests/components/smarthab/test_config_flow.py b/tests/components/smarthab/test_config_flow.py index 6b8c58b1f70..6201d6f6f28 100644 --- a/tests/components/smarthab/test_config_flow.py +++ b/tests/components/smarthab/test_config_flow.py @@ -1,12 +1,12 @@ """Test the SmartHab config flow.""" +from unittest.mock import patch + import pysmarthab from homeassistant import config_entries, setup from homeassistant.components.smarthab import DOMAIN from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from tests.async_mock import patch - async def test_form(hass): """Test we get the form.""" diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index 8588c81654e..b99309bea52 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -1,5 +1,6 @@ """Test configuration and mocks for the SmartThings component.""" import secrets +from unittest.mock import Mock, patch from uuid import uuid4 from pysmartthings import ( @@ -45,7 +46,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, patch from tests.common import MockConfigEntry from tests.components.light.conftest import mock_light_profiles # noqa diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index b776959ea5b..d8e0f6ed784 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the SmartThings config flow module.""" +from unittest.mock import AsyncMock, Mock, patch from uuid import uuid4 from aiohttp import ClientResponseError @@ -23,7 +24,6 @@ from homeassistant.const import ( HTTP_UNAUTHORIZED, ) -from tests.async_mock import AsyncMock, Mock, patch from tests.common import MockConfigEntry diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index 931e895cb65..9024b72bb85 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -1,4 +1,5 @@ """Tests for the SmartThings component init module.""" +from unittest.mock import Mock, patch from uuid import uuid4 from aiohttp import ClientConnectionError, ClientResponseError @@ -22,7 +23,6 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, patch from tests.common import MockConfigEntry diff --git a/tests/components/smartthings/test_smartapp.py b/tests/components/smartthings/test_smartapp.py index 42215def82f..7f26b26d577 100644 --- a/tests/components/smartthings/test_smartapp.py +++ b/tests/components/smartthings/test_smartapp.py @@ -1,4 +1,5 @@ """Tests for the smartapp module.""" +from unittest.mock import AsyncMock, Mock, patch from uuid import uuid4 from pysmartthings import CAPABILITIES, AppEntity, Capability @@ -10,7 +11,6 @@ from homeassistant.components.smartthings.const import ( DOMAIN, ) -from tests.async_mock import AsyncMock, Mock, patch from tests.common import MockConfigEntry diff --git a/tests/components/smhi/common.py b/tests/components/smhi/common.py index 92c9e13fb8a..6f215840324 100644 --- a/tests/components/smhi/common.py +++ b/tests/components/smhi/common.py @@ -1,5 +1,5 @@ """Common test utilities.""" -from tests.async_mock import Mock +from unittest.mock import Mock class AsyncMock(Mock): diff --git a/tests/components/smhi/test_config_flow.py b/tests/components/smhi/test_config_flow.py index 56a0745c1b3..3f189b52311 100644 --- a/tests/components/smhi/test_config_flow.py +++ b/tests/components/smhi/test_config_flow.py @@ -1,11 +1,11 @@ """Tests for SMHI config flow.""" +from unittest.mock import Mock, patch + from smhi.smhi_lib import Smhi as SmhiApi, SmhiForecastException from homeassistant.components.smhi import config_flow from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE -from tests.async_mock import Mock, patch - # pylint: disable=protected-access async def test_homeassistant_location_exists() -> None: diff --git a/tests/components/smhi/test_init.py b/tests/components/smhi/test_init.py index e6b523d96bb..450ac7e6ef0 100644 --- a/tests/components/smhi/test_init.py +++ b/tests/components/smhi/test_init.py @@ -1,10 +1,10 @@ """Test SMHI component setup process.""" +from unittest.mock import Mock + from homeassistant.components import smhi from .common import AsyncMock -from tests.async_mock import Mock - TEST_CONFIG = { "config": { "name": "0123456789ABCDEF", diff --git a/tests/components/smhi/test_weather.py b/tests/components/smhi/test_weather.py index 050dc663487..9170f3a9ed0 100644 --- a/tests/components/smhi/test_weather.py +++ b/tests/components/smhi/test_weather.py @@ -2,6 +2,7 @@ import asyncio from datetime import datetime import logging +from unittest.mock import AsyncMock, Mock, patch from smhi.smhi_lib import APIURL_TEMPLATE, SmhiForecastException @@ -25,7 +26,6 @@ from homeassistant.components.weather import ( from homeassistant.const import TEMP_CELSIUS from homeassistant.core import HomeAssistant -from tests.async_mock import AsyncMock, Mock, patch from tests.common import MockConfigEntry, load_fixture _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/smtp/test_notify.py b/tests/components/smtp/test_notify.py index 754df5945af..46f8d0efd5f 100644 --- a/tests/components/smtp/test_notify.py +++ b/tests/components/smtp/test_notify.py @@ -1,6 +1,7 @@ """The tests for the notify smtp platform.""" from os import path import re +from unittest.mock import patch import pytest @@ -11,8 +12,6 @@ from homeassistant.components.smtp.notify import MailNotificationService from homeassistant.const import SERVICE_RELOAD from homeassistant.setup import async_setup_component -from tests.async_mock import patch - class MockSMTP(MailNotificationService): """Test SMTP object that doesn't need a working server.""" diff --git a/tests/components/solaredge/test_config_flow.py b/tests/components/solaredge/test_config_flow.py index 835fc300982..4caae0edcfe 100644 --- a/tests/components/solaredge/test_config_flow.py +++ b/tests/components/solaredge/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for the SolarEdge config flow.""" +from unittest.mock import Mock, patch + import pytest from requests.exceptions import ConnectTimeout, HTTPError @@ -7,7 +9,6 @@ from homeassistant.components.solaredge import config_flow from homeassistant.components.solaredge.const import CONF_SITE_ID, DEFAULT_NAME from homeassistant.const import CONF_API_KEY, CONF_NAME -from tests.async_mock import Mock, patch from tests.common import MockConfigEntry NAME = "solaredge site 1 2 3" diff --git a/tests/components/solarlog/test_config_flow.py b/tests/components/solarlog/test_config_flow.py index 8266adfd417..3016a73f1b8 100644 --- a/tests/components/solarlog/test_config_flow.py +++ b/tests/components/solarlog/test_config_flow.py @@ -1,4 +1,6 @@ """Test the solarlog config flow.""" +from unittest.mock import patch + import pytest from homeassistant import config_entries, data_entry_flow, setup @@ -6,7 +8,6 @@ from homeassistant.components.solarlog import config_flow from homeassistant.components.solarlog.const import DEFAULT_HOST, DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME -from tests.async_mock import patch from tests.common import MockConfigEntry NAME = "Solarlog test 1 2 3" diff --git a/tests/components/soma/test_config_flow.py b/tests/components/soma/test_config_flow.py index 929463ecf81..1d00f83a608 100644 --- a/tests/components/soma/test_config_flow.py +++ b/tests/components/soma/test_config_flow.py @@ -1,11 +1,12 @@ """Tests for the Soma config flow.""" +from unittest.mock import patch + from api.soma_api import SomaApi from requests import RequestException from homeassistant import data_entry_flow from homeassistant.components.soma import DOMAIN, config_flow -from tests.async_mock import patch from tests.common import MockConfigEntry MOCK_HOST = "123.45.67.89" diff --git a/tests/components/somfy/test_config_flow.py b/tests/components/somfy/test_config_flow.py index 4276a6a18d4..47adb5bdc91 100644 --- a/tests/components/somfy/test_config_flow.py +++ b/tests/components/somfy/test_config_flow.py @@ -1,5 +1,6 @@ """Tests for the Somfy config flow.""" import asyncio +from unittest.mock import patch import pytest @@ -8,7 +9,6 @@ from homeassistant.components.somfy import DOMAIN, config_flow from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow -from tests.async_mock import patch from tests.common import MockConfigEntry CLIENT_ID_VALUE = "1234" diff --git a/tests/components/sonarr/__init__.py b/tests/components/sonarr/__init__.py index 61afac099d3..1313db4460d 100644 --- a/tests/components/sonarr/__init__.py +++ b/tests/components/sonarr/__init__.py @@ -1,5 +1,6 @@ """Tests for the Sonarr component.""" from socket import gaierror as SocketGIAError +from unittest.mock import patch from homeassistant.components.sonarr.const import ( CONF_BASE_PATH, @@ -19,7 +20,6 @@ from homeassistant.const import ( ) from homeassistant.helpers.typing import HomeAssistantType -from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/sonarr/test_config_flow.py b/tests/components/sonarr/test_config_flow.py index f872f7f8c18..701580ab37c 100644 --- a/tests/components/sonarr/test_config_flow.py +++ b/tests/components/sonarr/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Sonarr config flow.""" +from unittest.mock import patch + from homeassistant.components.sonarr.const import ( CONF_UPCOMING_DAYS, CONF_WANTED_MAX_ITEMS, @@ -15,7 +17,6 @@ from homeassistant.data_entry_flow import ( ) from homeassistant.helpers.typing import HomeAssistantType -from tests.async_mock import patch from tests.components.sonarr import ( HOST, MOCK_REAUTH_INPUT, diff --git a/tests/components/sonarr/test_init.py b/tests/components/sonarr/test_init.py index 258be0203bb..16d33a23072 100644 --- a/tests/components/sonarr/test_init.py +++ b/tests/components/sonarr/test_init.py @@ -1,4 +1,6 @@ """Tests for the Sonsrr integration.""" +from unittest.mock import patch + from homeassistant.components.sonarr.const import DOMAIN from homeassistant.config_entries import ( ENTRY_STATE_LOADED, @@ -10,7 +12,6 @@ from homeassistant.config_entries import ( from homeassistant.const import CONF_SOURCE from homeassistant.core import HomeAssistant -from tests.async_mock import patch from tests.components.sonarr import setup_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/sonarr/test_sensor.py b/tests/components/sonarr/test_sensor.py index 94230c9e726..0b306dc8240 100644 --- a/tests/components/sonarr/test_sensor.py +++ b/tests/components/sonarr/test_sensor.py @@ -1,5 +1,6 @@ """Tests for the Sonarr sensor platform.""" from datetime import timedelta +from unittest.mock import patch import pytest @@ -14,7 +15,6 @@ from homeassistant.const import ( from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import dt as dt_util -from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.sonarr import mock_connection, setup_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/songpal/__init__.py b/tests/components/songpal/__init__.py index bca879268cc..f3004ef22e2 100644 --- a/tests/components/songpal/__init__.py +++ b/tests/components/songpal/__init__.py @@ -1,11 +1,11 @@ """Test the songpal integration.""" +from unittest.mock import AsyncMock, MagicMock, patch + from songpal import SongpalException from homeassistant.components.songpal.const import CONF_ENDPOINT from homeassistant.const import CONF_NAME -from tests.async_mock import AsyncMock, MagicMock, patch - FRIENDLY_NAME = "name" ENTITY_ID = f"media_player.{FRIENDLY_NAME}" HOST = "0.0.0.0" diff --git a/tests/components/songpal/test_config_flow.py b/tests/components/songpal/test_config_flow.py index 155801dca03..a1751bca676 100644 --- a/tests/components/songpal/test_config_flow.py +++ b/tests/components/songpal/test_config_flow.py @@ -1,5 +1,6 @@ """Test the songpal config flow.""" import copy +from unittest.mock import patch from homeassistant.components import ssdp from homeassistant.components.songpal.const import CONF_ENDPOINT, DOMAIN @@ -21,7 +22,6 @@ from . import ( _patch_config_flow_device, ) -from tests.async_mock import patch from tests.common import MockConfigEntry UDN = "uuid:1234" diff --git a/tests/components/songpal/test_init.py b/tests/components/songpal/test_init.py index 9f5de326cc0..8efcab4148b 100644 --- a/tests/components/songpal/test_init.py +++ b/tests/components/songpal/test_init.py @@ -1,4 +1,6 @@ """Tests songpal setup.""" +from unittest.mock import patch + from homeassistant.components import songpal from homeassistant.setup import async_setup_component @@ -9,7 +11,6 @@ from . import ( _patch_media_player_device, ) -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/songpal/test_media_player.py b/tests/components/songpal/test_media_player.py index 61b59ee1b56..b43370b8200 100644 --- a/tests/components/songpal/test_media_player.py +++ b/tests/components/songpal/test_media_player.py @@ -1,6 +1,7 @@ """Test songpal media_player.""" from datetime import timedelta import logging +from unittest.mock import AsyncMock, MagicMock, call, patch from songpal import ( ConnectChange, @@ -32,7 +33,6 @@ from . import ( _patch_media_player_device, ) -from tests.async_mock import AsyncMock, MagicMock, call, patch from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index f6249d0bc81..1ce2205813b 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -1,11 +1,12 @@ """Configuration for Sonos tests.""" +from unittest.mock import Mock, patch as patch + import pytest from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.sonos import DOMAIN from homeassistant.const import CONF_HOSTS -from tests.async_mock import Mock, patch as patch from tests.common import MockConfigEntry diff --git a/tests/components/soundtouch/test_media_player.py b/tests/components/soundtouch/test_media_player.py index 85089854c4d..4ed8a648c77 100644 --- a/tests/components/soundtouch/test_media_player.py +++ b/tests/components/soundtouch/test_media_player.py @@ -1,4 +1,6 @@ """Test the Soundtouch component.""" +from unittest.mock import call, patch + from libsoundtouch.device import ( Config, Preset, @@ -26,8 +28,6 @@ from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING from homeassistant.helpers.discovery import async_load_platform from homeassistant.setup import async_setup_component -from tests.async_mock import call, patch - # pylint: disable=super-init-not-called diff --git a/tests/components/speedtestdotnet/test_config_flow.py b/tests/components/speedtestdotnet/test_config_flow.py index d421113a2d7..dee271d94a3 100644 --- a/tests/components/speedtestdotnet/test_config_flow.py +++ b/tests/components/speedtestdotnet/test_config_flow.py @@ -1,5 +1,6 @@ """Tests for SpeedTest config flow.""" from datetime import timedelta +from unittest.mock import patch import pytest from speedtest import NoMatchedServers @@ -17,7 +18,6 @@ from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_SCAN_INTERVAL from . import MOCK_SERVERS -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/speedtestdotnet/test_init.py b/tests/components/speedtestdotnet/test_init.py index cadf97fc761..72bcb743a8d 100644 --- a/tests/components/speedtestdotnet/test_init.py +++ b/tests/components/speedtestdotnet/test_init.py @@ -1,11 +1,12 @@ """Tests for SpeedTest integration.""" +from unittest.mock import patch + import speedtest from homeassistant import config_entries from homeassistant.components import speedtestdotnet from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/speedtestdotnet/test_sensor.py b/tests/components/speedtestdotnet/test_sensor.py index 5c1606f0f4b..c08a9f3304f 100644 --- a/tests/components/speedtestdotnet/test_sensor.py +++ b/tests/components/speedtestdotnet/test_sensor.py @@ -1,11 +1,12 @@ """Tests for SpeedTest sensors.""" +from unittest.mock import patch + from homeassistant.components import speedtestdotnet from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.speedtestdotnet.const import DEFAULT_NAME, SENSOR_TYPES from . import MOCK_RESULTS, MOCK_SERVERS, MOCK_STATES -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/spider/test_config_flow.py b/tests/components/spider/test_config_flow.py index ca1b37434d1..e8d10b51cf3 100644 --- a/tests/components/spider/test_config_flow.py +++ b/tests/components/spider/test_config_flow.py @@ -1,11 +1,12 @@ """Tests for the Spider config flow.""" +from unittest.mock import Mock, patch + import pytest from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.spider.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import Mock, patch from tests.common import MockConfigEntry USERNAME = "spider-username" diff --git a/tests/components/spotify/test_config_flow.py b/tests/components/spotify/test_config_flow.py index 53e87e5bdae..37a33ef66b2 100644 --- a/tests/components/spotify/test_config_flow.py +++ b/tests/components/spotify/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for the Spotify config flow.""" +from unittest.mock import patch + from spotipy import SpotifyException from homeassistant import data_entry_flow, setup @@ -7,7 +9,6 @@ from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/squeezebox/test_config_flow.py b/tests/components/squeezebox/test_config_flow.py index f325024cf00..2dae2f78d01 100644 --- a/tests/components/squeezebox/test_config_flow.py +++ b/tests/components/squeezebox/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Logitech Squeezebox config flow.""" +from unittest.mock import patch + from pysqueezebox import Server from homeassistant import config_entries @@ -16,7 +18,6 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_FORM, ) -from tests.async_mock import patch from tests.common import MockConfigEntry HOST = "1.1.1.1" diff --git a/tests/components/srp_energy/__init__.py b/tests/components/srp_energy/__init__.py index 34f06e2993e..5e2a4695d0b 100644 --- a/tests/components/srp_energy/__init__.py +++ b/tests/components/srp_energy/__init__.py @@ -1,9 +1,10 @@ """Tests for the SRP Energy integration.""" +from unittest.mock import patch + 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 = {} diff --git a/tests/components/srp_energy/test_config_flow.py b/tests/components/srp_energy/test_config_flow.py index acb9d28f75d..adad1878326 100644 --- a/tests/components/srp_energy/test_config_flow.py +++ b/tests/components/srp_energy/test_config_flow.py @@ -1,11 +1,11 @@ """Test the SRP Energy config flow.""" +from unittest.mock import patch + 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.""" diff --git a/tests/components/srp_energy/test_sensor.py b/tests/components/srp_energy/test_sensor.py index 3a70a3ec09f..a93e56b7b93 100644 --- a/tests/components/srp_energy/test_sensor.py +++ b/tests/components/srp_energy/test_sensor.py @@ -1,4 +1,6 @@ """Tests for the srp_energy sensor platform.""" +from unittest.mock import MagicMock + from homeassistant.components.srp_energy.const import ( ATTRIBUTION, DEFAULT_NAME, @@ -10,8 +12,6 @@ from homeassistant.components.srp_energy.const import ( 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.""" diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 24401963974..b6bebcfeeda 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -3,6 +3,7 @@ from datetime import datetime, timedelta from os import path import statistics import unittest +from unittest.mock import patch import pytest @@ -18,7 +19,6 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component, setup_component from homeassistant.util import dt as dt_util -from tests.async_mock import patch from tests.common import ( fire_time_changed, get_test_home_assistant, diff --git a/tests/components/statsd/test_init.py b/tests/components/statsd/test_init.py index 30b52659685..17e49b09951 100644 --- a/tests/components/statsd/test_init.py +++ b/tests/components/statsd/test_init.py @@ -1,5 +1,6 @@ """The tests for the StatsD feeder.""" from unittest import mock +from unittest.mock import MagicMock, patch import pytest import voluptuous as vol @@ -9,8 +10,6 @@ from homeassistant.const import EVENT_STATE_CHANGED, STATE_OFF, STATE_ON import homeassistant.core as ha from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, patch - @pytest.fixture def mock_client(): diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index 16d2d724f22..3af48cb580d 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -1,5 +1,6 @@ """The tests for hls streams.""" from datetime import timedelta +from unittest.mock import patch from urllib.parse import urlparse import av @@ -10,7 +11,6 @@ from homeassistant.const import HTTP_NOT_FOUND from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.stream.common import generate_h264_video, preload_stream diff --git a/tests/components/stream/test_init.py b/tests/components/stream/test_init.py index 505de7ca018..1515ff1a490 100644 --- a/tests/components/stream/test_init.py +++ b/tests/components/stream/test_init.py @@ -1,4 +1,6 @@ """The tests for stream.""" +from unittest.mock import AsyncMock, MagicMock, patch + import pytest from homeassistant.components.stream.const import ( @@ -12,8 +14,6 @@ from homeassistant.const import CONF_FILENAME from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock, MagicMock, patch - async def test_record_service_invalid_file(hass): """Test record service call with invalid file.""" diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index cb6a1c9d36f..220d0a062e3 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -1,6 +1,7 @@ """The tests for hls streams.""" from datetime import timedelta from io import BytesIO +from unittest.mock import patch import av import pytest @@ -10,7 +11,6 @@ from homeassistant.components.stream.recorder import recorder_save_worker from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.stream.common import generate_h264_video, preload_stream diff --git a/tests/components/sun/test_init.py b/tests/components/sun/test_init.py index 36eaa8398ac..1e95082b358 100644 --- a/tests/components/sun/test_init.py +++ b/tests/components/sun/test_init.py @@ -1,5 +1,6 @@ """The tests for the Sun component.""" from datetime import datetime, timedelta +from unittest.mock import patch from pytest import mark @@ -9,8 +10,6 @@ import homeassistant.core as ha from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch - async def test_setting_rising(hass, legacy_patchable_time): """Test retrieving sun setting and rising.""" diff --git a/tests/components/sun/test_trigger.py b/tests/components/sun/test_trigger.py index 1a3de56964c..a288150517d 100644 --- a/tests/components/sun/test_trigger.py +++ b/tests/components/sun/test_trigger.py @@ -1,5 +1,6 @@ """The tests for the sun automation.""" from datetime import datetime +from unittest.mock import patch import pytest @@ -16,7 +17,6 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component 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 diff --git a/tests/components/surepetcare/__init__.py b/tests/components/surepetcare/__init__.py index a7ce6d3b6a6..d4af323d063 100644 --- a/tests/components/surepetcare/__init__.py +++ b/tests/components/surepetcare/__init__.py @@ -1,9 +1,9 @@ """Tests for Sure Petcare integration.""" +from unittest.mock import patch + from homeassistant.components.surepetcare.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import patch - HOUSEHOLD_ID = "household-id" HUB_ID = "hub-id" diff --git a/tests/components/surepetcare/conftest.py b/tests/components/surepetcare/conftest.py index ed42fe2532b..9bc06a84267 100644 --- a/tests/components/surepetcare/conftest.py +++ b/tests/components/surepetcare/conftest.py @@ -1,11 +1,11 @@ """Define fixtures available for all tests.""" +from unittest.mock import AsyncMock, patch + from pytest import fixture from surepy import SurePetcare from homeassistant.helpers.aiohttp_client import async_get_clientsession -from tests.async_mock import AsyncMock, patch - @fixture async def surepetcare(hass): diff --git a/tests/components/switch/test_device_condition.py b/tests/components/switch/test_device_condition.py index 093758cbe62..67ba3e8e38e 100644 --- a/tests/components/switch/test_device_condition.py +++ b/tests/components/switch/test_device_condition.py @@ -1,5 +1,6 @@ """The test for switch device automation.""" from datetime import timedelta +from unittest.mock import patch import pytest @@ -10,7 +11,6 @@ from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import ( MockConfigEntry, async_get_device_automation_capabilities, diff --git a/tests/components/switcher_kis/conftest.py b/tests/components/switcher_kis/conftest.py index 37d61d6ca19..a95dd2fd6d1 100644 --- a/tests/components/switcher_kis/conftest.py +++ b/tests/components/switcher_kis/conftest.py @@ -3,6 +3,7 @@ from asyncio import Queue from datetime import datetime from typing import Any, Generator, Optional +from unittest.mock import AsyncMock, patch from pytest import fixture @@ -19,8 +20,6 @@ from .consts import ( DUMMY_REMAINING_TIME, ) -from tests.async_mock import AsyncMock, patch - @patch("aioswitcher.devices.SwitcherV2Device") class MockSwitcherV2Device: diff --git a/tests/components/syncthru/test_config_flow.py b/tests/components/syncthru/test_config_flow.py index 1df377ff0e8..70e8a80ad0f 100644 --- a/tests/components/syncthru/test_config_flow.py +++ b/tests/components/syncthru/test_config_flow.py @@ -1,6 +1,7 @@ """Tests for syncthru config flow.""" import re +from unittest.mock import patch from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components import ssdp @@ -8,7 +9,6 @@ from homeassistant.components.syncthru.config_flow import SyncThru from homeassistant.components.syncthru.const import DOMAIN from homeassistant.const import CONF_NAME, CONF_URL -from tests.async_mock import patch from tests.common import MockConfigEntry, mock_coro FIXTURE_USER_INPUT = { diff --git a/tests/components/synology_dsm/conftest.py b/tests/components/synology_dsm/conftest.py index db25bd59ada..74de072e229 100644 --- a/tests/components/synology_dsm/conftest.py +++ b/tests/components/synology_dsm/conftest.py @@ -1,7 +1,7 @@ """Configure Synology DSM tests.""" -import pytest +from unittest.mock import patch -from tests.async_mock import patch +import pytest def pytest_configure(config): diff --git a/tests/components/synology_dsm/test_config_flow.py b/tests/components/synology_dsm/test_config_flow.py index 59ed8eea657..85ed02a7a52 100644 --- a/tests/components/synology_dsm/test_config_flow.py +++ b/tests/components/synology_dsm/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for the Synology DSM config flow.""" +from unittest.mock import MagicMock, Mock, patch + import pytest from synology_dsm.exceptions import ( SynologyDSMException, @@ -50,7 +52,6 @@ from .consts import ( VERIFY_SSL, ) -from tests.async_mock import MagicMock, Mock, patch from tests.common import MockConfigEntry diff --git a/tests/components/synology_dsm/test_init.py b/tests/components/synology_dsm/test_init.py index b8be375b321..59864c56523 100644 --- a/tests/components/synology_dsm/test_init.py +++ b/tests/components/synology_dsm/test_init.py @@ -1,4 +1,6 @@ """Tests for the Synology DSM component.""" +from unittest.mock import patch + import pytest from homeassistant.components.synology_dsm.const import DOMAIN, SERVICES @@ -14,7 +16,6 @@ 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 diff --git a/tests/components/system_health/test_init.py b/tests/components/system_health/test_init.py index 28f54c81c42..212ec544629 100644 --- a/tests/components/system_health/test_init.py +++ b/tests/components/system_health/test_init.py @@ -1,12 +1,12 @@ """Tests for the system health component init.""" import asyncio +from unittest.mock import AsyncMock, Mock, patch from aiohttp.client_exceptions import ClientError from homeassistant.components import system_health from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock, Mock, patch from tests.common import get_system_health_info, mock_platform diff --git a/tests/components/system_log/test_init.py b/tests/components/system_log/test_init.py index 85e86d9c78f..287e7139aff 100644 --- a/tests/components/system_log/test_init.py +++ b/tests/components/system_log/test_init.py @@ -2,6 +2,7 @@ import asyncio import logging import queue +from unittest.mock import MagicMock, patch import pytest @@ -9,8 +10,6 @@ from homeassistant.bootstrap import async_setup_component from homeassistant.components import system_log from homeassistant.core import callback -from tests.async_mock import MagicMock, patch - _LOGGER = logging.getLogger("test_logger") BASIC_CONFIG = {"system_log": {"max_entries": 2}} diff --git a/tests/components/tado/test_config_flow.py b/tests/components/tado/test_config_flow.py index ce4af05b79c..1b0fcc268d6 100644 --- a/tests/components/tado/test_config_flow.py +++ b/tests/components/tado/test_config_flow.py @@ -1,11 +1,12 @@ """Test the Tado config flow.""" +from unittest.mock import MagicMock, patch + import requests from homeassistant import config_entries, setup from homeassistant.components.tado.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry diff --git a/tests/components/tag/test_init.py b/tests/components/tag/test_init.py index 442d3263f0a..2f9ff2310cf 100644 --- a/tests/components/tag/test_init.py +++ b/tests/components/tag/test_init.py @@ -1,4 +1,6 @@ """Tests for the tag component.""" +from unittest.mock import patch + import pytest from homeassistant.components.tag import DOMAIN, TAGS, async_scan_tag @@ -6,8 +8,6 @@ from homeassistant.helpers import collection from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from tests.async_mock import patch - @pytest.fixture def storage_setup(hass, hass_storage): diff --git a/tests/components/tasmota/conftest.py b/tests/components/tasmota/conftest.py index fadf8a44648..3c530a93d1e 100644 --- a/tests/components/tasmota/conftest.py +++ b/tests/components/tasmota/conftest.py @@ -1,4 +1,6 @@ """Test fixtures for Tasmota component.""" +from unittest.mock import patch + from hatasmota.discovery import get_status_sensor_entities import pytest @@ -9,7 +11,6 @@ from homeassistant.components.tasmota.const import ( DOMAIN, ) -from tests.async_mock import patch from tests.common import ( MockConfigEntry, async_mock_service, diff --git a/tests/components/tasmota/test_binary_sensor.py b/tests/components/tasmota/test_binary_sensor.py index 3f444e75bdc..6d4263853dc 100644 --- a/tests/components/tasmota/test_binary_sensor.py +++ b/tests/components/tasmota/test_binary_sensor.py @@ -2,6 +2,7 @@ import copy from datetime import timedelta import json +from unittest.mock import patch from hatasmota.utils import ( get_topic_stat_result, @@ -34,7 +35,6 @@ from .test_common import ( help_test_entity_id_update_subscriptions, ) -from tests.async_mock import patch from tests.common import async_fire_mqtt_message, async_fire_time_changed diff --git a/tests/components/tasmota/test_common.py b/tests/components/tasmota/test_common.py index 04346f915c4..973ecd3c890 100644 --- a/tests/components/tasmota/test_common.py +++ b/tests/components/tasmota/test_common.py @@ -1,6 +1,7 @@ """Common test objects.""" import copy import json +from unittest.mock import ANY from hatasmota.const import ( CONF_MAC, @@ -20,7 +21,6 @@ from hatasmota.utils import ( from homeassistant.components.tasmota.const import DEFAULT_PREFIX from homeassistant.const import STATE_UNAVAILABLE -from tests.async_mock import ANY from tests.common import async_fire_mqtt_message DEFAULT_CONFIG = { diff --git a/tests/components/tasmota/test_cover.py b/tests/components/tasmota/test_cover.py index d5ae01f1666..131f95842a5 100644 --- a/tests/components/tasmota/test_cover.py +++ b/tests/components/tasmota/test_cover.py @@ -1,6 +1,7 @@ """The tests for the Tasmota cover platform.""" import copy import json +from unittest.mock import patch from hatasmota.utils import ( get_topic_stat_result, @@ -26,7 +27,6 @@ from .test_common import ( help_test_entity_id_update_subscriptions, ) -from tests.async_mock import patch from tests.common import async_fire_mqtt_message diff --git a/tests/components/tasmota/test_device_trigger.py b/tests/components/tasmota/test_device_trigger.py index 2c88533f30d..ec8744881c5 100644 --- a/tests/components/tasmota/test_device_trigger.py +++ b/tests/components/tasmota/test_device_trigger.py @@ -1,6 +1,7 @@ """The tests for MQTT device triggers.""" import copy import json +from unittest.mock import patch from hatasmota.switch import TasmotaSwitchTriggerConfig import pytest @@ -12,7 +13,6 @@ from homeassistant.setup import async_setup_component from .test_common import DEFAULT_CONFIG -from tests.async_mock import patch from tests.common import ( assert_lists_same, async_fire_mqtt_message, diff --git a/tests/components/tasmota/test_discovery.py b/tests/components/tasmota/test_discovery.py index 40fecb6b695..35be7e50e62 100644 --- a/tests/components/tasmota/test_discovery.py +++ b/tests/components/tasmota/test_discovery.py @@ -1,6 +1,7 @@ """The tests for the MQTT discovery.""" import copy import json +from unittest.mock import patch from homeassistant.components.tasmota.const import DEFAULT_PREFIX from homeassistant.components.tasmota.discovery import ALREADY_DISCOVERED @@ -8,7 +9,6 @@ from homeassistant.components.tasmota.discovery import ALREADY_DISCOVERED from .conftest import setup_tasmota_helper from .test_common import DEFAULT_CONFIG, DEFAULT_CONFIG_9_0_0_3 -from tests.async_mock import patch from tests.common import async_fire_mqtt_message diff --git a/tests/components/tasmota/test_fan.py b/tests/components/tasmota/test_fan.py index 1aca8c84e07..4035c877bb8 100644 --- a/tests/components/tasmota/test_fan.py +++ b/tests/components/tasmota/test_fan.py @@ -1,6 +1,7 @@ """The tests for the Tasmota fan platform.""" import copy import json +from unittest.mock import patch from hatasmota.utils import ( get_topic_stat_result, @@ -26,7 +27,6 @@ from .test_common import ( 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 diff --git a/tests/components/tasmota/test_init.py b/tests/components/tasmota/test_init.py index 39d99872741..5b553164583 100644 --- a/tests/components/tasmota/test_init.py +++ b/tests/components/tasmota/test_init.py @@ -1,13 +1,13 @@ """The tests for the Tasmota binary sensor platform.""" import copy import json +from unittest.mock import call from homeassistant.components import websocket_api from homeassistant.components.tasmota.const import DEFAULT_PREFIX from .test_common import DEFAULT_CONFIG -from tests.async_mock import call from tests.common import MockConfigEntry, async_fire_mqtt_message diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py index f09c27da753..d04b9dcf02b 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -1,6 +1,7 @@ """The tests for the Tasmota light platform.""" import copy import json +from unittest.mock import patch from hatasmota.const import CONF_MAC from hatasmota.utils import ( @@ -34,7 +35,6 @@ from .test_common import ( help_test_entity_id_update_subscriptions, ) -from tests.async_mock import patch from tests.common import async_fire_mqtt_message from tests.components.light import common diff --git a/tests/components/tasmota/test_mixins.py b/tests/components/tasmota/test_mixins.py index 5f4ef443475..579f8e9aaeb 100644 --- a/tests/components/tasmota/test_mixins.py +++ b/tests/components/tasmota/test_mixins.py @@ -1,6 +1,7 @@ """The tests for the Tasmota mixins.""" import copy import json +from unittest.mock import call from hatasmota.const import CONF_MAC from hatasmota.utils import config_get_state_online, get_topic_tele_will @@ -9,7 +10,6 @@ from homeassistant.components.tasmota.const import DEFAULT_PREFIX from .test_common import DEFAULT_CONFIG -from tests.async_mock import call from tests.common import async_fire_mqtt_message diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index 4c8de9e339d..fe415c264ef 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -3,6 +3,7 @@ import copy import datetime from datetime import timedelta import json +from unittest.mock import Mock, patch import hatasmota from hatasmota.utils import ( @@ -31,7 +32,6 @@ from .test_common import ( help_test_entity_id_update_subscriptions, ) -from tests.async_mock import Mock, patch from tests.common import async_fire_mqtt_message, async_fire_time_changed DEFAULT_SENSOR_CONFIG = { diff --git a/tests/components/tasmota/test_switch.py b/tests/components/tasmota/test_switch.py index 208026c2de5..00b0a922e0a 100644 --- a/tests/components/tasmota/test_switch.py +++ b/tests/components/tasmota/test_switch.py @@ -1,6 +1,7 @@ """The tests for the Tasmota switch platform.""" import copy import json +from unittest.mock import patch from hatasmota.utils import ( get_topic_stat_result, @@ -25,7 +26,6 @@ from .test_common import ( help_test_entity_id_update_subscriptions, ) -from tests.async_mock import patch from tests.common import async_fire_mqtt_message from tests.components.switch import common diff --git a/tests/components/tcp/test_binary_sensor.py b/tests/components/tcp/test_binary_sensor.py index 4cde4d9ac31..2dc16ad79c7 100644 --- a/tests/components/tcp/test_binary_sensor.py +++ b/tests/components/tcp/test_binary_sensor.py @@ -1,11 +1,11 @@ """The tests for the TCP binary sensor platform.""" import unittest +from unittest.mock import Mock, patch from homeassistant.components.tcp import binary_sensor as bin_tcp import homeassistant.components.tcp.sensor as tcp from homeassistant.setup import setup_component -from tests.async_mock import Mock, patch from tests.common import assert_setup_component, get_test_home_assistant import tests.components.tcp.test_sensor as test_tcp diff --git a/tests/components/tcp/test_sensor.py b/tests/components/tcp/test_sensor.py index b06652dc53f..8e79d4e514d 100644 --- a/tests/components/tcp/test_sensor.py +++ b/tests/components/tcp/test_sensor.py @@ -2,6 +2,7 @@ from copy import copy import socket import unittest +from unittest.mock import Mock, patch from uuid import uuid4 import homeassistant.components.tcp.sensor as tcp @@ -9,7 +10,6 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.template import Template from homeassistant.setup import setup_component -from tests.async_mock import Mock, patch from tests.common import assert_setup_component, get_test_home_assistant TEST_CONFIG = { diff --git a/tests/components/telegram/test_notify.py b/tests/components/telegram/test_notify.py index 7488db49d9e..6f8d1f989f9 100644 --- a/tests/components/telegram/test_notify.py +++ b/tests/components/telegram/test_notify.py @@ -1,5 +1,6 @@ """The tests for the telegram.notify platform.""" from os import path +from unittest.mock import patch from homeassistant import config as hass_config import homeassistant.components.notify as notify @@ -7,8 +8,6 @@ from homeassistant.components.telegram import DOMAIN from homeassistant.const import SERVICE_RELOAD from homeassistant.setup import async_setup_component -from tests.async_mock import patch - async def test_reload_notify(hass): """Verify we can reload the notify service.""" diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index e8ff4c83f8d..241ac88328e 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -1,6 +1,7 @@ """The tests for the Template Binary sensor platform.""" from datetime import timedelta import logging +from unittest.mock import patch from homeassistant import setup from homeassistant.components import binary_sensor @@ -14,7 +15,6 @@ from homeassistant.const import ( from homeassistant.core import CoreState import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import assert_setup_component, async_fire_time_changed diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index c39fb474bca..7f560fa0abb 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -1,6 +1,7 @@ """The test for the Template sensor platform.""" from asyncio import Event from datetime import timedelta +from unittest.mock import patch from homeassistant.bootstrap import async_from_config_dict from homeassistant.components import sensor @@ -18,7 +19,6 @@ from homeassistant.helpers.template import Template from homeassistant.setup import ATTR_COMPONENT, async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import assert_setup_component, async_fire_time_changed diff --git a/tests/components/template/test_trigger.py b/tests/components/template/test_trigger.py index 822a274bf23..de4974cb1b6 100644 --- a/tests/components/template/test_trigger.py +++ b/tests/components/template/test_trigger.py @@ -1,6 +1,7 @@ """The tests for the Template automation.""" from datetime import timedelta from unittest import mock +from unittest.mock import patch import pytest @@ -11,7 +12,6 @@ from homeassistant.core import Context, callback from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import ( assert_setup_component, async_fire_time_changed, diff --git a/tests/components/tesla/test_config_flow.py b/tests/components/tesla/test_config_flow.py index 59cdf910bf4..7fb308ecc43 100644 --- a/tests/components/tesla/test_config_flow.py +++ b/tests/components/tesla/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Tesla config flow.""" +from unittest.mock import patch + from teslajsonpy import TeslaException from homeassistant import config_entries, data_entry_flow, setup @@ -18,7 +20,6 @@ from homeassistant.const import ( HTTP_NOT_FOUND, ) -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/tibber/test_config_flow.py b/tests/components/tibber/test_config_flow.py index c92ec5bc5c7..479f314123a 100644 --- a/tests/components/tibber/test_config_flow.py +++ b/tests/components/tibber/test_config_flow.py @@ -1,10 +1,11 @@ """Tests for Tibber config flow.""" +from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch + import pytest from homeassistant.components.tibber.const import DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN -from tests.async_mock import AsyncMock, MagicMock, PropertyMock, patch from tests.common import MockConfigEntry diff --git a/tests/components/tile/test_config_flow.py b/tests/components/tile/test_config_flow.py index b9d4799dc2f..e5561133a35 100644 --- a/tests/components/tile/test_config_flow.py +++ b/tests/components/tile/test_config_flow.py @@ -1,4 +1,6 @@ """Define tests for the Tile config flow.""" +from unittest.mock import patch + from pytile.errors import TileError from homeassistant import data_entry_flow @@ -6,7 +8,6 @@ from homeassistant.components.tile import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/time_date/test_sensor.py b/tests/components/time_date/test_sensor.py index b502dad3ea6..a9b5ea83c05 100644 --- a/tests/components/time_date/test_sensor.py +++ b/tests/components/time_date/test_sensor.py @@ -1,11 +1,11 @@ """The tests for time_date sensor platform.""" +from unittest.mock import patch + import pytest import homeassistant.components.time_date.sensor as time_date import homeassistant.util.dt as dt_util -from tests.async_mock import patch - ORIG_TZ = dt_util.DEFAULT_TIME_ZONE diff --git a/tests/components/timer/test_init.py b/tests/components/timer/test_init.py index 6b5a2596b71..74c3eceeea2 100644 --- a/tests/components/timer/test_init.py +++ b/tests/components/timer/test_init.py @@ -2,6 +2,7 @@ # pylint: disable=protected-access from datetime import timedelta import logging +from unittest.mock import patch import pytest @@ -42,7 +43,6 @@ from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from tests.async_mock import patch from tests.common import async_fire_time_changed _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/tod/test_binary_sensor.py b/tests/components/tod/test_binary_sensor.py index b0bca3e1c01..dff1e208ab6 100644 --- a/tests/components/tod/test_binary_sensor.py +++ b/tests/components/tod/test_binary_sensor.py @@ -1,5 +1,6 @@ """Test Times of the Day Binary Sensor.""" from datetime import datetime, timedelta +from unittest.mock import patch import pytest import pytz @@ -10,7 +11,6 @@ from homeassistant.helpers.sun import get_astral_event_date, get_astral_event_ne from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import assert_setup_component diff --git a/tests/components/toon/test_config_flow.py b/tests/components/toon/test_config_flow.py index b7eb3898b47..f3240991a37 100644 --- a/tests/components/toon/test_config_flow.py +++ b/tests/components/toon/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for the Toon config flow.""" +from unittest.mock import patch + from toonapi import Agreement, ToonError from homeassistant import data_entry_flow @@ -9,7 +11,6 @@ from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/totalconnect/common.py b/tests/components/totalconnect/common.py index c2d6f92015c..17fa244f9b2 100644 --- a/tests/components/totalconnect/common.py +++ b/tests/components/totalconnect/common.py @@ -1,11 +1,12 @@ """Common methods used across tests for TotalConnect.""" +from unittest.mock import patch + from total_connect_client import TotalConnectClient from homeassistant.components.totalconnect import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import MockConfigEntry LOCATION_INFO_BASIC_NORMAL = { diff --git a/tests/components/totalconnect/test_alarm_control_panel.py b/tests/components/totalconnect/test_alarm_control_panel.py index 75e07f09bf7..bc90c1aae2a 100644 --- a/tests/components/totalconnect/test_alarm_control_panel.py +++ b/tests/components/totalconnect/test_alarm_control_panel.py @@ -1,4 +1,6 @@ """Tests for the TotalConnect alarm control panel device.""" +from unittest.mock import patch + import pytest from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN @@ -24,8 +26,6 @@ from .common import ( setup_platform, ) -from tests.async_mock import patch - ENTITY_ID = "alarm_control_panel.test" CODE = "-1" DATA = {ATTR_ENTITY_ID: ENTITY_ID} diff --git a/tests/components/totalconnect/test_config_flow.py b/tests/components/totalconnect/test_config_flow.py index fd8cdcc5116..a1aa8780cfb 100644 --- a/tests/components/totalconnect/test_config_flow.py +++ b/tests/components/totalconnect/test_config_flow.py @@ -1,10 +1,11 @@ """Tests for the iCloud config flow.""" +from unittest.mock import patch + from homeassistant import data_entry_flow from homeassistant.components.totalconnect.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import patch from tests.common import MockConfigEntry USERNAME = "username@me.com" diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index 48812f8fb2b..4c34e754ec3 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging from typing import Callable, NamedTuple +from unittest.mock import Mock, PropertyMock, patch from pyHS100 import SmartDeviceException import pytest @@ -33,7 +34,6 @@ from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from tests.async_mock import Mock, PropertyMock, patch from tests.common import async_fire_time_changed diff --git a/tests/components/traccar/test_init.py b/tests/components/traccar/test_init.py index 12e1ff5562a..c7e031b2ca6 100644 --- a/tests/components/traccar/test_init.py +++ b/tests/components/traccar/test_init.py @@ -1,4 +1,6 @@ """The tests the for Traccar device tracker platform.""" +from unittest.mock import patch + import pytest from homeassistant import data_entry_flow @@ -15,8 +17,6 @@ from homeassistant.const import ( from homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component -from tests.async_mock import patch - HOME_LATITUDE = 37.239622 HOME_LONGITUDE = -115.815811 diff --git a/tests/components/tradfri/conftest.py b/tests/components/tradfri/conftest.py index c95bf2c036c..93675a9e4d1 100644 --- a/tests/components/tradfri/conftest.py +++ b/tests/components/tradfri/conftest.py @@ -1,9 +1,10 @@ """Common tradfri test fixtures.""" +from unittest.mock import Mock, patch + import pytest from . import MOCK_GATEWAY_ID -from tests.async_mock import Mock, patch from tests.components.light.conftest import mock_light_profiles # noqa # pylint: disable=protected-access diff --git a/tests/components/tradfri/test_config_flow.py b/tests/components/tradfri/test_config_flow.py index c9f72b7a9df..a155e8b383c 100644 --- a/tests/components/tradfri/test_config_flow.py +++ b/tests/components/tradfri/test_config_flow.py @@ -1,10 +1,11 @@ """Test the Tradfri config flow.""" +from unittest.mock import patch + import pytest from homeassistant import data_entry_flow from homeassistant.components.tradfri import config_flow -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/tradfri/test_init.py b/tests/components/tradfri/test_init.py index 34cc6d38091..0983b5aa22f 100644 --- a/tests/components/tradfri/test_init.py +++ b/tests/components/tradfri/test_init.py @@ -1,4 +1,6 @@ """Tests for Tradfri setup.""" +from unittest.mock import patch + from homeassistant.components import tradfri from homeassistant.helpers.device_registry import ( async_entries_for_config_entry, @@ -6,7 +8,6 @@ from homeassistant.helpers.device_registry import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/tradfri/test_light.py b/tests/components/tradfri/test_light.py index cf11d42411e..a4a1006d7fe 100644 --- a/tests/components/tradfri/test_light.py +++ b/tests/components/tradfri/test_light.py @@ -1,6 +1,7 @@ """Tradfri lights platform tests.""" from copy import deepcopy +from unittest.mock import MagicMock, Mock, PropertyMock, patch import pytest from pytradfri.device import Device @@ -11,7 +12,6 @@ from homeassistant.components import tradfri from . import MOCK_GATEWAY_ID -from tests.async_mock import MagicMock, Mock, PropertyMock, patch from tests.common import MockConfigEntry DEFAULT_TEST_FEATURES = { diff --git a/tests/components/transmission/test_config_flow.py b/tests/components/transmission/test_config_flow.py index 0b57ab59913..2982d363da9 100644 --- a/tests/components/transmission/test_config_flow.py +++ b/tests/components/transmission/test_config_flow.py @@ -1,5 +1,6 @@ """Tests for Transmission config flow.""" from datetime import timedelta +from unittest.mock import patch import pytest from transmissionrpc.error import TransmissionError @@ -25,7 +26,6 @@ from homeassistant.const import ( CONF_USERNAME, ) -from tests.async_mock import patch from tests.common import MockConfigEntry NAME = "Transmission" diff --git a/tests/components/transport_nsw/test_sensor.py b/tests/components/transport_nsw/test_sensor.py index a6f9c7dfd7f..baf7f793426 100644 --- a/tests/components/transport_nsw/test_sensor.py +++ b/tests/components/transport_nsw/test_sensor.py @@ -1,7 +1,7 @@ """The tests for the Transport NSW (AU) sensor platform.""" -from homeassistant.setup import async_setup_component +from unittest.mock import patch -from tests.async_mock import patch +from homeassistant.setup import async_setup_component VALID_CONFIG = { "sensor": { diff --git a/tests/components/trend/test_binary_sensor.py b/tests/components/trend/test_binary_sensor.py index 43db97b8f80..d1d77001bff 100644 --- a/tests/components/trend/test_binary_sensor.py +++ b/tests/components/trend/test_binary_sensor.py @@ -1,13 +1,13 @@ """The test for the Trend sensor platform.""" from datetime import timedelta from os import path +from unittest.mock import patch from homeassistant import config as hass_config, setup from homeassistant.components.trend import DOMAIN from homeassistant.const import SERVICE_RELOAD import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import assert_setup_component, get_test_home_assistant diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index d66efec8c64..61d77b6c8e2 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -1,4 +1,6 @@ """The tests for the TTS component.""" +from unittest.mock import PropertyMock, patch + import pytest import yarl @@ -16,7 +18,6 @@ from homeassistant.config import async_process_ha_core_config from homeassistant.const import HTTP_NOT_FOUND from homeassistant.setup import async_setup_component -from tests.async_mock import PropertyMock, patch from tests.common import assert_setup_component, async_mock_service diff --git a/tests/components/tts/test_notify.py b/tests/components/tts/test_notify.py index 90c0175831f..9989b1d349e 100644 --- a/tests/components/tts/test_notify.py +++ b/tests/components/tts/test_notify.py @@ -1,4 +1,6 @@ """The tests for the TTS component.""" +from unittest.mock import patch + import pytest import yarl @@ -12,7 +14,6 @@ import homeassistant.components.tts as tts from homeassistant.config import async_process_ha_core_config from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import assert_setup_component, async_mock_service diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py index e1b9bd3466c..0055b451e1a 100644 --- a/tests/components/tuya/test_config_flow.py +++ b/tests/components/tuya/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for the Tuya config flow.""" +from unittest.mock import Mock, patch + import pytest from tuyaha.tuyaapi import TuyaAPIException, TuyaNetException @@ -6,7 +8,6 @@ from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.tuya.const import CONF_COUNTRYCODE, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME -from tests.async_mock import Mock, patch from tests.common import MockConfigEntry USERNAME = "myUsername" diff --git a/tests/components/twilio/test_init.py b/tests/components/twilio/test_init.py index 185139077df..ee7f072a65c 100644 --- a/tests/components/twilio/test_init.py +++ b/tests/components/twilio/test_init.py @@ -1,10 +1,10 @@ """Test the init file of Twilio.""" +from unittest.mock import patch + from homeassistant import data_entry_flow from homeassistant.components import twilio from homeassistant.core import callback -from tests.async_mock import patch - async def test_config_flow_registers_webhook(hass, aiohttp_client): """Test setting up Twilio and sending webhook.""" diff --git a/tests/components/twinkly/test_config_flow.py b/tests/components/twinkly/test_config_flow.py index d1a56277fa7..afbe608219f 100644 --- a/tests/components/twinkly/test_config_flow.py +++ b/tests/components/twinkly/test_config_flow.py @@ -1,5 +1,7 @@ """Tests for the config_flow of the twinly component.""" +from unittest.mock import patch + from homeassistant import config_entries from homeassistant.components.twinkly.const import ( CONF_ENTRY_HOST, @@ -9,7 +11,6 @@ from homeassistant.components.twinkly.const import ( DOMAIN as TWINKLY_DOMAIN, ) -from tests.async_mock import patch from tests.components.twinkly import TEST_MODEL, ClientMock diff --git a/tests/components/twinkly/test_init.py b/tests/components/twinkly/test_init.py index d9dc4623d5e..3f55d2ffdf0 100644 --- a/tests/components/twinkly/test_init.py +++ b/tests/components/twinkly/test_init.py @@ -1,5 +1,6 @@ """Tests of the initialization of the twinly integration.""" +from unittest.mock import patch from uuid import uuid4 from homeassistant.components.twinkly import async_setup_entry, async_unload_entry @@ -12,7 +13,6 @@ from homeassistant.components.twinkly.const import ( ) 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 diff --git a/tests/components/twinkly/test_twinkly.py b/tests/components/twinkly/test_twinkly.py index 7f73589512a..7b7b44ee516 100644 --- a/tests/components/twinkly/test_twinkly.py +++ b/tests/components/twinkly/test_twinkly.py @@ -1,6 +1,7 @@ """Tests for the integration of a twinly device.""" from typing import Tuple +from unittest.mock import patch from homeassistant.components.twinkly.const import ( CONF_ENTRY_HOST, @@ -14,7 +15,6 @@ 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, diff --git a/tests/components/twitch/test_twitch.py b/tests/components/twitch/test_twitch.py index 33afde2a076..310be91c796 100644 --- a/tests/components/twitch/test_twitch.py +++ b/tests/components/twitch/test_twitch.py @@ -1,4 +1,6 @@ """The tests for an update of the Twitch component.""" +from unittest.mock import MagicMock, patch + from requests import HTTPError from twitch.resources import Channel, Follow, Stream, Subscription, User @@ -6,8 +8,6 @@ from homeassistant.components import sensor from homeassistant.const import CONF_CLIENT_ID from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, patch - ENTITY_ID = "sensor.channel123" CONFIG = { sensor.DOMAIN: { diff --git a/tests/components/uk_transport/test_sensor.py b/tests/components/uk_transport/test_sensor.py index ff6cf3d1142..54bc122aa56 100644 --- a/tests/components/uk_transport/test_sensor.py +++ b/tests/components/uk_transport/test_sensor.py @@ -1,5 +1,6 @@ """The tests for the uk_transport platform.""" import re +from unittest.mock import patch import requests_mock @@ -18,7 +19,6 @@ from homeassistant.components.uk_transport.sensor import ( from homeassistant.setup import async_setup_component from homeassistant.util.dt import now -from tests.async_mock import patch from tests.common import load_fixture BUS_ATCOCODE = "340000368SHE" diff --git a/tests/components/unifi/conftest.py b/tests/components/unifi/conftest.py index 8ce7cef0345..b0491a9fa2a 100644 --- a/tests/components/unifi/conftest.py +++ b/tests/components/unifi/conftest.py @@ -1,7 +1,7 @@ """Fixtures for UniFi methods.""" -import pytest +from unittest.mock import patch -from tests.async_mock import patch +import pytest @pytest.fixture(autouse=True) diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index 87233f9983c..c57024dc758 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -1,4 +1,6 @@ """Test UniFi config flow.""" +from unittest.mock import patch + import aiounifi from homeassistant import data_entry_flow @@ -29,7 +31,6 @@ from homeassistant.const import ( from .test_controller import setup_unifi_integration -from tests.async_mock import patch from tests.common import MockConfigEntry CLIENTS = [{"mac": "00:00:00:00:00:01"}] diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 8d5cb85bf9f..8105b88a6bf 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -2,6 +2,7 @@ from collections import deque from copy import deepcopy from datetime import timedelta +from unittest.mock import patch import aiounifi import pytest @@ -35,7 +36,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import MockConfigEntry CONTROLLER_HOST = { diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index 80e3e07fa17..cc2a4b3e4a3 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -1,5 +1,5 @@ """Test UniFi setup process.""" -from unittest.mock import Mock, patch +from unittest.mock import AsyncMock, Mock, patch from homeassistant.components import unifi from homeassistant.components.unifi.const import DOMAIN as UNIFI_DOMAIN @@ -7,7 +7,6 @@ from homeassistant.setup import async_setup_component from .test_controller import setup_unifi_integration -from tests.async_mock import AsyncMock from tests.common import MockConfigEntry, mock_coro diff --git a/tests/components/unifi_direct/test_device_tracker.py b/tests/components/unifi_direct/test_device_tracker.py index be58efa415a..1f96e3fd84b 100644 --- a/tests/components/unifi_direct/test_device_tracker.py +++ b/tests/components/unifi_direct/test_device_tracker.py @@ -1,6 +1,7 @@ """The tests for the Unifi direct device tracker platform.""" from datetime import timedelta import os +from unittest.mock import MagicMock, call, patch import pytest import voluptuous as vol @@ -22,7 +23,6 @@ from homeassistant.components.unifi_direct.device_tracker import ( from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, call, patch from tests.common import assert_setup_component, load_fixture, mock_component scanner_path = "homeassistant.components.unifi_direct.device_tracker.UnifiDeviceScanner" diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index 76a397496ad..fd75620f318 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -3,6 +3,7 @@ import asyncio from copy import copy from os import path import unittest +from unittest.mock import patch from voluptuous.error import MultipleInvalid @@ -23,7 +24,6 @@ from homeassistant.const import ( from homeassistant.core import Context, callback from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import get_test_home_assistant, mock_service diff --git a/tests/components/upb/test_config_flow.py b/tests/components/upb/test_config_flow.py index 0c874399f88..779c66f08aa 100644 --- a/tests/components/upb/test_config_flow.py +++ b/tests/components/upb/test_config_flow.py @@ -1,10 +1,10 @@ """Test the UPB Control config flow.""" +from unittest.mock import MagicMock, PropertyMock, patch + from homeassistant import config_entries, setup from homeassistant.components.upb.const import DOMAIN -from tests.async_mock import MagicMock, PropertyMock, patch - def mocked_upb(sync_complete=True, config_ok=True): """Mock UPB lib.""" diff --git a/tests/components/updater/test_init.py b/tests/components/updater/test_init.py index 89ebf9e1bbb..75ba6c1abd5 100644 --- a/tests/components/updater/test_init.py +++ b/tests/components/updater/test_init.py @@ -1,11 +1,12 @@ """The tests for the Updater component.""" +from unittest.mock import patch + import pytest from homeassistant.components import updater from homeassistant.helpers.update_coordinator import UpdateFailed from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import mock_component NEW_VERSION = "10000.0" diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index 997811a835f..be7794ce8e9 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -1,6 +1,7 @@ """Test UPnP/IGD config flow.""" from datetime import timedelta +from unittest.mock import AsyncMock, patch from homeassistant import config_entries, data_entry_flow from homeassistant.components import ssdp @@ -21,7 +22,6 @@ from homeassistant.setup import async_setup_component from .mock_device import MockDevice -from tests.async_mock import AsyncMock, patch from tests.common import MockConfigEntry diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py index 960d6dacfe5..4373e175bc9 100644 --- a/tests/components/upnp/test_init.py +++ b/tests/components/upnp/test_init.py @@ -1,5 +1,7 @@ """Test UPnP/IGD setup process.""" +from unittest.mock import AsyncMock, patch + from homeassistant.components import upnp from homeassistant.components.upnp.const import ( DISCOVERY_LOCATION, @@ -12,7 +14,6 @@ from homeassistant.setup import async_setup_component from .mock_device import MockDevice -from tests.async_mock import AsyncMock, patch from tests.common import MockConfigEntry diff --git a/tests/components/usgs_earthquakes_feed/test_geo_location.py b/tests/components/usgs_earthquakes_feed/test_geo_location.py index cfd8ae40bcd..ee845701d81 100644 --- a/tests/components/usgs_earthquakes_feed/test_geo_location.py +++ b/tests/components/usgs_earthquakes_feed/test_geo_location.py @@ -1,5 +1,6 @@ """The tests for the USGS Earthquake Hazards Program Feed platform.""" import datetime +from unittest.mock import MagicMock, call, patch from homeassistant.components import geo_location from homeassistant.components.geo_location import ATTR_SOURCE @@ -31,7 +32,6 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import MagicMock, call, patch from tests.common import assert_setup_component, async_fire_time_changed CONFIG = { diff --git a/tests/components/utility_meter/test_init.py b/tests/components/utility_meter/test_init.py index c3a5d829a05..c422d3b5c1f 100644 --- a/tests/components/utility_meter/test_init.py +++ b/tests/components/utility_meter/test_init.py @@ -1,5 +1,6 @@ """The tests for the utility_meter component.""" from datetime import timedelta +from unittest.mock import patch from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.utility_meter.const import ( @@ -18,8 +19,6 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch - async def test_services(hass): """Test energy sensor reset service.""" diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index 2856a63b4f5..bd34238592c 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -1,6 +1,7 @@ """The tests for the utility_meter sensor platform.""" from contextlib import contextmanager from datetime import timedelta +from unittest.mock import patch from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.utility_meter.const import ( @@ -19,7 +20,6 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import async_fire_time_changed diff --git a/tests/components/velbus/test_config_flow.py b/tests/components/velbus/test_config_flow.py index 0a7bc4489c7..f4a95f0fdf9 100644 --- a/tests/components/velbus/test_config_flow.py +++ b/tests/components/velbus/test_config_flow.py @@ -1,11 +1,12 @@ """Tests for the Velbus config flow.""" +from unittest.mock import Mock, patch + import pytest from homeassistant import data_entry_flow from homeassistant.components.velbus import config_flow from homeassistant.const import CONF_NAME, CONF_PORT -from tests.async_mock import Mock, patch from tests.common import MockConfigEntry PORT_SERIAL = "/dev/ttyACME100" diff --git a/tests/components/vera/common.py b/tests/components/vera/common.py index 29c6a0e8683..43ef154b588 100644 --- a/tests/components/vera/common.py +++ b/tests/components/vera/common.py @@ -1,6 +1,7 @@ """Common code for tests.""" from enum import Enum from typing import Callable, Dict, NamedTuple, Tuple +from unittest.mock import MagicMock import pyvera as pv @@ -13,7 +14,6 @@ from homeassistant.components.vera.const import ( from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock from tests.common import MockConfigEntry SetupCallback = Callable[[pv.VeraController, dict], None] diff --git a/tests/components/vera/conftest.py b/tests/components/vera/conftest.py index c20dc4ed499..da027207748 100644 --- a/tests/components/vera/conftest.py +++ b/tests/components/vera/conftest.py @@ -1,9 +1,10 @@ """Fixtures for tests.""" +from unittest.mock import patch + import pytest from .common import ComponentFactory -from tests.async_mock import patch from tests.components.light.conftest import mock_light_profiles # noqa diff --git a/tests/components/vera/test_binary_sensor.py b/tests/components/vera/test_binary_sensor.py index a02c2ef1635..1bcb8d1a183 100644 --- a/tests/components/vera/test_binary_sensor.py +++ b/tests/components/vera/test_binary_sensor.py @@ -1,12 +1,12 @@ """Vera tests.""" +from unittest.mock import MagicMock + import pyvera as pv from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config -from tests.async_mock import MagicMock - async def test_binary_sensor( hass: HomeAssistant, vera_component_factory: ComponentFactory diff --git a/tests/components/vera/test_climate.py b/tests/components/vera/test_climate.py index 370ecc18dcd..076b51997a0 100644 --- a/tests/components/vera/test_climate.py +++ b/tests/components/vera/test_climate.py @@ -1,4 +1,6 @@ """Vera tests.""" +from unittest.mock import MagicMock + import pyvera as pv from homeassistant.components.climate.const import ( @@ -13,8 +15,6 @@ from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config -from tests.async_mock import MagicMock - async def test_climate( hass: HomeAssistant, vera_component_factory: ComponentFactory diff --git a/tests/components/vera/test_common.py b/tests/components/vera/test_common.py index 509bbc5f96a..3832daf0710 100644 --- a/tests/components/vera/test_common.py +++ b/tests/components/vera/test_common.py @@ -1,11 +1,11 @@ """Tests for common vera code.""" from datetime import timedelta +from unittest.mock import MagicMock from homeassistant.components.vera import SubscriptionRegistry from homeassistant.core import HomeAssistant from homeassistant.util.dt import utcnow -from tests.async_mock import MagicMock from tests.common import async_fire_time_changed diff --git a/tests/components/vera/test_config_flow.py b/tests/components/vera/test_config_flow.py index dceac728e4d..780583e38ab 100644 --- a/tests/components/vera/test_config_flow.py +++ b/tests/components/vera/test_config_flow.py @@ -1,4 +1,6 @@ """Vera tests.""" +from unittest.mock import MagicMock, patch + from requests.exceptions import RequestException from homeassistant import config_entries, data_entry_flow @@ -7,7 +9,6 @@ from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS, CONF_SOURCE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry, mock_registry diff --git a/tests/components/vera/test_cover.py b/tests/components/vera/test_cover.py index f3dc2263749..0c05d84e2db 100644 --- a/tests/components/vera/test_cover.py +++ b/tests/components/vera/test_cover.py @@ -1,12 +1,12 @@ """Vera tests.""" +from unittest.mock import MagicMock + import pyvera as pv from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config -from tests.async_mock import MagicMock - async def test_cover( hass: HomeAssistant, vera_component_factory: ComponentFactory diff --git a/tests/components/vera/test_init.py b/tests/components/vera/test_init.py index b3f7b3249ef..b1d6010336a 100644 --- a/tests/components/vera/test_init.py +++ b/tests/components/vera/test_init.py @@ -1,4 +1,6 @@ """Vera tests.""" +from unittest.mock import MagicMock + import pytest import pyvera as pv from requests.exceptions import RequestException @@ -14,7 +16,6 @@ from homeassistant.core import HomeAssistant from .common import ComponentFactory, ConfigSource, new_simple_controller_config -from tests.async_mock import MagicMock from tests.common import MockConfigEntry, mock_registry diff --git a/tests/components/vera/test_light.py b/tests/components/vera/test_light.py index 72118e33a31..3b14aba7429 100644 --- a/tests/components/vera/test_light.py +++ b/tests/components/vera/test_light.py @@ -1,4 +1,6 @@ """Vera tests.""" +from unittest.mock import MagicMock + import pyvera as pv from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_HS_COLOR @@ -6,8 +8,6 @@ from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config -from tests.async_mock import MagicMock - async def test_light( hass: HomeAssistant, vera_component_factory: ComponentFactory diff --git a/tests/components/vera/test_lock.py b/tests/components/vera/test_lock.py index b3433b2bafb..c288ac8709e 100644 --- a/tests/components/vera/test_lock.py +++ b/tests/components/vera/test_lock.py @@ -1,4 +1,6 @@ """Vera tests.""" +from unittest.mock import MagicMock + import pyvera as pv from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED @@ -6,8 +8,6 @@ from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config -from tests.async_mock import MagicMock - async def test_lock( hass: HomeAssistant, vera_component_factory: ComponentFactory diff --git a/tests/components/vera/test_scene.py b/tests/components/vera/test_scene.py index 6c80f27d8c8..2d4b7375498 100644 --- a/tests/components/vera/test_scene.py +++ b/tests/components/vera/test_scene.py @@ -1,12 +1,12 @@ """Vera tests.""" +from unittest.mock import MagicMock + import pyvera as pv from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config -from tests.async_mock import MagicMock - async def test_scene( hass: HomeAssistant, vera_component_factory: ComponentFactory diff --git a/tests/components/vera/test_sensor.py b/tests/components/vera/test_sensor.py index 3d6b11b0685..43777642816 100644 --- a/tests/components/vera/test_sensor.py +++ b/tests/components/vera/test_sensor.py @@ -1,5 +1,6 @@ """Vera tests.""" from typing import Any, Callable, Tuple +from unittest.mock import MagicMock import pyvera as pv @@ -8,8 +9,6 @@ from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config -from tests.async_mock import MagicMock - async def run_sensor_test( hass: HomeAssistant, diff --git a/tests/components/vera/test_switch.py b/tests/components/vera/test_switch.py index 42c74e4e843..b61564c56bc 100644 --- a/tests/components/vera/test_switch.py +++ b/tests/components/vera/test_switch.py @@ -1,12 +1,12 @@ """Vera tests.""" +from unittest.mock import MagicMock + import pyvera as pv from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config -from tests.async_mock import MagicMock - async def test_switch( hass: HomeAssistant, vera_component_factory: ComponentFactory diff --git a/tests/components/verisure/test_ethernet_status.py b/tests/components/verisure/test_ethernet_status.py index 139ac01a1c6..611adde19d9 100644 --- a/tests/components/verisure/test_ethernet_status.py +++ b/tests/components/verisure/test_ethernet_status.py @@ -1,12 +1,11 @@ """Test Verisure ethernet status.""" from contextlib import contextmanager +from unittest.mock import patch from homeassistant.components.verisure import DOMAIN as VERISURE_DOMAIN from homeassistant.const import STATE_UNAVAILABLE from homeassistant.setup import async_setup_component -from tests.async_mock import patch - CONFIG = { "verisure": { "username": "test", diff --git a/tests/components/verisure/test_lock.py b/tests/components/verisure/test_lock.py index decce67dc11..d41bbab2037 100644 --- a/tests/components/verisure/test_lock.py +++ b/tests/components/verisure/test_lock.py @@ -1,6 +1,7 @@ """Tests for the Verisure platform.""" from contextlib import contextmanager +from unittest.mock import call, patch from homeassistant.components.lock import ( DOMAIN as LOCK_DOMAIN, @@ -11,8 +12,6 @@ from homeassistant.components.verisure import DOMAIN as VERISURE_DOMAIN from homeassistant.const import STATE_UNLOCKED from homeassistant.setup import async_setup_component -from tests.async_mock import call, patch - NO_DEFAULT_LOCK_CODE_CONFIG = { "verisure": { "username": "test", diff --git a/tests/components/version/test_sensor.py b/tests/components/version/test_sensor.py index 471043ae3ae..164b4090e5f 100644 --- a/tests/components/version/test_sensor.py +++ b/tests/components/version/test_sensor.py @@ -1,7 +1,7 @@ """The test for the version sensor platform.""" -from homeassistant.setup import async_setup_component +from unittest.mock import patch -from tests.async_mock import patch +from homeassistant.setup import async_setup_component MOCK_VERSION = "10.0" diff --git a/tests/components/vesync/test_config_flow.py b/tests/components/vesync/test_config_flow.py index c82307d351c..f302d0ca5b3 100644 --- a/tests/components/vesync/test_config_flow.py +++ b/tests/components/vesync/test_config_flow.py @@ -1,9 +1,10 @@ """Test for vesync config flow.""" +from unittest.mock import patch + from homeassistant import data_entry_flow from homeassistant.components.vesync import DOMAIN, config_flow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/vilfo/test_config_flow.py b/tests/components/vilfo/test_config_flow.py index ca77b199cfa..6e98ef3fdd9 100644 --- a/tests/components/vilfo/test_config_flow.py +++ b/tests/components/vilfo/test_config_flow.py @@ -1,12 +1,12 @@ """Test the Vilfo Router config flow.""" +from unittest.mock import Mock, patch + import vilfo from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.vilfo.const import DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_ID, CONF_MAC -from tests.async_mock import Mock, patch - async def test_form(hass): """Test we get the form.""" diff --git a/tests/components/vizio/conftest.py b/tests/components/vizio/conftest.py index c8a9083bb1a..917e6f7f291 100644 --- a/tests/components/vizio/conftest.py +++ b/tests/components/vizio/conftest.py @@ -1,4 +1,6 @@ """Configure py.test.""" +from unittest.mock import AsyncMock, patch + import pytest from pyvizio.api.apps import AppConfig from pyvizio.const import DEVICE_CLASS_SPEAKER, MAX_VOLUME @@ -22,8 +24,6 @@ from .const import ( MockStartPairingResponse, ) -from tests.async_mock import AsyncMock, patch - class MockInput: """Mock Vizio device input.""" diff --git a/tests/components/vizio/test_media_player.py b/tests/components/vizio/test_media_player.py index 0d11ec2289c..78976032b00 100644 --- a/tests/components/vizio/test_media_player.py +++ b/tests/components/vizio/test_media_player.py @@ -2,6 +2,7 @@ from contextlib import asynccontextmanager from datetime import timedelta from typing import Any, Dict, List, Optional +from unittest.mock import call, patch import pytest from pytest import raises @@ -75,7 +76,6 @@ from .const import ( VOLUME_STEP, ) -from tests.async_mock import call, patch from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/volumio/test_config_flow.py b/tests/components/volumio/test_config_flow.py index 477ef25cb87..86b5027cd3d 100644 --- a/tests/components/volumio/test_config_flow.py +++ b/tests/components/volumio/test_config_flow.py @@ -1,9 +1,10 @@ """Test the Volumio config flow.""" +from unittest.mock import patch + from homeassistant import config_entries from homeassistant.components.volumio.config_flow import CannotConnectError from homeassistant.components.volumio.const import DOMAIN -from tests.async_mock import patch from tests.common import MockConfigEntry TEST_SYSTEM_INFO = {"id": "1111-1111-1111-1111", "name": "TestVolumio"} diff --git a/tests/components/vultr/test_binary_sensor.py b/tests/components/vultr/test_binary_sensor.py index af99dc12c5f..ab12bfda12c 100644 --- a/tests/components/vultr/test_binary_sensor.py +++ b/tests/components/vultr/test_binary_sensor.py @@ -1,6 +1,7 @@ """Test the Vultr binary sensor platform.""" import json import unittest +from unittest.mock import patch import pytest import requests_mock @@ -19,7 +20,6 @@ from homeassistant.components.vultr import ( ) from homeassistant.const import CONF_NAME, CONF_PLATFORM -from tests.async_mock import patch from tests.common import get_test_home_assistant, load_fixture from tests.components.vultr.test_init import VALID_CONFIG diff --git a/tests/components/vultr/test_init.py b/tests/components/vultr/test_init.py index 9ce96d7969c..80480a2cec2 100644 --- a/tests/components/vultr/test_init.py +++ b/tests/components/vultr/test_init.py @@ -2,13 +2,13 @@ from copy import deepcopy import json import unittest +from unittest.mock import patch import requests_mock from homeassistant import setup import homeassistant.components.vultr as vultr -from tests.async_mock import patch from tests.common import get_test_home_assistant, load_fixture VALID_CONFIG = {"vultr": {"api_key": "ABCDEFG1234567"}} diff --git a/tests/components/vultr/test_sensor.py b/tests/components/vultr/test_sensor.py index 8e9cb606d1e..2b5e07c80b5 100644 --- a/tests/components/vultr/test_sensor.py +++ b/tests/components/vultr/test_sensor.py @@ -1,6 +1,7 @@ """The tests for the Vultr sensor platform.""" import json import unittest +from unittest.mock import patch import pytest import requests_mock @@ -16,7 +17,6 @@ from homeassistant.const import ( DATA_GIGABYTES, ) -from tests.async_mock import patch from tests.common import get_test_home_assistant, load_fixture from tests.components.vultr.test_init import VALID_CONFIG diff --git a/tests/components/vultr/test_switch.py b/tests/components/vultr/test_switch.py index 77eb1e7a8c6..12af400a44a 100644 --- a/tests/components/vultr/test_switch.py +++ b/tests/components/vultr/test_switch.py @@ -1,6 +1,7 @@ """Test the Vultr switch platform.""" import json import unittest +from unittest.mock import patch import pytest import requests_mock @@ -19,7 +20,6 @@ from homeassistant.components.vultr import ( ) from homeassistant.const import CONF_NAME, CONF_PLATFORM -from tests.async_mock import patch from tests.common import get_test_home_assistant, load_fixture from tests.components.vultr.test_init import VALID_CONFIG diff --git a/tests/components/wake_on_lan/test_init.py b/tests/components/wake_on_lan/test_init.py index 60a725a7f9e..3ec7a53a436 100644 --- a/tests/components/wake_on_lan/test_init.py +++ b/tests/components/wake_on_lan/test_init.py @@ -1,12 +1,12 @@ """Tests for Wake On LAN component.""" +from unittest.mock import patch + import pytest import voluptuous as vol from homeassistant.components.wake_on_lan import DOMAIN, SERVICE_SEND_MAGIC_PACKET from homeassistant.setup import async_setup_component -from tests.async_mock import patch - async def test_send_magic_packet(hass): """Test of send magic packet service call.""" diff --git a/tests/components/wake_on_lan/test_switch.py b/tests/components/wake_on_lan/test_switch.py index 598eb3a522e..c2e32f77ccf 100644 --- a/tests/components/wake_on_lan/test_switch.py +++ b/tests/components/wake_on_lan/test_switch.py @@ -1,6 +1,7 @@ """The tests for the wake on lan switch platform.""" import platform import subprocess +from unittest.mock import patch import pytest @@ -14,7 +15,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import async_mock_service diff --git a/tests/components/webhook/test_trigger.py b/tests/components/webhook/test_trigger.py index 46459bd88c4..2a22c330e14 100644 --- a/tests/components/webhook/test_trigger.py +++ b/tests/components/webhook/test_trigger.py @@ -1,10 +1,11 @@ """The tests for the webhook automation trigger.""" +from unittest.mock import patch + import pytest 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 diff --git a/tests/components/webostv/test_media_player.py b/tests/components/webostv/test_media_player.py index 1b85daa1922..8ddc7b31657 100644 --- a/tests/components/webostv/test_media_player.py +++ b/tests/components/webostv/test_media_player.py @@ -26,9 +26,9 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component if sys.version_info >= (3, 8, 0): - from tests.async_mock import patch + from unittest.mock import patch else: - from tests.async_mock import patch + from unittest.mock import patch NAME = "fake" diff --git a/tests/components/websocket_api/test_http.py b/tests/components/websocket_api/test_http.py index e76cbe0dbdc..d3cf4b854f8 100644 --- a/tests/components/websocket_api/test_http.py +++ b/tests/components/websocket_api/test_http.py @@ -1,5 +1,6 @@ """Test Websocket API http module.""" from datetime import timedelta +from unittest.mock import patch from aiohttp import WSMsgType import pytest @@ -7,7 +8,6 @@ import pytest from homeassistant.components.websocket_api import const, http from homeassistant.util.dt import utcnow -from tests.async_mock import patch from tests.common import async_fire_time_changed diff --git a/tests/components/websocket_api/test_init.py b/tests/components/websocket_api/test_init.py index 2d656de8eeb..041c0e76533 100644 --- a/tests/components/websocket_api/test_init.py +++ b/tests/components/websocket_api/test_init.py @@ -1,11 +1,11 @@ """Tests for the Home Assistant Websocket API.""" +from unittest.mock import Mock, patch + from aiohttp import WSMsgType import voluptuous as vol from homeassistant.components.websocket_api import const, messages -from tests.async_mock import Mock, patch - async def test_invalid_message_format(websocket_client): """Test sending invalid JSON.""" diff --git a/tests/components/wemo/conftest.py b/tests/components/wemo/conftest.py index 573de21c692..0e0a69216b2 100644 --- a/tests/components/wemo/conftest.py +++ b/tests/components/wemo/conftest.py @@ -1,5 +1,6 @@ """Fixtures for pywemo.""" import asyncio +from unittest.mock import create_autospec, patch import pytest import pywemo @@ -8,8 +9,6 @@ from homeassistant.components.wemo import CONF_DISCOVERY, CONF_STATIC from homeassistant.components.wemo.const import DOMAIN from homeassistant.setup import async_setup_component -from tests.async_mock import create_autospec, patch - MOCK_HOST = "127.0.0.1" MOCK_PORT = 50000 MOCK_NAME = "WemoDeviceName" diff --git a/tests/components/wemo/entity_test_helpers.py b/tests/components/wemo/entity_test_helpers.py index 16a2f8b3f0d..0ecfc46d526 100644 --- a/tests/components/wemo/entity_test_helpers.py +++ b/tests/components/wemo/entity_test_helpers.py @@ -4,6 +4,7 @@ This is not a test module. These test methods are used by the platform test modu """ import asyncio import threading +from unittest.mock import patch from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, @@ -13,8 +14,6 @@ from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_UNAVAILABLE from homeassistant.core import callback from homeassistant.setup import async_setup_component -from tests.async_mock import patch - def _perform_registry_callback(hass, pywemo_registry, pywemo_device): """Return a callable method to trigger a state callback from the device.""" diff --git a/tests/components/wemo/test_init.py b/tests/components/wemo/test_init.py index 2af91c0fe32..374222d8688 100644 --- a/tests/components/wemo/test_init.py +++ b/tests/components/wemo/test_init.py @@ -1,5 +1,6 @@ """Tests for the wemo component.""" from datetime import timedelta +from unittest.mock import create_autospec, patch import pywemo @@ -10,7 +11,6 @@ from homeassistant.util import dt from .conftest import MOCK_HOST, MOCK_NAME, MOCK_PORT, MOCK_SERIAL_NUMBER -from tests.async_mock import create_autospec, patch from tests.common import async_fire_time_changed diff --git a/tests/components/wemo/test_light_bridge.py b/tests/components/wemo/test_light_bridge.py index 1a36e5421ec..3e7f79200c6 100644 --- a/tests/components/wemo/test_light_bridge.py +++ b/tests/components/wemo/test_light_bridge.py @@ -1,4 +1,6 @@ """Tests for the Wemo light entity via the bridge.""" +from unittest.mock import create_autospec, patch + import pytest import pywemo @@ -13,8 +15,6 @@ import homeassistant.util.dt as dt_util from . import entity_test_helpers -from tests.async_mock import create_autospec, patch - @pytest.fixture def pywemo_model(): diff --git a/tests/components/wiffi/test_config_flow.py b/tests/components/wiffi/test_config_flow.py index cd129d112bc..50433d377a9 100644 --- a/tests/components/wiffi/test_config_flow.py +++ b/tests/components/wiffi/test_config_flow.py @@ -1,5 +1,6 @@ """Test the wiffi integration config flow.""" import errno +from unittest.mock import patch import pytest @@ -12,7 +13,6 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_FORM, ) -from tests.async_mock import patch from tests.common import MockConfigEntry MOCK_CONFIG = {CONF_PORT: 8765} diff --git a/tests/components/wilight/test_config_flow.py b/tests/components/wilight/test_config_flow.py index c8476cbe349..41a44cda174 100644 --- a/tests/components/wilight/test_config_flow.py +++ b/tests/components/wilight/test_config_flow.py @@ -1,4 +1,6 @@ """Test the WiLight config flow.""" +from unittest.mock import patch + import pytest from homeassistant.components.wilight.config_flow import ( @@ -15,7 +17,6 @@ from homeassistant.data_entry_flow import ( ) from homeassistant.helpers.typing import HomeAssistantType -from tests.async_mock import patch from tests.common import MockConfigEntry from tests.components.wilight import ( CONF_COMPONENTS, diff --git a/tests/components/wilight/test_init.py b/tests/components/wilight/test_init.py index efd779a29df..c1557fb44d3 100644 --- a/tests/components/wilight/test_init.py +++ b/tests/components/wilight/test_init.py @@ -1,4 +1,6 @@ """Tests for the WiLight integration.""" +from unittest.mock import patch + import pytest import pywilight @@ -10,7 +12,6 @@ from homeassistant.config_entries import ( ) from homeassistant.helpers.typing import HomeAssistantType -from tests.async_mock import patch from tests.components.wilight import ( HOST, UPNP_MAC_ADDRESS, diff --git a/tests/components/wilight/test_light.py b/tests/components/wilight/test_light.py index d02c7233e60..b7250df546d 100644 --- a/tests/components/wilight/test_light.py +++ b/tests/components/wilight/test_light.py @@ -1,4 +1,6 @@ """Tests for the WiLight integration.""" +from unittest.mock import patch + import pytest import pywilight @@ -16,7 +18,6 @@ from homeassistant.const import ( ) from homeassistant.helpers.typing import HomeAssistantType -from tests.async_mock import patch from tests.components.wilight import ( HOST, UPNP_MAC_ADDRESS, diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index 000900c3355..80e6d07654c 100644 --- a/tests/components/withings/common.py +++ b/tests/components/withings/common.py @@ -1,6 +1,7 @@ """Common data for for the withings component tests.""" from dataclasses import dataclass from typing import List, Optional, Tuple, Union +from unittest.mock import MagicMock from urllib.parse import urlparse from aiohttp.test_utils import TestClient @@ -39,7 +40,6 @@ from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.config_entry_oauth2_flow import AUTH_CALLBACK_PATH from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/withings/test_common.py b/tests/components/withings/test_common.py index 2bf71ab7aa5..a5946ff0533 100644 --- a/tests/components/withings/test_common.py +++ b/tests/components/withings/test_common.py @@ -2,6 +2,7 @@ import datetime import re from typing import Any +from unittest.mock import MagicMock from urllib.parse import urlparse from aiohttp.test_utils import TestClient @@ -17,7 +18,6 @@ from homeassistant.components.withings.common import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2Implementation -from tests.async_mock import MagicMock from tests.common import MockConfigEntry from tests.components.withings.common import ( ComponentFactory, diff --git a/tests/components/withings/test_init.py b/tests/components/withings/test_init.py index 4f4a85585bf..a9948860745 100644 --- a/tests/components/withings/test_init.py +++ b/tests/components/withings/test_init.py @@ -1,4 +1,6 @@ """Tests for the Withings component.""" +from unittest.mock import MagicMock, patch + import pytest import voluptuous as vol from withings_api.common import UnauthorizedException @@ -25,7 +27,6 @@ from .common import ( new_profile_config, ) -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry diff --git a/tests/components/wled/test_config_flow.py b/tests/components/wled/test_config_flow.py index aed10ca3466..4ed1723be77 100644 --- a/tests/components/wled/test_config_flow.py +++ b/tests/components/wled/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for the WLED config flow.""" +from unittest.mock import MagicMock, patch + import aiohttp from wled import WLEDConnectionError @@ -10,7 +12,6 @@ from homeassistant.core import HomeAssistant from . import init_integration -from tests.async_mock import MagicMock, patch from tests.common import load_fixture from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/wled/test_init.py b/tests/components/wled/test_init.py index 8ed043530ae..edce49cfd80 100644 --- a/tests/components/wled/test_init.py +++ b/tests/components/wled/test_init.py @@ -1,11 +1,12 @@ """Tests for the WLED integration.""" +from unittest.mock import MagicMock, patch + from wled import WLEDConnectionError from homeassistant.components.wled.const import DOMAIN from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY from homeassistant.core import HomeAssistant -from tests.async_mock import MagicMock, patch from tests.components.wled import init_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py index fbcc6ca71b6..eb9124ab906 100644 --- a/tests/components/wled/test_light.py +++ b/tests/components/wled/test_light.py @@ -1,5 +1,6 @@ """Tests for the WLED light platform.""" import json +from unittest.mock import patch from wled import Device as WLEDDevice, WLEDConnectionError @@ -37,7 +38,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import async_fire_time_changed, load_fixture from tests.components.wled import init_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py index d5c1c738d2f..11e14bd79d9 100644 --- a/tests/components/wled/test_sensor.py +++ b/tests/components/wled/test_sensor.py @@ -1,5 +1,6 @@ """Tests for the WLED sensor platform.""" from datetime import datetime +from unittest.mock import patch import pytest @@ -24,7 +25,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util -from tests.async_mock import patch from tests.components.wled import init_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/wled/test_switch.py b/tests/components/wled/test_switch.py index 388e3317b39..c6e30ef903e 100644 --- a/tests/components/wled/test_switch.py +++ b/tests/components/wled/test_switch.py @@ -1,4 +1,6 @@ """Tests for the WLED switch platform.""" +from unittest.mock import patch + from wled import WLEDConnectionError from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN @@ -19,7 +21,6 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant -from tests.async_mock import patch from tests.components.wled import init_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/wolflink/test_config_flow.py b/tests/components/wolflink/test_config_flow.py index 897650b48e8..5108883ed81 100644 --- a/tests/components/wolflink/test_config_flow.py +++ b/tests/components/wolflink/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Wolf SmartSet Service config flow.""" +from unittest.mock import patch + from httpcore import ConnectError from wolf_smartset.models import Device from wolf_smartset.token_auth import InvalidAuth @@ -12,7 +14,6 @@ from homeassistant.components.wolflink.const import ( ) from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import patch from tests.common import MockConfigEntry CONFIG = { diff --git a/tests/components/workday/test_binary_sensor.py b/tests/components/workday/test_binary_sensor.py index 7d50ec2994c..3ec17c3e6d3 100644 --- a/tests/components/workday/test_binary_sensor.py +++ b/tests/components/workday/test_binary_sensor.py @@ -1,5 +1,6 @@ """Tests the Home Assistant workday binary sensor.""" from datetime import date +from unittest.mock import patch import pytest import voluptuous as vol @@ -7,7 +8,6 @@ import voluptuous as vol import homeassistant.components.workday.binary_sensor as binary_sensor from homeassistant.setup import setup_component -from tests.async_mock import patch from tests.common import assert_setup_component, get_test_home_assistant FUNCTION_PATH = "homeassistant.components.workday.binary_sensor.get_date" diff --git a/tests/components/xbox/test_config_flow.py b/tests/components/xbox/test_config_flow.py index 516a57c039b..7e2863a5861 100644 --- a/tests/components/xbox/test_config_flow.py +++ b/tests/components/xbox/test_config_flow.py @@ -1,9 +1,10 @@ """Test the xbox config flow.""" +from unittest.mock import patch + from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.xbox.const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN from homeassistant.helpers import config_entry_oauth2_flow -from tests.async_mock import patch from tests.common import MockConfigEntry CLIENT_ID = "1234" diff --git a/tests/components/xiaomi/test_device_tracker.py b/tests/components/xiaomi/test_device_tracker.py index 1760b3274a4..af5e192b9cf 100644 --- a/tests/components/xiaomi/test_device_tracker.py +++ b/tests/components/xiaomi/test_device_tracker.py @@ -1,5 +1,6 @@ """The tests for the Xiaomi router device tracker platform.""" import logging +from unittest.mock import MagicMock, call, patch import requests @@ -8,8 +9,6 @@ import homeassistant.components.xiaomi.device_tracker as xiaomi from homeassistant.components.xiaomi.device_tracker import get_scanner from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME -from tests.async_mock import MagicMock, call, patch - _LOGGER = logging.getLogger(__name__) INVALID_USERNAME = "bob" diff --git a/tests/components/xiaomi_aqara/test_config_flow.py b/tests/components/xiaomi_aqara/test_config_flow.py index c84758f8b63..280775a7130 100644 --- a/tests/components/xiaomi_aqara/test_config_flow.py +++ b/tests/components/xiaomi_aqara/test_config_flow.py @@ -1,5 +1,6 @@ """Test the Xiaomi Aqara config flow.""" from socket import gaierror +from unittest.mock import Mock, patch import pytest @@ -8,8 +9,6 @@ from homeassistant.components import zeroconf from homeassistant.components.xiaomi_aqara import config_flow, const from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT -from tests.async_mock import Mock, patch - ZEROCONF_NAME = "name" ZEROCONF_PROP = "properties" ZEROCONF_MAC = "mac" diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index d8ddd657efc..dbe78957586 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Xiaomi Miio config flow.""" +from unittest.mock import Mock, patch + from miio import DeviceException from homeassistant import config_entries @@ -6,8 +8,6 @@ from homeassistant.components import zeroconf from homeassistant.components.xiaomi_miio import config_flow, const from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN -from tests.async_mock import Mock, patch - ZEROCONF_NAME = "name" ZEROCONF_PROP = "properties" ZEROCONF_MAC = "mac" diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index 0fa241fb0b9..b1a3c08b84b 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -1,6 +1,7 @@ """The tests for the Xiaomi vacuum platform.""" from datetime import datetime, time, timedelta from unittest import mock +from unittest.mock import MagicMock, patch from miio import DeviceException import pytest @@ -57,8 +58,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, patch - PLATFORM = "xiaomi_miio" # calls made when device status is requested diff --git a/tests/components/yamaha/test_media_player.py b/tests/components/yamaha/test_media_player.py index 6a13c1d46e1..84a9e475c32 100644 --- a/tests/components/yamaha/test_media_player.py +++ b/tests/components/yamaha/test_media_player.py @@ -1,4 +1,6 @@ """The tests for the Yamaha Media player platform.""" +from unittest.mock import MagicMock, PropertyMock, call, patch + import pytest import homeassistant.components.media_player as mp @@ -7,8 +9,6 @@ from homeassistant.components.yamaha.const import DOMAIN from homeassistant.helpers.discovery import async_load_platform from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, PropertyMock, call, patch - CONFIG = {"media_player": {"platform": "yamaha", "host": "127.0.0.1"}} diff --git a/tests/components/yandex_transport/test_yandex_transport_sensor.py b/tests/components/yandex_transport/test_yandex_transport_sensor.py index c70ebf6806c..a727ea6e6cd 100644 --- a/tests/components/yandex_transport/test_yandex_transport_sensor.py +++ b/tests/components/yandex_transport/test_yandex_transport_sensor.py @@ -1,6 +1,7 @@ """Tests for the yandex transport platform.""" import json +from unittest.mock import AsyncMock, patch import pytest @@ -9,7 +10,6 @@ from homeassistant.const import CONF_NAME from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import AsyncMock, patch from tests.common import assert_setup_component, load_fixture REPLY = json.loads(load_fixture("yandex_transport_reply.json")) diff --git a/tests/components/yeelight/__init__.py b/tests/components/yeelight/__init__.py index 5405b69490b..38c28a9a900 100644 --- a/tests/components/yeelight/__init__.py +++ b/tests/components/yeelight/__init__.py @@ -1,4 +1,6 @@ """Tests for the Yeelight integration.""" +from unittest.mock import MagicMock, patch + from yeelight import BulbException, BulbType from yeelight.main import _MODEL_SPECS @@ -12,8 +14,6 @@ from homeassistant.components.yeelight import ( ) from homeassistant.const import CONF_DEVICES, CONF_ID, CONF_NAME -from tests.async_mock import MagicMock, patch - IP_ADDRESS = "192.168.1.239" MODEL = "color" ID = "0x000000000015243f" diff --git a/tests/components/yeelight/test_binary_sensor.py b/tests/components/yeelight/test_binary_sensor.py index 8b2ec835722..f716469fc9a 100644 --- a/tests/components/yeelight/test_binary_sensor.py +++ b/tests/components/yeelight/test_binary_sensor.py @@ -1,4 +1,6 @@ """Test the Yeelight binary sensor.""" +from unittest.mock import patch + from homeassistant.components.yeelight import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_component @@ -6,8 +8,6 @@ from homeassistant.setup import async_setup_component from . import MODULE, NAME, PROPERTIES, YAML_CONFIGURATION, _mocked_bulb -from tests.async_mock import patch - ENTITY_BINARY_SENSOR = f"binary_sensor.{NAME}_nightlight" diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index 10191a5f6c7..8fa1ba5c988 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Yeelight config flow.""" +from unittest.mock import MagicMock, patch + from homeassistant import config_entries from homeassistant.components.yeelight import ( CONF_DEVICE, @@ -30,7 +32,6 @@ from . import ( _patch_discovery, ) -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry DEFAULT_CONFIG = { diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py index 882f9944ca1..c91ae33d986 100644 --- a/tests/components/yeelight/test_init.py +++ b/tests/components/yeelight/test_init.py @@ -1,5 +1,5 @@ """Test Yeelight.""" -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch from yeelight import BulbType @@ -32,7 +32,6 @@ from . import ( _patch_discovery, ) -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/yeelight/test_light.py b/tests/components/yeelight/test_light.py index 115b825d863..0b7415140a3 100644 --- a/tests/components/yeelight/test_light.py +++ b/tests/components/yeelight/test_light.py @@ -1,5 +1,6 @@ """Test the Yeelight light.""" import logging +from unittest.mock import MagicMock, patch from yeelight import ( BulbException, @@ -107,7 +108,6 @@ from . import ( _patch_discovery, ) -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry CONFIG_ENTRY_DATA = { diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 6b79c552911..51bdf269bf2 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -1,4 +1,6 @@ """Test Zeroconf component setup process.""" +from unittest.mock import patch + from zeroconf import ( BadTypeInNameException, InterfaceChoice, @@ -17,8 +19,6 @@ from homeassistant.const import ( from homeassistant.generated import zeroconf as zc_gen from homeassistant.setup import async_setup_component -from tests.async_mock import patch - NON_UTF8_VALUE = b"ABCDEF\x8a" NON_ASCII_KEY = b"non-ascii-key\x8a" PROPERTIES = { diff --git a/tests/components/zeroconf/test_usage.py b/tests/components/zeroconf/test_usage.py index 9cf953f4c8d..0f902632db8 100644 --- a/tests/components/zeroconf/test_usage.py +++ b/tests/components/zeroconf/test_usage.py @@ -1,12 +1,12 @@ """Test Zeroconf multiple instance protection.""" +from unittest.mock import Mock, patch + import zeroconf from homeassistant.components.zeroconf import async_get_instance from homeassistant.components.zeroconf.usage import install_multiple_zeroconf_catcher from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, patch - DOMAIN = "zeroconf" diff --git a/tests/components/zerproc/test_config_flow.py b/tests/components/zerproc/test_config_flow.py index 1a607bb8c9c..53b90e0364e 100644 --- a/tests/components/zerproc/test_config_flow.py +++ b/tests/components/zerproc/test_config_flow.py @@ -1,11 +1,11 @@ """Test the zerproc config flow.""" +from unittest.mock import patch + import pyzerproc from homeassistant import config_entries, setup from homeassistant.components.zerproc.config_flow import DOMAIN -from tests.async_mock import patch - async def test_flow_success(hass): """Test we get the form.""" diff --git a/tests/components/zerproc/test_light.py b/tests/components/zerproc/test_light.py index 77fdfb7d48a..1f0c7652bfd 100644 --- a/tests/components/zerproc/test_light.py +++ b/tests/components/zerproc/test_light.py @@ -1,4 +1,6 @@ """Test the zerproc lights.""" +from unittest.mock import MagicMock, patch + import pytest import pyzerproc @@ -24,7 +26,6 @@ from homeassistant.const import ( ) import homeassistant.util.dt as dt_util -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 249e1bf58b2..234ca0c9ba5 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -1,5 +1,6 @@ """Common test objects.""" import time +from unittest.mock import AsyncMock, Mock from zigpy.device import Device as zigpy_dev from zigpy.endpoint import Endpoint as zigpy_ep @@ -13,8 +14,6 @@ import zigpy.zdo.types import homeassistant.components.zha.core.const as zha_const from homeassistant.util import slugify -from tests.async_mock import AsyncMock, Mock - class FakeEndpoint: """Fake endpoint for moking zigpy.""" diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index 2a2ea6a1bb0..57241b9bb74 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -1,4 +1,6 @@ """Test configuration for the ZHA component.""" +from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch + import pytest import zigpy from zigpy.application import ControllerApplication @@ -13,7 +15,6 @@ from homeassistant.setup import async_setup_component from .common import FakeDevice, FakeEndpoint, get_zha_gateway -from tests.async_mock import AsyncMock, MagicMock, PropertyMock, patch from tests.common import MockConfigEntry from tests.components.light.conftest import mock_light_profiles # noqa diff --git a/tests/components/zha/test_api.py b/tests/components/zha/test_api.py index 53861bc0d9a..363aa12db6e 100644 --- a/tests/components/zha/test_api.py +++ b/tests/components/zha/test_api.py @@ -1,5 +1,6 @@ """Test ZHA API.""" from binascii import unhexlify +from unittest.mock import AsyncMock, patch import pytest import voluptuous as vol @@ -41,8 +42,6 @@ from homeassistant.core import Context from .conftest import FIXTURE_GRP_ID, FIXTURE_GRP_NAME -from tests.async_mock import AsyncMock, patch - IEEE_SWITCH_DEVICE = "01:2d:6f:00:0a:90:69:e7" IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py index e0c31d38bbb..8a2ca1f05c3 100644 --- a/tests/components/zha/test_channels.py +++ b/tests/components/zha/test_channels.py @@ -1,6 +1,7 @@ """Test ZHA Core channels.""" import asyncio from unittest import mock +from unittest.mock import AsyncMock, patch import pytest import zigpy.profiles.zha @@ -14,7 +15,6 @@ import homeassistant.components.zha.core.registries as registries from .common import get_zha_gateway, make_zcl_header -from tests.async_mock import AsyncMock, patch from tests.common import async_capture_events diff --git a/tests/components/zha/test_climate.py b/tests/components/zha/test_climate.py index cc152c1a36d..05412ddb64d 100644 --- a/tests/components/zha/test_climate.py +++ b/tests/components/zha/test_climate.py @@ -1,5 +1,7 @@ """Test zha climate.""" +from unittest.mock import patch + import pytest import zigpy.zcl.clusters from zigpy.zcl.clusters.hvac import Thermostat @@ -46,8 +48,6 @@ from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_UNKNOWN from .common import async_enable_traffic, find_entity_id, send_attributes_report -from tests.async_mock import patch - CLIMATE = { 1: { "device_type": zigpy.profiles.zha.DeviceType.THERMOSTAT, diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 6fcc369182d..fe65def839d 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -1,6 +1,7 @@ """Tests for ZHA config flow.""" import os +from unittest.mock import AsyncMock, MagicMock, patch, sentinel import pytest import serial.tools.list_ports @@ -13,7 +14,6 @@ from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_SOURCE from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM -from tests.async_mock import AsyncMock, MagicMock, patch, sentinel from tests.common import MockConfigEntry diff --git a/tests/components/zha/test_cover.py b/tests/components/zha/test_cover.py index 97fa5c7579d..c926618813c 100644 --- a/tests/components/zha/test_cover.py +++ b/tests/components/zha/test_cover.py @@ -1,5 +1,6 @@ """Test zha cover.""" import asyncio +from unittest.mock import AsyncMock, patch import pytest import zigpy.profiles.zha @@ -32,7 +33,6 @@ from .common import ( send_attributes_report, ) -from tests.async_mock import AsyncMock, patch from tests.common import async_capture_events, mock_coro, mock_restore_cache diff --git a/tests/components/zha/test_device.py b/tests/components/zha/test_device.py index 1cc9fb27d89..1ce75045d38 100644 --- a/tests/components/zha/test_device.py +++ b/tests/components/zha/test_device.py @@ -2,6 +2,7 @@ from datetime import timedelta import time from unittest import mock +from unittest.mock import patch import pytest import zigpy.profiles.zha @@ -14,7 +15,6 @@ import homeassistant.util.dt as dt_util from .common import async_enable_traffic, make_zcl_header -from tests.async_mock import patch from tests.common import async_fire_time_changed diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py index b5da98dc01f..ac2ef085e14 100644 --- a/tests/components/zha/test_discover.py +++ b/tests/components/zha/test_discover.py @@ -2,6 +2,7 @@ import re from unittest import mock +from unittest.mock import AsyncMock, patch import pytest import zigpy.profiles.zha @@ -30,8 +31,6 @@ import homeassistant.helpers.entity_registry from .common import get_zha_gateway from .zha_devices_list import DEVICES -from tests.async_mock import AsyncMock, patch - NO_TAIL_ID = re.compile("_\\d$") diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index 65be13fd96c..61828c135bc 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -1,4 +1,6 @@ """Test zha fan.""" +from unittest.mock import AsyncMock, call, patch + import pytest import zigpy.profiles.zha as zha import zigpy.zcl.clusters.general as general @@ -37,8 +39,6 @@ from .common import ( send_attributes_report, ) -from tests.async_mock import AsyncMock, call, patch - IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" IEEE_GROUPABLE_DEVICE2 = "02:2d:6f:00:0a:90:69:e8" diff --git a/tests/components/zha/test_init.py b/tests/components/zha/test_init.py index f0b82231fa9..f259febd817 100644 --- a/tests/components/zha/test_init.py +++ b/tests/components/zha/test_init.py @@ -1,5 +1,7 @@ """Tests for ZHA integration init.""" +from unittest.mock import AsyncMock, patch + import pytest from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH @@ -12,7 +14,6 @@ from homeassistant.components.zha.core.const import ( from homeassistant.const import MAJOR_VERSION, MINOR_VERSION from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock, patch from tests.common import MockConfigEntry DATA_RADIO_TYPE = "deconz" diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index ea1b8487b7c..0a9a492a148 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -1,5 +1,6 @@ """Test zha light.""" from datetime import timedelta +from unittest.mock import AsyncMock, MagicMock, call, patch, sentinel import pytest import zigpy.profiles.zha as zha @@ -23,7 +24,6 @@ from .common import ( send_attributes_report, ) -from tests.async_mock import AsyncMock, MagicMock, call, patch, sentinel from tests.common import async_fire_time_changed ON = 1 diff --git a/tests/components/zha/test_number.py b/tests/components/zha/test_number.py index 947bad37e01..1bb9aa947ff 100644 --- a/tests/components/zha/test_number.py +++ b/tests/components/zha/test_number.py @@ -1,4 +1,6 @@ """Test zha analog output.""" +from unittest.mock import call, patch + import pytest import zigpy.profiles.zha import zigpy.types @@ -16,7 +18,6 @@ from .common import ( send_attributes_report, ) -from tests.async_mock import call, patch from tests.common import mock_coro diff --git a/tests/components/zodiac/test_sensor.py b/tests/components/zodiac/test_sensor.py index b08352dd2c0..6c784d3998f 100644 --- a/tests/components/zodiac/test_sensor.py +++ b/tests/components/zodiac/test_sensor.py @@ -1,5 +1,6 @@ """The test for the zodiac sensor platform.""" from datetime import datetime +from unittest.mock import patch import pytest @@ -19,8 +20,6 @@ from homeassistant.components.zodiac.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch - DAY1 = datetime(2020, 11, 15, tzinfo=dt_util.UTC) DAY2 = datetime(2020, 4, 20, tzinfo=dt_util.UTC) DAY3 = datetime(2020, 4, 21, tzinfo=dt_util.UTC) diff --git a/tests/components/zone/test_init.py b/tests/components/zone/test_init.py index 994bd5e6dda..07fd83cbe77 100644 --- a/tests/components/zone/test_init.py +++ b/tests/components/zone/test_init.py @@ -1,4 +1,6 @@ """Test zone component.""" +from unittest.mock import patch + import pytest from homeassistant import setup @@ -15,7 +17,6 @@ from homeassistant.core import Context from homeassistant.exceptions import Unauthorized from homeassistant.helpers import entity_registry -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/zwave/conftest.py b/tests/components/zwave/conftest.py index 5366f028328..13da12c67ff 100644 --- a/tests/components/zwave/conftest.py +++ b/tests/components/zwave/conftest.py @@ -1,9 +1,10 @@ """Fixtures for Z-Wave tests.""" +from unittest.mock import AsyncMock, MagicMock, patch + import pytest from homeassistant.components.zwave import const -from tests.async_mock import AsyncMock, MagicMock, patch from tests.components.light.conftest import mock_light_profiles # noqa from tests.mock.zwave import MockNetwork, MockNode, MockOption, MockValue diff --git a/tests/components/zwave/test_binary_sensor.py b/tests/components/zwave/test_binary_sensor.py index 815563d2a5e..731e413caf8 100644 --- a/tests/components/zwave/test_binary_sensor.py +++ b/tests/components/zwave/test_binary_sensor.py @@ -1,9 +1,9 @@ """Test Z-Wave binary sensors.""" import datetime +from unittest.mock import patch from homeassistant.components.zwave import binary_sensor, const -from tests.async_mock import patch from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed diff --git a/tests/components/zwave/test_cover.py b/tests/components/zwave/test_cover.py index cde0957e2b3..e8b784feefe 100644 --- a/tests/components/zwave/test_cover.py +++ b/tests/components/zwave/test_cover.py @@ -1,4 +1,6 @@ """Test Z-Wave cover devices.""" +from unittest.mock import MagicMock + from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN from homeassistant.components.zwave import ( CONF_INVERT_OPENCLOSE_BUTTONS, @@ -7,7 +9,6 @@ from homeassistant.components.zwave import ( cover, ) -from tests.async_mock import MagicMock from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 5f408518271..d70c3d631d5 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -2,6 +2,7 @@ import asyncio from collections import OrderedDict from datetime import datetime +from unittest.mock import MagicMock, patch import pytest from pytz import utc @@ -20,7 +21,6 @@ from homeassistant.const import ATTR_ENTITY_ID from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg from homeassistant.helpers.entity_registry import async_get_registry -from tests.async_mock import MagicMock, patch from tests.common import async_fire_time_changed, mock_registry from tests.mock.zwave import MockEntityValues, MockNetwork, MockNode, MockValue diff --git a/tests/components/zwave/test_light.py b/tests/components/zwave/test_light.py index 1b973294daf..9e943c54bb4 100644 --- a/tests/components/zwave/test_light.py +++ b/tests/components/zwave/test_light.py @@ -1,4 +1,6 @@ """Test Z-Wave lights.""" +from unittest.mock import MagicMock, patch + from homeassistant.components import zwave from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -14,7 +16,6 @@ from homeassistant.components.light import ( ) from homeassistant.components.zwave import const, light -from tests.async_mock import MagicMock, patch from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed diff --git a/tests/components/zwave/test_lock.py b/tests/components/zwave/test_lock.py index 2f82bcb2764..d5b6d0a0d27 100644 --- a/tests/components/zwave/test_lock.py +++ b/tests/components/zwave/test_lock.py @@ -1,8 +1,9 @@ """Test Z-Wave locks.""" +from unittest.mock import MagicMock, patch + from homeassistant import config_entries from homeassistant.components.zwave import const, lock -from tests.async_mock import MagicMock, patch from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed diff --git a/tests/components/zwave/test_node_entity.py b/tests/components/zwave/test_node_entity.py index 29c1126b5d1..ba77aabc923 100644 --- a/tests/components/zwave/test_node_entity.py +++ b/tests/components/zwave/test_node_entity.py @@ -1,8 +1,9 @@ """Test Z-Wave node entity.""" +from unittest.mock import MagicMock, patch + from homeassistant.components.zwave import const, node_entity from homeassistant.const import ATTR_ENTITY_ID -from tests.async_mock import MagicMock, patch import tests.mock.zwave as mock_zwave diff --git a/tests/components/zwave/test_switch.py b/tests/components/zwave/test_switch.py index b61c456ccb9..4293a4a23fd 100644 --- a/tests/components/zwave/test_switch.py +++ b/tests/components/zwave/test_switch.py @@ -1,7 +1,8 @@ """Test Z-Wave switches.""" +from unittest.mock import patch + from homeassistant.components.zwave import switch -from tests.async_mock import patch from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed diff --git a/tests/conftest.py b/tests/conftest.py index d8fb9f2914b..55249a58fc9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,7 @@ import functools import logging import ssl import threading +from unittest.mock import MagicMock, patch from aiohttp.test_utils import make_mocked_request import multidict @@ -27,7 +28,6 @@ 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, patch from tests.ignore_uncaught_exceptions import IGNORE_UNCAUGHT_EXCEPTIONS pytest.register_assert_rewrite("tests.common") diff --git a/tests/helpers/test_aiohttp_client.py b/tests/helpers/test_aiohttp_client.py index 0f37ebd7b3b..e6f113c7699 100644 --- a/tests/helpers/test_aiohttp_client.py +++ b/tests/helpers/test_aiohttp_client.py @@ -1,5 +1,6 @@ """Test the aiohttp client helper.""" import asyncio +from unittest.mock import Mock, patch import aiohttp import pytest @@ -8,8 +9,6 @@ from homeassistant.core import EVENT_HOMEASSISTANT_CLOSE import homeassistant.helpers.aiohttp_client as client from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, patch - @pytest.fixture(name="camera_client") def camera_client_fixture(hass, hass_client): diff --git a/tests/helpers/test_area_registry.py b/tests/helpers/test_area_registry.py index 4b6ca7da3fe..ec008dde7da 100644 --- a/tests/helpers/test_area_registry.py +++ b/tests/helpers/test_area_registry.py @@ -1,12 +1,12 @@ """Tests for the Area Registry.""" import asyncio +import unittest.mock import pytest from homeassistant.core import callback from homeassistant.helpers import area_registry -import tests.async_mock from tests.common import flush_store, mock_area_registry @@ -178,7 +178,7 @@ async def test_loading_area_from_storage(hass, hass_storage): async def test_loading_race_condition(hass): """Test only one storage load called when concurrent loading occurred .""" - with tests.async_mock.patch( + with unittest.mock.patch( "homeassistant.helpers.area_registry.AreaRegistry.async_load" ) as mock_load: results = await asyncio.gather( diff --git a/tests/helpers/test_check_config.py b/tests/helpers/test_check_config.py index 7959cf66403..c5b75b84342 100644 --- a/tests/helpers/test_check_config.py +++ b/tests/helpers/test_check_config.py @@ -1,5 +1,6 @@ """Test check_config helper.""" import logging +from unittest.mock import Mock, patch from homeassistant.config import YAML_CONFIG_FILE from homeassistant.helpers.check_config import ( @@ -7,7 +8,6 @@ from homeassistant.helpers.check_config import ( async_check_ha_config_file, ) -from tests.async_mock import Mock, patch from tests.common import mock_platform, patch_yaml_files _LOGGER = logging.getLogger(__name__) diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 802ad7699e3..fe2a9aa4406 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -1,5 +1,6 @@ """Test the condition helper.""" from logging import ERROR +from unittest.mock import patch import pytest @@ -9,8 +10,6 @@ from homeassistant.helpers.template import Template from homeassistant.setup import async_setup_component from homeassistant.util import dt -from tests.async_mock import patch - async def test_invalid_condition(hass): """Test if invalid condition raises.""" diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index 926aa98e308..a70f6ad8d5c 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -1,11 +1,12 @@ """Tests for the Config Entry Flow helper.""" +from unittest.mock import Mock, patch + 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_flow -from tests.async_mock import Mock, patch from tests.common import ( MockConfigEntry, MockModule, diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index f2f2db37d7f..617c4690696 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -2,6 +2,7 @@ import asyncio import logging import time +from unittest.mock import patch import aiohttp import pytest @@ -10,7 +11,6 @@ from homeassistant import config_entries, data_entry_flow, setup from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.network import NoURLAvailableError -from tests.async_mock import patch from tests.common import MockConfigEntry, mock_platform TEST_DOMAIN = "oauth2_test" diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 5d907408b61..1397e499c7e 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -3,6 +3,7 @@ from datetime import date, datetime, timedelta import enum import os from socket import _GLOBAL_DEFAULT_TIMEOUT +from unittest.mock import Mock, patch import uuid import pytest @@ -11,8 +12,6 @@ import voluptuous as vol import homeassistant from homeassistant.helpers import config_validation as cv, template -from tests.async_mock import Mock, patch - def test_boolean(): """Test boolean validation.""" diff --git a/tests/helpers/test_debounce.py b/tests/helpers/test_debounce.py index e9cb5749eaf..cdced565e73 100644 --- a/tests/helpers/test_debounce.py +++ b/tests/helpers/test_debounce.py @@ -1,7 +1,7 @@ """Tests for debounce.""" -from homeassistant.helpers import debounce +from unittest.mock import AsyncMock -from tests.async_mock import AsyncMock +from homeassistant.helpers import debounce async def test_immediate_works(hass): diff --git a/tests/helpers/test_deprecation.py b/tests/helpers/test_deprecation.py index c7e903f7b16..38410c3bf0f 100644 --- a/tests/helpers/test_deprecation.py +++ b/tests/helpers/test_deprecation.py @@ -1,7 +1,7 @@ """Test deprecation helpers.""" -from homeassistant.helpers.deprecation import deprecated_substitute, get_deprecated +from unittest.mock import MagicMock, patch -from tests.async_mock import MagicMock, patch +from homeassistant.helpers.deprecation import deprecated_substitute, get_deprecated class MockBaseClass: diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 7fa787e023e..7f15239556d 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -1,5 +1,6 @@ """Tests for the Device Registry.""" import asyncio +from unittest.mock import patch import pytest @@ -7,7 +8,6 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import CoreState, callback from homeassistant.helpers import device_registry, entity_registry -from tests.async_mock import patch from tests.common import MockConfigEntry, flush_store, mock_device_registry diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 15e9bf55aa4..52149b060e4 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -3,6 +3,7 @@ import asyncio from datetime import timedelta import threading +from unittest.mock import MagicMock, PropertyMock, patch import pytest @@ -10,7 +11,6 @@ from homeassistant.const import ATTR_DEVICE_CLASS, STATE_UNAVAILABLE from homeassistant.core import Context from homeassistant.helpers import entity, entity_registry -from tests.async_mock import MagicMock, PropertyMock, patch from tests.common import ( MockConfigEntry, MockEntity, diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 0373705186f..8d61ec7d509 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -3,6 +3,7 @@ from collections import OrderedDict from datetime import timedelta import logging +from unittest.mock import AsyncMock, Mock, patch import pytest import voluptuous as vol @@ -15,7 +16,6 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import AsyncMock, Mock, patch from tests.common import ( MockConfigEntry, MockEntity, diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index d12c574d1a9..25d105ab549 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -2,6 +2,7 @@ import asyncio from datetime import timedelta import logging +from unittest.mock import Mock, patch import pytest @@ -16,7 +17,6 @@ from homeassistant.helpers.entity_component import ( ) import homeassistant.util.dt as dt_util -from tests.async_mock import Mock, patch from tests.common import ( MockConfigEntry, MockEntity, diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 19af3715160..21f4392122e 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -1,5 +1,7 @@ """Tests for the Entity Registry.""" import asyncio +import unittest.mock +from unittest.mock import patch import pytest @@ -7,8 +9,6 @@ from homeassistant.const import EVENT_HOMEASSISTANT_START, STATE_UNAVAILABLE from homeassistant.core import CoreState, callback, valid_entity_id from homeassistant.helpers import entity_registry -import tests.async_mock -from tests.async_mock import patch from tests.common import ( MockConfigEntry, flush_store, @@ -429,7 +429,7 @@ async def test_loading_invalid_entity_id(hass, hass_storage): async def test_loading_race_condition(hass): """Test only one storage load called when concurrent loading occurred .""" - with tests.async_mock.patch( + with unittest.mock.patch( "homeassistant.helpers.entity_registry.EntityRegistry.async_load" ) as mock_load: results = await asyncio.gather( diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 9c7ddb09f85..b0f58f76a66 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -2,6 +2,7 @@ # pylint: disable=protected-access import asyncio from datetime import datetime, timedelta +from unittest.mock import patch from astral import Astral import jinja2 @@ -39,7 +40,6 @@ from homeassistant.helpers.template import Template from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import async_fire_time_changed DEFAULT_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE diff --git a/tests/helpers/test_frame.py b/tests/helpers/test_frame.py index 6daae51403c..7fc46b3699d 100644 --- a/tests/helpers/test_frame.py +++ b/tests/helpers/test_frame.py @@ -1,10 +1,10 @@ """Test the frame helper.""" +from unittest.mock import Mock, patch + import pytest from homeassistant.helpers import frame -from tests.async_mock import Mock, patch - async def test_extract_frame_integration(caplog): """Test extracting the current frame from integration context.""" diff --git a/tests/helpers/test_httpx_client.py b/tests/helpers/test_httpx_client.py index 5444cd4643d..53a6985f5cc 100644 --- a/tests/helpers/test_httpx_client.py +++ b/tests/helpers/test_httpx_client.py @@ -1,13 +1,13 @@ """Test the httpx client helper.""" +from unittest.mock import Mock, patch + import httpx import pytest from homeassistant.core import EVENT_HOMEASSISTANT_CLOSE import homeassistant.helpers.httpx_client as client -from tests.async_mock import Mock, patch - async def test_get_async_client_with_ssl(hass): """Test init async client with ssl.""" diff --git a/tests/helpers/test_instance_id.py b/tests/helpers/test_instance_id.py index 36e87b31a43..c8417c519a1 100644 --- a/tests/helpers/test_instance_id.py +++ b/tests/helpers/test_instance_id.py @@ -1,5 +1,5 @@ """Tests for instance ID helper.""" -from tests.async_mock import patch +from unittest.mock import patch async def test_get_id_empty(hass, hass_storage): diff --git a/tests/helpers/test_integration_platform.py b/tests/helpers/test_integration_platform.py index 6f0e56d34c8..d6c844c0d91 100644 --- a/tests/helpers/test_integration_platform.py +++ b/tests/helpers/test_integration_platform.py @@ -1,7 +1,8 @@ """Test integration platform helpers.""" +from unittest.mock import Mock + from homeassistant.setup import ATTR_COMPONENT, EVENT_COMPONENT_LOADED -from tests.async_mock import Mock from tests.common import mock_platform diff --git a/tests/helpers/test_network.py b/tests/helpers/test_network.py index 3330c42d9fc..06158558d5e 100644 --- a/tests/helpers/test_network.py +++ b/tests/helpers/test_network.py @@ -1,4 +1,6 @@ """Test network helper.""" +from unittest.mock import Mock, patch + import pytest from homeassistant.components import cloud @@ -15,7 +17,6 @@ from homeassistant.helpers.network import ( is_internal_request, ) -from tests.async_mock import Mock, patch from tests.common import mock_component diff --git a/tests/helpers/test_reload.py b/tests/helpers/test_reload.py index 3ed8d17b3f4..b4f47fa65b1 100644 --- a/tests/helpers/test_reload.py +++ b/tests/helpers/test_reload.py @@ -1,6 +1,7 @@ """Tests for the reload helper.""" import logging from os import path +from unittest.mock import AsyncMock, Mock, patch import pytest @@ -16,7 +17,6 @@ from homeassistant.helpers.reload import ( ) from homeassistant.loader import async_get_integration -from tests.async_mock import AsyncMock, Mock, patch from tests.common import ( MockModule, MockPlatform, diff --git a/tests/helpers/test_restore_state.py b/tests/helpers/test_restore_state.py index 15eed1c7e19..1a2fb2f57b5 100644 --- a/tests/helpers/test_restore_state.py +++ b/tests/helpers/test_restore_state.py @@ -1,5 +1,6 @@ """The tests for the Restore component.""" from datetime import datetime +from unittest.mock import patch from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.core import CoreState, State @@ -14,8 +15,6 @@ from homeassistant.helpers.restore_state import ( ) from homeassistant.util import dt as dt_util -from tests.async_mock import patch - async def test_caching_data(hass): """Test that we cache data.""" diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index c81ed681d42..5be5f6bc91f 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -6,6 +6,7 @@ from datetime import timedelta import logging from types import MappingProxyType from unittest import mock +from unittest.mock import patch import pytest import voluptuous as vol @@ -19,7 +20,6 @@ from homeassistant.helpers import config_validation as cv, script from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import ( async_capture_events, async_fire_time_changed, diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index a75593ddd40..95ccdc84395 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -2,6 +2,7 @@ from collections import OrderedDict from copy import deepcopy import unittest +from unittest.mock import AsyncMock, Mock, patch import pytest import voluptuous as vol @@ -26,7 +27,6 @@ from homeassistant.helpers import ( import homeassistant.helpers.config_validation as cv from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock, Mock, patch from tests.common import ( MockEntity, get_test_home_assistant, diff --git a/tests/helpers/test_singleton.py b/tests/helpers/test_singleton.py index 03230a02ab8..c695efd94a8 100644 --- a/tests/helpers/test_singleton.py +++ b/tests/helpers/test_singleton.py @@ -1,10 +1,10 @@ """Test singleton helper.""" +from unittest.mock import Mock + import pytest from homeassistant.helpers import singleton -from tests.async_mock import Mock - @pytest.fixture def mock_hass(): diff --git a/tests/helpers/test_state.py b/tests/helpers/test_state.py index 626f8a83744..89b0f3c6850 100644 --- a/tests/helpers/test_state.py +++ b/tests/helpers/test_state.py @@ -1,6 +1,7 @@ """Test state helpers.""" import asyncio from datetime import timedelta +from unittest.mock import patch import pytest @@ -21,7 +22,6 @@ import homeassistant.core as ha from homeassistant.helpers import state from homeassistant.util import dt as dt_util -from tests.async_mock import patch from tests.common import async_mock_service diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py index 7fa6dd61845..61bf9fa8d0e 100644 --- a/tests/helpers/test_storage.py +++ b/tests/helpers/test_storage.py @@ -2,6 +2,7 @@ import asyncio from datetime import timedelta import json +from unittest.mock import Mock, patch import pytest @@ -13,7 +14,6 @@ from homeassistant.core import CoreState from homeassistant.helpers import storage from homeassistant.util import dt -from tests.async_mock import Mock, patch from tests.common import async_fire_time_changed MOCK_VERSION = 1 diff --git a/tests/helpers/test_storage_remove.py b/tests/helpers/test_storage_remove.py index 9a447771ea6..aa118aded59 100644 --- a/tests/helpers/test_storage_remove.py +++ b/tests/helpers/test_storage_remove.py @@ -2,11 +2,11 @@ import asyncio from datetime import timedelta import os +from unittest.mock import patch from homeassistant.helpers import storage from homeassistant.util import dt -from tests.async_mock import patch from tests.common import async_fire_time_changed, async_test_home_assistant diff --git a/tests/helpers/test_sun.py b/tests/helpers/test_sun.py index a877c7cdb00..b8ecd1ed86a 100644 --- a/tests/helpers/test_sun.py +++ b/tests/helpers/test_sun.py @@ -1,13 +1,12 @@ """The tests for the Sun helpers.""" # pylint: disable=protected-access from datetime import datetime, timedelta +from unittest.mock import patch from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET import homeassistant.helpers.sun as sun import homeassistant.util.dt as dt_util -from tests.async_mock import patch - def test_next_events(hass): """Test retrieving next sun events.""" diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index c8a8bc0710c..174d61ea470 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -2,6 +2,7 @@ from datetime import datetime import math import random +from unittest.mock import patch import pytest import pytz @@ -23,8 +24,6 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import UnitSystem -from tests.async_mock import patch - def _set_up_units(hass): """Set up the tests.""" diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index e8c5c756d59..8f555914682 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -2,6 +2,7 @@ import asyncio from os import path import pathlib +from unittest.mock import Mock, patch import pytest @@ -10,8 +11,6 @@ from homeassistant.helpers import translation from homeassistant.loader import async_get_integration from homeassistant.setup import async_setup_component, setup_component -from tests.async_mock import Mock, patch - @pytest.fixture def mock_config_flows(): diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py index 90567456bfb..0651a4a9324 100644 --- a/tests/helpers/test_update_coordinator.py +++ b/tests/helpers/test_update_coordinator.py @@ -2,6 +2,7 @@ import asyncio from datetime import timedelta import logging +from unittest.mock import AsyncMock, Mock, patch import urllib.error import aiohttp @@ -11,7 +12,6 @@ import requests from homeassistant.helpers import update_coordinator from homeassistant.util.dt import utcnow -from tests.async_mock import AsyncMock, Mock, patch from tests.common import async_fire_time_changed _LOGGER = logging.getLogger(__name__) diff --git a/tests/mock/zwave.py b/tests/mock/zwave.py index 41c0aa5727b..5565b43a78e 100644 --- a/tests/mock/zwave.py +++ b/tests/mock/zwave.py @@ -1,7 +1,7 @@ """Mock helpers for Z-Wave component.""" -from pydispatch import dispatcher +from unittest.mock import MagicMock -from tests.async_mock import MagicMock +from pydispatch import dispatcher def value_changed(value): diff --git a/tests/scripts/test_auth.py b/tests/scripts/test_auth.py index c9ada99dc29..3ab19450879 100644 --- a/tests/scripts/test_auth.py +++ b/tests/scripts/test_auth.py @@ -1,10 +1,11 @@ """Test the auth script to manage local users.""" +from unittest.mock import Mock, patch + import pytest from homeassistant.auth.providers import homeassistant as hass_auth from homeassistant.scripts import auth as script_auth -from tests.async_mock import Mock, patch from tests.common import register_auth_provider diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 10034cb08af..6eaaee87af0 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -1,12 +1,12 @@ """Test check_config script.""" import logging +from unittest.mock import patch import pytest from homeassistant.config import YAML_CONFIG_FILE import homeassistant.scripts.check_config as check_config -from tests.async_mock import patch from tests.common import get_test_config_dir, patch_yaml_files _LOGGER = logging.getLogger(__name__) diff --git a/tests/scripts/test_init.py b/tests/scripts/test_init.py index 2c14bfdcf0a..8feef2d3384 100644 --- a/tests/scripts/test_init.py +++ b/tests/scripts/test_init.py @@ -1,7 +1,7 @@ """Test script init.""" -import homeassistant.scripts as scripts +from unittest.mock import patch -from tests.async_mock import patch +import homeassistant.scripts as scripts @patch("homeassistant.scripts.get_default_config_dir", return_value="/default") diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index ed31724b58b..fc653c25d0b 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -2,7 +2,7 @@ # pylint: disable=protected-access import asyncio import os -from unittest.mock import Mock +from unittest.mock import Mock, patch import pytest @@ -11,7 +11,6 @@ import homeassistant.config as config_util from homeassistant.exceptions import HomeAssistantError import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import ( MockModule, MockPlatform, diff --git a/tests/test_config.py b/tests/test_config.py index 931b672d01b..7dd7d61e8ef 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -4,6 +4,7 @@ from collections import OrderedDict import copy import os from unittest import mock +from unittest.mock import AsyncMock, Mock, patch import pytest import voluptuous as vol @@ -34,7 +35,6 @@ from homeassistant.loader import async_get_integration from homeassistant.util import dt as dt_util from homeassistant.util.yaml import SECRET_YAML -from tests.async_mock import AsyncMock, Mock, patch from tests.common import get_test_config_dir, patch_yaml_files CONFIG_DIR = get_test_config_dir() diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 0be89af4810..a048f5a7043 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1,6 +1,7 @@ """Test the config manager.""" import asyncio from datetime import timedelta +from unittest.mock import AsyncMock, patch import pytest @@ -10,7 +11,6 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.setup import async_setup_component from homeassistant.util import dt -from tests.async_mock import AsyncMock, patch from tests.common import ( MockConfigEntry, MockEntity, diff --git a/tests/test_core.py b/tests/test_core.py index 541f75e6343..dbed2b8c0bf 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -6,6 +6,7 @@ import functools import logging import os from tempfile import TemporaryDirectory +from unittest.mock import MagicMock, Mock, PropertyMock, patch import pytest import pytz @@ -40,7 +41,6 @@ from homeassistant.exceptions import ( 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_capture_events, async_mock_service PST = pytz.timezone("America/Los_Angeles") diff --git a/tests/test_loader.py b/tests/test_loader.py index 64c8c5764cf..00c6e2b0c20 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -1,11 +1,12 @@ """Test to verify that we can load components.""" +from unittest.mock import ANY, patch + import pytest from homeassistant import core, loader from homeassistant.components import http, hue from homeassistant.components.hue import light as hue_light -from tests.async_mock import ANY, patch from tests.common import MockModule, async_mock_service, mock_integration diff --git a/tests/test_main.py b/tests/test_main.py index 40c34b77b50..5ec6460301f 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,9 +1,9 @@ """Test methods in __main__.""" +from unittest.mock import PropertyMock, patch + from homeassistant import __main__ as main from homeassistant.const import REQUIRED_PYTHON_VER -from tests.async_mock import PropertyMock, patch - @patch("sys.exit") def test_validate_python(mock_exit): diff --git a/tests/test_requirements.py b/tests/test_requirements.py index c0a1f0723ac..bc206a136c2 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -1,5 +1,6 @@ """Test requirements module.""" import os +from unittest.mock import call, patch import pytest @@ -11,7 +12,6 @@ from homeassistant.requirements import ( async_process_requirements, ) -from tests.async_mock import call, patch from tests.common import MockModule, mock_integration diff --git a/tests/test_setup.py b/tests/test_setup.py index ebcb1093779..539ed3f1442 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -3,6 +3,7 @@ import asyncio import os import threading +from unittest.mock import Mock, patch import pytest import voluptuous as vol @@ -18,7 +19,6 @@ from homeassistant.helpers.config_validation import ( ) import homeassistant.util.dt as dt_util -from tests.async_mock import Mock, patch from tests.common import ( MockConfigEntry, MockModule, diff --git a/tests/util/test_async.py b/tests/util/test_async.py index 047697168b4..db088ada93e 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -1,13 +1,12 @@ """Tests for async util methods from Python source.""" import asyncio import time +from unittest.mock import MagicMock, Mock, patch import pytest from homeassistant.util import async_ as hasync -from tests.async_mock import MagicMock, Mock, patch - @patch("asyncio.coroutines.iscoroutine") @patch("concurrent.futures.Future") diff --git a/tests/util/test_init.py b/tests/util/test_init.py index ef5ecd898d7..2ffca07082b 100644 --- a/tests/util/test_init.py +++ b/tests/util/test_init.py @@ -1,13 +1,12 @@ """Test Home Assistant util methods.""" from datetime import datetime, timedelta +from unittest.mock import MagicMock, patch import pytest from homeassistant import util import homeassistant.util.dt as dt_util -from tests.async_mock import MagicMock, patch - def test_sanitize_filename(): """Test sanitize_filename.""" diff --git a/tests/util/test_json.py b/tests/util/test_json.py index af858967150..1cbaaae7d23 100644 --- a/tests/util/test_json.py +++ b/tests/util/test_json.py @@ -7,6 +7,7 @@ import os import sys from tempfile import mkdtemp import unittest +from unittest.mock import Mock import pytest @@ -19,8 +20,6 @@ from homeassistant.util.json import ( save_json, ) -from tests.async_mock import Mock - # Test data that can be saved as JSON TEST_JSON_A = {"a": 1, "B": "two"} TEST_JSON_B = {"a": "one", "B": 2} diff --git a/tests/util/test_location.py b/tests/util/test_location.py index 403e24121ad..9eb2dc70561 100644 --- a/tests/util/test_location.py +++ b/tests/util/test_location.py @@ -1,10 +1,11 @@ """Test Home Assistant location util methods.""" +from unittest.mock import Mock, patch + import aiohttp import pytest import homeassistant.util.location as location_util -from tests.async_mock import Mock, patch from tests.common import load_fixture # Paris diff --git a/tests/util/test_logging.py b/tests/util/test_logging.py index 04d6f133381..1a82c35e82d 100644 --- a/tests/util/test_logging.py +++ b/tests/util/test_logging.py @@ -2,13 +2,12 @@ import asyncio import logging import queue +from unittest.mock import patch import pytest import homeassistant.util.logging as logging_util -from tests.async_mock import patch - def test_sensitive_data_filter(): """Test the logging sensitive data filter.""" diff --git a/tests/util/test_package.py b/tests/util/test_package.py index 064d1379e08..0c251662444 100644 --- a/tests/util/test_package.py +++ b/tests/util/test_package.py @@ -4,14 +4,13 @@ import logging import os from subprocess import PIPE import sys +from unittest.mock import MagicMock, call, patch import pkg_resources import pytest import homeassistant.util.package as package -from tests.async_mock import MagicMock, call, patch - RESOURCE_DIR = os.path.abspath( os.path.join(os.path.dirname(__file__), "..", "resources") ) diff --git a/tests/util/yaml/test_init.py b/tests/util/yaml/test_init.py index 1c5b9bd9fd8..34097287bc3 100644 --- a/tests/util/yaml/test_init.py +++ b/tests/util/yaml/test_init.py @@ -3,6 +3,7 @@ import io import logging import os import unittest +from unittest.mock import patch import pytest @@ -11,7 +12,6 @@ from homeassistant.exceptions import HomeAssistantError import homeassistant.util.yaml as yaml from homeassistant.util.yaml import loader as yaml_loader -from tests.async_mock import patch from tests.common import get_test_config_dir, patch_yaml_files From 508d33a220d46588f31a5421a630ba229edd27e5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 1 Jan 2021 23:36:15 +0100 Subject: [PATCH 054/507] Remove deprecated PTVSD integration (#44748) --- .coveragerc | 1 - CODEOWNERS | 1 - homeassistant/bootstrap.py | 2 +- homeassistant/components/ptvsd/__init__.py | 70 -------------------- homeassistant/components/ptvsd/manifest.json | 7 -- requirements_all.txt | 3 - requirements_test_all.txt | 3 - tests/components/ptvsd/__init__py | 1 - tests/components/ptvsd/test_ptvsd.py | 48 -------------- 9 files changed, 1 insertion(+), 135 deletions(-) delete mode 100644 homeassistant/components/ptvsd/__init__.py delete mode 100644 homeassistant/components/ptvsd/manifest.json delete mode 100644 tests/components/ptvsd/__init__py delete mode 100644 tests/components/ptvsd/test_ptvsd.py diff --git a/.coveragerc b/.coveragerc index ccbdaa9c6fa..1fdabeff103 100644 --- a/.coveragerc +++ b/.coveragerc @@ -703,7 +703,6 @@ omit = homeassistant/components/prowl/notify.py homeassistant/components/proxmoxve/* homeassistant/components/proxy/camera.py - homeassistant/components/ptvsd/* homeassistant/components/pulseaudio_loopback/switch.py homeassistant/components/pushbullet/notify.py homeassistant/components/pushbullet/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 7b874bb0ebf..1ec8f6f30f1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -352,7 +352,6 @@ homeassistant/components/progettihwsw/* @ardaseremet homeassistant/components/prometheus/* @knyar homeassistant/components/proxmoxve/* @k4ds3 @jhollowe homeassistant/components/ps4/* @ktnrg45 -homeassistant/components/ptvsd/* @swamp-ig homeassistant/components/push/* @dgomes homeassistant/components/pvoutput/* @fabaff homeassistant/components/pvpc_hourly_pricing/* @azogue diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index b68fc9d17ac..ff8ecfa0070 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -48,7 +48,7 @@ COOLDOWN_TIME = 60 MAX_LOAD_CONCURRENTLY = 6 -DEBUGGER_INTEGRATIONS = {"debugpy", "ptvsd"} +DEBUGGER_INTEGRATIONS = {"debugpy"} CORE_INTEGRATIONS = ("homeassistant", "persistent_notification") LOGGING_INTEGRATIONS = { # Set log levels diff --git a/homeassistant/components/ptvsd/__init__.py b/homeassistant/components/ptvsd/__init__.py deleted file mode 100644 index 258589084a0..00000000000 --- a/homeassistant/components/ptvsd/__init__.py +++ /dev/null @@ -1,70 +0,0 @@ -""" -Enable ptvsd debugger to attach to HA. - -Attach ptvsd debugger by default to port 5678. -""" - -from asyncio import Event -import logging -from threading import Thread - -import voluptuous as vol - -from homeassistant.const import CONF_HOST, CONF_PORT -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType, HomeAssistantType - -DOMAIN = "ptvsd" - -CONF_WAIT = "wait" - -_LOGGER = logging.getLogger(__name__) - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_HOST, default="0.0.0.0"): cv.string, - vol.Optional(CONF_PORT, default=5678): cv.port, - vol.Optional(CONF_WAIT, default=False): cv.boolean, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass: HomeAssistantType, config: ConfigType): - """Set up ptvsd debugger.""" - _LOGGER.warning( - "ptvsd is deprecated and will be removed in Home Assistant Core 0.120." - "The debugpy integration can be used as a full replacement for ptvsd" - ) - - # This is a local import, since importing this at the top, will cause - # ptvsd to hook into `sys.settrace`. So does `coverage` to generate - # coverage, resulting in a battle and incomplete code test coverage. - import ptvsd # pylint: disable=import-outside-toplevel - - conf = config[DOMAIN] - host = conf[CONF_HOST] - port = conf[CONF_PORT] - - ptvsd.enable_attach((host, port)) - - wait = conf[CONF_WAIT] - if wait: - _LOGGER.warning("Waiting for ptvsd connection on %s:%s", host, port) - ready = Event() - - def waitfor(): - ptvsd.wait_for_attach() - hass.loop.call_soon_threadsafe(ready.set) - - Thread(target=waitfor).start() - - await ready.wait() - else: - _LOGGER.warning("Listening for ptvsd connection on %s:%s", host, port) - - return True diff --git a/homeassistant/components/ptvsd/manifest.json b/homeassistant/components/ptvsd/manifest.json deleted file mode 100644 index 5feb04e92bb..00000000000 --- a/homeassistant/components/ptvsd/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "domain": "ptvsd", - "name": "PTVSD - Python Tools for Visual Studio Debug Server", - "documentation": "https://www.home-assistant.io/integrations/ptvsd", - "requirements": ["ptvsd==4.3.2"], - "codeowners": ["@swamp-ig"] -} diff --git a/requirements_all.txt b/requirements_all.txt index 940d2b96a89..4bef886080d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1170,9 +1170,6 @@ proxmoxer==1.1.1 # homeassistant.components.systemmonitor psutil==5.8.0 -# homeassistant.components.ptvsd -ptvsd==4.3.2 - # homeassistant.components.wink pubnubsub-handler==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c7da734d3e9..6585308074a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -587,9 +587,6 @@ progettihwsw==0.1.1 # homeassistant.components.prometheus prometheus_client==0.7.1 -# homeassistant.components.ptvsd -ptvsd==4.3.2 - # homeassistant.components.androidtv pure-python-adb[async]==0.3.0.dev0 diff --git a/tests/components/ptvsd/__init__py b/tests/components/ptvsd/__init__py deleted file mode 100644 index e2a1a9ba0a6..00000000000 --- a/tests/components/ptvsd/__init__py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for PTVSD Debugger""" diff --git a/tests/components/ptvsd/test_ptvsd.py b/tests/components/ptvsd/test_ptvsd.py deleted file mode 100644 index b3e408833dc..00000000000 --- a/tests/components/ptvsd/test_ptvsd.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Tests for PTVSD Debugger.""" - -from unittest.mock import AsyncMock, patch - -from pytest import mark - -from homeassistant.bootstrap import _async_set_up_integrations -import homeassistant.components.ptvsd as ptvsd_component -from homeassistant.setup import async_setup_component - - -@mark.skip("causes code cover to fail") -async def test_ptvsd(hass): - """Test loading ptvsd component.""" - with patch("ptvsd.enable_attach") as attach: - with patch("ptvsd.wait_for_attach") as wait: - assert await async_setup_component( - hass, ptvsd_component.DOMAIN, {ptvsd_component.DOMAIN: {}} - ) - - attach.assert_called_once_with(("0.0.0.0", 5678)) - assert wait.call_count == 0 - - -@mark.skip("causes code cover to fail") -async def test_ptvsd_wait(hass): - """Test loading ptvsd component with wait.""" - with patch("ptvsd.enable_attach") as attach: - with patch("ptvsd.wait_for_attach") as wait: - assert await async_setup_component( - hass, - ptvsd_component.DOMAIN, - {ptvsd_component.DOMAIN: {ptvsd_component.CONF_WAIT: True}}, - ) - - attach.assert_called_once_with(("0.0.0.0", 5678)) - assert wait.call_count == 1 - - -async def test_ptvsd_bootstrap(hass): - """Test loading ptvsd component with wait.""" - config = {ptvsd_component.DOMAIN: {ptvsd_component.CONF_WAIT: True}} - - with patch("homeassistant.components.ptvsd.async_setup", AsyncMock()) as setup_mock: - setup_mock.return_value = True - await _async_set_up_integrations(hass, config) - - assert setup_mock.call_count == 1 From a2ca08905f2614a3da2dc97aee911c8828924e55 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 2 Jan 2021 01:34:10 +0100 Subject: [PATCH 055/507] Guard unbound var for DSMR (#44673) --- homeassistant/components/dsmr/sensor.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 0c53fbd6079..78cd317bb3e 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -197,6 +197,10 @@ async def async_setup_entry( async def connect_and_reconnect(): """Connect to DSMR and keep reconnecting until Home Assistant stops.""" + stop_listener = None + transport = None + protocol = None + while hass.state != CoreState.stopping: # Start DSMR asyncio.Protocol reader try: @@ -211,10 +215,9 @@ async def async_setup_entry( # Wait for reader to close await protocol.wait_closed() - # Unexpected disconnect - if transport: - # remove listener - stop_listener() + # Unexpected disconnect + if not hass.is_stopping: + stop_listener() transport = None protocol = None @@ -234,7 +237,7 @@ async def async_setup_entry( protocol = None except CancelledError: if stop_listener: - stop_listener() + stop_listener() # pylint: disable=not-callable if transport: transport.close() From 321c0a87ae4c990e0b53408e0439f95890b605d1 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 1 Jan 2021 16:51:01 -0800 Subject: [PATCH 056/507] Resolve nest pub/sub subscriber token refresh issues (#44686) --- .coveragerc | 1 - homeassistant/components/nest/__init__.py | 12 +- homeassistant/components/nest/api.py | 35 ++++- tests/components/nest/common.py | 41 ++++-- tests/components/nest/test_api.py | 151 ++++++++++++++++++++++ tests/components/nest/test_init_sdm.py | 6 +- 6 files changed, 212 insertions(+), 34 deletions(-) create mode 100644 tests/components/nest/test_api.py diff --git a/.coveragerc b/.coveragerc index 1fdabeff103..5c85b2553d1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -580,7 +580,6 @@ omit = homeassistant/components/neato/vacuum.py homeassistant/components/nederlandse_spoorwegen/sensor.py homeassistant/components/nello/lock.py - homeassistant/components/nest/api.py homeassistant/components/nest/legacy/* homeassistant/components/netatmo/__init__.py homeassistant/components/netatmo/api.py diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 1240d30f027..04c8b9f3627 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -30,14 +30,7 @@ from homeassistant.helpers import ( ) from . import api, config_flow -from .const import ( - API_URL, - DATA_SDM, - DATA_SUBSCRIBER, - DOMAIN, - OAUTH2_AUTHORIZE, - OAUTH2_TOKEN, -) +from .const import DATA_SDM, DATA_SUBSCRIBER, DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN from .events import EVENT_NAME_MAP, NEST_EVENT from .legacy import async_setup_legacy, async_setup_legacy_entry @@ -161,7 +154,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): auth = api.AsyncConfigEntryAuth( aiohttp_client.async_get_clientsession(hass), session, - API_URL, + config[CONF_CLIENT_ID], + config[CONF_CLIENT_SECRET], ) subscriber = GoogleNestSubscriber( auth, config[CONF_PROJECT_ID], config[CONF_SUBSCRIBER_ID] diff --git a/homeassistant/components/nest/api.py b/homeassistant/components/nest/api.py index 46138469d6d..3b571354c0f 100644 --- a/homeassistant/components/nest/api.py +++ b/homeassistant/components/nest/api.py @@ -1,11 +1,15 @@ """API for Google Nest Device Access bound to Home Assistant OAuth.""" +import datetime + from aiohttp import ClientSession from google.oauth2.credentials import Credentials from google_nest_sdm.auth import AbstractAuth from homeassistant.helpers import config_entry_oauth2_flow +from .const import API_URL, OAUTH2_TOKEN, SDM_SCOPES + # See https://developers.google.com/nest/device-access/registration @@ -16,20 +20,37 @@ class AsyncConfigEntryAuth(AbstractAuth): self, websession: ClientSession, oauth_session: config_entry_oauth2_flow.OAuth2Session, - api_url: str, + client_id: str, + client_secret: str, ): """Initialize Google Nest Device Access auth.""" - super().__init__(websession, api_url) + super().__init__(websession, API_URL) self._oauth_session = oauth_session + self._client_id = client_id + self._client_secret = client_secret async def async_get_access_token(self): - """Return a valid access token.""" + """Return a valid access token for SDM API.""" if not self._oauth_session.valid_token: await self._oauth_session.async_ensure_token_valid() - return self._oauth_session.token["access_token"] async def async_get_creds(self): - """Return a minimal OAuth credential.""" - token = await self.async_get_access_token() - return Credentials(token=token) + """Return an OAuth credential for Pub/Sub Subscriber.""" + # We don't have a way for Home Assistant to refresh creds on behalf + # of the google pub/sub subscriber. Instead, build a full + # Credentials object with enough information for the subscriber to + # handle this on its own. We purposely don't refresh the token here + # even when it is expired to fully hand off this responsibility and + # know it is working at startup (then if not, fail loudly). + token = self._oauth_session.token + creds = Credentials( + token=token["access_token"], + refresh_token=token["refresh_token"], + token_uri=OAUTH2_TOKEN, + client_id=self._client_id, + client_secret=self._client_secret, + scopes=SDM_SCOPES, + ) + creds.expiry = datetime.datetime.fromtimestamp(token["expires_at"]) + return creds diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index 2721cd08c19..d6dc730ec11 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -9,30 +9,45 @@ from google_nest_sdm.event import EventMessage from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber from homeassistant.components.nest import DOMAIN +from homeassistant.components.nest.const import SDM_SCOPES from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry +PROJECT_ID = "some-project-id" +CLIENT_ID = "some-client-id" +CLIENT_SECRET = "some-client-secret" + CONFIG = { "nest": { - "client_id": "some-client-id", - "client_secret": "some-client-secret", + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET, # Required fields for using SDM API - "project_id": "some-project-id", + "project_id": PROJECT_ID, "subscriber_id": "projects/example/subscriptions/subscriber-id-9876", }, } -CONFIG_ENTRY_DATA = { - "sdm": {}, # Indicates new SDM API, not legacy API - "auth_implementation": "local", - "token": { - "expires_at": time.time() + 86400, - "access_token": { - "token": "some-token", +FAKE_TOKEN = "some-token" +FAKE_REFRESH_TOKEN = "some-refresh-token" + + +def create_config_entry(hass, token_expiration_time=None): + """Create a ConfigEntry and add it to Home Assistant.""" + if token_expiration_time is None: + token_expiration_time = time.time() + 86400 + config_entry_data = { + "sdm": {}, # Indicates new SDM API, not legacy API + "auth_implementation": "nest", + "token": { + "access_token": FAKE_TOKEN, + "refresh_token": FAKE_REFRESH_TOKEN, + "scope": " ".join(SDM_SCOPES), + "token_type": "Bearer", + "expires_at": token_expiration_time, }, - }, -} + } + MockConfigEntry(domain=DOMAIN, data=config_entry_data).add_to_hass(hass) class FakeDeviceManager(DeviceManager): @@ -86,7 +101,7 @@ class FakeSubscriber(GoogleNestSubscriber): async def async_setup_sdm_platform(hass, platform, devices={}, structures={}): """Set up the platform and prerequisites.""" - MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA).add_to_hass(hass) + create_config_entry(hass) device_manager = FakeDeviceManager(devices=devices, structures=structures) subscriber = FakeSubscriber(device_manager) with patch( diff --git a/tests/components/nest/test_api.py b/tests/components/nest/test_api.py new file mode 100644 index 00000000000..835edf0c3a2 --- /dev/null +++ b/tests/components/nest/test_api.py @@ -0,0 +1,151 @@ +"""Tests for the Nest integration API glue library. + +There are two interesting cases to exercise that have different strategies +for token refresh and for testing: +- API based requests, tested using aioclient_mock +- Pub/sub subcriber initialization, intercepted with patch() + +The tests below exercise both cases during integration setup. +""" + +import time +from unittest.mock import patch + +from homeassistant.components.nest import DOMAIN +from homeassistant.components.nest.const import API_URL, OAUTH2_TOKEN, SDM_SCOPES +from homeassistant.setup import async_setup_component +from homeassistant.util import dt + +from .common import ( + CLIENT_ID, + CLIENT_SECRET, + CONFIG, + FAKE_REFRESH_TOKEN, + FAKE_TOKEN, + PROJECT_ID, + create_config_entry, +) + +FAKE_UPDATED_TOKEN = "fake-updated-token" + + +async def async_setup_sdm(hass): + """Set up the integration.""" + assert await async_setup_component(hass, DOMAIN, CONFIG) + await hass.async_block_till_done() + + +async def test_auth(hass, aioclient_mock): + """Exercise authentication library creates valid credentials.""" + + expiration_time = time.time() + 86400 + create_config_entry(hass, expiration_time) + + # Prepare to capture credentials in API request. Empty payloads just mean + # no devices or structures are loaded. + aioclient_mock.get(f"{API_URL}/enterprises/{PROJECT_ID}/structures", json={}) + aioclient_mock.get(f"{API_URL}/enterprises/{PROJECT_ID}/devices", json={}) + + # Prepare to capture credentials for Subscriber + captured_creds = None + + async def async_new_subscriber(creds, subscription_name, loop, async_callback): + """Capture credentials for tests.""" + nonlocal captured_creds + captured_creds = creds + return None # GoogleNestSubscriber + + with patch( + "google_nest_sdm.google_nest_subscriber.DefaultSubscriberFactory.async_new_subscriber", + side_effect=async_new_subscriber, + ) as new_subscriber_mock: + await async_setup_sdm(hass) + + # Verify API requests are made with the correct credentials + calls = aioclient_mock.mock_calls + assert len(calls) == 2 + (method, url, data, headers) = calls[0] + assert headers == {"Authorization": f"Bearer {FAKE_TOKEN}"} + (method, url, data, headers) = calls[1] + assert headers == {"Authorization": f"Bearer {FAKE_TOKEN}"} + + # Verify the susbcriber was created with the correct credentials + assert len(new_subscriber_mock.mock_calls) == 1 + assert captured_creds + creds = captured_creds + assert creds.token == FAKE_TOKEN + assert creds.refresh_token == FAKE_REFRESH_TOKEN + assert int(dt.as_timestamp(creds.expiry)) == int(expiration_time) + assert creds.valid + assert not creds.expired + assert creds.token_uri == OAUTH2_TOKEN + assert creds.client_id == CLIENT_ID + assert creds.client_secret == CLIENT_SECRET + assert creds.scopes == SDM_SCOPES + + +async def test_auth_expired_token(hass, aioclient_mock): + """Verify behavior of an expired token.""" + + expiration_time = time.time() - 86400 + create_config_entry(hass, expiration_time) + + # Prepare a token refresh response + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "access_token": FAKE_UPDATED_TOKEN, + "expires_at": time.time() + 86400, + "expires_in": 86400, + }, + ) + # Prepare to capture credentials in API request. Empty payloads just mean + # no devices or structures are loaded. + aioclient_mock.get(f"{API_URL}/enterprises/{PROJECT_ID}/structures", json={}) + aioclient_mock.get(f"{API_URL}/enterprises/{PROJECT_ID}/devices", json={}) + + # Prepare to capture credentials for Subscriber + captured_creds = None + + async def async_new_subscriber(creds, subscription_name, loop, async_callback): + """Capture credentials for tests.""" + nonlocal captured_creds + captured_creds = creds + return None # GoogleNestSubscriber + + with patch( + "google_nest_sdm.google_nest_subscriber.DefaultSubscriberFactory.async_new_subscriber", + side_effect=async_new_subscriber, + ) as new_subscriber_mock: + await async_setup_sdm(hass) + + calls = aioclient_mock.mock_calls + assert len(calls) == 3 + # Verify refresh token call to get an updated token + (method, url, data, headers) = calls[0] + assert data == { + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET, + "grant_type": "refresh_token", + "refresh_token": FAKE_REFRESH_TOKEN, + } + # Verify API requests are made with the new token + (method, url, data, headers) = calls[1] + assert headers == {"Authorization": f"Bearer {FAKE_UPDATED_TOKEN}"} + (method, url, data, headers) = calls[2] + assert headers == {"Authorization": f"Bearer {FAKE_UPDATED_TOKEN}"} + + # The subscriber is created with a token that is expired. Verify that the + # credential is expired so the subscriber knows it needs to refresh it. + assert len(new_subscriber_mock.mock_calls) == 1 + assert captured_creds + creds = captured_creds + assert creds.token == FAKE_TOKEN + assert creds.refresh_token == FAKE_REFRESH_TOKEN + assert int(dt.as_timestamp(creds.expiry)) == int(expiration_time) + assert not creds.valid + assert creds.expired + assert creds.token_uri == OAUTH2_TOKEN + assert creds.client_id == CLIENT_ID + assert creds.client_secret == CLIENT_SECRET + assert creds.scopes == SDM_SCOPES diff --git a/tests/components/nest/test_init_sdm.py b/tests/components/nest/test_init_sdm.py index ee2a7f4f242..27bc02e3ea8 100644 --- a/tests/components/nest/test_init_sdm.py +++ b/tests/components/nest/test_init_sdm.py @@ -19,9 +19,7 @@ from homeassistant.config_entries import ( ) from homeassistant.setup import async_setup_component -from .common import CONFIG, CONFIG_ENTRY_DATA, async_setup_sdm_platform - -from tests.common import MockConfigEntry +from .common import CONFIG, async_setup_sdm_platform, create_config_entry PLATFORM = "sensor" @@ -39,7 +37,7 @@ async def test_setup_success(hass, caplog): async def async_setup_sdm(hass, config=CONFIG): """Prepare test setup.""" - MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA).add_to_hass(hass) + create_config_entry(hass) with patch( "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation" ): From a6e474c7c9027568ab21d0b82bfbc0b964efd69e Mon Sep 17 00:00:00 2001 From: Will Hughes Date: Sat, 2 Jan 2021 15:52:33 +1300 Subject: [PATCH 057/507] Update surepy to v0.4.0 (#44556) * Update surepy to v0.4.0 * Clarify pylint disable Co-authored-by: Martin Hjelmare --- .../components/surepetcare/__init__.py | 32 +++++++++++-------- .../components/surepetcare/binary_sensor.py | 20 ++++++------ .../components/surepetcare/manifest.json | 2 +- .../components/surepetcare/sensor.py | 12 +++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/surepetcare/conftest.py | 3 +- .../surepetcare/test_binary_sensor.py | 5 +-- 8 files changed, 41 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/surepetcare/__init__.py b/homeassistant/components/surepetcare/__init__.py index 9aac8f11941..bd951ab2641 100644 --- a/homeassistant/components/surepetcare/__init__.py +++ b/homeassistant/components/surepetcare/__init__.py @@ -3,10 +3,11 @@ import logging from typing import Any, Dict, List from surepy import ( + MESTART_RESOURCE, SurePetcare, SurePetcareAuthenticationError, SurePetcareError, - SureProductID, + SurepyProduct, ) import voluptuous as vol @@ -81,7 +82,7 @@ async def async_setup(hass, config) -> bool: async_get_clientsession(hass), api_timeout=SURE_API_TIMEOUT, ) - await surepy.get_data() + except SurePetcareAuthenticationError: _LOGGER.error("Unable to connect to surepetcare.io: Wrong credentials!") return False @@ -91,14 +92,14 @@ async def async_setup(hass, config) -> bool: # add feeders things = [ - {CONF_ID: feeder, CONF_TYPE: SureProductID.FEEDER} + {CONF_ID: feeder, CONF_TYPE: SurepyProduct.FEEDER} for feeder in conf[CONF_FEEDERS] ] # add flaps (don't differentiate between CAT and PET for now) things.extend( [ - {CONF_ID: flap, CONF_TYPE: SureProductID.PET_FLAP} + {CONF_ID: flap, CONF_TYPE: SurepyProduct.PET_FLAP} for flap in conf[CONF_FLAPS] ] ) @@ -109,20 +110,20 @@ async def async_setup(hass, config) -> bool: device_data = await surepy.device(device[CONF_ID]) if ( CONF_PARENT in device_data - and device_data[CONF_PARENT][CONF_PRODUCT_ID] == SureProductID.HUB + and device_data[CONF_PARENT][CONF_PRODUCT_ID] == SurepyProduct.HUB and device_data[CONF_PARENT][CONF_ID] not in hub_ids ): things.append( { CONF_ID: device_data[CONF_PARENT][CONF_ID], - CONF_TYPE: SureProductID.HUB, + CONF_TYPE: SurepyProduct.HUB, } ) hub_ids.add(device_data[CONF_PARENT][CONF_ID]) # add pets things.extend( - [{CONF_ID: pet, CONF_TYPE: SureProductID.PET} for pet in conf[CONF_PETS]] + [{CONF_ID: pet, CONF_TYPE: SurepyProduct.PET} for pet in conf[CONF_PETS]] ) _LOGGER.debug("Devices and Pets to setup: %s", things) @@ -158,8 +159,11 @@ class SurePetcareAPI: async def async_update(self, arg: Any = None) -> None: """Refresh Sure Petcare data.""" - await self.surepy.get_data() - + # Fetch all data from SurePet API, refreshing the surepy cache + # TODO: get surepy upstream to add a method to clear the cache explicitly pylint: disable=fixme + await self.surepy._get_resource( # pylint: disable=protected-access + resource=MESTART_RESOURCE + ) for thing in self.ids: sure_id = thing[CONF_ID] sure_type = thing[CONF_TYPE] @@ -168,13 +172,13 @@ class SurePetcareAPI: type_state = self.states.setdefault(sure_type, {}) if sure_type in [ - SureProductID.CAT_FLAP, - SureProductID.PET_FLAP, - SureProductID.FEEDER, - SureProductID.HUB, + SurepyProduct.CAT_FLAP, + SurepyProduct.PET_FLAP, + SurepyProduct.FEEDER, + SurepyProduct.HUB, ]: type_state[sure_id] = await self.surepy.device(sure_id) - elif sure_type == SureProductID.PET: + elif sure_type == SurepyProduct.PET: type_state[sure_id] = await self.surepy.pet(sure_id) except SurePetcareError as error: diff --git a/homeassistant/components/surepetcare/binary_sensor.py b/homeassistant/components/surepetcare/binary_sensor.py index 0cb96731058..2a624b580ac 100644 --- a/homeassistant/components/surepetcare/binary_sensor.py +++ b/homeassistant/components/surepetcare/binary_sensor.py @@ -3,7 +3,7 @@ from datetime import datetime import logging from typing import Any, Dict, Optional -from surepy import SureLocationID, SureProductID +from surepy import SureLocationID, SurepyProduct from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, @@ -37,15 +37,15 @@ async def async_setup_platform( # connectivity if sure_type in [ - SureProductID.CAT_FLAP, - SureProductID.PET_FLAP, - SureProductID.FEEDER, + SurepyProduct.CAT_FLAP, + SurepyProduct.PET_FLAP, + SurepyProduct.FEEDER, ]: entities.append(DeviceConnectivity(sure_id, sure_type, spc)) - if sure_type == SureProductID.PET: + if sure_type == SurepyProduct.PET: entity = Pet(sure_id, spc) - elif sure_type == SureProductID.HUB: + elif sure_type == SurepyProduct.HUB: entity = Hub(sure_id, spc) else: continue @@ -63,7 +63,7 @@ class SurePetcareBinarySensor(BinarySensorEntity): _id: int, spc: SurePetcareAPI, device_class: str, - sure_type: SureProductID, + sure_type: SurepyProduct, ): """Initialize a Sure Petcare binary sensor.""" self._id = _id @@ -138,7 +138,7 @@ class Hub(SurePetcareBinarySensor): def __init__(self, _id: int, spc: SurePetcareAPI) -> None: """Initialize a Sure Petcare Hub.""" - super().__init__(_id, spc, DEVICE_CLASS_CONNECTIVITY, SureProductID.HUB) + super().__init__(_id, spc, DEVICE_CLASS_CONNECTIVITY, SurepyProduct.HUB) @property def available(self) -> bool: @@ -168,7 +168,7 @@ class Pet(SurePetcareBinarySensor): def __init__(self, _id: int, spc: SurePetcareAPI) -> None: """Initialize a Sure Petcare Pet.""" - super().__init__(_id, spc, DEVICE_CLASS_PRESENCE, SureProductID.PET) + super().__init__(_id, spc, DEVICE_CLASS_PRESENCE, SurepyProduct.PET) @property def is_on(self) -> bool: @@ -205,7 +205,7 @@ class DeviceConnectivity(SurePetcareBinarySensor): def __init__( self, _id: int, - sure_type: SureProductID, + sure_type: SurepyProduct, spc: SurePetcareAPI, ) -> None: """Initialize a Sure Petcare Device.""" diff --git a/homeassistant/components/surepetcare/manifest.json b/homeassistant/components/surepetcare/manifest.json index 2fbe4fe245f..99b52a68c8d 100644 --- a/homeassistant/components/surepetcare/manifest.json +++ b/homeassistant/components/surepetcare/manifest.json @@ -3,5 +3,5 @@ "name": "Sure Petcare", "documentation": "https://www.home-assistant.io/integrations/surepetcare", "codeowners": ["@benleb"], - "requirements": ["surepy==0.2.6"] + "requirements": ["surepy==0.4.0"] } diff --git a/homeassistant/components/surepetcare/sensor.py b/homeassistant/components/surepetcare/sensor.py index d3fcb41dbf4..e2d3d070867 100644 --- a/homeassistant/components/surepetcare/sensor.py +++ b/homeassistant/components/surepetcare/sensor.py @@ -2,7 +2,7 @@ import logging from typing import Any, Dict, Optional -from surepy import SureLockStateID, SureProductID +from surepy import SureLockStateID, SurepyProduct from homeassistant.const import ( ATTR_VOLTAGE, @@ -40,13 +40,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= sure_type = entity[CONF_TYPE] if sure_type in [ - SureProductID.CAT_FLAP, - SureProductID.PET_FLAP, - SureProductID.FEEDER, + SurepyProduct.CAT_FLAP, + SurepyProduct.PET_FLAP, + SurepyProduct.FEEDER, ]: entities.append(SureBattery(entity[CONF_ID], sure_type, spc)) - if sure_type in [SureProductID.CAT_FLAP, SureProductID.PET_FLAP]: + if sure_type in [SurepyProduct.CAT_FLAP, SurepyProduct.PET_FLAP]: entities.append(Flap(entity[CONF_ID], sure_type, spc)) async_add_entities(entities, True) @@ -55,7 +55,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class SurePetcareSensor(Entity): """A binary sensor implementation for Sure Petcare Entities.""" - def __init__(self, _id: int, sure_type: SureProductID, spc: SurePetcareAPI): + def __init__(self, _id: int, sure_type: SurepyProduct, spc: SurePetcareAPI): """Initialize a Sure Petcare sensor.""" self._id = _id diff --git a/requirements_all.txt b/requirements_all.txt index 4bef886080d..b7d0691b5c4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2127,7 +2127,7 @@ sucks==0.9.4 sunwatcher==0.2.1 # homeassistant.components.surepetcare -surepy==0.2.6 +surepy==0.4.0 # homeassistant.components.swiss_hydrological_data swisshydrodata==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6585308074a..ceec361fa7f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1049,7 +1049,7 @@ stringcase==1.2.0 sunwatcher==0.2.1 # homeassistant.components.surepetcare -surepy==0.2.6 +surepy==0.4.0 # homeassistant.components.synology_dsm synologydsm-api==1.0.1 diff --git a/tests/components/surepetcare/conftest.py b/tests/components/surepetcare/conftest.py index 9bc06a84267..44e2a722406 100644 --- a/tests/components/surepetcare/conftest.py +++ b/tests/components/surepetcare/conftest.py @@ -18,6 +18,5 @@ async def surepetcare(hass): async_get_clientsession(hass), api_timeout=1, ) - instance.get_data = AsyncMock(return_value=None) - + instance._get_resource = AsyncMock(return_value=None) yield mock_surepetcare diff --git a/tests/components/surepetcare/test_binary_sensor.py b/tests/components/surepetcare/test_binary_sensor.py index 9478ca7a1d4..ad723f707f5 100644 --- a/tests/components/surepetcare/test_binary_sensor.py +++ b/tests/components/surepetcare/test_binary_sensor.py @@ -1,4 +1,6 @@ """The tests for the Sure Petcare binary sensor platform.""" +from surepy import MESTART_RESOURCE + from homeassistant.components.surepetcare.const import DOMAIN from homeassistant.setup import async_setup_component @@ -16,8 +18,7 @@ EXPECTED_ENTITY_IDS = { async def test_binary_sensors(hass, surepetcare) -> None: """Test the generation of unique ids.""" instance = surepetcare.return_value - instance.data = MOCK_API_DATA - instance.get_data.return_value = MOCK_API_DATA + instance._resource[MESTART_RESOURCE] = {"data": MOCK_API_DATA} with _patch_sensor_setup(): assert await async_setup_component(hass, DOMAIN, MOCK_CONFIG) From 40cbe597bec0d3daadd9783e01856ece221f3f47 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 2 Jan 2021 12:07:52 +0100 Subject: [PATCH 058/507] Update docker base image 2021.01.0 (#44761) --- build.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.json b/build.json index 49cee1ff280..a7ce097ae84 100644 --- a/build.json +++ b/build.json @@ -1,11 +1,11 @@ { "image": "homeassistant/{arch}-homeassistant", "build_from": { - "aarch64": "homeassistant/aarch64-homeassistant-base:2020.11.2", - "armhf": "homeassistant/armhf-homeassistant-base:2020.11.2", - "armv7": "homeassistant/armv7-homeassistant-base:2020.11.2", - "amd64": "homeassistant/amd64-homeassistant-base:2020.11.2", - "i386": "homeassistant/i386-homeassistant-base:2020.11.2" + "aarch64": "homeassistant/aarch64-homeassistant-base:2021.01.0", + "armhf": "homeassistant/armhf-homeassistant-base:2021.01.0", + "armv7": "homeassistant/armv7-homeassistant-base:2021.01.0", + "amd64": "homeassistant/amd64-homeassistant-base:2021.01.0", + "i386": "homeassistant/i386-homeassistant-base:2021.01.0" }, "labels": { "io.hass.type": "core" From 067f2d0098d13134284e442d75ae5f1015ae044c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Sat, 2 Jan 2021 13:35:59 +0100 Subject: [PATCH 059/507] Add tado zone binary sensors (#44576) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These should be binary sensors. Signed-off-by: Álvaro Fernández Rojas --- .../components/tado/binary_sensor.py | 143 +++++++++++++++++- homeassistant/components/tado/entity.py | 1 + homeassistant/components/tado/sensor.py | 41 +---- tests/components/tado/test_binary_sensor.py | 56 ++++++- tests/components/tado/test_sensor.py | 42 ----- 5 files changed, 197 insertions(+), 86 deletions(-) diff --git a/homeassistant/components/tado/binary_sensor.py b/homeassistant/components/tado/binary_sensor.py index 279633b07b1..bf958cceb5d 100644 --- a/homeassistant/components/tado/binary_sensor.py +++ b/homeassistant/components/tado/binary_sensor.py @@ -4,14 +4,25 @@ import logging from homeassistant.components.binary_sensor import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_WINDOW, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import DATA, DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED, TYPE_BATTERY, TYPE_POWER -from .entity import TadoDeviceEntity +from .const import ( + DATA, + DOMAIN, + SIGNAL_TADO_UPDATE_RECEIVED, + TYPE_AIR_CONDITIONING, + TYPE_BATTERY, + TYPE_HEATING, + TYPE_HOT_WATER, + TYPE_POWER, +) +from .entity import TadoDeviceEntity, TadoZoneEntity _LOGGER = logging.getLogger(__name__) @@ -25,6 +36,23 @@ DEVICE_SENSORS = { ], } +ZONE_SENSORS = { + TYPE_HEATING: [ + "power", + "link", + "overlay", + "early start", + "open window", + ], + TYPE_AIR_CONDITIONING: [ + "power", + "link", + "overlay", + "open window", + ], + TYPE_HOT_WATER: ["power", "link", "overlay"], +} + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities @@ -33,6 +61,7 @@ async def async_setup_entry( tado = hass.data[DOMAIN][entry.entry_id][DATA] devices = tado.devices + zones = tado.zones entities = [] # Create device sensors @@ -44,16 +73,30 @@ async def async_setup_entry( entities.extend( [ - TadoDeviceSensor(tado, device, variable) + TadoDeviceBinarySensor(tado, device, variable) for variable in DEVICE_SENSORS[device_type] ] ) + # Create zone sensors + for zone in zones: + zone_type = zone["type"] + if zone_type not in ZONE_SENSORS: + _LOGGER.warning("Unknown zone type skipped: %s", zone_type) + continue + + entities.extend( + [ + TadoZoneBinarySensor(tado, zone["name"], zone["id"], variable) + for variable in ZONE_SENSORS[zone_type] + ] + ) + if entities: async_add_entities(entities, True) -class TadoDeviceSensor(TadoDeviceEntity, BinarySensorEntity): +class TadoDeviceBinarySensor(TadoDeviceEntity, BinarySensorEntity): """Representation of a tado Sensor.""" def __init__(self, tado, device_info, device_variable): @@ -125,3 +168,95 @@ class TadoDeviceSensor(TadoDeviceEntity, BinarySensorEntity): self._state = self._device_info.get("connectionState", {}).get( "value", False ) + + +class TadoZoneBinarySensor(TadoZoneEntity, BinarySensorEntity): + """Representation of a tado Sensor.""" + + def __init__(self, tado, zone_name, zone_id, zone_variable): + """Initialize of the Tado Sensor.""" + self._tado = tado + super().__init__(zone_name, tado.home_id, zone_id) + + self.zone_variable = zone_variable + + self._unique_id = f"{zone_variable} {zone_id} {tado.home_id}" + + self._state = None + self._tado_zone_data = None + + async def async_added_to_hass(self): + """Register for sensor updates.""" + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_TADO_UPDATE_RECEIVED.format( + self._tado.home_id, "zone", self.zone_id + ), + self._async_update_callback, + ) + ) + self._async_update_zone_data() + + @property + def unique_id(self): + """Return the unique id.""" + return self._unique_id + + @property + def name(self): + """Return the name of the sensor.""" + return f"{self.zone_name} {self.zone_variable}" + + @property + def is_on(self): + """Return true if sensor is on.""" + return self._state + + @property + def device_class(self): + """Return the class of this sensor.""" + if self.zone_variable == "early start": + return DEVICE_CLASS_POWER + if self.zone_variable == "link": + return DEVICE_CLASS_CONNECTIVITY + if self.zone_variable == "open window": + return DEVICE_CLASS_WINDOW + if self.zone_variable == "overlay": + return DEVICE_CLASS_POWER + if self.zone_variable == "power": + return DEVICE_CLASS_POWER + return None + + @callback + def _async_update_callback(self): + """Update and write state.""" + self._async_update_zone_data() + self.async_write_ha_state() + + @callback + def _async_update_zone_data(self): + """Handle update callbacks.""" + try: + self._tado_zone_data = self._tado.data["zone"][self.zone_id] + except KeyError: + return + + if self.zone_variable == "power": + self._state = self._tado_zone_data.power + + elif self.zone_variable == "link": + self._state = self._tado_zone_data.link + + elif self.zone_variable == "overlay": + self._state = self._tado_zone_data.overlay_active + + elif self.zone_variable == "early start": + self._state = self._tado_zone_data.preparation + + elif self.zone_variable == "open window": + self._state = bool( + self._tado_zone_data.open_window + or self._tado_zone_data.open_window_detected + ) diff --git a/homeassistant/components/tado/entity.py b/homeassistant/components/tado/entity.py index 03900fdeeb5..bd0c605b0eb 100644 --- a/homeassistant/components/tado/entity.py +++ b/homeassistant/components/tado/entity.py @@ -40,6 +40,7 @@ class TadoZoneEntity(Entity): super().__init__() self._device_zone_id = f"{home_id}_{zone_id}" self.zone_name = zone_name + self.zone_id = zone_id @property def device_info(self): diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index 4e8f69b17c8..f9a924b52c6 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -23,25 +23,16 @@ ZONE_SENSORS = { TYPE_HEATING: [ "temperature", "humidity", - "power", - "link", "heating", "tado mode", - "overlay", - "early start", - "open window", ], TYPE_AIR_CONDITIONING: [ "temperature", "humidity", - "power", - "link", "ac", "tado mode", - "overlay", - "open window", ], - TYPE_HOT_WATER: ["power", "link", "tado mode", "overlay"], + TYPE_HOT_WATER: ["tado mode"], } @@ -51,10 +42,10 @@ async def async_setup_entry( """Set up the Tado sensor platform.""" tado = hass.data[DOMAIN][entry.entry_id][DATA] - # Create zone sensors zones = tado.zones entities = [] + # Create zone sensors for zone in zones: zone_type = zone["type"] if zone_type not in ZONE_SENSORS: @@ -80,7 +71,6 @@ class TadoZoneSensor(TadoZoneEntity, Entity): self._tado = tado super().__init__(zone_name, tado.home_id, zone_id) - self.zone_id = zone_id self.zone_variable = zone_variable self._unique_id = f"{zone_variable} {zone_id} {tado.home_id}" @@ -172,12 +162,6 @@ class TadoZoneSensor(TadoZoneEntity, Entity): "time": self._tado_zone_data.current_humidity_timestamp } - elif self.zone_variable == "power": - self._state = self._tado_zone_data.power - - elif self.zone_variable == "link": - self._state = self._tado_zone_data.link - elif self.zone_variable == "heating": self._state = self._tado_zone_data.heating_power_percentage self._state_attributes = { @@ -188,26 +172,5 @@ class TadoZoneSensor(TadoZoneEntity, Entity): self._state = self._tado_zone_data.ac_power self._state_attributes = {"time": self._tado_zone_data.ac_power_timestamp} - elif self.zone_variable == "tado bridge status": - self._state = self._tado_zone_data.connection - elif self.zone_variable == "tado mode": self._state = self._tado_zone_data.tado_mode - - elif self.zone_variable == "overlay": - self._state = self._tado_zone_data.overlay_active - self._state_attributes = ( - {"termination": self._tado_zone_data.overlay_termination_type} - if self._tado_zone_data.overlay_active - else {} - ) - - elif self.zone_variable == "early start": - self._state = self._tado_zone_data.preparation - - elif self.zone_variable == "open window": - self._state = bool( - self._tado_zone_data.open_window - or self._tado_zone_data.open_window_detected - ) - self._state_attributes = self._tado_zone_data.open_window_attr diff --git a/tests/components/tado/test_binary_sensor.py b/tests/components/tado/test_binary_sensor.py index 39dd068f5a6..c811314e4f9 100644 --- a/tests/components/tado/test_binary_sensor.py +++ b/tests/components/tado/test_binary_sensor.py @@ -1,10 +1,64 @@ """The sensor tests for the tado platform.""" -from homeassistant.const import STATE_ON +from homeassistant.const import STATE_OFF, STATE_ON from .util import async_init_integration +async def test_air_con_create_binary_sensors(hass): + """Test creation of aircon sensors.""" + + await async_init_integration(hass) + + state = hass.states.get("binary_sensor.air_conditioning_power") + assert state.state == STATE_ON + + state = hass.states.get("binary_sensor.air_conditioning_link") + assert state.state == STATE_ON + + state = hass.states.get("binary_sensor.air_conditioning_overlay") + assert state.state == STATE_ON + + state = hass.states.get("binary_sensor.air_conditioning_open_window") + assert state.state == STATE_OFF + + +async def test_heater_create_binary_sensors(hass): + """Test creation of heater sensors.""" + + await async_init_integration(hass) + + state = hass.states.get("binary_sensor.baseboard_heater_power") + assert state.state == STATE_ON + + state = hass.states.get("binary_sensor.baseboard_heater_link") + assert state.state == STATE_ON + + state = hass.states.get("binary_sensor.baseboard_heater_early_start") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.baseboard_heater_overlay") + assert state.state == STATE_ON + + state = hass.states.get("binary_sensor.baseboard_heater_open_window") + assert state.state == STATE_OFF + + +async def test_water_heater_create_binary_sensors(hass): + """Test creation of water heater sensors.""" + + await async_init_integration(hass) + + state = hass.states.get("binary_sensor.water_heater_link") + assert state.state == STATE_ON + + state = hass.states.get("binary_sensor.water_heater_overlay") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.water_heater_power") + assert state.state == STATE_ON + + async def test_home_create_binary_sensors(hass): """Test creation of home binary sensors.""" diff --git a/tests/components/tado/test_sensor.py b/tests/components/tado/test_sensor.py index 646e7741530..2fac88bc22e 100644 --- a/tests/components/tado/test_sensor.py +++ b/tests/components/tado/test_sensor.py @@ -8,15 +8,6 @@ async def test_air_con_create_sensors(hass): await async_init_integration(hass) - state = hass.states.get("sensor.air_conditioning_power") - assert state.state == "ON" - - state = hass.states.get("sensor.air_conditioning_link") - assert state.state == "ONLINE" - - state = hass.states.get("sensor.air_conditioning_link") - assert state.state == "ONLINE" - state = hass.states.get("sensor.air_conditioning_tado_mode") assert state.state == "HOME" @@ -26,48 +17,24 @@ async def test_air_con_create_sensors(hass): state = hass.states.get("sensor.air_conditioning_ac") assert state.state == "ON" - state = hass.states.get("sensor.air_conditioning_overlay") - assert state.state == "True" - state = hass.states.get("sensor.air_conditioning_humidity") assert state.state == "60.9" - state = hass.states.get("sensor.air_conditioning_open_window") - assert state.state == "False" - async def test_heater_create_sensors(hass): """Test creation of heater sensors.""" await async_init_integration(hass) - state = hass.states.get("sensor.baseboard_heater_power") - assert state.state == "ON" - - state = hass.states.get("sensor.baseboard_heater_link") - assert state.state == "ONLINE" - - state = hass.states.get("sensor.baseboard_heater_link") - assert state.state == "ONLINE" - state = hass.states.get("sensor.baseboard_heater_tado_mode") assert state.state == "HOME" state = hass.states.get("sensor.baseboard_heater_temperature") assert state.state == "20.65" - state = hass.states.get("sensor.baseboard_heater_early_start") - assert state.state == "False" - - state = hass.states.get("sensor.baseboard_heater_overlay") - assert state.state == "True" - state = hass.states.get("sensor.baseboard_heater_humidity") assert state.state == "45.2" - state = hass.states.get("sensor.baseboard_heater_open_window") - assert state.state == "False" - async def test_water_heater_create_sensors(hass): """Test creation of water heater sensors.""" @@ -76,12 +43,3 @@ async def test_water_heater_create_sensors(hass): state = hass.states.get("sensor.water_heater_tado_mode") assert state.state == "HOME" - - state = hass.states.get("sensor.water_heater_link") - assert state.state == "ONLINE" - - state = hass.states.get("sensor.water_heater_overlay") - assert state.state == "False" - - state = hass.states.get("sensor.water_heater_power") - assert state.state == "ON" From 3de0610909e97f92a09a9efae117e0fabfeb0067 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 2 Jan 2021 23:01:20 +0100 Subject: [PATCH 060/507] Support google assistant stopping for assumed state covers (#44266) * Support stopping for assumed state covers * Adjust black formatting --- .../components/google_assistant/trait.py | 9 +++++++- .../components/google_assistant/test_trait.py | 22 +++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 8790c3c7402..b5dc2afd3e2 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -648,7 +648,14 @@ class StartStopTrait(_Trait): """Execute a StartStop command.""" if command == COMMAND_STARTSTOP: if params["start"] is False: - if self.state.state in (cover.STATE_CLOSING, cover.STATE_OPENING): + if ( + self.state.state + in ( + cover.STATE_CLOSING, + cover.STATE_OPENING, + ) + or self.state.attributes.get(ATTR_ASSUMED_STATE) + ): await self.hass.services.async_call( self.state.domain, cover.SERVICE_STOP_COVER, diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index b7034f927b4..9b573f1cf71 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -384,8 +384,8 @@ 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.""" +async def test_startstop_cover(hass): + """Test startStop trait support for cover domain.""" assert helpers.get_google_type(cover.DOMAIN, None) is not None assert trait.StartStopTrait.supported(cover.DOMAIN, cover.SUPPORT_STOP, None) @@ -429,6 +429,24 @@ async def test_startstop_covert(hass): await trt.execute(trait.COMMAND_PAUSEUNPAUSE, BASIC_DATA, {"start": True}, {}) +async def test_startstop_cover_assumed(hass): + """Test startStop trait support for cover domain of assumed state.""" + trt = trait.StartStopTrait( + hass, + State( + "cover.bla", + cover.STATE_CLOSED, + {ATTR_SUPPORTED_FEATURES: cover.SUPPORT_STOP, ATTR_ASSUMED_STATE: True}, + ), + BASIC_CONFIG, + ) + + 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"} + + 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 From cc21639f00a2cc2452883d097ed277d5411b82a6 Mon Sep 17 00:00:00 2001 From: badguy99 <61918526+badguy99@users.noreply.github.com> Date: Sun, 3 Jan 2021 02:38:45 +0000 Subject: [PATCH 061/507] Fix Soma integration reload (#44750) * fix async_unload_entry so that component reload works from GUI * update to use asyncio based on review feedback --- homeassistant/components/soma/__init__.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/soma/__init__.py b/homeassistant/components/soma/__init__.py index 3439684f977..d4dbbced453 100644 --- a/homeassistant/components/soma/__init__.py +++ b/homeassistant/components/soma/__init__.py @@ -1,4 +1,5 @@ """Support for Soma Smartshades.""" +import asyncio import logging from api.soma_api import SomaApi @@ -63,7 +64,16 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): """Unload a config entry.""" - return True + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in SOMA_COMPONENTS + ] + ) + ) + + return unload_ok class SomaEntity(Entity): From 2ed7b90027cb28b5f78f40257845475cc546da9c Mon Sep 17 00:00:00 2001 From: dzukero <47693760+dzukero@users.noreply.github.com> Date: Sun, 3 Jan 2021 13:59:22 +0200 Subject: [PATCH 062/507] Add RFXtrx Rfy venetian blinds tilt control (#44309) * Add tilt control for RFXtrx Rfy venetian blinds * Update Rfy cover test * Update the required version of pyRFXtrx * Update required pyRFXtrx version to 0.26.1 * Revert "Update required pyRFXtrx version to 0.26.1" This reverts commit d54f1645d5ec8bb67f64b0f9b1a1bb498f275739. * Revert "Update the required version of pyRFXtrx" This reverts commit ac36d6532623ee75347797f1c3ac0cc52aaa05fd. * Update required version of pyRFXtrx to 0.26.1 * @dzukero Update required version of pyRFXtrx to 0.26.1 * Make requested changes from review * Fix isort * Remove set tilt position support * Remove set tilt position support per review --- .../components/rfxtrx/config_flow.py | 25 +++ homeassistant/components/rfxtrx/const.py | 5 + homeassistant/components/rfxtrx/cover.py | 84 ++++++++- homeassistant/components/rfxtrx/manifest.json | 2 +- homeassistant/components/rfxtrx/strings.json | 1 + .../components/rfxtrx/translations/en.json | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/rfxtrx/test_config_flow.py | 84 +++++++++ tests/components/rfxtrx/test_cover.py | 177 ++++++++++++++++++ 10 files changed, 375 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py index f81fdde3c4c..5eeb9b38411 100644 --- a/homeassistant/components/rfxtrx/config_flow.py +++ b/homeassistant/components/rfxtrx/config_flow.py @@ -40,6 +40,10 @@ from .const import ( CONF_REMOVE_DEVICE, CONF_REPLACE_DEVICE, CONF_SIGNAL_REPETITIONS, + CONF_VENETIAN_BLIND_MODE, + CONST_VENETIAN_BLIND_MODE_DEFAULT, + CONST_VENETIAN_BLIND_MODE_EU, + CONST_VENETIAN_BLIND_MODE_US, DEVICE_PACKET_TYPE_LIGHTING4, ) from .cover import supported as cover_supported @@ -218,6 +222,10 @@ class OptionsFlow(config_entries.OptionsFlow): device[CONF_COMMAND_ON] = command_on if command_off: device[CONF_COMMAND_OFF] = command_off + if user_input.get(CONF_VENETIAN_BLIND_MODE): + device[CONF_VENETIAN_BLIND_MODE] = user_input[ + CONF_VENETIAN_BLIND_MODE + ] self.update_config_data( global_options=self._global_options, devices=devices @@ -282,6 +290,23 @@ class OptionsFlow(config_entries.OptionsFlow): } ) + if isinstance(self._selected_device_object.device, rfxtrxmod.RfyDevice): + data_schema.update( + { + vol.Optional( + CONF_VENETIAN_BLIND_MODE, + default=device_data.get( + CONF_VENETIAN_BLIND_MODE, CONST_VENETIAN_BLIND_MODE_DEFAULT + ), + ): vol.In( + [ + CONST_VENETIAN_BLIND_MODE_DEFAULT, + CONST_VENETIAN_BLIND_MODE_US, + CONST_VENETIAN_BLIND_MODE_EU, + ] + ), + } + ) devices = { entry.id: entry.name_by_user if entry.name_by_user else entry.name for entry in self._device_entries diff --git a/homeassistant/components/rfxtrx/const.py b/homeassistant/components/rfxtrx/const.py index 28aec125644..1f36b00e184 100644 --- a/homeassistant/components/rfxtrx/const.py +++ b/homeassistant/components/rfxtrx/const.py @@ -6,10 +6,15 @@ CONF_AUTOMATIC_ADD = "automatic_add" CONF_SIGNAL_REPETITIONS = "signal_repetitions" CONF_DEBUG = "debug" CONF_OFF_DELAY = "off_delay" +CONF_VENETIAN_BLIND_MODE = "venetian_blind_mode" CONF_REMOVE_DEVICE = "remove_device" CONF_REPLACE_DEVICE = "replace_device" +CONST_VENETIAN_BLIND_MODE_DEFAULT = "Unknown" +CONST_VENETIAN_BLIND_MODE_EU = "EU" +CONST_VENETIAN_BLIND_MODE_US = "US" + COMMAND_ON_LIST = [ "On", "Up", diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py index dfbaa60f589..a5f5edd0e42 100644 --- a/homeassistant/components/rfxtrx/cover.py +++ b/homeassistant/components/rfxtrx/cover.py @@ -1,7 +1,15 @@ """Support for RFXtrx covers.""" import logging -from homeassistant.components.cover import CoverEntity +from homeassistant.components.cover import ( + SUPPORT_CLOSE, + SUPPORT_CLOSE_TILT, + SUPPORT_OPEN, + SUPPORT_OPEN_TILT, + SUPPORT_STOP, + SUPPORT_STOP_TILT, + CoverEntity, +) from homeassistant.const import CONF_DEVICES, STATE_OPEN from homeassistant.core import callback @@ -14,7 +22,13 @@ from . import ( get_device_id, get_rfx_object, ) -from .const import COMMAND_OFF_LIST, COMMAND_ON_LIST +from .const import ( + COMMAND_OFF_LIST, + COMMAND_ON_LIST, + CONF_VENETIAN_BLIND_MODE, + CONST_VENETIAN_BLIND_MODE_EU, + CONST_VENETIAN_BLIND_MODE_US, +) _LOGGER = logging.getLogger(__name__) @@ -50,7 +64,10 @@ async def async_setup_entry( device_ids.add(device_id) entity = RfxtrxCover( - event.device, device_id, entity_info[CONF_SIGNAL_REPETITIONS] + event.device, + device_id, + signal_repetitions=entity_info[CONF_SIGNAL_REPETITIONS], + venetian_blind_mode=entity_info.get(CONF_VENETIAN_BLIND_MODE), ) entities.append(entity) @@ -86,6 +103,18 @@ async def async_setup_entry( class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): """Representation of a RFXtrx cover.""" + def __init__( + self, + device, + device_id, + signal_repetitions, + event=None, + venetian_blind_mode=None, + ): + """Initialize the RFXtrx cover device.""" + super().__init__(device, device_id, signal_repetitions, event) + self._venetian_blind_mode = venetian_blind_mode + async def async_added_to_hass(self): """Restore device state.""" await super().async_added_to_hass() @@ -95,6 +124,21 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): if old_state is not None: self._state = old_state.state == STATE_OPEN + @property + def supported_features(self): + """Flag supported features.""" + supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP + + if self._venetian_blind_mode in ( + CONST_VENETIAN_BLIND_MODE_US, + CONST_VENETIAN_BLIND_MODE_EU, + ): + supported_features |= ( + SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_STOP_TILT + ) + + return supported_features + @property def is_closed(self): """Return if the cover is closed.""" @@ -102,13 +146,23 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): async def async_open_cover(self, **kwargs): """Move the cover up.""" - await self._async_send(self._device.send_open) + if self._venetian_blind_mode == CONST_VENETIAN_BLIND_MODE_US: + await self._async_send(self._device.send_up05sec) + elif self._venetian_blind_mode == CONST_VENETIAN_BLIND_MODE_EU: + await self._async_send(self._device.send_up2sec) + else: + await self._async_send(self._device.send_open) self._state = True self.async_write_ha_state() async def async_close_cover(self, **kwargs): """Move the cover down.""" - await self._async_send(self._device.send_close) + if self._venetian_blind_mode == CONST_VENETIAN_BLIND_MODE_US: + await self._async_send(self._device.send_down05sec) + elif self._venetian_blind_mode == CONST_VENETIAN_BLIND_MODE_EU: + await self._async_send(self._device.send_down2sec) + else: + await self._async_send(self._device.send_close) self._state = False self.async_write_ha_state() @@ -118,6 +172,26 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): self._state = True self.async_write_ha_state() + async def async_open_cover_tilt(self, **kwargs): + """Tilt the cover up.""" + if self._venetian_blind_mode == CONST_VENETIAN_BLIND_MODE_US: + await self._async_send(self._device.send_up2sec) + elif self._venetian_blind_mode == CONST_VENETIAN_BLIND_MODE_EU: + await self._async_send(self._device.send_up05sec) + + async def async_close_cover_tilt(self, **kwargs): + """Tilt the cover down.""" + if self._venetian_blind_mode == CONST_VENETIAN_BLIND_MODE_US: + await self._async_send(self._device.send_down2sec) + elif self._venetian_blind_mode == CONST_VENETIAN_BLIND_MODE_EU: + await self._async_send(self._device.send_down05sec) + + async def async_stop_cover_tilt(self, **kwargs): + """Stop the cover tilt.""" + await self._async_send(self._device.send_stop) + self._state = True + self.async_write_ha_state() + def _apply_event(self, event): """Apply command from rfxtrx.""" super()._apply_event(event) diff --git a/homeassistant/components/rfxtrx/manifest.json b/homeassistant/components/rfxtrx/manifest.json index e62fc5c3c83..19e834d11d6 100644 --- a/homeassistant/components/rfxtrx/manifest.json +++ b/homeassistant/components/rfxtrx/manifest.json @@ -2,7 +2,7 @@ "domain": "rfxtrx", "name": "RFXCOM RFXtrx", "documentation": "https://www.home-assistant.io/integrations/rfxtrx", - "requirements": ["pyRFXtrx==0.26"], + "requirements": ["pyRFXtrx==0.26.1"], "codeowners": ["@danielhiversen", "@elupus", "@RobBie1221"], "config_flow": true } diff --git a/homeassistant/components/rfxtrx/strings.json b/homeassistant/components/rfxtrx/strings.json index 574cff29ba1..c89fcddb002 100644 --- a/homeassistant/components/rfxtrx/strings.json +++ b/homeassistant/components/rfxtrx/strings.json @@ -56,6 +56,7 @@ "command_on": "Data bits value for command on", "command_off": "Data bits value for command off", "signal_repetitions": "Number of signal repetitions", + "venetian_blind_mode": "Venetian blind mode", "replace_device": "Select device to replace" }, "title": "Configure device options" diff --git a/homeassistant/components/rfxtrx/translations/en.json b/homeassistant/components/rfxtrx/translations/en.json index 3f8fcf12702..2d73ac56810 100644 --- a/homeassistant/components/rfxtrx/translations/en.json +++ b/homeassistant/components/rfxtrx/translations/en.json @@ -64,6 +64,7 @@ "off_delay": "Off delay", "off_delay_enabled": "Enable off delay", "replace_device": "Select device to replace", + "venetian_blind_mode": "Venetian blind mode (tilt by: US - long press, EU - short press)", "signal_repetitions": "Number of signal repetitions" }, "title": "Configure device options" diff --git a/requirements_all.txt b/requirements_all.txt index b7d0691b5c4..830a5e4f456 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1229,7 +1229,7 @@ pyHS100==0.3.5.2 pyMetno==0.8.1 # homeassistant.components.rfxtrx -pyRFXtrx==0.26 +pyRFXtrx==0.26.1 # homeassistant.components.switchmate # pySwitchmate==0.4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ceec361fa7f..b5b906a9fb6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -619,7 +619,7 @@ pyHS100==0.3.5.2 pyMetno==0.8.1 # homeassistant.components.rfxtrx -pyRFXtrx==0.26 +pyRFXtrx==0.26.1 # homeassistant.components.tibber pyTibber==0.16.0 diff --git a/tests/components/rfxtrx/test_config_flow.py b/tests/components/rfxtrx/test_config_flow.py index 5d4f5edaf2a..e39c766bfd2 100644 --- a/tests/components/rfxtrx/test_config_flow.py +++ b/tests/components/rfxtrx/test_config_flow.py @@ -1103,6 +1103,90 @@ async def test_options_add_and_configure_device(hass): assert "delay_off" not in entry.data["devices"]["0913000022670e013970"] +async def test_options_configure_rfy_cover_device(hass): + """Test we can configure the venetion blind mode of an Rfy cover.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + "host": None, + "port": None, + "device": "/dev/tty123", + "automatic_add": False, + "devices": {}, + }, + unique_id=DOMAIN, + ) + entry.add_to_hass(hass) + + 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) + + assert result["type"] == "form" + assert result["step_id"] == "prompt_options" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "automatic_add": True, + "event_code": "071a000001020301", + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "set_device_options" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "fire_event": False, + "venetian_blind_mode": "EU", + }, + ) + + await hass.async_block_till_done() + + assert entry.data["devices"]["071a000001020301"]["venetian_blind_mode"] == "EU" + + device_registry = await async_get_device_registry(hass) + device_entries = async_entries_for_config_entry(device_registry, entry.entry_id) + + assert device_entries[0].id + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == "form" + assert result["step_id"] == "prompt_options" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "automatic_add": False, + "device": device_entries[0].id, + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "set_device_options" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "fire_event": False, + "venetian_blind_mode": "EU", + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + await hass.async_block_till_done() + + assert entry.data["devices"]["071a000001020301"]["venetian_blind_mode"] == "EU" + + def test_get_serial_by_id_no_dir(): """Test serial by id conversion if there's no /dev/serial/by-id.""" p1 = patch("os.path.isdir", MagicMock(return_value=False)) diff --git a/tests/components/rfxtrx/test_cover.py b/tests/components/rfxtrx/test_cover.py index b3e5ce224c6..fe7f49d728b 100644 --- a/tests/components/rfxtrx/test_cover.py +++ b/tests/components/rfxtrx/test_cover.py @@ -140,3 +140,180 @@ async def test_duplicate_cover(hass, rfxtrx): assert state assert state.state == "closed" assert state.attributes.get("friendly_name") == "LightwaveRF, Siemens 0213c7:242" + + +async def test_rfy_cover(hass, rfxtrx): + """Test Rfy venetian blind covers.""" + entry_data = create_rfx_test_cfg( + devices={ + "071a000001020301": { + "signal_repetitions": 1, + "venetian_blind_mode": "Unknown", + }, + "071a000001020302": {"signal_repetitions": 1, "venetian_blind_mode": "US"}, + "071a000001020303": {"signal_repetitions": 1, "venetian_blind_mode": "EU"}, + } + ) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + # Test a blind with no venetian mode setting + state = hass.states.get("cover.rfy_010203_1") + assert state + + await hass.services.async_call( + "cover", + "stop_cover", + {"entity_id": "cover.rfy_010203_1"}, + blocking=True, + ) + + await hass.services.async_call( + "cover", + "open_cover", + {"entity_id": "cover.rfy_010203_1"}, + blocking=True, + ) + + await hass.services.async_call( + "cover", + "close_cover", + {"entity_id": "cover.rfy_010203_1"}, + blocking=True, + ) + + await hass.services.async_call( + "cover", + "open_cover_tilt", + {"entity_id": "cover.rfy_010203_1"}, + blocking=True, + ) + + await hass.services.async_call( + "cover", + "close_cover_tilt", + {"entity_id": "cover.rfy_010203_1"}, + blocking=True, + ) + + assert rfxtrx.transport.send.mock_calls == [ + call(bytearray(b"\x08\x1a\x00\x00\x01\x02\x03\x01\x00")), + call(bytearray(b"\x08\x1a\x00\x01\x01\x02\x03\x01\x01")), + call(bytearray(b"\x08\x1a\x00\x02\x01\x02\x03\x01\x03")), + ] + + # Test a blind with venetian mode set to US + state = hass.states.get("cover.rfy_010203_2") + assert state + rfxtrx.transport.send.mock_calls = [] + + await hass.services.async_call( + "cover", + "stop_cover", + {"entity_id": "cover.rfy_010203_2"}, + blocking=True, + ) + + await hass.services.async_call( + "cover", + "open_cover", + {"entity_id": "cover.rfy_010203_2"}, + blocking=True, + ) + + await hass.services.async_call( + "cover", + "close_cover", + {"entity_id": "cover.rfy_010203_2"}, + blocking=True, + ) + + await hass.services.async_call( + "cover", + "open_cover_tilt", + {"entity_id": "cover.rfy_010203_2"}, + blocking=True, + ) + + await hass.services.async_call( + "cover", + "close_cover_tilt", + {"entity_id": "cover.rfy_010203_2"}, + blocking=True, + ) + + await hass.services.async_call( + "cover", + "stop_cover_tilt", + {"entity_id": "cover.rfy_010203_2"}, + blocking=True, + ) + + assert rfxtrx.transport.send.mock_calls == [ + call(bytearray(b"\x08\x1a\x00\x00\x01\x02\x03\x02\x00")), + call(bytearray(b"\x08\x1a\x00\x01\x01\x02\x03\x02\x0F")), + call(bytearray(b"\x08\x1a\x00\x02\x01\x02\x03\x02\x10")), + call(bytearray(b"\x08\x1a\x00\x03\x01\x02\x03\x02\x11")), + call(bytearray(b"\x08\x1a\x00\x04\x01\x02\x03\x02\x12")), + call(bytearray(b"\x08\x1a\x00\x00\x01\x02\x03\x02\x00")), + ] + + # Test a blind with venetian mode set to EU + state = hass.states.get("cover.rfy_010203_3") + assert state + rfxtrx.transport.send.mock_calls = [] + + await hass.services.async_call( + "cover", + "stop_cover", + {"entity_id": "cover.rfy_010203_3"}, + blocking=True, + ) + + await hass.services.async_call( + "cover", + "open_cover", + {"entity_id": "cover.rfy_010203_3"}, + blocking=True, + ) + + await hass.services.async_call( + "cover", + "close_cover", + {"entity_id": "cover.rfy_010203_3"}, + blocking=True, + ) + + await hass.services.async_call( + "cover", + "open_cover_tilt", + {"entity_id": "cover.rfy_010203_3"}, + blocking=True, + ) + + await hass.services.async_call( + "cover", + "close_cover_tilt", + {"entity_id": "cover.rfy_010203_3"}, + blocking=True, + ) + + await hass.services.async_call( + "cover", + "stop_cover_tilt", + {"entity_id": "cover.rfy_010203_3"}, + blocking=True, + ) + + assert rfxtrx.transport.send.mock_calls == [ + call(bytearray(b"\x08\x1a\x00\x00\x01\x02\x03\x03\x00")), + call(bytearray(b"\x08\x1a\x00\x01\x01\x02\x03\x03\x11")), + call(bytearray(b"\x08\x1a\x00\x02\x01\x02\x03\x03\x12")), + call(bytearray(b"\x08\x1a\x00\x03\x01\x02\x03\x03\x0F")), + call(bytearray(b"\x08\x1a\x00\x04\x01\x02\x03\x03\x10")), + call(bytearray(b"\x08\x1a\x00\x00\x01\x02\x03\x03\x00")), + ] From 1fc4284a29602168708036c69d8d9fdea25bbc65 Mon Sep 17 00:00:00 2001 From: Will Hughes Date: Mon, 4 Jan 2021 01:43:16 +1300 Subject: [PATCH 063/507] Add service to lock/unlock Sure Petcare pet flaps (#44557) * Add service to lock/unlock Sure Petcare pet flaps Adds a service to the Sure Petcare pet flaps to allow lock, unlocking, locking in and locking out pets using the pet flap * Linting * Changes from code review --- .../components/surepetcare/__init__.py | 47 +++++++++++++++++++ homeassistant/components/surepetcare/const.py | 5 ++ .../components/surepetcare/services.yaml | 9 ++++ 3 files changed, 61 insertions(+) create mode 100644 homeassistant/components/surepetcare/services.yaml diff --git a/homeassistant/components/surepetcare/__init__.py b/homeassistant/components/surepetcare/__init__.py index bd951ab2641..8ba6809ee05 100644 --- a/homeassistant/components/surepetcare/__init__.py +++ b/homeassistant/components/surepetcare/__init__.py @@ -4,6 +4,7 @@ from typing import Any, Dict, List from surepy import ( MESTART_RESOURCE, + SureLockStateID, SurePetcare, SurePetcareAuthenticationError, SurePetcareError, @@ -24,6 +25,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval from .const import ( + ATTR_FLAP_ID, + ATTR_LOCK_STATE, CONF_FEEDERS, CONF_FLAPS, CONF_PARENT, @@ -32,6 +35,7 @@ from .const import ( DATA_SURE_PETCARE, DEFAULT_SCAN_INTERVAL, DOMAIN, + SERVICE_SET_LOCK_STATE, SPC, SURE_API_TIMEOUT, TOPIC_UPDATE, @@ -143,6 +147,38 @@ async def async_setup(hass, config) -> bool: hass.helpers.discovery.async_load_platform("sensor", DOMAIN, {}, config) ) + async def handle_set_lock_state(call): + """Call when setting the lock state.""" + await spc.set_lock_state(call.data[ATTR_FLAP_ID], call.data[ATTR_LOCK_STATE]) + await spc.async_update() + + lock_state_service_schema = vol.Schema( + { + vol.Required(ATTR_FLAP_ID): vol.All( + cv.positive_int, vol.In(conf[CONF_FLAPS]) + ), + vol.Required(ATTR_LOCK_STATE): vol.All( + cv.string, + vol.Lower, + vol.In( + [ + SureLockStateID.UNLOCKED.name.lower(), + SureLockStateID.LOCKED_IN.name.lower(), + SureLockStateID.LOCKED_OUT.name.lower(), + SureLockStateID.LOCKED_ALL.name.lower(), + ] + ), + ), + } + ) + + hass.services.async_register( + DOMAIN, + SERVICE_SET_LOCK_STATE, + handle_set_lock_state, + schema=lock_state_service_schema, + ) + return True @@ -185,3 +221,14 @@ class SurePetcareAPI: _LOGGER.error("Unable to retrieve data from surepetcare.io: %s", error) async_dispatcher_send(self.hass, TOPIC_UPDATE) + + async def set_lock_state(self, flap_id: int, state: str) -> None: + """Update the lock state of a flap.""" + if state == SureLockStateID.UNLOCKED.name.lower(): + await self.surepy.unlock(flap_id) + elif state == SureLockStateID.LOCKED_IN.name.lower(): + await self.surepy.lock_in(flap_id) + elif state == SureLockStateID.LOCKED_OUT.name.lower(): + await self.surepy.lock_out(flap_id) + elif state == SureLockStateID.LOCKED_ALL.name.lower(): + await self.surepy.lock(flap_id) diff --git a/homeassistant/components/surepetcare/const.py b/homeassistant/components/surepetcare/const.py index 165e0bfd98a..86215c12ade 100644 --- a/homeassistant/components/surepetcare/const.py +++ b/homeassistant/components/surepetcare/const.py @@ -31,3 +31,8 @@ BATTERY_ICON = "mdi:battery" SURE_BATT_VOLTAGE_FULL = 1.6 # voltage SURE_BATT_VOLTAGE_LOW = 1.25 # voltage SURE_BATT_VOLTAGE_DIFF = SURE_BATT_VOLTAGE_FULL - SURE_BATT_VOLTAGE_LOW + +# lock state service +SERVICE_SET_LOCK_STATE = "set_lock_state" +ATTR_FLAP_ID = "flap_id" +ATTR_LOCK_STATE = "lock_state" diff --git a/homeassistant/components/surepetcare/services.yaml b/homeassistant/components/surepetcare/services.yaml new file mode 100644 index 00000000000..145256efe86 --- /dev/null +++ b/homeassistant/components/surepetcare/services.yaml @@ -0,0 +1,9 @@ +set_lock_state: + description: Sets lock state + fields: + flap_id: + description: Flap ID to lock/unlock + example: "123456" + lock_state: + description: New lock state - unlocked, locked_in, locked_out or locked_all + example: "unlocked" From 10b591290184824804a88ebed2bb2364ef1bba80 Mon Sep 17 00:00:00 2001 From: migube Date: Sun, 3 Jan 2021 20:01:05 +0100 Subject: [PATCH 064/507] Reconnect mochad light on on/off command (#44507) --- homeassistant/components/mochad/light.py | 58 +++++++++++++++--------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/mochad/light.py b/homeassistant/components/mochad/light.py index 90af33bd0cd..a3a06a58ea5 100644 --- a/homeassistant/components/mochad/light.py +++ b/homeassistant/components/mochad/light.py @@ -1,5 +1,8 @@ """Support for X10 dimmer over Mochad.""" +import logging + from pymochad import device +from pymochad.exceptions import MochadException import voluptuous as vol from homeassistant.components.light import ( @@ -13,6 +16,7 @@ from homeassistant.helpers import config_validation as cv from . import CONF_COMM_TYPE, DOMAIN, REQ_LOCK +_LOGGER = logging.getLogger(__name__) CONF_BRIGHTNESS_LEVELS = "brightness_levels" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -103,30 +107,42 @@ class MochadLight(LightEntity): def turn_on(self, **kwargs): """Send the command to turn the light on.""" + _LOGGER.debug("Reconnect %s:%s", self._controller.server, self._controller.port) brightness = kwargs.get(ATTR_BRIGHTNESS, 255) with REQ_LOCK: - if self._brightness_levels > 32: - out_brightness = self._calculate_brightness_value(brightness) - self.light.send_cmd(f"xdim {out_brightness}") - self._controller.read_data() - else: - self.light.send_cmd("on") - self._controller.read_data() - # There is no persistence for X10 modules so a fresh on command - # will be full brightness - if self._brightness == 0: - self._brightness = 255 - self._adjust_brightness(brightness) - self._brightness = brightness - self._state = True + try: + # Recycle socket on new command to recover mochad connection + self._controller.reconnect() + if self._brightness_levels > 32: + out_brightness = self._calculate_brightness_value(brightness) + self.light.send_cmd(f"xdim {out_brightness}") + self._controller.read_data() + else: + self.light.send_cmd("on") + self._controller.read_data() + # There is no persistence for X10 modules so a fresh on command + # will be full brightness + if self._brightness == 0: + self._brightness = 255 + self._adjust_brightness(brightness) + self._brightness = brightness + self._state = True + except (MochadException, OSError) as exc: + _LOGGER.error("Error with mochad communication: %s", exc) def turn_off(self, **kwargs): """Send the command to turn the light on.""" + _LOGGER.debug("Reconnect %s:%s", self._controller.server, self._controller.port) with REQ_LOCK: - self.light.send_cmd("off") - self._controller.read_data() - # There is no persistence for X10 modules so we need to prepare - # to track a fresh on command will full brightness - if self._brightness_levels == 31: - self._brightness = 0 - self._state = False + try: + # Recycle socket on new command to recover mochad connection + self._controller.reconnect() + self.light.send_cmd("off") + self._controller.read_data() + # There is no persistence for X10 modules so we need to prepare + # to track a fresh on command will full brightness + if self._brightness_levels == 31: + self._brightness = 0 + self._state = False + except (MochadException, OSError) as exc: + _LOGGER.error("Error with mochad communication: %s", exc) From 2bd8ee34f4660f8fa47807a38791e63ce9134bdf Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 4 Jan 2021 03:07:12 +0100 Subject: [PATCH 065/507] Update xknx to 0.16.0 (#44749) * update xknx to 0.16.0 * fix telegram in knx_event and knx.send service * fix knx.send to not coerce floats to int fixes #44792 also enables strings to be sent * Revert "fix knx.send to not coerce floats to int" This reverts commit ac40fb53f965b3c0e4a801f1a0557b49be884551. --- homeassistant/components/knx/__init__.py | 23 +++++-- homeassistant/components/knx/factory.py | 67 +++++++++++++++++- homeassistant/components/knx/manifest.json | 2 +- homeassistant/components/knx/schema.py | 79 ++++++++++++++++------ requirements_all.txt | 2 +- 5 files changed, 146 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 1d547e895bf..204f8613883 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -14,6 +14,7 @@ from xknx.io import ( ConnectionType, ) from xknx.telegram import AddressFilter, GroupAddress, Telegram +from xknx.telegram.apci import GroupValueResponse, GroupValueWrite from homeassistant.const import ( CONF_ENTITY_ID, @@ -322,9 +323,21 @@ class KNXModule: async def telegram_received_cb(self, telegram): """Call invoked after a KNX telegram was received.""" + data = None + + # Not all telegrams have serializable data. + if isinstance(telegram.payload, (GroupValueWrite, GroupValueResponse)): + data = telegram.payload.value.value + self.hass.bus.async_fire( "knx_event", - {"address": str(telegram.group_address), "data": telegram.payload.value}, + { + "data": data, + "destination": str(telegram.destination_address), + "direction": telegram.direction.value, + "source": str(telegram.source_address), + "telegramtype": telegram.payload.__class__.__name__, + }, ) async def service_send_to_knx_bus(self, call): @@ -344,10 +357,10 @@ class KNXModule: return DPTBinary(attr_payload) return DPTArray(attr_payload) - payload = calculate_payload(attr_payload) - address = GroupAddress(attr_address) - - telegram = Telegram(group_address=address, payload=payload) + telegram = Telegram( + destination_address=GroupAddress(attr_address), + payload=GroupValueWrite(calculate_payload(attr_payload)), + ) await self.xknx.telegrams.put(telegram) diff --git a/homeassistant/components/knx/factory.py b/homeassistant/components/knx/factory.py index 385b7c009ed..c1e73733b22 100644 --- a/homeassistant/components/knx/factory.py +++ b/homeassistant/components/knx/factory.py @@ -1,4 +1,6 @@ """Factory function to initialize KNX devices from config.""" +from typing import Optional, Tuple + from xknx import XKNX from xknx.devices import ( BinarySensor as XknxBinarySensor, @@ -86,8 +88,30 @@ def _create_cover(knx_module: XKNX, config: ConfigType) -> XknxCover: ) +def _create_light_color( + color: str, config: ConfigType +) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[str]]: + """Load color configuration from configuration structure.""" + if "individual_colors" in config and color in config["individual_colors"]: + sub_config = config["individual_colors"][color] + group_address_switch = sub_config.get(CONF_ADDRESS) + group_address_switch_state = sub_config.get(LightSchema.CONF_STATE_ADDRESS) + group_address_brightness = sub_config.get(LightSchema.CONF_BRIGHTNESS_ADDRESS) + group_address_brightness_state = sub_config.get( + LightSchema.CONF_BRIGHTNESS_STATE_ADDRESS + ) + return ( + group_address_switch, + group_address_switch_state, + group_address_brightness, + group_address_brightness_state, + ) + return None, None, None, None + + def _create_light(knx_module: XKNX, config: ConfigType) -> XknxLight: """Return a KNX Light device to be used within XKNX.""" + group_address_tunable_white = None group_address_tunable_white_state = None group_address_color_temp = None @@ -103,10 +127,35 @@ def _create_light(knx_module: XKNX, config: ConfigType) -> XknxLight: LightSchema.CONF_COLOR_TEMP_STATE_ADDRESS ) + ( + red_switch, + red_switch_state, + red_brightness, + red_brightness_state, + ) = _create_light_color(LightSchema.CONF_RED, config) + ( + green_switch, + green_switch_state, + green_brightness, + green_brightness_state, + ) = _create_light_color(LightSchema.CONF_GREEN, config) + ( + blue_switch, + blue_switch_state, + blue_brightness, + blue_brightness_state, + ) = _create_light_color(LightSchema.CONF_BLUE, config) + ( + white_switch, + white_switch_state, + white_brightness, + white_brightness_state, + ) = _create_light_color(LightSchema.CONF_WHITE, config) + return XknxLight( knx_module, name=config[CONF_NAME], - group_address_switch=config[CONF_ADDRESS], + group_address_switch=config.get(CONF_ADDRESS), group_address_switch_state=config.get(LightSchema.CONF_STATE_ADDRESS), group_address_brightness=config.get(LightSchema.CONF_BRIGHTNESS_ADDRESS), group_address_brightness_state=config.get( @@ -120,6 +169,22 @@ def _create_light(knx_module: XKNX, config: ConfigType) -> XknxLight: group_address_tunable_white_state=group_address_tunable_white_state, group_address_color_temperature=group_address_color_temp, group_address_color_temperature_state=group_address_color_temp_state, + group_address_switch_red=red_switch, + group_address_switch_red_state=red_switch_state, + group_address_brightness_red=red_brightness, + group_address_brightness_red_state=red_brightness_state, + group_address_switch_green=green_switch, + group_address_switch_green_state=green_switch_state, + group_address_brightness_green=green_brightness, + group_address_brightness_green_state=green_brightness_state, + group_address_switch_blue=blue_switch, + group_address_switch_blue_state=blue_switch_state, + group_address_brightness_blue=blue_brightness, + group_address_brightness_blue_state=blue_brightness_state, + group_address_switch_white=white_switch, + group_address_switch_white_state=white_switch_state, + group_address_brightness_white=white_brightness, + group_address_brightness_white_state=white_brightness_state, min_kelvin=config[LightSchema.CONF_MIN_KELVIN], max_kelvin=config[LightSchema.CONF_MAX_KELVIN], ) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 631a6329c8c..eb084b5ebd4 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.6"], + "requirements": ["xknx==0.16.0"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "silver" } diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index b1d791e3284..4243dcc24e4 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -139,31 +139,72 @@ class LightSchema: DEFAULT_MIN_KELVIN = 2700 # 370 mireds DEFAULT_MAX_KELVIN = 6000 # 166 mireds - SCHEMA = vol.Schema( + CONF_INDIVIDUAL_COLORS = "individual_colors" + CONF_RED = "red" + CONF_GREEN = "green" + CONF_BLUE = "blue" + CONF_WHITE = "white" + + COLOR_SCHEMA = vol.Schema( { - vol.Required(CONF_ADDRESS): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_ADDRESS): cv.string, vol.Optional(CONF_STATE_ADDRESS): cv.string, - vol.Optional(CONF_BRIGHTNESS_ADDRESS): cv.string, + vol.Required(CONF_BRIGHTNESS_ADDRESS): cv.string, vol.Optional(CONF_BRIGHTNESS_STATE_ADDRESS): cv.string, - vol.Optional(CONF_COLOR_ADDRESS): cv.string, - vol.Optional(CONF_COLOR_STATE_ADDRESS): cv.string, - vol.Optional(CONF_COLOR_TEMP_ADDRESS): cv.string, - vol.Optional(CONF_COLOR_TEMP_STATE_ADDRESS): cv.string, - vol.Optional( - CONF_COLOR_TEMP_MODE, default=DEFAULT_COLOR_TEMP_MODE - ): cv.enum(ColorTempModes), - vol.Optional(CONF_RGBW_ADDRESS): cv.string, - vol.Optional(CONF_RGBW_STATE_ADDRESS): cv.string, - vol.Optional(CONF_MIN_KELVIN, default=DEFAULT_MIN_KELVIN): vol.All( - vol.Coerce(int), vol.Range(min=1) - ), - vol.Optional(CONF_MAX_KELVIN, default=DEFAULT_MAX_KELVIN): vol.All( - vol.Coerce(int), vol.Range(min=1) - ), } ) + SCHEMA = vol.All( + vol.Schema( + { + vol.Optional(CONF_ADDRESS): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_STATE_ADDRESS): cv.string, + vol.Optional(CONF_BRIGHTNESS_ADDRESS): cv.string, + vol.Optional(CONF_BRIGHTNESS_STATE_ADDRESS): cv.string, + vol.Exclusive(CONF_INDIVIDUAL_COLORS, "color"): { + vol.Inclusive(CONF_RED, "colors"): COLOR_SCHEMA, + vol.Inclusive(CONF_GREEN, "colors"): COLOR_SCHEMA, + vol.Inclusive(CONF_BLUE, "colors"): COLOR_SCHEMA, + vol.Optional(CONF_WHITE): COLOR_SCHEMA, + }, + vol.Exclusive(CONF_COLOR_ADDRESS, "color"): cv.string, + vol.Optional(CONF_COLOR_STATE_ADDRESS): cv.string, + vol.Optional(CONF_COLOR_TEMP_ADDRESS): cv.string, + vol.Optional(CONF_COLOR_TEMP_STATE_ADDRESS): cv.string, + vol.Optional( + CONF_COLOR_TEMP_MODE, default=DEFAULT_COLOR_TEMP_MODE + ): cv.enum(ColorTempModes), + vol.Exclusive(CONF_RGBW_ADDRESS, "color"): cv.string, + vol.Optional(CONF_RGBW_STATE_ADDRESS): cv.string, + vol.Optional(CONF_MIN_KELVIN, default=DEFAULT_MIN_KELVIN): vol.All( + vol.Coerce(int), vol.Range(min=1) + ), + vol.Optional(CONF_MAX_KELVIN, default=DEFAULT_MAX_KELVIN): vol.All( + vol.Coerce(int), vol.Range(min=1) + ), + } + ), + vol.Any( + vol.Schema( + { + vol.Required(CONF_INDIVIDUAL_COLORS): { + vol.Required(CONF_RED): {vol.Required(CONF_ADDRESS): object}, + vol.Required(CONF_GREEN): {vol.Required(CONF_ADDRESS): object}, + vol.Required(CONF_BLUE): {vol.Required(CONF_ADDRESS): object}, + }, + }, + extra=vol.ALLOW_EXTRA, + ), + vol.Schema( + { + vol.Required(CONF_ADDRESS): object, + }, + extra=vol.ALLOW_EXTRA, + ), + ), + ) + class ClimateSchema: """Voluptuous schema for KNX climate devices.""" diff --git a/requirements_all.txt b/requirements_all.txt index 830a5e4f456..7d24c803a31 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2305,7 +2305,7 @@ xboxapi==2.0.1 xfinity-gateway==0.0.4 # homeassistant.components.knx -xknx==0.15.6 +xknx==0.16.0 # homeassistant.components.bluesound # homeassistant.components.rest From ec926105a0021b79f711d9c01a0407570d75975e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Mon, 4 Jan 2021 03:53:15 +0100 Subject: [PATCH 066/507] Bump PyTado to 0.10.0 (#44770) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Bump PyTado to v0.10.0 Signed-off-by: Álvaro Fernández Rojas * Tado: switch to getDeviceInfo This function has been introduced in version 0.10.0 of PyTado. Signed-off-by: Álvaro Fernández Rojas * Tado: update tests Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/tado/__init__.py | 8 ++++++-- .../components/tado/binary_sensor.py | 8 ++++---- homeassistant/components/tado/entity.py | 4 ++-- homeassistant/components/tado/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tado/util.py | 7 +++++++ tests/fixtures/tado/device_wr1.json | 20 +++++++++++++++++++ 8 files changed, 42 insertions(+), 11 deletions(-) create mode 100644 tests/fixtures/tado/device_wr1.json diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 228ac48bcb2..5e929548817 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -173,6 +173,7 @@ class TadoConnector: self.zones = None self.devices = None self.data = { + "device": {}, "zone": {}, } @@ -193,15 +194,18 @@ class TadoConnector: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Update the registered zones.""" + for device in self.devices: + self.update_sensor("device", device["shortSerialNo"]) for zone in self.zones: self.update_sensor("zone", zone["id"]) - self.devices = self.tado.getDevices() def update_sensor(self, sensor_type, sensor): """Update the internal data from Tado.""" _LOGGER.debug("Updating %s %s", sensor_type, sensor) try: - if sensor_type == "zone": + if sensor_type == "device": + data = self.tado.getDeviceInfo(sensor) + elif sensor_type == "zone": data = self.tado.getZoneState(sensor) else: _LOGGER.debug("Unknown sensor: %s", sensor_type) diff --git a/homeassistant/components/tado/binary_sensor.py b/homeassistant/components/tado/binary_sensor.py index bf958cceb5d..1acefdb4c16 100644 --- a/homeassistant/components/tado/binary_sensor.py +++ b/homeassistant/components/tado/binary_sensor.py @@ -157,10 +157,10 @@ class TadoDeviceBinarySensor(TadoDeviceEntity, BinarySensorEntity): @callback def _async_update_device_data(self): """Handle update callbacks.""" - for device in self._tado.devices: - if device["serialNo"] == self.device_id: - self._device_info = device - break + try: + self._device_info = self._tado.data["device"][self.device_id] + except KeyError: + return if self.device_variable == "battery state": self._state = self._device_info["batteryState"] == "LOW" diff --git a/homeassistant/components/tado/entity.py b/homeassistant/components/tado/entity.py index bd0c605b0eb..e9fefe2848b 100644 --- a/homeassistant/components/tado/entity.py +++ b/homeassistant/components/tado/entity.py @@ -11,8 +11,8 @@ class TadoDeviceEntity(Entity): """Initialize a Tado device.""" super().__init__() self._device_info = device_info - self.device_name = device_info["shortSerialNo"] - self.device_id = device_info["serialNo"] + self.device_name = device_info["serialNo"] + self.device_id = device_info["shortSerialNo"] @property def device_info(self): diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index feff03d0b81..9b166027df3 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -2,7 +2,7 @@ "domain": "tado", "name": "Tado", "documentation": "https://www.home-assistant.io/integrations/tado", - "requirements": ["python-tado==0.8.1"], + "requirements": ["python-tado==0.10.0"], "codeowners": ["@michaelarnauts", "@bdraco"], "config_flow": true, "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 7d24c803a31..4736ab989bb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1807,7 +1807,7 @@ python-sochain-api==0.0.2 python-songpal==0.12 # homeassistant.components.tado -python-tado==0.8.1 +python-tado==0.10.0 # homeassistant.components.telegram_bot python-telegram-bot==13.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b5b906a9fb6..d2099f35404 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -894,7 +894,7 @@ python-openzwave-mqtt[mqtt-client]==1.4.0 python-songpal==0.12 # homeassistant.components.tado -python-tado==0.8.1 +python-tado==0.10.0 # homeassistant.components.twitch python-twitch-client==0.6.0 diff --git a/tests/components/tado/util.py b/tests/components/tado/util.py index 7dc6a21faa7..187c0f269bf 100644 --- a/tests/components/tado/util.py +++ b/tests/components/tado/util.py @@ -20,6 +20,9 @@ async def async_init_integration( me_fixture = "tado/me.json" zones_fixture = "tado/zones.json" + # WR1 Device + device_wr1_fixture = "tado/device_wr1.json" + # Smart AC with Swing zone_5_state_fixture = "tado/smartac3.with_swing.json" zone_5_capabilities_fixture = "tado/zone_with_swing_capabilities.json" @@ -50,6 +53,10 @@ async def async_init_integration( "https://my.tado.com/api/v2/homes/1/devices", text=load_fixture(devices_fixture), ) + m.get( + "https://my.tado.com/api/v2/devices/WR1/", + text=load_fixture(device_wr1_fixture), + ) m.get( "https://my.tado.com/api/v2/homes/1/zones", text=load_fixture(zones_fixture), diff --git a/tests/fixtures/tado/device_wr1.json b/tests/fixtures/tado/device_wr1.json new file mode 100644 index 00000000000..676784aeba3 --- /dev/null +++ b/tests/fixtures/tado/device_wr1.json @@ -0,0 +1,20 @@ +{ + "deviceType" : "WR02", + "currentFwVersion" : "59.4", + "accessPointWiFi" : { + "ssid" : "tado8480" + }, + "characteristics" : { + "capabilities" : [ + "INSIDE_TEMPERATURE_MEASUREMENT", + "IDENTIFY" + ] + }, + "serialNo" : "WR1", + "commandTableUploadState" : "FINISHED", + "connectionState" : { + "timestamp" : "2020-03-23T18:30:07.377Z", + "value" : true + }, + "shortSerialNo" : "WR1" +} From ad4804f38afce18ed70057548a5b8aa205a1bb00 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 4 Jan 2021 04:49:29 +0100 Subject: [PATCH 067/507] Fix knx.send service not accepting floats (#44802) --- homeassistant/components/knx/__init__.py | 25 ++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 204f8613883..9a183667c55 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -132,14 +132,23 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -SERVICE_KNX_SEND_SCHEMA = vol.Schema( - { - vol.Required(SERVICE_KNX_ATTR_ADDRESS): cv.string, - vol.Required(SERVICE_KNX_ATTR_PAYLOAD): vol.Any( - cv.positive_int, [cv.positive_int] - ), - vol.Optional(SERVICE_KNX_ATTR_TYPE): vol.Any(int, float, str), - } +SERVICE_KNX_SEND_SCHEMA = vol.Any( + vol.Schema( + { + vol.Required(SERVICE_KNX_ATTR_ADDRESS): cv.string, + vol.Required(SERVICE_KNX_ATTR_PAYLOAD): cv.match_all, + vol.Required(SERVICE_KNX_ATTR_TYPE): vol.Any(int, float, str), + } + ), + vol.Schema( + # without type given payload is treated as raw bytes + { + vol.Required(SERVICE_KNX_ATTR_ADDRESS): cv.string, + vol.Required(SERVICE_KNX_ATTR_PAYLOAD): vol.Any( + cv.positive_int, [cv.positive_int] + ), + } + ), ) From e9d2f583b6ec820334c995da8806ab13c48b441c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Jan 2021 09:43:43 +0100 Subject: [PATCH 068/507] Bump dessant/lock-threads from v2.0.1 to v2.0.3 (#44806) Bumps [dessant/lock-threads](https://github.com/dessant/lock-threads) from v2.0.1 to v2.0.3. - [Release notes](https://github.com/dessant/lock-threads/releases) - [Changelog](https://github.com/dessant/lock-threads/blob/master/CHANGELOG.md) - [Commits](https://github.com/dessant/lock-threads/compare/v2.0.1...486f7380c15596f92b724e4260e4981c68d6bde6) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/lock.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 4f7a0efb2d7..3059dc5e2ef 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -9,7 +9,7 @@ jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v2.0.1 + - uses: dessant/lock-threads@v2.0.3 with: github-token: ${{ github.token }} issue-lock-inactive-days: "30" From 805a9bcb97b61c7dc459d51cb8b0e24e24d26931 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 4 Jan 2021 09:59:08 +0100 Subject: [PATCH 069/507] Update to denonavr version 0.9.10 (#44791) --- homeassistant/components/denonavr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index 44cbd69bcd2..8d2052181f8 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.9", "getmac==0.8.2"], + "requirements": ["denonavr==0.9.10", "getmac==0.8.2"], "codeowners": ["@scarface-4711", "@starkillerOG"], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 4736ab989bb..c24ab96a76b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -478,7 +478,7 @@ defusedxml==0.6.0 deluge-client==1.7.1 # homeassistant.components.denonavr -denonavr==0.9.9 +denonavr==0.9.10 # homeassistant.components.devolo_home_control devolo-home-control-api==0.16.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d2099f35404..6a07d05a9ad 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -254,7 +254,7 @@ debugpy==1.2.1 defusedxml==0.6.0 # homeassistant.components.denonavr -denonavr==0.9.9 +denonavr==0.9.10 # homeassistant.components.devolo_home_control devolo-home-control-api==0.16.0 From 3c62c219915d8a8fe247f70e4d4b2f6a4cd4d67d Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Mon, 4 Jan 2021 09:31:13 +0000 Subject: [PATCH 070/507] Bumo pyroon version to 0.0.30 (#44800) --- homeassistant/components/roon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roon/manifest.json b/homeassistant/components/roon/manifest.json index 4bd5903253a..763d9b3dc92 100644 --- a/homeassistant/components/roon/manifest.json +++ b/homeassistant/components/roon/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/roon", "requirements": [ - "roonapi==0.0.28" + "roonapi==0.0.30" ], "codeowners": [ "@pavoni" diff --git a/requirements_all.txt b/requirements_all.txt index c24ab96a76b..b1ebef3e7eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1952,7 +1952,7 @@ rokuecp==0.6.0 roombapy==1.6.2 # homeassistant.components.roon -roonapi==0.0.28 +roonapi==0.0.30 # homeassistant.components.rova rova==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6a07d05a9ad..9e0f02eeaa3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -960,7 +960,7 @@ rokuecp==0.6.0 roombapy==1.6.2 # homeassistant.components.roon -roonapi==0.0.28 +roonapi==0.0.30 # homeassistant.components.rpi_power rpi-bad-power==0.1.0 From 12af87bc6e85f98623afc2231c55bb30aeb38938 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Jan 2021 23:51:44 -1000 Subject: [PATCH 071/507] Add index to old_state_id column for postgres and older databases (#44757) * Add index to old_state_id column for older databases The schema was updated in #43610 but the index was not added on migration. * Handle postgresql missing ondelete * create index first --- homeassistant/components/recorder/migration.py | 12 +++++++++++- homeassistant/components/recorder/models.py | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index c633c114b46..4501b25385e 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -211,7 +211,13 @@ def _update_states_table_with_foreign_key_options(engine): 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"]: + if foreign_key["name"] and ( + # MySQL/MariaDB will have empty options + not foreign_key["options"] + or + # Postgres will have ondelete set to None + foreign_key["options"].get("ondelete") is None + ): alters.append( { "old_fk": ForeignKeyConstraint((), (), name=foreign_key["name"]), @@ -312,6 +318,10 @@ def _apply_update(engine, new_version, old_version): _create_index(engine, "events", "ix_events_event_type_time_fired") _drop_index(engine, "events", "ix_events_event_type") elif new_version == 10: + # Now done in step 11 + pass + elif new_version == 11: + _create_index(engine, "states", "ix_states_old_state_id") _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 5b37f7e3f9d..9481e954bde 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 = 10 +SCHEMA_VERSION = 11 _LOGGER = logging.getLogger(__name__) From 134db3f710990a002451fa767d321cd35c5d3bb9 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Mon, 4 Jan 2021 02:46:58 -0800 Subject: [PATCH 072/507] Bump pyobihai (#44768) --- homeassistant/components/obihai/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/obihai/manifest.json b/homeassistant/components/obihai/manifest.json index bb72a967605..78123cc07f5 100644 --- a/homeassistant/components/obihai/manifest.json +++ b/homeassistant/components/obihai/manifest.json @@ -2,6 +2,6 @@ "domain": "obihai", "name": "Obihai", "documentation": "https://www.home-assistant.io/integrations/obihai", - "requirements": ["pyobihai==1.2.3"], + "requirements": ["pyobihai==1.3.1"], "codeowners": ["@dshokouhi"] } diff --git a/requirements_all.txt b/requirements_all.txt index b1ebef3e7eb..43f36695a51 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1569,7 +1569,7 @@ pynx584==0.5 pynzbgetapi==0.2.0 # homeassistant.components.obihai -pyobihai==1.2.3 +pyobihai==1.3.1 # homeassistant.components.ombi pyombi==0.1.10 From 43474762b24e285bc525d9ffa09c6f7658525353 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 4 Jan 2021 12:47:29 +0200 Subject: [PATCH 073/507] Drop remaining Python < 3.8 support (#44743) Co-authored-by: Paulus Schoutsen Co-authored-by: Franck Nijhof --- .pre-commit-config.yaml | 2 +- .readthedocs.yml | 2 +- azure-pipelines-release.yml | 4 ++-- azure-pipelines-translation.yml | 4 ++-- homeassistant/bootstrap.py | 10 ++++---- homeassistant/core.py | 2 -- homeassistant/package_constraints.txt | 1 - homeassistant/runner.py | 10 +------- homeassistant/util/package.py | 12 +--------- homeassistant/util/thread.py | 23 ------------------- pyproject.toml | 2 +- requirements.txt | 1 - setup.cfg | 5 ++-- setup.py | 1 - tests/components/plex/mock_classes.py | 2 +- tests/components/webostv/test_media_player.py | 9 ++------ tox.ini | 2 +- 17 files changed, 20 insertions(+), 72 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 11cd4aa7731..087dc914035 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: rev: v2.7.2 hooks: - id: pyupgrade - args: [--py37-plus] + args: [--py38-plus] - repo: https://github.com/psf/black rev: 20.8b1 hooks: diff --git a/.readthedocs.yml b/.readthedocs.yml index 0303f84d51c..e8344e0a655 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -4,7 +4,7 @@ build: image: latest python: - version: 3.7 + version: 3.8 setup_py_install: true requirements_file: requirements_docs.txt diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 6da0b128e47..418fdf5b26c 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -60,9 +60,9 @@ stages: vmImage: 'ubuntu-latest' steps: - task: UsePythonVersion@0 - displayName: 'Use Python 3.7' + displayName: 'Use Python 3.8' inputs: - versionSpec: '3.7' + versionSpec: '3.8' - script: pip install twine wheel displayName: 'Install tools' - script: python setup.py sdist bdist_wheel diff --git a/azure-pipelines-translation.yml b/azure-pipelines-translation.yml index 9f4db8d2005..481b98bc484 100644 --- a/azure-pipelines-translation.yml +++ b/azure-pipelines-translation.yml @@ -30,9 +30,9 @@ jobs: vmImage: 'ubuntu-latest' steps: - task: UsePythonVersion@0 - displayName: 'Use Python 3.7' + displayName: 'Use Python 3.8' inputs: - versionSpec: '3.7' + versionSpec: '3.8' - script: | export LOKALISE_TOKEN="$(lokaliseToken)" export AZURE_BRANCH="$(Build.SourceBranchName)" diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index ff8ecfa0070..bf16c4091f1 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -307,12 +307,10 @@ def async_enable_logging( sys.excepthook = lambda *args: logging.getLogger(None).exception( "Uncaught exception", exc_info=args # type: ignore ) - - if sys.version_info[:2] >= (3, 8): - threading.excepthook = lambda args: logging.getLogger(None).exception( - "Uncaught thread exception", - exc_info=(args.exc_type, args.exc_value, args.exc_traceback), - ) + threading.excepthook = lambda args: logging.getLogger(None).exception( + "Uncaught thread exception", + exc_info=(args.exc_type, args.exc_value, args.exc_traceback), # type: ignore[arg-type] + ) # Log errors to a file if we have write access to file or config dir if log_file is None: diff --git a/homeassistant/core.py b/homeassistant/core.py index 6b657f600d8..18b69fd5366 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -73,7 +73,6 @@ from homeassistant.exceptions import ( from homeassistant.util import location, network from homeassistant.util.async_ import fire_coroutine_threadsafe, run_callback_threadsafe import homeassistant.util.dt as dt_util -from homeassistant.util.thread import fix_threading_exception_logging from homeassistant.util.timeout import TimeoutManager from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem import homeassistant.util.uuid as uuid_util @@ -86,7 +85,6 @@ if TYPE_CHECKING: block_async_io.enable() -fix_threading_exception_logging() T = TypeVar("T") _UNDEF: dict = {} # Internal; not helpers.typing.UNDEFINED due to circular dependency diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d273d730b97..06f2c5a6704 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,6 @@ emoji==0.5.4 hass-nabucasa==0.39.0 home-assistant-frontend==20201229.0 httpx==0.16.1 -importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 netdisco==2.8.2 paho-mqtt==1.5.1 diff --git a/homeassistant/runner.py b/homeassistant/runner.py index 0f8bb836da5..54d20a7deff 100644 --- a/homeassistant/runner.py +++ b/homeassistant/runner.py @@ -3,7 +3,6 @@ import asyncio from concurrent.futures import ThreadPoolExecutor import dataclasses import logging -import sys from typing import Any, Dict, Optional from homeassistant import bootstrap @@ -41,14 +40,7 @@ class RuntimeConfig: open_ui: bool = False -# In Python 3.8+ proactor policy is the default on Windows -if sys.platform == "win32" and sys.version_info[:2] < (3, 8): - PolicyBase = asyncio.WindowsProactorEventLoopPolicy -else: - PolicyBase = asyncio.DefaultEventLoopPolicy - - -class HassEventLoopPolicy(PolicyBase): # type: ignore +class HassEventLoopPolicy(asyncio.DefaultEventLoopPolicy): # type: ignore[valid-type,misc] """Event loop policy for Home Assistant.""" def __init__(self, debug: bool) -> None: diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index a665fd78914..5391d92ed89 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -1,5 +1,6 @@ """Helpers to install PyPi packages.""" import asyncio +from importlib.metadata import PackageNotFoundError, version import logging import os from pathlib import Path @@ -10,17 +11,6 @@ from urllib.parse import urlparse import pkg_resources -if sys.version_info[:2] >= (3, 8): - from importlib.metadata import ( # pylint: disable=no-name-in-module,import-error - PackageNotFoundError, - version, - ) -else: - from importlib_metadata import ( # pylint: disable=import-error - PackageNotFoundError, - version, - ) - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/util/thread.py b/homeassistant/util/thread.py index bf61c67172a..7743e1d159c 100644 --- a/homeassistant/util/thread.py +++ b/homeassistant/util/thread.py @@ -1,33 +1,10 @@ """Threading util helpers.""" import ctypes import inspect -import sys import threading from typing import Any -def fix_threading_exception_logging() -> None: - """Fix threads passing uncaught exceptions to our exception hook. - - https://bugs.python.org/issue1230540 - Fixed in Python 3.8. - """ - if sys.version_info[:2] >= (3, 8): - return - - run_old = threading.Thread.run - - def run(*args: Any, **kwargs: Any) -> None: - try: - run_old(*args, **kwargs) - except (KeyboardInterrupt, SystemExit): # pylint: disable=try-except-raise - raise - except Exception: # pylint: disable=broad-except - sys.excepthook(*sys.exc_info()) - - threading.Thread.run = run # type: ignore - - def _async_raise(tid: int, exctype: Any) -> None: """Raise an exception in the threads with id tid.""" if not inspect.isclass(exctype): diff --git a/pyproject.toml b/pyproject.toml index 0f416d9e014..445f13e8724 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.black] -target-version = ["py37", "py38"] +target-version = ["py38"] exclude = 'generated' [tool.isort] diff --git a/requirements.txt b/requirements.txt index 7492e794172..d566cb738ae 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,6 @@ bcrypt==3.1.7 certifi>=2020.12.5 ciso8601==2.1.3 httpx==0.16.1 -importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 PyJWT==1.7.1 cryptography==3.2 diff --git a/setup.cfg b/setup.cfg index de5092dcecf..1fc973ef21c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,7 +11,8 @@ classifier = Intended Audience :: Developers License :: OSI Approved :: Apache Software License Operating System :: OS Independent - Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 Topic :: Home Automation [flake8] @@ -31,7 +32,7 @@ ignore = W504 [mypy] -python_version = 3.7 +python_version = 3.8 show_error_codes = true ignore_errors = true follow_imports = silent diff --git a/setup.py b/setup.py index 59ea906344c..82d6efacb1b 100755 --- a/setup.py +++ b/setup.py @@ -40,7 +40,6 @@ REQUIRES = [ "certifi>=2020.12.5", "ciso8601==2.1.3", "httpx==0.16.1", - "importlib-metadata==1.6.0;python_version<'3.8'", "jinja2>=2.11.2", "PyJWT==1.7.1", # PyJWT has loose dependency. We want the latest one. diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py index 8ac894438be..5f2fad6a8f1 100644 --- a/tests/components/plex/mock_classes.py +++ b/tests/components/plex/mock_classes.py @@ -247,7 +247,7 @@ class MockPlexServer: """Mock the playlist lookup method.""" return MockPlexMediaItem(playlist, mediatype="playlist") - @lru_cache() + @lru_cache def playlists(self): """Mock the playlists lookup method with a lazy init.""" return [ diff --git a/tests/components/webostv/test_media_player.py b/tests/components/webostv/test_media_player.py index 8ddc7b31657..70bc8274684 100644 --- a/tests/components/webostv/test_media_player.py +++ b/tests/components/webostv/test_media_player.py @@ -1,5 +1,6 @@ """The tests for the LG webOS media player platform.""" -import sys + +from unittest.mock import patch import pytest @@ -25,12 +26,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -if sys.version_info >= (3, 8, 0): - from unittest.mock import patch -else: - from unittest.mock import patch - - NAME = "fake" ENTITY_ID = f"{media_player.DOMAIN}.{NAME}" diff --git a/tox.ini b/tox.ini index cc1df307bfe..9c9963c28ee 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37, py38, lint, pylint, typing, cov +envlist = py38, py39, lint, pylint, typing, cov skip_missing_interpreters = True ignore_basepython_conflict = True From 34bd70aee66fa41424163ea3f28b7e404edecb90 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Jan 2021 12:28:17 +0100 Subject: [PATCH 074/507] Fix race when handling MQTT discovery messages (#44730) * Fix race when handling MQTT discovery messages * Lint * retrigger checks --- homeassistant/components/mqtt/__init__.py | 29 ++++- .../components/mqtt/alarm_control_panel.py | 13 +- .../components/mqtt/binary_sensor.py | 13 +- homeassistant/components/mqtt/camera.py | 13 +- homeassistant/components/mqtt/climate.py | 13 +- homeassistant/components/mqtt/cover.py | 13 +- .../components/mqtt/device_automation.py | 13 +- .../mqtt/device_tracker/schema_discovery.py | 13 +- .../components/mqtt/device_trigger.py | 11 +- homeassistant/components/mqtt/discovery.py | 62 ++++++++- homeassistant/components/mqtt/fan.py | 13 +- .../components/mqtt/light/__init__.py | 13 +- homeassistant/components/mqtt/lock.py | 13 +- homeassistant/components/mqtt/scene.py | 13 +- homeassistant/components/mqtt/sensor.py | 13 +- homeassistant/components/mqtt/switch.py | 13 +- homeassistant/components/mqtt/tag.py | 25 +++- .../components/mqtt/vacuum/__init__.py | 13 +- tests/components/mqtt/test_discovery.py | 118 +++++++++++++++++- 19 files changed, 368 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index cced3670cca..eb1e8c01ae0 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -35,7 +35,11 @@ from homeassistant.const import CONF_UNIQUE_ID # noqa: F401 from homeassistant.core import CoreState, Event, HassJob, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.helpers import config_validation as cv, event, template -from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, + dispatcher_send, +) from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceDataType from homeassistant.loader import bind_hass @@ -78,6 +82,7 @@ from .const import ( from .debug_info import log_messages from .discovery import ( LAST_DISCOVERY, + MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_UPDATED, clear_discovery_hash, set_discovery_hash, @@ -1315,6 +1320,9 @@ class MqttDiscoveryUpdate(Entity): else: # Non-empty, unchanged payload: Ignore to avoid changing states _LOGGER.info("Ignoring unchanged update for: %s", self.entity_id) + async_dispatcher_send( + self.hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) if discovery_hash: debug_info.add_entity_discovery_data( @@ -1327,17 +1335,26 @@ class MqttDiscoveryUpdate(Entity): MQTT_DISCOVERY_UPDATED.format(discovery_hash), discovery_callback, ) + async_dispatcher_send( + self.hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) async def async_removed_from_registry(self) -> None: """Clear retained discovery topic in broker.""" if not self._removed_from_hass: discovery_topic = self._discovery_data[ATTR_DISCOVERY_TOPIC] - publish( - self.hass, - discovery_topic, - "", - retain=True, + publish(self.hass, discovery_topic, "", retain=True) + + @callback + def add_to_platform_abort(self) -> None: + """Abort adding an entity to a platform.""" + if self._discovery_data: + discovery_hash = self._discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(self.hass, discovery_hash) + async_dispatcher_send( + self.hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None ) + super().add_to_platform_abort() async def async_will_remove_from_hass(self) -> None: """Stop listening to signal and cleanup discovery data..""" diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index edf383a6819..46eaa912615 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -29,7 +29,10 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -49,7 +52,7 @@ from . import ( ) from .. import mqtt from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -119,7 +122,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass, config, async_add_entities, config_entry, discovery_data ) except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) + discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(hass, discovery_hash) + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) raise async_dispatcher_connect( diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index e081423d590..17dcd301cd0 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -21,7 +21,10 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) import homeassistant.helpers.event as evt from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.reload import async_setup_reload_service @@ -42,7 +45,7 @@ from . import ( ) from .. import mqtt from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -92,7 +95,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass, config, async_add_entities, config_entry, discovery_data ) except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) + discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(hass, discovery_hash) + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) raise async_dispatcher_connect( diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index e8783f74bd4..20d68d1c4b0 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -8,7 +8,10 @@ from homeassistant.components.camera import Camera from homeassistant.const import CONF_DEVICE, CONF_NAME, CONF_UNIQUE_ID from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -25,7 +28,7 @@ from . import ( ) from .. import mqtt from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -66,7 +69,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config, async_add_entities, config_entry, discovery_data ) except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) + discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(hass, discovery_hash) + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) raise async_dispatcher_connect( diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index c5835f8e7c7..715658417b2 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -47,7 +47,10 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -66,7 +69,7 @@ from . import ( ) from .. import mqtt from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -259,7 +262,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass, config, async_add_entities, config_entry, discovery_data ) except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) + discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(hass, discovery_hash) + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) raise async_dispatcher_connect( diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 25fcf0ad0d2..59dedd7f475 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -33,7 +33,10 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -53,7 +56,7 @@ from . import ( ) from .. import mqtt from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -190,7 +193,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass, config, async_add_entities, config_entry, discovery_data ) except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) + discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(hass, discovery_hash) + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) raise async_dispatcher_connect( diff --git a/homeassistant/components/mqtt/device_automation.py b/homeassistant/components/mqtt/device_automation.py index c064cca599d..1186a212243 100644 --- a/homeassistant/components/mqtt/device_automation.py +++ b/homeassistant/components/mqtt/device_automation.py @@ -4,11 +4,14 @@ import logging import voluptuous as vol from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from . import ATTR_DISCOVERY_HASH, device_trigger from .. import mqtt -from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -42,7 +45,11 @@ async def async_setup_entry(hass, config_entry): hass, config, config_entry, discovery_data ) except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) + discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(hass, discovery_hash) + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) raise async_dispatcher_connect( diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker/schema_discovery.py index 4de2ae4fa6d..08a4871084c 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_discovery.py +++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py @@ -20,7 +20,10 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from .. import ( MqttAttributes, @@ -32,7 +35,7 @@ from .. import ( from ... import mqtt from ..const import ATTR_DISCOVERY_HASH, CONF_QOS, CONF_STATE_TOPIC from ..debug_info import log_messages -from ..discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from ..discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -69,7 +72,11 @@ async def async_setup_entry_from_discovery(hass, config_entry, async_add_entitie hass, config, async_add_entities, config_entry, discovery_data ) except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) + discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(hass, discovery_hash) + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) raise async_dispatcher_connect( diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 9fa51bebf09..8a8b525da61 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -11,7 +11,10 @@ from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import ( @@ -28,7 +31,7 @@ from . import ( trigger as mqtt_trigger, ) from .. import mqtt -from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_UPDATED, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -206,6 +209,7 @@ async def async_setup_trigger(hass, config, config_entry, discovery_data): await _update_device(hass, config_entry, config) device_trigger = hass.data[DEVICE_TRIGGERS][discovery_id] await device_trigger.update_trigger(config, discovery_hash, remove_signal) + async_dispatcher_send(hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None) remove_signal = async_dispatcher_connect( hass, MQTT_DISCOVERY_UPDATED.format(discovery_hash), discovery_update @@ -220,6 +224,7 @@ async def async_setup_trigger(hass, config, config_entry, discovery_data): ) if device is None: + async_dispatcher_send(hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None) return if DEVICE_TRIGGERS not in hass.data: @@ -244,6 +249,8 @@ async def async_setup_trigger(hass, config, config_entry, discovery_data): hass, discovery_hash, discovery_data, device.id ) + async_dispatcher_send(hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None) + async def async_device_removed(hass: HomeAssistant, device_id: str): """Handle the removal of a device.""" diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 5452d15aa30..eec1a63f932 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -1,5 +1,6 @@ """Support for MQTT discovery.""" import asyncio +from collections import deque import functools import json import logging @@ -7,7 +8,10 @@ import re import time from homeassistant.const import CONF_DEVICE, CONF_PLATFORM -from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import async_get_mqtt @@ -46,6 +50,7 @@ SUPPORTED_COMPONENTS = [ ] ALREADY_DISCOVERED = "mqtt_discovered_components" +PENDING_DISCOVERED = "mqtt_pending_components" CONFIG_ENTRY_IS_SETUP = "mqtt_config_entry_is_setup" DATA_CONFIG_ENTRY_LOCK = "mqtt_config_entry_lock" DATA_CONFIG_FLOW_LOCK = "mqtt_discovery_config_flow_lock" @@ -53,6 +58,7 @@ DISCOVERY_UNSUBSCRIBE = "mqtt_discovery_unsubscribe" INTEGRATION_UNSUBSCRIBE = "mqtt_integration_discovery_unsubscribe" MQTT_DISCOVERY_UPDATED = "mqtt_discovery_updated_{}" MQTT_DISCOVERY_NEW = "mqtt_discovery_new_{}_{}" +MQTT_DISCOVERY_DONE = "mqtt_discovery_done_{}" LAST_DISCOVERY = "mqtt_last_discovery" TOPIC_BASE = "~" @@ -78,7 +84,7 @@ async def async_start( """Start MQTT Discovery.""" mqtt_integrations = {} - async def async_entity_message_received(msg): + async def async_discovery_message_received(msg): """Process the received message.""" hass.data[LAST_DISCOVERY] = time.time() payload = msg.payload @@ -141,8 +147,46 @@ async def async_start( payload[CONF_PLATFORM] = "mqtt" - if ALREADY_DISCOVERED not in hass.data: - hass.data[ALREADY_DISCOVERED] = {} + if discovery_hash in hass.data[PENDING_DISCOVERED]: + pending = hass.data[PENDING_DISCOVERED][discovery_hash]["pending"] + pending.appendleft(payload) + _LOGGER.info( + "Component has already been discovered: %s %s, queuing update", + component, + discovery_id, + ) + return + + await async_process_discovery_payload(component, discovery_id, payload) + + async def async_process_discovery_payload(component, discovery_id, payload): + + _LOGGER.debug("Process discovery payload %s", payload) + discovery_hash = (component, discovery_id) + if discovery_hash in hass.data[ALREADY_DISCOVERED] or payload: + + async def discovery_done(_): + pending = hass.data[PENDING_DISCOVERED][discovery_hash]["pending"] + _LOGGER.debug("Pending discovery for %s: %s", discovery_hash, pending) + if not pending: + hass.data[PENDING_DISCOVERED][discovery_hash]["unsub"]() + hass.data[PENDING_DISCOVERED].pop(discovery_hash) + else: + payload = pending.pop() + await async_process_discovery_payload( + component, discovery_id, payload + ) + + if discovery_hash not in hass.data[PENDING_DISCOVERED]: + hass.data[PENDING_DISCOVERED][discovery_hash] = { + "unsub": async_dispatcher_connect( + hass, + MQTT_DISCOVERY_DONE.format(discovery_hash), + discovery_done, + ), + "pending": deque([]), + } + if discovery_hash in hass.data[ALREADY_DISCOVERED]: # Dispatch update _LOGGER.info( @@ -182,13 +226,21 @@ async def async_start( async_dispatcher_send( hass, MQTT_DISCOVERY_NEW.format(component, "mqtt"), payload ) + else: + # Unhandled discovery message + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock() hass.data[DATA_CONFIG_FLOW_LOCK] = asyncio.Lock() hass.data[CONFIG_ENTRY_IS_SETUP] = set() + hass.data[ALREADY_DISCOVERED] = {} + hass.data[PENDING_DISCOVERED] = {} + hass.data[DISCOVERY_UNSUBSCRIBE] = await mqtt.async_subscribe( - hass, f"{discovery_topic}/#", async_entity_message_received, 0 + hass, f"{discovery_topic}/#", async_discovery_message_received, 0 ) hass.data[LAST_DISCOVERY] = time.time() mqtt_integrations = await async_get_mqtt(hass) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 96d5fe720c3..cc635ab6e45 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -25,7 +25,10 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -45,7 +48,7 @@ from . import ( ) from .. import mqtt from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -131,7 +134,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass, config, async_add_entities, config_entry, discovery_data ) except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) + discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(hass, discovery_hash) + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) raise async_dispatcher_connect( diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index 393cb2fcf13..1ab0888866c 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -4,12 +4,15 @@ import logging import voluptuous as vol from homeassistant.components import light -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .. import ATTR_DISCOVERY_HASH, DOMAIN, PLATFORMS -from ..discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from ..discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash from .schema import CONF_SCHEMA, MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import PLATFORM_SCHEMA_BASIC, async_setup_entity_basic from .schema_json import PLATFORM_SCHEMA_JSON, async_setup_entity_json @@ -53,7 +56,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass, config, async_add_entities, config_entry, discovery_data ) except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) + discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(hass, discovery_hash) + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) raise async_dispatcher_connect( diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 712f2e0e376..70c771ba22d 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -14,7 +14,10 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -34,7 +37,7 @@ from . import ( ) from .. import mqtt from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -93,7 +96,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass, config, async_add_entities, config_entry, discovery_data ) except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) + discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(hass, discovery_hash) + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) raise async_dispatcher_connect( diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index 673eb169b19..eebcdc26bac 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -7,7 +7,10 @@ from homeassistant.components import 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.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -22,7 +25,7 @@ from . import ( MqttDiscoveryUpdate, ) from .. import mqtt -from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -61,7 +64,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config, async_add_entities, config_entry, discovery_data ) except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) + discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(hass, discovery_hash) + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) raise async_dispatcher_connect( diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 1fda8986ef7..cfcd46c9e95 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -19,7 +19,10 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.reload import async_setup_reload_service @@ -40,7 +43,7 @@ from . import ( ) from .. import mqtt from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -86,7 +89,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass, config, async_add_entities, config_entry, discovery_data ) except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) + discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(hass, discovery_hash) + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) raise async_dispatcher_connect( diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 76019680110..e074cb819d2 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -18,7 +18,10 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -39,7 +42,7 @@ from . import ( ) from .. import mqtt from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -89,7 +92,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass, config, async_add_entities, config_entry, discovery_data ) except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) + discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(hass, discovery_hash) + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) raise async_dispatcher_connect( diff --git a/homeassistant/components/mqtt/tag.py b/homeassistant/components/mqtt/tag.py index 75f3bb50309..1185f925b74 100644 --- a/homeassistant/components/mqtt/tag.py +++ b/homeassistant/components/mqtt/tag.py @@ -6,7 +6,10 @@ import voluptuous as vol from homeassistant.const import CONF_PLATFORM, CONF_VALUE_TEMPLATE import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from . import ( ATTR_DISCOVERY_HASH, @@ -21,7 +24,12 @@ from . import ( subscription, ) from .. import mqtt -from .discovery import MQTT_DISCOVERY_NEW, MQTT_DISCOVERY_UPDATED, clear_discovery_hash +from .discovery import ( + MQTT_DISCOVERY_DONE, + MQTT_DISCOVERY_NEW, + MQTT_DISCOVERY_UPDATED, + clear_discovery_hash, +) from .util import valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -50,7 +58,11 @@ async def async_setup_entry(hass, config_entry): config = PLATFORM_SCHEMA(discovery_payload) await async_setup_tag(hass, config, config_entry, discovery_data) except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) + discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(hass, discovery_hash) + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) raise async_dispatcher_connect( @@ -142,6 +154,10 @@ class MQTTTagScanner: self._setup_from_config(config) await self.subscribe_topics() + async_dispatcher_send( + self.hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) + def _setup_from_config(self, config): self._value_template = lambda value, error_value: value if CONF_VALUE_TEMPLATE in config: @@ -163,6 +179,9 @@ class MQTTTagScanner: MQTT_DISCOVERY_UPDATED.format(discovery_hash), self.discovery_update, ) + async_dispatcher_send( + self.hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) async def subscribe_topics(self): """Subscribe to MQTT topics.""" diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index f6265d1b96b..36e6df4ed1d 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -4,11 +4,14 @@ import logging import voluptuous as vol from homeassistant.components.vacuum import DOMAIN -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.reload import async_setup_reload_service from .. import ATTR_DISCOVERY_HASH, DOMAIN as MQTT_DOMAIN, PLATFORMS -from ..discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from ..discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash from .schema import CONF_SCHEMA, LEGACY, MQTT_VACUUM_SCHEMA, STATE from .schema_legacy import PLATFORM_SCHEMA_LEGACY, async_setup_entity_legacy from .schema_state import PLATFORM_SCHEMA_STATE, async_setup_entity_state @@ -45,7 +48,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config, async_add_entities, config_entry, discovery_data ) except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) + discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(hass, discovery_hash) + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) raise async_dispatcher_connect( diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 7f22e0eef6f..04d35ab1d26 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -12,7 +12,8 @@ from homeassistant.components.mqtt.abbreviations import ( DEVICE_ABBREVIATIONS, ) from homeassistant.components.mqtt.discovery import ALREADY_DISCOVERED, async_start -from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.const import EVENT_STATE_CHANGED, STATE_OFF, STATE_ON +import homeassistant.core as ha from tests.common import ( async_fire_mqtt_message, @@ -252,6 +253,121 @@ async def test_rediscover(hass, mqtt_mock, caplog): assert state is not None +async def test_rapid_rediscover(hass, mqtt_mock, caplog): + """Test immediate rediscover of removed component.""" + + events = [] + + @ha.callback + def callback(event): + """Verify event got called.""" + events.append(event) + + hass.bus.async_listen(EVENT_STATE_CHANGED, callback) + + async_fire_mqtt_message( + hass, + "homeassistant/binary_sensor/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', + ) + await hass.async_block_till_done() + state = hass.states.get("binary_sensor.beer") + assert state is not None + assert len(events) == 1 + + # Removal immediately followed by rediscover + async_fire_mqtt_message(hass, "homeassistant/binary_sensor/bla/config", "") + async_fire_mqtt_message( + hass, + "homeassistant/binary_sensor/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', + ) + async_fire_mqtt_message(hass, "homeassistant/binary_sensor/bla/config", "") + async_fire_mqtt_message( + hass, + "homeassistant/binary_sensor/bla/config", + '{ "name": "Milk", "state_topic": "test-topic" }', + ) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids("binary_sensor")) == 1 + state = hass.states.get("binary_sensor.milk") + assert state is not None + + assert len(events) == 5 + # Remove the entity + assert events[1].data["entity_id"] == "binary_sensor.beer" + assert events[1].data["new_state"] is None + # Add the entity + assert events[2].data["entity_id"] == "binary_sensor.beer" + assert events[2].data["old_state"] is None + # Remove the entity + assert events[3].data["entity_id"] == "binary_sensor.beer" + assert events[3].data["new_state"] is None + # Add the entity + assert events[4].data["entity_id"] == "binary_sensor.milk" + assert events[4].data["old_state"] is None + + +async def test_rapid_rediscover_unique(hass, mqtt_mock, caplog): + """Test immediate rediscover of removed component.""" + + events = [] + + @ha.callback + def callback(event): + """Verify event got called.""" + events.append(event) + + hass.bus.async_listen(EVENT_STATE_CHANGED, callback) + + async_fire_mqtt_message( + hass, + "homeassistant/binary_sensor/bla2/config", + '{ "name": "Ale", "state_topic": "test-topic", "unique_id": "very_unique" }', + ) + await hass.async_block_till_done() + state = hass.states.get("binary_sensor.ale") + assert state is not None + assert len(events) == 1 + + # Duplicate unique_id, immediately followed by correct unique_id + async_fire_mqtt_message( + hass, + "homeassistant/binary_sensor/bla/config", + '{ "name": "Beer", "state_topic": "test-topic", "unique_id": "very_unique" }', + ) + async_fire_mqtt_message( + hass, + "homeassistant/binary_sensor/bla/config", + '{ "name": "Beer", "state_topic": "test-topic", "unique_id": "even_uniquer" }', + ) + async_fire_mqtt_message(hass, "homeassistant/binary_sensor/bla/config", "") + async_fire_mqtt_message( + hass, + "homeassistant/binary_sensor/bla/config", + '{ "name": "Milk", "state_topic": "test-topic", "unique_id": "even_uniquer" }', + ) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids("binary_sensor")) == 2 + state = hass.states.get("binary_sensor.ale") + assert state is not None + state = hass.states.get("binary_sensor.milk") + assert state is not None + + assert len(events) == 4 + # Add the entity + assert events[1].data["entity_id"] == "binary_sensor.beer" + assert events[1].data["old_state"] is None + # Remove the entity + assert events[2].data["entity_id"] == "binary_sensor.beer" + assert events[2].data["new_state"] is None + # Add the entity + assert events[3].data["entity_id"] == "binary_sensor.milk" + assert events[3].data["old_state"] is None + + async def test_duplicate_removal(hass, mqtt_mock, caplog): """Test for a non duplicate component.""" async_fire_mqtt_message( From 0cff069c98816b68ffc7cb73f9c3bdcbef6f849a Mon Sep 17 00:00:00 2001 From: Bob Matcuk Date: Mon, 4 Jan 2021 06:33:34 -0500 Subject: [PATCH 075/507] Fix bug with blink auth flow (#44769) --- homeassistant/components/blink/config_flow.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/blink/config_flow.py b/homeassistant/components/blink/config_flow.py index d244c316483..5c77add3118 100644 --- a/homeassistant/components/blink/config_flow.py +++ b/homeassistant/components/blink/config_flow.py @@ -36,6 +36,7 @@ def _send_blink_2fa_pin(auth, pin): """Send 2FA pin to blink servers.""" blink = Blink() blink.auth = auth + blink.setup_login_ids() blink.setup_urls() return auth.send_auth_key(blink, pin) From 5b67030c26a75c65bb2757d8980692aa90c3243c Mon Sep 17 00:00:00 2001 From: Kendell R Date: Mon, 4 Jan 2021 03:40:03 -0800 Subject: [PATCH 076/507] Change WHITELIST to ALLOWLIST for websockets (#44766) --- homeassistant/components/websocket_api/commands.py | 4 ++-- homeassistant/components/websocket_api/permissions.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index fb9ffea2904..2dd6ff47e3c 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -58,11 +58,11 @@ def handle_subscribe_events(hass, connection, msg): """Handle subscribe events command.""" # Circular dep # pylint: disable=import-outside-toplevel - from .permissions import SUBSCRIBE_WHITELIST + from .permissions import SUBSCRIBE_ALLOWLIST event_type = msg["event_type"] - if event_type not in SUBSCRIBE_WHITELIST and not connection.user.is_admin: + if event_type not in SUBSCRIBE_ALLOWLIST and not connection.user.is_admin: raise Unauthorized if event_type == EVENT_STATE_CHANGED: diff --git a/homeassistant/components/websocket_api/permissions.py b/homeassistant/components/websocket_api/permissions.py index 8b00981fb04..010a18f972c 100644 --- a/homeassistant/components/websocket_api/permissions.py +++ b/homeassistant/components/websocket_api/permissions.py @@ -22,7 +22,7 @@ from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED # These are events that do not contain any sensitive data # Except for state_changed, which is handled accordingly. -SUBSCRIBE_WHITELIST = { +SUBSCRIBE_ALLOWLIST = { EVENT_AREA_REGISTRY_UPDATED, EVENT_COMPONENT_LOADED, EVENT_CORE_CONFIG_UPDATE, From a7a4875f529db523aff6e2d599b852897d0bb979 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 4 Jan 2021 13:10:39 +0100 Subject: [PATCH 077/507] Add more debug details to running timeouts (#43644) Co-authored-by: Franck Nijhof --- homeassistant/bootstrap.py | 7 +++-- homeassistant/util/timeout.py | 12 +++++--- tests/util/test_timeout.py | 57 +++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 6 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index bf16c4091f1..0f5bda7fbf2 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -381,7 +381,7 @@ def _get_domains(hass: core.HomeAssistant, config: Dict[str, Any]) -> Set[str]: async def _async_log_pending_setups( - domains: Set[str], setup_started: Dict[str, datetime] + hass: core.HomeAssistant, domains: Set[str], setup_started: Dict[str, datetime] ) -> None: """Periodic log of setups that are pending for longer than LOG_SLOW_STARTUP_INTERVAL.""" while True: @@ -393,6 +393,7 @@ async def _async_log_pending_setups( "Waiting on integrations to complete setup: %s", ", ".join(remaining), ) + _LOGGER.debug("Running timeout Zones: %s", hass.timeout.zones) async def async_setup_multi_components( @@ -406,7 +407,9 @@ async def async_setup_multi_components( domain: hass.async_create_task(async_setup_component(hass, domain, config)) for domain in domains } - log_task = asyncio.create_task(_async_log_pending_setups(domains, setup_started)) + log_task = asyncio.create_task( + _async_log_pending_setups(hass, domains, setup_started) + ) await asyncio.wait(futures.values()) log_task.cancel() errors = [domain for domain in domains if futures[domain].exception()] diff --git a/homeassistant/util/timeout.py b/homeassistant/util/timeout.py index 8c3d38a3700..d8fc3e48fe6 100644 --- a/homeassistant/util/timeout.py +++ b/homeassistant/util/timeout.py @@ -49,14 +49,14 @@ class _GlobalFreezeContext: self._loop.call_soon_threadsafe(self._enter) return self - def __exit__( + def __exit__( # pylint: disable=useless-return self, exc_type: Type[BaseException], exc_val: BaseException, exc_tb: TracebackType, ) -> Optional[bool]: self._loop.call_soon_threadsafe(self._exit) - return True + return None def _enter(self) -> None: """Run freeze.""" @@ -117,14 +117,14 @@ class _ZoneFreezeContext: self._loop.call_soon_threadsafe(self._enter) return self - def __exit__( + def __exit__( # pylint: disable=useless-return self, exc_type: Type[BaseException], exc_val: BaseException, exc_tb: TracebackType, ) -> Optional[bool]: self._loop.call_soon_threadsafe(self._exit) - return True + return None def _enter(self) -> None: """Run freeze.""" @@ -347,6 +347,10 @@ class _ZoneTimeoutManager: self._tasks: List[_ZoneTaskContext] = [] self._freezes: List[_ZoneFreezeContext] = [] + def __repr__(self) -> str: + """Representation of a zone.""" + return f"<{self.name}: {len(self._tasks)} / {len(self._freezes)}>" + @property def name(self) -> str: """Return Zone name.""" diff --git a/tests/util/test_timeout.py b/tests/util/test_timeout.py index edd8f4107a4..39ec8d916bd 100644 --- a/tests/util/test_timeout.py +++ b/tests/util/test_timeout.py @@ -266,3 +266,60 @@ async def test_mix_zone_timeout_trigger_global_cool_down(): pass await asyncio.sleep(0.2) + + +async def test_simple_zone_timeout_freeze_without_timeout_cleanup(hass): + """Test a simple zone timeout freeze on a zone that does not have a timeout set.""" + timeout = TimeoutManager() + + async def background(): + async with timeout.async_freeze("test"): + await asyncio.sleep(0.4) + + async with timeout.async_timeout(0.1): + hass.async_create_task(background()) + await asyncio.sleep(0.2) + + +async def test_simple_zone_timeout_freeze_without_timeout_cleanup2(hass): + """Test a simple zone timeout freeze on a zone that does not have a timeout set.""" + timeout = TimeoutManager() + + async def background(): + async with timeout.async_freeze("test"): + await asyncio.sleep(0.2) + + with pytest.raises(asyncio.TimeoutError): + async with timeout.async_timeout(0.1): + hass.async_create_task(background()) + await asyncio.sleep(0.3) + + +async def test_simple_zone_timeout_freeze_without_timeout_exeption(): + """Test a simple zone timeout freeze on a zone that does not have a timeout set.""" + timeout = TimeoutManager() + + with pytest.raises(asyncio.TimeoutError): + async with timeout.async_timeout(0.1): + try: + async with timeout.async_freeze("test"): + raise RuntimeError() + except RuntimeError: + pass + + await asyncio.sleep(0.4) + + +async def test_simple_zone_timeout_zone_with_timeout_exeption(): + """Test a simple zone timeout freeze on a zone that does not have a timeout set.""" + timeout = TimeoutManager() + + with pytest.raises(asyncio.TimeoutError): + async with timeout.async_timeout(0.1): + try: + async with timeout.async_timeout(0.3, "test"): + raise RuntimeError() + except RuntimeError: + pass + + await asyncio.sleep(0.3) From f771d8ff142a98a47eb25adf726e8771bf2bf36a Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 4 Jan 2021 13:14:07 +0100 Subject: [PATCH 078/507] Change rest sensors update interval for Shelly Motion (#44692) * Change rest sensors update interval for Shelly Motion * Cleaning * Fix typo * Remove unnecessary parentheses --- homeassistant/components/shelly/__init__.py | 12 +++++++++++- homeassistant/components/shelly/const.py | 3 +++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index cd08b625a5d..147d9fb950d 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -24,6 +24,7 @@ from homeassistant.helpers import ( ) from .const import ( + BATTERY_DEVICES_WITH_PERMANENT_CONNECTION, COAP, DATA_CONFIG_ENTRY, DOMAIN, @@ -241,12 +242,21 @@ class ShellyDeviceRestWrapper(update_coordinator.DataUpdateCoordinator): def __init__(self, hass, device: aioshelly.Device): """Initialize the Shelly device wrapper.""" + if ( + device.settings["device"]["type"] + in BATTERY_DEVICES_WITH_PERMANENT_CONNECTION + ): + update_interval = ( + SLEEP_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"] + ) + else: + update_interval = REST_SENSORS_UPDATE_INTERVAL super().__init__( hass, _LOGGER, name=get_device_name(device), - update_interval=timedelta(seconds=REST_SENSORS_UPDATE_INTERVAL), + update_interval=timedelta(seconds=update_interval), ) self.device = device diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index cd747466973..9f5c5b2efc7 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -32,3 +32,6 @@ INPUTS_EVENTS_DICT = { "SL": "single_long", "LS": "long_single", } + +# List of battery devices that maintain a permanent WiFi connection +BATTERY_DEVICES_WITH_PERMANENT_CONNECTION = ["SHMOS-01"] From f07bf6a88e0854bf196d2ba303f668588dcbd7fa Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 4 Jan 2021 14:04:40 +0100 Subject: [PATCH 079/507] Cleanup timeouts values for Shelly (#44790) * Updated timeouts * Small cleanup * Fix + small cleanup of test code --- homeassistant/components/shelly/__init__.py | 6 +- .../components/shelly/config_flow.py | 5 +- homeassistant/components/shelly/const.py | 4 +- tests/components/shelly/test_config_flow.py | 56 +++++++++++-------- 4 files changed, 42 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 147d9fb950d..90dbee29771 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -24,6 +24,7 @@ from homeassistant.helpers import ( ) from .const import ( + AIOSHELLY_DEVICE_TIMEOUT_SEC, BATTERY_DEVICES_WITH_PERMANENT_CONNECTION, COAP, DATA_CONFIG_ENTRY, @@ -32,7 +33,6 @@ from .const import ( POLLING_TIMEOUT_MULTIPLIER, REST, REST_SENSORS_UPDATE_INTERVAL, - SETUP_ENTRY_TIMEOUT_SEC, SLEEP_PERIOD_MULTIPLIER, UPDATE_PERIOD_MULTIPLIER, ) @@ -79,7 +79,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): coap_context = await get_coap_context(hass) try: - async with async_timeout.timeout(SETUP_ENTRY_TIMEOUT_SEC): + async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): device = await aioshelly.Device.create( aiohttp_client.async_get_clientsession(hass), coap_context, @@ -263,7 +263,7 @@ class ShellyDeviceRestWrapper(update_coordinator.DataUpdateCoordinator): async def _async_update_data(self): """Fetch data.""" try: - async with async_timeout.timeout(5): + async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): _LOGGER.debug("REST update for %s", self.name) return await self.device.update_status() except OSError as err: diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index b3dd7bb80fe..b47c76cbb7a 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -18,6 +18,7 @@ from homeassistant.const import ( from homeassistant.helpers import aiohttp_client from . import get_coap_context +from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC from .const import DOMAIN # pylint:disable=unused-import _LOGGER = logging.getLogger(__name__) @@ -39,7 +40,7 @@ async def validate_input(hass: core.HomeAssistant, host, data): ) coap_context = await get_coap_context(hass) - async with async_timeout.timeout(5): + async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): device = await aioshelly.Device.create( aiohttp_client.async_get_clientsession(hass), coap_context, @@ -187,7 +188,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_get_info(self, host): """Get info from shelly device.""" - async with async_timeout.timeout(5): + async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): return await aioshelly.get_info( aiohttp_client.async_get_clientsession(self.hass), host, diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 9f5c5b2efc7..2dfbf067387 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -11,8 +11,8 @@ 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 +# Timeout used for aioshelly calls +AIOSHELLY_DEVICE_TIMEOUT_SEC = 10 # Multiplier used to calculate the "update_interval" for sleeping devices. SLEEP_PERIOD_MULTIPLIER = 1.2 diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index ca62e41abd6..592ac7a384c 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -53,7 +53,7 @@ async def test_form(hass): ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result2["title"] == "Test name" assert result2["data"] == { "host": "1.1.1.1", @@ -68,7 +68,7 @@ async def test_title_without_name(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} settings = MOCK_SETTINGS.copy() @@ -97,7 +97,7 @@ async def test_title_without_name(hass): ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result2["title"] == "shelly1pm-12345" assert result2["data"] == { "host": "1.1.1.1", @@ -111,7 +111,7 @@ async def test_form_auth(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} with patch( @@ -123,7 +123,7 @@ async def test_form_auth(hass): {"host": "1.1.1.1"}, ) - assert result2["type"] == "form" + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} with patch( @@ -145,7 +145,7 @@ async def test_form_auth(hass): ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result3["title"] == "Test name" assert result3["data"] == { "host": "1.1.1.1", @@ -172,7 +172,7 @@ async def test_form_errors_get_info(hass, error): {"host": "1.1.1.1"}, ) - assert result2["type"] == "form" + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["errors"] == {"base": base_error} @@ -194,7 +194,7 @@ async def test_form_errors_test_connection(hass, error): {"host": "1.1.1.1"}, ) - assert result2["type"] == "form" + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["errors"] == {"base": base_error} @@ -219,7 +219,7 @@ async def test_form_already_configured(hass): {"host": "1.1.1.1"}, ) - assert result2["type"] == "abort" + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result2["reason"] == "already_configured" # Test config entry got updated with latest IP @@ -241,16 +241,28 @@ async def test_user_setup_ignored_device(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) + settings = MOCK_SETTINGS.copy() + settings["device"]["type"] = "SHSW-1" + settings["fw"] = "20201124-092534/v1.9.0@57ac4ad8" + with patch( "aioshelly.get_info", return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, + ), patch( + "aioshelly.Device.create", + new=AsyncMock( + return_value=Mock( + settings=settings, + ) + ), ): + result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY # Test config entry got updated with latest IP assert entry.data["host"] == "1.1.1.1" @@ -268,7 +280,7 @@ async def test_form_firmware_unsupported(hass): {"host": "1.1.1.1"}, ) - assert result2["type"] == "abort" + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result2["reason"] == "unsupported_firmware" @@ -302,7 +314,7 @@ async def test_form_auth_errors_test_connection(hass, error): result2["flow_id"], {"username": "test username", "password": "test password"}, ) - assert result3["type"] == "form" + assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM assert result3["errors"] == {"base": base_error} @@ -319,7 +331,7 @@ async def test_zeroconf(hass): data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == "form" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} context = next( flow["context"] @@ -346,7 +358,7 @@ async def test_zeroconf(hass): ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result2["title"] == "Test name" assert result2["data"] == { "host": "1.1.1.1", @@ -372,7 +384,7 @@ async def test_zeroconf_confirm_error(hass, error): data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == "form" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} with patch( @@ -384,7 +396,7 @@ async def test_zeroconf_confirm_error(hass, error): {}, ) - assert result2["type"] == "form" + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["errors"] == {"base": base_error} @@ -405,7 +417,7 @@ async def test_zeroconf_already_configured(hass): data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == "abort" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" # Test config entry got updated with latest IP @@ -421,7 +433,7 @@ async def test_zeroconf_firmware_unsupported(hass): context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == "abort" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "unsupported_firmware" @@ -433,7 +445,7 @@ async def test_zeroconf_cannot_connect(hass): data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == "abort" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "cannot_connect" @@ -450,14 +462,14 @@ async def test_zeroconf_require_auth(hass): data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == "form" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) - assert result2["type"] == "form" + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["errors"] == {} with patch( @@ -479,7 +491,7 @@ async def test_zeroconf_require_auth(hass): ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result3["title"] == "Test name" assert result3["data"] == { "host": "1.1.1.1", From 3a32e16f4dc1eee4e3bf38fe10f0a6a15d0292a0 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 4 Jan 2021 14:14:09 +0100 Subject: [PATCH 080/507] Add myself to codeowners for Shelly (#44814) --- CODEOWNERS | 2 +- homeassistant/components/shelly/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 1ec8f6f30f1..924fe98b46a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -396,7 +396,7 @@ homeassistant/components/seven_segments/* @fabaff homeassistant/components/seventeentrack/* @bachya homeassistant/components/sharkiq/* @ajmarks homeassistant/components/shell_command/* @home-assistant/core -homeassistant/components/shelly/* @balloob @bieniu @thecode +homeassistant/components/shelly/* @balloob @bieniu @thecode @chemelli74 homeassistant/components/shiftr/* @fabaff homeassistant/components/shodan/* @fabaff homeassistant/components/sighthound/* @robmarkcole diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 00f9620b3c5..71ee230d83d 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/shelly", "requirements": ["aioshelly==0.5.1"], "zeroconf": [{ "type": "_http._tcp.local.", "name": "shelly*" }], - "codeowners": ["@balloob", "@bieniu", "@thecode"] + "codeowners": ["@balloob", "@bieniu", "@thecode", "@chemelli74"] } From e9f7e67f4c093f2900433df508bf044c4e8dcaec Mon Sep 17 00:00:00 2001 From: On Freund Date: Mon, 4 Jan 2021 16:23:47 +0200 Subject: [PATCH 081/507] Try to fix flaky Risco test (#44788) --- homeassistant/components/risco/__init__.py | 12 +++++-- homeassistant/components/risco/sensor.py | 1 - .../risco/test_alarm_control_panel.py | 10 +++--- tests/components/risco/test_sensor.py | 31 +++++++++---------- tests/components/risco/util.py | 5 ++- 5 files changed, 33 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index 3fb8f19a1db..685fee43adf 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -60,10 +60,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): EVENTS_COORDINATOR: events_coordinator, } - for component in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + async def start_platforms(): + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_setup(entry, component) + for component in PLATFORMS + ] ) + await events_coordinator.async_refresh() + + hass.async_create_task(start_platforms()) return True diff --git a/homeassistant/components/risco/sensor.py b/homeassistant/components/risco/sensor.py index 62ef6643551..43d763a35fa 100644 --- a/homeassistant/components/risco/sensor.py +++ b/homeassistant/components/risco/sensor.py @@ -73,7 +73,6 @@ class RiscoSensor(CoordinatorEntity): self.async_on_remove( self.coordinator.async_add_listener(self._refresh_from_coordinator) ) - await self.coordinator.async_request_refresh() def _refresh_from_coordinator(self): events = self.coordinator.data diff --git a/tests/components/risco/test_alarm_control_panel.py b/tests/components/risco/test_alarm_control_panel.py index 5bde97f8abd..366fc7814c4 100644 --- a/tests/components/risco/test_alarm_control_panel.py +++ b/tests/components/risco/test_alarm_control_panel.py @@ -167,7 +167,7 @@ async def _check_state(hass, alarm, property, state, entity_id, partition_id): async def test_states(hass, two_part_alarm): """Test the various alarm states.""" - await setup_risco(hass, CUSTOM_MAPPING_OPTIONS) + await setup_risco(hass, [], CUSTOM_MAPPING_OPTIONS) assert hass.states.get(FIRST_ENTITY_ID).state == STATE_UNKNOWN for partition_id, entity_id in {0: FIRST_ENTITY_ID, 1: SECOND_ENTITY_ID}.items(): @@ -249,7 +249,7 @@ async def _call_alarm_service(hass, service, entity_id, **kwargs): async def test_sets_custom_mapping(hass, two_part_alarm): """Test settings the various modes when mapping some states.""" - await setup_risco(hass, CUSTOM_MAPPING_OPTIONS) + await setup_risco(hass, [], CUSTOM_MAPPING_OPTIONS) registry = await hass.helpers.entity_registry.async_get_registry() entity = registry.async_get(FIRST_ENTITY_ID) @@ -275,7 +275,7 @@ async def test_sets_custom_mapping(hass, two_part_alarm): async def test_sets_full_custom_mapping(hass, two_part_alarm): """Test settings the various modes when mapping all states.""" - await setup_risco(hass, FULL_CUSTOM_MAPPING) + await setup_risco(hass, [], FULL_CUSTOM_MAPPING) registry = await hass.helpers.entity_registry.async_get_registry() entity = registry.async_get(FIRST_ENTITY_ID) @@ -309,7 +309,7 @@ async def test_sets_full_custom_mapping(hass, two_part_alarm): async def test_sets_with_correct_code(hass, two_part_alarm): """Test settings the various modes when code is required.""" - await setup_risco(hass, {**CUSTOM_MAPPING_OPTIONS, **CODES_REQUIRED_OPTIONS}) + await setup_risco(hass, [], {**CUSTOM_MAPPING_OPTIONS, **CODES_REQUIRED_OPTIONS}) code = {"code": 1234} await _test_service_call( @@ -351,7 +351,7 @@ async def test_sets_with_correct_code(hass, two_part_alarm): async def test_sets_with_incorrect_code(hass, two_part_alarm): """Test settings the various modes when code is required and incorrect.""" - await setup_risco(hass, {**CUSTOM_MAPPING_OPTIONS, **CODES_REQUIRED_OPTIONS}) + await setup_risco(hass, [], {**CUSTOM_MAPPING_OPTIONS, **CODES_REQUIRED_OPTIONS}) code = {"code": 4321} await _test_no_service_call( diff --git a/tests/components/risco/test_sensor.py b/tests/components/risco/test_sensor.py index 09726727901..3d449f10e46 100644 --- a/tests/components/risco/test_sensor.py +++ b/tests/components/risco/test_sensor.py @@ -1,17 +1,19 @@ """Tests for the Risco event sensors.""" -from unittest.mock import MagicMock, patch +from datetime import timedelta +from unittest.mock import MagicMock, PropertyMock, patch from homeassistant.components.risco import ( LAST_EVENT_TIMESTAMP_KEY, CannotConnectError, UnauthorizedError, ) -from homeassistant.components.risco.const import DOMAIN, EVENTS_COORDINATOR +from homeassistant.components.risco.const import DOMAIN +from homeassistant.util import dt -from .util import TEST_CONFIG, setup_risco +from .util import TEST_CONFIG, TEST_SITE_UUID, setup_risco from .util import two_zone_alarm # noqa: F401 -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed ENTITY_IDS = { "Alarm": "sensor.risco_test_site_name_alarm_events", @@ -171,31 +173,28 @@ async def test_setup(hass, two_zone_alarm): # noqa: F811 assert not registry.async_is_registered(id) with patch( - "homeassistant.components.risco.RiscoAPI.get_events", - return_value=TEST_EVENTS, + "homeassistant.components.risco.RiscoAPI.site_uuid", + new_callable=PropertyMock(return_value=TEST_SITE_UUID), ), patch( "homeassistant.components.risco.Store.async_save", ) as save_mock: - entry = await setup_risco(hass) - await hass.async_block_till_done() + await setup_risco(hass, TEST_EVENTS) + for id in ENTITY_IDS.values(): + assert registry.async_is_registered(id) + save_mock.assert_awaited_once_with( {LAST_EVENT_TIMESTAMP_KEY: TEST_EVENTS[0].time} ) + for category, entity_id in ENTITY_IDS.items(): + _check_state(hass, category, entity_id) - for id in ENTITY_IDS.values(): - assert registry.async_is_registered(id) - - for category, entity_id in ENTITY_IDS.items(): - _check_state(hass, category, entity_id) - - coordinator = hass.data[DOMAIN][entry.entry_id][EVENTS_COORDINATOR] with patch( "homeassistant.components.risco.RiscoAPI.get_events", return_value=[] ) as events_mock, patch( "homeassistant.components.risco.Store.async_load", return_value={LAST_EVENT_TIMESTAMP_KEY: TEST_EVENTS[0].time}, ): - await coordinator.async_refresh() + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=65)) await hass.async_block_till_done() events_mock.assert_awaited_once_with(TEST_EVENTS[0].time, 10) diff --git a/tests/components/risco/util.py b/tests/components/risco/util.py index 009f8c22fb1..8b918f32c12 100644 --- a/tests/components/risco/util.py +++ b/tests/components/risco/util.py @@ -17,7 +17,7 @@ TEST_SITE_UUID = "test-site-uuid" TEST_SITE_NAME = "test-site-name" -async def setup_risco(hass, options={}): +async def setup_risco(hass, events=[], options={}): """Set up a Risco integration for testing.""" config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG, options=options) config_entry.add_to_hass(hass) @@ -33,6 +33,9 @@ async def setup_risco(hass, options={}): new_callable=PropertyMock(return_value=TEST_SITE_NAME), ), patch( "homeassistant.components.risco.RiscoAPI.close" + ), patch( + "homeassistant.components.risco.RiscoAPI.get_events", + return_value=events, ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() From c1027cace6842305ff025594c0efa693f8b4d458 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 4 Jan 2021 15:31:10 +0100 Subject: [PATCH 082/507] Add device entry id to events (#44407) * Add device entry id to events * Only add device id if device is known for now --- homeassistant/components/rfxtrx/__init__.py | 21 ++++++++++++++++++--- tests/components/rfxtrx/test_init.py | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 9eaa705bf3e..be56f4b789c 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -12,6 +12,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA from homeassistant.const import ( + ATTR_DEVICE_ID, CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_DEVICE, @@ -37,6 +38,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceRegistry from homeassistant.helpers.restore_state import RestoreEntity from .const import ( @@ -275,6 +277,10 @@ async def async_setup_internal(hass, entry: config_entries.ConfigEntry): # Setup some per device config devices = _get_device_lookup(config[CONF_DEVICES]) + device_registry: DeviceRegistry = ( + await hass.helpers.device_registry.async_get_registry() + ) + # Declare the Handle event @callback def async_handle_receive(event): @@ -297,9 +303,18 @@ async def async_setup_internal(hass, entry: config_entries.ConfigEntry): data_bits = get_device_data_bits(event.device, devices) device_id = get_device_id(event.device, data_bits=data_bits) - # Register new devices - if config[CONF_AUTOMATIC_ADD] and device_id not in devices: - _add_device(event, device_id) + if device_id not in devices: + if config[CONF_AUTOMATIC_ADD]: + _add_device(event, device_id) + else: + return + + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, *device_id)}, + connections=set(), + ) + if device_entry: + event_data[ATTR_DEVICE_ID] = device_entry.id # Callback to HA registered components. hass.helpers.dispatcher.async_dispatcher_send(SIGNAL_EVENT, event, device_id) diff --git a/tests/components/rfxtrx/test_init.py b/tests/components/rfxtrx/test_init.py index c2e1870638f..1480aa300d0 100644 --- a/tests/components/rfxtrx/test_init.py +++ b/tests/components/rfxtrx/test_init.py @@ -5,6 +5,10 @@ from unittest.mock import call from homeassistant.components.rfxtrx import DOMAIN from homeassistant.components.rfxtrx.const import EVENT_RFXTRX_EVENT from homeassistant.core import callback +from homeassistant.helpers.device_registry import ( + DeviceRegistry, + async_get_registry as async_get_device_registry, +) from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -75,6 +79,8 @@ async def test_fire_event(hass, rfxtrx): await hass.async_block_till_done() await hass.async_start() + device_registry: DeviceRegistry = await async_get_device_registry(hass) + calls = [] @callback @@ -88,6 +94,16 @@ async def test_fire_event(hass, rfxtrx): await rfxtrx.signal("0b1100cd0213c7f210010f51") await rfxtrx.signal("0716000100900970") + device_id_1 = device_registry.async_get_device( + identifiers={("rfxtrx", "11", "0", "213c7f2:16")}, connections=set() + ) + assert device_id_1 + + device_id_2 = device_registry.async_get_device( + identifiers={("rfxtrx", "16", "0", "00:90")}, connections=set() + ) + assert device_id_2 + assert calls == [ { "packet_type": 17, @@ -96,6 +112,7 @@ async def test_fire_event(hass, rfxtrx): "id_string": "213c7f2:16", "data": "0b1100cd0213c7f210010f51", "values": {"Command": "On", "Rssi numeric": 5}, + "device_id": device_id_1.id, }, { "packet_type": 22, @@ -104,6 +121,7 @@ async def test_fire_event(hass, rfxtrx): "id_string": "00:90", "data": "0716000100900970", "values": {"Command": "Chime", "Rssi numeric": 7, "Sound": 9}, + "device_id": device_id_2.id, }, ] From c92353088c037942f2de67ded7030f62ce92f062 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 4 Jan 2021 07:43:41 -0800 Subject: [PATCH 083/507] Fix Fan support in nest climate by adding HVAC_MODE_FAN_ONLY support (#44203) * Add HVAC_MODE_FAN_ONLY to nest climate * Remove unreachable code * Fix HVAC_FAV_ONLY bug; must also turn off hvac --- homeassistant/components/nest/climate_sdm.py | 14 ++- tests/components/nest/climate_sdm_test.py | 93 +++++++++++++++++++- tests/components/nest/conftest.py | 20 ++--- 3 files changed, 112 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index 08cb0161bd9..6413b2e0dfe 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -23,6 +23,7 @@ from homeassistant.components.climate.const import ( FAN_ON, HVAC_MODE_AUTO, HVAC_MODE_COOL, + HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, @@ -188,11 +189,14 @@ class ThermostatEntity(ClimateEntity): @property def hvac_mode(self): """Return the current operation (e.g. heat, cool, idle).""" + hvac_mode = HVAC_MODE_OFF if ThermostatModeTrait.NAME in self._device.traits: trait = self._device.traits[ThermostatModeTrait.NAME] if trait.mode in THERMOSTAT_MODE_MAP: - return THERMOSTAT_MODE_MAP[trait.mode] - return HVAC_MODE_OFF + hvac_mode = THERMOSTAT_MODE_MAP[trait.mode] + if hvac_mode == HVAC_MODE_OFF and self.fan_mode == FAN_ON: + hvac_mode = HVAC_MODE_FAN_ONLY + return hvac_mode @property def hvac_modes(self): @@ -201,6 +205,8 @@ class ThermostatEntity(ClimateEntity): for mode in self._get_device_hvac_modes: if mode in THERMOSTAT_MODE_MAP: supported_modes.append(THERMOSTAT_MODE_MAP[mode]) + if self.supported_features & SUPPORT_FAN_MODE: + supported_modes.append(HVAC_MODE_FAN_ONLY) return supported_modes @property @@ -280,6 +286,10 @@ class ThermostatEntity(ClimateEntity): """Set new target hvac mode.""" if hvac_mode not in self.hvac_modes: raise ValueError(f"Unsupported hvac_mode '{hvac_mode}'") + if hvac_mode == HVAC_MODE_FAN_ONLY: + # Turn the fan on but also turn off the hvac if it is on + await self.async_set_fan_mode(FAN_ON) + hvac_mode = HVAC_MODE_OFF api_mode = THERMOSTAT_INV_MODE_MAP[hvac_mode] trait = self._device.traits[ThermostatModeTrait.NAME] await trait.set_mode(api_mode) diff --git a/tests/components/nest/climate_sdm_test.py b/tests/components/nest/climate_sdm_test.py index 886b67f8e2a..43a422e223e 100644 --- a/tests/components/nest/climate_sdm_test.py +++ b/tests/components/nest/climate_sdm_test.py @@ -27,6 +27,7 @@ from homeassistant.components.climate.const import ( FAN_ON, HVAC_MODE_COOL, HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, @@ -699,6 +700,7 @@ async def test_thermostat_fan_off(hass): HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_HEAT_COOL, + HVAC_MODE_FAN_ONLY, HVAC_MODE_OFF, } assert thermostat.attributes[ATTR_FAN_MODE] == FAN_OFF @@ -730,13 +732,49 @@ async def test_thermostat_fan_on(hass): assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_OFF + assert thermostat.state == HVAC_MODE_FAN_ONLY assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2 assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_HEAT_COOL, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_OFF, + } + assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON + assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] + + +async def test_thermostat_cool_with_fan(hass): + """Test a thermostat cooling while the fan is on.""" + await setup_climate( + hass, + { + "sdm.devices.traits.Fan": { + "timerMode": "ON", + "timerTimeout": "2019-05-10T03:22:54Z", + }, + "sdm.devices.traits.ThermostatHvac": { + "status": "OFF", + }, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], + "mode": "COOL", + }, + }, + ) + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == HVAC_MODE_COOL + assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_FAN_ONLY, HVAC_MODE_OFF, } assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON @@ -766,7 +804,7 @@ async def test_thermostat_set_fan(hass, auth): assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_OFF + assert thermostat.state == HVAC_MODE_FAN_ONLY assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] @@ -845,13 +883,14 @@ async def test_thermostat_invalid_fan_mode(hass): assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_OFF + assert thermostat.state == HVAC_MODE_FAN_ONLY assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2 assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_HEAT_COOL, + HVAC_MODE_FAN_ONLY, HVAC_MODE_OFF, } assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON @@ -862,6 +901,54 @@ async def test_thermostat_invalid_fan_mode(hass): await hass.async_block_till_done() +async def test_thermostat_set_hvac_fan_only(hass, auth): + """Test a thermostat enabling the fan via hvac_mode.""" + await setup_climate( + hass, + { + "sdm.devices.traits.Fan": { + "timerMode": "OFF", + "timerTimeout": "2019-05-10T03:22:54Z", + }, + "sdm.devices.traits.ThermostatHvac": { + "status": "OFF", + }, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], + "mode": "OFF", + }, + }, + auth=auth, + ) + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == HVAC_MODE_OFF + assert thermostat.attributes[ATTR_FAN_MODE] == FAN_OFF + assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] + + await common.async_set_hvac_mode(hass, HVAC_MODE_FAN_ONLY) + await hass.async_block_till_done() + + assert len(auth.captured_requests) == 2 + + (method, url, json) = auth.captured_requests.pop(0) + assert method == "post" + assert url == "some-device-id:executeCommand" + assert json == { + "command": "sdm.devices.commands.Fan.SetTimer", + "params": {"timerMode": "ON"}, + } + (method, url, json) = auth.captured_requests.pop(0) + assert method == "post" + assert url == "some-device-id:executeCommand" + assert json == { + "command": "sdm.devices.commands.ThermostatMode.SetMode", + "params": {"mode": "OFF"}, + } + + async def test_thermostat_target_temp(hass, auth): """Test a thermostat changing hvac modes and affected on target temps.""" subscriber = await setup_climate( diff --git a/tests/components/nest/conftest.py b/tests/components/nest/conftest.py index 7e183ab9c82..4ab780f57e6 100644 --- a/tests/components/nest/conftest.py +++ b/tests/components/nest/conftest.py @@ -14,19 +14,18 @@ class FakeAuth(AbstractAuth): 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) + # Tests can set fake responses here. + self.responses = [] + # The last request is recorded here. + self.method = None + self.url = None + self.json = None + self.captured_requests = [] + # Set up by fixture + self.client = None async def async_get_access_token(self) -> str: """Return a valid access token.""" @@ -37,6 +36,7 @@ class FakeAuth(AbstractAuth): self.method = method self.url = url self.json = json + self.captured_requests.append((method, url, json)) return await self.client.get("/") async def response_handler(self, request): From de780c6d35869515cd85a82db7130ed6354e8736 Mon Sep 17 00:00:00 2001 From: JeromeHXP Date: Mon, 4 Jan 2021 17:09:01 +0100 Subject: [PATCH 084/507] Add Ondilo ico integration (#44728) * First implementationof Ondilo component support * Update manifest toadd pypi pkg dependency * Update entities name and corrected refresh issue * Changed percentage unit name * Corrected merge issues * Updated coveragerc * cleaned up code and corrected config flow tests * Code cleanup and added test for exisitng entry * Changes following PR comments: - Inherit CoordinatorEntity instead of Entity - Merged pools blocking calls into one - Renamed devices vars to sensors - Check supported sensor types - Stop relying on array index position for pools - Stop relying on attribute position in dict for sensors * Corrected unit test * Reformat sensor type check --- .coveragerc | 5 + CODEOWNERS | 1 + .../components/ondilo_ico/__init__.py | 59 ++++++ homeassistant/components/ondilo_ico/api.py | 33 ++++ .../components/ondilo_ico/config_flow.py | 43 ++++ homeassistant/components/ondilo_ico/const.py | 8 + .../components/ondilo_ico/manifest.json | 18 ++ .../components/ondilo_ico/oauth_impl.py | 32 +++ homeassistant/components/ondilo_ico/sensor.py | 185 ++++++++++++++++++ .../components/ondilo_ico/strings.json | 17 ++ .../ondilo_ico/translations/en.json | 17 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/ondilo_ico/__init__.py | 1 + .../components/ondilo_ico/test_config_flow.py | 88 +++++++++ 16 files changed, 514 insertions(+) create mode 100644 homeassistant/components/ondilo_ico/__init__.py create mode 100644 homeassistant/components/ondilo_ico/api.py create mode 100644 homeassistant/components/ondilo_ico/config_flow.py create mode 100644 homeassistant/components/ondilo_ico/const.py create mode 100644 homeassistant/components/ondilo_ico/manifest.json create mode 100644 homeassistant/components/ondilo_ico/oauth_impl.py create mode 100644 homeassistant/components/ondilo_ico/sensor.py create mode 100644 homeassistant/components/ondilo_ico/strings.json create mode 100644 homeassistant/components/ondilo_ico/translations/en.json create mode 100644 tests/components/ondilo_ico/__init__.py create mode 100644 tests/components/ondilo_ico/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 5c85b2553d1..d14bf41195e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -625,6 +625,11 @@ omit = homeassistant/components/omnilogic/__init__.py homeassistant/components/omnilogic/common.py homeassistant/components/omnilogic/sensor.py + homeassistant/components/ondilo_ico/__init__.py + homeassistant/components/ondilo_ico/api.py + homeassistant/components/ondilo_ico/const.py + homeassistant/components/ondilo_ico/oauth_impl.py + homeassistant/components/ondilo_ico/sensor.py homeassistant/components/onkyo/media_player.py homeassistant/components/onvif/__init__.py homeassistant/components/onvif/base.py diff --git a/CODEOWNERS b/CODEOWNERS index 924fe98b46a..8c9ffeea599 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -319,6 +319,7 @@ homeassistant/components/ohmconnect/* @robbiet480 homeassistant/components/ombi/* @larssont homeassistant/components/omnilogic/* @oliver84 @djtimca @gentoosu homeassistant/components/onboarding/* @home-assistant/core +homeassistant/components/ondilo_ico/* @JeromeHXP homeassistant/components/onewire/* @garbled1 @epenet homeassistant/components/onvif/* @hunterjm homeassistant/components/openerz/* @misialq diff --git a/homeassistant/components/ondilo_ico/__init__.py b/homeassistant/components/ondilo_ico/__init__.py new file mode 100644 index 00000000000..69538c5e8b3 --- /dev/null +++ b/homeassistant/components/ondilo_ico/__init__.py @@ -0,0 +1,59 @@ +"""The Ondilo ICO integration.""" +import asyncio + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_entry_oauth2_flow + +from . import api, config_flow +from .const import DOMAIN +from .oauth_impl import OndiloOauth2Implementation + +PLATFORMS = ["sensor"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Ondilo ICO component.""" + hass.data.setdefault(DOMAIN, {}) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Ondilo ICO from a config entry.""" + + config_flow.OAuth2FlowHandler.async_register_implementation( + hass, + OndiloOauth2Implementation(hass), + ) + + implementation = ( + await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) + ) + + hass.data[DOMAIN][entry.entry_id] = api.OndiloClient(hass, entry, implementation) + + 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/ondilo_ico/api.py b/homeassistant/components/ondilo_ico/api.py new file mode 100644 index 00000000000..3de10403211 --- /dev/null +++ b/homeassistant/components/ondilo_ico/api.py @@ -0,0 +1,33 @@ +"""API for Ondilo ICO bound to Home Assistant OAuth.""" +from asyncio import run_coroutine_threadsafe + +from ondilo import Ondilo + +from homeassistant import config_entries, core +from homeassistant.helpers import config_entry_oauth2_flow + + +class OndiloClient(Ondilo): + """Provide Ondilo ICO authentication tied to an OAuth2 based config entry.""" + + def __init__( + self, + hass: core.HomeAssistant, + config_entry: config_entries.ConfigEntry, + implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation, + ): + """Initialize Ondilo ICO Auth.""" + self.hass = hass + self.config_entry = config_entry + self.session = config_entry_oauth2_flow.OAuth2Session( + hass, config_entry, implementation + ) + super().__init__(self.session.token) + + def refresh_tokens(self) -> dict: + """Refresh and return new Ondilo ICO tokens using Home Assistant OAuth2 session.""" + run_coroutine_threadsafe( + self.session.async_ensure_token_valid(), self.hass.loop + ).result() + + return self.session.token diff --git a/homeassistant/components/ondilo_ico/config_flow.py b/homeassistant/components/ondilo_ico/config_flow.py new file mode 100644 index 00000000000..c6a164e913b --- /dev/null +++ b/homeassistant/components/ondilo_ico/config_flow.py @@ -0,0 +1,43 @@ +"""Config flow for Ondilo ICO.""" +import logging + +from homeassistant import config_entries +from homeassistant.helpers import config_entry_oauth2_flow + +from .const import DOMAIN +from .oauth_impl import OndiloOauth2Implementation + +_LOGGER = logging.getLogger(__name__) + + +class OAuth2FlowHandler( + config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN +): + """Config flow to handle Ondilo ICO OAuth2 authentication.""" + + DOMAIN = DOMAIN + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + await self.async_set_unique_id(DOMAIN) + + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + self.async_register_implementation( + self.hass, + OndiloOauth2Implementation(self.hass), + ) + + return await super().async_step_user(user_input) + + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) + + @property + def extra_authorize_data(self) -> dict: + """Extra data that needs to be appended to the authorize url.""" + return {"scope": "api"} diff --git a/homeassistant/components/ondilo_ico/const.py b/homeassistant/components/ondilo_ico/const.py new file mode 100644 index 00000000000..3c947776857 --- /dev/null +++ b/homeassistant/components/ondilo_ico/const.py @@ -0,0 +1,8 @@ +"""Constants for the Ondilo ICO integration.""" + +DOMAIN = "ondilo_ico" + +OAUTH2_AUTHORIZE = "https://interop.ondilo.com/oauth2/authorize" +OAUTH2_TOKEN = "https://interop.ondilo.com/oauth2/token" +OAUTH2_CLIENTID = "customer_api" +OAUTH2_CLIENTSECRET = "" diff --git a/homeassistant/components/ondilo_ico/manifest.json b/homeassistant/components/ondilo_ico/manifest.json new file mode 100644 index 00000000000..55585b2c766 --- /dev/null +++ b/homeassistant/components/ondilo_ico/manifest.json @@ -0,0 +1,18 @@ +{ + "domain": "ondilo_ico", + "name": "Ondilo ICO", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/ondilo_ico", + "requirements": [ + "ondilo==0.2.0" + ], + "ssdp": [], + "zeroconf": [], + "homekit": {}, + "dependencies": [ + "http" + ], + "codeowners": [ + "@JeromeHXP" + ] +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/oauth_impl.py b/homeassistant/components/ondilo_ico/oauth_impl.py new file mode 100644 index 00000000000..d6072cd6f6f --- /dev/null +++ b/homeassistant/components/ondilo_ico/oauth_impl.py @@ -0,0 +1,32 @@ +"""Local implementation of OAuth2 specific to Ondilo to hard code client id and secret and return a proper name.""" + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.config_entry_oauth2_flow import LocalOAuth2Implementation + +from .const import ( + DOMAIN, + OAUTH2_AUTHORIZE, + OAUTH2_CLIENTID, + OAUTH2_CLIENTSECRET, + OAUTH2_TOKEN, +) + + +class OndiloOauth2Implementation(LocalOAuth2Implementation): + """Local implementation of OAuth2 specific to Ondilo to hard code client id and secret and return a proper name.""" + + def __init__(self, hass: HomeAssistant): + """Just init default class with default values.""" + super().__init__( + hass, + DOMAIN, + OAUTH2_CLIENTID, + OAUTH2_CLIENTSECRET, + OAUTH2_AUTHORIZE, + OAUTH2_TOKEN, + ) + + @property + def name(self) -> str: + """Name of the implementation.""" + return "Ondilo" diff --git a/homeassistant/components/ondilo_ico/sensor.py b/homeassistant/components/ondilo_ico/sensor.py new file mode 100644 index 00000000000..4ed8656c456 --- /dev/null +++ b/homeassistant/components/ondilo_ico/sensor.py @@ -0,0 +1,185 @@ +"""Platform for sensor integration.""" +import asyncio +from datetime import timedelta +import logging + +from ondilo import OndiloError + +from homeassistant.const import ( + CONCENTRATION_PARTS_PER_MILLION, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, + TEMP_CELSIUS, +) +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) + +from .const import DOMAIN + +SENSOR_TYPES = { + "temperature": [ + "Temperature", + TEMP_CELSIUS, + "mdi:thermometer", + DEVICE_CLASS_TEMPERATURE, + ], + "orp": ["Oxydo Reduction Potential", "mV", "mdi:pool", None], + "ph": ["pH", "", "mdi:pool", None], + "tds": ["TDS", CONCENTRATION_PARTS_PER_MILLION, "mdi:pool", None], + "battery": ["Battery", PERCENTAGE, "mdi:battery", DEVICE_CLASS_BATTERY], + "rssi": [ + "RSSI", + PERCENTAGE, + "mdi:wifi-strength-2", + DEVICE_CLASS_SIGNAL_STRENGTH, + ], + "salt": ["Salt", "mg/L", "mdi:pool", None], +} + +SCAN_INTERVAL = timedelta(hours=1) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the Ondilo ICO sensors.""" + + api = hass.data[DOMAIN][entry.entry_id] + + def get_all_pool_data(pool): + """Add pool details and last measures to pool data.""" + pool["ICO"] = api.get_ICO_details(pool["id"]) + pool["sensors"] = api.get_last_pool_measures(pool["id"]) + + return pool + + 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: + pools = await hass.async_add_executor_job(api.get_pools) + + return await asyncio.gather( + *[ + hass.async_add_executor_job(get_all_pool_data, pool) + for pool in pools + ] + ) + + except OndiloError as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + # Name of the data. For logging purposes. + name="sensor", + update_method=async_update_data, + # Polling interval. Will only be polled if there are subscribers. + update_interval=SCAN_INTERVAL, + ) + + # Fetch initial data so we have data when entities subscribe + await coordinator.async_refresh() + + entities = [] + for poolidx, pool in enumerate(coordinator.data): + for sensor_idx, sensor in enumerate(pool["sensors"]): + if sensor["data_type"] in SENSOR_TYPES: + entities.append(OndiloICO(coordinator, poolidx, sensor_idx)) + + async_add_entities(entities) + + +class OndiloICO(CoordinatorEntity): + """Representation of a Sensor.""" + + def __init__( + self, coordinator: DataUpdateCoordinator, poolidx: int, sensor_idx: int + ): + """Initialize sensor entity with data from coordinator.""" + super().__init__(coordinator) + + self._poolid = self.coordinator.data[poolidx]["id"] + + pooldata = self._pooldata() + self._data_type = pooldata["sensors"][sensor_idx]["data_type"] + self._unique_id = f"{pooldata['ICO']['serial_number']}-{self._data_type}" + self._device_name = pooldata["name"] + self._name = f"{self._device_name} {SENSOR_TYPES[self._data_type][0]}" + self._device_class = SENSOR_TYPES[self._data_type][3] + self._icon = SENSOR_TYPES[self._data_type][2] + self._unit = SENSOR_TYPES[self._data_type][1] + + def _pooldata(self): + """Get pool data dict.""" + return next( + (pool for pool in self.coordinator.data if pool["id"] == self._poolid), + None, + ) + + def _devdata(self): + """Get device data dict.""" + return next( + ( + data_type + for data_type in self._pooldata()["sensors"] + if data_type["data_type"] == self._data_type + ), + None, + ) + + @property + def name(self): + """Name of the sensor.""" + return self._name + + @property + def state(self): + """Last value of the sensor.""" + _LOGGER.debug( + "Retrieving Ondilo sensor %s state value: %s", + self._name, + self._devdata()["value"], + ) + return self._devdata()["value"] + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self._icon + + @property + def device_class(self): + """Return the device class of the sensor.""" + return self._device_class + + @property + def unit_of_measurement(self): + """Return the Unit of the sensor's measurement.""" + return self._unit + + @property + def unique_id(self): + """Return the unique ID of this entity.""" + return self._unique_id + + @property + def device_info(self): + """Return the device info for the sensor.""" + pooldata = self._pooldata() + return { + "identifiers": {(DOMAIN, pooldata["ICO"]["serial_number"])}, + "name": self._device_name, + "manufacturer": "Ondilo", + "model": "ICO", + "sw_version": pooldata["ICO"]["sw_version"], + } diff --git a/homeassistant/components/ondilo_ico/strings.json b/homeassistant/components/ondilo_ico/strings.json new file mode 100644 index 00000000000..3606bfec5ef --- /dev/null +++ b/homeassistant/components/ondilo_ico/strings.json @@ -0,0 +1,17 @@ +{ + "title": "Ondilo ICO", + "config": { + "step": { + "pick_implementation": { + "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" + } + }, + "abort": { + "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", + "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]" + }, + "create_entry": { + "default": "[%key:common::config_flow::create_entry::authenticated%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/en.json b/homeassistant/components/ondilo_ico/translations/en.json new file mode 100644 index 00000000000..c88a152ef81 --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/en.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Timeout generating authorize URL.", + "missing_configuration": "The component is not configured. Please follow the documentation." + }, + "create_entry": { + "default": "Successfully authenticated" + }, + "step": { + "pick_implementation": { + "title": "Pick Authentication Method" + } + } + }, + "title": "Ondilo ICO" +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index b94b4deee94..43e0647a258 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -140,6 +140,7 @@ FLOWS = [ "nws", "nzbget", "omnilogic", + "ondilo_ico", "onewire", "onvif", "opentherm_gw", diff --git a/requirements_all.txt b/requirements_all.txt index 43f36695a51..316f7f91a55 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1033,6 +1033,9 @@ oemthermostat==1.1.1 # homeassistant.components.omnilogic omnilogic==0.4.2 +# homeassistant.components.ondilo_ico +ondilo==0.2.0 + # homeassistant.components.onkyo onkyo-eiscp==1.2.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9e0f02eeaa3..4e8606308d8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -513,6 +513,9 @@ objgraph==3.4.1 # homeassistant.components.omnilogic omnilogic==0.4.2 +# homeassistant.components.ondilo_ico +ondilo==0.2.0 + # homeassistant.components.onvif onvif-zeep-async==1.0.0 diff --git a/tests/components/ondilo_ico/__init__.py b/tests/components/ondilo_ico/__init__.py new file mode 100644 index 00000000000..12d8d3e2b9f --- /dev/null +++ b/tests/components/ondilo_ico/__init__.py @@ -0,0 +1 @@ +"""Tests for the Ondilo ICO integration.""" diff --git a/tests/components/ondilo_ico/test_config_flow.py b/tests/components/ondilo_ico/test_config_flow.py new file mode 100644 index 00000000000..b7505a85b3d --- /dev/null +++ b/tests/components/ondilo_ico/test_config_flow.py @@ -0,0 +1,88 @@ +"""Test the Ondilo ICO config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.ondilo_ico import config_flow +from homeassistant.components.ondilo_ico.const import ( + DOMAIN, + OAUTH2_AUTHORIZE, + OAUTH2_CLIENTID, + OAUTH2_CLIENTSECRET, + OAUTH2_TOKEN, +) +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.helpers import config_entry_oauth2_flow + +from tests.common import MockConfigEntry + +CLIENT_ID = OAUTH2_CLIENTID +CLIENT_SECRET = OAUTH2_CLIENTSECRET + + +async def test_abort_if_existing_entry(hass): + """Check flow abort when an entry already exist.""" + MockConfigEntry(domain=DOMAIN).add_to_hass(hass) + + flow = config_flow.OAuth2FlowHandler() + flow.hass = hass + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "single_instance_allowed" + + +async def test_full_flow( + hass, aiohttp_client, aioclient_mock, current_request_with_host +): + """Check full flow.""" + assert await setup.async_setup_component( + hass, + DOMAIN, + { + DOMAIN: {CONF_CLIENT_ID: CLIENT_ID, CONF_CLIENT_SECRET: CLIENT_SECRET}, + "http": {"base_url": "https://example.com"}, + }, + ) + + 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"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + "&scope=api" + ) + + client = await aiohttp_client(hass.http.app) + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + with patch( + "homeassistant.components.ondilo_ico.async_setup_entry", return_value=True + ) as mock_setup: + await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert len(mock_setup.mock_calls) == 1 From 766f89f3385882752b0febba7add6ff91cbe1205 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 4 Jan 2021 18:02:46 +0100 Subject: [PATCH 085/507] Adjust system info for lovelace with multiple dashboards (#44796) --- homeassistant/components/lovelace/const.py | 1 + .../components/lovelace/system_health.py | 30 +++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lovelace/const.py b/homeassistant/components/lovelace/const.py index a093c672dd6..e93649de451 100644 --- a/homeassistant/components/lovelace/const.py +++ b/homeassistant/components/lovelace/const.py @@ -16,6 +16,7 @@ DEFAULT_ICON = "hass:view-dashboard" CONF_MODE = "mode" MODE_YAML = "yaml" MODE_STORAGE = "storage" +MODE_AUTO = "auto-gen" LOVELACE_CONFIG_FILE = "ui-lovelace.yaml" CONF_RESOURCES = "resources" diff --git a/homeassistant/components/lovelace/system_health.py b/homeassistant/components/lovelace/system_health.py index e0d1152a049..a148427c9bd 100644 --- a/homeassistant/components/lovelace/system_health.py +++ b/homeassistant/components/lovelace/system_health.py @@ -1,8 +1,10 @@ """Provide info to system health.""" +import asyncio + from homeassistant.components import system_health from homeassistant.core import HomeAssistant, callback -from .const import DOMAIN +from .const import CONF_MODE, DOMAIN, MODE_AUTO, MODE_STORAGE, MODE_YAML @callback @@ -16,6 +18,30 @@ def async_register( async def system_health_info(hass): """Get info for the info page.""" health_info = {"dashboards": len(hass.data[DOMAIN]["dashboards"])} - health_info.update(await hass.data[DOMAIN]["dashboards"][None].async_get_info()) health_info.update(await hass.data[DOMAIN]["resources"].async_get_info()) + + dashboards_info = await asyncio.gather( + *[ + hass.data[DOMAIN]["dashboards"][dashboard].async_get_info() + for dashboard in hass.data[DOMAIN]["dashboards"] + ] + ) + + modes = set() + for dashboard in dashboards_info: + for key in dashboard: + if isinstance(dashboard[key], int): + health_info[key] = health_info.get(key, 0) + dashboard[key] + elif key == CONF_MODE: + modes.add(dashboard[key]) + else: + health_info[key] = dashboard[key] + + if MODE_STORAGE in modes: + health_info[CONF_MODE] = MODE_STORAGE + elif MODE_YAML in modes: + health_info[CONF_MODE] = MODE_YAML + else: + health_info[CONF_MODE] = MODE_AUTO + return health_info From cd756f20b16b24af5b481799d84554fafe8d345c Mon Sep 17 00:00:00 2001 From: JeromeHXP Date: Mon, 4 Jan 2021 19:57:25 +0100 Subject: [PATCH 086/507] Updated Ondilo translation files to remove title (#44824) --- homeassistant/components/ondilo_ico/strings.json | 1 - homeassistant/components/ondilo_ico/translations/en.json | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/ondilo_ico/strings.json b/homeassistant/components/ondilo_ico/strings.json index 3606bfec5ef..7350cc18236 100644 --- a/homeassistant/components/ondilo_ico/strings.json +++ b/homeassistant/components/ondilo_ico/strings.json @@ -1,5 +1,4 @@ { - "title": "Ondilo ICO", "config": { "step": { "pick_implementation": { diff --git a/homeassistant/components/ondilo_ico/translations/en.json b/homeassistant/components/ondilo_ico/translations/en.json index c88a152ef81..e3849fc17a3 100644 --- a/homeassistant/components/ondilo_ico/translations/en.json +++ b/homeassistant/components/ondilo_ico/translations/en.json @@ -12,6 +12,5 @@ "title": "Pick Authentication Method" } } - }, - "title": "Ondilo ICO" + } } \ No newline at end of file From 7657a5c901798629625a357da2b5a3aa5b259ea1 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 4 Jan 2021 12:01:14 -0700 Subject: [PATCH 087/507] Move RainMachine services to entity services (#44139) --- .../components/rainmachine/__init__.py | 121 +----------------- .../components/rainmachine/services.yaml | 35 +++++ .../components/rainmachine/switch.py | 103 +++++++++++++++ 3 files changed, 140 insertions(+), 119 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index c8697adbcd4..98fbdbcf401 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -6,7 +6,6 @@ from functools import partial from regenmaschine import Client from regenmaschine.controller import Controller from regenmaschine.errors import RainMachineError -import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -16,10 +15,9 @@ from homeassistant.const import ( CONF_PORT, CONF_SSL, ) -from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv -from homeassistant.helpers.service import verify_domain_control from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -35,15 +33,10 @@ from .const import ( DATA_RESTRICTIONS_CURRENT, DATA_RESTRICTIONS_UNIVERSAL, DATA_ZONES, - DEFAULT_ZONE_RUN, DOMAIN, LOGGER, ) -CONF_PROGRAM_ID = "program_id" -CONF_SECONDS = "seconds" -CONF_ZONE_ID = "zone_id" - DATA_LISTENER = "listener" DEFAULT_ATTRIBUTION = "Data provided by Green Electronics LLC" @@ -51,29 +44,6 @@ DEFAULT_ICON = "mdi:water" DEFAULT_SSL = True DEFAULT_UPDATE_INTERVAL = timedelta(seconds=15) -SERVICE_ALTER_PROGRAM = vol.Schema({vol.Required(CONF_PROGRAM_ID): cv.positive_int}) - -SERVICE_ALTER_ZONE = vol.Schema({vol.Required(CONF_ZONE_ID): cv.positive_int}) - -SERVICE_PAUSE_WATERING = vol.Schema({vol.Required(CONF_SECONDS): cv.positive_int}) - -SERVICE_START_PROGRAM_SCHEMA = vol.Schema( - {vol.Required(CONF_PROGRAM_ID): cv.positive_int} -) - -SERVICE_START_ZONE_SCHEMA = vol.Schema( - { - vol.Required(CONF_ZONE_ID): cv.positive_int, - vol.Optional(CONF_ZONE_RUN_TIME, default=DEFAULT_ZONE_RUN): cv.positive_int, - } -) - -SERVICE_STOP_PROGRAM_SCHEMA = vol.Schema( - {vol.Required(CONF_PROGRAM_ID): cv.positive_int} -) - -SERVICE_STOP_ZONE_SCHEMA = vol.Schema({vol.Required(CONF_ZONE_ID): cv.positive_int}) - CONFIG_SCHEMA = cv.deprecated(DOMAIN) PLATFORMS = ["binary_sensor", "sensor", "switch"] @@ -125,8 +95,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if entry_updates: hass.config_entries.async_update_entry(entry, **entry_updates) - _verify_domain_control = verify_domain_control(hass, DOMAIN) - websession = aiohttp_client.async_get_clientsession(hass) client = Client(session=websession) @@ -192,92 +160,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_forward_entry_setup(entry, component) ) - @_verify_domain_control - async def disable_program(call: ServiceCall): - """Disable a program.""" - await controller.programs.disable(call.data[CONF_PROGRAM_ID]) - await async_update_programs_and_zones(hass, entry) - - @_verify_domain_control - async def disable_zone(call: ServiceCall): - """Disable a zone.""" - await controller.zones.disable(call.data[CONF_ZONE_ID]) - await async_update_programs_and_zones(hass, entry) - - @_verify_domain_control - async def enable_program(call: ServiceCall): - """Enable a program.""" - await controller.programs.enable(call.data[CONF_PROGRAM_ID]) - await async_update_programs_and_zones(hass, entry) - - @_verify_domain_control - async def enable_zone(call: ServiceCall): - """Enable a zone.""" - await controller.zones.enable(call.data[CONF_ZONE_ID]) - await async_update_programs_and_zones(hass, entry) - - @_verify_domain_control - async def pause_watering(call: ServiceCall): - """Pause watering for a set number of seconds.""" - await controller.watering.pause_all(call.data[CONF_SECONDS]) - await async_update_programs_and_zones(hass, entry) - - @_verify_domain_control - async def start_program(call: ServiceCall): - """Start a particular program.""" - await controller.programs.start(call.data[CONF_PROGRAM_ID]) - await async_update_programs_and_zones(hass, entry) - - @_verify_domain_control - async def start_zone(call: ServiceCall): - """Start a particular zone for a certain amount of time.""" - await controller.zones.start( - call.data[CONF_ZONE_ID], call.data[CONF_ZONE_RUN_TIME] - ) - await async_update_programs_and_zones(hass, entry) - - @_verify_domain_control - async def stop_all(call: ServiceCall): - """Stop all watering.""" - await controller.watering.stop_all() - await async_update_programs_and_zones(hass, entry) - - @_verify_domain_control - async def stop_program(call: ServiceCall): - """Stop a program.""" - await controller.programs.stop(call.data[CONF_PROGRAM_ID]) - await async_update_programs_and_zones(hass, entry) - - @_verify_domain_control - async def stop_zone(call: ServiceCall): - """Stop a zone.""" - await controller.zones.stop(call.data[CONF_ZONE_ID]) - await async_update_programs_and_zones(hass, entry) - - @_verify_domain_control - async def unpause_watering(call: ServiceCall): - """Unpause watering.""" - await controller.watering.unpause_all() - await async_update_programs_and_zones(hass, entry) - - for service, method, schema in [ - ("disable_program", disable_program, SERVICE_ALTER_PROGRAM), - ("disable_zone", disable_zone, SERVICE_ALTER_ZONE), - ("enable_program", enable_program, SERVICE_ALTER_PROGRAM), - ("enable_zone", enable_zone, SERVICE_ALTER_ZONE), - ("pause_watering", pause_watering, SERVICE_PAUSE_WATERING), - ("start_program", start_program, SERVICE_START_PROGRAM_SCHEMA), - ("start_zone", start_zone, SERVICE_START_ZONE_SCHEMA), - ("stop_all", stop_all, {}), - ("stop_program", stop_program, SERVICE_STOP_PROGRAM_SCHEMA), - ("stop_zone", stop_zone, SERVICE_STOP_ZONE_SCHEMA), - ("unpause_watering", unpause_watering, {}), - ]: - hass.services.async_register(DOMAIN, service, method, schema=schema) - - hass.data[DOMAIN][DATA_LISTENER][entry.entry_id] = entry.add_update_listener( - async_reload_entry - ) + hass.data[DOMAIN][DATA_LISTENER] = entry.add_update_listener(async_reload_entry) return True diff --git a/homeassistant/components/rainmachine/services.yaml b/homeassistant/components/rainmachine/services.yaml index e66bd2a1d14..a73dc5c899d 100644 --- a/homeassistant/components/rainmachine/services.yaml +++ b/homeassistant/components/rainmachine/services.yaml @@ -2,42 +2,63 @@ disable_program: description: Disable a program. fields: + entity_id: + description: An entity from the desired RainMachine controller + example: switch.zone_1 program_id: description: The program to disable. example: 3 disable_zone: description: Disable a zone. fields: + entity_id: + description: An entity from the desired RainMachine controller + example: switch.zone_1 zone_id: description: The zone to disable. example: 3 enable_program: description: Enable a program. fields: + entity_id: + description: An entity from the desired RainMachine controller + example: switch.zone_1 program_id: description: The program to enable. example: 3 enable_zone: description: Enable a zone. fields: + entity_id: + description: An entity from the desired RainMachine controller + example: switch.zone_1 zone_id: description: The zone to enable. example: 3 pause_watering: description: Pause all watering for a number of seconds. fields: + entity_id: + description: An entity from the desired RainMachine controller + example: switch.zone_1 seconds: description: The number of seconds to pause. example: 30 start_program: description: Start a program. fields: + entity_id: + description: An entity from the desired RainMachine controller + example: switch.zone_1 program_id: description: The program to start. example: 3 start_zone: description: Start a zone for a set number of seconds. fields: + entity_id: + description: An entity from the desired RainMachine controller + example: switch.zone_1 zone_id: description: The zone to start. example: 3 @@ -46,17 +67,31 @@ start_zone: example: 120 stop_all: description: Stop all watering activities. + fields: + entity_id: + description: An entity from the desired RainMachine controller + example: switch.zone_1 stop_program: description: Stop a program. fields: + entity_id: + description: An entity from the desired RainMachine controller + example: switch.zone_1 program_id: description: The program to stop. example: 3 stop_zone: description: Stop a zone. fields: + entity_id: + description: An entity from the desired RainMachine controller + example: switch.zone_1 zone_id: description: The zone to stop. example: 3 unpause_watering: description: Unpause all watering. + fields: + entity_id: + description: An entity from the desired RainMachine controller + example: switch.zone_1 diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 5c54000a15f..6741abbfc9f 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -4,11 +4,13 @@ from typing import Callable, Coroutine from regenmaschine.controller import Controller from regenmaschine.errors import RequestError +import voluptuous as vol from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ID from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import RainMachineEntity, async_update_programs_and_zones @@ -18,6 +20,7 @@ from .const import ( DATA_COORDINATOR, DATA_PROGRAMS, DATA_ZONES, + DEFAULT_ZONE_RUN, DOMAIN, LOGGER, ) @@ -43,6 +46,10 @@ ATTR_TIME_REMAINING = "time_remaining" ATTR_VEGETATION_TYPE = "vegetation_type" ATTR_ZONES = "zones" +CONF_PROGRAM_ID = "program_id" +CONF_SECONDS = "seconds" +CONF_ZONE_ID = "zone_id" + DAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] RUN_STATUS_MAP = {0: "Not Running", 1: "Running", 2: "Queued"} @@ -103,6 +110,47 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable ) -> None: """Set up RainMachine switches based on a config entry.""" + platform = entity_platform.current_platform.get() + + alter_program_schema = {vol.Required(CONF_PROGRAM_ID): cv.positive_int} + alter_zone_schema = {vol.Required(CONF_ZONE_ID): cv.positive_int} + + for service_name, schema, method in [ + ("disable_program", alter_program_schema, "async_disable_program"), + ("disable_zone", alter_zone_schema, "async_disable_zone"), + ("enable_program", alter_program_schema, "async_enable_program"), + ("enable_zone", alter_zone_schema, "async_enable_zone"), + ( + "pause_watering", + {vol.Required(CONF_SECONDS): cv.positive_int}, + "async_pause_watering", + ), + ( + "start_program", + {vol.Required(CONF_PROGRAM_ID): cv.positive_int}, + "async_start_program", + ), + ( + "start_zone", + { + vol.Required(CONF_ZONE_ID): cv.positive_int, + vol.Optional( + CONF_ZONE_RUN_TIME, default=DEFAULT_ZONE_RUN + ): cv.positive_int, + }, + "async_start_zone", + ), + ("stop_all", {}, "async_stop_all"), + ( + "stop_program", + {vol.Required(CONF_PROGRAM_ID): cv.positive_int}, + "async_stop_program", + ), + ("stop_zone", {vol.Required(CONF_ZONE_ID): cv.positive_int}, "async_stop_zone"), + ("unpause_watering", {}, "async_unpause_watering"), + ]: + platform.async_register_entity_service(service_name, schema, method) + controller = hass.data[DOMAIN][DATA_CONTROLLER][entry.entry_id] programs_coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][ DATA_PROGRAMS @@ -193,6 +241,61 @@ class RainMachineSwitch(RainMachineEntity, SwitchEntity): async_update_programs_and_zones(self.hass, self._entry) ) + async def async_disable_program(self, *, program_id): + """Disable a program.""" + await self._controller.programs.disable(program_id) + await async_update_programs_and_zones(self.hass, self._entry) + + async def async_disable_zone(self, *, zone_id): + """Disable a zone.""" + await self._controller.zones.disable(zone_id) + await async_update_programs_and_zones(self.hass, self._entry) + + async def async_enable_program(self, *, program_id): + """Enable a program.""" + await self._controller.programs.enable(program_id) + await async_update_programs_and_zones(self.hass, self._entry) + + async def async_enable_zone(self, *, zone_id): + """Enable a zone.""" + await self._controller.zones.enable(zone_id) + await async_update_programs_and_zones(self.hass, self._entry) + + async def async_pause_watering(self, *, seconds): + """Pause watering for a set number of seconds.""" + await self._controller.watering.pause_all(seconds) + await async_update_programs_and_zones(self.hass, self._entry) + + async def async_start_program(self, *, program_id): + """Start a particular program.""" + await self._controller.programs.start(program_id) + await async_update_programs_and_zones(self.hass, self._entry) + + async def async_start_zone(self, *, zone_id, zone_run_time): + """Start a particular zone for a certain amount of time.""" + await self._controller.zones.start(zone_id, zone_run_time) + await async_update_programs_and_zones(self.hass, self._entry) + + async def async_stop_all(self): + """Stop all watering.""" + await self._controller.watering.stop_all() + await async_update_programs_and_zones(self.hass, self._entry) + + async def async_stop_program(self, *, program_id): + """Stop a program.""" + await self._controller.programs.stop(program_id) + await async_update_programs_and_zones(self.hass, self._entry) + + async def async_stop_zone(self, *, zone_id): + """Stop a zone.""" + await self._controller.zones.stop(zone_id) + await async_update_programs_and_zones(self.hass, self._entry) + + async def async_unpause_watering(self): + """Unpause watering.""" + await self._controller.watering.unpause_all() + await async_update_programs_and_zones(self.hass, self._entry) + @callback def update_from_latest_data(self) -> None: """Update the state.""" From 773d95251e37a94fb7baabb576a06e526736d402 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 4 Jan 2021 10:18:54 -1000 Subject: [PATCH 088/507] Fix zeroconf outgoing dns compression corruption for large packets (#44828) --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 753ac2a2441..654eec820c3 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.7"], + "requirements": ["zeroconf==0.28.8"], "dependencies": ["api"], "codeowners": ["@bdraco"], "quality_scale": "internal" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 06f2c5a6704..892f5a7552b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -29,7 +29,7 @@ sqlalchemy==1.3.22 voluptuous-serialize==2.4.0 voluptuous==0.12.1 yarl==1.4.2 -zeroconf==0.28.7 +zeroconf==0.28.8 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index 316f7f91a55..b1365094bc2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2339,7 +2339,7 @@ zeep[async]==4.0.0 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.28.7 +zeroconf==0.28.8 # homeassistant.components.zha zha-quirks==0.0.51 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4e8606308d8..7c0ee678a6c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1144,7 +1144,7 @@ yeelight==0.5.4 zeep[async]==4.0.0 # homeassistant.components.zeroconf -zeroconf==0.28.7 +zeroconf==0.28.8 # homeassistant.components.zha zha-quirks==0.0.51 From 76537305e2f4188f3c54f2a831f01af7a53028c5 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Tue, 5 Jan 2021 00:10:42 +0200 Subject: [PATCH 089/507] Add logbook and device trigger platforms to Shelly (#44020) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add logbook and device trigger platforms to Shelly Add `logbook` platform for describing “shelly.click” event Add `device_trigger` platform for adding automation based on click events: Example of logbook event: Shelly 'single' click event for Test I3 channel 3 was fired. (Test I3 is the name of the device) Example of automation triggers: First button triple clicked First button long clicked and then single clicked First button double clicked First button long clicked First button single clicked and then long clicked First button single clicked Second button triple clicked .. Second button single clicked * Fix codespell * Remove pylint added for debug * Add tests * Rebase * Fix Rebase & Apply PR review suggestions Fix tests after rebasing Use `INPUTS_EVENTS_DICT` for input triggers Apply PR suggestions --- homeassistant/components/shelly/__init__.py | 15 +- homeassistant/components/shelly/const.py | 35 ++++ .../components/shelly/device_trigger.py | 110 +++++++++++ homeassistant/components/shelly/logbook.py | 37 ++++ homeassistant/components/shelly/strings.json | 16 ++ .../components/shelly/translations/en.json | 18 +- homeassistant/components/shelly/utils.py | 145 +++++++++++---- tests/components/shelly/conftest.py | 72 +++++++- .../components/shelly/test_device_trigger.py | 173 ++++++++++++++++++ tests/components/shelly/test_logbook.py | 62 +++++++ 10 files changed, 639 insertions(+), 44 deletions(-) create mode 100644 homeassistant/components/shelly/device_trigger.py create mode 100644 homeassistant/components/shelly/logbook.py create mode 100644 tests/components/shelly/test_device_trigger.py create mode 100644 tests/components/shelly/test_logbook.py diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 90dbee29771..d2df03b44a5 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -9,6 +9,7 @@ import async_timeout from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + ATTR_DEVICE_ID, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, @@ -25,10 +26,14 @@ from homeassistant.helpers import ( from .const import ( AIOSHELLY_DEVICE_TIMEOUT_SEC, + ATTR_CHANNEL, + ATTR_CLICK_TYPE, + ATTR_DEVICE, BATTERY_DEVICES_WITH_PERMANENT_CONNECTION, COAP, DATA_CONFIG_ENTRY, DOMAIN, + EVENT_SHELLY_CLICK, INPUTS_EVENTS_DICT, POLLING_TIMEOUT_MULTIPLIER, REST, @@ -170,12 +175,12 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): if event_type in INPUTS_EVENTS_DICT: self.hass.bus.async_fire( - "shelly.click", + EVENT_SHELLY_CLICK, { - "device_id": self.device_id, - "device": self.device.settings["device"]["hostname"], - "channel": channel, - "click_type": INPUTS_EVENTS_DICT[event_type], + ATTR_DEVICE_ID: self.device_id, + ATTR_DEVICE: self.device.settings["device"]["hostname"], + ATTR_CHANNEL: channel, + ATTR_CLICK_TYPE: INPUTS_EVENTS_DICT[event_type], }, ) else: diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 2dfbf067387..b63de2e5fe0 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -35,3 +35,38 @@ INPUTS_EVENTS_DICT = { # List of battery devices that maintain a permanent WiFi connection BATTERY_DEVICES_WITH_PERMANENT_CONNECTION = ["SHMOS-01"] + +EVENT_SHELLY_CLICK = "shelly.click" + +ATTR_CLICK_TYPE = "click_type" +ATTR_CHANNEL = "channel" +ATTR_DEVICE = "device" +CONF_SUBTYPE = "subtype" + +BASIC_INPUTS_EVENTS_TYPES = { + "single", + "long", +} + +SHBTN_1_INPUTS_EVENTS_TYPES = { + "single", + "double", + "triple", + "long", +} + +SUPPORTED_INPUTS_EVENTS_TYPES = SHIX3_1_INPUTS_EVENTS_TYPES = { + "single", + "double", + "triple", + "long", + "single_long", + "long_single", +} + +INPUTS_EVENTS_SUBTYPES = { + "button": 1, + "button1": 1, + "button2": 2, + "button3": 3, +} diff --git a/homeassistant/components/shelly/device_trigger.py b/homeassistant/components/shelly/device_trigger.py new file mode 100644 index 00000000000..f6cdfaee19f --- /dev/null +++ b/homeassistant/components/shelly/device_trigger.py @@ -0,0 +1,110 @@ +"""Provides device triggers for Shelly.""" +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 ( + ATTR_DEVICE_ID, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_EVENT, + CONF_PLATFORM, + CONF_TYPE, +) +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.typing import ConfigType + +from .const import ( + ATTR_CHANNEL, + ATTR_CLICK_TYPE, + CONF_SUBTYPE, + DOMAIN, + EVENT_SHELLY_CLICK, + INPUTS_EVENTS_SUBTYPES, + SUPPORTED_INPUTS_EVENTS_TYPES, +) +from .utils import get_device_wrapper, get_input_triggers + +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): vol.In(SUPPORTED_INPUTS_EVENTS_TYPES), + vol.Required(CONF_SUBTYPE): vol.In(INPUTS_EVENTS_SUBTYPES), + } +) + + +async def async_validate_trigger_config(hass, config): + """Validate config.""" + config = TRIGGER_SCHEMA(config) + + # if device is available verify parameters against device capabilities + wrapper = get_device_wrapper(hass, config[CONF_DEVICE_ID]) + if not wrapper: + return config + + trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) + + for block in wrapper.device.blocks: + input_triggers = get_input_triggers(wrapper.device, block) + if trigger in input_triggers: + return config + + raise InvalidDeviceAutomationConfig( + f"Invalid ({CONF_TYPE},{CONF_SUBTYPE}): {trigger}" + ) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device triggers for Shelly devices.""" + triggers = [] + + wrapper = get_device_wrapper(hass, device_id) + if not wrapper: + raise InvalidDeviceAutomationConfig(f"Device not found: {device_id}") + + for block in wrapper.device.blocks: + input_triggers = get_input_triggers(wrapper.device, block) + + for trigger, subtype in input_triggers: + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_TYPE: trigger, + CONF_SUBTYPE: subtype, + } + ) + + return triggers + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + config = TRIGGER_SCHEMA(config) + event_config = event_trigger.TRIGGER_SCHEMA( + { + event_trigger.CONF_PLATFORM: CONF_EVENT, + event_trigger.CONF_EVENT_TYPE: EVENT_SHELLY_CLICK, + event_trigger.CONF_EVENT_DATA: { + ATTR_DEVICE_ID: config[CONF_DEVICE_ID], + ATTR_CHANNEL: INPUTS_EVENTS_SUBTYPES[config[CONF_SUBTYPE]], + ATTR_CLICK_TYPE: config[CONF_TYPE], + }, + } + ) + event_config = event_trigger.TRIGGER_SCHEMA(event_config) + return await event_trigger.async_attach_trigger( + hass, event_config, action, automation_info, platform_type="device" + ) diff --git a/homeassistant/components/shelly/logbook.py b/homeassistant/components/shelly/logbook.py new file mode 100644 index 00000000000..78a5c279a93 --- /dev/null +++ b/homeassistant/components/shelly/logbook.py @@ -0,0 +1,37 @@ +"""Describe Shelly logbook events.""" + +from homeassistant.const import ATTR_DEVICE_ID +from homeassistant.core import callback + +from .const import ( + ATTR_CHANNEL, + ATTR_CLICK_TYPE, + ATTR_DEVICE, + DOMAIN, + EVENT_SHELLY_CLICK, +) +from .utils import get_device_name, get_device_wrapper + + +@callback +def async_describe_events(hass, async_describe_event): + """Describe logbook events.""" + + @callback + def async_describe_shelly_click_event(event): + """Describe shelly.click logbook event.""" + wrapper = get_device_wrapper(hass, event.data[ATTR_DEVICE_ID]) + if wrapper: + device_name = get_device_name(wrapper.device) + else: + device_name = event.data[ATTR_DEVICE] + + channel = event.data[ATTR_CHANNEL] + click_type = event.data[ATTR_CLICK_TYPE] + + return { + "name": "Shelly", + "message": f"'{click_type}' click event for {device_name} channel {channel} was fired.", + } + + async_describe_event(DOMAIN, EVENT_SHELLY_CLICK, async_describe_shelly_click_event) diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index 6a1cbbd5797..341328801cc 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -27,5 +27,21 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "unsupported_firmware": "The device is using an unsupported firmware version." } + }, + "device_automation":{ + "trigger_subtype": { + "button": "Button", + "button1": "First button", + "button2": "Second button", + "button3": "Third button" + }, + "trigger_type": { + "single": "{subtype} single clicked", + "double": "{subtype} double clicked", + "triple": "{subtype} triple clicked", + "long": " {subtype} long clicked", + "single_long": "{subtype} single clicked and then long clicked", + "long_single": "{subtype} long clicked and then single clicked" + } } } diff --git a/homeassistant/components/shelly/translations/en.json b/homeassistant/components/shelly/translations/en.json index edb6da27a99..a1fa6b72598 100644 --- a/homeassistant/components/shelly/translations/en.json +++ b/homeassistant/components/shelly/translations/en.json @@ -27,5 +27,21 @@ "description": "Before set up, battery-powered devices must be woken up by pressing the button on the device." } } + }, + "device_automation": { + "trigger_subtype": { + "button": "Button", + "button1": "First button", + "button2": "Second button", + "button3": "Third button" + }, + "trigger_type": { + "single": "{subtype} single clicked", + "double": "{subtype} double clicked", + "triple": "{subtype} triple clicked", + "long":" {subtype} long clicked", + "single_long": "{subtype} single clicked and then long clicked", + "long_single": "{subtype} long clicked and then single clicked" + } } -} \ No newline at end of file +} diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 976afdd755b..2a78343440b 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -2,14 +2,22 @@ from datetime import timedelta import logging -from typing import Optional +from typing import List, Optional, Tuple import aioshelly from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.core import HomeAssistant from homeassistant.util.dt import parse_datetime, utcnow -from .const import DOMAIN +from .const import ( + BASIC_INPUTS_EVENTS_TYPES, + COAP, + DATA_CONFIG_ENTRY, + DOMAIN, + SHBTN_1_INPUTS_EVENTS_TYPES, + SHIX3_1_INPUTS_EVENTS_TYPES, +) _LOGGER = logging.getLogger(__name__) @@ -35,54 +43,76 @@ def get_device_name(device: aioshelly.Device) -> str: return device.settings["name"] or device.settings["device"]["hostname"] +def get_number_of_channels(device: aioshelly.Device, block: aioshelly.Block) -> int: + """Get number of channels for block type.""" + 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: + 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 + + return channels or 1 + + def get_entity_name( device: aioshelly.Device, block: aioshelly.Block, description: Optional[str] = None, ) -> str: """Naming for switch and sensors.""" - entity_name = get_device_name(device) - - 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: - 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 - - 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)}" - ) + channel_name = get_device_channel_name(device, block) if description: - entity_name = f"{entity_name} {description}" + return f"{channel_name} {description}" - return entity_name + return channel_name + + +def get_device_channel_name( + device: aioshelly.Device, + block: aioshelly.Block, +) -> str: + """Get name based on device and channel name.""" + entity_name = get_device_name(device) + + if ( + not block + or block.type == "device" + or get_number_of_channels(device, block) == 1 + ): + return entity_name + + channel_name = None + mode = block.type + "s" + if mode in device.settings: + channel_name = device.settings[mode][int(block.channel)].get("name") + + if channel_name: + return channel_name + + if device.settings["device"]["type"] == "SHEM-3": + base = ord("A") + else: + base = ord("1") + + return f"{entity_name} channel {chr(int(block.channel)+base)}" def is_momentary_input(settings: dict, block: aioshelly.Block) -> bool: """Return true if input button settings is set to a momentary type.""" + # Shelly Button type is fixed to momentary and no btn_type + if settings["device"]["type"] == "SHBTN-1": + return True + button = settings.get("relays") or settings.get("lights") or settings.get("inputs") # Shelly 1L has two button settings in the first channel @@ -108,3 +138,44 @@ def get_device_uptime(status: dict, last_uptime: str) -> str: return uptime.replace(microsecond=0).isoformat() return last_uptime + + +def get_input_triggers( + device: aioshelly.Device, block: aioshelly.Block +) -> List[Tuple[str, str]]: + """Return list of input triggers for block.""" + if "inputEvent" not in block.sensor_ids or "inputEventCnt" not in block.sensor_ids: + return [] + + if not is_momentary_input(device.settings, block): + return [] + + triggers = [] + + if block.type == "device" or get_number_of_channels(device, block) == 1: + subtype = "button" + else: + subtype = f"button{int(block.channel)+1}" + + if device.settings["device"]["type"] == "SHBTN-1": + trigger_types = SHBTN_1_INPUTS_EVENTS_TYPES + elif device.settings["device"]["type"] == "SHIX3-1": + trigger_types = SHIX3_1_INPUTS_EVENTS_TYPES + else: + trigger_types = BASIC_INPUTS_EVENTS_TYPES + + for trigger_type in trigger_types: + triggers.append((trigger_type, subtype)) + + return triggers + + +def get_device_wrapper(hass: HomeAssistant, device_id: str): + """Get a Shelly device wrapper for the given device id.""" + for config_entry in hass.data[DOMAIN][DATA_CONFIG_ENTRY]: + wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry][COAP] + + if wrapper.device_id == device_id: + return wrapper + + return None diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index eb19813dc95..6887730b3b1 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -1,11 +1,81 @@ """Test configuration for Shelly.""" -from unittest.mock import patch +from unittest.mock import Mock, patch import pytest +from homeassistant.components.shelly import ShellyDeviceWrapper +from homeassistant.components.shelly.const import ( + COAP, + DATA_CONFIG_ENTRY, + DOMAIN, + EVENT_SHELLY_CLICK, +) +from homeassistant.core import callback as ha_callback +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, async_mock_service, mock_device_registry + +MOCK_SETTINGS = { + "name": "Test name", + "device": { + "mac": "test-mac", + "hostname": "test-host", + "type": "SHSW-25", + "num_outputs": 2, + }, + "coiot": {"update_period": 15}, + "fw": "20201124-092159/v1.9.0@57ac4ad8", + "relays": [{"btn_type": "momentary"}, {"btn_type": "toggle"}], +} + +MOCK_BLOCKS = [ + Mock(sensor_ids={"inputEvent": "S", "inputEventCnt": 2}, channel="0", type="relay") +] + @pytest.fixture(autouse=True) def mock_coap(): """Mock out coap.""" with patch("homeassistant.components.shelly.get_coap_context"): yield + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock service.""" + return async_mock_service(hass, "test", "automation") + + +@pytest.fixture +def events(hass): + """Yield caught shelly_click events.""" + ha_events = [] + hass.bus.async_listen(EVENT_SHELLY_CLICK, ha_callback(ha_events.append)) + yield ha_events + + +@pytest.fixture +async def coap_wrapper(hass): + """Setups a coap wrapper with mocked device.""" + await async_setup_component(hass, "shelly", {}) + + config_entry = MockConfigEntry(domain=DOMAIN, data={}) + config_entry.add_to_hass(hass) + + device = Mock(blocks=MOCK_BLOCKS, settings=MOCK_SETTINGS) + + hass.data[DOMAIN] = {DATA_CONFIG_ENTRY: {}} + hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id] = {} + wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][ + COAP + ] = ShellyDeviceWrapper(hass, config_entry, device) + + await wrapper.async_setup() + + return wrapper diff --git a/tests/components/shelly/test_device_trigger.py b/tests/components/shelly/test_device_trigger.py new file mode 100644 index 00000000000..a725f5a1f30 --- /dev/null +++ b/tests/components/shelly/test_device_trigger.py @@ -0,0 +1,173 @@ +"""The tests for Shelly device triggers.""" +import pytest + +from homeassistant import setup +from homeassistant.components import automation +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) +from homeassistant.components.shelly.const import ( + ATTR_CHANNEL, + ATTR_CLICK_TYPE, + CONF_SUBTYPE, + DOMAIN, + EVENT_SHELLY_CLICK, +) +from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE +from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_get_device_automations, + async_mock_service, +) + + +async def test_get_triggers(hass, coap_wrapper): + """Test we get the expected triggers from a shelly.""" + assert coap_wrapper + expected_triggers = [ + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: coap_wrapper.device_id, + CONF_DOMAIN: DOMAIN, + CONF_TYPE: "single", + CONF_SUBTYPE: "button1", + }, + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: coap_wrapper.device_id, + CONF_DOMAIN: DOMAIN, + CONF_TYPE: "long", + CONF_SUBTYPE: "button1", + }, + ] + + triggers = await async_get_device_automations( + hass, "trigger", coap_wrapper.device_id + ) + + assert_lists_same(triggers, expected_triggers) + + +async def test_get_triggers_for_invalid_device_id(hass, device_reg, coap_wrapper): + """Test error raised for invalid shelly device_id.""" + assert coap_wrapper + config_entry = MockConfigEntry(domain=DOMAIN, data={}) + config_entry.add_to_hass(hass) + invalid_device = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + with pytest.raises(InvalidDeviceAutomationConfig): + await async_get_device_automations(hass, "trigger", invalid_device.id) + + +async def test_if_fires_on_click_event(hass, calls, coap_wrapper): + """Test for click_event trigger firing.""" + assert coap_wrapper + await setup.async_setup_component(hass, "persistent_notification", {}) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: coap_wrapper.device_id, + CONF_TYPE: "single", + CONF_SUBTYPE: "button1", + }, + "action": { + "service": "test.automation", + "data_template": {"some": "test_trigger_single_click"}, + }, + }, + ] + }, + ) + + message = { + CONF_DEVICE_ID: coap_wrapper.device_id, + ATTR_CLICK_TYPE: "single", + ATTR_CHANNEL: 1, + } + hass.bus.async_fire(EVENT_SHELLY_CLICK, message) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data["some"] == "test_trigger_single_click" + + +async def test_validate_trigger_config_no_device(hass, calls, coap_wrapper): + """Test for click_event with no device.""" + assert coap_wrapper + await setup.async_setup_component(hass, "persistent_notification", {}) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: "no_device", + CONF_TYPE: "single", + CONF_SUBTYPE: "button1", + }, + "action": { + "service": "test.automation", + "data_template": {"some": "test_trigger_single_click"}, + }, + }, + ] + }, + ) + message = {CONF_DEVICE_ID: "no_device", ATTR_CLICK_TYPE: "single", ATTR_CHANNEL: 1} + hass.bus.async_fire(EVENT_SHELLY_CLICK, message) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data["some"] == "test_trigger_single_click" + + +async def test_validate_trigger_invalid_triggers(hass, coap_wrapper): + """Test for click_event with invalid triggers.""" + assert coap_wrapper + notification_calls = async_mock_service(hass, "persistent_notification", "create") + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: coap_wrapper.device_id, + CONF_TYPE: "single", + CONF_SUBTYPE: "button3", + }, + "action": { + "service": "test.automation", + "data_template": {"some": "test_trigger_single_click"}, + }, + }, + ] + }, + ) + + assert len(notification_calls) == 1 + assert ( + "The following integrations and platforms could not be set up" + in notification_calls[0].data["message"] + ) diff --git a/tests/components/shelly/test_logbook.py b/tests/components/shelly/test_logbook.py new file mode 100644 index 00000000000..9cfda9ddcaa --- /dev/null +++ b/tests/components/shelly/test_logbook.py @@ -0,0 +1,62 @@ +"""The tests for Shelly logbook.""" +from homeassistant.components import logbook +from homeassistant.components.shelly.const import ( + ATTR_CHANNEL, + ATTR_CLICK_TYPE, + ATTR_DEVICE, + DOMAIN, + EVENT_SHELLY_CLICK, +) +from homeassistant.const import ATTR_DEVICE_ID +from homeassistant.setup import async_setup_component + +from tests.components.logbook.test_init import MockLazyEventPartialState + + +async def test_humanify_shelly_click_event(hass, coap_wrapper): + """Test humanifying Shelly click event.""" + assert coap_wrapper + hass.config.components.add("recorder") + assert await async_setup_component(hass, "logbook", {}) + entity_attr_cache = logbook.EntityAttributeCache(hass) + + event1, event2 = list( + logbook.humanify( + hass, + [ + MockLazyEventPartialState( + EVENT_SHELLY_CLICK, + { + ATTR_DEVICE_ID: coap_wrapper.device_id, + ATTR_DEVICE: "shellyix3-12345678", + ATTR_CLICK_TYPE: "single", + ATTR_CHANNEL: 1, + }, + ), + MockLazyEventPartialState( + EVENT_SHELLY_CLICK, + { + ATTR_DEVICE_ID: "no_device_id", + ATTR_DEVICE: "shellyswitch25-12345678", + ATTR_CLICK_TYPE: "long", + ATTR_CHANNEL: 2, + }, + ), + ], + entity_attr_cache, + {}, + ) + ) + + assert event1["name"] == "Shelly" + assert event1["domain"] == DOMAIN + assert ( + event1["message"] == "'single' click event for Test name channel 1 was fired." + ) + + assert event2["name"] == "Shelly" + assert event2["domain"] == DOMAIN + assert ( + event2["message"] + == "'long' click event for shellyswitch25-12345678 channel 2 was fired." + ) From 2e50c1be8e9594df2c88e1ecb34d7a46d040045c Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 4 Jan 2021 23:14:45 +0100 Subject: [PATCH 090/507] Add nearest method to get data for Airly integration (#44288) * Add nearest method * Add tests * Move urls to consts * Simplify config flow * Fix tests * Update tests * Use in instead get * Fix AirlyError message in tests * Fix manual update entity tests * Clean up tests * Fix after rebase * Increase test coverage * Format the code * Fix after rebase --- homeassistant/components/airly/__init__.py | 23 +++++++-- homeassistant/components/airly/air_quality.py | 21 ++++++--- homeassistant/components/airly/config_flow.py | 47 +++++++++++++------ homeassistant/components/airly/const.py | 1 + homeassistant/components/airly/sensor.py | 4 +- tests/components/airly/__init__.py | 1 + tests/components/airly/test_config_flow.py | 31 +++++++++++- tests/components/airly/test_init.py | 1 + 8 files changed, 102 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index de09d767b1f..9d6b46f82e5 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -20,6 +20,7 @@ from .const import ( ATTR_API_CAQI, ATTR_API_CAQI_DESCRIPTION, ATTR_API_CAQI_LEVEL, + CONF_USE_NEAREST, DOMAIN, MAX_REQUESTS_PER_DAY, NO_AIRLY_SENSORS, @@ -53,6 +54,7 @@ async def async_setup_entry(hass, config_entry): api_key = config_entry.data[CONF_API_KEY] latitude = config_entry.data[CONF_LATITUDE] longitude = config_entry.data[CONF_LONGITUDE] + use_nearest = config_entry.data.get(CONF_USE_NEAREST, False) # For backwards compat, set unique ID if config_entry.unique_id is None: @@ -67,7 +69,7 @@ async def async_setup_entry(hass, config_entry): ) coordinator = AirlyDataUpdateCoordinator( - hass, websession, api_key, latitude, longitude, update_interval + hass, websession, api_key, latitude, longitude, update_interval, use_nearest ) await coordinator.async_refresh() @@ -107,21 +109,36 @@ async def async_unload_entry(hass, config_entry): class AirlyDataUpdateCoordinator(DataUpdateCoordinator): """Define an object to hold Airly data.""" - def __init__(self, hass, session, api_key, latitude, longitude, update_interval): + def __init__( + self, + hass, + session, + api_key, + latitude, + longitude, + update_interval, + use_nearest, + ): """Initialize.""" self.latitude = latitude self.longitude = longitude self.airly = Airly(api_key, session) + self.use_nearest = use_nearest super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) async def _async_update_data(self): """Update data via library.""" data = {} - with async_timeout.timeout(20): + if self.use_nearest: + measurements = self.airly.create_measurements_session_nearest( + self.latitude, self.longitude, max_distance_km=5 + ) + else: measurements = self.airly.create_measurements_session_point( self.latitude, self.longitude ) + with async_timeout.timeout(20): try: await measurements.update() except (AirlyError, ClientConnectorError) as error: diff --git a/homeassistant/components/airly/air_quality.py b/homeassistant/components/airly/air_quality.py index 4a3de1e6543..e43a76b3418 100644 --- a/homeassistant/components/airly/air_quality.py +++ b/homeassistant/components/airly/air_quality.py @@ -87,13 +87,13 @@ class AirlyAirQuality(CoordinatorEntity, AirQualityEntity): @round_state def particulate_matter_2_5(self): """Return the particulate matter 2.5 level.""" - return self.coordinator.data[ATTR_API_PM25] + return self.coordinator.data.get(ATTR_API_PM25) @property @round_state def particulate_matter_10(self): """Return the particulate matter 10 level.""" - return self.coordinator.data[ATTR_API_PM10] + return self.coordinator.data.get(ATTR_API_PM10) @property def attribution(self): @@ -120,12 +120,19 @@ class AirlyAirQuality(CoordinatorEntity, AirQualityEntity): @property def device_state_attributes(self): """Return the state attributes.""" - return { + attrs = { LABEL_AQI_DESCRIPTION: self.coordinator.data[ATTR_API_CAQI_DESCRIPTION], LABEL_ADVICE: self.coordinator.data[ATTR_API_ADVICE], LABEL_AQI_LEVEL: self.coordinator.data[ATTR_API_CAQI_LEVEL], - LABEL_PM_2_5_LIMIT: self.coordinator.data[ATTR_API_PM25_LIMIT], - LABEL_PM_2_5_PERCENT: round(self.coordinator.data[ATTR_API_PM25_PERCENT]), - LABEL_PM_10_LIMIT: self.coordinator.data[ATTR_API_PM10_LIMIT], - LABEL_PM_10_PERCENT: round(self.coordinator.data[ATTR_API_PM10_PERCENT]), } + if ATTR_API_PM25 in self.coordinator.data: + attrs[LABEL_PM_2_5_LIMIT] = self.coordinator.data[ATTR_API_PM25_LIMIT] + attrs[LABEL_PM_2_5_PERCENT] = round( + self.coordinator.data[ATTR_API_PM25_PERCENT] + ) + if ATTR_API_PM10 in self.coordinator.data: + attrs[LABEL_PM_10_LIMIT] = self.coordinator.data[ATTR_API_PM10_LIMIT] + attrs[LABEL_PM_10_PERCENT] = round( + self.coordinator.data[ATTR_API_PM10_PERCENT] + ) + return attrs diff --git a/homeassistant/components/airly/config_flow.py b/homeassistant/components/airly/config_flow.py index 58d6a4295e9..d7636d1db33 100644 --- a/homeassistant/components/airly/config_flow.py +++ b/homeassistant/components/airly/config_flow.py @@ -10,12 +10,17 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, + HTTP_NOT_FOUND, HTTP_UNAUTHORIZED, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from .const import DOMAIN, NO_AIRLY_SENSORS # pylint:disable=unused-import +from .const import ( # pylint:disable=unused-import + CONF_USE_NEAREST, + DOMAIN, + NO_AIRLY_SENSORS, +) class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -27,6 +32,7 @@ class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" errors = {} + use_nearest = False websession = async_get_clientsession(self.hass) @@ -36,23 +42,32 @@ class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) self._abort_if_unique_id_configured() try: - location_valid = await test_location( + location_point_valid = await test_location( websession, user_input["api_key"], user_input["latitude"], user_input["longitude"], ) + if not location_point_valid: + await test_location( + websession, + user_input["api_key"], + user_input["latitude"], + user_input["longitude"], + use_nearest=True, + ) except AirlyError as err: if err.status_code == HTTP_UNAUTHORIZED: errors["base"] = "invalid_api_key" - else: - if not location_valid: + if err.status_code == HTTP_NOT_FOUND: errors["base"] = "wrong_location" - - if not errors: - return self.async_create_entry( - title=user_input[CONF_NAME], data=user_input - ) + else: + if not location_point_valid: + use_nearest = True + return self.async_create_entry( + title=user_input[CONF_NAME], + data={**user_input, CONF_USE_NEAREST: use_nearest}, + ) return self.async_show_form( step_id="user", @@ -74,13 +89,17 @@ class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -async def test_location(client, api_key, latitude, longitude): +async def test_location(client, api_key, latitude, longitude, use_nearest=False): """Return true if location is valid.""" airly = Airly(api_key, client) - measurements = airly.create_measurements_session_point( - latitude=latitude, longitude=longitude - ) - + if use_nearest: + measurements = airly.create_measurements_session_nearest( + latitude=latitude, longitude=longitude, max_distance_km=5 + ) + else: + measurements = airly.create_measurements_session_point( + latitude=latitude, longitude=longitude + ) with async_timeout.timeout(10): await measurements.update() diff --git a/homeassistant/components/airly/const.py b/homeassistant/components/airly/const.py index dc21d68a8d8..b4711b50dd2 100644 --- a/homeassistant/components/airly/const.py +++ b/homeassistant/components/airly/const.py @@ -13,6 +13,7 @@ ATTR_API_PM25_LIMIT = "PM25_LIMIT" ATTR_API_PM25_PERCENT = "PM25_PERCENT" ATTR_API_PRESSURE = "PRESSURE" ATTR_API_TEMPERATURE = "TEMPERATURE" +CONF_USE_NEAREST = "use_nearest" DEFAULT_NAME = "Airly" DOMAIN = "airly" MANUFACTURER = "Airly sp. z o.o." diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index d4f472dfca8..420d11a5963 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -67,7 +67,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): sensors = [] for sensor in SENSOR_TYPES: - sensors.append(AirlySensor(coordinator, name, sensor)) + # When we use the nearest method, we are not sure which sensors are available + if coordinator.data.get(sensor): + sensors.append(AirlySensor(coordinator, name, sensor)) async_add_entities(sensors, False) diff --git a/tests/components/airly/__init__.py b/tests/components/airly/__init__.py index 197864b807c..64f2059857a 100644 --- a/tests/components/airly/__init__.py +++ b/tests/components/airly/__init__.py @@ -3,6 +3,7 @@ from homeassistant.components.airly.const import DOMAIN from tests.common import MockConfigEntry, load_fixture +API_NEAREST_URL = "https://airapi.airly.eu/v2/measurements/nearest?lat=123.000000&lng=456.000000&maxDistanceKM=5.000000" API_POINT_URL = ( "https://airapi.airly.eu/v2/measurements/point?lat=123.000000&lng=456.000000" ) diff --git a/tests/components/airly/test_config_flow.py b/tests/components/airly/test_config_flow.py index 46dc5510b18..5683a06bb28 100644 --- a/tests/components/airly/test_config_flow.py +++ b/tests/components/airly/test_config_flow.py @@ -2,17 +2,18 @@ from airly.exceptions import AirlyError from homeassistant import data_entry_flow -from homeassistant.components.airly.const import DOMAIN +from homeassistant.components.airly.const import CONF_USE_NEAREST, DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, + HTTP_NOT_FOUND, HTTP_UNAUTHORIZED, ) -from . import API_POINT_URL +from . import API_NEAREST_URL, API_POINT_URL from tests.common import MockConfigEntry, load_fixture, patch @@ -54,6 +55,11 @@ async def test_invalid_location(hass, aioclient_mock): """Test that errors are shown when location is invalid.""" aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_no_station.json")) + aioclient_mock.get( + API_NEAREST_URL, + exc=AirlyError(HTTP_NOT_FOUND, {"message": "Installation was not found"}), + ) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) @@ -88,3 +94,24 @@ async def test_create_entry(hass, aioclient_mock): assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] assert result["data"][CONF_API_KEY] == CONFIG[CONF_API_KEY] + assert result["data"][CONF_USE_NEAREST] is False + + +async def test_create_entry_with_nearest_method(hass, aioclient_mock): + """Test that the user step works with nearest method.""" + + aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_no_station.json")) + + aioclient_mock.get(API_NEAREST_URL, text=load_fixture("airly_valid_station.json")) + + with patch("homeassistant.components.airly.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == CONFIG[CONF_NAME] + assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] + assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] + assert result["data"][CONF_API_KEY] == CONFIG[CONF_API_KEY] + assert result["data"][CONF_USE_NEAREST] is True diff --git a/tests/components/airly/test_init.py b/tests/components/airly/test_init.py index cb0ccf268f7..2898bd5c6f6 100644 --- a/tests/components/airly/test_init.py +++ b/tests/components/airly/test_init.py @@ -36,6 +36,7 @@ async def test_config_not_ready(hass, aioclient_mock): "latitude": 123, "longitude": 456, "name": "Home", + "use_nearest": True, }, ) From 60a1948ab0e24e0cf1d9862e87831eb16c708952 Mon Sep 17 00:00:00 2001 From: Mike Keesey Date: Mon, 4 Jan 2021 16:21:14 -0700 Subject: [PATCH 091/507] Generate switches for harmony activities automatically (#42331) * Adding switch code for harmony activities * Working on-off * Removing poll code for now * Async updates for current activity * Update our state based on events * Notifications we got connected or disconnected * Remove unncessary constructor arg * Initial switch tests * Additional tests for switch transitions * Test transitions for availability * Testing switch state changes * Tests passing * Final tests * Updating manifest. * Correctly mock the return value from a call to the library * Adding new subscriber classes * Update class name and location * Got the refactor working locally. * Tests passing * Tracking state changes * Remove write_to_config_file - this appears to never be read. It was added far back in the past to account for a harmony library change, but nothing ever reads that path. Removing that side effect from tests is a pain - avoid the side effect completely. * Connection changes tested * Clean up temporary code * Update .coveragerc for harmony component Specifically exclude untested files instead of the whole module * Fix linting * test sending activity change commands by id * Improving coverage * Testing channel change commands * Splitting subscriber logic into it's own class * Improve coverage and tighten up .coveragerc * Test cleanups. * re-add config file writing for harmony remote * Create fixture for the mock harmonyclient * Reduce duplication in subscription callbacks * use async_run_job to call callbacks * Adding some tests for async behaviors with subscribers. * async_call_later for delay in marking remote unavailable * Test disconnection handling in harmony remote * Early exit if activity not specified * Use connection state mixin * Lint fix after rebase * Fix isort * super init for ConnectionStateMixin * Adding @mkeesey to harmony CODEOWNERS --- .coveragerc | 5 +- CODEOWNERS | 2 +- homeassistant/components/harmony/__init__.py | 24 +- .../components/harmony/connection_state.py | 44 +++ homeassistant/components/harmony/const.py | 2 +- homeassistant/components/harmony/data.py | 251 +++++++++++++++++ .../components/harmony/manifest.json | 3 +- homeassistant/components/harmony/remote.py | 243 +++------------- .../components/harmony/subscriber.py | 77 +++++ homeassistant/components/harmony/switch.py | 87 ++++++ tests/components/harmony/conftest.py | 147 ++++++++++ tests/components/harmony/const.py | 6 + .../harmony/test_activity_changes.py | 137 +++++++++ tests/components/harmony/test_commands.py | 263 ++++++++++++++++++ tests/components/harmony/test_config_flow.py | 42 +-- .../harmony/test_connection_changes.py | 67 +++++ tests/components/harmony/test_subscriber.py | 143 ++++++++++ 17 files changed, 1291 insertions(+), 252 deletions(-) create mode 100644 homeassistant/components/harmony/connection_state.py create mode 100644 homeassistant/components/harmony/data.py create mode 100644 homeassistant/components/harmony/subscriber.py create mode 100644 homeassistant/components/harmony/switch.py create mode 100644 tests/components/harmony/conftest.py create mode 100644 tests/components/harmony/const.py create mode 100644 tests/components/harmony/test_activity_changes.py create mode 100644 tests/components/harmony/test_commands.py create mode 100644 tests/components/harmony/test_connection_changes.py create mode 100644 tests/components/harmony/test_subscriber.py diff --git a/.coveragerc b/.coveragerc index d14bf41195e..b859e229e74 100644 --- a/.coveragerc +++ b/.coveragerc @@ -358,7 +358,10 @@ omit = homeassistant/components/hangouts/hangouts_bot.py homeassistant/components/hangouts/hangups_utils.py homeassistant/components/harman_kardon_avr/media_player.py - homeassistant/components/harmony/* + homeassistant/components/harmony/const.py + homeassistant/components/harmony/data.py + homeassistant/components/harmony/remote.py + homeassistant/components/harmony/util.py homeassistant/components/haveibeenpwned/sensor.py homeassistant/components/hdmi_cec/* homeassistant/components/heatmiser/climate.py diff --git a/CODEOWNERS b/CODEOWNERS index 8c9ffeea599..03912a0dec0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -180,7 +180,7 @@ homeassistant/components/griddy/* @bdraco homeassistant/components/group/* @home-assistant/core homeassistant/components/growatt_server/* @indykoning homeassistant/components/guardian/* @bachya -homeassistant/components/harmony/* @ehendrix23 @bramkragten @bdraco +homeassistant/components/harmony/* @ehendrix23 @bramkragten @bdraco @mkeesey homeassistant/components/hassio/* @home-assistant/supervisor homeassistant/components/hdmi_cec/* @newAM homeassistant/components/heatmiser/* @andylockran diff --git a/homeassistant/components/harmony/__init__.py b/homeassistant/components/harmony/__init__.py index e425b5ce94a..6ba63ee0f81 100644 --- a/homeassistant/components/harmony/__init__.py +++ b/homeassistant/components/harmony/__init__.py @@ -1,11 +1,7 @@ """The Logitech Harmony Hub integration.""" import asyncio -from homeassistant.components.remote import ( - ATTR_ACTIVITY, - ATTR_DELAY_SECS, - DEFAULT_DELAY_SECS, -) +from homeassistant.components.remote import ATTR_ACTIVITY, ATTR_DELAY_SECS from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant, callback @@ -13,7 +9,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import DOMAIN, HARMONY_OPTIONS_UPDATE, PLATFORMS -from .remote import HarmonyRemote +from .data import HarmonyData async def async_setup(hass: HomeAssistant, config: dict): @@ -33,22 +29,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): address = entry.data[CONF_HOST] name = entry.data[CONF_NAME] - activity = entry.options.get(ATTR_ACTIVITY) - delay_secs = entry.options.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS) - - harmony_conf_file = hass.config.path(f"harmony_{entry.unique_id}.conf") + data = HarmonyData(hass, address, name, entry.unique_id) try: - device = HarmonyRemote( - name, entry.unique_id, address, activity, harmony_conf_file, delay_secs - ) - connected_ok = await device.connect() + connected_ok = await data.connect() except (asyncio.TimeoutError, ValueError, AttributeError) as err: raise ConfigEntryNotReady from err if not connected_ok: raise ConfigEntryNotReady - hass.data[DOMAIN][entry.entry_id] = device + hass.data[DOMAIN][entry.entry_id] = data entry.add_update_listener(_update_listener) @@ -92,8 +82,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): ) # Shutdown a harmony remote for removal - device = hass.data[DOMAIN][entry.entry_id] - await device.shutdown() + data = hass.data[DOMAIN][entry.entry_id] + await data.shutdown() if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/harmony/connection_state.py b/homeassistant/components/harmony/connection_state.py new file mode 100644 index 00000000000..9706ba28776 --- /dev/null +++ b/homeassistant/components/harmony/connection_state.py @@ -0,0 +1,44 @@ +"""Mixin class for handling connection state changes.""" +import logging + +from homeassistant.helpers.event import async_call_later + +_LOGGER = logging.getLogger(__name__) + +TIME_MARK_DISCONNECTED = 10 + + +class ConnectionStateMixin: + """Base implementation for connection state handling.""" + + def __init__(self): + """Initialize this mixin instance.""" + super().__init__() + self._unsub_mark_disconnected = None + + async def got_connected(self, _=None): + """Notification that we're connected to the HUB.""" + _LOGGER.debug("%s: connected to the HUB", self._name) + self.async_write_ha_state() + + self._clear_disconnection_delay() + + async def got_disconnected(self, _=None): + """Notification that we're disconnected from the HUB.""" + _LOGGER.debug("%s: disconnected from the HUB", self._name) + # We're going to wait for 10 seconds before announcing we're + # unavailable, this to allow a reconnection to happen. + self._unsub_mark_disconnected = async_call_later( + self.hass, TIME_MARK_DISCONNECTED, self._mark_disconnected_if_unavailable + ) + + def _clear_disconnection_delay(self): + if self._unsub_mark_disconnected: + self._unsub_mark_disconnected() + self._unsub_mark_disconnected = None + + def _mark_disconnected_if_unavailable(self, _): + self._unsub_mark_disconnected = None + if not self.available: + # Still disconnected. Let the state engine know. + self.async_write_ha_state() diff --git a/homeassistant/components/harmony/const.py b/homeassistant/components/harmony/const.py index f6315b57b57..ee4a454847e 100644 --- a/homeassistant/components/harmony/const.py +++ b/homeassistant/components/harmony/const.py @@ -2,7 +2,7 @@ DOMAIN = "harmony" SERVICE_SYNC = "sync" SERVICE_CHANGE_CHANNEL = "change_channel" -PLATFORMS = ["remote"] +PLATFORMS = ["remote", "switch"] UNIQUE_ID = "unique_id" ACTIVITY_POWER_OFF = "PowerOff" HARMONY_OPTIONS_UPDATE = "harmony_options_update" diff --git a/homeassistant/components/harmony/data.py b/homeassistant/components/harmony/data.py new file mode 100644 index 00000000000..6c3ad874fa9 --- /dev/null +++ b/homeassistant/components/harmony/data.py @@ -0,0 +1,251 @@ +"""Harmony data object which contains the Harmony Client.""" + +import logging +from typing import Iterable + +from aioharmony.const import ClientCallbackType, SendCommandDevice +import aioharmony.exceptions as aioexc +from aioharmony.harmonyapi import HarmonyAPI as HarmonyClient + +from .const import ACTIVITY_POWER_OFF +from .subscriber import HarmonySubscriberMixin + +_LOGGER = logging.getLogger(__name__) + + +class HarmonyData(HarmonySubscriberMixin): + """HarmonyData registers for Harmony hub updates.""" + + def __init__(self, hass, address: str, name: str, unique_id: str): + """Initialize a data object.""" + super().__init__(hass) + self._name = name + self._unique_id = unique_id + self._available = False + + callbacks = { + "config_updated": self._config_updated, + "connect": self._connected, + "disconnect": self._disconnected, + "new_activity_starting": self._activity_starting, + "new_activity": self._activity_started, + } + self._client = HarmonyClient( + ip_address=address, callbacks=ClientCallbackType(**callbacks) + ) + + @property + def activity_names(self): + """Names of all the remotes activities.""" + activity_infos = self._client.config.get("activity", []) + activities = [activity["label"] for activity in activity_infos] + + # Remove both ways of representing PowerOff + if None in activities: + activities.remove(None) + if ACTIVITY_POWER_OFF in activities: + activities.remove(ACTIVITY_POWER_OFF) + + return activities + + @property + def device_names(self): + """Names of all of the devices connected to the hub.""" + device_infos = self._client.config.get("device", []) + devices = [device["label"] for device in device_infos] + + return devices + + @property + def name(self): + """Return the Harmony device's name.""" + return self._name + + @property + def unique_id(self): + """Return the Harmony device's unique_id.""" + return self._unique_id + + @property + def json_config(self): + """Return the hub config as json.""" + if self._client.config is None: + return None + return self._client.json_config + + @property + def available(self) -> bool: + """Return if connected to the hub.""" + return self._available + + @property + def current_activity(self) -> tuple: + """Return the current activity tuple.""" + return self._client.current_activity + + def device_info(self, domain: str): + """Return hub device info.""" + model = "Harmony Hub" + if "ethernetStatus" in self._client.hub_config.info: + model = "Harmony Hub Pro 2400" + return { + "identifiers": {(domain, self.unique_id)}, + "manufacturer": "Logitech", + "sw_version": self._client.hub_config.info.get( + "hubSwVersion", self._client.fw_version + ), + "name": self.name, + "model": model, + } + + async def connect(self) -> bool: + """Connect to the Harmony Hub.""" + _LOGGER.debug("%s: Connecting", self._name) + try: + if not await self._client.connect(): + _LOGGER.warning("%s: Unable to connect to HUB", self._name) + await self._client.close() + return False + except aioexc.TimeOut: + _LOGGER.warning("%s: Connection timed-out", self._name) + return False + return True + + async def shutdown(self): + """Close connection on shutdown.""" + _LOGGER.debug("%s: Closing Harmony Hub", self._name) + try: + await self._client.close() + except aioexc.TimeOut: + _LOGGER.warning("%s: Disconnect timed-out", self._name) + + async def async_start_activity(self, activity: str): + """Start an activity from the Harmony device.""" + + if not activity: + _LOGGER.error("%s: No activity specified with turn_on service", self.name) + return + + activity_id = None + activity_name = None + + if activity.isdigit() or activity == "-1": + _LOGGER.debug("%s: Activity is numeric", self.name) + activity_name = self._client.get_activity_name(int(activity)) + if activity_name: + activity_id = activity + + if activity_id is None: + _LOGGER.debug("%s: Find activity ID based on name", self.name) + activity_name = str(activity) + activity_id = self._client.get_activity_id(activity_name) + + if activity_id is None: + _LOGGER.error("%s: Activity %s is invalid", self.name, activity) + return + + _, current_activity_name = self.current_activity + if current_activity_name == activity_name: + # Automations or HomeKit may turn the device on multiple times + # when the current activity is already active which will cause + # harmony to loose state. This behavior is unexpected as turning + # the device on when its already on isn't expected to reset state. + _LOGGER.debug( + "%s: Current activity is already %s", self.name, activity_name + ) + return + + try: + await self._client.start_activity(activity_id) + except aioexc.TimeOut: + _LOGGER.error("%s: Starting activity %s timed-out", self.name, activity) + + async def async_power_off(self): + """Start the PowerOff activity.""" + _LOGGER.debug("%s: Turn Off", self.name) + try: + await self._client.power_off() + except aioexc.TimeOut: + _LOGGER.error("%s: Powering off timed-out", self.name) + + async def async_send_command( + self, + commands: Iterable[str], + device: str, + num_repeats: int, + delay_secs: float, + hold_secs: float, + ): + """Send a list of commands to one device.""" + device_id = None + if device.isdigit(): + _LOGGER.debug("%s: Device %s is numeric", self.name, device) + if self._client.get_device_name(int(device)): + device_id = device + + if device_id is None: + _LOGGER.debug( + "%s: Find device ID %s based on device name", self.name, device + ) + device_id = self._client.get_device_id(str(device).strip()) + + if device_id is None: + _LOGGER.error("%s: Device %s is invalid", self.name, device) + return + + _LOGGER.debug( + "Sending commands to device %s holding for %s seconds " + "with a delay of %s seconds", + device, + hold_secs, + delay_secs, + ) + + # Creating list of commands to send. + snd_cmnd_list = [] + for _ in range(num_repeats): + for single_command in commands: + send_command = SendCommandDevice( + device=device_id, command=single_command, delay=hold_secs + ) + snd_cmnd_list.append(send_command) + if delay_secs > 0: + snd_cmnd_list.append(float(delay_secs)) + + _LOGGER.debug("%s: Sending commands", self.name) + try: + result_list = await self._client.send_commands(snd_cmnd_list) + except aioexc.TimeOut: + _LOGGER.error("%s: Sending commands timed-out", self.name) + return + + for result in result_list: + _LOGGER.error( + "Sending command %s to device %s failed with code %s: %s", + result.command.command, + result.command.device, + result.code, + result.msg, + ) + + async def change_channel(self, channel: int): + """Change the channel using Harmony remote.""" + _LOGGER.debug("%s: Changing channel to %s", self.name, channel) + try: + await self._client.change_channel(channel) + except aioexc.TimeOut: + _LOGGER.error("%s: Changing channel to %s timed-out", self.name, channel) + + async def sync(self) -> bool: + """Sync the Harmony device with the web service. + + Returns True if the sync was successful. + """ + _LOGGER.debug("%s: Syncing hub with Harmony cloud", self.name) + try: + await self._client.sync() + except aioexc.TimeOut: + _LOGGER.error("%s: Syncing hub with Harmony cloud timed-out", self.name) + return False + else: + return True diff --git a/homeassistant/components/harmony/manifest.json b/homeassistant/components/harmony/manifest.json index 4d8b83f4643..7509f3d4f4d 100644 --- a/homeassistant/components/harmony/manifest.json +++ b/homeassistant/components/harmony/manifest.json @@ -3,12 +3,13 @@ "name": "Logitech Harmony Hub", "documentation": "https://www.home-assistant.io/integrations/harmony", "requirements": ["aioharmony==0.2.6"], - "codeowners": ["@ehendrix23", "@bramkragten", "@bdraco"], + "codeowners": ["@ehendrix23", "@bramkragten", "@bdraco", "@mkeesey"], "ssdp": [ { "manufacturer": "Logitech", "deviceType": "urn:myharmony-com:device:harmony:1" } ], + "dependencies": ["remote", "switch"], "config_flow": true } diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index ff7825013e8..b9205a4befb 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -1,11 +1,7 @@ """Support for Harmony Hub devices.""" -import asyncio import json import logging -from aioharmony.const import ClientCallbackType -import aioharmony.exceptions as aioexc -from aioharmony.harmonyapi import HarmonyAPI as HarmonyClient, SendCommandDevice import voluptuous as vol from homeassistant.components import remote @@ -27,6 +23,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.restore_state import RestoreEntity +from .connection_state import ConnectionStateMixin from .const import ( ACTIVITY_POWER_OFF, ATTR_ACTIVITY_LIST, @@ -41,12 +38,12 @@ from .const import ( SERVICE_SYNC, UNIQUE_ID, ) +from .subscriber import HarmonyCallback from .util import ( find_best_name_for_remote, find_matching_config_entries_for_host, find_unique_id_for_remote, get_harmony_client_if_available, - list_names_from_hublist, ) _LOGGER = logging.getLogger(__name__) @@ -113,10 +110,15 @@ async def async_setup_entry( ): """Set up the Harmony config entry.""" - device = hass.data[DOMAIN][entry.entry_id] + data = hass.data[DOMAIN][entry.entry_id] - _LOGGER.debug("Harmony Remote: %s", device) + _LOGGER.debug("HarmonyData : %s", data) + default_activity = entry.options.get(ATTR_ACTIVITY) + delay_secs = entry.options.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS) + + harmony_conf_file = hass.config.path(f"harmony_{entry.unique_id}.conf") + device = HarmonyRemote(data, default_activity, delay_secs, harmony_conf_file) async_add_entities([device]) platform = entity_platform.current_platform.get() @@ -131,37 +133,23 @@ async def async_setup_entry( ) -class HarmonyRemote(remote.RemoteEntity, RestoreEntity): +class HarmonyRemote(ConnectionStateMixin, remote.RemoteEntity, RestoreEntity): """Remote representation used to control a Harmony device.""" - def __init__(self, name, unique_id, host, activity, out_path, delay_secs): + def __init__(self, data, activity, delay_secs, out_path): """Initialize HarmonyRemote class.""" - self._name = name - self.host = host + super().__init__() + self._data = data + self._name = data.name self._state = None self._current_activity = ACTIVITY_POWER_OFF self.default_activity = activity self._activity_starting = None self._is_initial_update = True - self._client = HarmonyClient(ip_address=host) - self._config_path = out_path self.delay_secs = delay_secs - self._available = False - self._unique_id = unique_id + self._unique_id = data.unique_id self._last_activity = None - - @property - def activity_names(self): - """Names of all the remotes activities.""" - activities = [activity["label"] for activity in self._client.config["activity"]] - - # Remove both ways of representing PowerOff - if None in activities: - activities.remove(None) - if ACTIVITY_POWER_OFF in activities: - activities.remove(ACTIVITY_POWER_OFF) - - return activities + self._config_path = out_path async def _async_update_options(self, data): """Change options when the options flow does.""" @@ -171,15 +159,16 @@ class HarmonyRemote(remote.RemoteEntity, RestoreEntity): if ATTR_ACTIVITY in data: self.default_activity = data[ATTR_ACTIVITY] - def _update_callbacks(self): + def _setup_callbacks(self): callbacks = { + "connected": self.got_connected, + "disconnected": self.got_disconnected, "config_updated": self.new_config, - "connect": self.got_connected, - "disconnect": self.got_disconnected, - "new_activity_starting": self.new_activity, - "new_activity": self._new_activity_finished, + "activity_starting": self.new_activity, + "activity_started": self._new_activity_finished, } - self._client.callbacks = ClientCallbackType(**callbacks) + + self.async_on_remove(self._data.async_subscribe(HarmonyCallback(**callbacks))) def _new_activity_finished(self, activity_info: tuple) -> None: """Call for finished updated current activity.""" @@ -191,8 +180,9 @@ class HarmonyRemote(remote.RemoteEntity, RestoreEntity): await super().async_added_to_hass() _LOGGER.debug("%s: Harmony Hub added", self._name) - # Register the callbacks - self._update_callbacks() + + self.async_on_remove(self._clear_disconnection_delay) + self._setup_callbacks() self.async_on_remove( async_dispatcher_connect( @@ -219,29 +209,10 @@ class HarmonyRemote(remote.RemoteEntity, RestoreEntity): self._last_activity = last_state.attributes[ATTR_LAST_ACTIVITY] - async def shutdown(self): - """Close connection on shutdown.""" - _LOGGER.debug("%s: Closing Harmony Hub", self._name) - try: - await self._client.close() - except aioexc.TimeOut: - _LOGGER.warning("%s: Disconnect timed-out", self._name) - @property def device_info(self): """Return device info.""" - model = "Harmony Hub" - if "ethernetStatus" in self._client.hub_config.info: - model = "Harmony Hub Pro 2400" - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "manufacturer": "Logitech", - "sw_version": self._client.hub_config.info.get( - "hubSwVersion", self._client.fw_version - ), - "name": self.name, - "model": model, - } + self._data.device_info(DOMAIN) @property def unique_id(self): @@ -264,10 +235,8 @@ class HarmonyRemote(remote.RemoteEntity, RestoreEntity): return { ATTR_ACTIVITY_STARTING: self._activity_starting, ATTR_CURRENT_ACTIVITY: self._current_activity, - ATTR_ACTIVITY_LIST: list_names_from_hublist( - self._client.hub_config.activities - ), - ATTR_DEVICES_LIST: list_names_from_hublist(self._client.hub_config.devices), + ATTR_ACTIVITY_LIST: self._data.activity_names, + ATTR_DEVICES_LIST: self._data.device_names, ATTR_LAST_ACTIVITY: self._last_activity, } @@ -279,20 +248,7 @@ class HarmonyRemote(remote.RemoteEntity, RestoreEntity): @property def available(self): """Return True if connected to Hub, otherwise False.""" - return self._available - - async def connect(self): - """Connect to the Harmony HUB.""" - _LOGGER.debug("%s: Connecting", self._name) - try: - if not await self._client.connect(): - _LOGGER.warning("%s: Unable to connect to HUB", self._name) - await self._client.close() - return False - except aioexc.TimeOut: - _LOGGER.warning("%s: Connection timed-out", self._name) - return False - return True + return self._data.available def new_activity(self, activity_info: tuple) -> None: """Call for updating the current activity.""" @@ -309,34 +265,14 @@ class HarmonyRemote(remote.RemoteEntity, RestoreEntity): # when turning on self._last_activity = activity_name self._state = bool(activity_id != -1) - self._available = True self.async_write_ha_state() async def new_config(self, _=None): """Call for updating the current activity.""" _LOGGER.debug("%s: configuration has been updated", self._name) - self.new_activity(self._client.current_activity) + self.new_activity(self._data.current_activity) await self.hass.async_add_executor_job(self.write_config_file) - async def got_connected(self, _=None): - """Notification that we're connected to the HUB.""" - _LOGGER.debug("%s: connected to the HUB", self._name) - if not self._available: - # We were disconnected before. - await self.new_config() - - async def got_disconnected(self, _=None): - """Notification that we're disconnected from the HUB.""" - _LOGGER.debug("%s: disconnected from the HUB", self._name) - self._available = False - # We're going to wait for 10 seconds before announcing we're - # unavailable, this to allow a reconnection to happen. - await asyncio.sleep(10) - - if not self._available: - # Still disconnected. Let the state engine know. - self.async_write_ha_state() - async def async_turn_on(self, **kwargs): """Start an activity from the Harmony device.""" _LOGGER.debug("%s: Turn On", self.name) @@ -347,55 +283,18 @@ class HarmonyRemote(remote.RemoteEntity, RestoreEntity): if self._last_activity: activity = self._last_activity else: - all_activities = list_names_from_hublist( - self._client.hub_config.activities - ) + all_activities = self._data.activity_names if all_activities: activity = all_activities[0] if activity: - activity_id = None - activity_name = None - - if activity.isdigit() or activity == "-1": - _LOGGER.debug("%s: Activity is numeric", self.name) - activity_name = self._client.get_activity_name(int(activity)) - if activity_name: - activity_id = activity - - if activity_id is None: - _LOGGER.debug("%s: Find activity ID based on name", self.name) - activity_name = str(activity) - activity_id = self._client.get_activity_id(activity_name) - - if activity_id is None: - _LOGGER.error("%s: Activity %s is invalid", self.name, activity) - return - - if self._current_activity == activity_name: - # Automations or HomeKit may turn the device on multiple times - # when the current activity is already active which will cause - # harmony to loose state. This behavior is unexpected as turning - # the device on when its already on isn't expected to reset state. - _LOGGER.debug( - "%s: Current activity is already %s", self.name, activity_name - ) - return - - try: - await self._client.start_activity(activity_id) - except aioexc.TimeOut: - _LOGGER.error("%s: Starting activity %s timed-out", self.name, activity) + await self._data.async_start_activity(activity) else: _LOGGER.error("%s: No activity specified with turn_on service", self.name) async def async_turn_off(self, **kwargs): """Start the PowerOff activity.""" - _LOGGER.debug("%s: Turn Off", self.name) - try: - await self._client.power_off() - except aioexc.TimeOut: - _LOGGER.error("%s: Powering off timed-out", self.name) + await self._data.async_power_off() async def async_send_command(self, command, **kwargs): """Send a list of commands to one device.""" @@ -405,90 +304,38 @@ class HarmonyRemote(remote.RemoteEntity, RestoreEntity): _LOGGER.error("%s: Missing required argument: device", self.name) return - device_id = None - if device.isdigit(): - _LOGGER.debug("%s: Device %s is numeric", self.name, device) - if self._client.get_device_name(int(device)): - device_id = device - - if device_id is None: - _LOGGER.debug( - "%s: Find device ID %s based on device name", self.name, device - ) - device_id = self._client.get_device_id(str(device).strip()) - - if device_id is None: - _LOGGER.error("%s: Device %s is invalid", self.name, device) - return - num_repeats = kwargs[ATTR_NUM_REPEATS] delay_secs = kwargs.get(ATTR_DELAY_SECS, self.delay_secs) hold_secs = kwargs[ATTR_HOLD_SECS] - _LOGGER.debug( - "Sending commands to device %s holding for %s seconds " - "with a delay of %s seconds", - device, - hold_secs, - delay_secs, + await self._data.async_send_command( + command, device, num_repeats, delay_secs, hold_secs ) - # Creating list of commands to send. - snd_cmnd_list = [] - for _ in range(num_repeats): - for single_command in command: - send_command = SendCommandDevice( - device=device_id, command=single_command, delay=hold_secs - ) - snd_cmnd_list.append(send_command) - if delay_secs > 0: - snd_cmnd_list.append(float(delay_secs)) - - _LOGGER.debug("%s: Sending commands", self.name) - try: - result_list = await self._client.send_commands(snd_cmnd_list) - except aioexc.TimeOut: - _LOGGER.error("%s: Sending commands timed-out", self.name) - return - - for result in result_list: - _LOGGER.error( - "Sending command %s to device %s failed with code %s: %s", - result.command.command, - result.command.device, - result.code, - result.msg, - ) - async def change_channel(self, channel): """Change the channel using Harmony remote.""" - _LOGGER.debug("%s: Changing channel to %s", self.name, channel) - try: - await self._client.change_channel(channel) - except aioexc.TimeOut: - _LOGGER.error("%s: Changing channel to %s timed-out", self.name, channel) + await self._data.change_channel(channel) async def sync(self): """Sync the Harmony device with the web service.""" - _LOGGER.debug("%s: Syncing hub with Harmony cloud", self.name) - try: - await self._client.sync() - except aioexc.TimeOut: - _LOGGER.error("%s: Syncing hub with Harmony cloud timed-out", self.name) - else: + if await self._data.sync(): await self.hass.async_add_executor_job(self.write_config_file) def write_config_file(self): - """Write Harmony configuration file.""" + """Write Harmony configuration file. + + This is a handy way for users to figure out the available commands for automations. + """ _LOGGER.debug( "%s: Writing hub configuration to file: %s", self.name, self._config_path ) - if self._client.config is None: + json_config = self._data.json_config + if json_config is None: _LOGGER.warning("%s: No configuration received from hub", self.name) return try: with open(self._config_path, "w+", encoding="utf-8") as file_out: - json.dump(self._client.json_config, file_out, sort_keys=True, indent=4) + json.dump(json_config, file_out, sort_keys=True, indent=4) except OSError as exc: _LOGGER.error( "%s: Unable to write HUB configuration to %s: %s", diff --git a/homeassistant/components/harmony/subscriber.py b/homeassistant/components/harmony/subscriber.py new file mode 100644 index 00000000000..d3bed33d560 --- /dev/null +++ b/homeassistant/components/harmony/subscriber.py @@ -0,0 +1,77 @@ +"""Mixin class for handling harmony callback subscriptions.""" + +import logging +from typing import Any, Callable, NamedTuple, Optional + +from homeassistant.core import callback + +_LOGGER = logging.getLogger(__name__) + +NoParamCallback = Optional[Callable[[object], Any]] +ActivityCallback = Optional[Callable[[object, tuple], Any]] + + +class HarmonyCallback(NamedTuple): + """Callback type for Harmony Hub notifications.""" + + connected: NoParamCallback + disconnected: NoParamCallback + config_updated: NoParamCallback + activity_starting: ActivityCallback + activity_started: ActivityCallback + + +class HarmonySubscriberMixin: + """Base implementation for a subscriber.""" + + def __init__(self, hass): + """Initialize an subscriber.""" + super().__init__() + self._hass = hass + self._subscriptions = [] + + @callback + def async_subscribe(self, update_callbacks: HarmonyCallback) -> Callable: + """Add a callback subscriber.""" + self._subscriptions.append(update_callbacks) + + def _unsubscribe(): + self.async_unsubscribe(update_callbacks) + + return _unsubscribe + + @callback + def async_unsubscribe(self, update_callback: HarmonyCallback): + """Remove a callback subscriber.""" + self._subscriptions.remove(update_callback) + + def _config_updated(self, _=None) -> None: + _LOGGER.debug("config_updated") + self._call_callbacks("config_updated") + + def _connected(self, _=None) -> None: + _LOGGER.debug("connected") + self._available = True + self._call_callbacks("connected") + + def _disconnected(self, _=None) -> None: + _LOGGER.debug("disconnected") + self._available = False + self._call_callbacks("disconnected") + + def _activity_starting(self, activity_info: tuple) -> None: + _LOGGER.debug("activity %s starting", activity_info) + self._call_callbacks("activity_starting", activity_info) + + def _activity_started(self, activity_info: tuple) -> None: + _LOGGER.debug("activity %s started", activity_info) + self._call_callbacks("activity_started", activity_info) + + def _call_callbacks(self, callback_func_name: str, argument: tuple = None): + for subscription in self._subscriptions: + current_callback = getattr(subscription, callback_func_name) + if current_callback: + if argument: + self._hass.async_run_job(current_callback, argument) + else: + self._hass.async_run_job(current_callback) diff --git a/homeassistant/components/harmony/switch.py b/homeassistant/components/harmony/switch.py new file mode 100644 index 00000000000..5fae07c431b --- /dev/null +++ b/homeassistant/components/harmony/switch.py @@ -0,0 +1,87 @@ +"""Support for Harmony Hub activities.""" +import logging + +from homeassistant.components.switch import SwitchEntity +from homeassistant.const import CONF_NAME + +from .connection_state import ConnectionStateMixin +from .const import DOMAIN +from .data import HarmonyData +from .subscriber import HarmonyCallback + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up harmony activity switches.""" + data = hass.data[DOMAIN][entry.entry_id] + activities = data.activity_names + + switches = [] + for activity in activities: + _LOGGER.debug("creating switch for activity: %s", activity) + name = f"{entry.data[CONF_NAME]} {activity}" + switches.append(HarmonyActivitySwitch(name, activity, data)) + + async_add_entities(switches, True) + + +class HarmonyActivitySwitch(ConnectionStateMixin, SwitchEntity): + """Switch representation of a Harmony activity.""" + + def __init__(self, name: str, activity: str, data: HarmonyData): + """Initialize HarmonyActivitySwitch class.""" + super().__init__() + self._name = name + self._activity = activity + self._data = data + + @property + def name(self): + """Return the Harmony activity's name.""" + return self._name + + @property + def unique_id(self): + """Return the unique id.""" + return f"{self._data.unique_id}-{self._activity}" + + @property + def is_on(self): + """Return if the current activity is the one for this switch.""" + _, activity_name = self._data.current_activity + return activity_name == self._activity + + @property + def should_poll(self): + """Return that we shouldn't be polled.""" + return False + + @property + def available(self): + """Return True if we're connected to the Hub, otherwise False.""" + return self._data.available + + async def async_turn_on(self, **kwargs): + """Start this activity.""" + await self._data.async_start_activity(self._activity) + + async def async_turn_off(self, **kwargs): + """Stop this activity.""" + await self._data.async_power_off() + + async def async_added_to_hass(self): + """Call when entity is added to hass.""" + + callbacks = { + "connected": self.got_connected, + "disconnected": self.got_disconnected, + "activity_starting": self._activity_update, + "activity_started": self._activity_update, + "config_updated": None, + } + + self.async_on_remove(self._data.async_subscribe(HarmonyCallback(**callbacks))) + + def _activity_update(self, activity_info: tuple): + self.async_write_ha_state() diff --git a/tests/components/harmony/conftest.py b/tests/components/harmony/conftest.py new file mode 100644 index 00000000000..e758a2795a9 --- /dev/null +++ b/tests/components/harmony/conftest.py @@ -0,0 +1,147 @@ +"""Fixtures for harmony tests.""" +import logging +from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch + +from aioharmony.const import ClientCallbackType +import pytest + +from homeassistant.components.harmony.const import ACTIVITY_POWER_OFF + +_LOGGER = logging.getLogger(__name__) + +WATCH_TV_ACTIVITY_ID = 123 +PLAY_MUSIC_ACTIVITY_ID = 456 + +ACTIVITIES_TO_IDS = { + ACTIVITY_POWER_OFF: -1, + "Watch TV": WATCH_TV_ACTIVITY_ID, + "Play Music": PLAY_MUSIC_ACTIVITY_ID, +} + +IDS_TO_ACTIVITIES = { + -1: ACTIVITY_POWER_OFF, + WATCH_TV_ACTIVITY_ID: "Watch TV", + PLAY_MUSIC_ACTIVITY_ID: "Play Music", +} + +TV_DEVICE_ID = 1234 +TV_DEVICE_NAME = "My TV" + +DEVICES_TO_IDS = { + TV_DEVICE_NAME: TV_DEVICE_ID, +} + +IDS_TO_DEVICES = { + TV_DEVICE_ID: TV_DEVICE_NAME, +} + + +class FakeHarmonyClient: + """FakeHarmonyClient to mock away network calls.""" + + def __init__( + self, ip_address: str = "", callbacks: ClientCallbackType = MagicMock() + ): + """Initialize FakeHarmonyClient class.""" + self._activity_name = "Watch TV" + self.close = AsyncMock() + self.send_commands = AsyncMock() + self.change_channel = AsyncMock() + self.sync = AsyncMock() + self._callbacks = callbacks + + async def connect(self): + """Connect and call the appropriate callbacks.""" + self._callbacks.connect(None) + return AsyncMock(return_value=(True)) + + def get_activity_name(self, activity_id): + """Return the activity name with the given activity_id.""" + return IDS_TO_ACTIVITIES.get(activity_id) + + def get_activity_id(self, activity_name): + """Return the mapping of an activity name to the internal id.""" + return ACTIVITIES_TO_IDS.get(activity_name) + + def get_device_name(self, device_id): + """Return the device name with the given device_id.""" + return IDS_TO_DEVICES.get(device_id) + + def get_device_id(self, device_name): + """Return the device id with the given device_name.""" + return DEVICES_TO_IDS.get(device_name) + + async def start_activity(self, activity_id): + """Update the current activity and call the appropriate callbacks.""" + self._activity_name = IDS_TO_ACTIVITIES.get(int(activity_id)) + activity_tuple = (activity_id, self._activity_name) + self._callbacks.new_activity_starting(activity_tuple) + self._callbacks.new_activity(activity_tuple) + + return AsyncMock(return_value=(True, "unused message")) + + async def power_off(self): + """Power off all activities.""" + await self.start_activity(-1) + + @property + def current_activity(self): + """Return the current activity tuple.""" + return ( + self.get_activity_id(self._activity_name), + self._activity_name, + ) + + @property + def config(self): + """Return the config object.""" + return self.hub_config.config + + @property + def json_config(self): + """Return the json config as a dict.""" + return {} + + @property + def hub_config(self): + """Return the client_config type.""" + config = MagicMock() + type(config).activities = PropertyMock( + return_value=[ + {"name": "Watch TV", "id": WATCH_TV_ACTIVITY_ID}, + {"name": "Play Music", "id": PLAY_MUSIC_ACTIVITY_ID}, + ] + ) + type(config).devices = PropertyMock( + return_value=[{"name": TV_DEVICE_NAME, "id": TV_DEVICE_ID}] + ) + type(config).info = PropertyMock(return_value={}) + type(config).hub_state = PropertyMock(return_value={}) + type(config).config = PropertyMock( + return_value={ + "activity": [ + {"id": WATCH_TV_ACTIVITY_ID, "label": "Watch TV"}, + {"id": PLAY_MUSIC_ACTIVITY_ID, "label": "Play Music"}, + ] + } + ) + return config + + +@pytest.fixture() +def mock_hc(): + """Create a mock HarmonyClient.""" + with patch( + "homeassistant.components.harmony.data.HarmonyClient", + side_effect=FakeHarmonyClient, + ) as fake: + yield fake + + +@pytest.fixture() +def mock_write_config(): + """Patches write_config_file to remove side effects.""" + with patch( + "homeassistant.components.harmony.remote.HarmonyRemote.write_config_file", + ) as mock: + yield mock diff --git a/tests/components/harmony/const.py b/tests/components/harmony/const.py new file mode 100644 index 00000000000..1911ea949af --- /dev/null +++ b/tests/components/harmony/const.py @@ -0,0 +1,6 @@ +"""Constants for Logitch Harmony Hub tests.""" + +HUB_NAME = "Guest Room" +ENTITY_REMOTE = "remote.guest_room" +ENTITY_WATCH_TV = "switch.guest_room_watch_tv" +ENTITY_PLAY_MUSIC = "switch.guest_room_play_music" diff --git a/tests/components/harmony/test_activity_changes.py b/tests/components/harmony/test_activity_changes.py new file mode 100644 index 00000000000..ff76c3ce998 --- /dev/null +++ b/tests/components/harmony/test_activity_changes.py @@ -0,0 +1,137 @@ +"""Test the Logitech Harmony Hub activity switches.""" + +import logging + +from homeassistant.components.harmony.const import DOMAIN +from homeassistant.components.remote import ATTR_ACTIVITY, DOMAIN as REMOTE_DOMAIN +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_HOST, + CONF_NAME, + STATE_OFF, + STATE_ON, +) + +from .conftest import ACTIVITIES_TO_IDS +from .const import ENTITY_PLAY_MUSIC, ENTITY_REMOTE, ENTITY_WATCH_TV, HUB_NAME + +from tests.common import MockConfigEntry + +_LOGGER = logging.getLogger(__name__) + + +async def test_switch_toggles(mock_hc, hass, mock_write_config): + """Ensure calls to the switch modify the harmony state.""" + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} + ) + + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # mocks start with current activity == Watch TV + assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) + + # turn off watch tv switch + await _toggle_switch_and_wait(hass, SERVICE_TURN_OFF, ENTITY_WATCH_TV) + assert hass.states.is_state(ENTITY_REMOTE, STATE_OFF) + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_OFF) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) + + # turn on play music switch + await _toggle_switch_and_wait(hass, SERVICE_TURN_ON, ENTITY_PLAY_MUSIC) + assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_OFF) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_ON) + + # turn on watch tv switch + await _toggle_switch_and_wait(hass, SERVICE_TURN_ON, ENTITY_WATCH_TV) + assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) + + +async def test_remote_toggles(mock_hc, hass, mock_write_config): + """Ensure calls to the remote also updates the switches.""" + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} + ) + + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # mocks start with current activity == Watch TV + assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) + + # turn off remote + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_REMOTE}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.is_state(ENTITY_REMOTE, STATE_OFF) + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_OFF) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) + + # turn on remote, restoring the last activity + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_REMOTE}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) + + # send new activity command, with activity name + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_REMOTE, ATTR_ACTIVITY: "Play Music"}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_OFF) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_ON) + + # send new activity command, with activity id + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_REMOTE, ATTR_ACTIVITY: ACTIVITIES_TO_IDS["Watch TV"]}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) + + +async def _toggle_switch_and_wait(hass, service_name, entity): + await hass.services.async_call( + SWITCH_DOMAIN, + service_name, + {ATTR_ENTITY_ID: entity}, + blocking=True, + ) + await hass.async_block_till_done() diff --git a/tests/components/harmony/test_commands.py b/tests/components/harmony/test_commands.py new file mode 100644 index 00000000000..62056a08e1d --- /dev/null +++ b/tests/components/harmony/test_commands.py @@ -0,0 +1,263 @@ +"""Test sending commands to the Harmony Hub remote.""" + +from aioharmony.const import SendCommandDevice + +from homeassistant.components.harmony.const import ( + DOMAIN, + SERVICE_CHANGE_CHANNEL, + SERVICE_SYNC, +) +from homeassistant.components.harmony.remote import ATTR_CHANNEL, ATTR_DELAY_SECS +from homeassistant.components.remote import ( + ATTR_COMMAND, + ATTR_DEVICE, + ATTR_NUM_REPEATS, + DEFAULT_DELAY_SECS, + DEFAULT_HOLD_SECS, + DOMAIN as REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, +) +from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME + +from .conftest import TV_DEVICE_ID, TV_DEVICE_NAME +from .const import ENTITY_REMOTE, HUB_NAME + +from tests.common import MockConfigEntry + +PLAY_COMMAND = "Play" +STOP_COMMAND = "Stop" + + +async def test_async_send_command(mock_hc, hass, mock_write_config): + """Ensure calls to send remote commands properly propagate to devices.""" + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} + ) + + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + data = hass.data[DOMAIN][entry.entry_id] + send_commands_mock = data._client.send_commands + + # No device provided + await _send_commands_and_wait( + hass, {ATTR_ENTITY_ID: ENTITY_REMOTE, ATTR_COMMAND: PLAY_COMMAND} + ) + send_commands_mock.assert_not_awaited() + + # Tell the TV to play by id + await _send_commands_and_wait( + hass, + { + ATTR_ENTITY_ID: ENTITY_REMOTE, + ATTR_COMMAND: PLAY_COMMAND, + ATTR_DEVICE: TV_DEVICE_ID, + }, + ) + + send_commands_mock.assert_awaited_once_with( + [ + SendCommandDevice( + device=str(TV_DEVICE_ID), + command=PLAY_COMMAND, + delay=float(DEFAULT_HOLD_SECS), + ), + DEFAULT_DELAY_SECS, + ] + ) + send_commands_mock.reset_mock() + + # Tell the TV to play by name + await _send_commands_and_wait( + hass, + { + ATTR_ENTITY_ID: ENTITY_REMOTE, + ATTR_COMMAND: PLAY_COMMAND, + ATTR_DEVICE: TV_DEVICE_NAME, + }, + ) + + send_commands_mock.assert_awaited_once_with( + [ + SendCommandDevice( + device=TV_DEVICE_ID, + command=PLAY_COMMAND, + delay=float(DEFAULT_HOLD_SECS), + ), + DEFAULT_DELAY_SECS, + ] + ) + send_commands_mock.reset_mock() + + # Tell the TV to play and stop by name + await _send_commands_and_wait( + hass, + { + ATTR_ENTITY_ID: ENTITY_REMOTE, + ATTR_COMMAND: [PLAY_COMMAND, STOP_COMMAND], + ATTR_DEVICE: TV_DEVICE_NAME, + }, + ) + + send_commands_mock.assert_awaited_once_with( + [ + SendCommandDevice( + device=TV_DEVICE_ID, + command=PLAY_COMMAND, + delay=float(DEFAULT_HOLD_SECS), + ), + DEFAULT_DELAY_SECS, + SendCommandDevice( + device=TV_DEVICE_ID, + command=STOP_COMMAND, + delay=float(DEFAULT_HOLD_SECS), + ), + DEFAULT_DELAY_SECS, + ] + ) + send_commands_mock.reset_mock() + + # Tell the TV to play by name multiple times + await _send_commands_and_wait( + hass, + { + ATTR_ENTITY_ID: ENTITY_REMOTE, + ATTR_COMMAND: PLAY_COMMAND, + ATTR_DEVICE: TV_DEVICE_NAME, + ATTR_NUM_REPEATS: 2, + }, + ) + + send_commands_mock.assert_awaited_once_with( + [ + SendCommandDevice( + device=TV_DEVICE_ID, + command=PLAY_COMMAND, + delay=float(DEFAULT_HOLD_SECS), + ), + DEFAULT_DELAY_SECS, + SendCommandDevice( + device=TV_DEVICE_ID, + command=PLAY_COMMAND, + delay=float(DEFAULT_HOLD_SECS), + ), + DEFAULT_DELAY_SECS, + ] + ) + send_commands_mock.reset_mock() + + # Send commands to an unknown device + await _send_commands_and_wait( + hass, + { + ATTR_ENTITY_ID: ENTITY_REMOTE, + ATTR_COMMAND: PLAY_COMMAND, + ATTR_DEVICE: "no-such-device", + }, + ) + send_commands_mock.assert_not_awaited() + send_commands_mock.reset_mock() + + +async def test_async_send_command_custom_delay(mock_hc, hass, mock_write_config): + """Ensure calls to send remote commands properly propagate to devices with custom delays.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "192.0.2.0", + CONF_NAME: HUB_NAME, + ATTR_DELAY_SECS: DEFAULT_DELAY_SECS + 2, + }, + ) + + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + data = hass.data[DOMAIN][entry.entry_id] + send_commands_mock = data._client.send_commands + + # Tell the TV to play by id + await _send_commands_and_wait( + hass, + { + ATTR_ENTITY_ID: ENTITY_REMOTE, + ATTR_COMMAND: PLAY_COMMAND, + ATTR_DEVICE: TV_DEVICE_ID, + }, + ) + + send_commands_mock.assert_awaited_once_with( + [ + SendCommandDevice( + device=str(TV_DEVICE_ID), + command=PLAY_COMMAND, + delay=float(DEFAULT_HOLD_SECS), + ), + DEFAULT_DELAY_SECS + 2, + ] + ) + send_commands_mock.reset_mock() + + +async def test_change_channel(mock_hc, hass, mock_write_config): + """Test change channel commands.""" + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} + ) + + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + data = hass.data[DOMAIN][entry.entry_id] + change_channel_mock = data._client.change_channel + + # Tell the remote to change channels + await hass.services.async_call( + DOMAIN, + SERVICE_CHANGE_CHANNEL, + {ATTR_ENTITY_ID: ENTITY_REMOTE, ATTR_CHANNEL: 100}, + blocking=True, + ) + await hass.async_block_till_done() + + change_channel_mock.assert_awaited_once_with(100) + + +async def test_sync(mock_hc, mock_write_config, hass): + """Test the sync command.""" + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} + ) + + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + data = hass.data[DOMAIN][entry.entry_id] + sync_mock = data._client.sync + + # Tell the remote to change channels + await hass.services.async_call( + DOMAIN, + SERVICE_SYNC, + {ATTR_ENTITY_ID: ENTITY_REMOTE}, + blocking=True, + ) + await hass.async_block_till_done() + + sync_mock.assert_awaited_once() + mock_write_config.assert_called() + + +async def _send_commands_and_wait(hass, service_data): + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + service_data, + blocking=True, + ) + await hass.async_block_till_done() diff --git a/tests/components/harmony/test_config_flow.py b/tests/components/harmony/test_config_flow.py index f43c9f6b478..e5d0c6f0570 100644 --- a/tests/components/harmony/test_config_flow.py +++ b/tests/components/harmony/test_config_flow.py @@ -1,5 +1,5 @@ """Test the Logitech Harmony Hub config flow.""" -from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.harmony.config_flow import CannotConnect @@ -17,23 +17,6 @@ def _get_mock_harmonyapi(connect=None, close=None): return harmonyapi_mock -def _get_mock_harmonyclient(): - harmonyclient_mock = MagicMock() - type(harmonyclient_mock).connect = AsyncMock() - type(harmonyclient_mock).close = AsyncMock() - type(harmonyclient_mock).get_activity_name = MagicMock(return_value="Watch TV") - type(harmonyclient_mock.hub_config).activities = PropertyMock( - return_value=[{"name": "Watch TV", "id": 123}] - ) - type(harmonyclient_mock.hub_config).devices = PropertyMock( - return_value=[{"name": "My TV", "id": 1234}] - ) - type(harmonyclient_mock.hub_config).info = PropertyMock(return_value={}) - type(harmonyclient_mock.hub_config).hub_state = PropertyMock(return_value={}) - - return harmonyclient_mock - - async def test_user_form(hass): """Test we get the user form.""" await setup.async_setup_component(hass, "persistent_notification", {}) @@ -213,9 +196,8 @@ async def test_form_cannot_connect(hass): assert result2["errors"] == {"base": "cannot_connect"} -async def test_options_flow(hass): +async def test_options_flow(hass, mock_hc): """Test config flow options.""" - config_entry = MockConfigEntry( domain=DOMAIN, unique_id="abcde12345", @@ -223,19 +205,13 @@ async def test_options_flow(hass): options={"activity": "Watch TV", "delay_secs": 0.5}, ) - harmony_client = _get_mock_harmonyclient() - - with patch( - "aioharmony.harmonyapi.HarmonyClient", - return_value=harmony_client, - ), patch("homeassistant.components.harmony.remote.HarmonyRemote.write_config_file"): - config_entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - result = await hass.config_entries.options.async_init(config_entry.entry_id) - await hass.async_block_till_done() - assert await hass.config_entries.async_unload(config_entry.entry_id) - await hass.async_block_till_done() + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.options.async_init(config_entry.entry_id) + await hass.async_block_till_done() + assert await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "init" diff --git a/tests/components/harmony/test_connection_changes.py b/tests/components/harmony/test_connection_changes.py new file mode 100644 index 00000000000..15d46298855 --- /dev/null +++ b/tests/components/harmony/test_connection_changes.py @@ -0,0 +1,67 @@ +"""Test the Logitech Harmony Hub entities with connection state changes.""" + +from datetime import timedelta + +from homeassistant.components.harmony.const import DOMAIN +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.util import utcnow + +from .const import ENTITY_PLAY_MUSIC, ENTITY_REMOTE, ENTITY_WATCH_TV, HUB_NAME + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_connection_state_changes(mock_hc, hass, mock_write_config): + """Ensure connection changes are reflected in the switch states.""" + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} + ) + + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + data = hass.data[DOMAIN][entry.entry_id] + + # mocks start with current activity == Watch TV + assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) + + data._disconnected() + await hass.async_block_till_done() + + # Entities do not immediately show as unavailable + assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) + + future_time = utcnow() + timedelta(seconds=10) + async_fire_time_changed(hass, future_time) + await hass.async_block_till_done() + assert hass.states.is_state(ENTITY_REMOTE, STATE_UNAVAILABLE) + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_UNAVAILABLE) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_UNAVAILABLE) + + data._connected() + await hass.async_block_till_done() + + assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) + + data._disconnected() + data._connected() + future_time = utcnow() + timedelta(seconds=10) + async_fire_time_changed(hass, future_time) + + await hass.async_block_till_done() + assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) diff --git a/tests/components/harmony/test_subscriber.py b/tests/components/harmony/test_subscriber.py new file mode 100644 index 00000000000..5c357bef825 --- /dev/null +++ b/tests/components/harmony/test_subscriber.py @@ -0,0 +1,143 @@ +"""Test the HarmonySubscriberMixin class.""" + +import asyncio +from unittest.mock import AsyncMock, MagicMock + +from homeassistant.components.harmony.subscriber import ( + HarmonyCallback, + HarmonySubscriberMixin, +) + +_NO_PARAM_CALLBACKS = { + "connected": "_connected", + "disconnected": "_disconnected", + "config_updated": "_config_updated", +} + +_ACTIVITY_CALLBACKS = { + "activity_starting": "_activity_starting", + "activity_started": "_activity_started", +} + +_ALL_CALLBACK_NAMES = list(_NO_PARAM_CALLBACKS.keys()) + list( + _ACTIVITY_CALLBACKS.keys() +) + +_ACTIVITY_TUPLE = ("not", "used") + + +async def test_no_callbacks(hass): + """Ensure we handle no subscriptions.""" + subscriber = HarmonySubscriberMixin(hass) + _call_all_callbacks(subscriber) + await hass.async_block_till_done() + + +async def test_empty_callbacks(hass): + """Ensure we handle a missing callback in a subscription.""" + subscriber = HarmonySubscriberMixin(hass) + + callbacks = {k: None for k in _ALL_CALLBACK_NAMES} + subscriber.async_subscribe(HarmonyCallback(**callbacks)) + _call_all_callbacks(subscriber) + await hass.async_block_till_done() + + +async def test_async_callbacks(hass): + """Ensure we handle async callbacks.""" + subscriber = HarmonySubscriberMixin(hass) + + callbacks = {k: AsyncMock() for k in _ALL_CALLBACK_NAMES} + subscriber.async_subscribe(HarmonyCallback(**callbacks)) + _call_all_callbacks(subscriber) + await hass.async_block_till_done() + + for callback_name in _NO_PARAM_CALLBACKS.keys(): + callback_mock = callbacks[callback_name] + callback_mock.assert_awaited_once() + + for callback_name in _ACTIVITY_CALLBACKS.keys(): + callback_mock = callbacks[callback_name] + callback_mock.assert_awaited_once_with(_ACTIVITY_TUPLE) + + +async def test_long_async_callbacks(hass): + """Ensure we handle async callbacks that may have sleeps.""" + subscriber = HarmonySubscriberMixin(hass) + + blocker_event = asyncio.Event() + notifier_event_one = asyncio.Event() + notifier_event_two = asyncio.Event() + + async def blocks_until_notified(): + await blocker_event.wait() + notifier_event_one.set() + + async def notifies_when_called(): + notifier_event_two.set() + + callbacks_one = {k: blocks_until_notified for k in _ALL_CALLBACK_NAMES} + callbacks_two = {k: notifies_when_called for k in _ALL_CALLBACK_NAMES} + subscriber.async_subscribe(HarmonyCallback(**callbacks_one)) + subscriber.async_subscribe(HarmonyCallback(**callbacks_two)) + + subscriber._connected() + await notifier_event_two.wait() + blocker_event.set() + await notifier_event_one.wait() + + +async def test_callbacks(hass): + """Ensure we handle non-async callbacks.""" + subscriber = HarmonySubscriberMixin(hass) + + callbacks = {k: MagicMock() for k in _ALL_CALLBACK_NAMES} + subscriber.async_subscribe(HarmonyCallback(**callbacks)) + _call_all_callbacks(subscriber) + await hass.async_block_till_done() + + for callback_name in _NO_PARAM_CALLBACKS.keys(): + callback_mock = callbacks[callback_name] + callback_mock.assert_called_once() + + for callback_name in _ACTIVITY_CALLBACKS.keys(): + callback_mock = callbacks[callback_name] + callback_mock.assert_called_once_with(_ACTIVITY_TUPLE) + + +async def test_subscribe_unsubscribe(hass): + """Ensure we handle subscriptions and unsubscriptions correctly.""" + subscriber = HarmonySubscriberMixin(hass) + + callback_one = {k: MagicMock() for k in _ALL_CALLBACK_NAMES} + unsub_one = subscriber.async_subscribe(HarmonyCallback(**callback_one)) + callback_two = {k: MagicMock() for k in _ALL_CALLBACK_NAMES} + _ = subscriber.async_subscribe(HarmonyCallback(**callback_two)) + callback_three = {k: MagicMock() for k in _ALL_CALLBACK_NAMES} + unsub_three = subscriber.async_subscribe(HarmonyCallback(**callback_three)) + + unsub_one() + unsub_three() + + _call_all_callbacks(subscriber) + await hass.async_block_till_done() + + for callback_name in _NO_PARAM_CALLBACKS.keys(): + callback_one[callback_name].assert_not_called() + callback_two[callback_name].assert_called_once() + callback_three[callback_name].assert_not_called() + + for callback_name in _ACTIVITY_CALLBACKS.keys(): + callback_one[callback_name].assert_not_called() + callback_two[callback_name].assert_called_once_with(_ACTIVITY_TUPLE) + callback_three[callback_name].assert_not_called() + + +def _call_all_callbacks(subscriber): + for callback_method in _NO_PARAM_CALLBACKS.values(): + to_call = getattr(subscriber, callback_method) + to_call() + + for callback_method in _ACTIVITY_CALLBACKS.values(): + to_call = getattr(subscriber, callback_method) + to_call(_ACTIVITY_TUPLE) From d315ab2cf5d69f98260f39cf4b4f217478d940cf Mon Sep 17 00:00:00 2001 From: chpego <38792705+chpego@users.noreply.github.com> Date: Tue, 5 Jan 2021 00:49:01 +0100 Subject: [PATCH 092/507] Bump caldav version to 0.7.1 (#44815) * bump caldav version 0.7.1 * Update requirements_all.txt * Update requirements_test_all.txt --- homeassistant/components/caldav/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/caldav/manifest.json b/homeassistant/components/caldav/manifest.json index 94d786c8825..992b79f0d3b 100644 --- a/homeassistant/components/caldav/manifest.json +++ b/homeassistant/components/caldav/manifest.json @@ -2,6 +2,6 @@ "domain": "caldav", "name": "CalDAV", "documentation": "https://www.home-assistant.io/integrations/caldav", - "requirements": ["caldav==0.6.1"], + "requirements": ["caldav==0.7.1"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index b1365094bc2..85a8fbaaaa0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ btsmarthub_devicelist==0.2.0 buienradar==1.0.4 # homeassistant.components.caldav -caldav==0.6.1 +caldav==0.7.1 # homeassistant.components.circuit circuit-webhook==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7c0ee678a6c..6896c8cf923 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -216,7 +216,7 @@ bsblan==0.4.0 buienradar==1.0.4 # homeassistant.components.caldav -caldav==0.6.1 +caldav==0.7.1 # homeassistant.components.coinmarketcap coinmarketcap==5.0.3 From 65e56d03bf4ebf3adc6329cd10e6fe48db83e426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 5 Jan 2021 03:03:16 +0200 Subject: [PATCH 093/507] Complete device and entity registry type hints (#44406) --- homeassistant/helpers/device_registry.py | 184 ++++++++++++----------- homeassistant/helpers/entity_registry.py | 85 +++++------ 2 files changed, 133 insertions(+), 136 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 6e8c09bbd60..a115434fad9 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -11,13 +11,11 @@ import homeassistant.util.uuid as uuid_util from .debounce import Debouncer from .singleton import singleton -from .typing import UNDEFINED, HomeAssistantType +from .typing import UNDEFINED, HomeAssistantType, UndefinedType if TYPE_CHECKING: from . import entity_registry -# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) DATA_REGISTRY = "device_registry" @@ -40,26 +38,6 @@ DISABLED_INTEGRATION = "integration" DISABLED_USER = "user" -@attr.s(slots=True, frozen=True) -class DeletedDeviceEntry: - """Deleted Device Registry Entry.""" - - config_entries: Set[str] = attr.ib() - connections: Set[Tuple[str, str]] = attr.ib() - identifiers: Set[Tuple[str, str]] = attr.ib() - id: str = attr.ib() - - def to_device_entry(self, config_entry_id, connections, identifiers): - """Create DeviceEntry from DeletedDeviceEntry.""" - return DeviceEntry( - config_entries={config_entry_id}, - connections=self.connections & connections, - identifiers=self.identifiers & identifiers, - id=self.id, - is_new=True, - ) - - @attr.s(slots=True, frozen=True) class DeviceEntry: """Device Registry Entry.""" @@ -67,14 +45,14 @@ class DeviceEntry: config_entries: Set[str] = attr.ib(converter=set, factory=set) connections: Set[Tuple[str, str]] = attr.ib(converter=set, factory=set) identifiers: Set[Tuple[str, str]] = attr.ib(converter=set, factory=set) - manufacturer: str = attr.ib(default=None) - model: str = attr.ib(default=None) - name: str = attr.ib(default=None) - sw_version: str = attr.ib(default=None) - via_device_id: str = attr.ib(default=None) - area_id: str = attr.ib(default=None) - name_by_user: str = attr.ib(default=None) - entry_type: str = attr.ib(default=None) + manufacturer: Optional[str] = attr.ib(default=None) + model: Optional[str] = attr.ib(default=None) + name: Optional[str] = attr.ib(default=None) + sw_version: Optional[str] = attr.ib(default=None) + via_device_id: Optional[str] = attr.ib(default=None) + area_id: Optional[str] = attr.ib(default=None) + name_by_user: Optional[str] = attr.ib(default=None) + entry_type: Optional[str] = attr.ib(default=None) 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) @@ -95,6 +73,32 @@ class DeviceEntry: return self.disabled_by is not None +@attr.s(slots=True, frozen=True) +class DeletedDeviceEntry: + """Deleted Device Registry Entry.""" + + config_entries: Set[str] = attr.ib() + connections: Set[Tuple[str, str]] = attr.ib() + identifiers: Set[Tuple[str, str]] = attr.ib() + id: str = attr.ib() + + def to_device_entry( + self, + config_entry_id: str, + connections: Set[Tuple[str, str]], + identifiers: Set[Tuple[str, str]], + ) -> DeviceEntry: + """Create DeviceEntry from DeletedDeviceEntry.""" + return DeviceEntry( + # type ignores: likely https://github.com/python/mypy/issues/8625 + config_entries={config_entry_id}, # type: ignore[arg-type] + connections=self.connections & connections, # type: ignore[arg-type] + identifiers=self.identifiers & identifiers, # type: ignore[arg-type] + id=self.id, + is_new=True, + ) + + def format_mac(mac: str) -> str: """Format the mac address string for entry into dev reg.""" to_test = mac @@ -201,40 +205,40 @@ class DeviceRegistry: _remove_device_from_index(devices_index, old_device) _add_device_to_index(devices_index, new_device) - def _clear_index(self): + def _clear_index(self) -> None: """Clear the index.""" self._devices_index = { REGISTERED_DEVICE: {IDX_IDENTIFIERS: {}, IDX_CONNECTIONS: {}}, DELETED_DEVICE: {IDX_IDENTIFIERS: {}, IDX_CONNECTIONS: {}}, } - def _rebuild_index(self): + def _rebuild_index(self) -> None: """Create the index after loading devices.""" self._clear_index() for device in self.devices.values(): _add_device_to_index(self._devices_index[REGISTERED_DEVICE], device) - for device in self.deleted_devices.values(): - _add_device_to_index(self._devices_index[DELETED_DEVICE], device) + for deleted_device in self.deleted_devices.values(): + _add_device_to_index(self._devices_index[DELETED_DEVICE], deleted_device) @callback def async_get_or_create( self, *, - config_entry_id, - connections=None, - identifiers=None, - manufacturer=UNDEFINED, - model=UNDEFINED, - name=UNDEFINED, - default_manufacturer=UNDEFINED, - default_model=UNDEFINED, - default_name=UNDEFINED, - sw_version=UNDEFINED, - entry_type=UNDEFINED, - via_device=None, + config_entry_id: str, + connections: Optional[set] = None, + identifiers: Optional[set] = None, + manufacturer: Union[str, None, UndefinedType] = UNDEFINED, + model: Union[str, None, UndefinedType] = UNDEFINED, + name: Union[str, None, UndefinedType] = UNDEFINED, + default_manufacturer: Union[str, None, UndefinedType] = UNDEFINED, + default_model: Union[str, None, UndefinedType] = UNDEFINED, + default_name: Union[str, None, UndefinedType] = UNDEFINED, + sw_version: Union[str, None, UndefinedType] = UNDEFINED, + entry_type: Union[str, None, UndefinedType] = UNDEFINED, + via_device: Optional[str] = None, # To disable a device if it gets created - disabled_by=UNDEFINED, - ): + disabled_by: Union[str, None, UndefinedType] = UNDEFINED, + ) -> Optional[DeviceEntry]: """Get device. Create if it doesn't exist.""" if not identifiers and not connections: return None @@ -271,7 +275,7 @@ class DeviceRegistry: if via_device is not None: via = self.async_get_device({via_device}, set()) - via_device_id = via.id if via else UNDEFINED + via_device_id: Union[str, UndefinedType] = via.id if via else UNDEFINED else: via_device_id = UNDEFINED @@ -292,19 +296,19 @@ class DeviceRegistry: @callback def async_update_device( self, - device_id, + device_id: str, *, - area_id=UNDEFINED, - manufacturer=UNDEFINED, - model=UNDEFINED, - name=UNDEFINED, - name_by_user=UNDEFINED, - new_identifiers=UNDEFINED, - sw_version=UNDEFINED, - via_device_id=UNDEFINED, - remove_config_entry_id=UNDEFINED, - disabled_by=UNDEFINED, - ): + area_id: Union[str, None, UndefinedType] = UNDEFINED, + manufacturer: Union[str, None, UndefinedType] = UNDEFINED, + model: Union[str, None, UndefinedType] = UNDEFINED, + name: Union[str, None, UndefinedType] = UNDEFINED, + name_by_user: Union[str, None, UndefinedType] = UNDEFINED, + new_identifiers: Union[set, UndefinedType] = UNDEFINED, + sw_version: Union[str, None, UndefinedType] = UNDEFINED, + via_device_id: Union[str, None, UndefinedType] = UNDEFINED, + remove_config_entry_id: Union[str, UndefinedType] = UNDEFINED, + disabled_by: Union[str, None, UndefinedType] = UNDEFINED, + ) -> Optional[DeviceEntry]: """Update properties of a device.""" return self._async_update_device( device_id, @@ -323,27 +327,27 @@ class DeviceRegistry: @callback def _async_update_device( self, - device_id, + device_id: str, *, - add_config_entry_id=UNDEFINED, - remove_config_entry_id=UNDEFINED, - merge_connections=UNDEFINED, - merge_identifiers=UNDEFINED, - new_identifiers=UNDEFINED, - manufacturer=UNDEFINED, - model=UNDEFINED, - name=UNDEFINED, - sw_version=UNDEFINED, - entry_type=UNDEFINED, - via_device_id=UNDEFINED, - area_id=UNDEFINED, - name_by_user=UNDEFINED, - disabled_by=UNDEFINED, - ): + add_config_entry_id: Union[str, UndefinedType] = UNDEFINED, + remove_config_entry_id: Union[str, UndefinedType] = UNDEFINED, + merge_connections: Union[set, UndefinedType] = UNDEFINED, + merge_identifiers: Union[set, UndefinedType] = UNDEFINED, + new_identifiers: Union[set, UndefinedType] = UNDEFINED, + manufacturer: Union[str, None, UndefinedType] = UNDEFINED, + model: Union[str, None, UndefinedType] = UNDEFINED, + name: Union[str, None, UndefinedType] = UNDEFINED, + sw_version: Union[str, None, UndefinedType] = UNDEFINED, + entry_type: Union[str, None, UndefinedType] = UNDEFINED, + via_device_id: Union[str, None, UndefinedType] = UNDEFINED, + area_id: Union[str, None, UndefinedType] = UNDEFINED, + name_by_user: Union[str, None, UndefinedType] = UNDEFINED, + disabled_by: Union[str, None, UndefinedType] = UNDEFINED, + ) -> Optional[DeviceEntry]: """Update device attributes.""" old = self.devices[device_id] - changes = {} + changes: Dict[str, Any] = {} config_entries = old.config_entries @@ -359,21 +363,21 @@ class DeviceRegistry: ): if config_entries == {remove_config_entry_id}: self.async_remove_device(device_id) - return + return None config_entries = config_entries - {remove_config_entry_id} if config_entries != old.config_entries: changes["config_entries"] = config_entries - for attr_name, value in ( + for attr_name, setvalue in ( ("connections", merge_connections), ("identifiers", merge_identifiers), ): old_value = getattr(old, attr_name) # If not undefined, check if `value` contains new items. - if value is not UNDEFINED and not value.issubset(old_value): - changes[attr_name] = old_value | value + if setvalue is not UNDEFINED and not setvalue.issubset(old_value): + changes[attr_name] = old_value | setvalue if new_identifiers is not UNDEFINED: changes["identifiers"] = new_identifiers @@ -434,7 +438,7 @@ class DeviceRegistry: ) self.async_schedule_save() - async def async_load(self): + async def async_load(self) -> None: """Load the device registry.""" async_setup_cleanup(self.hass, self) @@ -447,8 +451,9 @@ class DeviceRegistry: for device in data["devices"]: devices[device["id"]] = DeviceEntry( config_entries=set(device["config_entries"]), - connections={tuple(conn) for conn in device["connections"]}, - identifiers={tuple(iden) for iden in device["identifiers"]}, + # type ignores (if tuple arg was cast): likely https://github.com/python/mypy/issues/8625 + connections={tuple(conn) for conn in device["connections"]}, # type: ignore[misc] + identifiers={tuple(iden) for iden in device["identifiers"]}, # type: ignore[misc] manufacturer=device["manufacturer"], model=device["model"], name=device["name"], @@ -471,8 +476,9 @@ class DeviceRegistry: for device in data.get("deleted_devices", []): deleted_devices[device["id"]] = DeletedDeviceEntry( config_entries=set(device["config_entries"]), - connections={tuple(conn) for conn in device["connections"]}, - identifiers={tuple(iden) for iden in device["identifiers"]}, + # type ignores (if tuple arg was cast): likely https://github.com/python/mypy/issues/8625 + connections={tuple(conn) for conn in device["connections"]}, # type: ignore[misc] + identifiers={tuple(iden) for iden in device["identifiers"]}, # type: ignore[misc] id=device["id"], ) @@ -614,7 +620,7 @@ def async_setup_cleanup(hass: HomeAssistantType, dev_reg: DeviceRegistry) -> Non """Clean up device registry when entities removed.""" from . import entity_registry # pylint: disable=import-outside-toplevel - async def cleanup(): + async def cleanup() -> None: """Cleanup.""" ent_reg = await entity_registry.async_get_registry(hass) async_cleanup(hass, dev_reg, ent_reg) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 44f5c9c56f7..95497f7179c 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -18,7 +18,7 @@ from typing import ( List, Optional, Tuple, - cast, + Union, ) import attr @@ -39,13 +39,11 @@ from homeassistant.util import slugify from homeassistant.util.yaml import load_yaml from .singleton import singleton -from .typing import UNDEFINED, HomeAssistantType +from .typing import UNDEFINED, HomeAssistantType, UndefinedType if TYPE_CHECKING: from homeassistant.config_entries import ConfigEntry # noqa: F401 -# mypy: allow-untyped-defs, no-check-untyped-defs - PATH_REGISTRY = "entity_registry.yaml" DATA_REGISTRY = "entity_registry" EVENT_ENTITY_REGISTRY_UPDATED = "entity_registry_updated" @@ -222,7 +220,7 @@ class EntityRegistry: entity_id = self.async_get_entity_id(domain, platform, unique_id) if entity_id: - return self._async_update_entity( # type: ignore + return self._async_update_entity( entity_id, config_entry_id=config_entry_id or UNDEFINED, device_id=device_id or UNDEFINED, @@ -316,63 +314,56 @@ class EntityRegistry: for entity in entities: if entity.disabled_by != DISABLED_DEVICE: continue - self.async_update_entity( # type: ignore - entity.entity_id, disabled_by=None - ) + self.async_update_entity(entity.entity_id, disabled_by=None) return entities = async_entries_for_device(self, event.data["device_id"]) for entity in entities: - self.async_update_entity( # type: ignore - entity.entity_id, disabled_by=DISABLED_DEVICE - ) + self.async_update_entity(entity.entity_id, disabled_by=DISABLED_DEVICE) @callback def async_update_entity( self, - entity_id, + entity_id: str, *, - name=UNDEFINED, - icon=UNDEFINED, - area_id=UNDEFINED, - new_entity_id=UNDEFINED, - new_unique_id=UNDEFINED, - disabled_by=UNDEFINED, - ): + name: Union[str, None, UndefinedType] = UNDEFINED, + icon: Union[str, None, UndefinedType] = UNDEFINED, + area_id: Union[str, None, UndefinedType] = UNDEFINED, + new_entity_id: Union[str, UndefinedType] = UNDEFINED, + new_unique_id: Union[str, UndefinedType] = UNDEFINED, + disabled_by: Union[str, None, UndefinedType] = UNDEFINED, + ) -> RegistryEntry: """Update properties of an entity.""" - return cast( # cast until we have _async_update_entity type hinted - RegistryEntry, - self._async_update_entity( - entity_id, - name=name, - icon=icon, - area_id=area_id, - new_entity_id=new_entity_id, - new_unique_id=new_unique_id, - disabled_by=disabled_by, - ), + return self._async_update_entity( + entity_id, + name=name, + icon=icon, + area_id=area_id, + new_entity_id=new_entity_id, + new_unique_id=new_unique_id, + disabled_by=disabled_by, ) @callback def _async_update_entity( self, - entity_id, + entity_id: str, *, - name=UNDEFINED, - icon=UNDEFINED, - config_entry_id=UNDEFINED, - new_entity_id=UNDEFINED, - device_id=UNDEFINED, - area_id=UNDEFINED, - new_unique_id=UNDEFINED, - disabled_by=UNDEFINED, - capabilities=UNDEFINED, - supported_features=UNDEFINED, - device_class=UNDEFINED, - unit_of_measurement=UNDEFINED, - original_name=UNDEFINED, - original_icon=UNDEFINED, - ): + name: Union[str, None, UndefinedType] = UNDEFINED, + icon: Union[str, None, UndefinedType] = UNDEFINED, + config_entry_id: Union[str, None, UndefinedType] = UNDEFINED, + new_entity_id: Union[str, UndefinedType] = UNDEFINED, + device_id: Union[str, None, UndefinedType] = UNDEFINED, + area_id: Union[str, None, UndefinedType] = UNDEFINED, + new_unique_id: Union[str, UndefinedType] = UNDEFINED, + disabled_by: Union[str, None, UndefinedType] = UNDEFINED, + capabilities: Union[Dict[str, Any], None, UndefinedType] = UNDEFINED, + supported_features: Union[int, UndefinedType] = UNDEFINED, + device_class: Union[str, None, UndefinedType] = UNDEFINED, + unit_of_measurement: Union[str, None, UndefinedType] = UNDEFINED, + original_name: Union[str, None, UndefinedType] = UNDEFINED, + original_icon: Union[str, None, UndefinedType] = UNDEFINED, + ) -> RegistryEntry: """Private facing update properties method.""" old = self.entities[entity_id] @@ -526,7 +517,7 @@ class EntityRegistry: """Clear area id from registry entries.""" for entity_id, entry in self.entities.items(): if area_id == entry.area_id: - self._async_update_entity(entity_id, area_id=None) # type: ignore + self._async_update_entity(entity_id, area_id=None) def _register_entry(self, entry: RegistryEntry) -> None: self.entities[entry.entity_id] = entry From 86154744e436785674b1b98b08f372b77b39a8f7 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Tue, 5 Jan 2021 03:57:05 +0100 Subject: [PATCH 094/507] Implement color mode for ZHA light polling (#44829) --- homeassistant/components/zha/light.py | 35 +++++++++++++++++++-------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 4a25fa3c988..32b8a064054 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -1,6 +1,7 @@ """Lights on Zigbee Home Automation networks.""" from collections import Counter from datetime import timedelta +import enum import functools import itertools import logging @@ -88,6 +89,14 @@ SUPPORT_GROUP_LIGHT = ( ) +class LightColorMode(enum.IntEnum): + """ZCL light color mode enum.""" + + HS_COLOR = 0x00 + XY_COLOR = 0x01 + COLOR_TEMP = 0x02 + + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Zigbee Home Automation light from config entry.""" entities_to_create = hass.data[DATA_ZHA][light.DOMAIN] @@ -442,6 +451,7 @@ class Light(BaseLight, ZhaEntity): self._brightness = level if self._color_channel: attributes = [ + "color_mode", "color_temperature", "current_x", "current_y", @@ -452,16 +462,21 @@ class Light(BaseLight, ZhaEntity): attributes, from_cache=False ) - 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") - if color_x is not None and color_y is not None: - self._hs_color = color_util.color_xy_to_hs( - float(color_x / 65535), float(color_y / 65535) - ) + color_mode = results.get("color_mode") + if color_mode is not None: + if color_mode == LightColorMode.COLOR_TEMP: + color_temp = results.get("color_temperature") + if color_temp is not None and color_mode: + self._color_temp = color_temp + self._hs_color = None + else: + color_x = results.get("current_x") + color_y = results.get("current_y") + if color_x is not None and color_y is not None: + self._hs_color = color_util.color_xy_to_hs( + float(color_x / 65535), float(color_y / 65535) + ) + self._color_temp = None color_loop_active = results.get("color_loop_active") if color_loop_active is not None: From 6cd18971b1cbd044b3addbc461b3469602dc0ff6 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 4 Jan 2021 19:45:33 -0800 Subject: [PATCH 095/507] Propose an integration quality for nest SDM integration (#44755) * Propose the nest SDM integration is silver quality Co-authored-by: Paulus Schoutsen --- homeassistant/components/nest/manifest.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 7d60bb1cf5d..c334633e362 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -11,5 +11,6 @@ "codeowners": [ "@awarecan", "@allenporter" - ] + ], + "quality_scale": "platinum" } From 0c85ed1385a003f4fa0f6232ac93b12f448a6068 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Jan 2021 09:51:19 +0100 Subject: [PATCH 096/507] Bump codecov/codecov-action from v1.1.1 to v1.2.0 (#44836) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from v1.1.1 to v1.2.0. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/master/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v1.1.1...a92c414703a4bba586f6df7fcc885c9d0bdff772) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7c1c8bac6b6..746bd0d895f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -782,4 +782,4 @@ jobs: coverage report --fail-under=94 coverage xml - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1.1.1 + uses: codecov/codecov-action@v1.2.0 From 106252ea211acce37a363480aea57fd112d7bcce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Jan 2021 09:51:48 +0100 Subject: [PATCH 097/507] Bump actions/upload-artifact from v2.2.1 to v2.2.2 (#44835) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from v2.2.1 to v2.2.2. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v2.2.1...e448a9b857ee2131e752b06002bf0e093c65e571) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 746bd0d895f..ac1ef6b7375 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -739,7 +739,7 @@ jobs: -p no:sugar \ tests - name: Upload coverage artifact - uses: actions/upload-artifact@v2.2.1 + uses: actions/upload-artifact@v2.2.2 with: name: coverage-${{ matrix.python-version }}-group${{ matrix.group }} path: .coverage From addafd517f3617071468b2f4ae3fa31f655a9ed2 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Tue, 5 Jan 2021 10:01:34 +0100 Subject: [PATCH 098/507] Bump pypck to 0.7.8 (#44834) --- homeassistant/components/lcn/__init__.py | 3 ++- homeassistant/components/lcn/binary_sensor.py | 17 ++++++++++------- homeassistant/components/lcn/climate.py | 5 +++-- homeassistant/components/lcn/cover.py | 3 ++- homeassistant/components/lcn/light.py | 6 ++++-- homeassistant/components/lcn/manifest.json | 8 ++++++-- homeassistant/components/lcn/sensor.py | 6 ++++-- homeassistant/components/lcn/switch.py | 6 ++++-- requirements_all.txt | 2 +- 9 files changed, 36 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index 72f11b7b005..cc1e47d71fc 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -139,7 +139,8 @@ class LcnEntity(Entity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" - self.device_connection.register_for_inputs(self.input_received) + if not self.device_connection.is_group: + self.device_connection.register_for_inputs(self.input_received) @property def name(self): diff --git a/homeassistant/components/lcn/binary_sensor.py b/homeassistant/components/lcn/binary_sensor.py index 5d712045c93..415668f5924 100644 --- a/homeassistant/components/lcn/binary_sensor.py +++ b/homeassistant/components/lcn/binary_sensor.py @@ -50,9 +50,10 @@ class LcnRegulatorLockSensor(LcnEntity, BinarySensorEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler( - self.setpoint_variable - ) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler( + self.setpoint_variable + ) @property def is_on(self): @@ -85,9 +86,10 @@ class LcnBinarySensor(LcnEntity, BinarySensorEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler( - self.bin_sensor_port - ) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler( + self.bin_sensor_port + ) @property def is_on(self): @@ -116,7 +118,8 @@ class LcnLockKeysSensor(LcnEntity, BinarySensorEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.source) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.source) @property def is_on(self): diff --git a/homeassistant/components/lcn/climate.py b/homeassistant/components/lcn/climate.py index e3eb92a426f..ece3994f651 100644 --- a/homeassistant/components/lcn/climate.py +++ b/homeassistant/components/lcn/climate.py @@ -63,8 +63,9 @@ class LcnClimate(LcnEntity, ClimateEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.variable) - await self.device_connection.activate_status_request_handler(self.setpoint) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.variable) + await self.device_connection.activate_status_request_handler(self.setpoint) @property def supported_features(self): diff --git a/homeassistant/components/lcn/cover.py b/homeassistant/components/lcn/cover.py index c5e407573ba..3d7c2a06a3b 100644 --- a/homeassistant/components/lcn/cover.py +++ b/homeassistant/components/lcn/cover.py @@ -161,7 +161,8 @@ class LcnRelayCover(LcnEntity, CoverEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.motor) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.motor) @property def is_closed(self): diff --git a/homeassistant/components/lcn/light.py b/homeassistant/components/lcn/light.py index c6ef895b7df..5242ed1cc59 100644 --- a/homeassistant/components/lcn/light.py +++ b/homeassistant/components/lcn/light.py @@ -68,7 +68,8 @@ class LcnOutputLight(LcnEntity, LightEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.output) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.output) @property def supported_features(self): @@ -155,7 +156,8 @@ class LcnRelayLight(LcnEntity, LightEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.output) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.output) @property def is_on(self): diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index f07c4d9c646..919051d7e7a 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -2,6 +2,10 @@ "domain": "lcn", "name": "LCN", "documentation": "https://www.home-assistant.io/integrations/lcn", - "requirements": ["pypck==0.7.7"], - "codeowners": ["@alengwenus"] + "requirements": [ + "pypck==0.7.8" + ], + "codeowners": [ + "@alengwenus" + ] } diff --git a/homeassistant/components/lcn/sensor.py b/homeassistant/components/lcn/sensor.py index 26b54def974..4d4be5e1259 100644 --- a/homeassistant/components/lcn/sensor.py +++ b/homeassistant/components/lcn/sensor.py @@ -57,7 +57,8 @@ class LcnVariableSensor(LcnEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.variable) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.variable) @property def state(self): @@ -98,7 +99,8 @@ class LcnLedLogicSensor(LcnEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.source) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.source) @property def state(self): diff --git a/homeassistant/components/lcn/switch.py b/homeassistant/components/lcn/switch.py index 5891629627e..6f9cc25db99 100644 --- a/homeassistant/components/lcn/switch.py +++ b/homeassistant/components/lcn/switch.py @@ -50,7 +50,8 @@ class LcnOutputSwitch(LcnEntity, SwitchEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.output) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.output) @property def is_on(self): @@ -97,7 +98,8 @@ class LcnRelaySwitch(LcnEntity, SwitchEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.output) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.output) @property def is_on(self): diff --git a/requirements_all.txt b/requirements_all.txt index 85a8fbaaaa0..2c0d9a4270c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1607,7 +1607,7 @@ pyownet==0.10.0.post1 pypca==0.0.7 # homeassistant.components.lcn -pypck==0.7.7 +pypck==0.7.8 # homeassistant.components.pjlink pypjlink2==1.2.1 From c654476e24b93a2e90014e3c5b64b758979e3462 Mon Sep 17 00:00:00 2001 From: Shane Qi Date: Tue, 5 Jan 2021 04:24:30 -0600 Subject: [PATCH 099/507] Add support for reordering Shopping List Items via Drag and Drop (#41585) Co-authored-by: Paulus Schoutsen --- .../components/shopping_list/__init__.py | 50 +++++++ tests/components/shopping_list/test_init.py | 128 +++++++++++++++++- 2 files changed, 177 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py index 8618e9bafb7..1831f894cec 100644 --- a/homeassistant/components/shopping_list/__init__.py +++ b/homeassistant/components/shopping_list/__init__.py @@ -128,6 +128,8 @@ async def async_setup_entry(hass, config_entry): SCHEMA_WEBSOCKET_CLEAR_ITEMS, ) + websocket_api.async_register_command(hass, websocket_handle_reorder) + return True @@ -163,6 +165,31 @@ class ShoppingData: self.items = [itm for itm in self.items if not itm["complete"]] await self.hass.async_add_executor_job(self.save) + @callback + def async_reorder(self, item_ids): + """Reorder items.""" + # The array for sorted items. + new_items = [] + all_items_mapping = {item["id"]: item for item in self.items} + # Append items by the order of passed in array. + for item_id in item_ids: + if item_id not in all_items_mapping: + raise KeyError + new_items.append(all_items_mapping[item_id]) + # Remove the item from mapping after it's appended in the result array. + del all_items_mapping[item_id] + # Append the rest of the items + for key in all_items_mapping: + # All the unchecked items must be passed in the item_ids array, + # so all items left in the mapping should be checked items. + if all_items_mapping[key]["complete"] is False: + raise vol.Invalid( + "The item ids array doesn't contain all the unchecked shopping list items." + ) + new_items.append(all_items_mapping[key]) + self.items = new_items + self.hass.async_add_executor_job(self.save) + async def async_load(self): """Load items.""" @@ -277,3 +304,26 @@ async def websocket_handle_clear(hass, connection, msg): await hass.data[DOMAIN].async_clear_completed() hass.bus.async_fire(EVENT, {"action": "clear"}) connection.send_message(websocket_api.result_message(msg["id"])) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "shopping_list/items/reorder", + vol.Required("item_ids"): [str], + } +) +def websocket_handle_reorder(hass, connection, msg): + """Handle reordering shopping_list items.""" + msg_id = msg.pop("id") + try: + hass.data[DOMAIN].async_reorder(msg.pop("item_ids")) + hass.bus.async_fire(EVENT, {"action": "reorder"}) + connection.send_result(msg_id) + except KeyError: + connection.send_error( + msg_id, + websocket_api.const.ERR_NOT_FOUND, + "One or more item id(s) not found.", + ) + except vol.Invalid as err: + connection.send_error(msg_id, websocket_api.const.ERR_INVALID_FORMAT, f"{err}") diff --git a/tests/components/shopping_list/test_init.py b/tests/components/shopping_list/test_init.py index b8930114665..0be4c70ef18 100644 --- a/tests/components/shopping_list/test_init.py +++ b/tests/components/shopping_list/test_init.py @@ -1,6 +1,10 @@ """Test shopping list component.""" -from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.components.websocket_api.const import ( + ERR_INVALID_FORMAT, + ERR_NOT_FOUND, + TYPE_RESULT, +) from homeassistant.const import HTTP_NOT_FOUND from homeassistant.helpers import intent @@ -311,3 +315,125 @@ async def test_ws_add_item_fail(hass, hass_ws_client, sl_setup): msg = await client.receive_json() assert msg["success"] is False assert len(hass.data["shopping_list"].items) == 0 + + +async def test_ws_reorder_items(hass, hass_ws_client, sl_setup): + """Test reordering shopping_list items websocket command.""" + await intent.async_handle( + hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} + ) + await intent.async_handle( + hass, "test", "HassShoppingListAddItem", {"item": {"value": "wine"}} + ) + await intent.async_handle( + hass, "test", "HassShoppingListAddItem", {"item": {"value": "apple"}} + ) + + beer_id = hass.data["shopping_list"].items[0]["id"] + wine_id = hass.data["shopping_list"].items[1]["id"] + apple_id = hass.data["shopping_list"].items[2]["id"] + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 6, + "type": "shopping_list/items/reorder", + "item_ids": [wine_id, apple_id, beer_id], + } + ) + msg = await client.receive_json() + assert msg["success"] is True + assert hass.data["shopping_list"].items[0] == { + "id": wine_id, + "name": "wine", + "complete": False, + } + assert hass.data["shopping_list"].items[1] == { + "id": apple_id, + "name": "apple", + "complete": False, + } + assert hass.data["shopping_list"].items[2] == { + "id": beer_id, + "name": "beer", + "complete": False, + } + + # Mark wine as completed. + await client.send_json( + { + "id": 7, + "type": "shopping_list/items/update", + "item_id": wine_id, + "complete": True, + } + ) + _ = await client.receive_json() + + await client.send_json( + { + "id": 8, + "type": "shopping_list/items/reorder", + "item_ids": [apple_id, beer_id], + } + ) + msg = await client.receive_json() + assert msg["success"] is True + assert hass.data["shopping_list"].items[0] == { + "id": apple_id, + "name": "apple", + "complete": False, + } + assert hass.data["shopping_list"].items[1] == { + "id": beer_id, + "name": "beer", + "complete": False, + } + assert hass.data["shopping_list"].items[2] == { + "id": wine_id, + "name": "wine", + "complete": True, + } + + +async def test_ws_reorder_items_failure(hass, hass_ws_client, sl_setup): + """Test reordering shopping_list items websocket command.""" + await intent.async_handle( + hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} + ) + await intent.async_handle( + hass, "test", "HassShoppingListAddItem", {"item": {"value": "wine"}} + ) + await intent.async_handle( + hass, "test", "HassShoppingListAddItem", {"item": {"value": "apple"}} + ) + + beer_id = hass.data["shopping_list"].items[0]["id"] + wine_id = hass.data["shopping_list"].items[1]["id"] + apple_id = hass.data["shopping_list"].items[2]["id"] + + client = await hass_ws_client(hass) + + # Testing sending bad item id. + await client.send_json( + { + "id": 8, + "type": "shopping_list/items/reorder", + "item_ids": [wine_id, apple_id, beer_id, "BAD_ID"], + } + ) + msg = await client.receive_json() + assert msg["success"] is False + assert msg["error"]["code"] == ERR_NOT_FOUND + + # Testing not sending all unchecked item ids. + await client.send_json( + { + "id": 9, + "type": "shopping_list/items/reorder", + "item_ids": [wine_id, apple_id], + } + ) + msg = await client.receive_json() + assert msg["success"] is False + assert msg["error"]["code"] == ERR_INVALID_FORMAT From 853420d97259e07278a54439d9cec7c305becb7b Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Tue, 5 Jan 2021 12:12:31 +0100 Subject: [PATCH 100/507] Add Vicare set mode service (#44563) * vicare: add set_vicare_mode service The set_vicare_mode service allows the user to set any of the possible heating modes of their heating device. Not just the ones that were mapped to home assistant climate modes. * vicare: Undo async changes and add heating mode Useless async changes were undone. To be able to set the most relevant modes the set_vicare_mode shall be able to also set the heating mode (without domestic hot water) * Extract kwarg and undo some more async changes Currectly extract the service argument Adapt according to review * Lint fixes * Replace kwargs with single arg Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/vicare/climate.py | 47 ++++++++++++++----- homeassistant/components/vicare/services.yaml | 9 ++++ 2 files changed, 44 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/vicare/services.yaml diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index ddfb28478df..d1accd8ea0a 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -2,6 +2,7 @@ import logging import requests +import voluptuous as vol from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -16,6 +17,7 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS +from homeassistant.helpers import entity_platform from . import ( DOMAIN as VICARE_DOMAIN, @@ -28,7 +30,11 @@ from . import ( _LOGGER = logging.getLogger(__name__) +SERVICE_SET_VICARE_MODE = "set_vicare_mode" +SERVICE_SET_VICARE_MODE_ATTR_MODE = "vicare_mode" + VICARE_MODE_DHW = "dhw" +VICARE_MODE_HEATING = "heating" VICARE_MODE_DHWANDHEATING = "dhwAndHeating" VICARE_MODE_DHWANDHEATINGCOOLING = "dhwAndHeatingCooling" VICARE_MODE_FORCEDREDUCED = "forcedReduced" @@ -55,6 +61,7 @@ SUPPORT_FLAGS_HEATING = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE VICARE_TO_HA_HVAC_HEATING = { VICARE_MODE_DHW: HVAC_MODE_OFF, + VICARE_MODE_HEATING: HVAC_MODE_HEAT, VICARE_MODE_DHWANDHEATING: HVAC_MODE_AUTO, VICARE_MODE_DHWANDHEATINGCOOLING: HVAC_MODE_AUTO, VICARE_MODE_FORCEDREDUCED: HVAC_MODE_OFF, @@ -79,22 +86,36 @@ HA_TO_VICARE_PRESET_HEATING = { } -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform( + hass, hass_config, async_add_entities, discovery_info=None +): """Create the ViCare climate devices.""" if discovery_info is None: return vicare_api = hass.data[VICARE_DOMAIN][VICARE_API] heating_type = hass.data[VICARE_DOMAIN][VICARE_HEATING_TYPE] - add_entities( + async_add_entities( [ ViCareClimate( - f"{hass.data[VICARE_DOMAIN][VICARE_NAME]} Heating", + f"{hass.data[VICARE_DOMAIN][VICARE_NAME]} Heating", vicare_api, heating_type, ) ] ) + platform = entity_platform.current_platform.get() + + platform.async_register_entity_service( + SERVICE_SET_VICARE_MODE, + { + vol.Required(SERVICE_SET_VICARE_MODE_ATTR_MODE): vol.In( + VICARE_TO_HA_HVAC_HEATING + ), + }, + "set_vicare_mode", + ) + class ViCareClimate(ClimateEntity): """Representation of the ViCare heating climate device.""" @@ -154,7 +175,6 @@ class ViCareClimate(ClimateEntity): elif self._heating_type == HeatingType.heatpump: self._current_action = self._api.getCompressorActive() - except requests.exceptions.ConnectionError: _LOGGER.error("Unable to retrieve data from ViCare server") except ValueError: @@ -194,10 +214,9 @@ class ViCareClimate(ClimateEntity): """Set a new hvac mode on the ViCare API.""" vicare_mode = HA_TO_VICARE_HVAC_HEATING.get(hvac_mode) if vicare_mode is None: - _LOGGER.error( - "Cannot set invalid vicare mode: %s / %s", hvac_mode, vicare_mode + raise ValueError( + f"Cannot set invalid vicare mode: {hvac_mode} / {vicare_mode}" ) - return _LOGGER.debug("Setting hvac mode to %s / %s", hvac_mode, vicare_mode) self._api.setMode(vicare_mode) @@ -250,12 +269,9 @@ class ViCareClimate(ClimateEntity): """Set new preset mode and deactivate any existing programs.""" vicare_program = HA_TO_VICARE_PRESET_HEATING.get(preset_mode) if vicare_program is None: - _LOGGER.error( - "Cannot set invalid vicare program: %s / %s", - preset_mode, - vicare_program, + raise ValueError( + f"Cannot set invalid vicare program: {preset_mode}/{vicare_program}" ) - return _LOGGER.debug("Setting preset to %s / %s", preset_mode, vicare_program) self._api.deactivateProgram(self._current_program) @@ -265,3 +281,10 @@ class ViCareClimate(ClimateEntity): def device_state_attributes(self): """Show Device Attributes.""" return self._attributes + + def set_vicare_mode(self, vicare_mode): + """Service function to set vicare modes directly.""" + if vicare_mode not in VICARE_TO_HA_HVAC_HEATING: + raise ValueError(f"Cannot set invalid vicare mode: {vicare_mode}") + + self._api.setMode(vicare_mode) diff --git a/homeassistant/components/vicare/services.yaml b/homeassistant/components/vicare/services.yaml new file mode 100644 index 00000000000..2efaf530a9c --- /dev/null +++ b/homeassistant/components/vicare/services.yaml @@ -0,0 +1,9 @@ +set_vicare_mode: + description: Set a ViCare mode. + fields: + entity_id: + description: Name(s) of vicare climate entities. + example: "climate.vicare_heating" + vicare_mode: + description: ViCare mode. One of "dhw", "dhwAndHeating", "heating", "dhwAndHeatingCooling", "forcedReduced", "forcedNormal" or "standby" + example: "dhw" From 67eebce55a766937af0fc083e8bb3d82fd2bbdfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 5 Jan 2021 14:55:38 +0200 Subject: [PATCH 101/507] Better general/fallback error message and traceback for unknown config errors (#44655) * Include error repr in config error message is str(error) yields nothing * Log traceback for config errors we don't have a "friendly" formatter for --- homeassistant/config.py | 11 +++++++---- homeassistant/helpers/check_config.py | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index c3fe18d69df..2da9b0331c9 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -412,17 +412,19 @@ def async_log_exception( """ if hass is not None: async_notify_setup_error(hass, domain, link) - _LOGGER.error(_format_config_error(ex, domain, config, link)) + message, is_friendly = _format_config_error(ex, domain, config, link) + _LOGGER.error(message, exc_info=not is_friendly and ex) @callback def _format_config_error( ex: Exception, domain: str, config: Dict, link: Optional[str] = None -) -> str: +) -> Tuple[str, bool]: """Generate log exception for configuration validation. This method must be run in the event loop. """ + is_friendly = False message = f"Invalid config for [{domain}]: " if isinstance(ex, vol.Invalid): if "extra keys not allowed" in ex.error_message: @@ -433,8 +435,9 @@ def _format_config_error( ) else: message += f"{humanize_error(config, ex)}." + is_friendly = True else: - message += str(ex) + message += str(ex) or repr(ex) try: domain_config = config.get(domain, config) @@ -449,7 +452,7 @@ def _format_config_error( if domain != CONF_CORE and link: message += f"Please check the docs at {link}" - return message + return message, is_friendly async def async_process_ha_core_config(hass: HomeAssistant, config: Dict) -> None: diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py index c98b563ac7e..97445b8cee2 100644 --- a/homeassistant/helpers/check_config.py +++ b/homeassistant/helpers/check_config.py @@ -78,7 +78,7 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> HomeAssistantConfig def _comp_error(ex: Exception, domain: str, config: ConfigType) -> None: """Handle errors from components: async_log_exception.""" - result.add_error(_format_config_error(ex, domain, config), domain, config) + result.add_error(_format_config_error(ex, domain, config)[0], domain, config) # Load configuration.yaml config_path = hass.config.path(YAML_CONFIG_FILE) From 16e1046dbc2cf28646a806af171f03b435703012 Mon Sep 17 00:00:00 2001 From: Finbarr Brady Date: Tue, 5 Jan 2021 16:22:25 +0000 Subject: [PATCH 102/507] =?UTF-8?q?Bump=20openwebifpy=20version:=203.1.6?= =?UTF-8?q?=20=E2=86=92=203.2.7=20(#44847)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homeassistant/components/enigma2/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/enigma2/manifest.json b/homeassistant/components/enigma2/manifest.json index fe461654ecd..da6765368ae 100644 --- a/homeassistant/components/enigma2/manifest.json +++ b/homeassistant/components/enigma2/manifest.json @@ -2,6 +2,6 @@ "domain": "enigma2", "name": "Enigma2 (OpenWebif)", "documentation": "https://www.home-assistant.io/integrations/enigma2", - "requirements": ["openwebifpy==3.1.6"], + "requirements": ["openwebifpy==3.2.7"], "codeowners": ["@fbradyirl"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2c0d9a4270c..ab1209437ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1061,7 +1061,7 @@ openhomedevice==0.7.2 opensensemap-api==0.1.5 # homeassistant.components.enigma2 -openwebifpy==3.1.6 +openwebifpy==3.2.7 # homeassistant.components.luci openwrt-luci-rpc==1.1.6 From 69b5176730caba7fb49239049b9531192c385245 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 5 Jan 2021 17:35:28 +0100 Subject: [PATCH 103/507] Make Alexa custom ID unique (#44839) * Make Alexa custom ID unique * Lint * Lint --- homeassistant/components/alexa/config.py | 5 +++++ homeassistant/components/alexa/entities.py | 2 +- .../components/alexa/smart_home_http.py | 5 +++++ .../components/cloud/alexa_config.py | 19 ++++++++++++++--- homeassistant/components/cloud/client.py | 16 ++++++++------ .../components/cloud/google_config.py | 2 +- homeassistant/components/cloud/http_api.py | 9 +++++--- tests/components/alexa/__init__.py | 7 ++++++- tests/components/alexa/test_entities.py | 21 +++++++++++++++++++ tests/components/cloud/test_alexa_config.py | 17 ++++++++++----- 10 files changed, 83 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/alexa/config.py b/homeassistant/components/alexa/config.py index 7d3a3994ace..cc5c604dc8c 100644 --- a/homeassistant/components/alexa/config.py +++ b/homeassistant/components/alexa/config.py @@ -45,6 +45,11 @@ class AbstractConfig(ABC): """Return if proactive mode is enabled.""" return self._unsub_proactive_report is not None + @callback + @abstractmethod + def user_identifier(self): + """Return an identifier for the user that represents this config.""" + async def async_enable_proactive_mode(self): """Enable proactive mode.""" if self._unsub_proactive_report is None: diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 574ba6b8ba7..c05d9641b9a 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -329,7 +329,7 @@ class AlexaEntity: "manufacturer": "Home Assistant", "model": self.entity.domain, "softwareVersion": __version__, - "customIdentifier": self.entity_id, + "customIdentifier": f"{self.config.user_identifier()}-{self.entity_id}", }, } diff --git a/homeassistant/components/alexa/smart_home_http.py b/homeassistant/components/alexa/smart_home_http.py index 41ebfb340eb..41738c824fb 100644 --- a/homeassistant/components/alexa/smart_home_http.py +++ b/homeassistant/components/alexa/smart_home_http.py @@ -53,6 +53,11 @@ class AlexaConfig(AbstractConfig): """Return config locale.""" return self._config.get(CONF_LOCALE) + @core.callback + def user_identifier(self): + """Return an identifier for the user that represents this config.""" + return "" + def should_expose(self, entity_id): """If an entity should be exposed.""" return self._config[CONF_FILTER](entity_id) diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 1bb74053ea4..7abbefe85ff 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -5,7 +5,7 @@ import logging import aiohttp import async_timeout -from hass_nabucasa import cloud_api +from hass_nabucasa import Cloud, cloud_api from homeassistant.components.alexa import ( config as alexa_config, @@ -14,7 +14,7 @@ from homeassistant.components.alexa import ( state_report as alexa_state_report, ) from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES, HTTP_BAD_REQUEST -from homeassistant.core import callback, split_entity_id +from homeassistant.core import HomeAssistant, callback, split_entity_id from homeassistant.helpers import entity_registry from homeassistant.helpers.event import async_call_later from homeassistant.util.dt import utcnow @@ -32,10 +32,18 @@ SYNC_DELAY = 1 class AlexaConfig(alexa_config.AbstractConfig): """Alexa Configuration.""" - def __init__(self, hass, config, prefs: CloudPreferences, cloud): + def __init__( + self, + hass: HomeAssistant, + config: dict, + cloud_user: str, + prefs: CloudPreferences, + cloud: Cloud, + ): """Initialize the Alexa config.""" super().__init__(hass) self._config = config + self._cloud_user = cloud_user self._prefs = prefs self._cloud = cloud self._token = None @@ -85,6 +93,11 @@ class AlexaConfig(alexa_config.AbstractConfig): """Return entity config.""" return self._config.get(CONF_ENTITY_CONFIG) or {} + @callback + def user_identifier(self): + """Return an identifier for the user that represents this config.""" + return self._cloud_user + def should_expose(self, entity_id): """If an entity should be exposed.""" if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 2a2d383f362..155a39e49b6 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -79,13 +79,15 @@ class CloudClient(Interface): """Return true if we want start a remote connection.""" return self._prefs.remote_enabled - @property - def alexa_config(self) -> alexa_config.AlexaConfig: + async def get_alexa_config(self) -> alexa_config.AlexaConfig: """Return Alexa config.""" if self._alexa_config is None: assert self.cloud is not None + + cloud_user = await self._prefs.get_cloud_user() + self._alexa_config = alexa_config.AlexaConfig( - self._hass, self.alexa_user_config, self._prefs, self.cloud + self._hass, self.alexa_user_config, cloud_user, self._prefs, self.cloud ) return self._alexa_config @@ -110,8 +112,9 @@ class CloudClient(Interface): async def enable_alexa(_): """Enable Alexa.""" + aconf = await self.get_alexa_config() try: - await self.alexa_config.async_enable_proactive_mode() + await aconf.async_enable_proactive_mode() except aiohttp.ClientError as err: # If no internet available yet if self._hass.is_running: logging.getLogger(__package__).warning( @@ -133,7 +136,7 @@ class CloudClient(Interface): tasks = [] - if self.alexa_config.enabled and self.alexa_config.should_report_state: + if self._prefs.alexa_enabled and self._prefs.alexa_report_state: tasks.append(enable_alexa) if self._prefs.google_enabled: @@ -164,9 +167,10 @@ class CloudClient(Interface): async def async_alexa_message(self, payload: Dict[Any, Any]) -> Dict[Any, Any]: """Process cloud alexa message to client.""" cloud_user = await self._prefs.get_cloud_user() + aconfig = await self.get_alexa_config() return await alexa_sh.async_handle_message( self._hass, - self.alexa_config, + aconfig, payload, context=Context(user_id=cloud_user), enabled=self._prefs.alexa_enabled, diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 4b5891359b6..2ac0bc40252 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -28,7 +28,7 @@ _LOGGER = logging.getLogger(__name__) class CloudGoogleConfig(AbstractConfig): """HA Cloud Configuration for Google Assistant.""" - def __init__(self, hass, config, cloud_user, prefs: CloudPreferences, cloud): + def __init__(self, hass, config, cloud_user: str, prefs: CloudPreferences, cloud): """Initialize the Google config.""" super().__init__(hass) self._config = config diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 3075f6a3f9d..a4d8b84b1ad 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -397,9 +397,10 @@ async def websocket_update_prefs(hass, connection, msg): # If we turn alexa linking on, validate that we can fetch access token if changes.get(PREF_ALEXA_REPORT_STATE): + alexa_config = await cloud.client.get_alexa_config() try: with async_timeout.timeout(10): - await cloud.client.alexa_config.async_get_access_token() + await alexa_config.async_get_access_token() except asyncio.TimeoutError: connection.send_error( msg["id"], "alexa_timeout", "Timeout validating Alexa access token." @@ -555,7 +556,8 @@ async def google_assistant_update(hass, connection, msg): async def alexa_list(hass, connection, msg): """List all alexa entities.""" cloud = hass.data[DOMAIN] - entities = alexa_entities.async_get_entities(hass, cloud.client.alexa_config) + alexa_config = await cloud.client.get_alexa_config() + entities = alexa_entities.async_get_entities(hass, alexa_config) result = [] @@ -603,10 +605,11 @@ async def alexa_update(hass, connection, msg): async def alexa_sync(hass, connection, msg): """Sync with Alexa.""" cloud = hass.data[DOMAIN] + alexa_config = await cloud.client.get_alexa_config() with async_timeout.timeout(10): try: - success = await cloud.client.alexa_config.async_sync_entities() + success = await alexa_config.async_sync_entities() except alexa_errors.NoTokenAvailable: connection.send_error( msg["id"], diff --git a/tests/components/alexa/__init__.py b/tests/components/alexa/__init__.py index d9c1a5a40cd..bc007fefb84 100644 --- a/tests/components/alexa/__init__.py +++ b/tests/components/alexa/__init__.py @@ -2,7 +2,7 @@ from uuid import uuid4 from homeassistant.components.alexa import config, smart_home -from homeassistant.core import Context +from homeassistant.core import Context, callback from tests.common import async_mock_service @@ -37,6 +37,11 @@ class MockConfig(config.AbstractConfig): """Return config locale.""" return TEST_LOCALE + @callback + def user_identifier(self): + """Return an identifier for the user that represents this config.""" + return "mock-user-id" + def should_expose(self, entity_id): """If an entity should be exposed.""" return True diff --git a/tests/components/alexa/test_entities.py b/tests/components/alexa/test_entities.py index c1769bc8d06..9a1ef032762 100644 --- a/tests/components/alexa/test_entities.py +++ b/tests/components/alexa/test_entities.py @@ -2,6 +2,7 @@ from unittest.mock import patch from homeassistant.components.alexa import smart_home +from homeassistant.const import __version__ from . import DEFAULT_CONFIG, get_new_request @@ -20,6 +21,26 @@ async def test_unsupported_domain(hass): assert not msg["payload"]["endpoints"] +async def test_serialize_discovery(hass): + """Test we handle an interface raising unexpectedly during serialize discovery.""" + request = get_new_request("Alexa.Discovery", "Discover") + + hass.states.async_set("switch.bla", "on", {"friendly_name": "Boop Woz"}) + + msg = await smart_home.async_handle_message(hass, DEFAULT_CONFIG, request) + + assert "event" in msg + msg = msg["event"] + endpoint = msg["payload"]["endpoints"][0] + + assert endpoint["additionalAttributes"] == { + "manufacturer": "Home Assistant", + "model": "switch", + "softwareVersion": __version__, + "customIdentifier": "mock-user-id-switch.bla", + } + + async def test_serialize_discovery_recovers(hass, caplog): """Test we handle an interface raising unexpectedly during serialize discovery.""" request = get_new_request("Alexa.Discovery", "Discover") diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index 7286ece2c53..966ef4b0af3 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -16,7 +16,9 @@ async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs): alexa_entity_configs={"light.kitchen": entity_conf}, alexa_default_expose=["light"], ) - conf = alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None) + conf = alexa_config.AlexaConfig( + hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, None + ) assert not conf.should_expose("light.kitchen") entity_conf["should_expose"] = True @@ -33,7 +35,9 @@ async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs): async def test_alexa_config_report_state(hass, cloud_prefs): """Test Alexa config should expose using prefs.""" - conf = alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None) + conf = alexa_config.AlexaConfig( + hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, None + ) assert cloud_prefs.alexa_report_state is False assert conf.should_report_state is False @@ -68,6 +72,7 @@ async def test_alexa_config_invalidate_token(hass, cloud_prefs, aioclient_mock): conf = alexa_config.AlexaConfig( hass, ALEXA_SCHEMA({}), + "mock-user-id", cloud_prefs, Mock( alexa_access_token_url="http://example/alexa_token", @@ -114,7 +119,7 @@ def patch_sync_helper(): async def test_alexa_update_expose_trigger_sync(hass, cloud_prefs): """Test Alexa config responds to updating exposed entities.""" - alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None) + alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, None) with patch_sync_helper() as (to_update, to_remove): await cloud_prefs.async_update_alexa_entity_config( @@ -147,7 +152,9 @@ async def test_alexa_update_expose_trigger_sync(hass, cloud_prefs): async def test_alexa_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): """Test Alexa config responds to entity registry.""" - alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, hass.data["cloud"]) + alexa_config.AlexaConfig( + hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"] + ) with patch_sync_helper() as (to_update, to_remove): hass.bus.async_fire( @@ -197,7 +204,7 @@ async def test_alexa_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): async def test_alexa_update_report_state(hass, cloud_prefs): """Test Alexa config responds to reporting state.""" - alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None) + alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, None) with patch( "homeassistant.components.cloud.alexa_config.AlexaConfig.async_sync_entities", From f1c116831fa894f8e1bb68127876cacd36a62fbb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 5 Jan 2021 18:35:54 +0100 Subject: [PATCH 104/507] Patch Shelly test setting up entry (#44842) --- tests/components/shelly/test_config_flow.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 592ac7a384c..60f899296f6 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -255,7 +255,12 @@ async def test_user_setup_ignored_device(hass): settings=settings, ) ), - ): + ), patch( + "homeassistant.components.shelly.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.shelly.async_setup_entry", + return_value=True, + ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -266,6 +271,8 @@ async def test_user_setup_ignored_device(hass): # Test config entry got updated with latest IP assert entry.data["host"] == "1.1.1.1" + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 async def test_form_firmware_unsupported(hass): From 35edc405374b4fadd95e97c05ae705e54a4e2c28 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Tue, 5 Jan 2021 20:46:54 +0100 Subject: [PATCH 105/507] Fix opentherm_gw firmware version in device registry (#44756) --- .../components/opentherm_gw/__init__.py | 23 +++++- tests/components/opentherm_gw/test_init.py | 73 +++++++++++++++++++ 2 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 tests/components/opentherm_gw/test_init.py diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 8d1d3ae4d62..cc08ec3da69 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -26,6 +26,9 @@ from homeassistant.const import ( PRECISION_WHOLE, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import ( + async_get_registry as async_get_dev_reg, +) from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( @@ -404,6 +407,7 @@ class OpenThermGatewayDevice: self.gw_id = config_entry.data[CONF_ID] self.name = config_entry.data[CONF_NAME] self.climate_config = config_entry.options + self.config_entry_id = config_entry.entry_id self.status = {} self.update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_update" self.options_update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_options_update" @@ -419,9 +423,22 @@ class OpenThermGatewayDevice: async def connect_and_subscribe(self): """Connect to serial device and subscribe report handler.""" self.status = await self.gateway.connect(self.hass.loop, self.device_path) - _LOGGER.debug("Connected to OpenTherm Gateway at %s", self.device_path) - self.gw_version = self.status.get(gw_vars.OTGW_BUILD) - + version_string = self.status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT) + self.gw_version = version_string[18:] if version_string else None + _LOGGER.debug( + "Connected to OpenTherm Gateway %s at %s", self.gw_version, self.device_path + ) + dev_reg = await async_get_dev_reg(self.hass) + gw_dev = dev_reg.async_get_or_create( + config_entry_id=self.config_entry_id, + identifiers={(DOMAIN, self.gw_id)}, + name=self.name, + manufacturer="Schelte Bron", + model="OpenTherm Gateway", + sw_version=self.gw_version, + ) + if gw_dev.sw_version != self.gw_version: + dev_reg.async_update_device(gw_dev.id, sw_version=self.gw_version) self.hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, self.cleanup) async def handle_report(status): diff --git a/tests/components/opentherm_gw/test_init.py b/tests/components/opentherm_gw/test_init.py new file mode 100644 index 00000000000..51d75bf0923 --- /dev/null +++ b/tests/components/opentherm_gw/test_init.py @@ -0,0 +1,73 @@ +"""Test Opentherm Gateway init.""" +from unittest.mock import patch + +from pyotgw.vars import OTGW, OTGW_ABOUT + +from homeassistant import setup +from homeassistant.components.opentherm_gw.const import DOMAIN +from homeassistant.const import CONF_DEVICE, CONF_ID, CONF_NAME + +from tests.common import MockConfigEntry, mock_device_registry + +VERSION_OLD = "4.2.5" +VERSION_NEW = "4.2.8.1" +MINIMAL_STATUS = {OTGW: {OTGW_ABOUT: f"OpenTherm Gateway {VERSION_OLD}"}} +MINIMAL_STATUS_UPD = {OTGW: {OTGW_ABOUT: f"OpenTherm Gateway {VERSION_NEW}"}} +MOCK_GATEWAY_ID = "mock_gateway" +MOCK_CONFIG_ENTRY = MockConfigEntry( + domain=DOMAIN, + title="Mock Gateway", + data={ + CONF_NAME: "Mock Gateway", + CONF_DEVICE: "/dev/null", + CONF_ID: MOCK_GATEWAY_ID, + }, + options={}, +) + + +async def test_device_registry_insert(hass): + """Test that the device registry is initialized correctly.""" + MOCK_CONFIG_ENTRY.add_to_hass(hass) + + with patch( + "homeassistant.components.opentherm_gw.OpenThermGatewayDevice.cleanup", + return_value=None, + ), patch("pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS): + await setup.async_setup_component(hass, DOMAIN, {}) + + await hass.async_block_till_done() + + device_registry = await hass.helpers.device_registry.async_get_registry() + + gw_dev = device_registry.async_get_device( + identifiers={(DOMAIN, MOCK_GATEWAY_ID)}, connections=set() + ) + assert gw_dev.sw_version == VERSION_OLD + + +async def test_device_registry_update(hass): + """Test that the device registry is updated correctly.""" + MOCK_CONFIG_ENTRY.add_to_hass(hass) + + dev_reg = mock_device_registry(hass) + dev_reg.async_get_or_create( + config_entry_id=MOCK_CONFIG_ENTRY.entry_id, + identifiers={(DOMAIN, MOCK_GATEWAY_ID)}, + name="Mock Gateway", + manufacturer="Schelte Bron", + model="OpenTherm Gateway", + sw_version=VERSION_OLD, + ) + + with patch( + "homeassistant.components.opentherm_gw.OpenThermGatewayDevice.cleanup", + return_value=None, + ), patch("pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS_UPD): + await setup.async_setup_component(hass, DOMAIN, {}) + + await hass.async_block_till_done() + gw_dev = dev_reg.async_get_device( + identifiers={(DOMAIN, MOCK_GATEWAY_ID)}, connections=set() + ) + assert gw_dev.sw_version == VERSION_NEW From 34161f3ff6109c71abcb40fac6bbd2374f131828 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 5 Jan 2021 20:55:17 +0100 Subject: [PATCH 106/507] Fix Canary doing I/O in event loop (#44854) --- homeassistant/components/canary/camera.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index fd2f08c1488..0493a964cc4 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -127,11 +127,14 @@ class CanaryCamera(CoordinatorEntity, Camera): async def async_camera_image(self): """Return a still image response from the camera.""" await self.hass.async_add_executor_job(self.renew_live_stream_session) + live_stream_url = await self.hass.async_add_executor_job( + getattr, self._live_stream_session, "live_stream_url" + ) ffmpeg = ImageFrame(self._ffmpeg.binary) image = await asyncio.shield( ffmpeg.get_image( - self._live_stream_session.live_stream_url, + live_stream_url, output_format=IMAGE_JPEG, extra_cmd=self._ffmpeg_arguments, ) From 009663602ad93892ce56e93ecc1d0071930fd34c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 5 Jan 2021 21:12:14 +0100 Subject: [PATCH 107/507] Avoid Ps4 doing I/O during tests (#44845) --- tests/components/ps4/conftest.py | 9 ++++++++- tests/components/ps4/test_media_player.py | 8 ++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/tests/components/ps4/conftest.py b/tests/components/ps4/conftest.py index 821c58d596d..155f1c6d5dd 100644 --- a/tests/components/ps4/conftest.py +++ b/tests/components/ps4/conftest.py @@ -18,6 +18,13 @@ def patch_save_json(): yield mock_save +@pytest.fixture +def patch_get_status(): + """Prevent save JSON being used.""" + with patch("pyps4_2ndscreen.ps4.get_status", return_value=None) as mock_get_status: + yield mock_get_status + + @pytest.fixture(autouse=True) -def patch_io(patch_load_json, patch_save_json): +def patch_io(patch_load_json, patch_save_json, patch_get_status): """Prevent PS4 doing I/O.""" diff --git a/tests/components/ps4/test_media_player.py b/tests/components/ps4/test_media_player.py index 48fb27ab6bc..e65813bcbbd 100644 --- a/tests/components/ps4/test_media_player.py +++ b/tests/components/ps4/test_media_player.py @@ -288,11 +288,11 @@ async def test_media_attributes_are_loaded(hass, patch_load_json): assert mock_attrs.get(ATTR_MEDIA_CONTENT_TYPE) == MOCK_TITLE_TYPE -async def test_device_info_is_set_from_status_correctly(hass): +async def test_device_info_is_set_from_status_correctly(hass, patch_get_status): """Test that device info is set correctly from status update.""" mock_d_registry = mock_device_registry(hass) - with patch("pyps4_2ndscreen.ps4.get_status", return_value=MOCK_STATUS_STANDBY): - mock_entity_id = await setup_mock_component(hass) + patch_get_status.return_value = MOCK_STATUS_STANDBY + mock_entity_id = await setup_mock_component(hass) await hass.async_block_till_done() @@ -305,7 +305,7 @@ async def test_device_info_is_set_from_status_correctly(hass): mock_d_entries = mock_d_registry.devices mock_entry = mock_d_registry.async_get_device( - identifiers={(DOMAIN, MOCK_HOST_ID)}, connections={()} + identifiers={(DOMAIN, MOCK_HOST_ID)}, connections=set() ) assert mock_state == STATE_STANDBY From cc57dd953451aefc4f9879d229193b7a25496dc4 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 5 Jan 2021 23:27:35 +0100 Subject: [PATCH 108/507] Update frontend to 20201229.1 (#44861) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index abe98b98c8c..241b07fd591 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==20201229.0"], + "requirements": ["home-assistant-frontend==20201229.1"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 892f5a7552b..3241cfb9391 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 hass-nabucasa==0.39.0 -home-assistant-frontend==20201229.0 +home-assistant-frontend==20201229.1 httpx==0.16.1 jinja2>=2.11.2 netdisco==2.8.2 diff --git a/requirements_all.txt b/requirements_all.txt index ab1209437ef..dcc95d14465 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -762,7 +762,7 @@ hole==0.5.1 holidays==0.10.4 # homeassistant.components.frontend -home-assistant-frontend==20201229.0 +home-assistant-frontend==20201229.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6896c8cf923..27447d735a3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -394,7 +394,7 @@ hole==0.5.1 holidays==0.10.4 # homeassistant.components.frontend -home-assistant-frontend==20201229.0 +home-assistant-frontend==20201229.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From f18880686c6fbcea920b9a180920232a341328c3 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Wed, 6 Jan 2021 07:27:46 +0000 Subject: [PATCH 109/507] Add MQTT Number (#44739) * Initial Commit * initial commit * add discovery and tests * increase coverage * address review * catchup with reality --- homeassistant/components/mqtt/__init__.py | 1 + homeassistant/components/mqtt/discovery.py | 1 + homeassistant/components/mqtt/number.py | 204 ++++++++++++++++ tests/components/mqtt/test_number.py | 263 +++++++++++++++++++++ 4 files changed, 469 insertions(+) create mode 100644 homeassistant/components/mqtt/number.py create mode 100644 tests/components/mqtt/test_number.py diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index eb1e8c01ae0..6982e4d728f 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -150,6 +150,7 @@ PLATFORMS = [ "fan", "light", "lock", + "number", "scene", "sensor", "switch", diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index eec1a63f932..1e47595058d 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -42,6 +42,7 @@ SUPPORTED_COMPONENTS = [ "fan", "light", "lock", + "number", "scene", "sensor", "switch", diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py new file mode 100644 index 00000000000..f469130cb1c --- /dev/null +++ b/homeassistant/components/mqtt/number.py @@ -0,0 +1,204 @@ +"""Configure number in a device through MQTT topic.""" +import logging + +import voluptuous as vol + +from homeassistant.components import number +from homeassistant.components.number import NumberEntity +from homeassistant.const import CONF_DEVICE, CONF_NAME, CONF_UNIQUE_ID +from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.reload import async_setup_reload_service +from homeassistant.helpers.typing import ConfigType, HomeAssistantType + +from . import ( + ATTR_DISCOVERY_HASH, + CONF_QOS, + DOMAIN, + PLATFORMS, + MqttAttributes, + MqttAvailability, + MqttDiscoveryUpdate, + MqttEntityDeviceInfo, + subscription, +) +from .. import mqtt +from .debug_info import log_messages +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash + +_LOGGER = logging.getLogger(__name__) + +CONF_TOPIC = "topic" +DEFAULT_NAME = "MQTT Number" + +PLATFORM_SCHEMA = ( + mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_UNIQUE_ID): cv.string, + } + ) + .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) + .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) +) + + +async def async_setup_platform( + hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None +): + """Set up MQTT number 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 number dynamically through MQTT discovery.""" + + async def async_discover(discovery_payload): + """Discover and add a MQTT number.""" + 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: + discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(hass, discovery_hash) + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) + raise + + async_dispatcher_connect( + hass, MQTT_DISCOVERY_NEW.format(number.DOMAIN, "mqtt"), async_discover + ) + + +async def _async_setup_entity( + config, async_add_entities, config_entry=None, discovery_data=None +): + """Set up the MQTT number.""" + async_add_entities([MqttNumber(config, config_entry, discovery_data)]) + + +class MqttNumber( + MqttAttributes, + MqttAvailability, + MqttDiscoveryUpdate, + MqttEntityDeviceInfo, + NumberEntity, +): + """representation of an MQTT number.""" + + def __init__(self, config, config_entry, discovery_data): + """Initialize the MQTT Number.""" + self._config = config + self._unique_id = config.get(CONF_UNIQUE_ID) + self._sub_state = None + + self._current_number = None + + device_config = config.get(CONF_DEVICE) + + NumberEntity.__init__(self) + MqttAttributes.__init__(self, config) + MqttAvailability.__init__(self, config) + MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) + MqttEntityDeviceInfo.__init__(self, device_config, config_entry) + + async def async_added_to_hass(self): + """Subscribe MQTT events.""" + await super().async_added_to_hass() + await self._subscribe_topics() + + async def discovery_update(self, discovery_payload): + """Handle updated discovery message.""" + config = PLATFORM_SCHEMA(discovery_payload) + self._config = config + await self.attributes_discovery_update(config) + await self.availability_discovery_update(config) + await self.device_info_discovery_update(config) + await self._subscribe_topics() + self.async_write_ha_state() + + async def _subscribe_topics(self): + """(Re)Subscribe to topics.""" + + @callback + @log_messages(self.hass, self.entity_id) + def message_received(msg): + """Handle new MQTT messages.""" + + try: + if msg.payload.decode("utf-8").isnumeric(): + self._current_number = int(msg.payload) + else: + self._current_number = float(msg.payload) + self.async_write_ha_state() + except ValueError: + _LOGGER.warning("We received <%s> which is not a Number", msg.payload) + + self._sub_state = await subscription.async_subscribe_topics( + self.hass, + self._sub_state, + { + "state_topic": { + "topic": self._config[CONF_TOPIC], + "msg_callback": message_received, + "qos": self._config[CONF_QOS], + "encoding": None, + } + }, + ) + + async def async_will_remove_from_hass(self): + """Unsubscribe when removed.""" + self._sub_state = await subscription.async_unsubscribe_topics( + self.hass, self._sub_state + ) + await MqttAttributes.async_will_remove_from_hass(self) + await MqttAvailability.async_will_remove_from_hass(self) + await MqttDiscoveryUpdate.async_will_remove_from_hass(self) + + @property + def value(self): + """Return the current value.""" + return self._current_number + + async def async_set_value(self, value: float) -> None: + """Update the current value.""" + if value.is_integer(): + self._current_number = int(value) + else: + self._current_number = value + + mqtt.async_publish( + self.hass, + self._config[CONF_TOPIC], + self._current_number, + self._config[CONF_QOS], + ) + + self.async_write_ha_state() + + @property + def name(self): + """Return the name of this number.""" + return self._config[CONF_NAME] + + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id + + @property + def should_poll(self): + """Return the polling state.""" + return False diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py new file mode 100644 index 00000000000..ac92b842f25 --- /dev/null +++ b/tests/components/mqtt/test_number.py @@ -0,0 +1,263 @@ +"""The tests for mqtt number component.""" +import json +from unittest.mock import patch + +import pytest + +from homeassistant.components import number +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.const import ATTR_ENTITY_ID +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_attr, + help_test_discovery_update_unchanged, + help_test_entity_debug_info_message, + help_test_entity_device_info_remove, + help_test_entity_device_info_update, + help_test_entity_device_info_with_connection, + help_test_entity_device_info_with_identifier, + help_test_entity_id_update_discovery_update, + help_test_entity_id_update_subscriptions, + help_test_setting_attribute_via_mqtt_json_message, + help_test_setting_attribute_with_template, + help_test_unique_id, + help_test_update_with_json_attrs_bad_JSON, + help_test_update_with_json_attrs_not_dict, +) + +from tests.common import async_fire_mqtt_message + +DEFAULT_CONFIG = { + number.DOMAIN: {"platform": "mqtt", "name": "test", "topic": "test_topic"} +} + + +async def test_run_number_setup(hass, mqtt_mock): + """Test that it fetches the given payload.""" + topic = "test/number" + await async_setup_component( + hass, + "number", + {"number": {"platform": "mqtt", "topic": topic, "name": "Test Number"}}, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, topic, "10") + + await hass.async_block_till_done() + + state = hass.states.get("number.test_number") + assert state.state == "10" + + async_fire_mqtt_message(hass, topic, "20.5") + + await hass.async_block_till_done() + + state = hass.states.get("number.test_number") + assert state.state == "20.5" + + +async def test_run_number_service(hass, mqtt_mock): + """Test that set_value service works.""" + topic = "test/number" + await async_setup_component( + hass, + "number", + {"number": {"platform": "mqtt", "topic": topic, "name": "Test Number"}}, + ) + await hass.async_block_till_done() + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: "number.test_number", ATTR_VALUE: 30}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with(topic, "30", 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("number.test_number") + assert state.state == "30" + + +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, number.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, number.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_default_availability_payload(hass, mqtt_mock): + """Test availability by default payload with defined topic.""" + await help_test_default_availability_payload( + hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_custom_availability_payload(hass, mqtt_mock): + """Test availability by custom payload with defined topic.""" + await help_test_custom_availability_payload( + hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_setting_attribute_with_template(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_attribute_with_template( + hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + await help_test_update_with_json_attrs_not_dict( + hass, mqtt_mock, caplog, number.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + await help_test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock, caplog, number.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_discovery_update_attr(hass, mqtt_mock, caplog): + """Test update of discovered MQTTAttributes.""" + await help_test_discovery_update_attr( + hass, mqtt_mock, caplog, number.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_unique_id(hass, mqtt_mock): + """Test unique id option only creates one number per unique_id.""" + config = { + number.DOMAIN: [ + { + "platform": "mqtt", + "name": "Test 1", + "topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "platform": "mqtt", + "name": "Test 2", + "topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } + await help_test_unique_id(hass, mqtt_mock, number.DOMAIN, config) + + +async def test_discovery_removal_number(hass, mqtt_mock, caplog): + """Test removal of discovered number.""" + data = json.dumps(DEFAULT_CONFIG[number.DOMAIN]) + await help_test_discovery_removal(hass, mqtt_mock, caplog, number.DOMAIN, data) + + +async def test_discovery_update_number(hass, mqtt_mock, caplog): + """Test update of discovered number.""" + data1 = '{ "name": "Beer", "topic": "test_topic"}' + data2 = '{ "name": "Milk", "topic": "test_topic"}' + + await help_test_discovery_update( + hass, mqtt_mock, caplog, number.DOMAIN, data1, data2 + ) + + +async def test_discovery_update_unchanged_number(hass, mqtt_mock, caplog): + """Test update of discovered number.""" + data1 = '{ "name": "Beer", "topic": "test_topic"}' + with patch( + "homeassistant.components.mqtt.number.MqttNumber.discovery_update" + ) as discovery_update: + await help_test_discovery_update_unchanged( + hass, mqtt_mock, caplog, number.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", "topic": "test_topic"}' + + await help_test_discovery_broken( + hass, mqtt_mock, caplog, number.DOMAIN, data1, data2 + ) + + +async def test_entity_device_info_with_connection(hass, mqtt_mock): + """Test MQTT number device registry integration.""" + await help_test_entity_device_info_with_connection( + hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_with_identifier(hass, mqtt_mock): + """Test MQTT number device registry integration.""" + await help_test_entity_device_info_with_identifier( + hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_update(hass, mqtt_mock): + """Test device registry update.""" + await help_test_entity_device_info_update( + hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_remove(hass, mqtt_mock): + """Test device registry remove.""" + await help_test_entity_device_info_remove( + hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_id_update_subscriptions(hass, mqtt_mock): + """Test MQTT subscriptions are managed when entity_id is updated.""" + await help_test_entity_id_update_subscriptions( + hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG, ["test_topic"] + ) + + +async def test_entity_id_update_discovery_update(hass, mqtt_mock): + """Test MQTT discovery update when entity_id is updated.""" + await help_test_entity_id_update_discovery_update( + hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_debug_info_message(hass, mqtt_mock): + """Test MQTT debug info.""" + await help_test_entity_debug_info_message( + hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG, "test_topic", b"ON" + ) From 02bfc688428d094b6b3104434ac987251491b4d8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 6 Jan 2021 09:23:18 +0100 Subject: [PATCH 110/507] Support dynamic Google Cast groups (#44484) * Re-add support for dynamic groups * Add tests * Add support for manufacturer * Refactor support for dynamic groups * Bump pychromecast to 7.7.0 * Bump pychromecast to 7.7.1 * Tweak tests * Apply review suggestion --- homeassistant/components/cast/discovery.py | 1 + homeassistant/components/cast/helpers.py | 89 +++++- homeassistant/components/cast/manifest.json | 2 +- homeassistant/components/cast/media_player.py | 161 +++++++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/cast/test_media_player.py | 291 ++++++++++++++++-- 7 files changed, 489 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/cast/discovery.py b/homeassistant/components/cast/discovery.py index 341ba0c4c5e..4858d37f732 100644 --- a/homeassistant/components/cast/discovery.py +++ b/homeassistant/components/cast/discovery.py @@ -25,6 +25,7 @@ def discover_chromecast(hass: HomeAssistant, info: ChromecastInfo): _LOGGER.error("Discovered chromecast without uuid %s", info) return + info = info.fill_out_missing_chromecast_info() if info.uuid in hass.data[KNOWN_CHROMECAST_INFO_KEY]: _LOGGER.debug("Discovered update for known chromecast %s", info) else: diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py index 0ad13d137d1..e7db380406b 100644 --- a/homeassistant/components/cast/helpers.py +++ b/homeassistant/components/cast/helpers.py @@ -1,7 +1,8 @@ """Helpers to deal with Cast devices.""" -from typing import Optional, Tuple +from typing import Optional import attr +from pychromecast import dial from pychromecast.const import CAST_MANUFACTURERS from .const import DEFAULT_PORT @@ -20,8 +21,10 @@ class ChromecastInfo: uuid: Optional[str] = attr.ib( converter=attr.converters.optional(str), default=None ) # always convert UUID to string if not None + _manufacturer = attr.ib(type=Optional[str], default=None) model_name: str = attr.ib(default="") friendly_name: Optional[str] = attr.ib(default=None) + is_dynamic_group = attr.ib(type=Optional[bool], default=None) @property def is_audio_group(self) -> bool: @@ -29,17 +32,84 @@ class ChromecastInfo: return self.port != DEFAULT_PORT @property - def host_port(self) -> Tuple[str, int]: - """Return the host+port tuple.""" - return self.host, self.port + def is_information_complete(self) -> bool: + """Return if all information is filled out.""" + want_dynamic_group = self.is_audio_group + have_dynamic_group = self.is_dynamic_group is not None + have_all_except_dynamic_group = all( + attr.astuple( + self, + filter=attr.filters.exclude( + attr.fields(ChromecastInfo).is_dynamic_group + ), + ) + ) + return have_all_except_dynamic_group and ( + not want_dynamic_group or have_dynamic_group + ) @property def manufacturer(self) -> str: """Return the manufacturer.""" + if self._manufacturer: + return self._manufacturer if not self.model_name: return None return CAST_MANUFACTURERS.get(self.model_name.lower(), "Google Inc.") + def fill_out_missing_chromecast_info(self) -> "ChromecastInfo": + """Return a new ChromecastInfo object with missing attributes filled in. + + Uses blocking HTTP / HTTPS. + """ + if self.is_information_complete: + # We have all information, no need to check HTTP API. + return self + + # Fill out missing group information via HTTP API. + if self.is_audio_group: + is_dynamic_group = False + http_group_status = None + if self.uuid: + http_group_status = dial.get_multizone_status( + self.host, + services=self.services, + zconf=ChromeCastZeroconf.get_zeroconf(), + ) + if http_group_status is not None: + is_dynamic_group = any( + str(g.uuid) == self.uuid + for g in http_group_status.dynamic_groups + ) + + return ChromecastInfo( + services=self.services, + host=self.host, + port=self.port, + uuid=self.uuid, + friendly_name=self.friendly_name, + model_name=self.model_name, + is_dynamic_group=is_dynamic_group, + ) + + # Fill out some missing information (friendly_name, uuid) via HTTP dial. + http_device_status = dial.get_device_status( + self.host, services=self.services, zconf=ChromeCastZeroconf.get_zeroconf() + ) + if http_device_status is None: + # HTTP dial didn't give us any new information. + return self + + return ChromecastInfo( + services=self.services, + host=self.host, + port=self.port, + uuid=(self.uuid or http_device_status.uuid), + friendly_name=(self.friendly_name or http_device_status.friendly_name), + manufacturer=(self.manufacturer or http_device_status.manufacturer), + model_name=(self.model_name or http_device_status.model_name), + ) + class ChromeCastZeroconf: """Class to hold a zeroconf instance.""" @@ -65,19 +135,22 @@ class CastStatusListener: potentially arrive. This class allows invalidating past chromecast objects. """ - def __init__(self, cast_device, chromecast, mz_mgr): + def __init__(self, cast_device, chromecast, mz_mgr, mz_only=False): """Initialize the status listener.""" self._cast_device = cast_device self._uuid = chromecast.uuid self._valid = True self._mz_mgr = mz_mgr + if cast_device._cast_info.is_audio_group: + self._mz_mgr.add_multizone(chromecast) + if mz_only: + return + chromecast.register_status_listener(self) chromecast.socket_client.media_controller.register_status_listener(self) chromecast.register_connection_listener(self) - if cast_device._cast_info.is_audio_group: - self._mz_mgr.add_multizone(chromecast) - else: + if not cast_device._cast_info.is_audio_group: self._mz_mgr.register_listener(chromecast.uuid, self) def new_cast_status(self, cast_status): diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 8072e06c2e5..c1c24a4cda8 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==7.6.0"], + "requirements": ["pychromecast==7.7.1"], "after_dependencies": ["cloud", "http", "media_source", "plex", "tts", "zeroconf"], "zeroconf": ["_googlecast._tcp.local."], "codeowners": ["@emontnemery"] diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index e68800efb44..6bedae1cac5 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -63,6 +63,7 @@ from .const import ( DOMAIN as CAST_DOMAIN, KNOWN_CHROMECAST_INFO_KEY, SIGNAL_CAST_DISCOVERED, + SIGNAL_CAST_REMOVED, SIGNAL_HASS_CAST_SHOW_VIEW, ) from .discovery import setup_internal_discovery @@ -115,6 +116,13 @@ def _async_create_cast_device(hass: HomeAssistantType, info: ChromecastInfo): return None # -> New cast device added_casts.add(info.uuid) + + if info.is_dynamic_group: + # This is a dynamic group, do not add it but connect to the service. + group = DynamicCastGroup(hass, info) + group.async_setup() + return None + return CastDevice(info) @@ -206,8 +214,9 @@ class CastDevice(MediaPlayerEntity): self.hass, SIGNAL_CAST_DISCOVERED, self._async_cast_discovered ) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_stop) + self.async_set_cast_info(self._cast_info) self.hass.async_create_task( - async_create_catching_coro(self.async_set_cast_info(self._cast_info)) + async_create_catching_coro(self.async_connect_to_chromecast()) ) self._cast_view_remove_handler = async_dispatcher_connect( @@ -228,15 +237,13 @@ class CastDevice(MediaPlayerEntity): self._cast_view_remove_handler() self._cast_view_remove_handler = None - async def async_set_cast_info(self, cast_info): - """Set the cast information and set up the chromecast object.""" + def async_set_cast_info(self, cast_info): + """Set the cast information.""" self._cast_info = cast_info - if self._chromecast is not None: - # Only setup the chromecast once, added elements to services - # will automatically be picked up. - return + async def async_connect_to_chromecast(self): + """Set up the chromecast object.""" _LOGGER.debug( "[%s %s] Connecting to cast device by service %s", @@ -248,9 +255,9 @@ class CastDevice(MediaPlayerEntity): pychromecast.get_chromecast_from_service, ( self.services, - cast_info.uuid, - cast_info.model_name, - cast_info.friendly_name, + self._cast_info.uuid, + self._cast_info.model_name, + self._cast_info.friendly_name, None, None, ), @@ -777,16 +784,12 @@ class CastDevice(MediaPlayerEntity): async def _async_cast_discovered(self, discover: ChromecastInfo): """Handle discovery of new Chromecast.""" - if self._cast_info.uuid is None: - # We can't handle empty UUIDs - return - if self._cast_info.uuid != discover.uuid: # Discovered is not our device. return _LOGGER.debug("Discovered chromecast with same UUID: %s", discover) - await self.async_set_cast_info(discover) + self.async_set_cast_info(discover) async def _async_stop(self, event): """Disconnect socket on Home Assistant stop.""" @@ -808,3 +811,131 @@ class CastDevice(MediaPlayerEntity): self._chromecast.register_handler(controller) self._hass_cast_controller.show_lovelace_view(view_path, url_path) + + +class DynamicCastGroup: + """Representation of a Cast device on the network - for dynamic cast groups.""" + + def __init__(self, hass, cast_info: ChromecastInfo): + """Initialize the cast device.""" + + self.hass = hass + self._cast_info = cast_info + self.services = cast_info.services + self._chromecast: Optional[pychromecast.Chromecast] = None + self.mz_mgr = None + self._status_listener: Optional[CastStatusListener] = None + + self._add_remove_handler = None + self._del_remove_handler = None + + def async_setup(self): + """Create chromecast object.""" + self._add_remove_handler = async_dispatcher_connect( + self.hass, SIGNAL_CAST_DISCOVERED, self._async_cast_discovered + ) + self._del_remove_handler = async_dispatcher_connect( + self.hass, SIGNAL_CAST_REMOVED, self._async_cast_removed + ) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_stop) + self.async_set_cast_info(self._cast_info) + self.hass.async_create_task( + async_create_catching_coro(self.async_connect_to_chromecast()) + ) + + async def async_tear_down(self) -> None: + """Disconnect Chromecast object.""" + await self._async_disconnect() + if self._cast_info.uuid is not None: + # Remove the entity from the added casts so that it can dynamically + # be re-added again. + self.hass.data[ADDED_CAST_DEVICES_KEY].remove(self._cast_info.uuid) + if self._add_remove_handler: + self._add_remove_handler() + self._add_remove_handler = None + if self._del_remove_handler: + self._del_remove_handler() + self._del_remove_handler = None + + def async_set_cast_info(self, cast_info): + """Set the cast information and set up the chromecast object.""" + + self._cast_info = cast_info + + async def async_connect_to_chromecast(self): + """Set the cast information and set up the chromecast object.""" + + _LOGGER.debug( + "[%s %s] Connecting to cast device by service %s", + "Dynamic group", + self._cast_info.friendly_name, + self.services, + ) + chromecast = await self.hass.async_add_executor_job( + pychromecast.get_chromecast_from_service, + ( + self.services, + self._cast_info.uuid, + self._cast_info.model_name, + self._cast_info.friendly_name, + None, + None, + ), + ChromeCastZeroconf.get_zeroconf(), + ) + self._chromecast = chromecast + + if CAST_MULTIZONE_MANAGER_KEY not in self.hass.data: + self.hass.data[CAST_MULTIZONE_MANAGER_KEY] = MultizoneManager() + + self.mz_mgr = self.hass.data[CAST_MULTIZONE_MANAGER_KEY] + + self._status_listener = CastStatusListener(self, chromecast, self.mz_mgr, True) + self._chromecast.start() + + async def _async_disconnect(self): + """Disconnect Chromecast object if it is set.""" + if self._chromecast is None: + # Can't disconnect if not connected. + return + _LOGGER.debug( + "[%s %s] Disconnecting from chromecast socket", + "Dynamic group", + self._cast_info.friendly_name, + ) + + await self.hass.async_add_executor_job(self._chromecast.disconnect) + + self._invalidate() + + def _invalidate(self): + """Invalidate some attributes.""" + self._chromecast = None + self.mz_mgr = None + if self._status_listener is not None: + self._status_listener.invalidate() + self._status_listener = None + + async def _async_cast_discovered(self, discover: ChromecastInfo): + """Handle discovery of new Chromecast.""" + if self._cast_info.uuid != discover.uuid: + # Discovered is not our device. + return + + _LOGGER.debug("Discovered dynamic group with same UUID: %s", discover) + self.async_set_cast_info(discover) + + async def _async_cast_removed(self, discover: ChromecastInfo): + """Handle removal of Chromecast.""" + if self._cast_info.uuid != discover.uuid: + # Removed is not our device. + return + + if not discover.services: + # Clean up the dynamic group + _LOGGER.debug("Clean up dynamic group: %s", discover) + await self.async_tear_down() + + async def _async_stop(self, event): + """Disconnect socket on Home Assistant stop.""" + await self._async_disconnect() diff --git a/requirements_all.txt b/requirements_all.txt index dcc95d14465..3ab3b02d995 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1304,7 +1304,7 @@ pycfdns==1.2.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==7.6.0 +pychromecast==7.7.1 # homeassistant.components.pocketcasts pycketcasts==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 27447d735a3..97143c68fa4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -664,7 +664,7 @@ pybotvac==0.0.19 pycfdns==1.2.1 # homeassistant.components.cast -pychromecast==7.6.0 +pychromecast==7.7.1 # homeassistant.components.coolmaster pycoolmasternet-async==0.1.2 diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index c04dc87ad11..050d6a6932d 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -14,6 +14,7 @@ from homeassistant.components.cast.media_player import ChromecastInfo from homeassistant.config import async_process_ha_core_config from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component @@ -21,12 +22,32 @@ from tests.common import MockConfigEntry, assert_setup_component from tests.components.media_player import common +@pytest.fixture() +def dial_mock(): + """Mock pychromecast dial.""" + dial_mock = MagicMock() + dial_mock.get_device_status.return_value.uuid = "fake_uuid" + dial_mock.get_device_status.return_value.manufacturer = "fake_manufacturer" + dial_mock.get_device_status.return_value.model_name = "fake_model_name" + dial_mock.get_device_status.return_value.friendly_name = "fake_friendly_name" + dial_mock.get_multizone_status.return_value.dynamic_groups = [] + return dial_mock + + @pytest.fixture() def mz_mock(): """Mock pychromecast MultizoneManager.""" return MagicMock() +@pytest.fixture() +def pycast_mock(): + """Mock pychromecast.""" + pycast_mock = MagicMock() + pycast_mock.start_discovery.return_value = (None, Mock()) + return pycast_mock + + @pytest.fixture() def quick_play_mock(): """Mock pychromecast quick_play.""" @@ -34,20 +55,14 @@ def quick_play_mock(): @pytest.fixture(autouse=True) -def cast_mock(mz_mock, quick_play_mock): +def cast_mock(dial_mock, mz_mock, pycast_mock, quick_play_mock): """Mock pychromecast.""" - pycast_mock = MagicMock() - pycast_mock.start_discovery.return_value = (None, Mock()) - dial_mock = MagicMock(name="XXX") - dial_mock.get_device_status.return_value.uuid = "fake_uuid" - dial_mock.get_device_status.return_value.manufacturer = "fake_manufacturer" - dial_mock.get_device_status.return_value.model_name = "fake_model_name" - dial_mock.get_device_status.return_value.friendly_name = "fake_friendly_name" - with patch( "homeassistant.components.cast.media_player.pychromecast", pycast_mock ), patch( "homeassistant.components.cast.discovery.pychromecast", pycast_mock + ), patch( + "homeassistant.components.cast.helpers.dial", dial_mock ), patch( "homeassistant.components.cast.media_player.MultizoneManager", return_value=mz_mock, @@ -130,6 +145,7 @@ async def async_setup_cast_internal_discovery(hass, config=None): assert start_discovery.call_count == 1 discovery_callback = cast_listener.call_args[0][0] + remove_callback = cast_listener.call_args[0][1] def discover_chromecast(service_name: str, info: ChromecastInfo) -> None: """Discover a chromecast device.""" @@ -141,7 +157,15 @@ async def async_setup_cast_internal_discovery(hass, config=None): ) discovery_callback(info.uuid, service_name) - return discover_chromecast, add_entities + def remove_chromecast(service_name: str, info: ChromecastInfo) -> None: + """Remove a chromecast device.""" + remove_callback( + info.uuid, + service_name, + (set(), info.uuid, info.model_name, info.friendly_name), + ) + + return discover_chromecast, remove_chromecast, add_entities async def async_setup_media_player_cast(hass: HomeAssistantType, info: ChromecastInfo): @@ -183,7 +207,18 @@ async def async_setup_media_player_cast(hass: HomeAssistantType, info: Chromecas await hass.async_block_till_done() await hass.async_block_till_done() assert get_chromecast.call_count == 1 - return chromecast + + def discover_chromecast(service_name: str, info: ChromecastInfo) -> None: + """Discover a chromecast device.""" + listener.services[info.uuid] = ( + {service_name}, + info.uuid, + info.model_name, + info.friendly_name, + ) + discovery_callback(info.uuid, service_name) + + return chromecast, discover_chromecast def get_status_callbacks(chromecast_mock, mz_mock=None): @@ -219,6 +254,123 @@ async def test_start_discovery_called_once(hass): assert start_discovery.call_count == 1 +async def test_internal_discovery_callback_fill_out(hass): + """Test internal discovery automatically filling out information.""" + discover_cast, _, _ = await async_setup_cast_internal_discovery(hass) + info = get_fake_chromecast_info(host="host1") + zconf = get_fake_zconf(host="host1", port=8009) + full_info = attr.evolve( + info, + model_name="google home", + friendly_name="Speaker", + uuid=FakeUUID, + manufacturer="Nabu Casa", + ) + + with patch( + "homeassistant.components.cast.helpers.dial.get_device_status", + return_value=full_info, + ), patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf, + ): + signal = MagicMock() + + async_dispatcher_connect(hass, "cast_discovered", signal) + discover_cast("the-service", info) + await hass.async_block_till_done() + + # when called with incomplete info, it should use HTTP to get missing + discover = signal.mock_calls[0][1][0] + assert discover == full_info + + +async def test_internal_discovery_callback_fill_out_default_manufacturer(hass): + """Test internal discovery automatically filling out information.""" + discover_cast, _, _ = await async_setup_cast_internal_discovery(hass) + info = get_fake_chromecast_info(host="host1") + zconf = get_fake_zconf(host="host1", port=8009) + full_info = attr.evolve( + info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID + ) + + with patch( + "homeassistant.components.cast.helpers.dial.get_device_status", + return_value=full_info, + ), patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf, + ): + signal = MagicMock() + + async_dispatcher_connect(hass, "cast_discovered", signal) + discover_cast("the-service", info) + await hass.async_block_till_done() + + # when called with incomplete info, it should use HTTP to get missing + discover = signal.mock_calls[0][1][0] + assert discover == attr.evolve(full_info, manufacturer="Google Inc.") + + +async def test_internal_discovery_callback_fill_out_fail(hass): + """Test internal discovery automatically filling out information.""" + discover_cast, _, _ = await async_setup_cast_internal_discovery(hass) + info = get_fake_chromecast_info(host="host1") + zconf = get_fake_zconf(host="host1", port=8009) + full_info = ( + info # attr.evolve(info, model_name="", friendly_name="Speaker", uuid=FakeUUID) + ) + + with patch( + "homeassistant.components.cast.helpers.dial.get_device_status", + return_value=None, + ), patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf, + ): + signal = MagicMock() + + async_dispatcher_connect(hass, "cast_discovered", signal) + discover_cast("the-service", info) + await hass.async_block_till_done() + + # when called with incomplete info, it should use HTTP to get missing + discover = signal.mock_calls[0][1][0] + assert discover == full_info + # assert 1 == 2 + + +async def test_internal_discovery_callback_fill_out_group(hass): + """Test internal discovery automatically filling out information.""" + discover_cast, _, _ = await async_setup_cast_internal_discovery(hass) + info = get_fake_chromecast_info(host="host1", port=12345) + zconf = get_fake_zconf(host="host1", port=12345) + full_info = attr.evolve( + info, + model_name="", + friendly_name="Speaker", + uuid=FakeUUID, + is_dynamic_group=False, + ) + + with patch( + "homeassistant.components.cast.helpers.dial.get_device_status", + return_value=full_info, + ), patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf, + ): + signal = MagicMock() + + async_dispatcher_connect(hass, "cast_discovered", signal) + discover_cast("the-service", info) + await hass.async_block_till_done() + + # when called with incomplete info, it should use HTTP to get missing + discover = signal.mock_calls[0][1][0] + assert discover == full_info + + async def test_stop_discovery_called_on_stop(hass): """Test pychromecast.stop_discovery called on shutdown.""" browser = MagicMock(zc={}) @@ -272,7 +424,7 @@ async def test_replay_past_chromecasts(hass): zconf_1 = get_fake_zconf(host="host1", port=8009) zconf_2 = get_fake_zconf(host="host2", port=8009) - discover_cast, add_dev1 = await async_setup_cast_internal_discovery( + discover_cast, _, add_dev1 = await async_setup_cast_internal_discovery( hass, config={"uuid": FakeUUID} ) @@ -308,7 +460,7 @@ async def test_manual_cast_chromecasts_uuid(hass): zconf_2 = get_fake_zconf(host="host_2") # Manual configuration of media player with host "configured_host" - discover_cast, add_dev1 = await async_setup_cast_internal_discovery( + discover_cast, _, add_dev1 = await async_setup_cast_internal_discovery( hass, config={"uuid": FakeUUID} ) with patch( @@ -338,7 +490,7 @@ async def test_auto_cast_chromecasts(hass): zconf_2 = get_fake_zconf(host="other_host") # Manual configuration of media player with host "configured_host" - discover_cast, add_dev1 = await async_setup_cast_internal_discovery(hass) + discover_cast, _, add_dev1 = await async_setup_cast_internal_discovery(hass) with patch( "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", return_value=zconf_1, @@ -358,6 +510,79 @@ async def test_auto_cast_chromecasts(hass): assert add_dev1.call_count == 2 +async def test_discover_dynamic_group(hass, dial_mock, pycast_mock, caplog): + """Test dynamic group does not create device or entity.""" + cast_1 = get_fake_chromecast_info(host="host_1", port=23456, uuid=FakeUUID) + cast_2 = get_fake_chromecast_info(host="host_2", port=34567, uuid=FakeUUID2) + zconf_1 = get_fake_zconf(host="host_1", port=23456) + zconf_2 = get_fake_zconf(host="host_2", port=34567) + + reg = await hass.helpers.entity_registry.async_get_registry() + + # Fake dynamic group info + tmp1 = MagicMock() + tmp1.uuid = FakeUUID + tmp2 = MagicMock() + tmp2.uuid = FakeUUID2 + dial_mock.get_multizone_status.return_value.dynamic_groups = [tmp1, tmp2] + + pycast_mock.get_chromecast_from_service.assert_not_called() + discover_cast, remove_cast, add_dev1 = await async_setup_cast_internal_discovery( + hass + ) + + # Discover cast service + with patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf_1, + ): + discover_cast("service", cast_1) + await hass.async_block_till_done() + await hass.async_block_till_done() # having tasks that add jobs + pycast_mock.get_chromecast_from_service.assert_called() + pycast_mock.get_chromecast_from_service.reset_mock() + assert add_dev1.call_count == 0 + assert reg.async_get_entity_id("media_player", "cast", cast_1.uuid) is None + + # Discover other dynamic group cast service + with patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf_2, + ): + discover_cast("service", cast_2) + await hass.async_block_till_done() + await hass.async_block_till_done() # having tasks that add jobs + pycast_mock.get_chromecast_from_service.assert_called() + pycast_mock.get_chromecast_from_service.reset_mock() + assert add_dev1.call_count == 0 + assert reg.async_get_entity_id("media_player", "cast", cast_1.uuid) is None + + # Get update for cast service + with patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf_1, + ): + discover_cast("service", cast_1) + await hass.async_block_till_done() + await hass.async_block_till_done() # having tasks that add jobs + pycast_mock.get_chromecast_from_service.assert_not_called() + assert add_dev1.call_count == 0 + assert reg.async_get_entity_id("media_player", "cast", cast_1.uuid) is None + + # Remove cast service + assert "Disconnecting from chromecast" not in caplog.text + + with patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf_1, + ): + remove_cast("service", cast_1) + await hass.async_block_till_done() + await hass.async_block_till_done() # having tasks that add jobs + + assert "Disconnecting from chromecast" in caplog.text + + async def test_update_cast_chromecasts(hass): """Test discovery of same UUID twice only adds one cast.""" cast_1 = get_fake_chromecast_info(host="old_host") @@ -366,7 +591,7 @@ async def test_update_cast_chromecasts(hass): zconf_2 = get_fake_zconf(host="new_host") # Manual configuration of media player with host "configured_host" - discover_cast, add_dev1 = await async_setup_cast_internal_discovery(hass) + discover_cast, _, add_dev1 = await async_setup_cast_internal_discovery(hass) with patch( "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", @@ -392,7 +617,7 @@ async def test_entity_availability(hass: HomeAssistantType): entity_id = "media_player.speaker" info = get_fake_chromecast_info() - chromecast = await async_setup_media_player_cast(hass, info) + chromecast, _ = await async_setup_media_player_cast(hass, info) _, conn_status_cb, _ = get_status_callbacks(chromecast) state = hass.states.get(entity_id) @@ -423,7 +648,7 @@ async def test_entity_cast_status(hass: HomeAssistantType): info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID ) - chromecast = await async_setup_media_player_cast(hass, info) + chromecast, _ = await async_setup_media_player_cast(hass, info) cast_status_cb, conn_status_cb, _ = get_status_callbacks(chromecast) connection_status = MagicMock() @@ -466,7 +691,7 @@ async def test_entity_play_media(hass: HomeAssistantType): info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID ) - chromecast = await async_setup_media_player_cast(hass, info) + chromecast, _ = await async_setup_media_player_cast(hass, info) _, conn_status_cb, _ = get_status_callbacks(chromecast) connection_status = MagicMock() @@ -495,7 +720,7 @@ async def test_entity_play_media_cast(hass: HomeAssistantType, quick_play_mock): info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID ) - chromecast = await async_setup_media_player_cast(hass, info) + chromecast, _ = await async_setup_media_player_cast(hass, info) _, conn_status_cb, _ = get_status_callbacks(chromecast) connection_status = MagicMock() @@ -528,7 +753,7 @@ async def test_entity_play_media_cast_invalid(hass, caplog, quick_play_mock): info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID ) - chromecast = await async_setup_media_player_cast(hass, info) + chromecast, _ = await async_setup_media_player_cast(hass, info) _, conn_status_cb, _ = get_status_callbacks(chromecast) connection_status = MagicMock() @@ -575,7 +800,7 @@ async def test_entity_play_media_sign_URL(hass: HomeAssistantType): info = get_fake_chromecast_info() - chromecast = await async_setup_media_player_cast(hass, info) + chromecast, _ = await async_setup_media_player_cast(hass, info) _, conn_status_cb, _ = get_status_callbacks(chromecast) connection_status = MagicMock() @@ -601,7 +826,7 @@ async def test_entity_media_content_type(hass: HomeAssistantType): info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID ) - chromecast = await async_setup_media_player_cast(hass, info) + chromecast, _ = await async_setup_media_player_cast(hass, info) _, conn_status_cb, media_status_cb = get_status_callbacks(chromecast) connection_status = MagicMock() @@ -655,7 +880,7 @@ async def test_entity_control(hass: HomeAssistantType): info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID ) - chromecast = await async_setup_media_player_cast(hass, info) + chromecast, _ = await async_setup_media_player_cast(hass, info) _, conn_status_cb, media_status_cb = get_status_callbacks(chromecast) connection_status = MagicMock() @@ -738,7 +963,7 @@ async def test_entity_media_states(hass: HomeAssistantType): info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID ) - chromecast = await async_setup_media_player_cast(hass, info) + chromecast, _ = await async_setup_media_player_cast(hass, info) _, conn_status_cb, media_status_cb = get_status_callbacks(chromecast) connection_status = MagicMock() @@ -797,7 +1022,7 @@ async def test_group_media_states(hass, mz_mock): info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID ) - chromecast = await async_setup_media_player_cast(hass, info) + chromecast, _ = await async_setup_media_player_cast(hass, info) _, conn_status_cb, media_status_cb, group_media_status_cb = get_status_callbacks( chromecast, mz_mock ) @@ -841,7 +1066,7 @@ async def test_group_media_states(hass, mz_mock): async def test_group_media_control(hass, mz_mock): - """Test media states are read from group if entity has no state.""" + """Test media controls are handled by group if entity has no state.""" entity_id = "media_player.speaker" reg = await hass.helpers.entity_registry.async_get_registry() @@ -850,7 +1075,7 @@ async def test_group_media_control(hass, mz_mock): info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID ) - chromecast = await async_setup_media_player_cast(hass, info) + chromecast, _ = await async_setup_media_player_cast(hass, info) _, conn_status_cb, media_status_cb, group_media_status_cb = get_status_callbacks( chromecast, mz_mock @@ -904,7 +1129,7 @@ async def test_group_media_control(hass, mz_mock): async def test_failed_cast_on_idle(hass, caplog): """Test no warning when unless player went idle with reason "ERROR".""" info = get_fake_chromecast_info() - chromecast = await async_setup_media_player_cast(hass, info) + chromecast, _ = await async_setup_media_player_cast(hass, info) _, _, media_status_cb = get_status_callbacks(chromecast) media_status = MagicMock(images=None) @@ -939,7 +1164,7 @@ async def test_failed_cast_other_url(hass, caplog): ) info = get_fake_chromecast_info() - chromecast = await async_setup_media_player_cast(hass, info) + chromecast, _ = await async_setup_media_player_cast(hass, info) _, _, media_status_cb = get_status_callbacks(chromecast) media_status = MagicMock(images=None) @@ -962,7 +1187,7 @@ async def test_failed_cast_internal_url(hass, caplog): ) info = get_fake_chromecast_info() - chromecast = await async_setup_media_player_cast(hass, info) + chromecast, _ = await async_setup_media_player_cast(hass, info) _, _, media_status_cb = get_status_callbacks(chromecast) media_status = MagicMock(images=None) @@ -990,7 +1215,7 @@ async def test_failed_cast_external_url(hass, caplog): ) info = get_fake_chromecast_info() - chromecast = await async_setup_media_player_cast(hass, info) + chromecast, _ = await async_setup_media_player_cast(hass, info) _, _, media_status_cb = get_status_callbacks(chromecast) media_status = MagicMock(images=None) @@ -1014,7 +1239,7 @@ async def test_failed_cast_tts_base_url(hass, caplog): ) info = get_fake_chromecast_info() - chromecast = await async_setup_media_player_cast(hass, info) + chromecast, _ = await async_setup_media_player_cast(hass, info) _, _, media_status_cb = get_status_callbacks(chromecast) media_status = MagicMock(images=None) @@ -1032,7 +1257,7 @@ async def test_disconnect_on_stop(hass: HomeAssistantType): """Test cast device disconnects socket on stop.""" info = get_fake_chromecast_info() - chromecast = await async_setup_media_player_cast(hass, info) + chromecast, _ = await async_setup_media_player_cast(hass, info) hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() From 1a65ab0b808c8707232fda38bd8b1830becc3348 Mon Sep 17 00:00:00 2001 From: JeromeHXP Date: Wed, 6 Jan 2021 12:36:39 +0100 Subject: [PATCH 111/507] Address late review of ondilo_ico (#44837) * Updates following comments in PR 44728 * Make all api calls in same thread context * Set API as parameter to get_all_pools_data * extract pools data retrieval function to api class --- homeassistant/components/ondilo_ico/api.py | 17 +++++++++++ .../components/ondilo_ico/manifest.json | 3 -- homeassistant/components/ondilo_ico/sensor.py | 29 +++---------------- .../components/ondilo_ico/test_config_flow.py | 4 --- 4 files changed, 21 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/ondilo_ico/api.py b/homeassistant/components/ondilo_ico/api.py index 3de10403211..e753f8d6dcb 100644 --- a/homeassistant/components/ondilo_ico/api.py +++ b/homeassistant/components/ondilo_ico/api.py @@ -1,11 +1,14 @@ """API for Ondilo ICO bound to Home Assistant OAuth.""" from asyncio import run_coroutine_threadsafe +import logging from ondilo import Ondilo from homeassistant import config_entries, core from homeassistant.helpers import config_entry_oauth2_flow +_LOGGER = logging.getLogger(__name__) + class OndiloClient(Ondilo): """Provide Ondilo ICO authentication tied to an OAuth2 based config entry.""" @@ -31,3 +34,17 @@ class OndiloClient(Ondilo): ).result() return self.session.token + + def get_all_pools_data(self) -> dict: + """Fetch pools and add pool details and last measures to pool data.""" + + pools = self.get_pools() + for pool in pools: + _LOGGER.debug( + "Retrieving data for pool/spa: %s, id: %d", pool["name"], pool["id"] + ) + pool["ICO"] = self.get_ICO_details(pool["id"]) + pool["sensors"] = self.get_last_pool_measures(pool["id"]) + _LOGGER.debug("Retrieved the following sensors data: %s", pool["sensors"]) + + return pools diff --git a/homeassistant/components/ondilo_ico/manifest.json b/homeassistant/components/ondilo_ico/manifest.json index 55585b2c766..ee1afd315d6 100644 --- a/homeassistant/components/ondilo_ico/manifest.json +++ b/homeassistant/components/ondilo_ico/manifest.json @@ -6,9 +6,6 @@ "requirements": [ "ondilo==0.2.0" ], - "ssdp": [], - "zeroconf": [], - "homekit": {}, "dependencies": [ "http" ], diff --git a/homeassistant/components/ondilo_ico/sensor.py b/homeassistant/components/ondilo_ico/sensor.py index 4ed8656c456..b34ee4eae35 100644 --- a/homeassistant/components/ondilo_ico/sensor.py +++ b/homeassistant/components/ondilo_ico/sensor.py @@ -1,5 +1,4 @@ """Platform for sensor integration.""" -import asyncio from datetime import timedelta import logging @@ -25,24 +24,23 @@ SENSOR_TYPES = { "temperature": [ "Temperature", TEMP_CELSIUS, - "mdi:thermometer", + None, DEVICE_CLASS_TEMPERATURE, ], "orp": ["Oxydo Reduction Potential", "mV", "mdi:pool", None], "ph": ["pH", "", "mdi:pool", None], "tds": ["TDS", CONCENTRATION_PARTS_PER_MILLION, "mdi:pool", None], - "battery": ["Battery", PERCENTAGE, "mdi:battery", DEVICE_CLASS_BATTERY], + "battery": ["Battery", PERCENTAGE, None, DEVICE_CLASS_BATTERY], "rssi": [ "RSSI", PERCENTAGE, - "mdi:wifi-strength-2", + None, DEVICE_CLASS_SIGNAL_STRENGTH, ], "salt": ["Salt", "mg/L", "mdi:pool", None], } SCAN_INTERVAL = timedelta(hours=1) - _LOGGER = logging.getLogger(__name__) @@ -51,13 +49,6 @@ async def async_setup_entry(hass, entry, async_add_entities): api = hass.data[DOMAIN][entry.entry_id] - def get_all_pool_data(pool): - """Add pool details and last measures to pool data.""" - pool["ICO"] = api.get_ICO_details(pool["id"]) - pool["sensors"] = api.get_last_pool_measures(pool["id"]) - - return pool - async def async_update_data(): """Fetch data from API endpoint. @@ -65,14 +56,7 @@ async def async_setup_entry(hass, entry, async_add_entities): so entities can quickly look up their data. """ try: - pools = await hass.async_add_executor_job(api.get_pools) - - return await asyncio.gather( - *[ - hass.async_add_executor_job(get_all_pool_data, pool) - for pool in pools - ] - ) + return await hass.async_add_executor_job(api.get_all_pools_data) except OndiloError as err: raise UpdateFailed(f"Error communicating with API: {err}") from err @@ -145,11 +129,6 @@ class OndiloICO(CoordinatorEntity): @property def state(self): """Last value of the sensor.""" - _LOGGER.debug( - "Retrieving Ondilo sensor %s state value: %s", - self._name, - self._devdata()["value"], - ) return self._devdata()["value"] @property diff --git a/tests/components/ondilo_ico/test_config_flow.py b/tests/components/ondilo_ico/test_config_flow.py index b7505a85b3d..69d69e06b7c 100644 --- a/tests/components/ondilo_ico/test_config_flow.py +++ b/tests/components/ondilo_ico/test_config_flow.py @@ -2,7 +2,6 @@ from unittest.mock import patch from homeassistant import config_entries, data_entry_flow, setup -from homeassistant.components.ondilo_ico import config_flow from homeassistant.components.ondilo_ico.const import ( DOMAIN, OAUTH2_AUTHORIZE, @@ -23,9 +22,6 @@ async def test_abort_if_existing_entry(hass): """Check flow abort when an entry already exist.""" MockConfigEntry(domain=DOMAIN).add_to_hass(hass) - flow = config_flow.OAuth2FlowHandler() - flow.hass = hass - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) From 93ae65d7047290ae8657bec180f666ba7a12abc6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 6 Jan 2021 13:27:05 +0100 Subject: [PATCH 112/507] Improve MQTT number test coverage (#44870) --- tests/components/mqtt/test_number.py | 31 ++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index ac92b842f25..dc7c7ebfe42 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -69,8 +69,8 @@ async def test_run_number_setup(hass, mqtt_mock): assert state.state == "20.5" -async def test_run_number_service(hass, mqtt_mock): - """Test that set_value service works.""" +async def test_run_number_service_optimistic(hass, mqtt_mock): + """Test that set_value service works in optimistic mode.""" topic = "test/number" await async_setup_component( hass, @@ -79,6 +79,7 @@ async def test_run_number_service(hass, mqtt_mock): ) await hass.async_block_till_done() + # Integer await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, @@ -91,6 +92,32 @@ async def test_run_number_service(hass, mqtt_mock): state = hass.states.get("number.test_number") assert state.state == "30" + # Float with no decimal -> integer + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: "number.test_number", ATTR_VALUE: 42.0}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with(topic, "42", 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("number.test_number") + assert state.state == "42" + + # Float with decimal -> float + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: "number.test_number", ATTR_VALUE: 42.1}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with(topic, "42.1", 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("number.test_number") + assert state.state == "42.1" + async def test_availability_when_connection_lost(hass, mqtt_mock): """Test availability after MQTT disconnection.""" From 1c2f88c5009fca23c3dfa39ef03637af3faa1515 Mon Sep 17 00:00:00 2001 From: treylok Date: Wed, 6 Jan 2021 06:40:24 -0600 Subject: [PATCH 113/507] Bump python-ecobee-api to 0.2.8 (#44866) --- homeassistant/components/ecobee/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecobee/manifest.json b/homeassistant/components/ecobee/manifest.json index 38d6b4577b6..040744b27aa 100644 --- a/homeassistant/components/ecobee/manifest.json +++ b/homeassistant/components/ecobee/manifest.json @@ -3,6 +3,6 @@ "name": "ecobee", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ecobee", - "requirements": ["python-ecobee-api==0.2.7"], + "requirements": ["python-ecobee-api==0.2.8"], "codeowners": ["@marthoc"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3ab3b02d995..5a80497cc14 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1741,7 +1741,7 @@ python-clementine-remote==1.0.1 python-digitalocean==1.13.2 # homeassistant.components.ecobee -python-ecobee-api==0.2.7 +python-ecobee-api==0.2.8 # homeassistant.components.eq3btsmart # python-eq3bt==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 97143c68fa4..58c9e256c42 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -873,7 +873,7 @@ pysqueezebox==0.5.5 pysyncthru==0.7.0 # homeassistant.components.ecobee -python-ecobee-api==0.2.7 +python-ecobee-api==0.2.8 # homeassistant.components.darksky python-forecastio==1.4.0 From 72e6d58a991098b9d4af46bf0c69b11a46b79e79 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 6 Jan 2021 13:41:21 +0100 Subject: [PATCH 114/507] Bump pychromecast to 7.7.2 (#44871) --- homeassistant/components/cast/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index c1c24a4cda8..5f3deb36552 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==7.7.1"], + "requirements": ["pychromecast==7.7.2"], "after_dependencies": ["cloud", "http", "media_source", "plex", "tts", "zeroconf"], "zeroconf": ["_googlecast._tcp.local."], "codeowners": ["@emontnemery"] diff --git a/requirements_all.txt b/requirements_all.txt index 5a80497cc14..1b2e8af3dc3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1304,7 +1304,7 @@ pycfdns==1.2.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==7.7.1 +pychromecast==7.7.2 # homeassistant.components.pocketcasts pycketcasts==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 58c9e256c42..94b0005b8cd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -664,7 +664,7 @@ pybotvac==0.0.19 pycfdns==1.2.1 # homeassistant.components.cast -pychromecast==7.7.1 +pychromecast==7.7.2 # homeassistant.components.coolmaster pycoolmasternet-async==0.1.2 From 2e864ca435ab81c2ab31f0b77f797b49ace15139 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Jan 2021 14:05:23 +0100 Subject: [PATCH 115/507] Bump codecov/codecov-action from v1.2.0 to v1.2.1 (#44869) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from v1.2.0 to v1.2.1. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/master/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v1.2.0...e156083f13aff6830c92fc5faa23505779fbf649) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ac1ef6b7375..6356803cbef 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -782,4 +782,4 @@ jobs: coverage report --fail-under=94 coverage xml - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1.2.0 + uses: codecov/codecov-action@v1.2.1 From d3d66c2e27e6561fabd11b71e78d758c1de4ac0b Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 6 Jan 2021 07:12:19 -0600 Subject: [PATCH 116/507] Fix Plex media summary attribute (#44863) --- homeassistant/components/plex/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 4d765cc0508..24e37216b70 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -518,7 +518,7 @@ class PlexMediaPlayer(MediaPlayerEntity): "media_content_rating", "media_library_title", "player_source", - "summary", + "media_summary", "username", ]: value = getattr(self, attr, None) From 560e3811a31af6b984b6c99c9dcb10898d1eeb75 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 6 Jan 2021 07:02:04 -0800 Subject: [PATCH 117/507] Generate nest images thumbnails from events (#44638) * Capture nest still images from events Use python google-nest-sdm API for fetching images. Update home assistant to use the google-nest-sdm API for fetching the image contents generated by the server. This uses the existing websession object for server fetches, reducing the amount of new code and facilites unit testing using the existing mechanism. Simplify tests using the image fetch API rather than a snapshot API --- homeassistant/components/nest/camera_sdm.py | 42 ++- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nest/camera_sdm_test.py | 331 ++++++++++++-------- tests/components/nest/climate_sdm_test.py | 4 +- tests/components/nest/conftest.py | 8 +- 7 files changed, 253 insertions(+), 138 deletions(-) diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index a643de0e6c9..262ed3325b2 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -4,7 +4,11 @@ import datetime import logging from typing import Optional -from google_nest_sdm.camera_traits import CameraImageTrait, CameraLiveStreamTrait +from google_nest_sdm.camera_traits import ( + CameraEventImageTrait, + CameraImageTrait, + CameraLiveStreamTrait, +) from google_nest_sdm.device import Device from google_nest_sdm.exceptions import GoogleNestException from haffmpeg.tools import IMAGE_JPEG @@ -59,6 +63,9 @@ class NestCamera(Camera): self._device_info = DeviceInfo(device) self._stream = None self._stream_refresh_unsub = None + # Cache of most recent event image + self._event_id = None + self._event_image_bytes = None @property def should_poll(self) -> bool: @@ -156,7 +163,40 @@ class NestCamera(Camera): async def async_camera_image(self): """Return bytes of camera image.""" + # Returns the snapshot of the last event for ~30 seconds after the event + active_event_image = await self._async_active_event_image() + if active_event_image: + return active_event_image + # Fetch still image from the live stream stream_url = await self.stream_source() if not stream_url: return None return await async_get_image(self.hass, stream_url, output_format=IMAGE_JPEG) + + async def _async_active_event_image(self): + """Return image from any active events happening.""" + if CameraEventImageTrait.NAME not in self._device.traits: + return None + trait = self._device.active_event_trait + if not trait: + return None + # Reuse image bytes if they have already been fetched + event_id = trait.last_event.event_id + if self._event_id is not None and self._event_id == event_id: + return self._event_image_bytes + _LOGGER.info("Fetching URL for event_id %s", event_id) + try: + event_image = await trait.generate_active_event_image() + except GoogleNestException as err: + _LOGGER.debug("Unable to generate event image URL: %s", err) + return None + if not event_image: + return None + try: + image_bytes = await event_image.contents() + except GoogleNestException as err: + _LOGGER.debug("Unable to fetch event image: %s", err) + return None + self._event_id = event_id + self._event_image_bytes = image_bytes + return image_bytes diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index c334633e362..1d64ba73d89 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.2.5" + "google-nest-sdm==0.2.6" ], "codeowners": [ "@awarecan", diff --git a/requirements_all.txt b/requirements_all.txt index 1b2e8af3dc3..e44562185e3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -681,7 +681,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.2.5 +google-nest-sdm==0.2.6 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 94b0005b8cd..15fc3010bb4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -355,7 +355,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.2.5 +google-nest-sdm==0.2.6 # homeassistant.components.gree greeclimate==0.10.3 diff --git a/tests/components/nest/camera_sdm_test.py b/tests/components/nest/camera_sdm_test.py index 07a21eb2b68..f2aee2d17c5 100644 --- a/tests/components/nest/camera_sdm_test.py +++ b/tests/components/nest/camera_sdm_test.py @@ -10,6 +10,7 @@ from unittest.mock import patch import aiohttp from google_nest_sdm.device import Device +from google_nest_sdm.event import EventMessage import pytest from homeassistant.components import camera @@ -36,9 +37,69 @@ DEVICE_TRAITS = { "videoCodecs": ["H264"], "audioCodecs": ["AAC"], }, + "sdm.devices.traits.CameraEventImage": {}, + "sdm.devices.traits.CameraMotion": {}, } DATETIME_FORMAT = "YY-MM-DDTHH:MM:SS" DOMAIN = "nest" +MOTION_EVENT_ID = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." + +# Tests can assert that image bytes came from an event or was decoded +# from the live stream. +IMAGE_BYTES_FROM_EVENT = b"test url image bytes" +IMAGE_BYTES_FROM_STREAM = b"test stream image bytes" + +TEST_IMAGE_URL = "https://domain/sdm_event_snapshot/dGTZwR3o4Y1..." +GENERATE_IMAGE_URL_RESPONSE = { + "results": { + "url": TEST_IMAGE_URL, + "token": "g.0.eventToken", + }, +} +IMAGE_AUTHORIZATION_HEADERS = {"Authorization": "Basic g.0.eventToken"} + + +def make_motion_event(timestamp: datetime.datetime = None) -> EventMessage: + """Create an EventMessage for a motion event.""" + if not timestamp: + timestamp = utcnow() + return EventMessage( + { + "eventId": "some-event-id", + "timestamp": timestamp.isoformat(timespec="seconds"), + "resourceUpdate": { + "name": DEVICE_ID, + "events": { + "sdm.devices.events.CameraMotion.Motion": { + "eventSessionId": "CjY5Y3VKaTZwR3o4Y19YbTVfMF...", + "eventId": MOTION_EVENT_ID, + }, + }, + }, + }, + auth=None, + ) + + +def make_stream_url_response( + expiration: datetime.datetime = None, token_num: int = 0 +) -> aiohttp.web.Response: + """Make response for the API that generates a streaming url.""" + if not expiration: + # Default to an arbitrary time in the future + expiration = utcnow() + datetime.timedelta(seconds=100) + return aiohttp.web.json_response( + { + "results": { + "streamUrls": { + "rtspUrl": f"rtsp://some/url?auth=g.{token_num}.streamingToken" + }, + "streamExtensionToken": f"g.{token_num}.extensionToken", + "streamToken": f"g.{token_num}.streamingToken", + "expiresAt": expiration.isoformat(timespec="seconds"), + }, + } + ) async def async_setup_camera(hass, traits={}, auth=None): @@ -63,6 +124,19 @@ async def fire_alarm(hass, point_in_time): await hass.async_block_till_done() +async def async_get_image(hass): + """Get image from the camera, a wrapper around camera.async_get_image.""" + # Note: this patches ImageFrame to simulate decoding an image from a live + # stream, however the test may not use it. Tests assert on the image + # contents to determine if the image came from the live stream or event. + with patch( + "homeassistant.components.ffmpeg.ImageFrame.get_image", + autopatch=True, + return_value=IMAGE_BYTES_FROM_STREAM, + ): + return await camera.async_get_image(hass, "camera.my_camera") + + async def test_no_devices(hass): """Test configuration that returns no devices.""" await async_setup_camera(hass) @@ -106,22 +180,7 @@ async def test_camera_device(hass): async def test_camera_stream(hass, auth): """Test a basic camera and fetch its live stream.""" - now = utcnow() - expiration = now + datetime.timedelta(seconds=100) - 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"), - }, - } - ) - ] + auth.responses = [make_stream_url_response()] await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) assert len(hass.states.async_all()) == 1 @@ -132,14 +191,8 @@ async def test_camera_stream(hass, auth): stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") assert stream_source == "rtsp://some/url?auth=g.0.streamingToken" - with patch( - "homeassistant.components.ffmpeg.ImageFrame.get_image", - autopatch=True, - return_value=b"image bytes", - ): - image = await camera.async_get_image(hass, "camera.my_camera") - - assert image.content == b"image bytes" + image = await async_get_image(hass) + assert image.content == IMAGE_BYTES_FROM_STREAM async def test_camera_stream_missing_trait(hass, auth): @@ -166,10 +219,9 @@ async def test_camera_stream_missing_trait(hass, auth): stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") assert stream_source is None - # Currently on support getting the image from a live stream + # Unable to get an image from the live stream with pytest.raises(HomeAssistantError): - image = await camera.async_get_image(hass, "camera.my_camera") - assert image is None + await async_get_image(hass) async def test_refresh_expired_stream_token(hass, auth): @@ -180,38 +232,11 @@ async def test_refresh_expired_stream_token(hass, auth): stream_3_expiration = now + datetime.timedelta(seconds=360) auth.responses = [ # Stream URL #1 - aiohttp.web.json_response( - { - "results": { - "streamUrls": { - "rtspUrl": "rtsp://some/url?auth=g.1.streamingToken" - }, - "streamExtensionToken": "g.1.extensionToken", - "streamToken": "g.1.streamingToken", - "expiresAt": stream_1_expiration.isoformat(timespec="seconds"), - }, - } - ), + make_stream_url_response(stream_1_expiration, token_num=1), # Stream URL #2 - aiohttp.web.json_response( - { - "results": { - "streamExtensionToken": "g.2.extensionToken", - "streamToken": "g.2.streamingToken", - "expiresAt": stream_2_expiration.isoformat(timespec="seconds"), - }, - } - ), + make_stream_url_response(stream_2_expiration, token_num=2), # Stream URL #3 - aiohttp.web.json_response( - { - "results": { - "streamExtensionToken": "g.3.extensionToken", - "streamToken": "g.3.streamingToken", - "expiresAt": stream_3_expiration.isoformat(timespec="seconds"), - }, - } - ), + make_stream_url_response(stream_3_expiration, token_num=3), ] await async_setup_camera( hass, @@ -258,36 +283,10 @@ async def test_stream_response_already_expired(hass, auth): stream_1_expiration = now + datetime.timedelta(seconds=-90) stream_2_expiration = now + datetime.timedelta(seconds=+90) auth.responses = [ - aiohttp.web.json_response( - { - "results": { - "streamUrls": { - "rtspUrl": "rtsp://some/url?auth=g.1.streamingToken" - }, - "streamExtensionToken": "g.1.extensionToken", - "streamToken": "g.1.streamingToken", - "expiresAt": stream_1_expiration.isoformat(timespec="seconds"), - }, - } - ), - aiohttp.web.json_response( - { - "results": { - "streamUrls": { - "rtspUrl": "rtsp://some/url?auth=g.2.streamingToken" - }, - "streamExtensionToken": "g.2.extensionToken", - "streamToken": "g.2.streamingToken", - "expiresAt": stream_2_expiration.isoformat(timespec="seconds"), - }, - } - ), + make_stream_url_response(stream_1_expiration, token_num=1), + make_stream_url_response(stream_2_expiration, token_num=2), ] - await async_setup_camera( - hass, - DEVICE_TRAITS, - auth=auth, - ) + await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) assert len(hass.states.async_all()) == 1 cam = hass.states.get("camera.my_camera") @@ -307,21 +306,8 @@ async def test_stream_response_already_expired(hass, auth): 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) 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"), - }, - } - ), + make_stream_url_response(), aiohttp.web.json_response({"results": {}}), ] await async_setup_camera( @@ -349,39 +335,13 @@ async def test_refresh_expired_stream_failure(hass, auth): stream_1_expiration = now + datetime.timedelta(seconds=90) stream_2_expiration = now + datetime.timedelta(seconds=180) auth.responses = [ - aiohttp.web.json_response( - { - "results": { - "streamUrls": { - "rtspUrl": "rtsp://some/url?auth=g.1.streamingToken" - }, - "streamExtensionToken": "g.1.extensionToken", - "streamToken": "g.1.streamingToken", - "expiresAt": stream_1_expiration.isoformat(timespec="seconds"), - }, - } - ), + make_stream_url_response(expiration=stream_1_expiration, token_num=1), # Extending the stream fails with arbitrary error aiohttp.web.Response(status=500), # Next attempt to get a stream fetches a new url - aiohttp.web.json_response( - { - "results": { - "streamUrls": { - "rtspUrl": "rtsp://some/url?auth=g.2.streamingToken" - }, - "streamExtensionToken": "g.2.extensionToken", - "streamToken": "g.2.streamingToken", - "expiresAt": stream_2_expiration.isoformat(timespec="seconds"), - }, - } - ), + make_stream_url_response(expiration=stream_2_expiration, token_num=2), ] - await async_setup_camera( - hass, - DEVICE_TRAITS, - auth=auth, - ) + await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) assert len(hass.states.async_all()) == 1 cam = hass.states.get("camera.my_camera") @@ -399,3 +359,116 @@ async def test_refresh_expired_stream_failure(hass, auth): # The stream is entirely refreshed stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") assert stream_source == "rtsp://some/url?auth=g.2.streamingToken" + + +async def test_camera_image_from_last_event(hass, auth): + """Test an image generated from an event.""" + # The subscriber receives a message related to an image event. The camera + # holds on to the event message. When the test asks for a capera snapshot + # it exchanges the event id for an image url and fetches the image. + subscriber = await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) + assert len(hass.states.async_all()) == 1 + assert hass.states.get("camera.my_camera") + + # Simulate a pubsub message received by the subscriber with a motion event. + await subscriber.async_receive_event(make_motion_event()) + await hass.async_block_till_done() + + auth.responses = [ + # Fake response from API that returns url image + aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), + # Fake response for the image content fetch + aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT), + ] + + image = await async_get_image(hass) + assert image.content == IMAGE_BYTES_FROM_EVENT + # Verify expected image fetch request was captured + assert auth.url == TEST_IMAGE_URL + assert auth.headers == IMAGE_AUTHORIZATION_HEADERS + + # An additional fetch uses the cache and does not send another RPC + image = await async_get_image(hass) + assert image.content == IMAGE_BYTES_FROM_EVENT + # Verify expected image fetch request was captured + assert auth.url == TEST_IMAGE_URL + assert auth.headers == IMAGE_AUTHORIZATION_HEADERS + + +async def test_camera_image_from_event_not_supported(hass, auth): + """Test fallback to stream image when event images are not supported.""" + # Create a device that does not support the CameraEventImgae trait + traits = DEVICE_TRAITS.copy() + del traits["sdm.devices.traits.CameraEventImage"] + subscriber = await async_setup_camera(hass, traits, auth=auth) + assert len(hass.states.async_all()) == 1 + assert hass.states.get("camera.my_camera") + + await subscriber.async_receive_event(make_motion_event()) + await hass.async_block_till_done() + + # Camera fetches a stream url since CameraEventImage is not supported + auth.responses = [make_stream_url_response()] + + image = await async_get_image(hass) + assert image.content == IMAGE_BYTES_FROM_STREAM + + +async def test_generate_event_image_url_failure(hass, auth): + """Test fallback to stream on failure to create an image url.""" + subscriber = await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) + assert len(hass.states.async_all()) == 1 + assert hass.states.get("camera.my_camera") + + await subscriber.async_receive_event(make_motion_event()) + await hass.async_block_till_done() + + auth.responses = [ + # Fail to generate the image url + aiohttp.web.Response(status=500), + # Camera fetches a stream url as a fallback + make_stream_url_response(), + ] + + image = await async_get_image(hass) + assert image.content == IMAGE_BYTES_FROM_STREAM + + +async def test_fetch_event_image_failure(hass, auth): + """Test fallback to a stream on image download failure.""" + subscriber = await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) + assert len(hass.states.async_all()) == 1 + assert hass.states.get("camera.my_camera") + + await subscriber.async_receive_event(make_motion_event()) + await hass.async_block_till_done() + + auth.responses = [ + # Fake response from API that returns url image + aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), + # Fail to download the image + aiohttp.web.Response(status=500), + # Camera fetches a stream url as a fallback + make_stream_url_response(), + ] + + image = await async_get_image(hass) + assert image.content == IMAGE_BYTES_FROM_STREAM + + +async def test_event_image_expired(hass, auth): + """Test fallback for an event event image that has expired.""" + subscriber = await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) + assert len(hass.states.async_all()) == 1 + assert hass.states.get("camera.my_camera") + + # Simulate a pubsub message has already expired + event_timestamp = utcnow() - datetime.timedelta(seconds=40) + await subscriber.async_receive_event(make_motion_event(event_timestamp)) + await hass.async_block_till_done() + + # Fallback to a stream url since the event message is expired. + auth.responses = [make_stream_url_response()] + + image = await async_get_image(hass) + assert image.content == IMAGE_BYTES_FROM_STREAM diff --git a/tests/components/nest/climate_sdm_test.py b/tests/components/nest/climate_sdm_test.py index 43a422e223e..ef332d0e848 100644 --- a/tests/components/nest/climate_sdm_test.py +++ b/tests/components/nest/climate_sdm_test.py @@ -933,14 +933,14 @@ async def test_thermostat_set_hvac_fan_only(hass, auth): assert len(auth.captured_requests) == 2 - (method, url, json) = auth.captured_requests.pop(0) + (method, url, json, headers) = auth.captured_requests.pop(0) assert method == "post" assert url == "some-device-id:executeCommand" assert json == { "command": "sdm.devices.commands.Fan.SetTimer", "params": {"timerMode": "ON"}, } - (method, url, json) = auth.captured_requests.pop(0) + (method, url, json, headers) = auth.captured_requests.pop(0) assert method == "post" assert url == "some-device-id:executeCommand" assert json == { diff --git a/tests/components/nest/conftest.py b/tests/components/nest/conftest.py index 4ab780f57e6..764f037d181 100644 --- a/tests/components/nest/conftest.py +++ b/tests/components/nest/conftest.py @@ -23,6 +23,7 @@ class FakeAuth(AbstractAuth): self.method = None self.url = None self.json = None + self.headers = None self.captured_requests = [] # Set up by fixture self.client = None @@ -31,12 +32,13 @@ class FakeAuth(AbstractAuth): """Return a valid access token.""" return "" - async def request(self, method, url, json): + async def request(self, method, url, **kwargs): """Capure the request arguments for tests to assert on.""" self.method = method self.url = url - self.json = json - self.captured_requests.append((method, url, json)) + self.json = kwargs.get("json") + self.headers = kwargs.get("headers") + self.captured_requests.append((method, url, self.json, self.headers)) return await self.client.get("/") async def response_handler(self, request): From 6de882498000f40be303b487e5c943811de6fc6a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 6 Jan 2021 16:47:02 +0100 Subject: [PATCH 118/507] Revert "Bump pypck to 0.7.8" (#44884) This reverts commit addafd517f3617071468b2f4ae3fa31f655a9ed2. --- homeassistant/components/lcn/__init__.py | 3 +-- homeassistant/components/lcn/binary_sensor.py | 17 +++++++---------- homeassistant/components/lcn/climate.py | 5 ++--- homeassistant/components/lcn/cover.py | 3 +-- homeassistant/components/lcn/light.py | 6 ++---- homeassistant/components/lcn/manifest.json | 8 ++------ homeassistant/components/lcn/sensor.py | 6 ++---- homeassistant/components/lcn/switch.py | 6 ++---- requirements_all.txt | 2 +- 9 files changed, 20 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index cc1e47d71fc..72f11b7b005 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -139,8 +139,7 @@ class LcnEntity(Entity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" - if not self.device_connection.is_group: - self.device_connection.register_for_inputs(self.input_received) + self.device_connection.register_for_inputs(self.input_received) @property def name(self): diff --git a/homeassistant/components/lcn/binary_sensor.py b/homeassistant/components/lcn/binary_sensor.py index 415668f5924..5d712045c93 100644 --- a/homeassistant/components/lcn/binary_sensor.py +++ b/homeassistant/components/lcn/binary_sensor.py @@ -50,10 +50,9 @@ class LcnRegulatorLockSensor(LcnEntity, BinarySensorEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler( - self.setpoint_variable - ) + await self.device_connection.activate_status_request_handler( + self.setpoint_variable + ) @property def is_on(self): @@ -86,10 +85,9 @@ class LcnBinarySensor(LcnEntity, BinarySensorEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler( - self.bin_sensor_port - ) + await self.device_connection.activate_status_request_handler( + self.bin_sensor_port + ) @property def is_on(self): @@ -118,8 +116,7 @@ class LcnLockKeysSensor(LcnEntity, BinarySensorEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler(self.source) + await self.device_connection.activate_status_request_handler(self.source) @property def is_on(self): diff --git a/homeassistant/components/lcn/climate.py b/homeassistant/components/lcn/climate.py index ece3994f651..e3eb92a426f 100644 --- a/homeassistant/components/lcn/climate.py +++ b/homeassistant/components/lcn/climate.py @@ -63,9 +63,8 @@ class LcnClimate(LcnEntity, ClimateEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler(self.variable) - await self.device_connection.activate_status_request_handler(self.setpoint) + await self.device_connection.activate_status_request_handler(self.variable) + await self.device_connection.activate_status_request_handler(self.setpoint) @property def supported_features(self): diff --git a/homeassistant/components/lcn/cover.py b/homeassistant/components/lcn/cover.py index 3d7c2a06a3b..c5e407573ba 100644 --- a/homeassistant/components/lcn/cover.py +++ b/homeassistant/components/lcn/cover.py @@ -161,8 +161,7 @@ class LcnRelayCover(LcnEntity, CoverEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler(self.motor) + await self.device_connection.activate_status_request_handler(self.motor) @property def is_closed(self): diff --git a/homeassistant/components/lcn/light.py b/homeassistant/components/lcn/light.py index 5242ed1cc59..c6ef895b7df 100644 --- a/homeassistant/components/lcn/light.py +++ b/homeassistant/components/lcn/light.py @@ -68,8 +68,7 @@ class LcnOutputLight(LcnEntity, LightEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler(self.output) + await self.device_connection.activate_status_request_handler(self.output) @property def supported_features(self): @@ -156,8 +155,7 @@ class LcnRelayLight(LcnEntity, LightEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler(self.output) + await self.device_connection.activate_status_request_handler(self.output) @property def is_on(self): diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index 919051d7e7a..f07c4d9c646 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -2,10 +2,6 @@ "domain": "lcn", "name": "LCN", "documentation": "https://www.home-assistant.io/integrations/lcn", - "requirements": [ - "pypck==0.7.8" - ], - "codeowners": [ - "@alengwenus" - ] + "requirements": ["pypck==0.7.7"], + "codeowners": ["@alengwenus"] } diff --git a/homeassistant/components/lcn/sensor.py b/homeassistant/components/lcn/sensor.py index 4d4be5e1259..26b54def974 100644 --- a/homeassistant/components/lcn/sensor.py +++ b/homeassistant/components/lcn/sensor.py @@ -57,8 +57,7 @@ class LcnVariableSensor(LcnEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler(self.variable) + await self.device_connection.activate_status_request_handler(self.variable) @property def state(self): @@ -99,8 +98,7 @@ class LcnLedLogicSensor(LcnEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler(self.source) + await self.device_connection.activate_status_request_handler(self.source) @property def state(self): diff --git a/homeassistant/components/lcn/switch.py b/homeassistant/components/lcn/switch.py index 6f9cc25db99..5891629627e 100644 --- a/homeassistant/components/lcn/switch.py +++ b/homeassistant/components/lcn/switch.py @@ -50,8 +50,7 @@ class LcnOutputSwitch(LcnEntity, SwitchEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler(self.output) + await self.device_connection.activate_status_request_handler(self.output) @property def is_on(self): @@ -98,8 +97,7 @@ class LcnRelaySwitch(LcnEntity, SwitchEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - if not self.device_connection.is_group: - await self.device_connection.activate_status_request_handler(self.output) + await self.device_connection.activate_status_request_handler(self.output) @property def is_on(self): diff --git a/requirements_all.txt b/requirements_all.txt index e44562185e3..7a1708a20f5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1607,7 +1607,7 @@ pyownet==0.10.0.post1 pypca==0.0.7 # homeassistant.components.lcn -pypck==0.7.8 +pypck==0.7.7 # homeassistant.components.pjlink pypjlink2==1.2.1 From 03ffeb9a02725837b429ad12b140dc753e412688 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 6 Jan 2021 18:18:04 +0200 Subject: [PATCH 119/507] Fix notion bridge id update device registry identifier usage (#44872) --- homeassistant/components/notion/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 88da19f5ab2..87871ce5f3c 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -230,10 +230,10 @@ class NotionEntity(CoordinatorEntity): device_registry = await dr.async_get_registry(self.hass) bridge = self.coordinator.data["bridges"][self._bridge_id] bridge_device = device_registry.async_get_device( - {DOMAIN: bridge["hardware_id"]}, set() + {(DOMAIN, bridge["hardware_id"])}, set() ) this_device = device_registry.async_get_device( - {DOMAIN: sensor["hardware_id"]}, set() + {(DOMAIN, sensor["hardware_id"])}, set() ) device_registry.async_update_device( From 92431049e5c00618466606288c056687fb3dbbf9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 6 Jan 2021 18:01:06 +0100 Subject: [PATCH 120/507] Revert "Revert "Bump pypck to 0.7.8"" (#44885) This reverts commit 6de882498000f40be303b487e5c943811de6fc6a. --- homeassistant/components/lcn/__init__.py | 3 ++- homeassistant/components/lcn/binary_sensor.py | 17 ++++++++++------- homeassistant/components/lcn/climate.py | 5 +++-- homeassistant/components/lcn/cover.py | 3 ++- homeassistant/components/lcn/light.py | 6 ++++-- homeassistant/components/lcn/manifest.json | 8 ++++++-- homeassistant/components/lcn/sensor.py | 6 ++++-- homeassistant/components/lcn/switch.py | 6 ++++-- requirements_all.txt | 2 +- 9 files changed, 36 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index 72f11b7b005..cc1e47d71fc 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -139,7 +139,8 @@ class LcnEntity(Entity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" - self.device_connection.register_for_inputs(self.input_received) + if not self.device_connection.is_group: + self.device_connection.register_for_inputs(self.input_received) @property def name(self): diff --git a/homeassistant/components/lcn/binary_sensor.py b/homeassistant/components/lcn/binary_sensor.py index 5d712045c93..415668f5924 100644 --- a/homeassistant/components/lcn/binary_sensor.py +++ b/homeassistant/components/lcn/binary_sensor.py @@ -50,9 +50,10 @@ class LcnRegulatorLockSensor(LcnEntity, BinarySensorEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler( - self.setpoint_variable - ) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler( + self.setpoint_variable + ) @property def is_on(self): @@ -85,9 +86,10 @@ class LcnBinarySensor(LcnEntity, BinarySensorEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler( - self.bin_sensor_port - ) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler( + self.bin_sensor_port + ) @property def is_on(self): @@ -116,7 +118,8 @@ class LcnLockKeysSensor(LcnEntity, BinarySensorEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.source) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.source) @property def is_on(self): diff --git a/homeassistant/components/lcn/climate.py b/homeassistant/components/lcn/climate.py index e3eb92a426f..ece3994f651 100644 --- a/homeassistant/components/lcn/climate.py +++ b/homeassistant/components/lcn/climate.py @@ -63,8 +63,9 @@ class LcnClimate(LcnEntity, ClimateEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.variable) - await self.device_connection.activate_status_request_handler(self.setpoint) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.variable) + await self.device_connection.activate_status_request_handler(self.setpoint) @property def supported_features(self): diff --git a/homeassistant/components/lcn/cover.py b/homeassistant/components/lcn/cover.py index c5e407573ba..3d7c2a06a3b 100644 --- a/homeassistant/components/lcn/cover.py +++ b/homeassistant/components/lcn/cover.py @@ -161,7 +161,8 @@ class LcnRelayCover(LcnEntity, CoverEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.motor) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.motor) @property def is_closed(self): diff --git a/homeassistant/components/lcn/light.py b/homeassistant/components/lcn/light.py index c6ef895b7df..5242ed1cc59 100644 --- a/homeassistant/components/lcn/light.py +++ b/homeassistant/components/lcn/light.py @@ -68,7 +68,8 @@ class LcnOutputLight(LcnEntity, LightEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.output) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.output) @property def supported_features(self): @@ -155,7 +156,8 @@ class LcnRelayLight(LcnEntity, LightEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.output) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.output) @property def is_on(self): diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index f07c4d9c646..919051d7e7a 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -2,6 +2,10 @@ "domain": "lcn", "name": "LCN", "documentation": "https://www.home-assistant.io/integrations/lcn", - "requirements": ["pypck==0.7.7"], - "codeowners": ["@alengwenus"] + "requirements": [ + "pypck==0.7.8" + ], + "codeowners": [ + "@alengwenus" + ] } diff --git a/homeassistant/components/lcn/sensor.py b/homeassistant/components/lcn/sensor.py index 26b54def974..4d4be5e1259 100644 --- a/homeassistant/components/lcn/sensor.py +++ b/homeassistant/components/lcn/sensor.py @@ -57,7 +57,8 @@ class LcnVariableSensor(LcnEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.variable) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.variable) @property def state(self): @@ -98,7 +99,8 @@ class LcnLedLogicSensor(LcnEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.source) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.source) @property def state(self): diff --git a/homeassistant/components/lcn/switch.py b/homeassistant/components/lcn/switch.py index 5891629627e..6f9cc25db99 100644 --- a/homeassistant/components/lcn/switch.py +++ b/homeassistant/components/lcn/switch.py @@ -50,7 +50,8 @@ class LcnOutputSwitch(LcnEntity, SwitchEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.output) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.output) @property def is_on(self): @@ -97,7 +98,8 @@ class LcnRelaySwitch(LcnEntity, SwitchEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.output) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.output) @property def is_on(self): diff --git a/requirements_all.txt b/requirements_all.txt index 7a1708a20f5..e44562185e3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1607,7 +1607,7 @@ pyownet==0.10.0.post1 pypca==0.0.7 # homeassistant.components.lcn -pypck==0.7.7 +pypck==0.7.8 # homeassistant.components.pjlink pypjlink2==1.2.1 From 751ac0b955592bf53aed9799f283215633e7d11c Mon Sep 17 00:00:00 2001 From: emufan Date: Wed, 6 Jan 2021 20:15:16 +0100 Subject: [PATCH 121/507] Bump pydaikin version to 2.4.1 (#44888) --- homeassistant/components/daikin/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index ebf31967cc7..245f10a0e83 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -3,7 +3,7 @@ "name": "Daikin AC", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/daikin", - "requirements": ["pydaikin==2.4.0"], + "requirements": ["pydaikin==2.4.1"], "codeowners": ["@fredrike"], "zeroconf": ["_dkapi._tcp.local."], "quality_scale": "platinum" diff --git a/requirements_all.txt b/requirements_all.txt index e44562185e3..841bccf3f4e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1328,7 +1328,7 @@ pycsspeechtts==1.0.4 # pycups==1.9.73 # homeassistant.components.daikin -pydaikin==2.4.0 +pydaikin==2.4.1 # homeassistant.components.danfoss_air pydanfossair==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 15fc3010bb4..d3daba57fe7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -670,7 +670,7 @@ pychromecast==7.7.2 pycoolmasternet-async==0.1.2 # homeassistant.components.daikin -pydaikin==2.4.0 +pydaikin==2.4.1 # homeassistant.components.deconz pydeconz==77 From 2fb3be50ab0b806cfc099bc3671ad6c8ee4195e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 7 Jan 2021 14:49:45 +0200 Subject: [PATCH 122/507] Make DeviceRegistry.async_get_device connections arg optional (#44897) * Make async_get_device connections Optional, default None * Remove unnecessary async_get_device connections arg usages Some of these were using an incorrect collection type, which didn't cause issues mostly just due to luck. --- homeassistant/components/acmeda/base.py | 4 +- homeassistant/components/acmeda/helpers.py | 4 +- homeassistant/components/broadlink/device.py | 4 +- homeassistant/components/heos/__init__.py | 2 +- homeassistant/components/hue/helpers.py | 4 +- homeassistant/components/insteon/utils.py | 4 +- homeassistant/components/kodi/media_player.py | 2 +- homeassistant/components/nest/__init__.py | 2 +- homeassistant/components/notion/__init__.py | 4 +- homeassistant/components/ozw/__init__.py | 4 +- homeassistant/components/rfxtrx/__init__.py | 1 - homeassistant/components/zwave/__init__.py | 2 +- homeassistant/components/zwave/node_entity.py | 6 +- homeassistant/helpers/device_registry.py | 10 +-- tests/components/august/test_binary_sensor.py | 4 +- tests/components/august/test_lock.py | 2 +- tests/components/bond/test_init.py | 4 +- tests/components/broadlink/test_device.py | 8 +-- tests/components/broadlink/test_remote.py | 6 +- tests/components/broadlink/test_sensors.py | 32 +++------- tests/components/canary/test_sensor.py | 4 +- tests/components/gogogate2/test_cover.py | 4 +- tests/components/heos/test_media_player.py | 4 +- tests/components/homekit/test_homekit.py | 4 +- tests/components/hue/test_device_trigger.py | 6 +- tests/components/mqtt/test_common.py | 20 +++--- .../mqtt/test_device_tracker_discovery.py | 4 +- tests/components/mqtt/test_device_trigger.py | 64 +++++++++---------- tests/components/mqtt/test_discovery.py | 4 +- tests/components/mqtt/test_init.py | 22 +++---- tests/components/mqtt/test_sensor.py | 2 +- tests/components/mqtt/test_tag.py | 50 +++++++-------- tests/components/nest/test_device_trigger.py | 12 +--- .../onewire/test_entity_owserver.py | 2 +- .../components/onewire/test_entity_sysbus.py | 2 +- tests/components/opentherm_gw/test_init.py | 8 +-- tests/components/powerwall/test_sensor.py | 1 - tests/components/ps4/test_media_player.py | 4 +- tests/components/rfxtrx/test_init.py | 4 +- .../risco/test_alarm_control_panel.py | 4 +- tests/components/risco/test_binary_sensor.py | 4 +- tests/components/sharkiq/test_vacuum.py | 2 +- .../smartthings/test_binary_sensor.py | 2 +- tests/components/smartthings/test_climate.py | 2 +- tests/components/smartthings/test_cover.py | 2 +- tests/components/smartthings/test_fan.py | 2 +- tests/components/smartthings/test_light.py | 2 +- tests/components/smartthings/test_lock.py | 2 +- tests/components/smartthings/test_sensor.py | 2 +- tests/components/smartthings/test_switch.py | 2 +- tests/components/songpal/test_media_player.py | 4 +- tests/components/sonos/test_media_player.py | 3 +- tests/components/twinkly/test_twinkly.py | 2 +- tests/components/zha/test_device_action.py | 4 +- tests/components/zha/test_device_trigger.py | 10 +-- tests/helpers/test_device_registry.py | 32 +++++----- tests/helpers/test_entity_platform.py | 2 +- 57 files changed, 181 insertions(+), 232 deletions(-) diff --git a/homeassistant/components/acmeda/base.py b/homeassistant/components/acmeda/base.py index c467fe17ba3..b325e2c944a 100644 --- a/homeassistant/components/acmeda/base.py +++ b/homeassistant/components/acmeda/base.py @@ -26,9 +26,7 @@ class AcmedaBase(entity.Entity): ent_registry.async_remove(self.entity_id) dev_registry = await get_dev_reg(self.hass) - device = dev_registry.async_get_device( - identifiers={(DOMAIN, self.unique_id)}, connections=set() - ) + device = dev_registry.async_get_device(identifiers={(DOMAIN, self.unique_id)}) if device is not None: dev_registry.async_update_device( device.id, remove_config_entry_id=self.registry_entry.config_entry_id diff --git a/homeassistant/components/acmeda/helpers.py b/homeassistant/components/acmeda/helpers.py index cec971e5fdd..1162aba5dc8 100644 --- a/homeassistant/components/acmeda/helpers.py +++ b/homeassistant/components/acmeda/helpers.py @@ -32,9 +32,7 @@ async def update_devices(hass, config_entry, api): for api_item in api.values(): # Update Device name - device = dev_registry.async_get_device( - identifiers={(DOMAIN, api_item.id)}, connections=set() - ) + device = dev_registry.async_get_device(identifiers={(DOMAIN, api_item.id)}) if device is not None: dev_registry.async_update_device( device.id, diff --git a/homeassistant/components/broadlink/device.py b/homeassistant/components/broadlink/device.py index 24f473c3262..be9c7626ac1 100644 --- a/homeassistant/components/broadlink/device.py +++ b/homeassistant/components/broadlink/device.py @@ -57,9 +57,7 @@ class BroadlinkDevice: Triggered when the device is renamed on the frontend. """ device_registry = await dr.async_get_registry(hass) - device_entry = device_registry.async_get_device( - {(DOMAIN, entry.unique_id)}, set() - ) + device_entry = device_registry.async_get_device({(DOMAIN, entry.unique_id)}) device_registry.async_update_device(device_entry.id, name=entry.title) await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index 81f9fd9dd0e..11020d1166e 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -196,7 +196,7 @@ class ControllerManager: # mapped_ids contains the mapped IDs (new:old) for new_id, old_id in mapped_ids.items(): # update device registry - entry = self._device_registry.async_get_device({(DOMAIN, old_id)}, set()) + entry = self._device_registry.async_get_device({(DOMAIN, old_id)}) new_identifiers = {(DOMAIN, new_id)} if entry: self._device_registry.async_update_device( diff --git a/homeassistant/components/hue/helpers.py b/homeassistant/components/hue/helpers.py index 885677dc269..1760c59a69d 100644 --- a/homeassistant/components/hue/helpers.py +++ b/homeassistant/components/hue/helpers.py @@ -22,9 +22,7 @@ async def remove_devices(bridge, api_ids, current): if entity.entity_id in ent_registry.entities: ent_registry.async_remove(entity.entity_id) dev_registry = await get_dev_reg(bridge.hass) - device = dev_registry.async_get_device( - identifiers={(DOMAIN, entity.device_id)}, connections=set() - ) + device = dev_registry.async_get_device(identifiers={(DOMAIN, entity.device_id)}) if device is not None: dev_registry.async_update_device( device.id, remove_config_entry_id=bridge.config_entry.entry_id diff --git a/homeassistant/components/insteon/utils.py b/homeassistant/components/insteon/utils.py index 7c7bf08792b..ace24644523 100644 --- a/homeassistant/components/insteon/utils.py +++ b/homeassistant/components/insteon/utils.py @@ -294,9 +294,7 @@ def async_register_services(hass): signal = f"{address.id}_{SIGNAL_REMOVE_ENTITY}" async_dispatcher_send(hass, signal) dev_registry = await hass.helpers.device_registry.async_get_registry() - device = dev_registry.async_get_device( - identifiers={(DOMAIN, str(address))}, connections=set() - ) + device = dev_registry.async_get_device(identifiers={(DOMAIN, str(address))}) if device: dev_registry.async_remove_device(device.id) diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index dfe3af4b11e..6b849cb711b 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -398,7 +398,7 @@ class KodiEntity(MediaPlayerEntity): 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)}, []) + 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) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 04c8b9f3627..85e591707ad 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -121,7 +121,7 @@ class SignalUpdateCallback: return _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)}, ()) + device_entry = device_registry.async_get_device({(DOMAIN, device_id)}) if not device_entry: return for event in events: diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 87871ce5f3c..e29ab171a6b 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -230,10 +230,10 @@ class NotionEntity(CoordinatorEntity): device_registry = await dr.async_get_registry(self.hass) bridge = self.coordinator.data["bridges"][self._bridge_id] bridge_device = device_registry.async_get_device( - {(DOMAIN, bridge["hardware_id"])}, set() + {(DOMAIN, bridge["hardware_id"])} ) this_device = device_registry.async_get_device( - {(DOMAIN, sensor["hardware_id"])}, set() + {(DOMAIN, sensor["hardware_id"])} ) device_registry.async_update_device( diff --git a/homeassistant/components/ozw/__init__.py b/homeassistant/components/ozw/__init__.py index 1f46e7a17c6..18fffdbc66e 100644 --- a/homeassistant/components/ozw/__init__.py +++ b/homeassistant/components/ozw/__init__.py @@ -348,7 +348,7 @@ async def async_handle_remove_node(hass: HomeAssistant, node: OZWNode): dev_registry = await get_dev_reg(hass) # grab device in device registry attached to this node dev_id = create_device_id(node) - device = dev_registry.async_get_device({(DOMAIN, dev_id)}, set()) + device = dev_registry.async_get_device({(DOMAIN, dev_id)}) if not device: return devices_to_remove = [device.id] @@ -372,7 +372,7 @@ async def async_handle_node_update(hass: HomeAssistant, node: OZWNode): dev_registry = await get_dev_reg(hass) # grab device in device registry attached to this node dev_id = create_device_id(node) - device = dev_registry.async_get_device({(DOMAIN, dev_id)}, set()) + device = dev_registry.async_get_device({(DOMAIN, dev_id)}) if not device: return # update device in device registry with (updated) info diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index be56f4b789c..067ffeb5313 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -311,7 +311,6 @@ async def async_setup_internal(hass, entry: config_entries.ConfigEntry): device_entry = device_registry.async_get_device( identifiers={(DOMAIN, *device_id)}, - connections=set(), ) if device_entry: event_data[ATTR_DEVICE_ID] = device_entry.id diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index d679de7cfbd..aba169a1919 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -505,7 +505,7 @@ async def async_setup_entry(hass, config_entry): async def _remove_device(node): dev_reg = await async_get_device_registry(hass) identifier, name = node_device_id_and_name(node) - device = dev_reg.async_get_device(identifiers={identifier}, connections=set()) + device = dev_reg.async_get_device(identifiers={identifier}) if device is not None: _LOGGER.debug("Removing Device - %s - %s", device.id, name) dev_reg.async_remove_device(device.id) diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py index 064f21c69b4..56dea1639a3 100644 --- a/homeassistant/components/zwave/node_entity.py +++ b/homeassistant/components/zwave/node_entity.py @@ -246,14 +246,12 @@ class ZWaveNodeEntity(ZWaveBaseEntity): # Set the name in the devices. If they're customised # the customisation will not be stored as name and will stick. dev_reg = await get_dev_reg(self.hass) - device = dev_reg.async_get_device(identifiers={identifier}, connections=set()) + device = dev_reg.async_get_device(identifiers={identifier}) dev_reg.async_update_device(device.id, name=self._name) # update sub-devices too for i in count(2): identifier, new_name = node_device_id_and_name(self.node, i) - device = dev_reg.async_get_device( - identifiers={identifier}, connections=set() - ) + device = dev_reg.async_get_device(identifiers={identifier}) if not device: break dev_reg.async_update_device(device.id, name=new_name) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index a115434fad9..4415babc009 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -139,7 +139,9 @@ class DeviceRegistry: @callback def async_get_device( - self, identifiers: set, connections: set + self, + identifiers: set, + connections: Optional[set] = None, ) -> Optional[DeviceEntry]: """Check if device is registered.""" device_id = self._async_get_device_id_from_index( @@ -150,7 +152,7 @@ class DeviceRegistry: return self.devices[device_id] def _async_get_deleted_device( - self, identifiers: set, connections: set + self, identifiers: set, connections: Optional[set] ) -> Optional[DeletedDeviceEntry]: """Check if device is deleted.""" device_id = self._async_get_device_id_from_index( @@ -161,7 +163,7 @@ class DeviceRegistry: return self.deleted_devices[device_id] def _async_get_device_id_from_index( - self, index: str, identifiers: set, connections: set + self, index: str, identifiers: set, connections: Optional[set] ) -> Optional[str]: """Check if device has previously been registered.""" devices_index = self._devices_index[index] @@ -274,7 +276,7 @@ class DeviceRegistry: name = default_name if via_device is not None: - via = self.async_get_device({via_device}, set()) + via = self.async_get_device({via_device}) via_device_id: Union[str, UndefinedType] = via.id if via else UNDEFINED else: via_device_id = UNDEFINED diff --git a/tests/components/august/test_binary_sensor.py b/tests/components/august/test_binary_sensor.py index 2112638e619..763f9f9528f 100644 --- a/tests/components/august/test_binary_sensor.py +++ b/tests/components/august/test_binary_sensor.py @@ -121,9 +121,7 @@ async def test_doorbell_device_registry(hass): device_registry = await hass.helpers.device_registry.async_get_registry() - reg_device = device_registry.async_get_device( - identifiers={("august", "tmt100")}, connections=set() - ) + reg_device = device_registry.async_get_device(identifiers={("august", "tmt100")}) assert reg_device.model == "hydra1" assert reg_device.name == "tmt100 Name" assert reg_device.manufacturer == "August Home Inc." diff --git a/tests/components/august/test_lock.py b/tests/components/august/test_lock.py index 8baefa68271..d013da30ff6 100644 --- a/tests/components/august/test_lock.py +++ b/tests/components/august/test_lock.py @@ -26,7 +26,7 @@ async def test_lock_device_registry(hass): device_registry = await hass.helpers.device_registry.async_get_registry() reg_device = device_registry.async_get_device( - identifiers={("august", "online_with_doorsense")}, connections=set() + identifiers={("august", "online_with_doorsense")} ) assert reg_device.model == "AUG-MD01" assert reg_device.sw_version == "undefined-4.3.0-1.8.14" diff --git a/tests/components/bond/test_init.py b/tests/components/bond/test_init.py index 3dd54f0de6f..98d86058c49 100644 --- a/tests/components/bond/test_init.py +++ b/tests/components/bond/test_init.py @@ -70,9 +70,7 @@ async def test_async_setup_entry_sets_up_hub_and_supported_domains(hass: HomeAss # verify hub device is registered correctly device_registry = await dr.async_get_registry(hass) - hub = device_registry.async_get_device( - identifiers={(DOMAIN, "test-bond-id")}, connections=set() - ) + hub = device_registry.async_get_device(identifiers={(DOMAIN, "test-bond-id")}) assert hub.name == "test-bond-id" assert hub.manufacturer == "Olibra" assert hub.model == "test-model" diff --git a/tests/components/broadlink/test_device.py b/tests/components/broadlink/test_device.py index a105ba26553..df22bcaffcb 100644 --- a/tests/components/broadlink/test_device.py +++ b/tests/components/broadlink/test_device.py @@ -253,9 +253,7 @@ async def test_device_setup_registry(hass): assert len(device_registry.devices) == 1 - device_entry = device_registry.async_get_device( - {(DOMAIN, mock_entry.unique_id)}, set() - ) + device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) assert device_entry.identifiers == {(DOMAIN, device.mac)} assert device_entry.name == device.name assert device_entry.model == device.model @@ -339,9 +337,7 @@ async def test_device_update_listener(hass): hass.config_entries.async_update_entry(mock_entry, title="New Name") await hass.async_block_till_done() - device_entry = device_registry.async_get_device( - {(DOMAIN, mock_entry.unique_id)}, set() - ) + device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) assert device_entry.name == "New Name" for entry in async_entries_for_device(entity_registry, device_entry.id): assert entry.original_name.startswith("New Name") diff --git a/tests/components/broadlink/test_remote.py b/tests/components/broadlink/test_remote.py index a1be8b364a3..2d21b588c33 100644 --- a/tests/components/broadlink/test_remote.py +++ b/tests/components/broadlink/test_remote.py @@ -31,7 +31,7 @@ async def test_remote_setup_works(hass): mock_api, mock_entry = await device.setup_entry(hass) device_entry = device_registry.async_get_device( - {(DOMAIN, mock_entry.unique_id)}, set() + {(DOMAIN, mock_entry.unique_id)} ) entries = async_entries_for_device(entity_registry, device_entry.id) remotes = {entry for entry in entries if entry.domain == REMOTE_DOMAIN} @@ -51,7 +51,7 @@ async def test_remote_send_command(hass): mock_api, mock_entry = await device.setup_entry(hass) device_entry = device_registry.async_get_device( - {(DOMAIN, mock_entry.unique_id)}, set() + {(DOMAIN, mock_entry.unique_id)} ) entries = async_entries_for_device(entity_registry, device_entry.id) remotes = {entry for entry in entries if entry.domain == REMOTE_DOMAIN} @@ -78,7 +78,7 @@ async def test_remote_turn_off_turn_on(hass): mock_api, mock_entry = await device.setup_entry(hass) device_entry = device_registry.async_get_device( - {(DOMAIN, mock_entry.unique_id)}, set() + {(DOMAIN, mock_entry.unique_id)} ) entries = async_entries_for_device(entity_registry, device_entry.id) remotes = {entry for entry in entries if entry.domain == REMOTE_DOMAIN} diff --git a/tests/components/broadlink/test_sensors.py b/tests/components/broadlink/test_sensors.py index a7d6a304654..de0cd88f288 100644 --- a/tests/components/broadlink/test_sensors.py +++ b/tests/components/broadlink/test_sensors.py @@ -25,9 +25,7 @@ async def test_a1_sensor_setup(hass): mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) assert mock_api.check_sensors_raw.call_count == 1 - device_entry = device_registry.async_get_device( - {(DOMAIN, mock_entry.unique_id)}, set() - ) + device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} assert len(sensors) == 5 @@ -62,9 +60,7 @@ async def test_a1_sensor_update(hass): mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) - device_entry = device_registry.async_get_device( - {(DOMAIN, mock_entry.unique_id)}, set() - ) + device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} assert len(sensors) == 5 @@ -106,9 +102,7 @@ async def test_rm_pro_sensor_setup(hass): mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) assert mock_api.check_sensors.call_count == 1 - device_entry = device_registry.async_get_device( - {(DOMAIN, mock_entry.unique_id)}, set() - ) + device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} assert len(sensors) == 1 @@ -131,9 +125,7 @@ async def test_rm_pro_sensor_update(hass): mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) - device_entry = device_registry.async_get_device( - {(DOMAIN, mock_entry.unique_id)}, set() - ) + device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} assert len(sensors) == 1 @@ -163,9 +155,7 @@ async def test_rm_mini3_no_sensor(hass): mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) assert mock_api.check_sensors.call_count <= 1 - device_entry = device_registry.async_get_device( - {(DOMAIN, mock_entry.unique_id)}, set() - ) + device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} assert len(sensors) == 0 @@ -183,9 +173,7 @@ async def test_rm4_pro_hts2_sensor_setup(hass): mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) assert mock_api.check_sensors.call_count == 1 - device_entry = device_registry.async_get_device( - {(DOMAIN, mock_entry.unique_id)}, set() - ) + device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} assert len(sensors) == 2 @@ -211,9 +199,7 @@ async def test_rm4_pro_hts2_sensor_update(hass): mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) - device_entry = device_registry.async_get_device( - {(DOMAIN, mock_entry.unique_id)}, set() - ) + device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} assert len(sensors) == 2 @@ -246,9 +232,7 @@ async def test_rm4_pro_no_sensor(hass): mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) assert mock_api.check_sensors.call_count <= 1 - device_entry = device_registry.async_get_device( - {(DOMAIN, mock_entry.unique_id)}, set() - ) + device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} assert len(sensors) == 0 diff --git a/tests/components/canary/test_sensor.py b/tests/components/canary/test_sensor.py index 3f44f047d1c..6419f81a62e 100644 --- a/tests/components/canary/test_sensor.py +++ b/tests/components/canary/test_sensor.py @@ -88,7 +88,7 @@ async def test_sensors_pro(hass, canary) -> None: assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == data[2] assert state.state == data[1] - device = device_registry.async_get_device({(DOMAIN, "20")}, set()) + device = device_registry.async_get_device({(DOMAIN, "20")}) assert device assert device.manufacturer == MANUFACTURER assert device.name == "Dining Room" @@ -206,7 +206,7 @@ async def test_sensors_flex(hass, canary) -> None: assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == data[2] assert state.state == data[1] - device = device_registry.async_get_device({(DOMAIN, "20")}, set()) + device = device_registry.async_get_device({(DOMAIN, "20")}) assert device assert device.manufacturer == MANUFACTURER assert device.name == "Dining Room" diff --git a/tests/components/gogogate2/test_cover.py b/tests/components/gogogate2/test_cover.py index 7a944d3f7f1..8f58224bb99 100644 --- a/tests/components/gogogate2/test_cover.py +++ b/tests/components/gogogate2/test_cover.py @@ -445,7 +445,7 @@ async def test_device_info_ismartgate(ismartgateapi_mock, hass: HomeAssistant) - assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - device = device_registry.async_get_device({(DOMAIN, "xyz")}, set()) + device = device_registry.async_get_device({(DOMAIN, "xyz")}) assert device assert device.manufacturer == MANUFACTURER assert device.name == "mycontroller" @@ -480,7 +480,7 @@ async def test_device_info_gogogate2(gogogate2api_mock, hass: HomeAssistant) -> assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - device = device_registry.async_get_device({(DOMAIN, "xyz")}, set()) + device = device_registry.async_get_device({(DOMAIN, "xyz")}) assert device assert device.manufacturer == MANUFACTURER assert device.name == "mycontroller" diff --git a/tests/components/heos/test_media_player.py b/tests/components/heos/test_media_player.py index eba0eb0f3fb..ef7285ab185 100644 --- a/tests/components/heos/test_media_player.py +++ b/tests/components/heos/test_media_player.py @@ -243,7 +243,7 @@ async def test_updates_from_players_changed_new_ids( event = asyncio.Event() # Assert device registry matches current id - assert device_registry.async_get_device({(DOMAIN, 1)}, []) + assert device_registry.async_get_device({(DOMAIN, 1)}) # Assert entity registry matches current id assert ( entity_registry.async_get_entity_id(MEDIA_PLAYER_DOMAIN, DOMAIN, "1") @@ -264,7 +264,7 @@ async def test_updates_from_players_changed_new_ids( # Assert device registry identifiers were updated assert len(device_registry.devices) == 1 - assert device_registry.async_get_device({(DOMAIN, 101)}, []) + assert device_registry.async_get_device({(DOMAIN, 101)}) # Assert entity registry unique id was updated assert len(entity_registry.entities) == 1 assert ( diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 0f9b458cdde..32d8a69417b 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -541,7 +541,7 @@ async def test_homekit_start(hass, hk_driver, device_reg, debounce_patcher): assert device_reg.async_get(bridge_with_wrong_mac.id) is None device = device_reg.async_get_device( - {(DOMAIN, entry.entry_id, BRIDGE_SERIAL_NUMBER)}, {} + {(DOMAIN, entry.entry_id, BRIDGE_SERIAL_NUMBER)} ) assert device formatted_mac = device_registry.format_mac(homekit.driver.state.mac) @@ -559,7 +559,7 @@ async def test_homekit_start(hass, hk_driver, device_reg, debounce_patcher): await homekit.async_start() device = device_reg.async_get_device( - {(DOMAIN, entry.entry_id, BRIDGE_SERIAL_NUMBER)}, {} + {(DOMAIN, entry.entry_id, BRIDGE_SERIAL_NUMBER)} ) assert device formatted_mac = device_registry.format_mac(homekit.driver.state.mac) diff --git a/tests/components/hue/test_device_trigger.py b/tests/components/hue/test_device_trigger.py index 0975c644e61..178cd7b09f1 100644 --- a/tests/components/hue/test_device_trigger.py +++ b/tests/components/hue/test_device_trigger.py @@ -43,7 +43,7 @@ async def test_get_triggers(hass, mock_bridge, device_reg): # Get triggers for specific tap switch hue_tap_device = device_reg.async_get_device( - {(hue.DOMAIN, "00:00:00:00:00:44:23:08")}, connections={} + {(hue.DOMAIN, "00:00:00:00:00:44:23:08")} ) triggers = await async_get_device_automations(hass, "trigger", hue_tap_device.id) @@ -61,7 +61,7 @@ async def test_get_triggers(hass, mock_bridge, device_reg): # Get triggers for specific dimmer switch hue_dimmer_device = device_reg.async_get_device( - {(hue.DOMAIN, "00:17:88:01:10:3e:3a:dc")}, connections={} + {(hue.DOMAIN, "00:17:88:01:10:3e:3a:dc")} ) triggers = await async_get_device_automations(hass, "trigger", hue_dimmer_device.id) @@ -97,7 +97,7 @@ async def test_if_fires_on_state_change(hass, mock_bridge, device_reg, calls): # Set an automation with a specific tap switch trigger hue_tap_device = device_reg.async_get_device( - {(hue.DOMAIN, "00:00:00:00:00:44:23:08")}, connections={} + {(hue.DOMAIN, "00:00:00:00:00:44:23:08")} ) assert await async_setup_component( hass, diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 18320a8c467..c5e271f1625 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -602,7 +602,7 @@ async def help_test_entity_device_info_with_identifier(hass, mqtt_mock, domain, async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None assert device.identifiers == {("mqtt", "helloworld")} assert device.manufacturer == "Whatever" @@ -650,14 +650,14 @@ async def help_test_entity_device_info_remove(hass, mqtt_mock, domain, config): async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() - device = dev_registry.async_get_device({("mqtt", "helloworld")}, set()) + device = dev_registry.async_get_device({("mqtt", "helloworld")}) assert device is not None assert ent_registry.async_get_entity_id(domain, mqtt.DOMAIN, "veryunique") async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", "") await hass.async_block_till_done() - device = dev_registry.async_get_device({("mqtt", "helloworld")}, set()) + device = dev_registry.async_get_device({("mqtt", "helloworld")}) assert device is None assert not ent_registry.async_get_entity_id(domain, mqtt.DOMAIN, "veryunique") @@ -678,7 +678,7 @@ async def help_test_entity_device_info_update(hass, mqtt_mock, domain, config): async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None assert device.name == "Beer" @@ -687,7 +687,7 @@ async def help_test_entity_device_info_update(hass, mqtt_mock, domain, config): async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None assert device.name == "Milk" @@ -790,7 +790,7 @@ async def help_test_entity_debug_info(hass, mqtt_mock, domain, config): async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None debug_info_data = await debug_info.info_for_device(hass, device.id) @@ -823,7 +823,7 @@ async def help_test_entity_debug_info_max_messages(hass, mqtt_mock, domain, conf async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None debug_info_data = await debug_info.info_for_device(hass, device.id) @@ -885,7 +885,7 @@ async def help_test_entity_debug_info_message( async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None debug_info_data = await debug_info.info_for_device(hass, device.id) @@ -931,7 +931,7 @@ async def help_test_entity_debug_info_remove(hass, mqtt_mock, domain, config): async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None debug_info_data = await debug_info.info_for_device(hass, device.id) @@ -975,7 +975,7 @@ async def help_test_entity_debug_info_update_entity_id(hass, mqtt_mock, domain, async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() - device = dev_registry.async_get_device({("mqtt", "helloworld")}, set()) + device = dev_registry.async_get_device({("mqtt", "helloworld")}) assert device is not None debug_info_data = await debug_info.info_for_device(hass, device.id) diff --git a/tests/components/mqtt/test_device_tracker_discovery.py b/tests/components/mqtt/test_device_tracker_discovery.py index 4ee6986e599..2c445ee0fa5 100644 --- a/tests/components/mqtt/test_device_tracker_discovery.py +++ b/tests/components/mqtt/test_device_tracker_discovery.py @@ -184,7 +184,7 @@ async def test_cleanup_device_tracker(hass, device_reg, entity_reg, mqtt_mock): await hass.async_block_till_done() # Verify device and registry entries are created - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) assert device_entry is not None entity_entry = entity_reg.async_get("device_tracker.mqtt_unique") assert entity_entry is not None @@ -196,7 +196,7 @@ async def test_cleanup_device_tracker(hass, device_reg, entity_reg, mqtt_mock): await hass.async_block_till_done() # Verify device and registry entries are cleared - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) assert device_entry is None entity_entry = entity_reg.async_get("device_tracker.mqtt_unique") assert entity_entry is None diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index a46406c330f..f200de6a274 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -50,7 +50,7 @@ async def test_get_triggers(hass, device_reg, entity_reg, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data1) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) expected_triggers = [ { "platform": "device", @@ -76,7 +76,7 @@ async def test_get_unknown_triggers(hass, device_reg, entity_reg, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data1) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) assert await async_setup_component( hass, @@ -116,7 +116,7 @@ async def test_get_non_existing_triggers(hass, device_reg, entity_reg, mqtt_mock async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data1) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) triggers = await async_get_device_automations(hass, "trigger", device_entry.id) assert_lists_same(triggers, []) @@ -135,7 +135,7 @@ async def test_discover_bad_triggers(hass, device_reg, entity_reg, mqtt_mock): ) async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data0) await hass.async_block_till_done() - assert device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) is None + assert device_reg.async_get_device({("mqtt", "0AFFD2")}) is None # Test sending correct data data1 = ( @@ -149,7 +149,7 @@ async def test_discover_bad_triggers(hass, device_reg, entity_reg, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data1) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) expected_triggers = [ { "platform": "device", @@ -185,7 +185,7 @@ async def test_update_remove_triggers(hass, device_reg, entity_reg, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data1) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) expected_triggers1 = [ { "platform": "device", @@ -213,7 +213,7 @@ async def test_update_remove_triggers(hass, device_reg, entity_reg, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", "") await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) assert device_entry is None @@ -238,7 +238,7 @@ async def test_if_fires_on_mqtt_message(hass, device_reg, calls, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) async_fire_mqtt_message(hass, "homeassistant/device_automation/bla2/config", data2) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) assert await async_setup_component( hass, @@ -317,7 +317,7 @@ async def test_if_fires_on_mqtt_message_late_discover( ) async_fire_mqtt_message(hass, "homeassistant/sensor/bla0/config", data0) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) assert await async_setup_component( hass, @@ -393,7 +393,7 @@ async def test_if_fires_on_mqtt_message_after_update( ) async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) assert await async_setup_component( hass, @@ -459,7 +459,7 @@ async def test_no_resubscribe_same_topic(hass, device_reg, mqtt_mock): ) async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) assert await async_setup_component( hass, @@ -503,7 +503,7 @@ async def test_not_fires_on_mqtt_message_after_remove_by_mqtt( ) async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) assert await async_setup_component( hass, @@ -563,7 +563,7 @@ async def test_not_fires_on_mqtt_message_after_remove_from_registry( ) async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) assert await async_setup_component( hass, @@ -614,7 +614,7 @@ async def test_attach_remove(hass, device_reg, mqtt_mock): ) async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) calls = [] @@ -668,7 +668,7 @@ async def test_attach_remove_late(hass, device_reg, mqtt_mock): ) async_fire_mqtt_message(hass, "homeassistant/sensor/bla0/config", data0) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) calls = [] @@ -725,7 +725,7 @@ async def test_attach_remove_late2(hass, device_reg, mqtt_mock): ) async_fire_mqtt_message(hass, "homeassistant/sensor/bla0/config", data0) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) calls = [] @@ -812,7 +812,7 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None assert device.identifiers == {("mqtt", "helloworld")} assert device.manufacturer == "Whatever" @@ -844,7 +844,7 @@ async def test_entity_device_info_update(hass, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None assert device.name == "Beer" @@ -853,7 +853,7 @@ async def test_entity_device_info_update(hass, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None assert device.name == "Milk" @@ -873,7 +873,7 @@ async def test_cleanup_trigger(hass, device_reg, entity_reg, mqtt_mock): await hass.async_block_till_done() # Verify device registry entry is created - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None triggers = await async_get_device_automations(hass, "trigger", device_entry.id) @@ -884,7 +884,7 @@ async def test_cleanup_trigger(hass, device_reg, entity_reg, mqtt_mock): await hass.async_block_till_done() # Verify device registry entry is cleared - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is None # Verify retained discovery topic has been cleared @@ -908,7 +908,7 @@ async def test_cleanup_device(hass, device_reg, entity_reg, mqtt_mock): await hass.async_block_till_done() # Verify device registry entry is created - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None triggers = await async_get_device_automations(hass, "trigger", device_entry.id) @@ -918,7 +918,7 @@ async def test_cleanup_device(hass, device_reg, entity_reg, mqtt_mock): await hass.async_block_till_done() # Verify device registry entry is cleared - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is None @@ -948,7 +948,7 @@ async def test_cleanup_device_several_triggers(hass, device_reg, entity_reg, mqt await hass.async_block_till_done() # Verify device registry entry is created - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None triggers = await async_get_device_automations(hass, "trigger", device_entry.id) @@ -960,7 +960,7 @@ async def test_cleanup_device_several_triggers(hass, device_reg, entity_reg, mqt await hass.async_block_till_done() # Verify device registry entry is not cleared - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None triggers = await async_get_device_automations(hass, "trigger", device_entry.id) @@ -971,7 +971,7 @@ async def test_cleanup_device_several_triggers(hass, device_reg, entity_reg, mqt await hass.async_block_till_done() # Verify device registry entry is cleared - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is None @@ -1003,7 +1003,7 @@ async def test_cleanup_device_with_entity1(hass, device_reg, entity_reg, mqtt_mo await hass.async_block_till_done() # Verify device registry entry is created - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None triggers = await async_get_device_automations(hass, "trigger", device_entry.id) @@ -1013,7 +1013,7 @@ async def test_cleanup_device_with_entity1(hass, device_reg, entity_reg, mqtt_mo await hass.async_block_till_done() # Verify device registry entry is not cleared - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None triggers = await async_get_device_automations(hass, "trigger", device_entry.id) @@ -1023,7 +1023,7 @@ async def test_cleanup_device_with_entity1(hass, device_reg, entity_reg, mqtt_mo await hass.async_block_till_done() # Verify device registry entry is cleared - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is None @@ -1055,7 +1055,7 @@ async def test_cleanup_device_with_entity2(hass, device_reg, entity_reg, mqtt_mo await hass.async_block_till_done() # Verify device registry entry is created - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None triggers = await async_get_device_automations(hass, "trigger", device_entry.id) @@ -1065,7 +1065,7 @@ async def test_cleanup_device_with_entity2(hass, device_reg, entity_reg, mqtt_mo await hass.async_block_till_done() # Verify device registry entry is not cleared - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None triggers = await async_get_device_automations(hass, "trigger", device_entry.id) @@ -1075,7 +1075,7 @@ async def test_cleanup_device_with_entity2(hass, device_reg, entity_reg, mqtt_mo await hass.async_block_till_done() # Verify device registry entry is cleared - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is None diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 04d35ab1d26..124f40c31fa 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -398,7 +398,7 @@ async def test_cleanup_device(hass, device_reg, entity_reg, mqtt_mock): await hass.async_block_till_done() # Verify device and registry entries are created - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) assert device_entry is not None entity_entry = entity_reg.async_get("sensor.mqtt_sensor") assert entity_entry is not None @@ -410,7 +410,7 @@ async def test_cleanup_device(hass, device_reg, entity_reg, mqtt_mock): await hass.async_block_till_done() # Verify device and registry entries are cleared - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) assert device_entry is None entity_entry = entity_reg.async_get("sensor.mqtt_sensor") assert entity_entry is None diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index bb698a24d7e..7288c3e4304 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -970,7 +970,7 @@ async def test_mqtt_ws_remove_discovered_device( await hass.async_block_till_done() # Verify device entry is created - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) assert device_entry is not None client = await hass_ws_client(hass) @@ -981,7 +981,7 @@ async def test_mqtt_ws_remove_discovered_device( assert response["success"] # Verify device entry is cleared - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) assert device_entry is None @@ -998,7 +998,7 @@ async def test_mqtt_ws_remove_discovered_device_twice( async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) assert device_entry is not None client = await hass_ws_client(hass) @@ -1030,7 +1030,7 @@ async def test_mqtt_ws_remove_discovered_device_same_topic( async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) assert device_entry is not None client = await hass_ws_client(hass) @@ -1086,7 +1086,7 @@ async def test_mqtt_ws_get_device_debug_info( await hass.async_block_till_done() # Verify device entry is created - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) assert device_entry is not None client = await hass_ws_client(hass) @@ -1168,7 +1168,7 @@ async def test_debug_info_multiple_devices(hass, mqtt_mock): for d in devices: domain = d["domain"] id = d["config"]["device"]["identifiers"][0] - device = registry.async_get_device({("mqtt", id)}, set()) + device = registry.async_get_device({("mqtt", id)}) assert device is not None debug_info_data = await debug_info.info_for_device(hass, device.id) @@ -1246,7 +1246,7 @@ async def test_debug_info_multiple_entities_triggers(hass, mqtt_mock): await hass.async_block_till_done() device_id = config[0]["config"]["device"]["identifiers"][0] - device = registry.async_get_device({("mqtt", device_id)}, set()) + device = registry.async_get_device({("mqtt", device_id)}) assert device is not None debug_info_data = await debug_info.info_for_device(hass, device.id) assert len(debug_info_data["entities"]) == 2 @@ -1316,7 +1316,7 @@ async def test_debug_info_wildcard(hass, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None debug_info_data = await debug_info.info_for_device(hass, device.id) @@ -1362,7 +1362,7 @@ async def test_debug_info_filter_same(hass, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None debug_info_data = await debug_info.info_for_device(hass, device.id) @@ -1421,7 +1421,7 @@ async def test_debug_info_same_topic(hass, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None debug_info_data = await debug_info.info_for_device(hass, device.id) @@ -1472,7 +1472,7 @@ async def test_debug_info_qos_retain(hass, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None debug_info_data = await debug_info.info_for_device(hass, device.id) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 7449d127dc1..d818338a0ca 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -579,7 +579,7 @@ async def test_entity_device_info_with_hub(hass, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None assert device.via_device_id == hub.id diff --git a/tests/components/mqtt/test_tag.py b/tests/components/mqtt/test_tag.py index 8ade3146455..67964a36e1a 100644 --- a/tests/components/mqtt/test_tag.py +++ b/tests/components/mqtt/test_tag.py @@ -64,13 +64,13 @@ async def test_discover_bad_tag(hass, device_reg, entity_reg, mqtt_mock, tag_moc data0 = '{ "device":{"identifiers":["0AFFD2"]}, "topics": "foobar/tag_scanned" }' async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", data0) await hass.async_block_till_done() - assert device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) is None + assert device_reg.async_get_device({("mqtt", "0AFFD2")}) is None # Test sending correct data async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", json.dumps(config1)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) # Fake tag scan. async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN) await hass.async_block_till_done() @@ -85,7 +85,7 @@ async def test_if_fires_on_mqtt_message_with_device( async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) # Fake tag scan. async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN) @@ -116,7 +116,7 @@ async def test_if_fires_on_mqtt_message_with_template( async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) # Fake tag scan. async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN_JSON) @@ -147,7 +147,7 @@ async def test_if_fires_on_mqtt_message_after_update_with_device( async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config1)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) # Fake tag scan. async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN) @@ -235,7 +235,7 @@ async def test_if_fires_on_mqtt_message_after_update_with_template( async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config1)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) # Fake tag scan. async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN_JSON) @@ -275,7 +275,7 @@ async def test_no_resubscribe_same_topic(hass, device_reg, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) await hass.async_block_till_done() - assert device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + assert device_reg.async_get_device({("mqtt", "0AFFD2")}) call_count = mqtt_mock.async_subscribe.call_count async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) @@ -291,7 +291,7 @@ async def test_not_fires_on_mqtt_message_after_remove_by_mqtt_with_device( async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) # Fake tag scan. async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN) @@ -359,7 +359,7 @@ async def test_not_fires_on_mqtt_message_after_remove_from_registry( async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) # Fake tag scan. async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN) @@ -423,7 +423,7 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None assert device.identifiers == {("mqtt", "helloworld")} assert device.manufacturer == "Whatever" @@ -452,7 +452,7 @@ async def test_entity_device_info_update(hass, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None assert device.name == "Beer" @@ -461,7 +461,7 @@ async def test_entity_device_info_update(hass, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None assert device.name == "Milk" @@ -478,7 +478,7 @@ async def test_cleanup_tag(hass, device_reg, entity_reg, mqtt_mock): await hass.async_block_till_done() # Verify device registry entry is created - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None device_reg.async_remove_device(device_entry.id) @@ -486,7 +486,7 @@ async def test_cleanup_tag(hass, device_reg, entity_reg, mqtt_mock): await hass.async_block_till_done() # Verify device registry entry is cleared - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is None # Verify retained discovery topic has been cleared @@ -507,14 +507,14 @@ async def test_cleanup_device(hass, device_reg, entity_reg, mqtt_mock): await hass.async_block_till_done() # Verify device registry entry is created - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", "") await hass.async_block_till_done() # Verify device registry entry is cleared - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is None @@ -538,14 +538,14 @@ async def test_cleanup_device_several_tags( await hass.async_block_till_done() # Verify device registry entry is created - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", "") await hass.async_block_till_done() # Verify device registry entry is not cleared - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None # Fake tag scan. @@ -558,7 +558,7 @@ async def test_cleanup_device_several_tags( await hass.async_block_till_done() # Verify device registry entry is cleared - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is None @@ -600,7 +600,7 @@ async def test_cleanup_device_with_entity_and_trigger_1( await hass.async_block_till_done() # Verify device registry entry is created - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None triggers = await async_get_device_automations(hass, "trigger", device_entry.id) @@ -610,7 +610,7 @@ async def test_cleanup_device_with_entity_and_trigger_1( await hass.async_block_till_done() # Verify device registry entry is not cleared - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None async_fire_mqtt_message(hass, "homeassistant/device_automation/bla2/config", "") @@ -620,7 +620,7 @@ async def test_cleanup_device_with_entity_and_trigger_1( await hass.async_block_till_done() # Verify device registry entry is cleared - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is None @@ -660,7 +660,7 @@ async def test_cleanup_device_with_entity2(hass, device_reg, entity_reg, mqtt_mo await hass.async_block_till_done() # Verify device registry entry is created - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None triggers = await async_get_device_automations(hass, "trigger", device_entry.id) @@ -673,12 +673,12 @@ async def test_cleanup_device_with_entity2(hass, device_reg, entity_reg, mqtt_mo await hass.async_block_till_done() # Verify device registry entry is not cleared - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", "") await hass.async_block_till_done() # Verify device registry entry is cleared - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is None diff --git a/tests/components/nest/test_device_trigger.py b/tests/components/nest/test_device_trigger.py index b7c75862153..5e3b9bde442 100644 --- a/tests/components/nest/test_device_trigger.py +++ b/tests/components/nest/test_device_trigger.py @@ -102,9 +102,7 @@ async def test_get_triggers(hass): 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={} - ) + device_entry = device_registry.async_get_device({("nest", DEVICE_ID)}) expected_triggers = [ { @@ -179,9 +177,7 @@ async def test_triggers_for_invalid_device_id(hass): 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={} - ) + device_entry = device_registry.async_get_device({("nest", DEVICE_ID)}) assert device_entry is not None # Create an additional device that does not exist. Fetching supported @@ -293,9 +289,7 @@ async def test_subscriber_automation(hass, calls): 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={} - ) + device_entry = device_registry.async_get_device({("nest", DEVICE_ID)}) assert await setup_automation(hass, device_entry.id, "camera_motion") diff --git a/tests/components/onewire/test_entity_owserver.py b/tests/components/onewire/test_entity_owserver.py index 76ab50419d2..854ceded284 100644 --- a/tests/components/onewire/test_entity_owserver.py +++ b/tests/components/onewire/test_entity_owserver.py @@ -745,7 +745,7 @@ async def test_owserver_setup_valid_device(owproxy, hass, device_id, platform): if len(expected_sensors) > 0: device_info = mock_device_sensor["device_info"] assert len(device_registry.devices) == 1 - registry_entry = device_registry.async_get_device({(DOMAIN, device_id)}, set()) + registry_entry = device_registry.async_get_device({(DOMAIN, device_id)}) assert registry_entry is not None assert registry_entry.identifiers == {(DOMAIN, device_id)} assert registry_entry.manufacturer == device_info["manufacturer"] diff --git a/tests/components/onewire/test_entity_sysbus.py b/tests/components/onewire/test_entity_sysbus.py index 437fb1bb7d6..61a38c10f73 100644 --- a/tests/components/onewire/test_entity_sysbus.py +++ b/tests/components/onewire/test_entity_sysbus.py @@ -157,7 +157,7 @@ async def test_onewiredirect_setup_valid_device(hass, device_id): if len(expected_sensors) > 0: device_info = mock_device_sensor["device_info"] assert len(device_registry.devices) == 1 - registry_entry = device_registry.async_get_device({(DOMAIN, device_id)}, set()) + registry_entry = device_registry.async_get_device({(DOMAIN, device_id)}) assert registry_entry is not None assert registry_entry.identifiers == {(DOMAIN, device_id)} assert registry_entry.manufacturer == device_info["manufacturer"] diff --git a/tests/components/opentherm_gw/test_init.py b/tests/components/opentherm_gw/test_init.py index 51d75bf0923..b28f869e1e6 100644 --- a/tests/components/opentherm_gw/test_init.py +++ b/tests/components/opentherm_gw/test_init.py @@ -40,9 +40,7 @@ async def test_device_registry_insert(hass): device_registry = await hass.helpers.device_registry.async_get_registry() - gw_dev = device_registry.async_get_device( - identifiers={(DOMAIN, MOCK_GATEWAY_ID)}, connections=set() - ) + gw_dev = device_registry.async_get_device(identifiers={(DOMAIN, MOCK_GATEWAY_ID)}) assert gw_dev.sw_version == VERSION_OLD @@ -67,7 +65,5 @@ async def test_device_registry_update(hass): await setup.async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() - gw_dev = dev_reg.async_get_device( - identifiers={(DOMAIN, MOCK_GATEWAY_ID)}, connections=set() - ) + gw_dev = dev_reg.async_get_device(identifiers={(DOMAIN, MOCK_GATEWAY_ID)}) assert gw_dev.sw_version == VERSION_NEW diff --git a/tests/components/powerwall/test_sensor.py b/tests/components/powerwall/test_sensor.py index 104ed0cbbf3..4c85661c157 100644 --- a/tests/components/powerwall/test_sensor.py +++ b/tests/components/powerwall/test_sensor.py @@ -26,7 +26,6 @@ async def test_sensors(hass): device_registry = await hass.helpers.device_registry.async_get_registry() reg_device = device_registry.async_get_device( identifiers={("powerwall", "TG0123456789AB_TG9876543210BA")}, - connections=set(), ) assert reg_device.model == "PowerWall 2 (GW1)" assert reg_device.sw_version == "1.45.1" diff --git a/tests/components/ps4/test_media_player.py b/tests/components/ps4/test_media_player.py index e65813bcbbd..d402cbb01ae 100644 --- a/tests/components/ps4/test_media_player.py +++ b/tests/components/ps4/test_media_player.py @@ -304,9 +304,7 @@ async def test_device_info_is_set_from_status_correctly(hass, patch_get_status): mock_state = hass.states.get(mock_entity_id).state mock_d_entries = mock_d_registry.devices - mock_entry = mock_d_registry.async_get_device( - identifiers={(DOMAIN, MOCK_HOST_ID)}, connections=set() - ) + mock_entry = mock_d_registry.async_get_device(identifiers={(DOMAIN, MOCK_HOST_ID)}) assert mock_state == STATE_STANDBY assert len(mock_d_entries) == 1 diff --git a/tests/components/rfxtrx/test_init.py b/tests/components/rfxtrx/test_init.py index 1480aa300d0..9112082a956 100644 --- a/tests/components/rfxtrx/test_init.py +++ b/tests/components/rfxtrx/test_init.py @@ -95,12 +95,12 @@ async def test_fire_event(hass, rfxtrx): await rfxtrx.signal("0716000100900970") device_id_1 = device_registry.async_get_device( - identifiers={("rfxtrx", "11", "0", "213c7f2:16")}, connections=set() + identifiers={("rfxtrx", "11", "0", "213c7f2:16")} ) assert device_id_1 device_id_2 = device_registry.async_get_device( - identifiers={("rfxtrx", "16", "0", "00:90")}, connections=set() + identifiers={("rfxtrx", "16", "0", "00:90")} ) assert device_id_2 diff --git a/tests/components/risco/test_alarm_control_panel.py b/tests/components/risco/test_alarm_control_panel.py index 366fc7814c4..a5a16379fe9 100644 --- a/tests/components/risco/test_alarm_control_panel.py +++ b/tests/components/risco/test_alarm_control_panel.py @@ -148,11 +148,11 @@ async def test_setup(hass, two_part_alarm): assert registry.async_is_registered(SECOND_ENTITY_ID) registry = await hass.helpers.device_registry.async_get_registry() - device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_0")}, {}) + device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_0")}) assert device is not None assert device.manufacturer == "Risco" - device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_1")}, {}) + device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_1")}) assert device is not None assert device.manufacturer == "Risco" diff --git a/tests/components/risco/test_binary_sensor.py b/tests/components/risco/test_binary_sensor.py index 2f750ff6d35..7533512e3ef 100644 --- a/tests/components/risco/test_binary_sensor.py +++ b/tests/components/risco/test_binary_sensor.py @@ -60,11 +60,11 @@ async def test_setup(hass, two_zone_alarm): # noqa: F811 assert registry.async_is_registered(SECOND_ENTITY_ID) registry = await hass.helpers.device_registry.async_get_registry() - device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_zone_0")}, {}) + device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_zone_0")}) assert device is not None assert device.manufacturer == "Risco" - device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_zone_1")}, {}) + device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_zone_1")}) assert device is not None assert device.manufacturer == "Risco" diff --git a/tests/components/sharkiq/test_vacuum.py b/tests/components/sharkiq/test_vacuum.py index a894869a723..edce5d75b2c 100644 --- a/tests/components/sharkiq/test_vacuum.py +++ b/tests/components/sharkiq/test_vacuum.py @@ -200,7 +200,7 @@ async def test_device_properties( ): """Test device properties.""" registry = await hass.helpers.device_registry.async_get_registry() - device = registry.async_get_device({(DOMAIN, "AC000Wxxxxxxxxx")}, []) + device = registry.async_get_device({(DOMAIN, "AC000Wxxxxxxxxx")}) assert getattr(device, device_property) == target_value diff --git a/tests/components/smartthings/test_binary_sensor.py b/tests/components/smartthings/test_binary_sensor.py index b007fff7caf..6931b3dfbb5 100644 --- a/tests/components/smartthings/test_binary_sensor.py +++ b/tests/components/smartthings/test_binary_sensor.py @@ -57,7 +57,7 @@ async def test_entity_and_device_attributes(hass, device_factory): entry = entity_registry.async_get("binary_sensor.motion_sensor_1_motion") assert entry assert entry.unique_id == f"{device.device_id}.{Attribute.motion}" - entry = device_registry.async_get_device({(DOMAIN, device.device_id)}, []) + entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) assert entry assert entry.name == device.label assert entry.model == device.device_type_name diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index 4229bd7cf94..11f7695a775 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -576,7 +576,7 @@ async def test_entity_and_device_attributes(hass, thermostat): assert entry assert entry.unique_id == thermostat.device_id - entry = device_registry.async_get_device({(DOMAIN, thermostat.device_id)}, []) + entry = device_registry.async_get_device({(DOMAIN, thermostat.device_id)}) assert entry assert entry.name == thermostat.label assert entry.model == thermostat.device_type_name diff --git a/tests/components/smartthings/test_cover.py b/tests/components/smartthings/test_cover.py index 9c5a80e27fb..0483480cb8a 100644 --- a/tests/components/smartthings/test_cover.py +++ b/tests/components/smartthings/test_cover.py @@ -40,7 +40,7 @@ async def test_entity_and_device_attributes(hass, device_factory): assert entry assert entry.unique_id == device.device_id - entry = device_registry.async_get_device({(DOMAIN, device.device_id)}, []) + entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) assert entry assert entry.name == device.label assert entry.model == device.device_type_name diff --git a/tests/components/smartthings/test_fan.py b/tests/components/smartthings/test_fan.py index 6b8eb56d65c..0ebef7e7323 100644 --- a/tests/components/smartthings/test_fan.py +++ b/tests/components/smartthings/test_fan.py @@ -62,7 +62,7 @@ async def test_entity_and_device_attributes(hass, device_factory): assert entry assert entry.unique_id == device.device_id - entry = device_registry.async_get_device({(DOMAIN, device.device_id)}, []) + entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) assert entry assert entry.name == device.label assert entry.model == device.device_type_name diff --git a/tests/components/smartthings/test_light.py b/tests/components/smartthings/test_light.py index 43a73113fec..bd9557c6b97 100644 --- a/tests/components/smartthings/test_light.py +++ b/tests/components/smartthings/test_light.py @@ -115,7 +115,7 @@ async def test_entity_and_device_attributes(hass, device_factory): assert entry assert entry.unique_id == device.device_id - entry = device_registry.async_get_device({(DOMAIN, device.device_id)}, []) + entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) assert entry assert entry.name == device.label assert entry.model == device.device_type_name diff --git a/tests/components/smartthings/test_lock.py b/tests/components/smartthings/test_lock.py index 65219852392..0492f2281ce 100644 --- a/tests/components/smartthings/test_lock.py +++ b/tests/components/smartthings/test_lock.py @@ -27,7 +27,7 @@ async def test_entity_and_device_attributes(hass, device_factory): assert entry assert entry.unique_id == device.device_id - entry = device_registry.async_get_device({(DOMAIN, device.device_id)}, []) + entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) assert entry assert entry.name == device.label assert entry.model == device.device_type_name diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py index b9669d0c8ed..3faf0f621a3 100644 --- a/tests/components/smartthings/test_sensor.py +++ b/tests/components/smartthings/test_sensor.py @@ -85,7 +85,7 @@ async def test_entity_and_device_attributes(hass, device_factory): entry = entity_registry.async_get("sensor.sensor_1_battery") assert entry assert entry.unique_id == f"{device.device_id}.{Attribute.battery}" - entry = device_registry.async_get_device({(DOMAIN, device.device_id)}, []) + entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) assert entry assert entry.name == device.label assert entry.model == device.device_type_name diff --git a/tests/components/smartthings/test_switch.py b/tests/components/smartthings/test_switch.py index 0b47739caf5..3ac86426eeb 100644 --- a/tests/components/smartthings/test_switch.py +++ b/tests/components/smartthings/test_switch.py @@ -30,7 +30,7 @@ async def test_entity_and_device_attributes(hass, device_factory): assert entry assert entry.unique_id == device.device_id - entry = device_registry.async_get_device({(DOMAIN, device.device_id)}, []) + entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) assert entry assert entry.name == device.label assert entry.model == device.device_type_name diff --git a/tests/components/songpal/test_media_player.py b/tests/components/songpal/test_media_player.py index b43370b8200..aff79ef62ff 100644 --- a/tests/components/songpal/test_media_player.py +++ b/tests/components/songpal/test_media_player.py @@ -114,9 +114,7 @@ async def test_state(hass): assert attributes["supported_features"] == SUPPORT_SONGPAL device_registry = await dr.async_get_registry(hass) - device = device_registry.async_get_device( - identifiers={(songpal.DOMAIN, MAC)}, connections={} - ) + device = device_registry.async_get_device(identifiers={(songpal.DOMAIN, MAC)}) assert device.connections == {(dr.CONNECTION_NETWORK_MAC, MAC)} assert device.manufacturer == "Sony Corporation" assert device.name == FRIENDLY_NAME diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index dd83aefba81..6a401ee0c16 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -50,8 +50,7 @@ async def test_device_registry(hass, config_entry, config, soco): device_registry = await hass.helpers.device_registry.async_get_registry() reg_device = device_registry.async_get_device( - identifiers={("sonos", "RINCON_test")}, - connections=set(), + identifiers={("sonos", "RINCON_test")} ) assert reg_device.model == "Model Name" assert reg_device.sw_version == "49.2-64250" diff --git a/tests/components/twinkly/test_twinkly.py b/tests/components/twinkly/test_twinkly.py index 7b7b44ee516..c8158354195 100644 --- a/tests/components/twinkly/test_twinkly.py +++ b/tests/components/twinkly/test_twinkly.py @@ -216,7 +216,7 @@ async def _create_entries( 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()) + device = device_registry.async_get_device({(TWINKLY_DOMAIN, client.id)}) assert entity is not None assert device is not None diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index c0350ce63a5..316d475f17f 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -50,7 +50,7 @@ async def test_get_actions(hass, device_ias): ieee_address = str(device_ias[0].ieee) ha_device_registry = await async_get_registry(hass) - reg_device = ha_device_registry.async_get_device({(DOMAIN, ieee_address)}, set()) + reg_device = ha_device_registry.async_get_device({(DOMAIN, ieee_address)}) actions = await async_get_device_automations(hass, "action", reg_device.id) @@ -73,7 +73,7 @@ async def test_action(hass, device_ias): ieee_address = str(zha_device.ieee) ha_device_registry = await async_get_registry(hass) - reg_device = ha_device_registry.async_get_device({(DOMAIN, ieee_address)}, set()) + reg_device = ha_device_registry.async_get_device({(DOMAIN, ieee_address)}) with patch( "zigpy.zcl.Cluster.request", diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py index b72f693e531..96ee5520e2a 100644 --- a/tests/components/zha/test_device_trigger.py +++ b/tests/components/zha/test_device_trigger.py @@ -87,7 +87,7 @@ async def test_triggers(hass, mock_devices): ieee_address = str(zha_device.ieee) ha_device_registry = await async_get_registry(hass) - reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) + reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}) triggers = await async_get_device_automations(hass, "trigger", reg_device.id) @@ -145,7 +145,7 @@ async def test_no_triggers(hass, mock_devices): ieee_address = str(zha_device.ieee) ha_device_registry = await async_get_registry(hass) - reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) + reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}) triggers = await async_get_device_automations(hass, "trigger", reg_device.id) assert triggers == [ @@ -174,7 +174,7 @@ async def test_if_fires_on_event(hass, mock_devices, calls): ieee_address = str(zha_device.ieee) ha_device_registry = await async_get_registry(hass) - reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) + reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}) assert await async_setup_component( hass, @@ -283,7 +283,7 @@ async def test_exception_no_triggers(hass, mock_devices, calls, caplog): ieee_address = str(zha_device.ieee) ha_device_registry = await async_get_registry(hass) - reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) + reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}) await async_setup_component( hass, @@ -325,7 +325,7 @@ async def test_exception_bad_trigger(hass, mock_devices, calls, caplog): ieee_address = str(zha_device.ieee) ha_device_registry = await async_get_registry(hass) - reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) + reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}) await async_setup_component( hass, diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 7f15239556d..44459ec45b0 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -229,8 +229,8 @@ async def test_removing_config_entries(hass, registry, update_events): assert entry2.config_entries == {"123", "456"} registry.async_clear_config_entry("123") - entry = registry.async_get_device({("bridgeid", "0123")}, set()) - entry3_removed = registry.async_get_device({("bridgeid", "4567")}, set()) + entry = registry.async_get_device({("bridgeid", "0123")}) + entry3_removed = registry.async_get_device({("bridgeid", "4567")}) assert entry.config_entries == {"456"} assert entry3_removed is None @@ -336,7 +336,7 @@ async def test_removing_area_id(registry): entry_w_area = registry.async_update_device(entry.id, area_id="12345A") registry.async_clear_area_id("12345A") - entry_wo_area = registry.async_get_device({("bridgeid", "0123")}, set()) + entry_wo_area = registry.async_get_device({("bridgeid", "0123")}) assert not entry_wo_area.area_id assert entry_w_area != entry_wo_area @@ -366,7 +366,7 @@ async def test_deleted_device_removing_area_id(registry): ) assert entry.id == entry2.id - entry_wo_area = registry.async_get_device({("bridgeid", "0123")}, set()) + entry_wo_area = registry.async_get_device({("bridgeid", "0123")}) assert not entry_wo_area.area_id assert entry_w_area != entry_wo_area @@ -505,9 +505,9 @@ async def test_loading_saving_data(hass, registry): assert list(registry.devices) == list(registry2.devices) assert list(registry.deleted_devices) == list(registry2.deleted_devices) - new_via = registry2.async_get_device({("hue", "0123")}, set()) - new_light = registry2.async_get_device({("hue", "456")}, set()) - new_light4 = registry2.async_get_device({("hue", "abc")}, set()) + new_via = registry2.async_get_device({("hue", "0123")}) + new_light = registry2.async_get_device({("hue", "456")}) + new_light4 = registry2.async_get_device({("hue", "abc")}) assert orig_via == new_via assert orig_light == new_light @@ -597,11 +597,11 @@ async def test_update(registry): 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 + assert registry.async_get_device({("hue", "456")}) is None + assert registry.async_get_device({("bla", "123")}) is None - assert registry.async_get_device({("hue", "654")}, {}) == updated_entry - assert registry.async_get_device({("bla", "321")}, {}) == updated_entry + assert registry.async_get_device({("hue", "654")}) == updated_entry + assert registry.async_get_device({("bla", "321")}) == updated_entry assert ( registry.async_get_device( @@ -652,7 +652,7 @@ async def test_update_remove_config_entries(hass, registry, update_events): assert updated_entry.config_entries == {"456"} assert removed_entry is None - removed_entry = registry.async_get_device({("bridgeid", "4567")}, set()) + removed_entry = registry.async_get_device({("bridgeid", "4567")}) assert removed_entry is None @@ -728,10 +728,10 @@ async def test_cleanup_device_registry(hass, registry): device_registry.async_cleanup(hass, registry, ent_reg) - assert registry.async_get_device({("hue", "d1")}, set()) is not None - assert registry.async_get_device({("hue", "d2")}, set()) is not None - assert registry.async_get_device({("hue", "d3")}, set()) is not None - assert registry.async_get_device({("something", "d4")}, set()) is None + assert registry.async_get_device({("hue", "d1")}) is not None + assert registry.async_get_device({("hue", "d2")}) is not None + assert registry.async_get_device({("hue", "d3")}) is not None + assert registry.async_get_device({("something", "d4")}) is None async def test_cleanup_startup(hass): diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 25d105ab549..0a939ba2825 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -747,7 +747,7 @@ async def test_device_info_called(hass): assert len(hass.states.async_entity_ids()) == 2 - device = registry.async_get_device({("hue", "1234")}, set()) + device = registry.async_get_device({("hue", "1234")}) assert device is not None assert device.identifiers == {("hue", "1234")} assert device.connections == {("mac", "abcd")} From caf14b78d14f07fafd0a8e786e7804dc3ca7c288 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 7 Jan 2021 19:44:34 +0100 Subject: [PATCH 123/507] Homekit has two types (#44879) --- homeassistant/components/zeroconf/__init__.py | 17 +++++-- tests/components/zeroconf/test_init.py | 46 ++++++++++++++----- 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index fdf4b98faf8..2ef7db3a1b4 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -45,7 +45,11 @@ ATTR_TYPE = "type" ATTR_PROPERTIES = "properties" ZEROCONF_TYPE = "_home-assistant._tcp.local." -HOMEKIT_TYPE = "_hap._tcp.local." +HOMEKIT_TYPES = [ + "_hap._tcp.local.", + # Thread based devices + "_hap._udp.local.", +] CONF_DEFAULT_INTERFACE = "default_interface" CONF_IPV6 = "ipv6" @@ -229,8 +233,9 @@ async def _async_start_zeroconf_browser(hass, zeroconf): types = list(zeroconf_types) - if HOMEKIT_TYPE not in zeroconf_types: - types.append(HOMEKIT_TYPE) + for hk_type in HOMEKIT_TYPES: + if hk_type not in zeroconf_types: + types.append(hk_type) def service_update(zeroconf, service_type, name, state_change): """Service state changed.""" @@ -261,7 +266,7 @@ async def _async_start_zeroconf_browser(hass, zeroconf): _LOGGER.debug("Discovered new device %s %s", name, info) # If we can handle it as a HomeKit discovery, we do that here. - if service_type == HOMEKIT_TYPE: + if service_type in HOMEKIT_TYPES: discovery_was_forwarded = handle_homekit(hass, homekit_models, info) # Continue on here as homekit_controller # still needs to get updates on devices @@ -294,7 +299,9 @@ async def _async_start_zeroconf_browser(hass, zeroconf): else: uppercase_mac = None - for entry in zeroconf_types[service_type]: + # Not all homekit types are currently used for discovery + # so not all service type exist in zeroconf_types + for entry in zeroconf_types.get(service_type, []): if len(entry) > 1: if ( uppercase_mac is not None diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 51bdf269bf2..cc34a511573 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -31,9 +31,11 @@ HOMEKIT_STATUS_UNPAIRED = b"1" HOMEKIT_STATUS_PAIRED = b"0" -def service_update_mock(zeroconf, services, handlers): +def service_update_mock(zeroconf, services, handlers, *, limit_service=None): """Call service update handler.""" for service in services: + if limit_service is not None and service != limit_service: + continue handlers[0](zeroconf, service, f"name.{service}", ServiceStateChange.Added) @@ -307,12 +309,16 @@ async def test_homekit_match_partial_space(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" with patch.dict( zc_gen.ZEROCONF, - {zeroconf.HOMEKIT_TYPE: [{"domain": "homekit_controller"}]}, + {"_hap._tcp.local.": [{"domain": "homekit_controller"}]}, clear=True, ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( - zeroconf, "HaServiceBrowser", side_effect=service_update_mock + zeroconf, + "HaServiceBrowser", + side_effect=lambda *args, **kwargs: service_update_mock( + *args, **kwargs, limit_service="_hap._tcp.local." + ), ) as mock_service_browser: mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( "LIFX bulb", HOMEKIT_STATUS_UNPAIRED @@ -330,12 +336,16 @@ async def test_homekit_match_partial_dash(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" with patch.dict( zc_gen.ZEROCONF, - {zeroconf.HOMEKIT_TYPE: [{"domain": "homekit_controller"}]}, + {"_hap._udp.local.": [{"domain": "homekit_controller"}]}, clear=True, ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( - zeroconf, "HaServiceBrowser", side_effect=service_update_mock + zeroconf, + "HaServiceBrowser", + side_effect=lambda *args, **kwargs: service_update_mock( + *args, **kwargs, limit_service="_hap._udp.local." + ), ) as mock_service_browser: mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( "Rachio-fa46ba", HOMEKIT_STATUS_UNPAIRED @@ -353,12 +363,16 @@ async def test_homekit_match_full(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" with patch.dict( zc_gen.ZEROCONF, - {zeroconf.HOMEKIT_TYPE: [{"domain": "homekit_controller"}]}, + {"_hap._udp.local.": [{"domain": "homekit_controller"}]}, clear=True, ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( - zeroconf, "HaServiceBrowser", side_effect=service_update_mock + zeroconf, + "HaServiceBrowser", + side_effect=lambda *args, **kwargs: service_update_mock( + *args, **kwargs, limit_service="_hap._udp.local." + ), ) as mock_service_browser: mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( "BSB002", HOMEKIT_STATUS_UNPAIRED @@ -376,12 +390,16 @@ async def test_homekit_already_paired(hass, mock_zeroconf): """Test that an already paired device is sent to homekit_controller.""" with patch.dict( zc_gen.ZEROCONF, - {zeroconf.HOMEKIT_TYPE: [{"domain": "homekit_controller"}]}, + {"_hap._tcp.local.": [{"domain": "homekit_controller"}]}, clear=True, ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( - zeroconf, "HaServiceBrowser", side_effect=service_update_mock + zeroconf, + "HaServiceBrowser", + side_effect=lambda *args, **kwargs: service_update_mock( + *args, **kwargs, limit_service="_hap._tcp.local." + ), ) as mock_service_browser: mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( "tado", HOMEKIT_STATUS_PAIRED @@ -400,12 +418,16 @@ async def test_homekit_invalid_paring_status(hass, mock_zeroconf): """Test that missing paring data is not sent to homekit_controller.""" with patch.dict( zc_gen.ZEROCONF, - {zeroconf.HOMEKIT_TYPE: [{"domain": "homekit_controller"}]}, + {"_hap._tcp.local.": [{"domain": "homekit_controller"}]}, clear=True, ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( - zeroconf, "HaServiceBrowser", side_effect=service_update_mock + zeroconf, + "HaServiceBrowser", + side_effect=lambda *args, **kwargs: service_update_mock( + *args, **kwargs, limit_service="_hap._tcp.local." + ), ) as mock_service_browser: mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( "tado", b"invalid" @@ -423,7 +445,7 @@ async def test_homekit_not_paired(hass, mock_zeroconf): """Test that an not paired device is sent to homekit_controller.""" with patch.dict( zc_gen.ZEROCONF, - {zeroconf.HOMEKIT_TYPE: [{"domain": "homekit_controller"}]}, + {"_hap._tcp.local.": [{"domain": "homekit_controller"}]}, clear=True, ), patch.object( hass.config_entries.flow, "async_init" From 0426b211f62a618f7277b9f178e832309699fbc5 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 7 Jan 2021 12:56:52 -0600 Subject: [PATCH 124/507] Rewrite Plex tests to use mocked payloads (#44044) --- .coveragerc | 2 - homeassistant/components/plex/server.py | 2 +- tests/components/plex/conftest.py | 371 +++++++++++- tests/components/plex/const.py | 2 + tests/components/plex/mock_classes.py | 530 ------------------ tests/components/plex/test_browse_media.py | 17 +- tests/components/plex/test_config_flow.py | 227 ++++---- tests/components/plex/test_init.py | 101 ++-- tests/components/plex/test_media_players.py | 34 +- tests/components/plex/test_playback.py | 92 ++- tests/components/plex/test_server.py | 328 +++++------ tests/components/plex/test_services.py | 111 ++-- tests/fixtures/plex/album.xml | 4 + tests/fixtures/plex/artist_albums.xml | 4 + tests/fixtures/plex/children_20.xml | 11 + tests/fixtures/plex/children_200.xml | 1 + tests/fixtures/plex/children_30.xml | 4 + tests/fixtures/plex/children_300.xml | 4 + tests/fixtures/plex/empty_library.xml | 1 + tests/fixtures/plex/empty_payload.xml | 1 + tests/fixtures/plex/grandchildren_300.xml | 1 + tests/fixtures/plex/library.xml | 5 + tests/fixtures/plex/library_movies_all.xml | 51 ++ tests/fixtures/plex/library_movies_sort.xml | 10 + tests/fixtures/plex/library_music_all.xml | 6 + tests/fixtures/plex/library_music_sort.xml | 7 + tests/fixtures/plex/library_sections.xml | 11 + tests/fixtures/plex/library_tvshows_all.xml | 6 + tests/fixtures/plex/library_tvshows_sort.xml | 8 + tests/fixtures/plex/media_1.xml | 11 + tests/fixtures/plex/media_100.xml | 1 + tests/fixtures/plex/media_200.xml | 4 + tests/fixtures/plex/media_30.xml | 6 + .../plex/player_plexweb_resources.xml | 1 + tests/fixtures/plex/playlist_500.xml | 11 + tests/fixtures/plex/playlists.xml | 6 + tests/fixtures/plex/playqueue_created.xml | 1 + tests/fixtures/plex/plex_server_accounts.xml | 6 + tests/fixtures/plex/plex_server_base.xml | 27 + tests/fixtures/plex/plex_server_clients.xml | 3 + tests/fixtures/plex/plextv_account.xml | 15 + tests/fixtures/plex/plextv_resources_base.xml | 21 + tests/fixtures/plex/security_token.xml | 1 + tests/fixtures/plex/session_base.xml | 11 + tests/fixtures/plex/session_photo.xml | 5 + tests/fixtures/plex/session_plexweb.xml | 11 + tests/fixtures/plex/show_seasons.xml | 4 + tests/fixtures/plex/sonos_resources.xml | 5 + 48 files changed, 1113 insertions(+), 989 deletions(-) create mode 100644 tests/fixtures/plex/album.xml create mode 100644 tests/fixtures/plex/artist_albums.xml create mode 100644 tests/fixtures/plex/children_20.xml create mode 100644 tests/fixtures/plex/children_200.xml create mode 100644 tests/fixtures/plex/children_30.xml create mode 100644 tests/fixtures/plex/children_300.xml create mode 100644 tests/fixtures/plex/empty_library.xml create mode 100644 tests/fixtures/plex/empty_payload.xml create mode 100644 tests/fixtures/plex/grandchildren_300.xml create mode 100644 tests/fixtures/plex/library.xml create mode 100644 tests/fixtures/plex/library_movies_all.xml create mode 100644 tests/fixtures/plex/library_movies_sort.xml create mode 100644 tests/fixtures/plex/library_music_all.xml create mode 100644 tests/fixtures/plex/library_music_sort.xml create mode 100644 tests/fixtures/plex/library_sections.xml create mode 100644 tests/fixtures/plex/library_tvshows_all.xml create mode 100644 tests/fixtures/plex/library_tvshows_sort.xml create mode 100644 tests/fixtures/plex/media_1.xml create mode 100644 tests/fixtures/plex/media_100.xml create mode 100644 tests/fixtures/plex/media_200.xml create mode 100644 tests/fixtures/plex/media_30.xml create mode 100644 tests/fixtures/plex/player_plexweb_resources.xml create mode 100644 tests/fixtures/plex/playlist_500.xml create mode 100644 tests/fixtures/plex/playlists.xml create mode 100644 tests/fixtures/plex/playqueue_created.xml create mode 100644 tests/fixtures/plex/plex_server_accounts.xml create mode 100644 tests/fixtures/plex/plex_server_base.xml create mode 100644 tests/fixtures/plex/plex_server_clients.xml create mode 100644 tests/fixtures/plex/plextv_account.xml create mode 100644 tests/fixtures/plex/plextv_resources_base.xml create mode 100644 tests/fixtures/plex/security_token.xml create mode 100644 tests/fixtures/plex/session_base.xml create mode 100644 tests/fixtures/plex/session_photo.xml create mode 100644 tests/fixtures/plex/session_plexweb.xml create mode 100644 tests/fixtures/plex/show_seasons.xml create mode 100644 tests/fixtures/plex/sonos_resources.xml diff --git a/.coveragerc b/.coveragerc index b859e229e74..b3e58591bfa 100644 --- a/.coveragerc +++ b/.coveragerc @@ -694,8 +694,6 @@ 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 homeassistant/components/point/* diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 3834833b740..f8d55c71fc4 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -146,7 +146,7 @@ class PlexServer: available_servers = [ (x.name, x.clientIdentifier) for x in self.account.resources() - if "server" in x.provides + if "server" in x.provides and x.presence ] if not available_servers: diff --git a/tests/components/plex/conftest.py b/tests/components/plex/conftest.py index 50fcf3eb64d..8fc25a819e8 100644 --- a/tests/components/plex/conftest.py +++ b/tests/components/plex/conftest.py @@ -3,13 +3,261 @@ from unittest.mock import patch import pytest -from homeassistant.components.plex.const import DOMAIN +from homeassistant.components.plex.const import DOMAIN, PLEX_SERVER_CONFIG, SERVERS +from homeassistant.const import CONF_URL -from .const import DEFAULT_DATA, DEFAULT_OPTIONS +from .const import DEFAULT_DATA, DEFAULT_OPTIONS, PLEX_DIRECT_URL from .helpers import websocket_connected -from .mock_classes import MockGDM, MockPlexAccount, MockPlexServer +from .mock_classes import MockGDM -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_fixture + + +def plex_server_url(entry): + """Return a protocol-less URL from a config entry.""" + return entry.data[PLEX_SERVER_CONFIG][CONF_URL].split(":", 1)[-1] + + +@pytest.fixture(name="album", scope="session") +def album_fixture(): + """Load album payload and return it.""" + return load_fixture("plex/album.xml") + + +@pytest.fixture(name="artist_albums", scope="session") +def artist_albums_fixture(): + """Load artist's albums payload and return it.""" + return load_fixture("plex/artist_albums.xml") + + +@pytest.fixture(name="children_20", scope="session") +def children_20_fixture(): + """Load children payload for item 20 and return it.""" + return load_fixture("plex/children_20.xml") + + +@pytest.fixture(name="children_30", scope="session") +def children_30_fixture(): + """Load children payload for item 30 and return it.""" + return load_fixture("plex/children_30.xml") + + +@pytest.fixture(name="children_200", scope="session") +def children_200_fixture(): + """Load children payload for item 200 and return it.""" + return load_fixture("plex/children_200.xml") + + +@pytest.fixture(name="children_300", scope="session") +def children_300_fixture(): + """Load children payload for item 300 and return it.""" + return load_fixture("plex/children_300.xml") + + +@pytest.fixture(name="empty_library", scope="session") +def empty_library_fixture(): + """Load an empty library payload and return it.""" + return load_fixture("plex/empty_library.xml") + + +@pytest.fixture(name="empty_payload", scope="session") +def empty_payload_fixture(): + """Load an empty payload and return it.""" + return load_fixture("plex/empty_payload.xml") + + +@pytest.fixture(name="grandchildren_300", scope="session") +def grandchildren_300_fixture(): + """Load grandchildren payload for item 300 and return it.""" + return load_fixture("plex/grandchildren_300.xml") + + +@pytest.fixture(name="library_movies_all", scope="session") +def library_movies_all_fixture(): + """Load payload for all items in the movies library and return it.""" + return load_fixture("plex/library_movies_all.xml") + + +@pytest.fixture(name="library_tvshows_all", scope="session") +def library_tvshows_all_fixture(): + """Load payload for all items in the tvshows library and return it.""" + return load_fixture("plex/library_tvshows_all.xml") + + +@pytest.fixture(name="library_music_all", scope="session") +def library_music_all_fixture(): + """Load payload for all items in the music library and return it.""" + return load_fixture("plex/library_music_all.xml") + + +@pytest.fixture(name="library_movies_sort", scope="session") +def library_movies_sort_fixture(): + """Load sorting payload for movie library and return it.""" + return load_fixture("plex/library_movies_sort.xml") + + +@pytest.fixture(name="library_tvshows_sort", scope="session") +def library_tvshows_sort_fixture(): + """Load sorting payload for tvshow library and return it.""" + return load_fixture("plex/library_tvshows_sort.xml") + + +@pytest.fixture(name="library_music_sort", scope="session") +def library_music_sort_fixture(): + """Load sorting payload for music library and return it.""" + return load_fixture("plex/library_music_sort.xml") + + +@pytest.fixture(name="library", scope="session") +def library_fixture(): + """Load library payload and return it.""" + return load_fixture("plex/library.xml") + + +@pytest.fixture(name="library_sections", scope="session") +def library_sections_fixture(): + """Load library sections payload and return it.""" + return load_fixture("plex/library_sections.xml") + + +@pytest.fixture(name="media_1", scope="session") +def media_1_fixture(): + """Load media payload for item 1 and return it.""" + return load_fixture("plex/media_1.xml") + + +@pytest.fixture(name="media_30", scope="session") +def media_30_fixture(): + """Load media payload for item 30 and return it.""" + return load_fixture("plex/media_30.xml") + + +@pytest.fixture(name="media_100", scope="session") +def media_100_fixture(): + """Load media payload for item 100 and return it.""" + return load_fixture("plex/media_100.xml") + + +@pytest.fixture(name="media_200", scope="session") +def media_200_fixture(): + """Load media payload for item 200 and return it.""" + return load_fixture("plex/media_200.xml") + + +@pytest.fixture(name="player_plexweb_resources", scope="session") +def player_plexweb_resources_fixture(): + """Load resources payload for a Plex Web player and return it.""" + return load_fixture("plex/player_plexweb_resources.xml") + + +@pytest.fixture(name="playlists", scope="session") +def playlists_fixture(): + """Load payload for all playlists and return it.""" + return load_fixture("plex/playlists.xml") + + +@pytest.fixture(name="playlist_500", scope="session") +def playlist_500_fixture(): + """Load payload for playlist 500 and return it.""" + return load_fixture("plex/playlist_500.xml") + + +@pytest.fixture(name="playqueue_created", scope="session") +def playqueue_created_fixture(): + """Load payload for playqueue creation response and return it.""" + return load_fixture("plex/playqueue_created.xml") + + +@pytest.fixture(name="plex_server_accounts", scope="session") +def plex_server_accounts_fixture(): + """Load payload accounts on the Plex server and return it.""" + return load_fixture("plex/plex_server_accounts.xml") + + +@pytest.fixture(name="plex_server_base", scope="session") +def plex_server_base_fixture(): + """Load base payload for Plex server info and return it.""" + return load_fixture("plex/plex_server_base.xml") + + +@pytest.fixture(name="plex_server_default", scope="session") +def plex_server_default_fixture(plex_server_base): + """Load default payload for Plex server info and return it.""" + return plex_server_base.format( + name="Plex Server 1", machine_identifier="unique_id_123" + ) + + +@pytest.fixture(name="plex_server_clients", scope="session") +def plex_server_clients_fixture(): + """Load available clients payload for Plex server and return it.""" + return load_fixture("plex/plex_server_clients.xml") + + +@pytest.fixture(name="plextv_account", scope="session") +def plextv_account_fixture(): + """Load account info from plex.tv and return it.""" + return load_fixture("plex/plextv_account.xml") + + +@pytest.fixture(name="plextv_resources_base", scope="session") +def plextv_resources_base_fixture(): + """Load base payload for plex.tv resources and return it.""" + return load_fixture("plex/plextv_resources_base.xml") + + +@pytest.fixture(name="plextv_resources", scope="session") +def plextv_resources_fixture(plextv_resources_base): + """Load default payload for plex.tv resources and return it.""" + return plextv_resources_base.format(second_server_enabled=0) + + +@pytest.fixture(name="session_base", scope="session") +def session_base_fixture(): + """Load the base session payload and return it.""" + return load_fixture("plex/session_base.xml") + + +@pytest.fixture(name="session_default", scope="session") +def session_default_fixture(session_base): + """Load the default session payload and return it.""" + return session_base.format(user_id=1) + + +@pytest.fixture(name="session_new_user", scope="session") +def session_new_user_fixture(session_base): + """Load the new user session payload and return it.""" + return session_base.format(user_id=1001) + + +@pytest.fixture(name="session_photo", scope="session") +def session_photo_fixture(): + """Load a photo session payload and return it.""" + return load_fixture("plex/session_photo.xml") + + +@pytest.fixture(name="session_plexweb", scope="session") +def session_plexweb_fixture(): + """Load a Plex Web session payload and return it.""" + return load_fixture("plex/session_plexweb.xml") + + +@pytest.fixture(name="security_token", scope="session") +def security_token_fixture(): + """Load a security token payload and return it.""" + return load_fixture("plex/security_token.xml") + + +@pytest.fixture(name="show_seasons", scope="session") +def show_seasons_fixture(): + """Load a show's seasons payload and return it.""" + return load_fixture("plex/show_seasons.xml") + + +@pytest.fixture(name="sonos_resources", scope="session") +def sonos_resources_fixture(): + """Load Sonos resources payload and return it.""" + return load_fixture("plex/sonos_resources.xml") @pytest.fixture(name="entry") @@ -23,14 +271,6 @@ def mock_config_entry(): ) -@pytest.fixture -def mock_plex_account(): - """Mock the PlexAccount class and return the used instance.""" - plex_account = MockPlexAccount() - with patch("plexapi.myplex.MyPlexAccount", return_value=plex_account): - yield plex_account - - @pytest.fixture def mock_websocket(): """Mock the PlexWebsocket class.""" @@ -39,15 +279,112 @@ def mock_websocket(): @pytest.fixture -def setup_plex_server(hass, entry, mock_plex_account, mock_websocket): +def mock_plex_calls( + entry, + requests_mock, + children_20, + children_30, + children_200, + children_300, + empty_library, + grandchildren_300, + library, + library_sections, + library_movies_all, + library_movies_sort, + library_music_all, + library_music_sort, + library_tvshows_all, + library_tvshows_sort, + media_1, + media_30, + media_100, + media_200, + playlists, + playlist_500, + plextv_account, + plextv_resources, + plex_server_accounts, + plex_server_clients, + plex_server_default, + security_token, +): + """Mock Plex API calls.""" + requests_mock.get("https://plex.tv/users/account", text=plextv_account) + requests_mock.get("https://plex.tv/api/resources", text=plextv_resources) + + url = plex_server_url(entry) + + for server in [url, PLEX_DIRECT_URL]: + requests_mock.get(server, text=plex_server_default) + requests_mock.get(f"{server}/accounts", text=plex_server_accounts) + + requests_mock.get(f"{url}/clients", text=plex_server_clients) + requests_mock.get(f"{url}/library", text=library) + requests_mock.get(f"{url}/library/sections", text=library_sections) + + requests_mock.get(f"{url}/library/onDeck", text=empty_library) + requests_mock.get(f"{url}/library/sections/1/sorts", text=library_movies_sort) + requests_mock.get(f"{url}/library/sections/2/sorts", text=library_tvshows_sort) + requests_mock.get(f"{url}/library/sections/3/sorts", text=library_music_sort) + + requests_mock.get(f"{url}/library/sections/1/all", text=library_movies_all) + requests_mock.get(f"{url}/library/sections/2/all", text=library_tvshows_all) + requests_mock.get(f"{url}/library/sections/3/all", text=library_music_all) + + requests_mock.get(f"{url}/library/metadata/200/children", text=children_200) + requests_mock.get(f"{url}/library/metadata/300/children", text=children_300) + requests_mock.get(f"{url}/library/metadata/300/allLeaves", text=grandchildren_300) + + requests_mock.get(f"{url}/library/metadata/1", text=media_1) + requests_mock.get(f"{url}/library/metadata/30", text=media_30) + requests_mock.get(f"{url}/library/metadata/100", text=media_100) + requests_mock.get(f"{url}/library/metadata/200", text=media_200) + + requests_mock.get(f"{url}/library/metadata/20/children", text=children_20) + requests_mock.get(f"{url}/library/metadata/30/children", text=children_30) + + requests_mock.get(f"{url}/playlists", text=playlists) + requests_mock.get(f"{url}/playlists/500/items", text=playlist_500) + requests_mock.get(f"{url}/security/token", text=security_token) + + +@pytest.fixture +def setup_plex_server( + hass, + entry, + mock_websocket, + mock_plex_calls, + requests_mock, + empty_payload, + session_default, + session_photo, + session_plexweb, +): """Set up and return a mocked Plex server instance.""" async def _wrapper(**kwargs): - """Wrap the fixture to allow passing arguments to the MockPlexServer instance.""" + """Wrap the fixture to allow passing arguments to the setup method.""" config_entry = kwargs.get("config_entry", entry) + disable_clients = kwargs.pop("disable_clients", False) disable_gdm = kwargs.pop("disable_gdm", True) - plex_server = MockPlexServer(**kwargs) - with patch("plexapi.server.PlexServer", return_value=plex_server), patch( + client_type = kwargs.pop("client_type", None) + session_type = kwargs.pop("session_type", None) + + if client_type == "plexweb": + session = session_plexweb + elif session_type == "photo": + session = session_photo + else: + session = session_default + + url = plex_server_url(entry) + requests_mock.get(f"{url}/status/sessions", text=session) + + if disable_clients: + requests_mock.get(f"{url}/clients", text=empty_payload) + + with patch( "homeassistant.components.plex.GDM", return_value=MockGDM(disabled=disable_gdm), ): @@ -56,6 +393,8 @@ def setup_plex_server(hass, entry, mock_plex_account, mock_websocket): await hass.async_block_till_done() websocket_connected(mock_websocket) await hass.async_block_till_done() + + plex_server = hass.data[DOMAIN][SERVERS][entry.unique_id] return plex_server return _wrapper diff --git a/tests/components/plex/const.py b/tests/components/plex/const.py index 548be2edeb8..9e376d19cac 100644 --- a/tests/components/plex/const.py +++ b/tests/components/plex/const.py @@ -61,3 +61,5 @@ DEFAULT_OPTIONS = { const.CONF_USE_EPISODE_ART: False, } } + +PLEX_DIRECT_URL = "https://1-2-3-4.123456789001234567890.plex.direct:32400" diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py index 5f2fad6a8f1..c6f1aeda9b7 100644 --- a/tests/components/plex/mock_classes.py +++ b/tests/components/plex/mock_classes.py @@ -1,17 +1,4 @@ """Mock classes used in tests.""" -from functools import lru_cache - -from aiohttp.helpers import reify -from plexapi.exceptions import NotFound - -from homeassistant.components.plex.const import ( - CONF_SERVER, - CONF_SERVER_IDENTIFIER, - PLEX_SERVER_CONFIG, -) -from homeassistant.const import CONF_URL - -from .const import DEFAULT_DATA, MOCK_SERVERS, MOCK_USERS GDM_SERVER_PAYLOAD = [ { @@ -94,520 +81,3 @@ class MockGDM: self.entries = GDM_CLIENT_PAYLOAD else: self.entries = GDM_SERVER_PAYLOAD - - -class MockResource: - """Mock a PlexAccount resource.""" - - def __init__(self, index, kind="server"): - """Initialize the object.""" - if kind == "server": - self.name = MOCK_SERVERS[index][CONF_SERVER] - self.clientIdentifier = MOCK_SERVERS[index][ # pylint: disable=invalid-name - CONF_SERVER_IDENTIFIER - ] - self.provides = ["server"] - self.device = MockPlexServer(index) - else: - self.name = f"plex.tv Resource Player {index+10}" - self.clientIdentifier = f"client-{index+10}" - self.provides = ["player"] - self.device = MockPlexClient( - baseurl=f"http://192.168.0.1{index}:32500", index=index + 10 - ) - self.presence = index == 0 - self.publicAddressMatches = True - - def connect(self, timeout): - """Mock the resource connect method.""" - return self.device - - -class MockPlexAccount: - """Mock a PlexAccount instance.""" - - def __init__(self, servers=1, players=3): - """Initialize the object.""" - self._resources = [] - for index in range(servers): - self._resources.append(MockResource(index)) - for index in range(players): - self._resources.append(MockResource(index, kind="player")) - - def resource(self, name): - """Mock the PlexAccount resource lookup method.""" - return [x for x in self._resources if x.name == name][0] - - def resources(self): - """Mock the PlexAccount resources listing method.""" - return self._resources - - def sonos_speaker(self, speaker_name): - """Mock the PlexAccount Sonos lookup method.""" - return MockPlexSonosClient(speaker_name) - - -class MockPlexSystemAccount: - """Mock a PlexSystemAccount instance.""" - - def __init__(self, index): - """Initialize the object.""" - # Start accountIDs at 1 to set proper owner. - self.name = list(MOCK_USERS)[index] - self.accountID = index + 1 - - -class MockPlexServer: - """Mock a PlexServer instance.""" - - def __init__( - self, - index=0, - config_entry=None, - num_users=len(MOCK_USERS), - session_type="video", - ): - """Initialize the object.""" - if config_entry: - self._data = config_entry.data - else: - self._data = DEFAULT_DATA - - self._baseurl = self._data[PLEX_SERVER_CONFIG][CONF_URL] - self.friendlyName = self._data[CONF_SERVER] - self.machineIdentifier = self._data[CONF_SERVER_IDENTIFIER] - - self._systemAccounts = list(map(MockPlexSystemAccount, range(num_users))) - - self._clients = [] - self._session = None - self._sessions = [] - self.set_clients(num_users) - self.set_sessions(num_users, session_type) - - self._cache = {} - - def set_clients(self, num_clients): - """Set up mock PlexClients for this PlexServer.""" - self._clients = [ - MockPlexClient(baseurl=self._baseurl, index=x) for x in range(num_clients) - ] - - def set_sessions(self, num_sessions, session_type): - """Set up mock PlexSessions for this PlexServer.""" - self._sessions = [ - MockPlexSession(self._clients[x], mediatype=session_type, index=x) - for x in range(num_sessions) - ] - - def clear_clients(self): - """Clear all active PlexClients.""" - self._clients = [] - - def clear_sessions(self): - """Clear all active PlexSessions.""" - self._sessions = [] - - def clients(self): - """Mock the clients method.""" - return self._clients - - def createToken(self): - """Mock the createToken method.""" - return "temporary_token" - - def sessions(self): - """Mock the sessions method.""" - return self._sessions - - def systemAccounts(self): - """Mock the systemAccounts lookup method.""" - return self._systemAccounts - - def url(self, path, includeToken=False): - """Mock method to generate a server URL.""" - return f"{self._baseurl}{path}" - - @property - def accounts(self): - """Mock the accounts property.""" - return set(MOCK_USERS) - - @property - def version(self): - """Mock version of PlexServer.""" - return "1.0" - - @reify - def library(self): - """Mock library object of PlexServer.""" - return MockPlexLibrary(self) - - def playlist(self, playlist): - """Mock the playlist lookup method.""" - return MockPlexMediaItem(playlist, mediatype="playlist") - - @lru_cache - def playlists(self): - """Mock the playlists lookup method with a lazy init.""" - return [ - MockPlexPlaylist( - self.library.section("Movies").all() - + self.library.section("TV Shows").all() - ), - MockPlexPlaylist(self.library.section("Music").all()), - ] - - def fetchItem(self, item): - """Mock the fetchItem method.""" - for section in self.library.sections(): - result = section.fetchItem(item) - if result: - return result - - -class MockPlexClient: - """Mock a PlexClient instance.""" - - def __init__(self, server=None, baseurl=None, token=None, index=0): - """Initialize the object.""" - self.machineIdentifier = f"client-{index+1}" - self._baseurl = baseurl - self._index = index - - def url(self, key): - """Mock the url method.""" - return f"{self._baseurl}{key}" - - @property - def device(self): - """Mock the device attribute.""" - return "DEVICE" - - @property - def platform(self): - """Mock the platform attribute.""" - return "PLATFORM" - - @property - def product(self): - """Mock the product attribute.""" - if self._index == 1: - return "Plex Web" - return "PRODUCT" - - @property - def protocolCapabilities(self): - """Mock the protocolCapabilities attribute.""" - return ["playback"] - - @property - def state(self): - """Mock the state attribute.""" - return "playing" - - @property - def title(self): - """Mock the title attribute.""" - return "TITLE" - - @property - def version(self): - """Mock the version attribute.""" - return "1.0" - - def proxyThroughServer(self, value=True, server=None): - """Mock the proxyThroughServer method.""" - pass - - def playMedia(self, item): - """Mock the playMedia method.""" - pass - - -class MockPlexSession: - """Mock a PlexServer.sessions() instance.""" - - def __init__(self, player, mediatype, index=0): - """Initialize the object.""" - self.TYPE = mediatype - self.usernames = [list(MOCK_USERS)[index]] - self.players = [player] - self._section = MockPlexLibrarySection("Movies") - self.sessionKey = index + 1 - - @property - def duration(self): - """Mock the duration attribute.""" - return 10000000 - - @property - def librarySectionID(self): - """Mock the librarySectionID attribute.""" - return 1 - - @property - def ratingKey(self): - """Mock the ratingKey attribute.""" - return 123 - - def section(self): - """Mock the section method.""" - return self._section - - @property - def summary(self): - """Mock the summary attribute.""" - return "SUMMARY" - - @property - def thumbUrl(self): - """Mock the thumbUrl attribute.""" - return "http://1.2.3.4/thumb" - - @property - def title(self): - """Mock the title attribute.""" - return "TITLE" - - @property - def type(self): - """Mock the type attribute.""" - return "movie" - - @property - def viewOffset(self): - """Mock the viewOffset attribute.""" - return 0 - - @property - def year(self): - """Mock the year attribute.""" - return 2020 - - -class MockPlexLibrary: - """Mock a Plex Library instance.""" - - def __init__(self, plex_server): - """Initialize the object.""" - self._plex_server = plex_server - self._sections = {} - - for kind in ["Movies", "Music", "TV Shows", "Photos"]: - self._sections[kind] = MockPlexLibrarySection(kind) - - def section(self, title): - """Mock the LibrarySection lookup.""" - section = self._sections.get(title) - if section: - return section - raise NotFound - - def sections(self): - """Return all available sections.""" - return self._sections.values() - - def sectionByID(self, section_id): - """Mock the sectionByID lookup.""" - return [x for x in self.sections() if x.key == section_id][0] - - def onDeck(self): - """Mock an empty On Deck folder.""" - return [] - - def recentlyAdded(self): - """Mock an empty Recently Added folder.""" - return [] - - -class MockPlexLibrarySection: - """Mock a Plex LibrarySection instance.""" - - def __init__(self, library): - """Initialize the object.""" - self.title = library - - if library == "Music": - self._item = MockPlexArtist("Artist") - elif library == "TV Shows": - self._item = MockPlexShow("TV Show") - else: - self._item = MockPlexMediaItem(library[:-1]) - - def get(self, query): - """Mock the get lookup method.""" - if self._item.title == query: - return self._item - raise NotFound - - def all(self): - """Mock the all method.""" - return [self._item] - - def fetchItem(self, ratingKey): - """Return a specific item.""" - for item in self.all(): - if item.ratingKey == ratingKey: - return item - if item._children: - for child in item._children: - if child.ratingKey == ratingKey: - return child - - def onDeck(self): - """Mock an empty On Deck folder.""" - return [] - - def recentlyAdded(self): - """Mock an empty Recently Added folder.""" - return self.all() - - @property - def type(self): - """Mock the library type.""" - if self.title == "Movies": - return "movie" - if self.title == "Music": - return "artist" - if self.title == "TV Shows": - return "show" - if self.title == "Photos": - return "photo" - - @property - def TYPE(self): - """Return the library type.""" - return self.type - - @property - def key(self): - """Mock the key identifier property.""" - return str(id(self.title)) - - def search(self, **kwargs): - """Mock the LibrarySection search method.""" - if kwargs.get("libtype") == "movie": - return self.all() - - def update(self): - """Mock the update call.""" - pass - - -class MockPlexMediaItem: - """Mock a Plex Media instance.""" - - def __init__(self, title, mediatype="video", year=2020): - """Initialize the object.""" - self.title = str(title) - self.type = mediatype - self.thumbUrl = "http://1.2.3.4/thumb.png" - self.year = year - self._children = [] - - def __iter__(self): - """Provide iterator.""" - yield from self._children - - @property - def ratingKey(self): - """Mock the ratingKey property.""" - return id(self.title) - - -class MockPlexPlaylist(MockPlexMediaItem): - """Mock a Plex Playlist instance.""" - - def __init__(self, items): - """Initialize the object.""" - super().__init__(f"Playlist ({len(items)} Items)", "playlist") - for item in items: - self._children.append(item) - - -class MockPlexShow(MockPlexMediaItem): - """Mock a Plex Show instance.""" - - def __init__(self, show): - """Initialize the object.""" - super().__init__(show, "show") - for index in range(1, 5): - self._children.append(MockPlexSeason(index)) - - def season(self, season): - """Mock the season lookup method.""" - return [x for x in self._children if x.title == f"Season {season}"][0] - - -class MockPlexSeason(MockPlexMediaItem): - """Mock a Plex Season instance.""" - - def __init__(self, season): - """Initialize the object.""" - super().__init__(f"Season {season}", "season") - for index in range(1, 10): - self._children.append(MockPlexMediaItem(f"Episode {index}", "episode")) - - def episode(self, episode): - """Mock the episode lookup method.""" - return self._children[episode - 1] - - -class MockPlexAlbum(MockPlexMediaItem): - """Mock a Plex Album instance.""" - - def __init__(self, album): - """Initialize the object.""" - super().__init__(album, "album") - for index in range(1, 10): - self._children.append(MockPlexMediaTrack(index)) - - def track(self, track): - """Mock the track lookup method.""" - try: - return [x for x in self._children if x.title == track][0] - except IndexError: - raise NotFound - - def tracks(self): - """Mock the tracks lookup method.""" - return self._children - - -class MockPlexArtist(MockPlexMediaItem): - """Mock a Plex Artist instance.""" - - def __init__(self, artist): - """Initialize the object.""" - super().__init__(artist, "artist") - self._album = MockPlexAlbum("Album") - - def album(self, album): - """Mock the album lookup method.""" - return self._album - - def get(self, track): - """Mock the track lookup method.""" - return self._album.track(track) - - -class MockPlexMediaTrack(MockPlexMediaItem): - """Mock a Plex Track instance.""" - - def __init__(self, index=1): - """Initialize the object.""" - super().__init__(f"Track {index}", "track") - self.index = index - - -class MockPlexSonosClient: - """Mock a PlexSonosClient instance.""" - - def __init__(self, name): - """Initialize the object.""" - self.name = name - - def playMedia(self, item): - """Mock the playMedia method.""" - pass diff --git a/tests/components/plex/test_browse_media.py b/tests/components/plex/test_browse_media.py index 66cbc51ef82..f9966a18c27 100644 --- a/tests/components/plex/test_browse_media.py +++ b/tests/components/plex/test_browse_media.py @@ -10,7 +10,7 @@ from homeassistant.components.websocket_api.const import ERR_UNKNOWN_ERROR, TYPE from .const import DEFAULT_DATA -async def test_browse_media(hass, hass_ws_client, mock_plex_server, mock_websocket): +async def test_browse_media(hass, hass_ws_client, mock_plex_server, requests_mock): """Test getting Plex clients from plex.tv.""" websocket_client = await hass_ws_client(hass) @@ -51,8 +51,10 @@ async def test_browse_media(hass, hass_ws_client, mock_plex_server, mock_websock result = msg["result"] assert result[ATTR_MEDIA_CONTENT_TYPE] == "server" assert result[ATTR_MEDIA_CONTENT_ID] == DEFAULT_DATA[CONF_SERVER_IDENTIFIER] - assert len(result["children"]) == len(mock_plex_server.library.sections()) + len( - SPECIAL_METHODS + # Library Sections + Special Sections + Playlists + assert ( + len(result["children"]) + == len(mock_plex_server.library.sections()) + len(SPECIAL_METHODS) + 1 ) tvshows = next(iter(x for x in result["children"] if x["title"] == "TV Shows")) @@ -149,9 +151,14 @@ async def test_browse_media(hass, hass_ws_client, mock_plex_server, mock_websock result = msg["result"] assert result[ATTR_MEDIA_CONTENT_TYPE] == "show" result_id = int(result[ATTR_MEDIA_CONTENT_ID]) - assert result["title"] == mock_plex_server.fetchItem(result_id).title + assert result["title"] == mock_plex_server.fetch_item(result_id).title # Browse into a non-existent TV season + unknown_key = 99999999999999 + requests_mock.get( + f"{mock_plex_server.url_in_use}/library/metadata/{unknown_key}", status_code=404 + ) + msg_id += 1 await websocket_client.send_json( { @@ -159,7 +166,7 @@ async def test_browse_media(hass, hass_ws_client, mock_plex_server, mock_websock "type": "media_player/browse_media", "entity_id": media_players[0], ATTR_MEDIA_CONTENT_TYPE: result["children"][0][ATTR_MEDIA_CONTENT_TYPE], - ATTR_MEDIA_CONTENT_ID: str(99999999999999), + ATTR_MEDIA_CONTENT_ID: str(unknown_key), } ) diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index 13754a725db..bc0e59e658f 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -35,16 +35,11 @@ from homeassistant.const import ( CONF_URL, CONF_VERIFY_SSL, ) +from homeassistant.setup import async_setup_component -from .const import DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN +from .const import DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN, PLEX_DIRECT_URL from .helpers import trigger_plex_update, wait_for_debouncer -from .mock_classes import ( - MockGDM, - MockPlexAccount, - MockPlexClient, - MockPlexServer, - MockResource, -) +from .mock_classes import MockGDM from tests.common import MockConfigEntry @@ -82,7 +77,7 @@ async def test_bad_credentials(hass): assert result["errors"][CONF_TOKEN] == "faulty_credentials" -async def test_bad_hostname(hass): +async def test_bad_hostname(hass, mock_plex_calls): """Test when an invalid address is provided.""" await async_process_ha_core_config( hass, @@ -96,12 +91,9 @@ async def test_bad_hostname(hass): assert result["step_id"] == "user" with patch( - "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() - ), patch.object( - MockResource, "connect", side_effect=requests.exceptions.ConnectionError - ), patch( - "plexauth.PlexAuth.initiate_auth" - ), patch( + "plexapi.myplex.MyPlexResource.connect", + side_effect=requests.exceptions.ConnectionError, + ), patch("plexauth.PlexAuth.initiate_auth"), patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): result = await hass.config_entries.flow.async_configure( @@ -148,8 +140,9 @@ async def test_unknown_exception(hass): assert result["reason"] == "unknown" -async def test_no_servers_found(hass): +async def test_no_servers_found(hass, mock_plex_calls, requests_mock, empty_payload): """Test when no servers are on an account.""" + requests_mock.get("https://plex.tv/api/resources", text=empty_payload) await async_process_ha_core_config( hass, @@ -162,9 +155,7 @@ async def test_no_servers_found(hass): assert result["type"] == "form" assert result["step_id"] == "user" - with patch( - "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=0) - ), patch("plexauth.PlexAuth.initiate_auth"), patch( + with patch("plexauth.PlexAuth.initiate_auth"), patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): result = await hass.config_entries.flow.async_configure( @@ -181,11 +172,9 @@ async def test_no_servers_found(hass): assert result["errors"]["base"] == "no_servers" -async def test_single_available_server(hass): +async def test_single_available_server(hass, mock_plex_calls): """Test creating an entry with one server available.""" - mock_plex_server = MockPlexServer() - await async_process_ha_core_config( hass, {"internal_url": "http://example.local:8123"}, @@ -197,9 +186,7 @@ async def test_single_available_server(hass): assert result["type"] == "form" assert result["step_id"] == "user" - with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()), patch( - "plexapi.server.PlexServer", return_value=mock_plex_server - ), patch("plexauth.PlexAuth.initiate_auth"), patch( + with patch("plexauth.PlexAuth.initiate_auth"), patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): result = await hass.config_entries.flow.async_configure( @@ -212,20 +199,27 @@ async def test_single_available_server(hass): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "create_entry" - assert result["title"] == mock_plex_server.friendlyName - assert result["data"][CONF_SERVER] == mock_plex_server.friendlyName + + server_id = result["data"][CONF_SERVER_IDENTIFIER] + mock_plex_server = hass.data[DOMAIN][SERVERS][server_id] + + assert result["title"] == mock_plex_server.friendly_name + assert result["data"][CONF_SERVER] == mock_plex_server.friendly_name assert ( - result["data"][CONF_SERVER_IDENTIFIER] == mock_plex_server.machineIdentifier + result["data"][CONF_SERVER_IDENTIFIER] + == mock_plex_server.machine_identifier + ) + assert ( + result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server.url_in_use ) - assert result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server._baseurl assert result["data"][PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN -async def test_multiple_servers_with_selection(hass): +async def test_multiple_servers_with_selection( + hass, mock_plex_calls, requests_mock, plextv_resources_base +): """Test creating an entry with multiple servers available.""" - mock_plex_server = MockPlexServer() - await async_process_ha_core_config( hass, {"internal_url": "http://example.local:8123"}, @@ -237,11 +231,11 @@ async def test_multiple_servers_with_selection(hass): assert result["type"] == "form" assert result["step_id"] == "user" - with patch( - "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2) - ), patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "plexauth.PlexAuth.initiate_auth" - ), patch( + requests_mock.get( + "https://plex.tv/api/resources", + text=plextv_resources_base.format(second_server_enabled=1), + ) + with patch("plexauth.PlexAuth.initiate_auth"), patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): result = await hass.config_entries.flow.async_configure( @@ -261,20 +255,27 @@ async def test_multiple_servers_with_selection(hass): user_input={CONF_SERVER: MOCK_SERVERS[0][CONF_SERVER]}, ) assert result["type"] == "create_entry" - assert result["title"] == mock_plex_server.friendlyName - assert result["data"][CONF_SERVER] == mock_plex_server.friendlyName + + server_id = result["data"][CONF_SERVER_IDENTIFIER] + mock_plex_server = hass.data[DOMAIN][SERVERS][server_id] + + assert result["title"] == mock_plex_server.friendly_name + assert result["data"][CONF_SERVER] == mock_plex_server.friendly_name assert ( - result["data"][CONF_SERVER_IDENTIFIER] == mock_plex_server.machineIdentifier + result["data"][CONF_SERVER_IDENTIFIER] + == mock_plex_server.machine_identifier + ) + assert ( + result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server.url_in_use ) - assert result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server._baseurl assert result["data"][PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN -async def test_adding_last_unconfigured_server(hass): +async def test_adding_last_unconfigured_server( + hass, mock_plex_calls, requests_mock, plextv_resources_base +): """Test automatically adding last unconfigured server when multiple servers on account.""" - mock_plex_server = MockPlexServer() - await async_process_ha_core_config( hass, {"internal_url": "http://example.local:8123"}, @@ -294,11 +295,12 @@ async def test_adding_last_unconfigured_server(hass): assert result["type"] == "form" assert result["step_id"] == "user" - with patch( - "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2) - ), patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "plexauth.PlexAuth.initiate_auth" - ), patch( + requests_mock.get( + "https://plex.tv/api/resources", + text=plextv_resources_base.format(second_server_enabled=1), + ) + + with patch("plexauth.PlexAuth.initiate_auth"), patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): result = await hass.config_entries.flow.async_configure( @@ -311,16 +313,25 @@ async def test_adding_last_unconfigured_server(hass): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "create_entry" - assert result["title"] == mock_plex_server.friendlyName - assert result["data"][CONF_SERVER] == mock_plex_server.friendlyName + + server_id = result["data"][CONF_SERVER_IDENTIFIER] + mock_plex_server = hass.data[DOMAIN][SERVERS][server_id] + + assert result["title"] == mock_plex_server.friendly_name + assert result["data"][CONF_SERVER] == mock_plex_server.friendly_name assert ( - result["data"][CONF_SERVER_IDENTIFIER] == mock_plex_server.machineIdentifier + result["data"][CONF_SERVER_IDENTIFIER] + == mock_plex_server.machine_identifier + ) + assert ( + result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server.url_in_use ) - assert result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server._baseurl assert result["data"][PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN -async def test_all_available_servers_configured(hass): +async def test_all_available_servers_configured( + hass, entry, requests_mock, plextv_account, plextv_resources_base +): """Test when all available servers are already configured.""" await async_process_ha_core_config( @@ -328,13 +339,7 @@ async def test_all_available_servers_configured(hass): {"internal_url": "http://example.local:8123"}, ) - MockConfigEntry( - domain=DOMAIN, - data={ - CONF_SERVER_IDENTIFIER: MOCK_SERVERS[0][CONF_SERVER_IDENTIFIER], - CONF_SERVER: MOCK_SERVERS[0][CONF_SERVER], - }, - ).add_to_hass(hass) + entry.add_to_hass(hass) MockConfigEntry( domain=DOMAIN, @@ -350,9 +355,13 @@ async def test_all_available_servers_configured(hass): assert result["type"] == "form" assert result["step_id"] == "user" - with patch( - "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2) - ), patch("plexauth.PlexAuth.initiate_auth"), patch( + requests_mock.get("https://plex.tv/users/account", text=plextv_account) + requests_mock.get( + "https://plex.tv/api/resources", + text=plextv_resources_base.format(second_server_enabled=1), + ) + + with patch("plexauth.PlexAuth.initiate_auth"), patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): result = await hass.config_entries.flow.async_configure( @@ -432,32 +441,22 @@ async def test_missing_option_flow(hass, entry, mock_plex_server): } -async def test_option_flow_new_users_available( - hass, caplog, entry, mock_websocket, setup_plex_server -): +async def test_option_flow_new_users_available(hass, entry, setup_plex_server): """Test config options multiselect defaults when new Plex users are seen.""" OPTIONS_OWNER_ONLY = copy.deepcopy(DEFAULT_OPTIONS) - OPTIONS_OWNER_ONLY[MP_DOMAIN][CONF_MONITORED_USERS] = {"Owner": {"enabled": True}} + OPTIONS_OWNER_ONLY[MP_DOMAIN][CONF_MONITORED_USERS] = {"User 1": {"enabled": True}} entry.options = OPTIONS_OWNER_ONLY - with patch("homeassistant.components.plex.server.PlexClient", new=MockPlexClient): - mock_plex_server = await setup_plex_server( - config_entry=entry, disable_gdm=False - ) - await hass.async_block_till_done() + mock_plex_server = await setup_plex_server(config_entry=entry) + await hass.async_block_till_done() - server_id = mock_plex_server.machineIdentifier + server_id = mock_plex_server.machine_identifier monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users new_users = [x for x in mock_plex_server.accounts if x not in monitored_users] 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)) - result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"}, data=None ) @@ -465,7 +464,7 @@ async def test_option_flow_new_users_available( assert result["step_id"] == "plex_mp_settings" multiselect_defaults = result["data_schema"].schema["monitored_users"].options - assert "[Owner]" in multiselect_defaults["Owner"] + assert "[Owner]" in multiselect_defaults["User 1"] for user in new_users: assert "[New]" in multiselect_defaults[user] @@ -529,7 +528,7 @@ async def test_callback_view(hass, aiohttp_client): assert resp.status == 200 -async def test_manual_config(hass): +async def test_manual_config(hass, mock_plex_calls): """Test creating via manual configuration.""" await async_process_ha_core_config( hass, @@ -587,8 +586,6 @@ async def test_manual_config(hass): assert result["type"] == "form" assert result["step_id"] == "manual_setup" - mock_plex_server = MockPlexServer() - MANUAL_SERVER = { CONF_HOST: MOCK_SERVERS[0][CONF_HOST], CONF_PORT: MOCK_SERVERS[0][CONF_PORT], @@ -647,26 +644,26 @@ async def test_manual_config(hass): assert result["step_id"] == "manual_setup" assert result["errors"]["base"] == "ssl_error" - with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket", autospec=True - ), patch( + with patch("homeassistant.components.plex.PlexWebsocket", autospec=True), patch( "homeassistant.components.plex.GDM", return_value=MockGDM(disabled=True) - ), patch( - "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MANUAL_SERVER ) assert result["type"] == "create_entry" - assert result["title"] == mock_plex_server.friendlyName - assert result["data"][CONF_SERVER] == mock_plex_server.friendlyName - assert result["data"][CONF_SERVER_IDENTIFIER] == mock_plex_server.machineIdentifier - assert result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server._baseurl + + server_id = result["data"][CONF_SERVER_IDENTIFIER] + mock_plex_server = hass.data[DOMAIN][SERVERS][server_id] + + assert result["title"] == mock_plex_server.friendly_name + assert result["data"][CONF_SERVER] == mock_plex_server.friendly_name + assert result["data"][CONF_SERVER_IDENTIFIER] == mock_plex_server.machine_identifier + assert result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server.url_in_use assert result["data"][PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN -async def test_manual_config_with_token(hass): +async def test_manual_config_with_token(hass, mock_plex_calls): """Test creating via manual configuration with only token.""" result = await hass.config_entries.flow.async_init( @@ -683,37 +680,36 @@ async def test_manual_config_with_token(hass): assert result["type"] == "form" assert result["step_id"] == "manual_setup" - mock_plex_server = MockPlexServer() - - with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()), patch( - "plexapi.server.PlexServer", return_value=mock_plex_server - ), patch( + with patch( "homeassistant.components.plex.GDM", return_value=MockGDM(disabled=True) - ), patch( - "homeassistant.components.plex.PlexWebsocket", autospec=True - ): + ), patch("homeassistant.components.plex.PlexWebsocket", autospec=True): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_TOKEN: MOCK_TOKEN} ) assert result["type"] == "create_entry" - assert result["title"] == mock_plex_server.friendlyName - assert result["data"][CONF_SERVER] == mock_plex_server.friendlyName - assert result["data"][CONF_SERVER_IDENTIFIER] == mock_plex_server.machineIdentifier - assert result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server._baseurl + + server_id = result["data"][CONF_SERVER_IDENTIFIER] + mock_plex_server = hass.data[DOMAIN][SERVERS][server_id] + + assert result["title"] == mock_plex_server.friendly_name + assert result["data"][CONF_SERVER] == mock_plex_server.friendly_name + assert result["data"][CONF_SERVER_IDENTIFIER] == mock_plex_server.machine_identifier + assert result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server.url_in_use assert result["data"][PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN async def test_setup_with_limited_credentials(hass, entry, setup_plex_server): """Test setup with a user with limited permissions.""" - with patch.object( - MockPlexServer, "systemAccounts", side_effect=plexapi.exceptions.Unauthorized + with patch( + "plexapi.server.PlexServer.systemAccounts", + side_effect=plexapi.exceptions.Unauthorized, ) as mock_accounts: mock_plex_server = await setup_plex_server() assert mock_accounts.called - plex_server = hass.data[DOMAIN][SERVERS][mock_plex_server.machineIdentifier] + plex_server = hass.data[DOMAIN][SERVERS][mock_plex_server.machine_identifier] assert len(plex_server.accounts) == 0 assert plex_server.owner is None @@ -745,6 +741,7 @@ async def test_integration_discovery(hass): async def test_trigger_reauth(hass, entry, mock_plex_server, mock_websocket): """Test setup and reauthorization of a Plex token.""" + await async_setup_component(hass, "persistent_notification", {}) await async_process_ha_core_config( hass, {"internal_url": "http://example.local:8123"}, @@ -752,8 +749,8 @@ async def test_trigger_reauth(hass, entry, mock_plex_server, mock_websocket): assert entry.state == ENTRY_STATE_LOADED - with patch.object( - mock_plex_server, "clients", side_effect=plexapi.exceptions.Unauthorized + with patch( + "plexapi.server.PlexServer.clients", side_effect=plexapi.exceptions.Unauthorized ), patch("plexapi.server.PlexServer", side_effect=plexapi.exceptions.Unauthorized): trigger_plex_update(mock_websocket) await wait_for_debouncer(hass) @@ -767,9 +764,7 @@ async def test_trigger_reauth(hass, entry, mock_plex_server, mock_websocket): flow_id = flows[0]["flow_id"] - with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()), patch( - "plexapi.server.PlexServer", return_value=mock_plex_server - ), patch("plexauth.PlexAuth.initiate_auth"), patch( + with patch("plexauth.PlexAuth.initiate_auth"), patch( "plexauth.PlexAuth.token", return_value="BRAND_NEW_TOKEN" ): result = await hass.config_entries.flow.async_configure(flow_id, user_input={}) @@ -787,7 +782,7 @@ async def test_trigger_reauth(hass, entry, mock_plex_server, mock_websocket): assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.state == ENTRY_STATE_LOADED - assert entry.data[CONF_SERVER] == mock_plex_server.friendlyName - assert entry.data[CONF_SERVER_IDENTIFIER] == mock_plex_server.machineIdentifier - assert entry.data[PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server._baseurl + assert entry.data[CONF_SERVER] == mock_plex_server.friendly_name + assert entry.data[CONF_SERVER_IDENTIFIER] == mock_plex_server.machine_identifier + assert entry.data[PLEX_SERVER_CONFIG][CONF_URL] == PLEX_DIRECT_URL assert entry.data[PLEX_SERVER_CONFIG][CONF_TOKEN] == "BRAND_NEW_TOKEN" diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index 404f7c167a5..95d2ef9bddb 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -15,11 +15,11 @@ from homeassistant.config_entries import ( ENTRY_STATE_SETUP_RETRY, ) from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL, STATE_IDLE +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from .const import DEFAULT_DATA, DEFAULT_OPTIONS +from .const import DEFAULT_DATA, DEFAULT_OPTIONS, PLEX_DIRECT_URL from .helpers import trigger_plex_update, wait_for_debouncer -from .mock_classes import MockGDM, MockPlexAccount, MockPlexServer from tests.common import MockConfigEntry, async_fire_time_changed @@ -31,7 +31,7 @@ async def test_set_config_entry_unique_id(hass, entry, mock_plex_server): assert ( hass.config_entries.async_entries(const.DOMAIN)[0].unique_id - == mock_plex_server.machineIdentifier + == mock_plex_server.machine_identifier ) @@ -79,9 +79,9 @@ async def test_unload_config_entry(hass, entry, mock_plex_server): assert entry is config_entries[0] assert entry.state == ENTRY_STATE_LOADED - server_id = mock_plex_server.machineIdentifier + server_id = mock_plex_server.machine_identifier loaded_server = hass.data[const.DOMAIN][const.SERVERS][server_id] - assert loaded_server.plex_server == mock_plex_server + assert loaded_server == mock_plex_server websocket = hass.data[const.DOMAIN][const.WEBSOCKETS][server_id] await hass.config_entries.async_unload(entry.entry_id) @@ -89,7 +89,7 @@ async def test_unload_config_entry(hass, entry, mock_plex_server): assert entry.state == ENTRY_STATE_NOT_LOADED -async def test_setup_with_photo_session(hass, entry, mock_websocket, setup_plex_server): +async def test_setup_with_photo_session(hass, entry, setup_plex_server): """Test setup component with config.""" await setup_plex_server(session_type="photo") @@ -97,7 +97,9 @@ async def test_setup_with_photo_session(hass, entry, mock_websocket, setup_plex_ assert entry.state == ENTRY_STATE_LOADED await hass.async_block_till_done() - media_player = hass.states.get("media_player.plex_product_title") + media_player = hass.states.get( + "media_player.plex_plex_for_android_tv_shield_android_tv" + ) assert media_player.state == STATE_IDLE await wait_for_debouncer(hass) @@ -106,14 +108,17 @@ async def test_setup_with_photo_session(hass, entry, mock_websocket, setup_plex_ assert sensor.state == "0" -async def test_setup_when_certificate_changed(hass, entry): +async def test_setup_when_certificate_changed( + hass, + requests_mock, + empty_payload, + plex_server_accounts, + plex_server_default, + plextv_account, + plextv_resources, +): """Test setup component when the Plex certificate has changed.""" - - old_domain = "1-2-3-4.1234567890abcdef1234567890abcdef.plex.direct" - old_url = f"https://{old_domain}:32400" - - OLD_HOSTNAME_DATA = copy.deepcopy(DEFAULT_DATA) - OLD_HOSTNAME_DATA[const.PLEX_SERVER_CONFIG][CONF_URL] = old_url + await async_setup_component(hass, "persistent_notification", {}) class WrongCertHostnameException(requests.exceptions.SSLError): """Mock the exception showing a mismatched hostname.""" @@ -123,6 +128,12 @@ async def test_setup_when_certificate_changed(hass, entry): f"hostname '{old_domain}' doesn't match" ) + old_domain = "1-2-3-4.1111111111ffffff1111111111ffffff.plex.direct" + old_url = f"https://{old_domain}:32400" + + OLD_HOSTNAME_DATA = copy.deepcopy(DEFAULT_DATA) + OLD_HOSTNAME_DATA[const.PLEX_SERVER_CONFIG][CONF_URL] = old_url + old_entry = MockConfigEntry( domain=const.DOMAIN, data=OLD_HOSTNAME_DATA, @@ -130,46 +141,45 @@ async def test_setup_when_certificate_changed(hass, entry): unique_id=DEFAULT_DATA["server_id"], ) + requests_mock.get("https://plex.tv/users/account", text=plextv_account) + requests_mock.get("https://plex.tv/api/resources", text=plextv_resources) + requests_mock.get(old_url, exc=WrongCertHostnameException) + # Test with account failure - with patch( - "plexapi.server.PlexServer", side_effect=WrongCertHostnameException - ), patch( - "plexapi.myplex.MyPlexAccount", side_effect=plexapi.exceptions.Unauthorized - ): - old_entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(old_entry.entry_id) is False - await hass.async_block_till_done() + requests_mock.get(f"{old_url}/accounts", status_code=401) + old_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(old_entry.entry_id) is False + await hass.async_block_till_done() assert old_entry.state == ENTRY_STATE_SETUP_ERROR await hass.config_entries.async_unload(old_entry.entry_id) # Test with no servers found - with patch( - "plexapi.server.PlexServer", side_effect=WrongCertHostnameException - ), patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=0)): - assert await hass.config_entries.async_setup(old_entry.entry_id) is False - await hass.async_block_till_done() + requests_mock.get(f"{old_url}/accounts", text=plex_server_accounts) + requests_mock.get("https://plex.tv/api/resources", text=empty_payload) + + assert await hass.config_entries.async_setup(old_entry.entry_id) is False + await hass.async_block_till_done() assert old_entry.state == ENTRY_STATE_SETUP_ERROR await hass.config_entries.async_unload(old_entry.entry_id) # Test with success - with patch( - "plexapi.server.PlexServer", side_effect=WrongCertHostnameException - ), patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()): - assert await hass.config_entries.async_setup(old_entry.entry_id) - await hass.async_block_till_done() + new_url = PLEX_DIRECT_URL + requests_mock.get("https://plex.tv/api/resources", text=plextv_resources) + requests_mock.get(new_url, text=plex_server_default) + requests_mock.get(f"{new_url}/accounts", text=plex_server_accounts) + + assert await hass.config_entries.async_setup(old_entry.entry_id) + await hass.async_block_till_done() assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 assert old_entry.state == ENTRY_STATE_LOADED - assert ( - old_entry.data[const.PLEX_SERVER_CONFIG][CONF_URL] - == entry.data[const.PLEX_SERVER_CONFIG][CONF_URL] - ) + assert old_entry.data[const.PLEX_SERVER_CONFIG][CONF_URL] == new_url -async def test_tokenless_server(hass, entry, mock_websocket, setup_plex_server): +async def test_tokenless_server(entry, setup_plex_server): """Test setup with a server with token auth disabled.""" TOKENLESS_DATA = copy.deepcopy(DEFAULT_DATA) TOKENLESS_DATA[const.PLEX_SERVER_CONFIG].pop(CONF_TOKEN, None) @@ -179,18 +189,13 @@ async def test_tokenless_server(hass, entry, mock_websocket, setup_plex_server): assert entry.state == ENTRY_STATE_LOADED -async def test_bad_token_with_tokenless_server(hass, entry): +async def test_bad_token_with_tokenless_server( + hass, entry, mock_websocket, setup_plex_server, requests_mock +): """Test setup with a bad token and a server with token auth disabled.""" - with patch("plexapi.server.PlexServer", return_value=MockPlexServer()), patch( - "plexapi.myplex.MyPlexAccount", side_effect=plexapi.exceptions.Unauthorized - ), patch( - "homeassistant.components.plex.GDM", return_value=MockGDM(disabled=True) - ), patch( - "homeassistant.components.plex.PlexWebsocket", autospec=True - ) as mock_websocket: - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + requests_mock.get("https://plex.tv/users/account", status_code=401) + + await setup_plex_server() assert entry.state == ENTRY_STATE_LOADED diff --git a/tests/components/plex/test_media_players.py b/tests/components/plex/test_media_players.py index 3586cbc87bb..092d7e09008 100644 --- a/tests/components/plex/test_media_players.py +++ b/tests/components/plex/test_media_players.py @@ -3,39 +3,29 @@ from unittest.mock import patch from plexapi.exceptions import NotFound -from homeassistant.components.plex.const import DOMAIN, SERVERS - -async def test_plex_tv_clients(hass, entry, mock_plex_account, setup_plex_server): +async def test_plex_tv_clients( + hass, entry, setup_plex_server, requests_mock, player_plexweb_resources +): """Test getting Plex clients from plex.tv.""" - 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): - mock_plex_server = await setup_plex_server() + requests_mock.get("/resources", text=player_plexweb_resources) + + with patch("plexapi.myplex.MyPlexResource.connect", side_effect=NotFound): + 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")) + await hass.config_entries.async_unload(entry.entry_id) # 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 setup_plex_server() 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(num_users=0) - plex_server = hass.data[DOMAIN][SERVERS][server_id] - assert len(hass.states.async_entity_ids("media_player")) == 1 - # Ensure cache gets called - await plex_server._async_update_platforms() - await hass.async_block_till_done() + # Ensure only plex.tv resource client is found + with patch("plexapi.server.PlexServer.sessions", return_value=[]): + await setup_plex_server(disable_clients=True) assert len(hass.states.async_entity_ids("media_player")) == 1 diff --git a/tests/components/plex/test_playback.py b/tests/components/plex/test_playback.py index 89c3f0253be..e8d528eb073 100644 --- a/tests/components/plex/test_playback.py +++ b/tests/components/plex/test_playback.py @@ -10,21 +10,25 @@ from homeassistant.components.media_player.const import ( ) from homeassistant.components.plex.const import ( CONF_SERVER, + CONF_SERVER_IDENTIFIER, DOMAIN, + PLEX_SERVER_CONFIG, SERVERS, SERVICE_PLAY_ON_SONOS, ) -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, CONF_URL from homeassistant.exceptions import HomeAssistantError -from .const import DEFAULT_OPTIONS, SECONDARY_DATA +from .const import DEFAULT_OPTIONS, MOCK_SERVERS, SECONDARY_DATA from tests.common import MockConfigEntry -async def test_sonos_playback(hass, mock_plex_server): +async def test_sonos_playback( + hass, mock_plex_server, requests_mock, playqueue_created, sonos_resources +): """Test playing media on a Sonos speaker.""" - server_id = mock_plex_server.machineIdentifier + server_id = mock_plex_server.machine_identifier loaded_server = hass.data[DOMAIN][SERVERS][server_id] # Test Sonos integration lookup failure @@ -43,18 +47,23 @@ async def test_sonos_playback(hass, mock_plex_server): ) # Test success with plex_key + requests_mock.get("https://sonos.plex.tv/resources", text=sonos_resources) + requests_mock.get( + "https://sonos.plex.tv/player/playback/playMedia", status_code=200 + ) + requests_mock.post("/playqueues", text=playqueue_created) with patch.object( hass.components.sonos, "get_coordinator_name", - return_value="media_player.sonos_kitchen", - ), patch("plexapi.playqueue.PlayQueue.create"): + return_value="Speaker 2", + ): assert await hass.services.async_call( DOMAIN, SERVICE_PLAY_ON_SONOS, { ATTR_ENTITY_ID: "media_player.sonos_kitchen", ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, - ATTR_MEDIA_CONTENT_ID: "2", + ATTR_MEDIA_CONTENT_ID: "100", }, True, ) @@ -63,8 +72,8 @@ async def test_sonos_playback(hass, mock_plex_server): with patch.object( hass.components.sonos, "get_coordinator_name", - return_value="media_player.sonos_kitchen", - ), patch("plexapi.playqueue.PlayQueue.create"): + return_value="Speaker 2", + ): assert await hass.services.async_call( DOMAIN, SERVICE_PLAY_ON_SONOS, @@ -80,8 +89,8 @@ async def test_sonos_playback(hass, mock_plex_server): with patch.object( hass.components.sonos, "get_coordinator_name", - return_value="media_player.sonos_kitchen", - ), patch.object(mock_plex_server, "fetchItem", side_effect=NotFound): + return_value="Speaker 2", + ), patch("plexapi.server.PlexServer.fetchItem", side_effect=NotFound): assert await hass.services.async_call( DOMAIN, SERVICE_PLAY_ON_SONOS, @@ -97,7 +106,7 @@ async def test_sonos_playback(hass, mock_plex_server): with patch.object( hass.components.sonos, "get_coordinator_name", - return_value="media_player.sonos_kitchen", + return_value="Speaker 2", ): assert await hass.services.async_call( DOMAIN, @@ -116,7 +125,7 @@ async def test_sonos_playback(hass, mock_plex_server): ), patch.object( hass.components.sonos, "get_coordinator_name", - return_value="media_player.sonos_kitchen", + return_value="Speaker 2", ), patch( "plexapi.playqueue.PlayQueue.create" ): @@ -132,7 +141,17 @@ async def test_sonos_playback(hass, mock_plex_server): ) -async def test_playback_multiple_servers(hass, mock_websocket, setup_plex_server): +async def test_playback_multiple_servers( + hass, + setup_plex_server, + requests_mock, + caplog, + empty_payload, + playqueue_created, + plex_server_accounts, + plex_server_base, + sonos_resources, +): """Test playing media when multiple servers available.""" secondary_entry = MockConfigEntry( domain=DOMAIN, @@ -141,21 +160,60 @@ async def test_playback_multiple_servers(hass, mock_websocket, setup_plex_server unique_id=SECONDARY_DATA["server_id"], ) + secondary_url = SECONDARY_DATA[PLEX_SERVER_CONFIG][CONF_URL] + secondary_name = SECONDARY_DATA[CONF_SERVER] + secondary_id = SECONDARY_DATA[CONF_SERVER_IDENTIFIER] + requests_mock.get( + secondary_url, + text=plex_server_base.format( + name=secondary_name, machine_identifier=secondary_id + ), + ) + requests_mock.get(f"{secondary_url}/accounts", text=plex_server_accounts) + requests_mock.get(f"{secondary_url}/clients", text=empty_payload) + requests_mock.get(f"{secondary_url}/status/sessions", text=empty_payload) + await setup_plex_server() await setup_plex_server(config_entry=secondary_entry) + requests_mock.get("https://sonos.plex.tv/resources", text=sonos_resources) + requests_mock.get( + "https://sonos.plex.tv/player/playback/playMedia", status_code=200 + ) + requests_mock.post("/playqueues", text=playqueue_created) + with patch.object( hass.components.sonos, "get_coordinator_name", - return_value="media_player.sonos_kitchen", - ), patch("plexapi.playqueue.PlayQueue.create"): + return_value="Speaker 2", + ): assert await hass.services.async_call( DOMAIN, SERVICE_PLAY_ON_SONOS, { ATTR_ENTITY_ID: "media_player.sonos_kitchen", ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, - ATTR_MEDIA_CONTENT_ID: f'{{"plex_server": "{SECONDARY_DATA[CONF_SERVER]}", "library_name": "Music", "artist_name": "Artist", "album_name": "Album"}}', + ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album"}', + }, + True, + ) + + assert ( + "Multiple Plex servers configured, choose with 'plex_server' key" in caplog.text + ) + + with patch.object( + hass.components.sonos, + "get_coordinator_name", + return_value="Speaker 2", + ): + assert await hass.services.async_call( + DOMAIN, + SERVICE_PLAY_ON_SONOS, + { + ATTR_ENTITY_ID: "media_player.sonos_kitchen", + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_ID: f'{{"plex_server": "{MOCK_SERVERS[0][CONF_SERVER]}", "library_name": "Music", "artist_name": "Artist", "album_name": "Album"}}', }, True, ) diff --git a/tests/components/plex/test_server.py b/tests/components/plex/test_server.py index 2f8619834df..f9b34088601 100644 --- a/tests/components/plex/test_server.py +++ b/tests/components/plex/test_server.py @@ -28,29 +28,18 @@ from homeassistant.const import ATTR_ENTITY_ID from .const import DEFAULT_DATA, DEFAULT_OPTIONS from .helpers import trigger_plex_update, wait_for_debouncer -from .mock_classes import ( - MockPlexAccount, - MockPlexAlbum, - MockPlexArtist, - MockPlexLibrary, - MockPlexLibrarySection, - MockPlexMediaItem, - MockPlexSeason, - MockPlexServer, - MockPlexShow, -) -async def test_new_users_available(hass, entry, mock_websocket, setup_plex_server): +async def test_new_users_available(hass, entry, setup_plex_server): """Test setting up when new users available on Plex server.""" - MONITORED_USERS = {"Owner": {"enabled": True}} + MONITORED_USERS = {"User 1": {"enabled": True}} OPTIONS_WITH_USERS = copy.deepcopy(DEFAULT_OPTIONS) OPTIONS_WITH_USERS[MP_DOMAIN][CONF_MONITORED_USERS] = MONITORED_USERS entry.options = OPTIONS_WITH_USERS mock_plex_server = await setup_plex_server(config_entry=entry) - server_id = mock_plex_server.machineIdentifier + server_id = mock_plex_server.machine_identifier monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users @@ -58,17 +47,18 @@ async def test_new_users_available(hass, entry, mock_websocket, setup_plex_serve 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)) - async def test_new_ignored_users_available( - hass, caplog, entry, mock_websocket, setup_plex_server + hass, + caplog, + entry, + mock_websocket, + setup_plex_server, + requests_mock, + session_new_user, ): """Test setting up when new users available on Plex server but are ignored.""" - MONITORED_USERS = {"Owner": {"enabled": True}} + MONITORED_USERS = {"User 1": {"enabled": True}} OPTIONS_WITH_USERS = copy.deepcopy(DEFAULT_OPTIONS) OPTIONS_WITH_USERS[MP_DOMAIN][CONF_MONITORED_USERS] = MONITORED_USERS OPTIONS_WITH_USERS[MP_DOMAIN][CONF_IGNORE_NEW_SHARED_USERS] = True @@ -76,43 +66,50 @@ async def test_new_ignored_users_available( mock_plex_server = await setup_plex_server(config_entry=entry) - server_id = mock_plex_server.machineIdentifier + requests_mock.get( + f"{mock_plex_server.url_in_use}/status/sessions", + text=session_new_user, + ) + trigger_plex_update(mock_websocket) + await wait_for_debouncer(hass) + server_id = mock_plex_server.machine_identifier + + active_sessions = mock_plex_server._plex_server.sessions() 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] + assert len(monitored_users) == 1 assert len(ignored_users) == 2 + for ignored_user in ignored_users: ignored_client = [ - x.players[0] - for x in mock_plex_server.sessions() - if x.usernames[0] == ignored_user - ][0] - assert ( - f"Ignoring {ignored_client.product} client owned by '{ignored_user}'" - in caplog.text - ) + x.players[0] for x in active_sessions if x.usernames[0] == ignored_user + ] + if ignored_client: + assert ( + f"Ignoring {ignored_client[0].product} client owned by '{ignored_user}'" + 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)) + assert sensor.state == str(len(active_sessions)) -async def test_network_error_during_refresh( - hass, caplog, mock_plex_server, mock_websocket -): +async def test_network_error_during_refresh(hass, caplog, mock_plex_server): """Test network failures during refreshes.""" - server_id = mock_plex_server.machineIdentifier + server_id = mock_plex_server.machine_identifier loaded_server = hass.data[DOMAIN][SERVERS][server_id] + active_sessions = mock_plex_server._plex_server.sessions() 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 == str(len(active_sessions)) - with patch.object(mock_plex_server, "clients", side_effect=RequestException): + with patch("plexapi.server.PlexServer.clients", side_effect=RequestException): await loaded_server._async_update_platforms() await hass.async_block_till_done() @@ -129,25 +126,31 @@ async def test_gdm_client_failure(hass, mock_websocket, setup_plex_server): mock_plex_server = await setup_plex_server(disable_gdm=False) await hass.async_block_till_done() + active_sessions = mock_plex_server._plex_server.sessions() 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 == str(len(active_sessions)) - with patch.object(mock_plex_server, "clients", side_effect=RequestException): + with patch("plexapi.server.PlexServer.clients", side_effect=RequestException): trigger_plex_update(mock_websocket) await hass.async_block_till_done() -async def test_mark_sessions_idle(hass, mock_plex_server, mock_websocket): +async def test_mark_sessions_idle( + hass, mock_plex_server, mock_websocket, requests_mock, empty_payload +): """Test marking media_players as idle when sessions end.""" await wait_for_debouncer(hass) - sensor = hass.states.get("sensor.plex_plex_server_1") - assert sensor.state == str(len(mock_plex_server.accounts)) + active_sessions = mock_plex_server._plex_server.sessions() - mock_plex_server.clear_clients() - mock_plex_server.clear_sessions() + sensor = hass.states.get("sensor.plex_plex_server_1") + assert sensor.state == str(len(active_sessions)) + + url = mock_plex_server.url_in_use + requests_mock.get(f"{url}/clients", text=empty_payload) + requests_mock.get(f"{url}/status/sessions", text=empty_payload) trigger_plex_update(mock_websocket) await hass.async_block_till_done() @@ -157,43 +160,46 @@ async def test_mark_sessions_idle(hass, mock_plex_server, mock_websocket): assert sensor.state == "0" -async def test_ignore_plex_web_client(hass, entry, mock_websocket, setup_plex_server): +async def test_ignore_plex_web_client(hass, entry, 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 - 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) + mock_plex_server = await setup_plex_server( + config_entry=entry, client_type="plexweb", disable_clients=True + ) + await wait_for_debouncer(hass) + active_sessions = mock_plex_server._plex_server.sessions() sensor = hass.states.get("sensor.plex_plex_server_1") - assert sensor.state == str(len(mock_plex_server.accounts)) + assert sensor.state == str(len(active_sessions)) media_players = hass.states.async_entity_ids("media_player") assert len(media_players) == int(sensor.state) - 1 -async def test_media_lookups(hass, mock_plex_server, mock_websocket): +async def test_media_lookups(hass, mock_plex_server, requests_mock, playqueue_created): """Test media lookups to Plex server.""" - server_id = mock_plex_server.machineIdentifier + server_id = mock_plex_server.machine_identifier loaded_server = hass.data[DOMAIN][SERVERS][server_id] # Plex Key searches media_player_id = hass.states.async_entity_ids("media_player")[0] - with patch("homeassistant.components.plex.PlexServer.create_playqueue"): - assert await hass.services.async_call( - MP_DOMAIN, - SERVICE_PLAY_MEDIA, - { - ATTR_ENTITY_ID: media_player_id, - ATTR_MEDIA_CONTENT_TYPE: DOMAIN, - ATTR_MEDIA_CONTENT_ID: 123, - }, - True, - ) - with patch.object(MockPlexServer, "fetchItem", side_effect=NotFound): + requests_mock.post("/playqueues", text=playqueue_created) + requests_mock.get("/player/playback/playMedia", status_code=200) + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player_id, + ATTR_MEDIA_CONTENT_TYPE: DOMAIN, + ATTR_MEDIA_CONTENT_ID: 1, + }, + True, + ) + with patch("plexapi.server.PlexServer.fetchItem", side_effect=NotFound): assert await hass.services.async_call( MP_DOMAIN, SERVICE_PLAY_MEDIA, @@ -206,20 +212,18 @@ async def test_media_lookups(hass, mock_plex_server, mock_websocket): ) # TV show searches - with patch.object(MockPlexLibrary, "section", side_effect=NotFound): - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_EPISODE, library_name="Not a Library", show_name="TV Show" - ) - is None + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_EPISODE, library_name="Not a Library", show_name="TV Show" ) - with patch.object(MockPlexLibrarySection, "get", side_effect=NotFound): - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_EPISODE, library_name="TV Shows", show_name="Not a TV Show" - ) - is None + is None + ) + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_EPISODE, library_name="TV Shows", show_name="Not a TV Show" ) + is None + ) assert ( loaded_server.lookup_media( MEDIA_TYPE_EPISODE, library_name="TV Shows", episode_name="An Episode" @@ -233,36 +237,34 @@ async def test_media_lookups(hass, mock_plex_server, mock_websocket): MEDIA_TYPE_EPISODE, library_name="TV Shows", show_name="TV Show", - season_number=2, + season_number=1, ) assert loaded_server.lookup_media( MEDIA_TYPE_EPISODE, library_name="TV Shows", show_name="TV Show", - season_number=2, + season_number=1, episode_number=3, ) - with patch.object(MockPlexShow, "season", side_effect=NotFound): - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_EPISODE, - library_name="TV Shows", - show_name="TV Show", - season_number=2, - ) - is None + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_EPISODE, + library_name="TV Shows", + show_name="TV Show", + season_number=2, ) - with patch.object(MockPlexSeason, "episode", side_effect=NotFound): - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_EPISODE, - library_name="TV Shows", - show_name="TV Show", - season_number=2, - episode_number=1, - ) - is None + is None + ) + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_EPISODE, + library_name="TV Shows", + show_name="TV Show", + season_number=2, + episode_number=1, ) + is None + ) # Music searches assert ( @@ -286,47 +288,43 @@ async def test_media_lookups(hass, mock_plex_server, mock_websocket): artist_name="Artist", album_name="Album", ) - with patch.object(MockPlexLibrarySection, "get", side_effect=NotFound): - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_MUSIC, - library_name="Music", - artist_name="Not an Artist", - album_name="Album", - ) - is None + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_MUSIC, + library_name="Music", + artist_name="Not an Artist", + album_name="Album", ) - with patch.object(MockPlexArtist, "album", side_effect=NotFound): - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_MUSIC, - library_name="Music", - artist_name="Artist", - album_name="Not an Album", - ) - is None + is None + ) + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_MUSIC, + library_name="Music", + artist_name="Artist", + album_name="Not an Album", ) - with patch.object(MockPlexAlbum, "track", side_effect=NotFound): - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_MUSIC, - library_name="Music", - artist_name="Artist", - album_name=" Album", - track_name="Not a Track", - ) - is None + is None + ) + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_MUSIC, + library_name="Music", + artist_name="Artist", + album_name=" Album", + track_name="Not a Track", ) - with patch.object(MockPlexArtist, "get", side_effect=NotFound): - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_MUSIC, - library_name="Music", - artist_name="Artist", - track_name="Not a Track", - ) - is None + is None + ) + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_MUSIC, + library_name="Music", + artist_name="Artist", + track_name="Not a Track", ) + is None + ) assert loaded_server.lookup_media( MEDIA_TYPE_MUSIC, library_name="Music", @@ -353,44 +351,33 @@ async def test_media_lookups(hass, mock_plex_server, mock_websocket): ) # Playlist searches - assert loaded_server.lookup_media(MEDIA_TYPE_PLAYLIST, playlist_name="A Playlist") + assert loaded_server.lookup_media(MEDIA_TYPE_PLAYLIST, playlist_name="Playlist 1") assert loaded_server.lookup_media(MEDIA_TYPE_PLAYLIST) is None - with patch.object(MockPlexServer, "playlist", side_effect=NotFound): - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_PLAYLIST, playlist_name="Not a Playlist" - ) - is None - ) + assert ( + loaded_server.lookup_media(MEDIA_TYPE_PLAYLIST, playlist_name="Not a Playlist") + is None + ) # Legacy Movie searches assert loaded_server.lookup_media(MEDIA_TYPE_VIDEO, video_name="Movie") is None assert loaded_server.lookup_media(MEDIA_TYPE_VIDEO, library_name="Movies") is None assert loaded_server.lookup_media( - MEDIA_TYPE_VIDEO, library_name="Movies", video_name="Movie" + MEDIA_TYPE_VIDEO, library_name="Movies", video_name="Movie 1" ) - with patch.object(MockPlexLibrarySection, "get", side_effect=NotFound): - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_VIDEO, library_name="Movies", video_name="Not a Movie" - ) - is None + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_VIDEO, library_name="Movies", video_name="Not a Movie" ) + is None + ) # Movie searches assert loaded_server.lookup_media(MEDIA_TYPE_MOVIE, title="Movie") is None assert loaded_server.lookup_media(MEDIA_TYPE_MOVIE, library_name="Movies") is None assert loaded_server.lookup_media( - MEDIA_TYPE_MOVIE, library_name="Movies", title="Movie" + MEDIA_TYPE_MOVIE, library_name="Movies", title="Movie 1" ) - with patch.object(MockPlexLibrarySection, "search", side_effect=BadRequest): - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_MOVIE, library_name="Movies", title="Not a Movie" - ) - is None - ) - with patch.object(MockPlexLibrarySection, "search", return_value=[]): + with patch("plexapi.library.LibrarySection.search", side_effect=BadRequest): assert ( loaded_server.lookup_media( MEDIA_TYPE_MOVIE, library_name="Movies", title="Not a Movie" @@ -398,25 +385,8 @@ async def test_media_lookups(hass, mock_plex_server, mock_websocket): is None ) - similar_movies = [] - for title in "Duplicate Movie", "Duplicate Movie 2": - similar_movies.append(MockPlexMediaItem(title)) - with patch.object( - loaded_server.library.section("Movies"), "search", return_value=similar_movies - ): - found_media = loaded_server.lookup_media( - MEDIA_TYPE_MOVIE, library_name="Movies", title="Duplicate Movie" + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_MOVIE, library_name="Movies", title="Movie" ) - assert found_media.title == "Duplicate Movie" - - duplicate_movies = [] - for title in "Duplicate Movie - Original", "Duplicate Movie - Remake": - duplicate_movies.append(MockPlexMediaItem(title)) - with patch.object( - loaded_server.library.section("Movies"), "search", return_value=duplicate_movies - ): - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_MOVIE, library_name="Movies", title="Duplicate Movie" - ) - ) is None + ) is None diff --git a/tests/components/plex/test_services.py b/tests/components/plex/test_services.py index 06654a736c7..18375a9f80f 100644 --- a/tests/components/plex/test_services.py +++ b/tests/components/plex/test_services.py @@ -1,6 +1,4 @@ """Tests for various Plex services.""" -from unittest.mock import patch - from homeassistant.components.plex.const import ( CONF_SERVER, CONF_SERVER_IDENTIFIER, @@ -9,77 +7,84 @@ from homeassistant.components.plex.const import ( SERVICE_REFRESH_LIBRARY, SERVICE_SCAN_CLIENTS, ) -from homeassistant.const import ( - CONF_HOST, - CONF_PORT, - CONF_TOKEN, - CONF_URL, - CONF_VERIFY_SSL, -) +from homeassistant.const import CONF_URL -from .const import MOCK_SERVERS, MOCK_TOKEN -from .mock_classes import MockPlexLibrarySection +from .const import DEFAULT_OPTIONS, SECONDARY_DATA from tests.common import MockConfigEntry -async def test_refresh_library(hass, mock_plex_server, setup_plex_server): +async def test_refresh_library( + hass, + mock_plex_server, + setup_plex_server, + requests_mock, + empty_payload, + plex_server_accounts, + plex_server_base, +): """Test refresh_library service call.""" + url = mock_plex_server.url_in_use + refresh = requests_mock.get(f"{url}/library/sections/1/refresh", status_code=200) + # Test with non-existent server - with patch.object(MockPlexLibrarySection, "update") as mock_update: - assert await hass.services.async_call( - DOMAIN, - SERVICE_REFRESH_LIBRARY, - {"server_name": "Not a Server", "library_name": "Movies"}, - True, - ) - assert not mock_update.called + assert await hass.services.async_call( + DOMAIN, + SERVICE_REFRESH_LIBRARY, + {"server_name": "Not a Server", "library_name": "Movies"}, + True, + ) + assert not refresh.called # Test with non-existent library - with patch.object(MockPlexLibrarySection, "update") as mock_update: - assert await hass.services.async_call( - DOMAIN, - SERVICE_REFRESH_LIBRARY, - {"library_name": "Not a Library"}, - True, - ) - assert not mock_update.called + assert await hass.services.async_call( + DOMAIN, + SERVICE_REFRESH_LIBRARY, + {"library_name": "Not a Library"}, + True, + ) + assert not refresh.called # Test with valid library - with patch.object(MockPlexLibrarySection, "update") as mock_update: - assert await hass.services.async_call( - DOMAIN, - SERVICE_REFRESH_LIBRARY, - {"library_name": "Movies"}, - True, - ) - assert mock_update.called + assert await hass.services.async_call( + DOMAIN, + SERVICE_REFRESH_LIBRARY, + {"library_name": "Movies"}, + True, + ) + assert refresh.call_count == 1 # Add a second configured server + secondary_url = SECONDARY_DATA[PLEX_SERVER_CONFIG][CONF_URL] + secondary_name = SECONDARY_DATA[CONF_SERVER] + secondary_id = SECONDARY_DATA[CONF_SERVER_IDENTIFIER] + requests_mock.get( + secondary_url, + text=plex_server_base.format( + name=secondary_name, machine_identifier=secondary_id + ), + ) + requests_mock.get(f"{secondary_url}/accounts", text=plex_server_accounts) + requests_mock.get(f"{secondary_url}/clients", text=empty_payload) + requests_mock.get(f"{secondary_url}/status/sessions", text=empty_payload) + entry_2 = MockConfigEntry( domain=DOMAIN, - data={ - CONF_SERVER: MOCK_SERVERS[1][CONF_SERVER], - PLEX_SERVER_CONFIG: { - CONF_TOKEN: MOCK_TOKEN, - CONF_URL: f"https://{MOCK_SERVERS[1][CONF_HOST]}:{MOCK_SERVERS[1][CONF_PORT]}", - CONF_VERIFY_SSL: True, - }, - CONF_SERVER_IDENTIFIER: MOCK_SERVERS[1][CONF_SERVER_IDENTIFIER], - }, + data=SECONDARY_DATA, + options=DEFAULT_OPTIONS, + unique_id=SECONDARY_DATA["server_id"], ) await setup_plex_server(config_entry=entry_2) # Test multiple servers available but none specified - with patch.object(MockPlexLibrarySection, "update") as mock_update: - assert await hass.services.async_call( - DOMAIN, - SERVICE_REFRESH_LIBRARY, - {"library_name": "Movies"}, - True, - ) - assert not mock_update.called + assert await hass.services.async_call( + DOMAIN, + SERVICE_REFRESH_LIBRARY, + {"library_name": "Movies"}, + True, + ) + assert refresh.call_count == 1 async def test_scan_clients(hass, mock_plex_server): diff --git a/tests/fixtures/plex/album.xml b/tests/fixtures/plex/album.xml new file mode 100644 index 00000000000..380149cf5ac --- /dev/null +++ b/tests/fixtures/plex/album.xml @@ -0,0 +1,4 @@ + + + + diff --git a/tests/fixtures/plex/artist_albums.xml b/tests/fixtures/plex/artist_albums.xml new file mode 100644 index 00000000000..b1c8d1afb89 --- /dev/null +++ b/tests/fixtures/plex/artist_albums.xml @@ -0,0 +1,4 @@ + + + + diff --git a/tests/fixtures/plex/children_20.xml b/tests/fixtures/plex/children_20.xml new file mode 100644 index 00000000000..6f433fff9a8 --- /dev/null +++ b/tests/fixtures/plex/children_20.xml @@ -0,0 +1,11 @@ + diff --git a/tests/fixtures/plex/children_200.xml b/tests/fixtures/plex/children_200.xml new file mode 100644 index 00000000000..e1ff4934651 --- /dev/null +++ b/tests/fixtures/plex/children_200.xml @@ -0,0 +1 @@ + diff --git a/tests/fixtures/plex/children_30.xml b/tests/fixtures/plex/children_30.xml new file mode 100644 index 00000000000..bf87607f0b0 --- /dev/null +++ b/tests/fixtures/plex/children_30.xml @@ -0,0 +1,4 @@ + + + + diff --git a/tests/fixtures/plex/children_300.xml b/tests/fixtures/plex/children_300.xml new file mode 100644 index 00000000000..b1c8d1afb89 --- /dev/null +++ b/tests/fixtures/plex/children_300.xml @@ -0,0 +1,4 @@ + + + + diff --git a/tests/fixtures/plex/empty_library.xml b/tests/fixtures/plex/empty_library.xml new file mode 100644 index 00000000000..853d3b9791f --- /dev/null +++ b/tests/fixtures/plex/empty_library.xml @@ -0,0 +1 @@ + diff --git a/tests/fixtures/plex/empty_payload.xml b/tests/fixtures/plex/empty_payload.xml new file mode 100644 index 00000000000..89bcdba2d58 --- /dev/null +++ b/tests/fixtures/plex/empty_payload.xml @@ -0,0 +1 @@ + diff --git a/tests/fixtures/plex/grandchildren_300.xml b/tests/fixtures/plex/grandchildren_300.xml new file mode 100644 index 00000000000..2c9741e2c1b --- /dev/null +++ b/tests/fixtures/plex/grandchildren_300.xml @@ -0,0 +1 @@ + diff --git a/tests/fixtures/plex/library.xml b/tests/fixtures/plex/library.xml new file mode 100644 index 00000000000..4d6ec69990b --- /dev/null +++ b/tests/fixtures/plex/library.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/tests/fixtures/plex/library_movies_all.xml b/tests/fixtures/plex/library_movies_all.xml new file mode 100644 index 00000000000..cd194040b37 --- /dev/null +++ b/tests/fixtures/plex/library_movies_all.xml @@ -0,0 +1,51 @@ + diff --git a/tests/fixtures/plex/library_movies_sort.xml b/tests/fixtures/plex/library_movies_sort.xml new file mode 100644 index 00000000000..052eac3590a --- /dev/null +++ b/tests/fixtures/plex/library_movies_sort.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/tests/fixtures/plex/library_music_all.xml b/tests/fixtures/plex/library_music_all.xml new file mode 100644 index 00000000000..6676817780d --- /dev/null +++ b/tests/fixtures/plex/library_music_all.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/tests/fixtures/plex/library_music_sort.xml b/tests/fixtures/plex/library_music_sort.xml new file mode 100644 index 00000000000..3a516a2a2f8 --- /dev/null +++ b/tests/fixtures/plex/library_music_sort.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/tests/fixtures/plex/library_sections.xml b/tests/fixtures/plex/library_sections.xml new file mode 100644 index 00000000000..954af4b6928 --- /dev/null +++ b/tests/fixtures/plex/library_sections.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/tests/fixtures/plex/library_tvshows_all.xml b/tests/fixtures/plex/library_tvshows_all.xml new file mode 100644 index 00000000000..e734d396ca2 --- /dev/null +++ b/tests/fixtures/plex/library_tvshows_all.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/tests/fixtures/plex/library_tvshows_sort.xml b/tests/fixtures/plex/library_tvshows_sort.xml new file mode 100644 index 00000000000..63df4738a24 --- /dev/null +++ b/tests/fixtures/plex/library_tvshows_sort.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/tests/fixtures/plex/media_1.xml b/tests/fixtures/plex/media_1.xml new file mode 100644 index 00000000000..838afb2959c --- /dev/null +++ b/tests/fixtures/plex/media_1.xml @@ -0,0 +1,11 @@ + diff --git a/tests/fixtures/plex/media_100.xml b/tests/fixtures/plex/media_100.xml new file mode 100644 index 00000000000..e1326a4c862 --- /dev/null +++ b/tests/fixtures/plex/media_100.xml @@ -0,0 +1 @@ + diff --git a/tests/fixtures/plex/media_200.xml b/tests/fixtures/plex/media_200.xml new file mode 100644 index 00000000000..380149cf5ac --- /dev/null +++ b/tests/fixtures/plex/media_200.xml @@ -0,0 +1,4 @@ + + + + diff --git a/tests/fixtures/plex/media_30.xml b/tests/fixtures/plex/media_30.xml new file mode 100644 index 00000000000..14a69adc0c7 --- /dev/null +++ b/tests/fixtures/plex/media_30.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/tests/fixtures/plex/player_plexweb_resources.xml b/tests/fixtures/plex/player_plexweb_resources.xml new file mode 100644 index 00000000000..f3a2e31335a --- /dev/null +++ b/tests/fixtures/plex/player_plexweb_resources.xml @@ -0,0 +1 @@ + diff --git a/tests/fixtures/plex/playlist_500.xml b/tests/fixtures/plex/playlist_500.xml new file mode 100644 index 00000000000..d1d008549e8 --- /dev/null +++ b/tests/fixtures/plex/playlist_500.xml @@ -0,0 +1,11 @@ + diff --git a/tests/fixtures/plex/playlists.xml b/tests/fixtures/plex/playlists.xml new file mode 100644 index 00000000000..bc0dd69905e --- /dev/null +++ b/tests/fixtures/plex/playlists.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/tests/fixtures/plex/playqueue_created.xml b/tests/fixtures/plex/playqueue_created.xml new file mode 100644 index 00000000000..72a274ca7b9 --- /dev/null +++ b/tests/fixtures/plex/playqueue_created.xml @@ -0,0 +1 @@ + diff --git a/tests/fixtures/plex/plex_server_accounts.xml b/tests/fixtures/plex/plex_server_accounts.xml new file mode 100644 index 00000000000..22b92d89c4a --- /dev/null +++ b/tests/fixtures/plex/plex_server_accounts.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/tests/fixtures/plex/plex_server_base.xml b/tests/fixtures/plex/plex_server_base.xml new file mode 100644 index 00000000000..da983d2f356 --- /dev/null +++ b/tests/fixtures/plex/plex_server_base.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/fixtures/plex/plex_server_clients.xml b/tests/fixtures/plex/plex_server_clients.xml new file mode 100644 index 00000000000..c7f6180e9c3 --- /dev/null +++ b/tests/fixtures/plex/plex_server_clients.xml @@ -0,0 +1,3 @@ + + + diff --git a/tests/fixtures/plex/plextv_account.xml b/tests/fixtures/plex/plextv_account.xml new file mode 100644 index 00000000000..32d6eec7c2d --- /dev/null +++ b/tests/fixtures/plex/plextv_account.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + testuser + testuser@email.com + 2000-01-01 12:34:56 UTC + faketoken + diff --git a/tests/fixtures/plex/plextv_resources_base.xml b/tests/fixtures/plex/plextv_resources_base.xml new file mode 100644 index 00000000000..41e61711d36 --- /dev/null +++ b/tests/fixtures/plex/plextv_resources_base.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/fixtures/plex/security_token.xml b/tests/fixtures/plex/security_token.xml new file mode 100644 index 00000000000..1d7bde66fa6 --- /dev/null +++ b/tests/fixtures/plex/security_token.xml @@ -0,0 +1 @@ + diff --git a/tests/fixtures/plex/session_base.xml b/tests/fixtures/plex/session_base.xml new file mode 100644 index 00000000000..e7451e93af4 --- /dev/null +++ b/tests/fixtures/plex/session_base.xml @@ -0,0 +1,11 @@ + diff --git a/tests/fixtures/plex/session_photo.xml b/tests/fixtures/plex/session_photo.xml new file mode 100644 index 00000000000..952875e525e --- /dev/null +++ b/tests/fixtures/plex/session_photo.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/tests/fixtures/plex/session_plexweb.xml b/tests/fixtures/plex/session_plexweb.xml new file mode 100644 index 00000000000..40597d7b701 --- /dev/null +++ b/tests/fixtures/plex/session_plexweb.xml @@ -0,0 +1,11 @@ + diff --git a/tests/fixtures/plex/show_seasons.xml b/tests/fixtures/plex/show_seasons.xml new file mode 100644 index 00000000000..bf87607f0b0 --- /dev/null +++ b/tests/fixtures/plex/show_seasons.xml @@ -0,0 +1,4 @@ + + + + diff --git a/tests/fixtures/plex/sonos_resources.xml b/tests/fixtures/plex/sonos_resources.xml new file mode 100644 index 00000000000..334fdd311ef --- /dev/null +++ b/tests/fixtures/plex/sonos_resources.xml @@ -0,0 +1,5 @@ + + + + + From 20e2493f68b3cd00b327c12f03f381773bac5520 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 8 Jan 2021 03:38:57 +0200 Subject: [PATCH 125/507] Improve device registry type hints (#44919) * Fix async_get_or_create via_device type hint * Specify collection element types --- homeassistant/helpers/device_registry.py | 39 +++++++++++++++--------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 4415babc009..bbe4131b758 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -13,6 +13,8 @@ from .debounce import Debouncer from .singleton import singleton from .typing import UNDEFINED, HomeAssistantType, UndefinedType +# mypy: disallow_any_generics + if TYPE_CHECKING: from . import entity_registry @@ -124,7 +126,7 @@ class DeviceRegistry: devices: Dict[str, DeviceEntry] deleted_devices: Dict[str, DeletedDeviceEntry] - _devices_index: Dict[str, Dict[str, Dict[str, str]]] + _devices_index: Dict[str, Dict[str, Dict[Tuple[str, str], str]]] def __init__(self, hass: HomeAssistantType) -> None: """Initialize the device registry.""" @@ -140,8 +142,8 @@ class DeviceRegistry: @callback def async_get_device( self, - identifiers: set, - connections: Optional[set] = None, + identifiers: Set[Tuple[str, str]], + connections: Optional[Set[Tuple[str, str]]] = None, ) -> Optional[DeviceEntry]: """Check if device is registered.""" device_id = self._async_get_device_id_from_index( @@ -152,7 +154,9 @@ class DeviceRegistry: return self.devices[device_id] def _async_get_deleted_device( - self, identifiers: set, connections: Optional[set] + self, + identifiers: Set[Tuple[str, str]], + connections: Optional[Set[Tuple[str, str]]], ) -> Optional[DeletedDeviceEntry]: """Check if device is deleted.""" device_id = self._async_get_device_id_from_index( @@ -163,7 +167,10 @@ class DeviceRegistry: return self.deleted_devices[device_id] def _async_get_device_id_from_index( - self, index: str, identifiers: set, connections: Optional[set] + self, + index: str, + identifiers: Set[Tuple[str, str]], + connections: Optional[Set[Tuple[str, str]]], ) -> Optional[str]: """Check if device has previously been registered.""" devices_index = self._devices_index[index] @@ -227,8 +234,8 @@ class DeviceRegistry: self, *, config_entry_id: str, - connections: Optional[set] = None, - identifiers: Optional[set] = None, + connections: Optional[Set[Tuple[str, str]]] = None, + identifiers: Optional[Set[Tuple[str, str]]] = None, manufacturer: Union[str, None, UndefinedType] = UNDEFINED, model: Union[str, None, UndefinedType] = UNDEFINED, name: Union[str, None, UndefinedType] = UNDEFINED, @@ -237,7 +244,7 @@ class DeviceRegistry: default_name: Union[str, None, UndefinedType] = UNDEFINED, sw_version: Union[str, None, UndefinedType] = UNDEFINED, entry_type: Union[str, None, UndefinedType] = UNDEFINED, - via_device: Optional[str] = None, + via_device: Optional[Tuple[str, str]] = None, # To disable a device if it gets created disabled_by: Union[str, None, UndefinedType] = UNDEFINED, ) -> Optional[DeviceEntry]: @@ -305,7 +312,7 @@ class DeviceRegistry: model: Union[str, None, UndefinedType] = UNDEFINED, name: Union[str, None, UndefinedType] = UNDEFINED, name_by_user: Union[str, None, UndefinedType] = UNDEFINED, - new_identifiers: Union[set, UndefinedType] = UNDEFINED, + new_identifiers: Union[Set[Tuple[str, str]], UndefinedType] = UNDEFINED, sw_version: Union[str, None, UndefinedType] = UNDEFINED, via_device_id: Union[str, None, UndefinedType] = UNDEFINED, remove_config_entry_id: Union[str, UndefinedType] = UNDEFINED, @@ -333,9 +340,9 @@ class DeviceRegistry: *, add_config_entry_id: Union[str, UndefinedType] = UNDEFINED, remove_config_entry_id: Union[str, UndefinedType] = UNDEFINED, - merge_connections: Union[set, UndefinedType] = UNDEFINED, - merge_identifiers: Union[set, UndefinedType] = UNDEFINED, - new_identifiers: Union[set, UndefinedType] = UNDEFINED, + merge_connections: Union[Set[Tuple[str, str]], UndefinedType] = UNDEFINED, + merge_identifiers: Union[Set[Tuple[str, str]], UndefinedType] = UNDEFINED, + new_identifiers: Union[Set[Tuple[str, str]], UndefinedType] = UNDEFINED, manufacturer: Union[str, None, UndefinedType] = UNDEFINED, model: Union[str, None, UndefinedType] = UNDEFINED, name: Union[str, None, UndefinedType] = UNDEFINED, @@ -657,7 +664,7 @@ def async_setup_cleanup(hass: HomeAssistantType, dev_reg: DeviceRegistry) -> Non hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, startup_clean) -def _normalize_connections(connections: set) -> set: +def _normalize_connections(connections: Set[Tuple[str, str]]) -> Set[Tuple[str, str]]: """Normalize connections to ensure we can match mac addresses.""" return { (key, format_mac(value)) if key == CONNECTION_NETWORK_MAC else (key, value) @@ -666,7 +673,8 @@ def _normalize_connections(connections: set) -> set: def _add_device_to_index( - devices_index: dict, device: Union[DeviceEntry, DeletedDeviceEntry] + devices_index: Dict[str, Dict[Tuple[str, str], str]], + device: Union[DeviceEntry, DeletedDeviceEntry], ) -> None: """Add a device to the index.""" for identifier in device.identifiers: @@ -676,7 +684,8 @@ def _add_device_to_index( def _remove_device_from_index( - devices_index: dict, device: Union[DeviceEntry, DeletedDeviceEntry] + devices_index: Dict[str, Dict[Tuple[str, str], str]], + device: Union[DeviceEntry, DeletedDeviceEntry], ) -> None: """Remove a device from the index.""" for identifier in device.identifiers: From cb3b37a87ab63ff543256918df17518edb3a9655 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Fri, 8 Jan 2021 02:44:00 +0100 Subject: [PATCH 126/507] Correct Plugwise sensor scaling (#44344) * Remove sensor-scaling, handled by the back-end * Correct assert-values * Update test-fixtures * Revert "Correct assert-values" This reverts commit f1a1891f73414f1b74482cf963cc800220b9196a. * Adapt value to the updated userdata set * Link to plugwise v0.8.5, update fixtures * Correct test-values * Fix typo --- homeassistant/components/plugwise/manifest.json | 2 +- homeassistant/components/plugwise/sensor.py | 10 ++-------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/plugwise/test_sensor.py | 8 ++++---- .../get_all_devices.json | 2 +- .../680423ff840043738f42cc7f1ff97a36.json | 2 +- .../6a3bf693d05e48e0b460c815a4fdd09d.json | 2 +- .../90986d591dcd426cae3ec3e8111ff730.json | 2 +- .../a2c3583e0a6349358998b760cea82d2a.json | 2 +- .../b310b72a0e354bfab43089919b9a88bf.json | 2 +- .../b59bcebaf94b499ea7d46e4a66fb62d8.json | 2 +- .../d3da73bde12a47d5a6b8f9dad971f2ec.json | 2 +- .../df4a4a8169904cdb9c03d61a21f42140.json | 2 +- .../e7693eb9582644e5b865dba8d4447cf1.json | 2 +- .../f1fee6043d3642a9b0a65297455f008e.json | 2 +- .../plugwise/anna_heatpump/get_all_devices.json | 2 +- .../1cbf783bb11e4a7c8a6843dee3a86927.json | 2 +- .../3cb70739631c4d17a86b8b12e8a5161b.json | 2 +- .../plugwise/p1v3_full_option/get_all_devices.json | 2 +- .../e950c7d5e1ee407a858e2a8b5016c8b3.json | 2 +- .../fixtures/plugwise/stretch_v31/get_all_devices.json | 2 +- .../5871317346d045bc9f6b987ef25ee638.json | 2 +- .../5ca521ac179d468e91d772eeeb8a2117.json | 1 + .../71e1944f2a944b26ad73323e399efef0.json | 1 + .../99f89d097be34fca88d8598c6dbc18ea.json | 1 + .../aac7b735042c4832ac9ff33aae4f453b.json | 2 +- .../cfe95cf3de1948c0b8955125bf754614.json | 2 +- .../d03738edfcc947f7b8f4573571d90d2d.json | 1 + .../d950b314e9d8499f968e6db8d82ef78c.json | 2 +- .../e1c884e7dede431dadee09506ec4f859.json | 2 +- .../e309b52ea5684cf1a22f30cf0cd15051.json | 1 + tests/fixtures/plugwise/stretch_v31/notifications.json | 1 + 33 files changed, 37 insertions(+), 37 deletions(-) create mode 100644 tests/fixtures/plugwise/stretch_v31/get_device_data/5ca521ac179d468e91d772eeeb8a2117.json create mode 100644 tests/fixtures/plugwise/stretch_v31/get_device_data/71e1944f2a944b26ad73323e399efef0.json create mode 100644 tests/fixtures/plugwise/stretch_v31/get_device_data/99f89d097be34fca88d8598c6dbc18ea.json create mode 100644 tests/fixtures/plugwise/stretch_v31/get_device_data/d03738edfcc947f7b8f4573571d90d2d.json create mode 100644 tests/fixtures/plugwise/stretch_v31/get_device_data/e309b52ea5684cf1a22f30cf0cd15051.json create mode 100644 tests/fixtures/plugwise/stretch_v31/notifications.json diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index 5a32341139c..998b84fe5d4 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -2,7 +2,7 @@ "domain": "plugwise", "name": "Plugwise", "documentation": "https://www.home-assistant.io/integrations/plugwise", - "requirements": ["plugwise==0.8.3"], + "requirements": ["plugwise==0.8.5"], "codeowners": ["@CoMPaTech", "@bouwew", "@brefra"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py index 578f1bc7f7e..f57ff2b2a91 100644 --- a/homeassistant/components/plugwise/sensor.py +++ b/homeassistant/components/plugwise/sensor.py @@ -305,10 +305,7 @@ class PwThermostatSensor(SmileSensor, Entity): return if data.get(self._sensor) is not None: - measurement = data[self._sensor] - if self._unit_of_measurement == PERCENTAGE: - measurement = int(measurement * 100) - self._state = measurement + self._state = data[self._sensor] self._icon = CUSTOM_ICONS.get(self._sensor, self._icon) self.async_write_ha_state() @@ -380,10 +377,7 @@ class PwPowerSensor(SmileSensor, Entity): return if data.get(self._sensor) is not None: - measurement = data[self._sensor] - if self._unit_of_measurement == ENERGY_KILO_WATT_HOUR: - measurement = round((measurement / 1000), 1) - self._state = measurement + self._state = data[self._sensor] self._icon = CUSTOM_ICONS.get(self._sensor, self._icon) self.async_write_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index 841bccf3f4e..198b9231f53 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1140,7 +1140,7 @@ plexauth==0.0.6 plexwebsocket==0.0.12 # homeassistant.components.plugwise -plugwise==0.8.3 +plugwise==0.8.5 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d3daba57fe7..b37bbddd67e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -566,7 +566,7 @@ plexauth==0.0.6 plexwebsocket==0.0.12 # homeassistant.components.plugwise -plugwise==0.8.3 +plugwise==0.8.5 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/tests/components/plugwise/test_sensor.py b/tests/components/plugwise/test_sensor.py index a722749496f..a2bf4ebc50e 100644 --- a/tests/components/plugwise/test_sensor.py +++ b/tests/components/plugwise/test_sensor.py @@ -65,13 +65,13 @@ async def test_p1_dsmr_sensor_entities(hass, mock_smile_p1): assert float(state.state) == -2761.0 state = hass.states.get("sensor.p1_electricity_consumed_off_peak_cumulative") - assert float(state.state) == 551.1 + assert float(state.state) == 551.09 state = hass.states.get("sensor.p1_electricity_produced_peak_point") assert float(state.state) == 2761.0 state = hass.states.get("sensor.p1_electricity_consumed_peak_cumulative") - assert float(state.state) == 442.9 + assert float(state.state) == 442.932 state = hass.states.get("sensor.p1_gas_consumed_cumulative") assert float(state.state) == 584.85 @@ -83,7 +83,7 @@ async def test_stretch_sensor_entities(hass, mock_stretch): assert entry.state == ENTRY_STATE_LOADED state = hass.states.get("sensor.koelkast_92c4a_electricity_consumed") - assert float(state.state) == 53.2 + assert float(state.state) == 50.5 state = hass.states.get("sensor.droger_52559_electricity_consumed_interval") - assert float(state.state) == 1.06 + assert float(state.state) == 0.0 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 bcaf40b4196..5a3492a3c6b 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": ["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 +{"df4a4a8169904cdb9c03d61a21f42140": {"name": "Zone Lisa Bios", "model": "Zone Thermostat", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "12493538af164a409c6a1c79e38afe1c"}, "b310b72a0e354bfab43089919b9a88bf": {"name": "Floor kraan", "model": "Thermostatic Radiator Valve", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "c50f167537524366a5af7aa3942feb1e"}, "a2c3583e0a6349358998b760cea82d2a": {"name": "Bios Cv Thermostatic Radiator ", "model": "Thermostatic Radiator Valve", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "12493538af164a409c6a1c79e38afe1c"}, "b59bcebaf94b499ea7d46e4a66fb62d8": {"name": "Zone Lisa WK", "model": "Zone Thermostat", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "c50f167537524366a5af7aa3942feb1e"}, "fe799307f1624099878210aa0b9f1475": {"name": "Adam", "model": "Smile Adam", "types": {"py/set": ["temperature", "thermostat", "home"]}, "class": "gateway", "location": "1f9dcf83fd4e4b66b72ff787957bfe5d"}, "d3da73bde12a47d5a6b8f9dad971f2ec": {"name": "Thermostatic Radiator Jessie", "model": "Thermostatic Radiator Valve", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "82fa13f017d240daa0d0ea1775420f24"}, "21f2b542c49845e6bb416884c55778d6": {"name": "Playstation Smart Plug", "model": "Plug", "types": {"py/set": ["plug", "power"]}, "class": "game_console", "location": "cd143c07248f491493cea0533bc3d669"}, "78d1126fc4c743db81b61c20e88342a7": {"name": "CV Pomp", "model": "Plug", "types": {"py/set": ["plug", "power"]}, "class": "central_heating_pump", "location": "c50f167537524366a5af7aa3942feb1e"}, "90986d591dcd426cae3ec3e8111ff730": {"name": "Adam", "model": "Heater Central", "types": {"py/set": ["temperature", "thermostat", "home"]}, "class": "heater_central", "location": "1f9dcf83fd4e4b66b72ff787957bfe5d"}, "cd0ddb54ef694e11ac18ed1cbce5dbbd": {"name": "NAS", "model": "Plug", "types": {"py/set": ["plug", "power"]}, "class": "vcr", "location": "cd143c07248f491493cea0533bc3d669"}, "4a810418d5394b3f82727340b91ba740": {"name": "USG Smart Plug", "model": "Plug", "types": {"py/set": ["plug", "power"]}, "class": "router", "location": "cd143c07248f491493cea0533bc3d669"}, "02cf28bfec924855854c544690a609ef": {"name": "NVR", "model": "Plug", "types": {"py/set": ["plug", "power"]}, "class": "vcr", "location": "cd143c07248f491493cea0533bc3d669"}, "a28f588dc4a049a483fd03a30361ad3a": {"name": "Fibaro HC2", "model": "Plug", "types": {"py/set": ["plug", "power"]}, "class": "settop", "location": "cd143c07248f491493cea0533bc3d669"}, "6a3bf693d05e48e0b460c815a4fdd09d": {"name": "Zone Thermostat Jessie", "model": "Zone Thermostat", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "82fa13f017d240daa0d0ea1775420f24"}, "680423ff840043738f42cc7f1ff97a36": {"name": "Thermostatic Radiator Badkamer", "model": "Thermostatic Radiator Valve", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "08963fec7c53423ca5680aa4cb502c63"}, "f1fee6043d3642a9b0a65297455f008e": {"name": "Zone Thermostat Badkamer", "model": "Zone Thermostat", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "08963fec7c53423ca5680aa4cb502c63"}, "675416a629f343c495449970e2ca37b5": {"name": "Ziggo Modem", "model": "Plug", "types": {"py/set": ["plug", "power"]}, "class": "router", "location": "cd143c07248f491493cea0533bc3d669"}, "e7693eb9582644e5b865dba8d4447cf1": {"name": "CV Kraan Garage", "model": "Thermostatic Radiator Valve", "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 6754cf63d2d..3ea0a92387b 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 @@ -{"temperature": 19.1, "setpoint": 14.0, "battery": 0.51, "temperature_difference": -0.4, "valve_position": 0.0} \ No newline at end of file +{"temperature": 19.1, "setpoint": 14.0, "battery": 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 14d596fb315..2d8ace6fa3f 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 @@ -{"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 +{"temperature": 17.2, "setpoint": 15.0, "battery": 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 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 862a3159754..d2f2f82bdf6 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, "heating_state": true} \ No newline at end of file +{"water_temperature": 70.0, "intended_boiler_temperature": 70.0, "modulation_level": 1, "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 c3e1a35b292..3f01f47fc5c 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 @@ -{"temperature": 17.2, "setpoint": 13.0, "battery": 0.62, "temperature_difference": -0.2, "valve_position": 0.0} \ No newline at end of file +{"temperature": 17.2, "setpoint": 13.0, "battery": 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 8478716dc7b..3a1c902932a 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 @@ -{"temperature": 26.0, "setpoint": 21.5, "temperature_difference": 3.5, "valve_position": 1.0} \ No newline at end of file +{"temperature": 26.0, "setpoint": 21.5, "temperature_difference": 3.5, "valve_position": 100} \ 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 6d1a8d135a4..2b314f589b6 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 @@ -{"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 +{"temperature": 20.9, "setpoint": 21.5, "battery": 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 b5a26000c7f..3e061593953 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 @@ -{"temperature": 17.1, "setpoint": 15.0, "battery": 0.62, "temperature_difference": 0.1, "valve_position": 0.0} \ No newline at end of file +{"temperature": 17.1, "setpoint": 15.0, "battery": 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 f27c382fc0b..88420a8a6bd 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 @@ -{"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 +{"temperature": 16.5, "setpoint": 13.0, "battery": 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 610c019b686..7e4532987b0 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 @@ -{"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 +{"temperature": 15.6, "setpoint": 5.5, "battery": 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 c4b5769e6d1..0d6e19967dc 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 @@ -{"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 +{"temperature": 18.9, "setpoint": 14.0, "battery": 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/anna_heatpump/get_all_devices.json b/tests/fixtures/plugwise/anna_heatpump/get_all_devices.json index 191f5b442b7..ea46cd68054 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": ["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 +{"1cbf783bb11e4a7c8a6843dee3a86927": {"name": "Anna", "model": "Heater Central", "types": {"py/set": ["temperature", "thermostat", "home"]}, "class": "heater_central", "location": "a57efe5f145f498c9be62a9b63626fbf"}, "015ae9ea3f964e668e490fa39da3870b": {"name": "Anna", "model": "Smile Anna", "types": {"py/set": ["temperature", "thermostat", "home"]}, "class": "gateway", "location": "a57efe5f145f498c9be62a9b63626fbf"}, "3cb70739631c4d17a86b8b12e8a5161b": {"name": "Anna", "model": "Thermostat", "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 ddf807303a2..604b9388969 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 @@ -{"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 +{"water_temperature": 29.1, "dhw_state": false, "intended_boiler_temperature": 0.0, "heating_state": false, "modulation_level": 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 3177880705b..048cc0f77dc 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 @@ -{"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 +{"temperature": 23.3, "setpoint": 21.0, "heating_state": false, "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/p1v3_full_option/get_all_devices.json b/tests/fixtures/plugwise/p1v3_full_option/get_all_devices.json index 1feb33dd630..a78f45ead8a 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": ["home", "power"]}, "class": "gateway", "location": "cd3e822288064775a7c4afcdd70bdda2"}} \ No newline at end of file +{"e950c7d5e1ee407a858e2a8b5016c8b3": {"name": "P1", "model": "Smile 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 fcbc1bbce33..eed9382a7e9 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.85, "gas_consumed_interval": 0.0} \ No newline at end of file +{"net_electricity_point": -2761, "electricity_consumed_peak_point": 0, "electricity_consumed_off_peak_point": 0, "net_electricity_cumulative": 442.972, "electricity_consumed_peak_cumulative": 442.932, "electricity_consumed_off_peak_cumulative": 551.09, "net_electricity_interval": 0, "electricity_consumed_peak_interval": 0, "electricity_consumed_off_peak_interval": 0, "electricity_produced_peak_point": 2761, "electricity_produced_off_peak_point": 0, "electricity_produced_peak_cumulative": 396.559, "electricity_produced_off_peak_cumulative": 154.491, "electricity_produced_peak_interval": 0, "electricity_produced_off_peak_interval": 0, "gas_consumed_cumulative": 584.85, "gas_consumed_interval": 0.0} \ No newline at end of file diff --git a/tests/fixtures/plugwise/stretch_v31/get_all_devices.json b/tests/fixtures/plugwise/stretch_v31/get_all_devices.json index f40e902d5a9..dab74fb74a2 100644 --- a/tests/fixtures/plugwise/stretch_v31/get_all_devices.json +++ b/tests/fixtures/plugwise/stretch_v31/get_all_devices.json @@ -1 +1 @@ -{"cfe95cf3de1948c0b8955125bf754614": {"name": "Droger (52559)", "types": {"py/set": ["plug", "power"]}, "class": "dryer", "location": 0}, "aac7b735042c4832ac9ff33aae4f453b": {"name": "Vaatwasser (2a1ab)", "types": {"py/set": ["plug", "power"]}, "class": "dishwasher", "location": 0}, "5871317346d045bc9f6b987ef25ee638": {"name": "Boiler (1EB31)", "types": {"py/set": ["plug", "power"]}, "class": "water_heater_vessel", "location": 0}, "059e4d03c7a34d278add5c7a4a781d19": {"name": "Wasmachine (52AC1)", "types": {"py/set": ["plug", "power"]}, "class": "washingmachine", "location": 0}, "e1c884e7dede431dadee09506ec4f859": {"name": "Koelkast (92C4A)", "types": {"py/set": ["plug", "power"]}, "class": "refrigerator", "location": 0}, "d950b314e9d8499f968e6db8d82ef78c": {"name": "Stroomvreters", "types": {"py/set": ["switch_group"]}, "class": "switching", "members": [], "location": null}} \ No newline at end of file +{"5ca521ac179d468e91d772eeeb8a2117": {"name": "Oven (793F84)", "model": "Circle", "types": {"py/set": ["plug", "power"]}, "class": "zz_misc", "location": 0}, "5871317346d045bc9f6b987ef25ee638": {"name": "Boiler (1EB31)", "model": "Circle", "types": {"py/set": ["plug", "power"]}, "class": "water_heater_vessel", "location": 0}, "e1c884e7dede431dadee09506ec4f859": {"name": "Koelkast (92C4A)", "model": "Circle+", "types": {"py/set": ["plug", "power"]}, "class": "refrigerator", "location": 0}, "aac7b735042c4832ac9ff33aae4f453b": {"name": "Vaatwasser (2a1ab)", "model": "Circle", "types": {"py/set": ["plug", "power"]}, "class": "dishwasher", "location": 0}, "cfe95cf3de1948c0b8955125bf754614": {"name": "Droger (52559)", "model": "Circle", "types": {"py/set": ["plug", "power"]}, "class": "dryer", "location": 0}, "99f89d097be34fca88d8598c6dbc18ea": {"name": "Meterkast (787BFB)", "model": "Circle", "types": {"py/set": ["plug", "power"]}, "class": "router", "location": 0}, "059e4d03c7a34d278add5c7a4a781d19": {"name": "Wasmachine (52AC1)", "model": "Circle", "types": {"py/set": ["plug", "power"]}, "class": "washingmachine", "location": 0}, "e309b52ea5684cf1a22f30cf0cd15051": {"name": "Computer (788618)", "model": "Circle", "types": {"py/set": ["plug", "power"]}, "class": "computer_desktop", "location": 0}, "71e1944f2a944b26ad73323e399efef0": {"name": "Test", "model": "group_switch", "types": {"py/set": ["switch_group"]}, "class": "switching", "members": ["5ca521ac179d468e91d772eeeb8a2117"], "location": null}, "d950b314e9d8499f968e6db8d82ef78c": {"name": "Stroomvreters", "model": "group_switch", "types": {"py/set": ["switch_group"]}, "class": "report", "members": ["059e4d03c7a34d278add5c7a4a781d19", "5871317346d045bc9f6b987ef25ee638", "aac7b735042c4832ac9ff33aae4f453b", "cfe95cf3de1948c0b8955125bf754614", "e1c884e7dede431dadee09506ec4f859"], "location": null}, "d03738edfcc947f7b8f4573571d90d2d": {"name": "Schakel", "model": "group_switch", "types": {"py/set": ["switch_group"]}, "class": "switching", "members": ["059e4d03c7a34d278add5c7a4a781d19", "cfe95cf3de1948c0b8955125bf754614"], "location": null}} \ No newline at end of file diff --git a/tests/fixtures/plugwise/stretch_v31/get_device_data/5871317346d045bc9f6b987ef25ee638.json b/tests/fixtures/plugwise/stretch_v31/get_device_data/5871317346d045bc9f6b987ef25ee638.json index 529c8b76d95..4a3e493b246 100644 --- a/tests/fixtures/plugwise/stretch_v31/get_device_data/5871317346d045bc9f6b987ef25ee638.json +++ b/tests/fixtures/plugwise/stretch_v31/get_device_data/5871317346d045bc9f6b987ef25ee638.json @@ -1 +1 @@ -{"electricity_consumed": 1.19, "electricity_produced": 0.0, "relay": true} \ No newline at end of file +{"electricity_consumed": 1.19, "electricity_consumed_interval": 0.0, "electricity_produced": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/stretch_v31/get_device_data/5ca521ac179d468e91d772eeeb8a2117.json b/tests/fixtures/plugwise/stretch_v31/get_device_data/5ca521ac179d468e91d772eeeb8a2117.json new file mode 100644 index 00000000000..7325dff8271 --- /dev/null +++ b/tests/fixtures/plugwise/stretch_v31/get_device_data/5ca521ac179d468e91d772eeeb8a2117.json @@ -0,0 +1 @@ +{"electricity_consumed": 0.0, "electricity_consumed_interval": 0.0, "electricity_produced": 0.0, "electricity_produced_interval": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/stretch_v31/get_device_data/71e1944f2a944b26ad73323e399efef0.json b/tests/fixtures/plugwise/stretch_v31/get_device_data/71e1944f2a944b26ad73323e399efef0.json new file mode 100644 index 00000000000..bbb8ac98c1c --- /dev/null +++ b/tests/fixtures/plugwise/stretch_v31/get_device_data/71e1944f2a944b26ad73323e399efef0.json @@ -0,0 +1 @@ +{"relay": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/stretch_v31/get_device_data/99f89d097be34fca88d8598c6dbc18ea.json b/tests/fixtures/plugwise/stretch_v31/get_device_data/99f89d097be34fca88d8598c6dbc18ea.json new file mode 100644 index 00000000000..b0cab0e3f30 --- /dev/null +++ b/tests/fixtures/plugwise/stretch_v31/get_device_data/99f89d097be34fca88d8598c6dbc18ea.json @@ -0,0 +1 @@ +{"electricity_consumed": 27.6, "electricity_consumed_interval": 28.2, "electricity_produced": 0.0, "electricity_produced_interval": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/stretch_v31/get_device_data/aac7b735042c4832ac9ff33aae4f453b.json b/tests/fixtures/plugwise/stretch_v31/get_device_data/aac7b735042c4832ac9ff33aae4f453b.json index 35ce04f51cf..e58bc4c6d6f 100644 --- a/tests/fixtures/plugwise/stretch_v31/get_device_data/aac7b735042c4832ac9ff33aae4f453b.json +++ b/tests/fixtures/plugwise/stretch_v31/get_device_data/aac7b735042c4832ac9ff33aae4f453b.json @@ -1 +1 @@ -{"electricity_consumed": 0.0, "electricity_produced": 0.0, "relay": true} \ No newline at end of file +{"electricity_consumed": 0.0, "electricity_consumed_interval": 0.71, "electricity_produced": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/stretch_v31/get_device_data/cfe95cf3de1948c0b8955125bf754614.json b/tests/fixtures/plugwise/stretch_v31/get_device_data/cfe95cf3de1948c0b8955125bf754614.json index 42de4d3338b..b08f6d6093a 100644 --- a/tests/fixtures/plugwise/stretch_v31/get_device_data/cfe95cf3de1948c0b8955125bf754614.json +++ b/tests/fixtures/plugwise/stretch_v31/get_device_data/cfe95cf3de1948c0b8955125bf754614.json @@ -1 +1 @@ -{"electricity_consumed": 0.0, "electricity_consumed_interval": 1.06, "electricity_produced": 0.0, "relay": true} \ No newline at end of file +{"electricity_consumed": 0.0, "electricity_consumed_interval": 0.0, "electricity_produced": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/stretch_v31/get_device_data/d03738edfcc947f7b8f4573571d90d2d.json b/tests/fixtures/plugwise/stretch_v31/get_device_data/d03738edfcc947f7b8f4573571d90d2d.json new file mode 100644 index 00000000000..bbb8ac98c1c --- /dev/null +++ b/tests/fixtures/plugwise/stretch_v31/get_device_data/d03738edfcc947f7b8f4573571d90d2d.json @@ -0,0 +1 @@ +{"relay": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/stretch_v31/get_device_data/d950b314e9d8499f968e6db8d82ef78c.json b/tests/fixtures/plugwise/stretch_v31/get_device_data/d950b314e9d8499f968e6db8d82ef78c.json index de5baf4c9a6..bbb8ac98c1c 100644 --- a/tests/fixtures/plugwise/stretch_v31/get_device_data/d950b314e9d8499f968e6db8d82ef78c.json +++ b/tests/fixtures/plugwise/stretch_v31/get_device_data/d950b314e9d8499f968e6db8d82ef78c.json @@ -1 +1 @@ -{"relay": false} \ No newline at end of file +{"relay": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/stretch_v31/get_device_data/e1c884e7dede431dadee09506ec4f859.json b/tests/fixtures/plugwise/stretch_v31/get_device_data/e1c884e7dede431dadee09506ec4f859.json index 1a7249b68d5..11ebae52f49 100644 --- a/tests/fixtures/plugwise/stretch_v31/get_device_data/e1c884e7dede431dadee09506ec4f859.json +++ b/tests/fixtures/plugwise/stretch_v31/get_device_data/e1c884e7dede431dadee09506ec4f859.json @@ -1 +1 @@ -{"electricity_consumed": 53.2, "electricity_produced": 0.0, "relay": true} \ No newline at end of file +{"electricity_consumed": 50.5, "electricity_consumed_interval": 0.08, "electricity_produced": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/stretch_v31/get_device_data/e309b52ea5684cf1a22f30cf0cd15051.json b/tests/fixtures/plugwise/stretch_v31/get_device_data/e309b52ea5684cf1a22f30cf0cd15051.json new file mode 100644 index 00000000000..456fb6744d2 --- /dev/null +++ b/tests/fixtures/plugwise/stretch_v31/get_device_data/e309b52ea5684cf1a22f30cf0cd15051.json @@ -0,0 +1 @@ +{"electricity_consumed": 156, "electricity_consumed_interval": 163, "electricity_produced": 0.0, "electricity_produced_interval": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/stretch_v31/notifications.json b/tests/fixtures/plugwise/stretch_v31/notifications.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/tests/fixtures/plugwise/stretch_v31/notifications.json @@ -0,0 +1 @@ +{} \ No newline at end of file From c54a0f80afc32a508f8bce6bca370ba4166fe5b3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 7 Jan 2021 16:00:53 -1000 Subject: [PATCH 127/507] Update nexia to 0.9.5 (#44924) --- CODEOWNERS | 2 +- homeassistant/components/nexia/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 03912a0dec0..3a1f15c015d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -293,7 +293,7 @@ homeassistant/components/ness_alarm/* @nickw444 homeassistant/components/nest/* @awarecan @allenporter homeassistant/components/netatmo/* @cgtobi homeassistant/components/netdata/* @fabaff -homeassistant/components/nexia/* @ryannazaretian @bdraco +homeassistant/components/nexia/* @bdraco homeassistant/components/nextbus/* @vividboarder homeassistant/components/nextcloud/* @meichthys homeassistant/components/nightscout/* @marciogranzotto diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index 1a4d6c74e84..a3446f6168c 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,8 +1,8 @@ { "domain": "nexia", "name": "Nexia", - "requirements": ["nexia==0.9.4"], - "codeowners": ["@ryannazaretian", "@bdraco"], + "requirements": ["nexia==0.9.5"], + "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true } diff --git a/requirements_all.txt b/requirements_all.txt index 198b9231f53..3da0671e121 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -983,7 +983,7 @@ netdisco==2.8.2 neurio==0.3.1 # homeassistant.components.nexia -nexia==0.9.4 +nexia==0.9.5 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b37bbddd67e..1c040933b99 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -487,7 +487,7 @@ nessclient==0.9.15 netdisco==2.8.2 # homeassistant.components.nexia -nexia==0.9.4 +nexia==0.9.5 # homeassistant.components.nsw_fuel_station nsw-fuel-api-client==1.0.10 From 3b184ad11c2916a61add781e88c1c17055f4053a Mon Sep 17 00:00:00 2001 From: Jamin Collins Date: Fri, 8 Jan 2021 04:09:22 +0000 Subject: [PATCH 128/507] Implement support for additional ecobee hold modes (#40520) * useEndTime2hour - 2 hours * useEndTime4hour - 4 hours * indefinite - Until I change it These changes have been tested with an ecobee3 lite running firmware version 4.5.81.200 Signed-off-by: Jamin W. Collins --- homeassistant/components/ecobee/climate.py | 36 +++++++++++++++----- tests/components/ecobee/test_climate.py | 38 +++++++++++++++++----- 2 files changed, 57 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 6bb7dc1a870..c61428cbc78 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -617,6 +617,7 @@ class Thermostat(ClimateEntity): cool_temp_setpoint, heat_temp_setpoint, self.hold_preference(), + self.hold_hours(), ) _LOGGER.debug( "Setting ecobee hold_temp to: heat=%s, is=%s, cool=%s, is=%s", @@ -717,15 +718,32 @@ class Thermostat(ClimateEntity): def hold_preference(self): """Return user preference setting for hold time.""" - # Values returned from thermostat are 'useEndTime4hour', - # 'useEndTime2hour', 'nextTransition', 'indefinite', 'askMe' - default = self.thermostat["settings"]["holdAction"] - if default == "nextTransition": - return default - # add further conditions if other hold durations should be - # supported; note that this should not include 'indefinite' - # as an indefinite away hold is interpreted as away_mode - return "nextTransition" + # Values returned from thermostat are: + # "useEndTime2hour", "useEndTime4hour" + # "nextPeriod", "askMe" + # "indefinite" + device_preference = self.thermostat["settings"]["holdAction"] + # Currently supported pyecobee holdTypes: + # dateTime, nextTransition, indefinite, holdHours + hold_pref_map = { + "useEndTime2hour": "holdHours", + "useEndTime4hour": "holdHours", + "indefinite": "indefinite", + } + return hold_pref_map.get(device_preference, "nextTransition") + + def hold_hours(self): + """Return user preference setting for hold duration in hours.""" + # Values returned from thermostat are: + # "useEndTime2hour", "useEndTime4hour" + # "nextPeriod", "askMe" + # "indefinite" + device_preference = self.thermostat["settings"]["holdAction"] + hold_hours_map = { + "useEndTime2hour": 2, + "useEndTime4hour": 4, + } + return hold_hours_map.get(device_preference, 0) def create_vacation(self, service_data): """Create a vacation with user-specified parameters.""" diff --git a/tests/components/ecobee/test_climate.py b/tests/components/ecobee/test_climate.py index 32575e7188a..a9b9165d713 100644 --- a/tests/components/ecobee/test_climate.py +++ b/tests/components/ecobee/test_climate.py @@ -208,26 +208,32 @@ async def test_set_temperature(ecobee_fixture, thermostat, data): # 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")]) + data.ecobee.set_hold_temp.assert_has_calls( + [mock.call(1, 30, 20, "nextTransition", 0)] + ) # Auto -> Hold data.reset_mock() thermostat.set_temperature(temperature=20) - data.ecobee.set_hold_temp.assert_has_calls([mock.call(1, 25, 15, "nextTransition")]) + data.ecobee.set_hold_temp.assert_has_calls( + [mock.call(1, 25, 15, "nextTransition", 0)] + ) # 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")] + [mock.call(1, 20.5, 20.5, "nextTransition", 0)] ) # 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")]) + data.ecobee.set_hold_temp.assert_has_calls( + [mock.call(1, 20, 20, "nextTransition", 0)] + ) # Heat -> Auto data.reset_mock() @@ -280,16 +286,32 @@ async def test_resume_program(thermostat, data): async def test_hold_preference(ecobee_fixture, thermostat): """Test hold preference.""" - assert thermostat.hold_preference() == "nextTransition" + ecobee_fixture["settings"]["holdAction"] = "indefinite" + assert thermostat.hold_preference() == "indefinite" + for action in ["useEndTime2hour", "useEndTime4hour"]: + ecobee_fixture["settings"]["holdAction"] = action + assert thermostat.hold_preference() == "holdHours" + for action in [ + "nextPeriod", + "askMe", + ]: + ecobee_fixture["settings"]["holdAction"] = action + assert thermostat.hold_preference() == "nextTransition" + + +def test_hold_hours(ecobee_fixture, thermostat): + """Test hold hours preference.""" + ecobee_fixture["settings"]["holdAction"] = "useEndTime2hour" + assert thermostat.hold_hours() == 2 + ecobee_fixture["settings"]["holdAction"] = "useEndTime4hour" + assert thermostat.hold_hours() == 4 for action in [ - "useEndTime4hour", - "useEndTime2hour", "nextPeriod", "indefinite", "askMe", ]: ecobee_fixture["settings"]["holdAction"] = action - assert thermostat.hold_preference() == "nextTransition" + assert thermostat.hold_hours() == 0 async def test_set_fan_mode_on(thermostat, data): From e35e460e696f542826214fe968c05150ae363256 Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Fri, 8 Jan 2021 17:27:03 +1100 Subject: [PATCH 129/507] Use parent_id to find cause of logbook events with new contexts (#44416) * Use parent_id to find cause of events with new contexts When looking up the causing event for logbook display, use the `parent_id` of the current context if the current context just points back to the current event. This now shows in the logbook the cause of an event in the case that a component has created a new context from an existing context and tied them together via the `Context.parent_id`. * Fix exception when parent event not available * Use async_Log_entry to avoid jump into executor --- homeassistant/components/logbook/__init__.py | 81 ++++---- tests/components/logbook/test_init.py | 183 +++++++++++++++++++ 2 files changed, 228 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 254d99ed848..e2d8a22c251 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -93,6 +93,7 @@ EVENT_COLUMNS = [ Events.time_fired, Events.context_id, Events.context_user_id, + Events.context_parent_id, ] SCRIPT_AUTOMATION_EVENTS = [EVENT_AUTOMATION_TRIGGERED, EVENT_SCRIPT_STARTED] @@ -320,16 +321,14 @@ def humanify(hass, events, entity_attr_cache, context_lookup): if event.context_user_id: data["context_user_id"] = event.context_user_id - context_event = context_lookup.get(event.context_id) - if context_event and context_event != event: - _augment_data_with_context( - data, - entity_id, - event, - context_event, - entity_attr_cache, - external_events, - ) + _augment_data_with_context( + data, + entity_id, + event, + context_lookup, + entity_attr_cache, + external_events, + ) yield data @@ -340,16 +339,15 @@ def humanify(hass, events, entity_attr_cache, context_lookup): data["domain"] = domain if event.context_user_id: data["context_user_id"] = event.context_user_id - context_event = context_lookup.get(event.context_id) - if context_event: - _augment_data_with_context( - data, - data.get(ATTR_ENTITY_ID), - event, - context_event, - entity_attr_cache, - external_events, - ) + + _augment_data_with_context( + data, + data.get(ATTR_ENTITY_ID), + event, + context_lookup, + entity_attr_cache, + external_events, + ) yield data elif event.event_type == EVENT_HOMEASSISTANT_START: @@ -397,16 +395,14 @@ def humanify(hass, events, entity_attr_cache, context_lookup): if event.context_user_id: data["context_user_id"] = event.context_user_id - context_event = context_lookup.get(event.context_id) - if context_event and context_event != event: - _augment_data_with_context( - data, - entity_id, - event, - context_event, - entity_attr_cache, - external_events, - ) + _augment_data_with_context( + data, + entity_id, + event, + context_lookup, + entity_attr_cache, + external_events, + ) yield data @@ -597,16 +593,27 @@ def _keep_event(hass, event, entities_filter): def _augment_data_with_context( - data, entity_id, event, context_event, entity_attr_cache, external_events + data, entity_id, event, context_lookup, entity_attr_cache, external_events ): - event_type = context_event.event_type + context_event = context_lookup.get(event.context_id) - # State change - context_entity_id = context_event.entity_id - - if entity_id and context_entity_id == entity_id: + if not context_event: return + if event == context_event: + # This is the first event with the given ID. Was it directly caused by + # a parent event? + if event.context_parent_id: + context_event = context_lookup.get(event.context_parent_id) + # Ensure the (parent) context_event exists and is not the root cause of + # this log entry. + if not context_event or event == context_event: + return + + event_type = context_event.event_type + context_entity_id = context_event.entity_id + + # State change if context_entity_id: data["context_entity_id"] = context_entity_id data["context_entity_id_name"] = _entity_name_from_event( @@ -672,6 +679,7 @@ class LazyEventPartialState: "domain", "context_id", "context_user_id", + "context_parent_id", "time_fired_minute", ] @@ -687,6 +695,7 @@ class LazyEventPartialState: self.domain = self._row.domain self.context_id = self._row.context_id self.context_user_id = self._row.context_user_id + self.context_parent_id = self._row.context_parent_id self.time_fired_minute = self._row.time_fired.minute @property diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 8360759d564..cd3fb519ded 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -282,6 +282,7 @@ def create_state_changed_event_from_old_new( "time_fired" "context_id" "context_user_id" + "context_parent_id" "state" "entity_id" "domain" @@ -300,6 +301,7 @@ def create_state_changed_event_from_old_new( row.domain = entity_id and ha.split_entity_id(entity_id)[0] row.context_id = None row.context_user_id = None + row.context_parent_id = None row.old_state_id = old_state and 1 row.state_id = new_state and 1 return logbook.LazyEventPartialState(row) @@ -946,6 +948,187 @@ async def test_logbook_entity_context_id(hass, hass_client): assert json_dict[7]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474" +async def test_logbook_entity_context_parent_id(hass, hass_client): + """Test the logbook view links events via context parent_id.""" + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "logbook", {}) + await async_setup_component(hass, "automation", {}) + await async_setup_component(hass, "script", {}) + + await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + context = ha.Context( + id="ac5bd62de45711eaaeb351041eec8dd9", + user_id="b400facee45711eaa9308bfd3d19e474", + ) + + # An Automation triggering scripts with a new context + automation_entity_id_test = "automation.alarm" + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation", ATTR_ENTITY_ID: automation_entity_id_test}, + context=context, + ) + + child_context = ha.Context( + id="2798bfedf8234b5e9f4009c91f48f30c", + parent_id="ac5bd62de45711eaaeb351041eec8dd9", + user_id="b400facee45711eaa9308bfd3d19e474", + ) + hass.bus.async_fire( + EVENT_SCRIPT_STARTED, + {ATTR_NAME: "Mock script", ATTR_ENTITY_ID: "script.mock_script"}, + context=child_context, + ) + hass.states.async_set( + automation_entity_id_test, + STATE_ON, + {ATTR_FRIENDLY_NAME: "Alarm Automation"}, + context=child_context, + ) + + entity_id_test = "alarm_control_panel.area_001" + hass.states.async_set(entity_id_test, STATE_OFF, context=child_context) + await hass.async_block_till_done() + hass.states.async_set(entity_id_test, STATE_ON, context=child_context) + await hass.async_block_till_done() + entity_id_second = "alarm_control_panel.area_002" + hass.states.async_set(entity_id_second, STATE_OFF, context=child_context) + await hass.async_block_till_done() + hass.states.async_set(entity_id_second, STATE_ON, context=child_context) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + logbook.async_log_entry( + hass, + "mock_name", + "mock_message", + "alarm_control_panel", + "alarm_control_panel.area_003", + child_context, + ) + await hass.async_block_till_done() + + logbook.async_log_entry( + hass, + "mock_name", + "mock_message", + "homeassistant", + None, + child_context, + ) + await hass.async_block_till_done() + + # A state change via service call with the script as the parent + light_turn_off_service_context = ha.Context( + id="9c5bd62de45711eaaeb351041eec8dd9", + parent_id="2798bfedf8234b5e9f4009c91f48f30c", + user_id="9400facee45711eaa9308bfd3d19e474", + ) + hass.states.async_set("light.switch", STATE_ON) + await hass.async_block_till_done() + + hass.bus.async_fire( + EVENT_CALL_SERVICE, + { + ATTR_DOMAIN: "light", + ATTR_SERVICE: "turn_off", + ATTR_ENTITY_ID: "light.switch", + }, + context=light_turn_off_service_context, + ) + await hass.async_block_till_done() + + hass.states.async_set( + "light.switch", STATE_OFF, context=light_turn_off_service_context + ) + await hass.async_block_till_done() + + # An event with a parent event, but the parent event isn't available + missing_parent_context = ha.Context( + id="fc40b9a0d1f246f98c34b33c76228ee6", + parent_id="c8ce515fe58e442f8664246c65ed964f", + user_id="485cacf93ef84d25a99ced3126b921d2", + ) + logbook.async_log_entry( + hass, + "mock_name", + "mock_message", + "alarm_control_panel", + "alarm_control_panel.area_009", + missing_parent_context, + ) + await hass.async_block_till_done() + + await hass.async_add_executor_job(trigger_db_commit, hass) + await hass.async_block_till_done() + await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + client = await hass_client() + + # Today time 00:00:00 + start = dt_util.utcnow().date() + start_date = datetime(start.year, start.month, start.day) + + # Test today entries with filter by end_time + end_time = start + timedelta(hours=24) + response = await client.get( + f"/api/logbook/{start_date.isoformat()}?end_time={end_time}" + ) + assert response.status == 200 + json_dict = await response.json() + + assert json_dict[0]["entity_id"] == "automation.alarm" + assert "context_entity_id" not in json_dict[0] + assert json_dict[0]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" + + # New context, so this looks to be triggered by the Alarm Automation + assert json_dict[1]["entity_id"] == "script.mock_script" + assert json_dict[1]["context_event_type"] == "automation_triggered" + assert json_dict[1]["context_entity_id"] == "automation.alarm" + assert json_dict[1]["context_entity_id_name"] == "Alarm Automation" + assert json_dict[1]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" + + assert json_dict[2]["entity_id"] == entity_id_test + assert json_dict[2]["context_event_type"] == "script_started" + assert json_dict[2]["context_entity_id"] == "script.mock_script" + assert json_dict[2]["context_entity_id_name"] == "mock script" + assert json_dict[2]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" + + assert json_dict[3]["entity_id"] == entity_id_second + assert json_dict[3]["context_event_type"] == "script_started" + assert json_dict[3]["context_entity_id"] == "script.mock_script" + assert json_dict[3]["context_entity_id_name"] == "mock script" + assert json_dict[3]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" + + assert json_dict[4]["domain"] == "homeassistant" + + assert json_dict[5]["entity_id"] == "alarm_control_panel.area_003" + assert json_dict[5]["context_event_type"] == "script_started" + assert json_dict[5]["context_entity_id"] == "script.mock_script" + assert json_dict[5]["domain"] == "alarm_control_panel" + assert json_dict[5]["context_entity_id_name"] == "mock script" + assert json_dict[5]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" + + assert json_dict[6]["domain"] == "homeassistant" + assert json_dict[6]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" + + assert json_dict[7]["entity_id"] == "light.switch" + assert json_dict[7]["context_event_type"] == "call_service" + assert json_dict[7]["context_domain"] == "light" + assert json_dict[7]["context_service"] == "turn_off" + assert json_dict[7]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474" + + assert json_dict[8]["entity_id"] == "alarm_control_panel.area_009" + assert json_dict[8]["domain"] == "alarm_control_panel" + assert "context_event_type" not in json_dict[8] + assert "context_entity_id" not in json_dict[8] + assert "context_entity_id_name" not in json_dict[8] + assert json_dict[8]["context_user_id"] == "485cacf93ef84d25a99ced3126b921d2" + + async def test_logbook_context_from_template(hass, hass_client): """Test the logbook view with end_time and entity with automations and scripts.""" await hass.async_add_executor_job(init_recorder_component, hass) From 30189fb5d59f9123b1a45648a9e1e77e34adaf33 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 8 Jan 2021 11:50:02 +0100 Subject: [PATCH 130/507] Fix KNX cover state return open when unknown (#44926) --- homeassistant/components/knx/cover.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index b88b1cfe86a..33da600976e 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -79,6 +79,9 @@ class KNXCover(KnxEntity, CoverEntity): @property def is_closed(self): """Return if the cover is closed.""" + # state shall be "unknown" when xknx travelcalculator is not initialized + if self._device.current_position() is None: + return None return self._device.is_closed() @property From e134c17df22a94f262178f598fd48bc1def503db Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 8 Jan 2021 11:53:46 +0100 Subject: [PATCH 131/507] Upgrade discord.py to 1.6.0 (#44941) --- homeassistant/components/discord/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/discord/manifest.json b/homeassistant/components/discord/manifest.json index 88bebe509b7..474705913c0 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.5.1"], + "requirements": ["discord.py==1.6.0"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 3da0671e121..93e79789066 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -490,7 +490,7 @@ directv==0.4.0 discogs_client==2.3.0 # homeassistant.components.discord -discord.py==1.5.1 +discord.py==1.6.0 # homeassistant.components.updater distro==1.5.0 From c457ea854c3a94bae8c860e18619358e1b6af7db Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 8 Jan 2021 11:59:30 +0100 Subject: [PATCH 132/507] Upgrade youtube_dl to 2021.01.03 (#44942) --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 2c17d85f7b3..50850447f6e 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.12.29"], + "requirements": ["youtube_dl==2021.01.03"], "dependencies": ["media_player"], "codeowners": [], "quality_scale": "internal" diff --git a/requirements_all.txt b/requirements_all.txt index 93e79789066..09add60b9f3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2330,7 +2330,7 @@ yeelight==0.5.4 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2020.12.29 +youtube_dl==2021.01.03 # homeassistant.components.onvif zeep[async]==4.0.0 From 7c93a11aba7d80604c5af75a93700992f93ab164 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 8 Jan 2021 02:07:50 -1000 Subject: [PATCH 133/507] Fix wait_template incorrectly matching falsey values (#44938) --- homeassistant/helpers/script.py | 15 ++----- tests/helpers/test_script.py | 77 +++++++++++++++++++++++++++++---- 2 files changed, 72 insertions(+), 20 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 77c842a27fe..48a662e3a81 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -62,11 +62,7 @@ from homeassistant.core import ( callback, ) from homeassistant.helpers import condition, config_validation as cv, service, template -from homeassistant.helpers.event import ( - TrackTemplate, - async_call_later, - async_track_template_result, -) +from homeassistant.helpers.event import async_call_later, async_track_template from homeassistant.helpers.script_variables import ScriptVariables from homeassistant.helpers.trigger import ( async_initialize_triggers, @@ -359,7 +355,7 @@ class _ScriptRun: return @callback - def _async_script_wait(event, updates): + def async_script_wait(entity_id, from_s, to_s): """Handle script after template condition is true.""" self._variables["wait"] = { "remaining": to_context.remaining if to_context else delay, @@ -368,12 +364,9 @@ class _ScriptRun: done.set() to_context = None - info = async_track_template_result( - self._hass, - [TrackTemplate(wait_template, self._variables)], - _async_script_wait, + unsub = async_track_template( + self._hass, wait_template, async_script_wait, self._variables ) - unsub = info.async_remove self._changed() done = asyncio.Event() diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 5be5f6bc91f..18e510b7582 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -8,6 +8,7 @@ from types import MappingProxyType from unittest import mock from unittest.mock import patch +from async_timeout import timeout import pytest import voluptuous as vol @@ -544,6 +545,41 @@ async def test_wait_basic(hass, action_type): assert script_obj.last_action is None +@pytest.mark.parametrize("action_type", ["template", "trigger"]) +async def test_wait_basic_times_out(hass, action_type): + """Test wait actions times out when the action does not happen.""" + wait_alias = "wait step" + action = {"alias": wait_alias} + if action_type == "template": + action["wait_template"] = "{{ states.switch.test.state == 'off' }}" + else: + action["wait_for_trigger"] = { + "platform": "state", + "entity_id": "switch.test", + "to": "off", + } + sequence = cv.SCRIPT_SCHEMA(action) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + wait_started_flag = async_watch_for_action(script_obj, wait_alias) + timed_out = False + + try: + hass.states.async_set("switch.test", "on") + hass.async_create_task(script_obj.async_run(context=Context())) + await asyncio.wait_for(wait_started_flag.wait(), 1) + assert script_obj.is_running + assert script_obj.last_action == wait_alias + hass.states.async_set("switch.test", "not_on") + + with timeout(0.1): + await hass.async_block_till_done() + except asyncio.TimeoutError: + timed_out = True + await script_obj.async_stop() + + assert timed_out + + @pytest.mark.parametrize("action_type", ["template", "trigger"]) async def test_multiple_runs_wait(hass, action_type): """Test multiple runs with wait in script.""" @@ -782,30 +818,53 @@ async def test_wait_template_variables_in(hass): async def test_wait_template_with_utcnow(hass): """Test the wait template with utcnow.""" - sequence = cv.SCRIPT_SCHEMA({"wait_template": "{{ utcnow().hours == 12 }}"}) + sequence = cv.SCRIPT_SCHEMA({"wait_template": "{{ utcnow().hour == 12 }}"}) script_obj = script.Script(hass, sequence, "Test Name", "test_domain") wait_started_flag = async_watch_for_action(script_obj, "wait") - start_time = dt_util.utcnow() + timedelta(hours=24) + start_time = dt_util.utcnow().replace(minute=1) + timedelta(hours=48) try: hass.async_create_task(script_obj.async_run(context=Context())) - async_fire_time_changed(hass, start_time.replace(hour=5)) - assert not script_obj.is_running - async_fire_time_changed(hass, start_time.replace(hour=12)) - await asyncio.wait_for(wait_started_flag.wait(), 1) - assert script_obj.is_running + + match_time = start_time.replace(hour=12) + with patch("homeassistant.util.dt.utcnow", return_value=match_time): + async_fire_time_changed(hass, match_time) except (AssertionError, asyncio.TimeoutError): await script_obj.async_stop() raise else: - async_fire_time_changed(hass, start_time.replace(hour=3)) await hass.async_block_till_done() - assert not script_obj.is_running +async def test_wait_template_with_utcnow_no_match(hass): + """Test the wait template with utcnow that does not match.""" + sequence = cv.SCRIPT_SCHEMA({"wait_template": "{{ utcnow().hour == 12 }}"}) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + wait_started_flag = async_watch_for_action(script_obj, "wait") + start_time = dt_util.utcnow().replace(minute=1) + timedelta(hours=48) + timed_out = False + + try: + hass.async_create_task(script_obj.async_run(context=Context())) + await asyncio.wait_for(wait_started_flag.wait(), 1) + assert script_obj.is_running + + non_maching_time = start_time.replace(hour=3) + with patch("homeassistant.util.dt.utcnow", return_value=non_maching_time): + async_fire_time_changed(hass, non_maching_time) + + with timeout(0.1): + await hass.async_block_till_done() + except asyncio.TimeoutError: + timed_out = True + await script_obj.async_stop() + + assert timed_out + + @pytest.mark.parametrize("mode", ["no_timeout", "timeout_finish", "timeout_not_finish"]) @pytest.mark.parametrize("action_type", ["template", "trigger"]) async def test_wait_variables_out(hass, mode, action_type): From 58195c64b75bbebcdac04d659aba949f5cd5821a Mon Sep 17 00:00:00 2001 From: Ottavio Campana Date: Fri, 8 Jan 2021 14:15:54 +0100 Subject: [PATCH 134/507] Fix media renderers without volume control (#44874) Co-authored-by: Paulus Schoutsen --- homeassistant/components/dlna_dmr/media_player.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index 2b01ad2a4ae..f8af118caed 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -281,7 +281,9 @@ class DlnaDmrDevice(MediaPlayerEntity): @property def volume_level(self): """Volume level of the media player (0..1).""" - return self._device.volume_level + if self._device.has_volume_level: + return self._device.volume_level + return 0 @catch_request_errors() async def async_set_volume_level(self, volume): From d99bc99d9beea3ede29789c064a48fb60959e6d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 8 Jan 2021 16:03:06 +0100 Subject: [PATCH 135/507] Prefix versions in system health (#44921) * Prefix versions in system health * Adjust test * Update homeassistant/components/hassio/strings.json --- homeassistant/components/hassio/system_health.py | 2 +- homeassistant/components/hassio/translations/en.json | 4 ++-- homeassistant/components/homeassistant/system_health.py | 2 +- tests/components/hassio/test_system_health.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/hassio/system_health.py b/homeassistant/components/hassio/system_health.py index 530703d3e25..47e9a3d2995 100644 --- a/homeassistant/components/hassio/system_health.py +++ b/homeassistant/components/hassio/system_health.py @@ -43,7 +43,7 @@ async def system_health_info(hass: HomeAssistant): information = { "host_os": host_info.get("operating_system"), "update_channel": info.get("channel"), - "supervisor_version": info.get("supervisor"), + "supervisor_version": f"supervisor-{info.get('supervisor')}", "docker_version": info.get("docker"), "disk_total": f"{host_info.get('disk_total')} GB", "disk_used": f"{host_info.get('disk_used')} GB", diff --git a/homeassistant/components/hassio/translations/en.json b/homeassistant/components/hassio/translations/en.json index 230e0c11fea..aadcdabfb94 100644 --- a/homeassistant/components/hassio/translations/en.json +++ b/homeassistant/components/hassio/translations/en.json @@ -9,11 +9,11 @@ "host_os": "Host Operating System", "installed_addons": "Installed Add-ons", "supervisor_api": "Supervisor API", - "supervisor_version": "Supervisor Version", + "supervisor_version": "Version", "supported": "Supported", "update_channel": "Update Channel", "version_api": "Version API" } }, - "title": "Hass.io" + "title": "Home Assistant Supervisor" } \ No newline at end of file diff --git a/homeassistant/components/homeassistant/system_health.py b/homeassistant/components/homeassistant/system_health.py index b0245d9beec..ff3562a24f9 100644 --- a/homeassistant/components/homeassistant/system_health.py +++ b/homeassistant/components/homeassistant/system_health.py @@ -17,7 +17,7 @@ async def system_health_info(hass): info = await system_info.async_get_system_info(hass) return { - "version": info.get("version"), + "version": f"core-{info.get('version')}", "installation_type": info.get("installation_type"), "dev": info.get("dev"), "hassio": info.get("hassio"), diff --git a/tests/components/hassio/test_system_health.py b/tests/components/hassio/test_system_health.py index 88eb7ea20f8..8fa610b5442 100644 --- a/tests/components/hassio/test_system_health.py +++ b/tests/components/hassio/test_system_health.py @@ -60,7 +60,7 @@ async def test_hassio_system_health(hass, aioclient_mock): "host_os": "Home Assistant OS 5.9", "installed_addons": "Awesome Addon (1.0.0)", "supervisor_api": "ok", - "supervisor_version": "2020.11.1", + "supervisor_version": "supervisor-2020.11.1", "supported": True, "update_channel": "stable", "version_api": "ok", From 793adb7f40b462287956447eca5a82328dba92eb Mon Sep 17 00:00:00 2001 From: "J.P. Hutchins" <34154542+JPHutchins@users.noreply.github.com> Date: Fri, 8 Jan 2021 07:53:47 -0800 Subject: [PATCH 136/507] Add torrent id to Transmission events (#44187) * Fire event after object update; clarify code across related methods * Change var to torrent, clarity * Add typehints, _ prefix private attributes --- .../components/transmission/__init__.py | 104 +++++++++--------- .../components/transmission/sensor.py | 9 +- 2 files changed, 61 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index 00fc2d1b3b5..b1837708919 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -1,6 +1,7 @@ """Support for the Transmission BitTorrent client API.""" from datetime import timedelta import logging +from typing import List import transmissionrpc from transmissionrpc.error import TransmissionError @@ -152,13 +153,13 @@ class TransmissionClient: """Initialize the Transmission RPC API.""" self.hass = hass self.config_entry = config_entry - self.tm_api = None - self._tm_data = None + self.tm_api = None # type: transmissionrpc.Client + self._tm_data = None # type: TransmissionData self.unsub_timer = None @property - def api(self): - """Return the tm_data object.""" + def api(self) -> "TransmissionData": + """Return the TransmissionData object.""" return self._tm_data async def async_setup(self): @@ -278,18 +279,18 @@ class TransmissionClient: class TransmissionData: """Get the latest data and update the states.""" - def __init__(self, hass, config, api): + def __init__(self, hass, config, api: transmissionrpc.Client): """Initialize the Transmission RPC API.""" self.hass = hass self.config = config - self.data = None - self.torrents = [] - self.session = None - self.available = True - self._api = api - self.completed_torrents = [] - self.started_torrents = [] - self.all_torrents = [] + self.data = None # type: transmissionrpc.Session + self.available = True # type: bool + self._all_torrents = [] # type: List[transmissionrpc.Torrent] + self._api = api # type: transmissionrpc.Client + self._completed_torrents = [] # type: List[transmissionrpc.Torrent] + self._session = None # type: transmissionrpc.Session + self._started_torrents = [] # type: List[transmissionrpc.Torrent] + self._torrents = [] # type: List[transmissionrpc.Torrent] @property def host(self): @@ -301,12 +302,17 @@ class TransmissionData: """Update signal per transmission entry.""" return f"{DATA_UPDATED}-{self.host}" + @property + def torrents(self) -> List[transmissionrpc.Torrent]: + """Get the list of torrents.""" + return self._torrents + def update(self): """Get the latest data from Transmission instance.""" try: self.data = self._api.session_stats() - self.torrents = self._api.get_torrents() - self.session = self._api.get_session() + self._torrents = self._api.get_torrents() + self._session = self._api.get_session() self.check_completed_torrent() self.check_started_torrent() @@ -321,64 +327,62 @@ class TransmissionData: def init_torrent_list(self): """Initialize torrent lists.""" - self.torrents = self._api.get_torrents() - self.completed_torrents = [ - x.name for x in self.torrents if x.status == "seeding" + self._torrents = self._api.get_torrents() + self._completed_torrents = [ + torrent for torrent in self._torrents if torrent.status == "seeding" ] - self.started_torrents = [ - x.name for x in self.torrents if x.status == "downloading" + self._started_torrents = [ + torrent for torrent in self._torrents if torrent.status == "downloading" ] def check_completed_torrent(self): """Get completed torrent functionality.""" - actual_torrents = self.torrents - actual_completed_torrents = [ - var.name for var in actual_torrents if var.status == "seeding" + current_completed_torrents = [ + torrent for torrent in self._torrents if torrent.status == "seeding" ] - - tmp_completed_torrents = list( - set(actual_completed_torrents).difference(self.completed_torrents) + freshly_completed_torrents = set(current_completed_torrents).difference( + self._completed_torrents ) + self._completed_torrents = current_completed_torrents - for var in tmp_completed_torrents: - self.hass.bus.fire(EVENT_DOWNLOADED_TORRENT, {"name": var}) - - self.completed_torrents = actual_completed_torrents + for torrent in freshly_completed_torrents: + self.hass.bus.fire( + EVENT_DOWNLOADED_TORRENT, {"name": torrent.name, "id": torrent.id} + ) def check_started_torrent(self): """Get started torrent functionality.""" - actual_torrents = self.torrents - actual_started_torrents = [ - var.name for var in actual_torrents if var.status == "downloading" + current_started_torrents = [ + torrent for torrent in self._torrents if torrent.status == "downloading" ] - - tmp_started_torrents = list( - set(actual_started_torrents).difference(self.started_torrents) + freshly_started_torrents = set(current_started_torrents).difference( + self._started_torrents ) + self._started_torrents = current_started_torrents - for var in tmp_started_torrents: - self.hass.bus.fire(EVENT_STARTED_TORRENT, {"name": var}) - self.started_torrents = actual_started_torrents + for torrent in freshly_started_torrents: + self.hass.bus.fire( + EVENT_STARTED_TORRENT, {"name": torrent.name, "id": torrent.id} + ) def check_removed_torrent(self): """Get removed torrent functionality.""" - actual_torrents = self.torrents - actual_all_torrents = [var.name for var in actual_torrents] - - removed_torrents = list(set(self.all_torrents).difference(actual_all_torrents)) - for var in removed_torrents: - self.hass.bus.fire(EVENT_REMOVED_TORRENT, {"name": var}) - self.all_torrents = actual_all_torrents + freshly_removed_torrents = set(self._all_torrents).difference(self._torrents) + self._all_torrents = self._torrents + for torrent in freshly_removed_torrents: + self.hass.bus.fire( + EVENT_REMOVED_TORRENT, {"name": torrent.name, "id": torrent.id} + ) def start_torrents(self): """Start all torrents.""" - if len(self.torrents) <= 0: + if len(self._torrents) <= 0: return self._api.start_all() def stop_torrents(self): """Stop all active torrents.""" - torrent_ids = [torrent.id for torrent in self.torrents] + torrent_ids = [torrent.id for torrent in self._torrents] self._api.stop_torrent(torrent_ids) def set_alt_speed_enabled(self, is_enabled): @@ -387,7 +391,7 @@ class TransmissionData: def get_alt_speed_enabled(self): """Get the alternative speed flag.""" - if self.session is None: + if self._session is None: return None - return self.session.alt_speed_enabled + return self._session.alt_speed_enabled diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index ea62de71e8d..2a24b80be16 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -1,9 +1,14 @@ """Support for monitoring the Transmission BitTorrent client API.""" +from typing import List + +from transmissionrpc.torrent import Torrent + from homeassistant.const import CONF_NAME, DATA_RATE_MEGABYTES_PER_SECOND, STATE_IDLE from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity +from . import TransmissionClient from .const import ( CONF_LIMIT, CONF_ORDER, @@ -38,7 +43,7 @@ class TransmissionSensor(Entity): def __init__(self, tm_client, client_name, sensor_name, sub_type=None): """Initialize the sensor.""" - self._tm_client = tm_client + self._tm_client = tm_client # type: TransmissionClient self._client_name = client_name self._name = sensor_name self._sub_type = sub_type @@ -163,7 +168,7 @@ class TransmissionTorrentsSensor(TransmissionSensor): self._state = len(torrents) -def _filter_torrents(torrents, statuses=None): +def _filter_torrents(torrents: List[Torrent], statuses=None) -> List[Torrent]: return [ torrent for torrent in torrents From 905100a189f1ac43540f9f79814368f9b8d20fd1 Mon Sep 17 00:00:00 2001 From: Sergio Oller Date: Fri, 8 Jan 2021 17:28:22 +0100 Subject: [PATCH 137/507] Disambiguate Supervisor HTTPUnauthorized on user/password validation (#44940) * Disambiguate HTTPUnauthorized on user/password validation The HA core API usually returns 401 when the request does not have proper authentication tokens or they have expired. However the user/password validation endpoint may also return 401 when the given user/password is invalid. The supervisor is currently unable to distinguish both scenarios, and it needs to. See https://github.com/home-assistant/supervisor/issues/2408 * Return 404 if user& password are not found/valid * Fix test for invalid user/password --- homeassistant/components/hassio/auth.py | 2 +- tests/components/hassio/test_auth.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py index 23b91ac40bc..a1c032fe0fe 100644 --- a/homeassistant/components/hassio/auth.py +++ b/homeassistant/components/hassio/auth.py @@ -82,7 +82,7 @@ class HassIOAuth(HassIOBaseAuth): data[ATTR_USERNAME], data[ATTR_PASSWORD] ) except auth_ha.InvalidAuth: - raise HTTPUnauthorized() from None + raise HTTPNotFound() from None return web.Response(status=HTTP_OK) diff --git a/tests/components/hassio/test_auth.py b/tests/components/hassio/test_auth.py index 5f27c06a190..a533d468069 100644 --- a/tests/components/hassio/test_auth.py +++ b/tests/components/hassio/test_auth.py @@ -66,7 +66,7 @@ async def test_login_error(hass, hassio_client_supervisor): ) # Check we got right response - assert resp.status == 401 + assert resp.status == 404 mock_login.assert_called_with("test", "123456") From 8fa62329a4c1b5f187230a2648fc5d513dfad0d6 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 8 Jan 2021 18:41:31 +0100 Subject: [PATCH 138/507] Fix Netatmo climate boost for valves (#44957) --- homeassistant/components/netatmo/climate.py | 29 +++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 30ce38753c6..bb930c5a994 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -243,7 +243,11 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): return home = data["home"] - if self._home_id == home["id"] and data["event_type"] == EVENT_TYPE_THERM_MODE: + + if self._home_id != home["id"]: + return + + if data["event_type"] == EVENT_TYPE_THERM_MODE: self._preset = NETATMO_MAP_PRESET[home[EVENT_TYPE_THERM_MODE]] self._hvac_mode = HVAC_MAP_NETATMO[self._preset] if self._preset == PRESET_FROST_GUARD: @@ -266,8 +270,13 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): elif room["therm_setpoint_mode"] == STATE_NETATMO_MAX: self._hvac_mode = HVAC_MODE_HEAT self._target_temperature = DEFAULT_MAX_TEMP + elif room["therm_setpoint_mode"] == STATE_NETATMO_MANUAL: + self._hvac_mode = HVAC_MODE_HEAT + self._target_temperature = room["therm_setpoint_temperature"] else: self._target_temperature = room["therm_setpoint_temperature"] + if self._target_temperature == DEFAULT_MAX_TEMP: + self._hvac_mode = HVAC_MODE_HEAT self.async_write_ha_state() break @@ -341,12 +350,28 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): STATE_NETATMO_HOME, ) - if preset_mode in [PRESET_BOOST, STATE_NETATMO_MAX] and self._model == NA_VALVE: + if ( + preset_mode in [PRESET_BOOST, STATE_NETATMO_MAX] + and self._model == NA_VALVE + and self.hvac_mode == HVAC_MODE_HEAT + ): + self._home_status.set_room_thermpoint( + self._id, + STATE_NETATMO_HOME, + ) + elif ( + preset_mode in [PRESET_BOOST, STATE_NETATMO_MAX] and self._model == NA_VALVE + ): self._home_status.set_room_thermpoint( self._id, STATE_NETATMO_MANUAL, DEFAULT_MAX_TEMP, ) + elif ( + preset_mode in [PRESET_BOOST, STATE_NETATMO_MAX] + and self.hvac_mode == HVAC_MODE_HEAT + ): + self._home_status.set_room_thermpoint(self._id, STATE_NETATMO_HOME) elif preset_mode in [PRESET_BOOST, STATE_NETATMO_MAX]: self._home_status.set_room_thermpoint( self._id, PRESET_MAP_NETATMO[preset_mode] From e3c1281616cd074406e79810cb04d16dc977bab6 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Fri, 8 Jan 2021 22:43:14 +0000 Subject: [PATCH 139/507] Add MQTT Number (non optimistic) (#44883) * non optimistic * test restored state * ups * review * Ensure the entity is not in optimistic mode Co-authored-by: Erik Montnemery Co-authored-by: Erik Montnemery --- homeassistant/components/mqtt/number.py | 82 +++++++++++++------ tests/components/mqtt/test_number.py | 103 ++++++++++++++++++++---- 2 files changed, 144 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index f469130cb1c..bac70723eeb 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -5,7 +5,13 @@ import voluptuous as vol from homeassistant.components import number from homeassistant.components.number import NumberEntity -from homeassistant.const import CONF_DEVICE, CONF_NAME, CONF_UNIQUE_ID +from homeassistant.const import ( + CONF_DEVICE, + CONF_ICON, + CONF_NAME, + CONF_OPTIMISTIC, + CONF_UNIQUE_ID, +) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import ( @@ -13,11 +19,14 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.reload import async_setup_reload_service +from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import ( ATTR_DISCOVERY_HASH, + CONF_COMMAND_TOPIC, CONF_QOS, + CONF_STATE_TOPIC, DOMAIN, PLATFORMS, MqttAttributes, @@ -32,15 +41,16 @@ from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_ _LOGGER = logging.getLogger(__name__) -CONF_TOPIC = "topic" DEFAULT_NAME = "MQTT Number" +DEFAULT_OPTIMISTIC = False PLATFORM_SCHEMA = ( - mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( + mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_UNIQUE_ID): cv.string, } ) @@ -94,16 +104,18 @@ class MqttNumber( MqttDiscoveryUpdate, MqttEntityDeviceInfo, NumberEntity, + RestoreEntity, ): """representation of an MQTT number.""" def __init__(self, config, config_entry, discovery_data): """Initialize the MQTT Number.""" self._config = config - self._unique_id = config.get(CONF_UNIQUE_ID) self._sub_state = None self._current_number = None + self._optimistic = config.get(CONF_OPTIMISTIC) + self._unique_id = config.get(CONF_UNIQUE_ID) device_config = config.get(CONF_DEVICE) @@ -145,18 +157,27 @@ class MqttNumber( except ValueError: _LOGGER.warning("We received <%s> which is not a Number", msg.payload) - self._sub_state = await subscription.async_subscribe_topics( - self.hass, - self._sub_state, - { - "state_topic": { - "topic": self._config[CONF_TOPIC], - "msg_callback": message_received, - "qos": self._config[CONF_QOS], - "encoding": None, - } - }, - ) + if self._config.get(CONF_STATE_TOPIC) is None: + # Force into optimistic mode. + self._optimistic = True + else: + self._sub_state = await subscription.async_subscribe_topics( + self.hass, + self._sub_state, + { + "state_topic": { + "topic": self._config.get(CONF_STATE_TOPIC), + "msg_callback": message_received, + "qos": self._config[CONF_QOS], + "encoding": None, + } + }, + ) + + if self._optimistic: + last_state = await self.async_get_last_state() + if last_state: + self._current_number = last_state.state async def async_will_remove_from_hass(self): """Unsubscribe when removed.""" @@ -174,20 +195,23 @@ class MqttNumber( async def async_set_value(self, value: float) -> None: """Update the current value.""" + + current_number = value + if value.is_integer(): - self._current_number = int(value) - else: - self._current_number = value + current_number = int(value) + + if self._optimistic: + self._current_number = current_number + self.async_write_ha_state() mqtt.async_publish( self.hass, - self._config[CONF_TOPIC], - self._current_number, + self._config[CONF_COMMAND_TOPIC], + current_number, self._config[CONF_QOS], ) - self.async_write_ha_state() - @property def name(self): """Return the name of this number.""" @@ -202,3 +226,13 @@ class MqttNumber( def should_poll(self): """Return the polling state.""" return False + + @property + def assumed_state(self): + """Return true if we do optimistic updates.""" + return self._optimistic + + @property + def icon(self): + """Return the icon.""" + return self._config.get(CONF_ICON) diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index dc7c7ebfe42..ac5285e9855 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -10,7 +10,8 @@ from homeassistant.components.number import ( DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE, ) -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ASSUMED_STATE, ATTR_ENTITY_ID +import homeassistant.core as ha from homeassistant.setup import async_setup_component from .test_common import ( @@ -40,7 +41,7 @@ from .test_common import ( from tests.common import async_fire_mqtt_message DEFAULT_CONFIG = { - number.DOMAIN: {"platform": "mqtt", "name": "test", "topic": "test_topic"} + number.DOMAIN: {"platform": "mqtt", "name": "test", "command_topic": "test-topic"} } @@ -50,7 +51,14 @@ async def test_run_number_setup(hass, mqtt_mock): await async_setup_component( hass, "number", - {"number": {"platform": "mqtt", "topic": topic, "name": "Test Number"}}, + { + "number": { + "platform": "mqtt", + "state_topic": topic, + "command_topic": topic, + "name": "Test Number", + } + }, ) await hass.async_block_till_done() @@ -72,12 +80,29 @@ async def test_run_number_setup(hass, mqtt_mock): async def test_run_number_service_optimistic(hass, mqtt_mock): """Test that set_value service works in optimistic mode.""" topic = "test/number" - await async_setup_component( - hass, - "number", - {"number": {"platform": "mqtt", "topic": topic, "name": "Test Number"}}, - ) - await hass.async_block_till_done() + + fake_state = ha.State("switch.test", "3") + + with patch( + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", + return_value=fake_state, + ): + assert await async_setup_component( + hass, + number.DOMAIN, + { + "number": { + "platform": "mqtt", + "command_topic": topic, + "name": "Test Number", + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("number.test_number") + assert state.state == "3" + assert state.attributes.get(ATTR_ASSUMED_STATE) # Integer await hass.services.async_call( @@ -119,6 +144,40 @@ async def test_run_number_service_optimistic(hass, mqtt_mock): assert state.state == "42.1" +async def test_run_number_service(hass, mqtt_mock): + """Test that set_value service works in non optimistic mode.""" + cmd_topic = "test/number/set" + state_topic = "test/number" + + assert await async_setup_component( + hass, + number.DOMAIN, + { + "number": { + "platform": "mqtt", + "command_topic": cmd_topic, + "state_topic": state_topic, + "name": "Test Number", + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, state_topic, "32") + state = hass.states.get("number.test_number") + assert state.state == "32" + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: "number.test_number", ATTR_VALUE: 30}, + blocking=True, + ) + mqtt_mock.async_publish.assert_called_once_with(cmd_topic, "30", 0, False) + state = hass.states.get("number.test_number") + assert state.state == "32" + + async def test_availability_when_connection_lost(hass, mqtt_mock): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( @@ -189,13 +248,15 @@ async def test_unique_id(hass, mqtt_mock): { "platform": "mqtt", "name": "Test 1", - "topic": "test-topic", + "state_topic": "test-topic", + "command_topic": "test-topic", "unique_id": "TOTALLY_UNIQUE", }, { "platform": "mqtt", "name": "Test 2", - "topic": "test-topic", + "state_topic": "test-topic", + "command_topic": "test-topic", "unique_id": "TOTALLY_UNIQUE", }, ] @@ -211,8 +272,12 @@ async def test_discovery_removal_number(hass, mqtt_mock, caplog): async def test_discovery_update_number(hass, mqtt_mock, caplog): """Test update of discovered number.""" - data1 = '{ "name": "Beer", "topic": "test_topic"}' - data2 = '{ "name": "Milk", "topic": "test_topic"}' + data1 = ( + '{ "name": "Beer", "state_topic": "test-topic", "command_topic": "test-topic"}' + ) + data2 = ( + '{ "name": "Milk", "state_topic": "test-topic", "command_topic": "test-topic"}' + ) await help_test_discovery_update( hass, mqtt_mock, caplog, number.DOMAIN, data1, data2 @@ -221,7 +286,9 @@ async def test_discovery_update_number(hass, mqtt_mock, caplog): async def test_discovery_update_unchanged_number(hass, mqtt_mock, caplog): """Test update of discovered number.""" - data1 = '{ "name": "Beer", "topic": "test_topic"}' + data1 = ( + '{ "name": "Beer", "state_topic": "test-topic", "command_topic": "test-topic"}' + ) with patch( "homeassistant.components.mqtt.number.MqttNumber.discovery_update" ) as discovery_update: @@ -234,7 +301,9 @@ async def test_discovery_update_unchanged_number(hass, mqtt_mock, caplog): async def test_discovery_broken(hass, mqtt_mock, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' - data2 = '{ "name": "Milk", "topic": "test_topic"}' + data2 = ( + '{ "name": "Milk", "state_topic": "test-topic", "command_topic": "test-topic"}' + ) await help_test_discovery_broken( hass, mqtt_mock, caplog, number.DOMAIN, data1, data2 @@ -272,7 +341,7 @@ async def test_entity_device_info_remove(hass, mqtt_mock): async def test_entity_id_update_subscriptions(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG, ["test_topic"] + hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG ) @@ -286,5 +355,5 @@ async def test_entity_id_update_discovery_update(hass, mqtt_mock): async def test_entity_debug_info_message(hass, mqtt_mock): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG, "test_topic", b"ON" + hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG, payload=b"1" ) From 3569d92385be9069acc53859cc5e3a048e47e26c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 9 Jan 2021 00:58:39 +0200 Subject: [PATCH 140/507] Remove script/test (#44967) It's still referencing tox py36, which has been obsolete for over a year. --- script/test | 6 ------ 1 file changed, 6 deletions(-) delete mode 100755 script/test diff --git a/script/test b/script/test deleted file mode 100755 index 8c4688a4d65..00000000000 --- a/script/test +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -# Executes the tests with tox. - -cd "$(dirname "$0")/.." - -tox -e py36 From 3a88a4120ee77e751bc672b6bab23c143a61b937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 9 Jan 2021 01:08:34 +0200 Subject: [PATCH 141/507] Helpers type hint improvements (#44964) --- homeassistant/helpers/config_validation.py | 4 +- homeassistant/helpers/entity_platform.py | 13 +++--- homeassistant/helpers/script.py | 22 ++++----- homeassistant/helpers/template.py | 53 ++++++++++++---------- 4 files changed, 47 insertions(+), 45 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 0513c5c6e7e..a4499209640 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -556,7 +556,7 @@ def template(value: Optional[Any]) -> template_helper.Template: template_value = template_helper.Template(str(value)) # type: ignore try: - template_value.ensure_valid() # type: ignore[no-untyped-call] + template_value.ensure_valid() return template_value except TemplateError as ex: raise vol.Invalid(f"invalid template ({ex})") from ex @@ -574,7 +574,7 @@ def dynamic_template(value: Optional[Any]) -> template_helper.Template: template_value = template_helper.Template(str(value)) # type: ignore try: - template_value.ensure_valid() # type: ignore[no-untyped-call] + template_value.ensure_valid() return template_value except TemplateError as ex: raise vol.Invalid(f"invalid template ({ex})") from ex diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 7b38c102253..bd687ab7ce8 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -26,7 +26,6 @@ from .event import async_call_later, async_track_time_interval if TYPE_CHECKING: from .entity import Entity -# mypy: allow-untyped-defs SLOW_SETUP_WARNING = 10 SLOW_SETUP_MAX_WAIT = 60 @@ -81,7 +80,7 @@ class EntityPlatform: self.platform_name, [] ).append(self) - def __repr__(self): + def __repr__(self) -> str: """Represent an EntityPlatform.""" return f"" @@ -116,7 +115,7 @@ class EntityPlatform: return self.parallel_updates - async def async_setup(self, platform_config, discovery_info=None): + async def async_setup(self, platform_config, discovery_info=None): # type: ignore[no-untyped-def] """Set up the platform from a config file.""" platform = self.platform hass = self.hass @@ -162,7 +161,7 @@ class EntityPlatform: platform = self.platform @callback - def async_create_setup_task(): + def async_create_setup_task(): # type: ignore[no-untyped-def] """Get task to set up platform.""" return platform.async_setup_entry( # type: ignore self.hass, config_entry, self._async_schedule_add_entities @@ -218,7 +217,7 @@ class EntityPlatform: wait_time, ) - async def setup_again(now): + async def setup_again(now): # type: ignore[no-untyped-def] """Run setup again.""" self._async_cancel_retry_setup = None await self._async_setup_platform(async_create_setup_task, tries) @@ -340,7 +339,7 @@ class EntityPlatform: self.scan_interval, ) - async def _async_add_entity( + async def _async_add_entity( # type: ignore[no-untyped-def] self, entity, update_before_add, entity_registry, device_registry ): """Add an entity to the platform.""" @@ -560,7 +559,7 @@ class EntityPlatform: ) @callback - def async_register_entity_service(self, name, schema, func, required_features=None): + def async_register_entity_service(self, name, schema, func, required_features=None): # type: ignore[no-untyped-def] """Register an entity service. Services will automatically be shared by all platforms of the same domain. diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 48a662e3a81..a2328901d36 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -219,7 +219,7 @@ class _ScriptRun: self._stop = asyncio.Event() self._stopped = asyncio.Event() - def _changed(self): + def _changed(self) -> None: if not self._stop.is_set(): self._script._changed() # pylint: disable=protected-access @@ -227,7 +227,7 @@ class _ScriptRun: # pylint: disable=protected-access return await self._script._async_get_condition(config) - def _log(self, msg, *args, level=logging.INFO): + def _log(self, msg: str, *args: Any, level: int = logging.INFO) -> None: self._script._log(msg, *args, level=level) # pylint: disable=protected-access async def async_run(self) -> None: @@ -257,7 +257,7 @@ class _ScriptRun: self._log_exception(ex) raise - def _finish(self): + def _finish(self) -> None: self._script._runs.remove(self) # pylint: disable=protected-access if not self._script.is_running: self._script.last_action = None @@ -389,7 +389,7 @@ class _ScriptRun: async def _async_run_long_action(self, long_task): """Run a long task while monitoring for stop request.""" - async def async_cancel_long_task(): + async def async_cancel_long_task() -> None: # Stop long task and wait for it to finish. long_task.cancel() try: @@ -586,7 +586,7 @@ class _ScriptRun: else: del self._variables["repeat"] - async def _async_choose_step(self): + async def _async_choose_step(self) -> None: """Choose a sequence.""" # pylint: disable=protected-access choose_data = await self._script._async_get_choose_data(self._step) @@ -706,7 +706,7 @@ class _QueuedScriptRun(_ScriptRun): else: await super().async_run() - def _finish(self): + def _finish(self) -> None: # pylint: disable=protected-access if self.lock_acquired: self._script._queue_lck.release() @@ -868,7 +868,7 @@ class Script: if choose_data["default"]: choose_data["default"].update_logger(self._logger) - def _changed(self): + def _changed(self) -> None: if self._change_listener_job: self._hass.async_run_hass_job(self._change_listener_job) @@ -898,7 +898,7 @@ class Script: if self._referenced_devices is not None: return self._referenced_devices - referenced = set() + referenced: Set[str] = set() for step in self.sequence: action = cv.determine_script_action(step) @@ -927,7 +927,7 @@ class Script: if self._referenced_entities is not None: return self._referenced_entities - referenced = set() + referenced: Set[str] = set() for step in self.sequence: action = cv.determine_script_action(step) @@ -1128,9 +1128,9 @@ class Script: self._choose_data[step] = choose_data return choose_data - def _log(self, msg, *args, level=logging.INFO): + def _log(self, msg: str, *args: Any, level: int = logging.INFO) -> None: msg = f"%s: {msg}" - args = [self.name, *args] + args = (self.name, *args) if level == _LOG_EXCEPTION: self._logger.exception(msg, *args) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index fb3e6ba40b5..5f506c02eef 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -11,7 +11,7 @@ import math from operator import attrgetter import random import re -from typing import Any, Dict, Generator, Iterable, Optional, Type, Union +from typing import Any, Dict, Generator, Iterable, Optional, Type, Union, cast from urllib.parse import urlencode as urllib_urlencode import weakref @@ -38,8 +38,7 @@ from homeassistant.util import convert, dt as dt_util, location as loc_util from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.thread import ThreadWithException -# mypy: allow-untyped-calls, allow-untyped-defs -# mypy: no-check-untyped-defs, no-warn-return-any +# mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) _SENTINEL = object() @@ -140,7 +139,7 @@ def gen_result_wrapper(kls): if kls is set: return str(set(self)) - return kls.__str__(self) + return cast(str, kls.__str__(self)) return self.render_result @@ -173,7 +172,8 @@ class TupleWrapper(tuple, ResultWrapper): RESULT_WRAPPERS: Dict[Type, Type] = { - kls: gen_result_wrapper(kls) for kls in (list, dict, set) + kls: gen_result_wrapper(kls) # type: ignore[no-untyped-call] + for kls in (list, dict, set) } RESULT_WRAPPERS[tuple] = TupleWrapper @@ -195,15 +195,15 @@ class RenderInfo: # Will be set sensibly once frozen. self.filter_lifecycle = _true self.filter = _true - self._result = None + self._result: Optional[str] = None self.is_static = False - self.exception = None + self.exception: Optional[TemplateError] = None self.all_states = False self.all_states_lifecycle = False self.domains = set() self.domains_lifecycle = set() self.entities = set() - self.rate_limit = None + self.rate_limit: Optional[timedelta] = None self.has_time = False def __repr__(self) -> str: @@ -228,7 +228,7 @@ class RenderInfo: """Results of the template computation.""" if self.exception is not None: raise self.exception - return self._result + return cast(str, self._result) def _freeze_static(self) -> None: self.is_static = True @@ -288,26 +288,26 @@ class Template: self.template: str = template.strip() self._compiled_code = None - self._compiled = None + self._compiled: Optional[Template] = None self.hass = hass self.is_static = not is_template_string(template) @property - def _env(self): + def _env(self) -> "TemplateEnvironment": if self.hass is None: return _NO_HASS_ENV - ret = self.hass.data.get(_ENVIRONMENT) + ret: Optional[TemplateEnvironment] = self.hass.data.get(_ENVIRONMENT) if ret is None: - ret = self.hass.data[_ENVIRONMENT] = TemplateEnvironment(self.hass) + ret = self.hass.data[_ENVIRONMENT] = TemplateEnvironment(self.hass) # type: ignore[no-untyped-call] return ret - def ensure_valid(self): + def ensure_valid(self) -> None: """Return if template is valid.""" if self._compiled_code is not None: return try: - self._compiled_code = self._env.compile(self.template) + self._compiled_code = self._env.compile(self.template) # type: ignore[no-untyped-call] except jinja2.TemplateError as err: raise TemplateError(err) from err @@ -422,7 +422,7 @@ class Template: finish_event = asyncio.Event() - def _render_template(): + def _render_template() -> None: try: compiled.render(kwargs) except TimeoutError: @@ -449,7 +449,7 @@ class Template: """Render the template and collect an entity filter.""" assert self.hass and _RENDER_INFO not in self.hass.data - render_info = RenderInfo(self) + render_info = RenderInfo(self) # type: ignore[no-untyped-call] # pylint: disable=protected-access if self.is_static: @@ -519,7 +519,7 @@ class Template: ) return value if error_value is _SENTINEL else error_value - def _ensure_compiled(self): + def _ensure_compiled(self) -> "Template": """Bind a template to a specific hass instance.""" self.ensure_valid() @@ -527,8 +527,9 @@ class Template: env = self._env - self._compiled = jinja2.Template.from_code( - env, self._compiled_code, env.globals, None + self._compiled = cast( + Template, + jinja2.Template.from_code(env, self._compiled_code, env.globals, None), ) return self._compiled @@ -553,7 +554,7 @@ class Template: class AllStates: """Class to expose all HA states as attributes.""" - def __init__(self, hass): + def __init__(self, hass: HomeAssistantType) -> None: """Initialize all states.""" self._hass = hass @@ -607,7 +608,7 @@ class AllStates: class DomainStates: """Class to expose a specific HA domain as attributes.""" - def __init__(self, hass, domain): + def __init__(self, hass: HomeAssistantType, domain: str) -> None: """Initialize the domain states.""" self._hass = hass self._domain = domain @@ -652,13 +653,15 @@ class TemplateState(State): # Inheritance is done so functions that check against State keep working # pylint: disable=super-init-not-called - def __init__(self, hass, state, collect=True): + def __init__( + self, hass: HomeAssistantType, state: State, collect: bool = True + ) -> None: """Initialize template state.""" self._hass = hass self._state = state self._collect = collect - def _collect_state(self): + def _collect_state(self) -> None: if self._collect and _RENDER_INFO in self._hass.data: self._hass.data[_RENDER_INFO].entities.add(self._state.entity_id) @@ -1411,4 +1414,4 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): return cached -_NO_HASS_ENV = TemplateEnvironment(None) +_NO_HASS_ENV = TemplateEnvironment(None) # type: ignore[no-untyped-call] From b85efd343f738bda71f047cffeec6ba06fcac9c4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 9 Jan 2021 00:47:17 +0100 Subject: [PATCH 142/507] Move MQTT entity helpers to separate file (#44838) * Move MQTT entity helpers to separate file * Fix imports * Update MQTT number * Review comments * Fix formatting --- homeassistant/components/mqtt/__init__.py | 453 +---------------- .../components/mqtt/alarm_control_panel.py | 21 +- .../components/mqtt/binary_sensor.py | 25 +- homeassistant/components/mqtt/camera.py | 24 +- homeassistant/components/mqtt/climate.py | 21 +- homeassistant/components/mqtt/cover.py | 21 +- .../components/mqtt/device_automation.py | 3 +- .../mqtt/device_tracker/schema_discovery.py | 23 +- .../components/mqtt/device_trigger.py | 37 +- homeassistant/components/mqtt/fan.py | 21 +- .../components/mqtt/light/__init__.py | 3 +- .../components/mqtt/light/schema_basic.py | 21 +- .../components/mqtt/light/schema_json.py | 21 +- .../components/mqtt/light/schema_template.py | 21 +- homeassistant/components/mqtt/lock.py | 21 +- homeassistant/components/mqtt/mixins.py | 472 ++++++++++++++++++ homeassistant/components/mqtt/number.py | 21 +- homeassistant/components/mqtt/scene.py | 15 +- homeassistant/components/mqtt/sensor.py | 25 +- homeassistant/components/mqtt/switch.py | 21 +- homeassistant/components/mqtt/tag.py | 30 +- .../components/mqtt/vacuum/__init__.py | 3 +- .../components/mqtt/vacuum/schema_legacy.py | 17 +- .../components/mqtt/vacuum/schema_state.py | 21 +- tests/components/mqtt/test_init.py | 15 +- 25 files changed, 716 insertions(+), 660 deletions(-) create mode 100644 homeassistant/components/mqtt/mixins.py diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 6982e4d728f..3b7f6abdc9f 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -3,7 +3,6 @@ import asyncio from functools import lru_cache, partial, wraps import inspect from itertools import groupby -import json import logging from operator import attrgetter import os @@ -20,8 +19,6 @@ from homeassistant import config_entries from homeassistant.components import websocket_api from homeassistant.const import ( CONF_CLIENT_ID, - CONF_DEVICE, - CONF_NAME, CONF_PASSWORD, CONF_PAYLOAD, CONF_PORT, @@ -35,12 +32,7 @@ from homeassistant.const import CONF_UNIQUE_ID # noqa: F401 from homeassistant.core import CoreState, Event, HassJob, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.helpers import config_validation as cv, event, template -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, - dispatcher_send, -) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceDataType from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util @@ -51,9 +43,6 @@ from homeassistant.util.logging import catch_log_exception from . import config_flow # noqa: F401 pylint: disable=unused-import from . import debug_info, discovery from .const import ( - ATTR_DISCOVERY_HASH, - ATTR_DISCOVERY_PAYLOAD, - ATTR_DISCOVERY_TOPIC, ATTR_PAYLOAD, ATTR_QOS, ATTR_RETAIN, @@ -68,8 +57,6 @@ from .const import ( DATA_MQTT_CONFIG, DEFAULT_BIRTH, DEFAULT_DISCOVERY, - DEFAULT_PAYLOAD_AVAILABLE, - DEFAULT_PAYLOAD_NOT_AVAILABLE, DEFAULT_PREFIX, DEFAULT_QOS, DEFAULT_RETAIN, @@ -79,16 +66,8 @@ from .const import ( MQTT_DISCONNECTED, PROTOCOL_311, ) -from .debug_info import log_messages -from .discovery import ( - LAST_DISCOVERY, - MQTT_DISCOVERY_DONE, - MQTT_DISCOVERY_UPDATED, - clear_discovery_hash, - set_discovery_hash, -) +from .discovery import LAST_DISCOVERY from .models import Message, MessageCallbackType, PublishPayloadType -from .subscription import async_subscribe_topics, async_unsubscribe_topics from .util import _VALID_QOS_SCHEMA, valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -108,20 +87,6 @@ CONF_TLS_VERSION = "tls_version" CONF_COMMAND_TOPIC = "command_topic" CONF_TOPIC = "topic" -CONF_AVAILABILITY = "availability" -CONF_AVAILABILITY_TOPIC = "availability_topic" -CONF_PAYLOAD_AVAILABLE = "payload_available" -CONF_PAYLOAD_NOT_AVAILABLE = "payload_not_available" -CONF_JSON_ATTRS_TOPIC = "json_attributes_topic" -CONF_JSON_ATTRS_TEMPLATE = "json_attributes_template" - -CONF_IDENTIFIERS = "identifiers" -CONF_CONNECTIONS = "connections" -CONF_MANUFACTURER = "manufacturer" -CONF_MODEL = "model" -CONF_SW_VERSION = "sw_version" -CONF_VIA_DEVICE = "via_device" -CONF_DEPRECATED_VIA_HUB = "via_hub" PROTOCOL_31 = "3.1" @@ -158,16 +123,6 @@ PLATFORMS = [ ] -def validate_device_has_at_least_one_identifier(value: ConfigType) -> ConfigType: - """Validate that a device info entry has at least one identifying value.""" - if not value.get(CONF_IDENTIFIERS) and not value.get(CONF_CONNECTIONS): - raise vol.Invalid( - "Device must have at least one identifying value in " - "'identifiers' and/or 'connections'" - ) - return value - - CLIENT_KEY_AUTH_MSG = ( "client_key and client_cert must both be present in " "the MQTT broker configuration" @@ -243,69 +198,6 @@ CONFIG_SCHEMA = vol.Schema( SCHEMA_BASE = {vol.Optional(CONF_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA} -MQTT_AVAILABILITY_SINGLE_SCHEMA = vol.Schema( - { - vol.Exclusive(CONF_AVAILABILITY_TOPIC, "availability"): valid_subscribe_topic, - vol.Optional( - CONF_PAYLOAD_AVAILABLE, default=DEFAULT_PAYLOAD_AVAILABLE - ): cv.string, - vol.Optional( - CONF_PAYLOAD_NOT_AVAILABLE, default=DEFAULT_PAYLOAD_NOT_AVAILABLE - ): cv.string, - } -) - -MQTT_AVAILABILITY_LIST_SCHEMA = vol.Schema( - { - vol.Exclusive(CONF_AVAILABILITY, "availability"): vol.All( - cv.ensure_list, - [ - { - vol.Optional(CONF_TOPIC): valid_subscribe_topic, - vol.Optional( - CONF_PAYLOAD_AVAILABLE, default=DEFAULT_PAYLOAD_AVAILABLE - ): cv.string, - vol.Optional( - CONF_PAYLOAD_NOT_AVAILABLE, - default=DEFAULT_PAYLOAD_NOT_AVAILABLE, - ): cv.string, - } - ], - ), - } -) - -MQTT_AVAILABILITY_SCHEMA = MQTT_AVAILABILITY_SINGLE_SCHEMA.extend( - MQTT_AVAILABILITY_LIST_SCHEMA.schema -) - -MQTT_ENTITY_DEVICE_INFO_SCHEMA = vol.All( - cv.deprecated(CONF_DEPRECATED_VIA_HUB, CONF_VIA_DEVICE), - vol.Schema( - { - vol.Optional(CONF_IDENTIFIERS, default=list): vol.All( - cv.ensure_list, [cv.string] - ), - vol.Optional(CONF_CONNECTIONS, default=list): vol.All( - cv.ensure_list, [vol.All(vol.Length(2), [cv.string])] - ), - vol.Optional(CONF_MANUFACTURER): cv.string, - vol.Optional(CONF_MODEL): cv.string, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_SW_VERSION): cv.string, - vol.Optional(CONF_VIA_DEVICE): cv.string, - } - ), - validate_device_has_at_least_one_identifier, -) - -MQTT_JSON_ATTRS_SCHEMA = vol.Schema( - { - vol.Optional(CONF_JSON_ATTRS_TOPIC): valid_subscribe_topic, - vol.Optional(CONF_JSON_ATTRS_TEMPLATE): cv.template, - } -) - MQTT_BASE_PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(SCHEMA_BASE) # Sensor type platforms subscribe to MQTT events @@ -1085,347 +977,6 @@ def _matcher_for_topic(subscription: str) -> Any: return lambda topic: next(matcher.iter_match(topic), False) -class MqttAttributes(Entity): - """Mixin used for platforms that support JSON attributes.""" - - def __init__(self, config: dict) -> None: - """Initialize the JSON attributes mixin.""" - self._attributes = None - self._attributes_sub_state = None - self._attributes_config = config - - async def async_added_to_hass(self) -> None: - """Subscribe MQTT events.""" - await super().async_added_to_hass() - await self._attributes_subscribe_topics() - - async def attributes_discovery_update(self, config: dict): - """Handle updated discovery message.""" - self._attributes_config = config - await self._attributes_subscribe_topics() - - async def _attributes_subscribe_topics(self): - """(Re)Subscribe to topics.""" - attr_tpl = self._attributes_config.get(CONF_JSON_ATTRS_TEMPLATE) - if attr_tpl is not None: - attr_tpl.hass = self.hass - - @callback - @log_messages(self.hass, self.entity_id) - def attributes_message_received(msg: Message) -> None: - try: - payload = msg.payload - if attr_tpl is not None: - payload = attr_tpl.async_render_with_possible_json_value(payload) - json_dict = json.loads(payload) - if isinstance(json_dict, dict): - self._attributes = json_dict - self.async_write_ha_state() - else: - _LOGGER.warning("JSON result was not a dictionary") - self._attributes = None - except ValueError: - _LOGGER.warning("Erroneous JSON: %s", payload) - self._attributes = None - - self._attributes_sub_state = await async_subscribe_topics( - self.hass, - self._attributes_sub_state, - { - CONF_JSON_ATTRS_TOPIC: { - "topic": self._attributes_config.get(CONF_JSON_ATTRS_TOPIC), - "msg_callback": attributes_message_received, - "qos": self._attributes_config.get(CONF_QOS), - } - }, - ) - - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._attributes_sub_state = await async_unsubscribe_topics( - self.hass, self._attributes_sub_state - ) - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self._attributes - - -class MqttAvailability(Entity): - """Mixin used for platforms that report availability.""" - - def __init__(self, config: dict) -> None: - """Initialize the availability mixin.""" - self._availability_sub_state = None - self._available = False - self._availability_setup_from_config(config) - - async def async_added_to_hass(self) -> None: - """Subscribe MQTT events.""" - await super().async_added_to_hass() - await self._availability_subscribe_topics() - self.async_on_remove( - async_dispatcher_connect(self.hass, MQTT_CONNECTED, self.async_mqtt_connect) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, MQTT_DISCONNECTED, self.async_mqtt_connect - ) - ) - - async def availability_discovery_update(self, config: dict): - """Handle updated discovery message.""" - self._availability_setup_from_config(config) - await self._availability_subscribe_topics() - - def _availability_setup_from_config(self, config): - """(Re)Setup.""" - self._avail_topics = {} - if CONF_AVAILABILITY_TOPIC in config: - self._avail_topics[config[CONF_AVAILABILITY_TOPIC]] = { - CONF_PAYLOAD_AVAILABLE: config[CONF_PAYLOAD_AVAILABLE], - CONF_PAYLOAD_NOT_AVAILABLE: config[CONF_PAYLOAD_NOT_AVAILABLE], - } - - if CONF_AVAILABILITY in config: - for avail in config[CONF_AVAILABILITY]: - self._avail_topics[avail[CONF_TOPIC]] = { - CONF_PAYLOAD_AVAILABLE: avail[CONF_PAYLOAD_AVAILABLE], - CONF_PAYLOAD_NOT_AVAILABLE: avail[CONF_PAYLOAD_NOT_AVAILABLE], - } - - self._avail_config = config - - async def _availability_subscribe_topics(self): - """(Re)Subscribe to topics.""" - - @callback - @log_messages(self.hass, self.entity_id) - def availability_message_received(msg: Message) -> None: - """Handle a new received MQTT availability message.""" - topic = msg.topic - if msg.payload == self._avail_topics[topic][CONF_PAYLOAD_AVAILABLE]: - self._available = True - elif msg.payload == self._avail_topics[topic][CONF_PAYLOAD_NOT_AVAILABLE]: - self._available = False - - self.async_write_ha_state() - - topics = {} - for topic in self._avail_topics: - topics[f"availability_{topic}"] = { - "topic": topic, - "msg_callback": availability_message_received, - "qos": self._avail_config[CONF_QOS], - } - - self._availability_sub_state = await async_subscribe_topics( - self.hass, - self._availability_sub_state, - topics, - ) - - @callback - def async_mqtt_connect(self): - """Update state on connection/disconnection to MQTT broker.""" - if not self.hass.is_stopping: - self.async_write_ha_state() - - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._availability_sub_state = await async_unsubscribe_topics( - self.hass, self._availability_sub_state - ) - - @property - def available(self) -> bool: - """Return if the device is available.""" - if not self.hass.data[DATA_MQTT].connected and not self.hass.is_stopping: - return False - return not self._avail_topics or self._available - - -async def cleanup_device_registry(hass, device_id): - """Remove device registry entry if there are no remaining entities or triggers.""" - # Local import to avoid circular dependencies - # pylint: disable=import-outside-toplevel - from . import device_trigger, tag - - 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, include_disabled_entities=True - ) - and not await device_trigger.async_get_triggers(hass, device_id) - and not tag.async_has_tags(hass, device_id) - ): - device_registry.async_remove_device(device_id) - - -class MqttDiscoveryUpdate(Entity): - """Mixin used to handle updated discovery message.""" - - def __init__(self, discovery_data, discovery_update=None) -> None: - """Initialize the discovery update mixin.""" - self._discovery_data = discovery_data - self._discovery_update = discovery_update - self._remove_signal = None - self._removed_from_hass = False - - async def async_added_to_hass(self) -> None: - """Subscribe to discovery updates.""" - await super().async_added_to_hass() - self._removed_from_hass = False - discovery_hash = ( - self._discovery_data[ATTR_DISCOVERY_HASH] if self._discovery_data else None - ) - - async def _async_remove_state_and_registry_entry(self) -> None: - """Remove entity's state and entity registry entry. - - Remove entity from entity registry if it is registered, this also removes the state. - If the entity is not in the entity registry, just remove the state. - """ - entity_registry = ( - await self.hass.helpers.entity_registry.async_get_registry() - ) - if entity_registry.async_is_registered(self.entity_id): - entity_entry = entity_registry.async_get(self.entity_id) - entity_registry.async_remove(self.entity_id) - await cleanup_device_registry(self.hass, entity_entry.device_id) - else: - await self.async_remove() - - async def discovery_callback(payload): - """Handle discovery update.""" - _LOGGER.info( - "Got update for entity with hash: %s '%s'", - discovery_hash, - payload, - ) - old_payload = self._discovery_data[ATTR_DISCOVERY_PAYLOAD] - debug_info.update_entity_discovery_data(self.hass, payload, self.entity_id) - if not payload: - # Empty payload: Remove component - _LOGGER.info("Removing component: %s", self.entity_id) - self._cleanup_discovery_on_remove() - await _async_remove_state_and_registry_entry(self) - elif self._discovery_update: - if old_payload != self._discovery_data[ATTR_DISCOVERY_PAYLOAD]: - # Non-empty, changed payload: Notify component - _LOGGER.info("Updating component: %s", self.entity_id) - await self._discovery_update(payload) - else: - # Non-empty, unchanged payload: Ignore to avoid changing states - _LOGGER.info("Ignoring unchanged update for: %s", self.entity_id) - async_dispatcher_send( - self.hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None - ) - - if discovery_hash: - debug_info.add_entity_discovery_data( - self.hass, self._discovery_data, self.entity_id - ) - # Set in case the entity has been removed and is re-added, for example when changing entity_id - set_discovery_hash(self.hass, discovery_hash) - self._remove_signal = async_dispatcher_connect( - self.hass, - MQTT_DISCOVERY_UPDATED.format(discovery_hash), - discovery_callback, - ) - async_dispatcher_send( - self.hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None - ) - - async def async_removed_from_registry(self) -> None: - """Clear retained discovery topic in broker.""" - if not self._removed_from_hass: - discovery_topic = self._discovery_data[ATTR_DISCOVERY_TOPIC] - publish(self.hass, discovery_topic, "", retain=True) - - @callback - def add_to_platform_abort(self) -> None: - """Abort adding an entity to a platform.""" - if self._discovery_data: - discovery_hash = self._discovery_data[ATTR_DISCOVERY_HASH] - clear_discovery_hash(self.hass, discovery_hash) - async_dispatcher_send( - self.hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None - ) - super().add_to_platform_abort() - - async def async_will_remove_from_hass(self) -> None: - """Stop listening to signal and cleanup discovery data..""" - self._cleanup_discovery_on_remove() - - def _cleanup_discovery_on_remove(self) -> None: - """Stop listening to signal and cleanup discovery data.""" - if self._discovery_data and not self._removed_from_hass: - debug_info.remove_entity_data(self.hass, self.entity_id) - clear_discovery_hash(self.hass, self._discovery_data[ATTR_DISCOVERY_HASH]) - self._removed_from_hass = True - - if self._remove_signal: - self._remove_signal() - self._remove_signal = None - - -def device_info_from_config(config): - """Return a device description for device registry.""" - if not config: - return None - - info = { - "identifiers": {(DOMAIN, id_) for id_ in config[CONF_IDENTIFIERS]}, - "connections": {tuple(x) for x in config[CONF_CONNECTIONS]}, - } - - if CONF_MANUFACTURER in config: - info["manufacturer"] = config[CONF_MANUFACTURER] - - if CONF_MODEL in config: - info["model"] = config[CONF_MODEL] - - if CONF_NAME in config: - info["name"] = config[CONF_NAME] - - if CONF_SW_VERSION in config: - info["sw_version"] = config[CONF_SW_VERSION] - - if CONF_VIA_DEVICE in config: - info["via_device"] = (DOMAIN, config[CONF_VIA_DEVICE]) - - return info - - -class MqttEntityDeviceInfo(Entity): - """Mixin used for mqtt platforms that support the device registry.""" - - def __init__(self, device_config: Optional[ConfigType], config_entry=None) -> None: - """Initialize the device mixin.""" - self._device_config = device_config - self._config_entry = config_entry - - async def device_info_discovery_update(self, config: dict): - """Handle updated discovery message.""" - self._device_config = config.get(CONF_DEVICE) - device_registry = await self.hass.helpers.device_registry.async_get_registry() - config_entry_id = self._config_entry.entry_id - device_info = self.device_info - - if config_entry_id is not None and device_info is not None: - device_info["config_entry_id"] = config_entry_id - device_registry.async_get_or_create(**device_info) - - @property - def device_info(self): - """Return a device description for device registry.""" - return device_info_from_config(self._device_config) - - @websocket_api.websocket_command( {vol.Required("type"): "mqtt/device/debug_info", vol.Required("device_id"): str} ) diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 46eaa912615..21234fbc5b4 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -37,22 +37,27 @@ 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, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, + subscription, +) +from .. import mqtt +from .const import ATTR_DISCOVERY_HASH +from .debug_info import log_messages +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash +from .mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - subscription, ) -from .. import mqtt -from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -82,7 +87,7 @@ PLATFORM_SCHEMA = ( CONF_COMMAND_TEMPLATE, default=DEFAULT_COMMAND_TEMPLATE ): cv.template, vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string, vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string, @@ -97,8 +102,8 @@ PLATFORM_SCHEMA = ( vol.Optional(CONF_VALUE_TEMPLATE): cv.template, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) ) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 17dcd301cd0..c604181bdad 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -31,21 +31,20 @@ from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util import dt as dt_util -from . import ( - ATTR_DISCOVERY_HASH, - CONF_QOS, - CONF_STATE_TOPIC, - DOMAIN, - PLATFORMS, +from . import CONF_QOS, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, subscription +from .. import mqtt +from .const import ATTR_DISCOVERY_HASH +from .debug_info import log_messages +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash +from .mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - subscription, ) -from .. import mqtt -from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -59,7 +58,7 @@ CONF_EXPIRE_AFTER = "expire_after" PLATFORM_SCHEMA = ( mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, @@ -70,8 +69,8 @@ PLATFORM_SCHEMA = ( vol.Optional(CONF_UNIQUE_ID): cv.string, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) ) diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 20d68d1c4b0..3888fcd9663 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -15,20 +15,20 @@ from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from . import ( - ATTR_DISCOVERY_HASH, - CONF_QOS, - DOMAIN, - PLATFORMS, +from . import CONF_QOS, DOMAIN, PLATFORMS, subscription +from .. import mqtt +from .const import ATTR_DISCOVERY_HASH +from .debug_info import log_messages +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash +from .mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - subscription, ) -from .. import mqtt -from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -38,14 +38,14 @@ DEFAULT_NAME = "MQTT Camera" PLATFORM_SCHEMA = ( mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_UNIQUE_ID): cv.string, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) ) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 715658417b2..77503335ded 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -55,21 +55,26 @@ from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import ( - ATTR_DISCOVERY_HASH, CONF_QOS, CONF_RETAIN, DOMAIN, MQTT_BASE_PLATFORM_SCHEMA, PLATFORMS, + subscription, +) +from .. import mqtt +from .const import ATTR_DISCOVERY_HASH +from .debug_info import log_messages +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash +from .mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - subscription, ) -from .. import mqtt -from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -174,7 +179,7 @@ PLATFORM_SCHEMA = ( vol.Optional(CONF_AWAY_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_CURRENT_TEMP_TEMPLATE): cv.template, vol.Optional(CONF_CURRENT_TEMP_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_FAN_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional( CONF_FAN_MODE_LIST, @@ -237,8 +242,8 @@ PLATFORM_SCHEMA = ( vol.Optional(CONF_VALUE_TEMPLATE): cv.template, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) ) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 59dedd7f475..6de560b52b1 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -41,22 +41,27 @@ 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, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, + subscription, +) +from .. import mqtt +from .const import ATTR_DISCOVERY_HASH +from .debug_info import log_messages +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash +from .mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - subscription, ) -from .. import mqtt -from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -126,7 +131,7 @@ PLATFORM_SCHEMA = vol.All( mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_GET_POSITION_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -167,8 +172,8 @@ PLATFORM_SCHEMA = vol.All( vol.Optional(CONF_VALUE_TEMPLATE): cv.template, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema), + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema), validate_options, ) diff --git a/homeassistant/components/mqtt/device_automation.py b/homeassistant/components/mqtt/device_automation.py index 1186a212243..423690d8e69 100644 --- a/homeassistant/components/mqtt/device_automation.py +++ b/homeassistant/components/mqtt/device_automation.py @@ -9,8 +9,9 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) -from . import ATTR_DISCOVERY_HASH, device_trigger +from . import device_trigger from .. import mqtt +from .const import ATTR_DISCOVERY_HASH from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker/schema_discovery.py index 08a4871084c..9fa40c18037 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_discovery.py +++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py @@ -25,17 +25,20 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) -from .. import ( - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - subscription, -) +from .. import subscription from ... import mqtt from ..const import ATTR_DISCOVERY_HASH, CONF_QOS, CONF_STATE_TOPIC from ..debug_info import log_messages from ..discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash +from ..mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, + MqttAttributes, + MqttAvailability, + MqttDiscoveryUpdate, + MqttEntityDeviceInfo, +) _LOGGER = logging.getLogger(__name__) @@ -46,7 +49,7 @@ CONF_SOURCE_TYPE = "source_type" PLATFORM_SCHEMA_DISCOVERY = ( mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PAYLOAD_HOME, default=STATE_HOME): cv.string, @@ -55,8 +58,8 @@ PLATFORM_SCHEMA_DISCOVERY = ( vol.Optional(CONF_UNIQUE_ID): cv.string, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) ) diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 8a8b525da61..6a04fd48049 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -7,7 +7,13 @@ import voluptuous as vol from homeassistant.components.automation import AutomationActionType from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA -from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE +from homeassistant.const import ( + CONF_DEVICE, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_PLATFORM, + CONF_TYPE, +) from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv @@ -17,21 +23,18 @@ from homeassistant.helpers.dispatcher import ( ) from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from . import ( - ATTR_DISCOVERY_HASH, - ATTR_DISCOVERY_TOPIC, - CONF_CONNECTIONS, - CONF_DEVICE, - CONF_IDENTIFIERS, - CONF_PAYLOAD, - CONF_QOS, - DOMAIN, - cleanup_device_registry, - debug_info, - trigger as mqtt_trigger, -) +from . import CONF_PAYLOAD, CONF_QOS, DOMAIN, debug_info, trigger as mqtt_trigger from .. import mqtt +from .const import ATTR_DISCOVERY_HASH, ATTR_DISCOVERY_TOPIC from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_UPDATED, clear_discovery_hash +from .mixins import ( + CONF_CONNECTIONS, + CONF_IDENTIFIERS, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + cleanup_device_registry, + device_info_from_config, + validate_device_has_at_least_one_identifier, +) _LOGGER = logging.getLogger(__name__) @@ -62,13 +65,13 @@ TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( TRIGGER_DISCOVERY_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( { vol.Required(CONF_AUTOMATION_TYPE): str, - vol.Required(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Required(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_PAYLOAD, default=None): vol.Any(None, cv.string), vol.Required(CONF_TYPE): cv.string, vol.Required(CONF_SUBTYPE): cv.string, }, - mqtt.validate_device_has_at_least_one_identifier, + validate_device_has_at_least_one_identifier, ) DEVICE_TRIGGERS = "mqtt_device_triggers" @@ -172,7 +175,7 @@ async def _update_device(hass, config_entry, config): """Update device registry.""" device_registry = await hass.helpers.device_registry.async_get_registry() config_entry_id = config_entry.entry_id - device_info = mqtt.device_info_from_config(config[CONF_DEVICE]) + device_info = device_info_from_config(config[CONF_DEVICE]) if config_entry_id is not None and device_info is not None: device_info["config_entry_id"] = config_entry_id diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index cc635ab6e45..eb713655821 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -33,22 +33,27 @@ 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, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, + subscription, +) +from .. import mqtt +from .const import ATTR_DISCOVERY_HASH +from .debug_info import log_messages +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash +from .mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - subscription, ) -from .. import mqtt -from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -80,7 +85,7 @@ OSCILLATION = "oscillation" PLATFORM_SCHEMA = ( mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_OSCILLATION_COMMAND_TOPIC): mqtt.valid_publish_topic, @@ -109,8 +114,8 @@ PLATFORM_SCHEMA = ( vol.Optional(CONF_UNIQUE_ID): cv.string, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) ) diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index 1ab0888866c..cfeaaa0d7c9 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -11,7 +11,8 @@ from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from .. import ATTR_DISCOVERY_HASH, DOMAIN, PLATFORMS +from .. import DOMAIN, PLATFORMS +from ..const import ATTR_DISCOVERY_HASH from ..discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash from .schema import CONF_SCHEMA, MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import PLATFORM_SCHEMA_BASIC, async_setup_entity_basic diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 00ad2671391..230efa5e60a 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -31,19 +31,18 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.color as color_util -from .. import ( - CONF_COMMAND_TOPIC, - CONF_QOS, - CONF_RETAIN, - CONF_STATE_TOPIC, +from .. import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, subscription +from ... import mqtt +from ..debug_info import log_messages +from ..mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - subscription, ) -from ... import mqtt -from ..debug_info import log_messages from .schema import MQTT_LIGHT_SCHEMA_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -114,7 +113,7 @@ PLATFORM_SCHEMA_BASIC = ( vol.Optional(CONF_COLOR_TEMP_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_COLOR_TEMP_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_COLOR_TEMP_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_EFFECT_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_EFFECT_STATE_TOPIC): mqtt.valid_subscribe_topic, @@ -148,8 +147,8 @@ PLATFORM_SCHEMA_BASIC = ( vol.Optional(CONF_XY_VALUE_TEMPLATE): cv.template, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema) ) diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index bb10fd52ae7..16281da4169 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -42,19 +42,18 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType import homeassistant.util.color as color_util -from .. import ( - CONF_COMMAND_TOPIC, - CONF_QOS, - CONF_RETAIN, - CONF_STATE_TOPIC, +from .. import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, subscription +from ... import mqtt +from ..debug_info import log_messages +from ..mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - subscription, ) -from ... import mqtt -from ..debug_info import log_messages from .schema import MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import CONF_BRIGHTNESS_SCALE @@ -93,7 +92,7 @@ PLATFORM_SCHEMA_JSON = ( CONF_BRIGHTNESS_SCALE, default=DEFAULT_BRIGHTNESS_SCALE ): vol.All(vol.Coerce(int), vol.Range(min=1)), vol.Optional(CONF_COLOR_TEMP, default=DEFAULT_COLOR_TEMP): cv.boolean, - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_EFFECT, default=DEFAULT_EFFECT): cv.boolean, vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]), vol.Optional( @@ -118,8 +117,8 @@ PLATFORM_SCHEMA_JSON = ( vol.Optional(CONF_XY, default=DEFAULT_XY): cv.boolean, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema) ) diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index e6b22da5af0..e33d169e47a 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -33,19 +33,18 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.color as color_util -from .. import ( - CONF_COMMAND_TOPIC, - CONF_QOS, - CONF_RETAIN, - CONF_STATE_TOPIC, +from .. import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, subscription +from ... import mqtt +from ..debug_info import log_messages +from ..mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - subscription, ) -from ... import mqtt -from ..debug_info import log_messages from .schema import MQTT_LIGHT_SCHEMA_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -77,7 +76,7 @@ PLATFORM_SCHEMA_TEMPLATE = ( vol.Optional(CONF_COLOR_TEMP_TEMPLATE): cv.template, vol.Required(CONF_COMMAND_OFF_TEMPLATE): cv.template, vol.Required(CONF_COMMAND_ON_TEMPLATE): cv.template, - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_EFFECT_TEMPLATE): cv.template, vol.Optional(CONF_GREEN_TEMPLATE): cv.template, @@ -91,8 +90,8 @@ PLATFORM_SCHEMA_TEMPLATE = ( vol.Optional(CONF_WHITE_VALUE_TEMPLATE): cv.template, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema) ) diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 70c771ba22d..72df487d22a 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -22,22 +22,27 @@ 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, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, + subscription, +) +from .. import mqtt +from .const import ATTR_DISCOVERY_HASH +from .debug_info import log_messages +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash +from .mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - subscription, ) -from .. import mqtt -from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -57,7 +62,7 @@ DEFAULT_STATE_UNLOCKED = "UNLOCKED" PLATFORM_SCHEMA = ( mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_PAYLOAD_LOCK, default=DEFAULT_PAYLOAD_LOCK): cv.string, @@ -71,8 +76,8 @@ PLATFORM_SCHEMA = ( vol.Optional(CONF_UNIQUE_ID): cv.string, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) ) diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py new file mode 100644 index 00000000000..c627a254791 --- /dev/null +++ b/homeassistant/components/mqtt/mixins.py @@ -0,0 +1,472 @@ +"""MQTT component mixins and helpers.""" +import json +import logging +from typing import Optional + +import voluptuous as vol + +from homeassistant.const import CONF_DEVICE, CONF_NAME +from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import ConfigType + +from . import CONF_TOPIC, DATA_MQTT, debug_info, publish +from .const import ( + ATTR_DISCOVERY_HASH, + ATTR_DISCOVERY_PAYLOAD, + ATTR_DISCOVERY_TOPIC, + CONF_QOS, + DEFAULT_PAYLOAD_AVAILABLE, + DEFAULT_PAYLOAD_NOT_AVAILABLE, + DOMAIN, + MQTT_CONNECTED, + MQTT_DISCONNECTED, +) +from .debug_info import log_messages +from .discovery import ( + MQTT_DISCOVERY_DONE, + MQTT_DISCOVERY_UPDATED, + clear_discovery_hash, + set_discovery_hash, +) +from .models import Message +from .subscription import async_subscribe_topics, async_unsubscribe_topics +from .util import valid_subscribe_topic + +_LOGGER = logging.getLogger(__name__) + +CONF_AVAILABILITY = "availability" +CONF_AVAILABILITY_TOPIC = "availability_topic" +CONF_PAYLOAD_AVAILABLE = "payload_available" +CONF_PAYLOAD_NOT_AVAILABLE = "payload_not_available" +CONF_JSON_ATTRS_TOPIC = "json_attributes_topic" +CONF_JSON_ATTRS_TEMPLATE = "json_attributes_template" + +CONF_IDENTIFIERS = "identifiers" +CONF_CONNECTIONS = "connections" +CONF_MANUFACTURER = "manufacturer" +CONF_MODEL = "model" +CONF_SW_VERSION = "sw_version" +CONF_VIA_DEVICE = "via_device" +CONF_DEPRECATED_VIA_HUB = "via_hub" + +MQTT_AVAILABILITY_SINGLE_SCHEMA = vol.Schema( + { + vol.Exclusive(CONF_AVAILABILITY_TOPIC, "availability"): valid_subscribe_topic, + vol.Optional( + CONF_PAYLOAD_AVAILABLE, default=DEFAULT_PAYLOAD_AVAILABLE + ): cv.string, + vol.Optional( + CONF_PAYLOAD_NOT_AVAILABLE, default=DEFAULT_PAYLOAD_NOT_AVAILABLE + ): cv.string, + } +) + +MQTT_AVAILABILITY_LIST_SCHEMA = vol.Schema( + { + vol.Exclusive(CONF_AVAILABILITY, "availability"): vol.All( + cv.ensure_list, + [ + { + vol.Optional(CONF_TOPIC): valid_subscribe_topic, + vol.Optional( + CONF_PAYLOAD_AVAILABLE, default=DEFAULT_PAYLOAD_AVAILABLE + ): cv.string, + vol.Optional( + CONF_PAYLOAD_NOT_AVAILABLE, + default=DEFAULT_PAYLOAD_NOT_AVAILABLE, + ): cv.string, + } + ], + ), + } +) + +MQTT_AVAILABILITY_SCHEMA = MQTT_AVAILABILITY_SINGLE_SCHEMA.extend( + MQTT_AVAILABILITY_LIST_SCHEMA.schema +) + + +def validate_device_has_at_least_one_identifier(value: ConfigType) -> ConfigType: + """Validate that a device info entry has at least one identifying value.""" + if value.get(CONF_IDENTIFIERS) or value.get(CONF_CONNECTIONS): + return value + raise vol.Invalid( + "Device must have at least one identifying value in " + "'identifiers' and/or 'connections'" + ) + + +MQTT_ENTITY_DEVICE_INFO_SCHEMA = vol.All( + cv.deprecated(CONF_DEPRECATED_VIA_HUB, CONF_VIA_DEVICE), + vol.Schema( + { + vol.Optional(CONF_IDENTIFIERS, default=list): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(CONF_CONNECTIONS, default=list): vol.All( + cv.ensure_list, [vol.All(vol.Length(2), [cv.string])] + ), + vol.Optional(CONF_MANUFACTURER): cv.string, + vol.Optional(CONF_MODEL): cv.string, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_SW_VERSION): cv.string, + vol.Optional(CONF_VIA_DEVICE): cv.string, + } + ), + validate_device_has_at_least_one_identifier, +) + +MQTT_JSON_ATTRS_SCHEMA = vol.Schema( + { + vol.Optional(CONF_JSON_ATTRS_TOPIC): valid_subscribe_topic, + vol.Optional(CONF_JSON_ATTRS_TEMPLATE): cv.template, + } +) + + +class MqttAttributes(Entity): + """Mixin used for platforms that support JSON attributes.""" + + def __init__(self, config: dict) -> None: + """Initialize the JSON attributes mixin.""" + self._attributes = None + self._attributes_sub_state = None + self._attributes_config = config + + async def async_added_to_hass(self) -> None: + """Subscribe MQTT events.""" + await super().async_added_to_hass() + await self._attributes_subscribe_topics() + + async def attributes_discovery_update(self, config: dict): + """Handle updated discovery message.""" + self._attributes_config = config + await self._attributes_subscribe_topics() + + async def _attributes_subscribe_topics(self): + """(Re)Subscribe to topics.""" + attr_tpl = self._attributes_config.get(CONF_JSON_ATTRS_TEMPLATE) + if attr_tpl is not None: + attr_tpl.hass = self.hass + + @callback + @log_messages(self.hass, self.entity_id) + def attributes_message_received(msg: Message) -> None: + try: + payload = msg.payload + if attr_tpl is not None: + payload = attr_tpl.async_render_with_possible_json_value(payload) + json_dict = json.loads(payload) + if isinstance(json_dict, dict): + self._attributes = json_dict + self.async_write_ha_state() + else: + _LOGGER.warning("JSON result was not a dictionary") + self._attributes = None + except ValueError: + _LOGGER.warning("Erroneous JSON: %s", payload) + self._attributes = None + + self._attributes_sub_state = await async_subscribe_topics( + self.hass, + self._attributes_sub_state, + { + CONF_JSON_ATTRS_TOPIC: { + "topic": self._attributes_config.get(CONF_JSON_ATTRS_TOPIC), + "msg_callback": attributes_message_received, + "qos": self._attributes_config.get(CONF_QOS), + } + }, + ) + + async def async_will_remove_from_hass(self): + """Unsubscribe when removed.""" + self._attributes_sub_state = await async_unsubscribe_topics( + self.hass, self._attributes_sub_state + ) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes + + +class MqttAvailability(Entity): + """Mixin used for platforms that report availability.""" + + def __init__(self, config: dict) -> None: + """Initialize the availability mixin.""" + self._availability_sub_state = None + self._available = False + self._availability_setup_from_config(config) + + async def async_added_to_hass(self) -> None: + """Subscribe MQTT events.""" + await super().async_added_to_hass() + await self._availability_subscribe_topics() + self.async_on_remove( + async_dispatcher_connect(self.hass, MQTT_CONNECTED, self.async_mqtt_connect) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, MQTT_DISCONNECTED, self.async_mqtt_connect + ) + ) + + async def availability_discovery_update(self, config: dict): + """Handle updated discovery message.""" + self._availability_setup_from_config(config) + await self._availability_subscribe_topics() + + def _availability_setup_from_config(self, config): + """(Re)Setup.""" + self._avail_topics = {} + if CONF_AVAILABILITY_TOPIC in config: + self._avail_topics[config[CONF_AVAILABILITY_TOPIC]] = { + CONF_PAYLOAD_AVAILABLE: config[CONF_PAYLOAD_AVAILABLE], + CONF_PAYLOAD_NOT_AVAILABLE: config[CONF_PAYLOAD_NOT_AVAILABLE], + } + + if CONF_AVAILABILITY in config: + for avail in config[CONF_AVAILABILITY]: + self._avail_topics[avail[CONF_TOPIC]] = { + CONF_PAYLOAD_AVAILABLE: avail[CONF_PAYLOAD_AVAILABLE], + CONF_PAYLOAD_NOT_AVAILABLE: avail[CONF_PAYLOAD_NOT_AVAILABLE], + } + + self._avail_config = config + + async def _availability_subscribe_topics(self): + """(Re)Subscribe to topics.""" + + @callback + @log_messages(self.hass, self.entity_id) + def availability_message_received(msg: Message) -> None: + """Handle a new received MQTT availability message.""" + topic = msg.topic + if msg.payload == self._avail_topics[topic][CONF_PAYLOAD_AVAILABLE]: + self._available = True + elif msg.payload == self._avail_topics[topic][CONF_PAYLOAD_NOT_AVAILABLE]: + self._available = False + + self.async_write_ha_state() + + topics = { + f"availability_{topic}": { + "topic": topic, + "msg_callback": availability_message_received, + "qos": self._avail_config[CONF_QOS], + } + for topic in self._avail_topics + } + + self._availability_sub_state = await async_subscribe_topics( + self.hass, + self._availability_sub_state, + topics, + ) + + @callback + def async_mqtt_connect(self): + """Update state on connection/disconnection to MQTT broker.""" + if not self.hass.is_stopping: + self.async_write_ha_state() + + async def async_will_remove_from_hass(self): + """Unsubscribe when removed.""" + self._availability_sub_state = await async_unsubscribe_topics( + self.hass, self._availability_sub_state + ) + + @property + def available(self) -> bool: + """Return if the device is available.""" + if not self.hass.data[DATA_MQTT].connected and not self.hass.is_stopping: + return False + return not self._avail_topics or self._available + + +async def cleanup_device_registry(hass, device_id): + """Remove device registry entry if there are no remaining entities or triggers.""" + # Local import to avoid circular dependencies + # pylint: disable=import-outside-toplevel + from . import device_trigger, tag + + 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, include_disabled_entities=True + ) + and not await device_trigger.async_get_triggers(hass, device_id) + and not tag.async_has_tags(hass, device_id) + ): + device_registry.async_remove_device(device_id) + + +class MqttDiscoveryUpdate(Entity): + """Mixin used to handle updated discovery message.""" + + def __init__(self, discovery_data, discovery_update=None) -> None: + """Initialize the discovery update mixin.""" + self._discovery_data = discovery_data + self._discovery_update = discovery_update + self._remove_signal = None + self._removed_from_hass = False + + async def async_added_to_hass(self) -> None: + """Subscribe to discovery updates.""" + await super().async_added_to_hass() + self._removed_from_hass = False + discovery_hash = ( + self._discovery_data[ATTR_DISCOVERY_HASH] if self._discovery_data else None + ) + + async def _async_remove_state_and_registry_entry(self) -> None: + """Remove entity's state and entity registry entry. + + Remove entity from entity registry if it is registered, this also removes the state. + If the entity is not in the entity registry, just remove the state. + """ + entity_registry = ( + await self.hass.helpers.entity_registry.async_get_registry() + ) + if entity_registry.async_is_registered(self.entity_id): + entity_entry = entity_registry.async_get(self.entity_id) + entity_registry.async_remove(self.entity_id) + await cleanup_device_registry(self.hass, entity_entry.device_id) + else: + await self.async_remove() + + async def discovery_callback(payload): + """Handle discovery update.""" + _LOGGER.info( + "Got update for entity with hash: %s '%s'", + discovery_hash, + payload, + ) + old_payload = self._discovery_data[ATTR_DISCOVERY_PAYLOAD] + debug_info.update_entity_discovery_data(self.hass, payload, self.entity_id) + if not payload: + # Empty payload: Remove component + _LOGGER.info("Removing component: %s", self.entity_id) + self._cleanup_discovery_on_remove() + await _async_remove_state_and_registry_entry(self) + elif self._discovery_update: + if old_payload != self._discovery_data[ATTR_DISCOVERY_PAYLOAD]: + # Non-empty, changed payload: Notify component + _LOGGER.info("Updating component: %s", self.entity_id) + await self._discovery_update(payload) + else: + # Non-empty, unchanged payload: Ignore to avoid changing states + _LOGGER.info("Ignoring unchanged update for: %s", self.entity_id) + async_dispatcher_send( + self.hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) + + if discovery_hash: + debug_info.add_entity_discovery_data( + self.hass, self._discovery_data, self.entity_id + ) + # Set in case the entity has been removed and is re-added, for example when changing entity_id + set_discovery_hash(self.hass, discovery_hash) + self._remove_signal = async_dispatcher_connect( + self.hass, + MQTT_DISCOVERY_UPDATED.format(discovery_hash), + discovery_callback, + ) + async_dispatcher_send( + self.hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) + + async def async_removed_from_registry(self) -> None: + """Clear retained discovery topic in broker.""" + if not self._removed_from_hass: + discovery_topic = self._discovery_data[ATTR_DISCOVERY_TOPIC] + publish(self.hass, discovery_topic, "", retain=True) + + @callback + def add_to_platform_abort(self) -> None: + """Abort adding an entity to a platform.""" + if self._discovery_data: + discovery_hash = self._discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(self.hass, discovery_hash) + async_dispatcher_send( + self.hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) + super().add_to_platform_abort() + + async def async_will_remove_from_hass(self) -> None: + """Stop listening to signal and cleanup discovery data..""" + self._cleanup_discovery_on_remove() + + def _cleanup_discovery_on_remove(self) -> None: + """Stop listening to signal and cleanup discovery data.""" + if self._discovery_data and not self._removed_from_hass: + debug_info.remove_entity_data(self.hass, self.entity_id) + clear_discovery_hash(self.hass, self._discovery_data[ATTR_DISCOVERY_HASH]) + self._removed_from_hass = True + + if self._remove_signal: + self._remove_signal() + self._remove_signal = None + + +def device_info_from_config(config): + """Return a device description for device registry.""" + if not config: + return None + + info = { + "identifiers": {(DOMAIN, id_) for id_ in config[CONF_IDENTIFIERS]}, + "connections": {tuple(x) for x in config[CONF_CONNECTIONS]}, + } + + if CONF_MANUFACTURER in config: + info["manufacturer"] = config[CONF_MANUFACTURER] + + if CONF_MODEL in config: + info["model"] = config[CONF_MODEL] + + if CONF_NAME in config: + info["name"] = config[CONF_NAME] + + if CONF_SW_VERSION in config: + info["sw_version"] = config[CONF_SW_VERSION] + + if CONF_VIA_DEVICE in config: + info["via_device"] = (DOMAIN, config[CONF_VIA_DEVICE]) + + return info + + +class MqttEntityDeviceInfo(Entity): + """Mixin used for mqtt platforms that support the device registry.""" + + def __init__(self, device_config: Optional[ConfigType], config_entry=None) -> None: + """Initialize the device mixin.""" + self._device_config = device_config + self._config_entry = config_entry + + async def device_info_discovery_update(self, config: dict): + """Handle updated discovery message.""" + self._device_config = config.get(CONF_DEVICE) + device_registry = await self.hass.helpers.device_registry.async_get_registry() + config_entry_id = self._config_entry.entry_id + device_info = self.device_info + + if config_entry_id is not None and device_info is not None: + device_info["config_entry_id"] = config_entry_id + device_registry.async_get_or_create(**device_info) + + @property + def device_info(self): + """Return a device description for device registry.""" + return device_info_from_config(self._device_config) diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index bac70723eeb..7196b348a6f 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -23,21 +23,26 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import ( - ATTR_DISCOVERY_HASH, CONF_COMMAND_TOPIC, CONF_QOS, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, + subscription, +) +from .. import mqtt +from .const import ATTR_DISCOVERY_HASH +from .debug_info import log_messages +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash +from .mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - subscription, ) -from .. import mqtt -from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -47,15 +52,15 @@ DEFAULT_OPTIMISTIC = False PLATFORM_SCHEMA = ( mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_UNIQUE_ID): cv.string, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) ) diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index eebcdc26bac..2923f512d02 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -14,18 +14,11 @@ from homeassistant.helpers.dispatcher import ( 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 . import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, DOMAIN, PLATFORMS from .. import mqtt +from .const import ATTR_DISCOVERY_HASH from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash +from .mixins import MQTT_AVAILABILITY_SCHEMA, MqttAvailability, MqttDiscoveryUpdate _LOGGER = logging.getLogger(__name__) @@ -41,7 +34,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, } -).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) +).extend(MQTT_AVAILABILITY_SCHEMA.schema) async def async_setup_platform( diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index cfcd46c9e95..5ddfc29d80f 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -29,21 +29,20 @@ from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util import dt as dt_util -from . import ( - ATTR_DISCOVERY_HASH, - CONF_QOS, - CONF_STATE_TOPIC, - DOMAIN, - PLATFORMS, +from . import CONF_QOS, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, subscription +from .. import mqtt +from .const import ATTR_DISCOVERY_HASH +from .debug_info import log_messages +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash +from .mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - subscription, ) -from .. import mqtt -from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -54,7 +53,7 @@ DEFAULT_FORCE_UPDATE = False PLATFORM_SCHEMA = ( mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, @@ -64,8 +63,8 @@ PLATFORM_SCHEMA = ( vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) ) diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index e074cb819d2..35a436b0be8 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -27,22 +27,27 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import ( - ATTR_DISCOVERY_HASH, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, + subscription, +) +from .. import mqtt +from .const import ATTR_DISCOVERY_HASH +from .debug_info import log_messages +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash +from .mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - subscription, ) -from .. import mqtt -from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -56,7 +61,7 @@ CONF_STATE_OFF = "state_off" PLATFORM_SCHEMA = ( mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, @@ -67,8 +72,8 @@ PLATFORM_SCHEMA = ( vol.Optional(CONF_UNIQUE_ID): cv.string, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) ) diff --git a/homeassistant/components/mqtt/tag.py b/homeassistant/components/mqtt/tag.py index 1185f925b74..c4db7b5f4b9 100644 --- a/homeassistant/components/mqtt/tag.py +++ b/homeassistant/components/mqtt/tag.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.const import CONF_PLATFORM, CONF_VALUE_TEMPLATE +from homeassistant.const import CONF_DEVICE, CONF_PLATFORM, CONF_VALUE_TEMPLATE import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.helpers.dispatcher import ( @@ -11,25 +11,23 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) -from . import ( - ATTR_DISCOVERY_HASH, - ATTR_DISCOVERY_TOPIC, - CONF_CONNECTIONS, - CONF_DEVICE, - CONF_IDENTIFIERS, - CONF_QOS, - CONF_TOPIC, - DOMAIN, - cleanup_device_registry, - subscription, -) +from . import CONF_QOS, CONF_TOPIC, DOMAIN, subscription from .. import mqtt +from .const import ATTR_DISCOVERY_HASH, ATTR_DISCOVERY_TOPIC from .discovery import ( MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, MQTT_DISCOVERY_UPDATED, clear_discovery_hash, ) +from .mixins import ( + CONF_CONNECTIONS, + CONF_IDENTIFIERS, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + cleanup_device_registry, + device_info_from_config, + validate_device_has_at_least_one_identifier, +) from .util import valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -39,12 +37,12 @@ TAGS = "mqtt_tags" PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_PLATFORM): "mqtt", vol.Required(CONF_TOPIC): valid_subscribe_topic, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, }, - mqtt.validate_device_has_at_least_one_identifier, + validate_device_has_at_least_one_identifier, ) @@ -236,7 +234,7 @@ async def _update_device(hass, config_entry, config): """Update device registry.""" device_registry = await hass.helpers.device_registry.async_get_registry() config_entry_id = config_entry.entry_id - device_info = mqtt.device_info_from_config(config[CONF_DEVICE]) + device_info = device_info_from_config(config[CONF_DEVICE]) if config_entry_id is not None and device_info is not None: device_info["config_entry_id"] = config_entry_id diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index 36e6df4ed1d..09f25eaf732 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -10,7 +10,8 @@ from homeassistant.helpers.dispatcher import ( ) from homeassistant.helpers.reload import async_setup_reload_service -from .. import ATTR_DISCOVERY_HASH, DOMAIN as MQTT_DOMAIN, PLATFORMS +from .. import DOMAIN as MQTT_DOMAIN, PLATFORMS +from ..const import ATTR_DISCOVERY_HASH from ..discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash from .schema import CONF_SCHEMA, LEGACY, MQTT_VACUUM_SCHEMA, STATE from .schema_legacy import PLATFORM_SCHEMA_LEGACY, async_setup_entity_legacy diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index 65acc9afc71..dd156720a01 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -28,15 +28,18 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.icon import icon_for_battery_level -from .. import ( +from .. import subscription +from ... import mqtt +from ..debug_info import log_messages +from ..mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - subscription, ) -from ... import mqtt -from ..debug_info import log_messages from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services _LOGGER = logging.getLogger(__name__) @@ -120,7 +123,7 @@ PLATFORM_SCHEMA_LEGACY = ( vol.Inclusive(CONF_CHARGING_TOPIC, "charging"): mqtt.valid_publish_topic, vol.Inclusive(CONF_CLEANING_TEMPLATE, "cleaning"): cv.template, vol.Inclusive(CONF_CLEANING_TOPIC, "cleaning"): mqtt.valid_publish_topic, - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Inclusive(CONF_DOCKED_TEMPLATE, "docked"): cv.template, vol.Inclusive(CONF_DOCKED_TOPIC, "docked"): mqtt.valid_publish_topic, vol.Inclusive(CONF_ERROR_TEMPLATE, "error"): cv.template, @@ -160,8 +163,8 @@ PLATFORM_SCHEMA_LEGACY = ( vol.Optional(mqtt.CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) .extend(MQTT_VACUUM_SCHEMA.schema) ) diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index 5a8666e5a2e..536dd89b0fe 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -32,19 +32,18 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from .. import ( - CONF_COMMAND_TOPIC, - CONF_QOS, - CONF_RETAIN, - CONF_STATE_TOPIC, +from .. import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, subscription +from ... import mqtt +from ..debug_info import log_messages +from ..mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, - subscription, ) -from ... import mqtt -from ..debug_info import log_messages from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services _LOGGER = logging.getLogger(__name__) @@ -120,7 +119,7 @@ DEFAULT_PAYLOAD_PAUSE = "pause" PLATFORM_SCHEMA_STATE = ( mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_FAN_SPEED_LIST, default=[]): vol.All( cv.ensure_list, [cv.string] ), @@ -148,8 +147,8 @@ PLATFORM_SCHEMA_STATE = ( vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) .extend(MQTT_VACUUM_SCHEMA.schema) ) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 7288c3e4304..2907a0e4cfc 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -10,6 +10,7 @@ import voluptuous as vol from homeassistant.components import mqtt, websocket_api from homeassistant.components.mqtt import debug_info +from homeassistant.components.mqtt.mixins import MQTT_ENTITY_DEVICE_INFO_SCHEMA from homeassistant.const import ( ATTR_DOMAIN, ATTR_SERVICE, @@ -241,12 +242,12 @@ def test_validate_publish_topic(): def test_entity_device_info_schema(): """Test MQTT entity device info validation.""" # just identifier - mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA({"identifiers": ["abcd"]}) - mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA({"identifiers": "abcd"}) + MQTT_ENTITY_DEVICE_INFO_SCHEMA({"identifiers": ["abcd"]}) + MQTT_ENTITY_DEVICE_INFO_SCHEMA({"identifiers": "abcd"}) # just connection - mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA({"connections": [["mac", "02:5b:26:a8:dc:12"]]}) + MQTT_ENTITY_DEVICE_INFO_SCHEMA({"connections": [["mac", "02:5b:26:a8:dc:12"]]}) # full device info - mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA( + MQTT_ENTITY_DEVICE_INFO_SCHEMA( { "identifiers": ["helloworld", "hello"], "connections": [["mac", "02:5b:26:a8:dc:12"], ["zigbee", "zigbee_id"]], @@ -257,7 +258,7 @@ def test_entity_device_info_schema(): } ) # full device info with via_device - mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA( + MQTT_ENTITY_DEVICE_INFO_SCHEMA( { "identifiers": ["helloworld", "hello"], "connections": [["mac", "02:5b:26:a8:dc:12"], ["zigbee", "zigbee_id"]], @@ -270,7 +271,7 @@ def test_entity_device_info_schema(): ) # no identifiers with pytest.raises(vol.Invalid): - mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA( + MQTT_ENTITY_DEVICE_INFO_SCHEMA( { "manufacturer": "Whatever", "name": "Beer", @@ -280,7 +281,7 @@ def test_entity_device_info_schema(): ) # empty identifiers with pytest.raises(vol.Invalid): - mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA( + MQTT_ENTITY_DEVICE_INFO_SCHEMA( {"identifiers": [], "connections": [], "name": "Beer"} ) From 2d9eb251427e237f0d4dcc939a0b0d0a826a978e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 9 Jan 2021 01:10:47 +0100 Subject: [PATCH 143/507] Fix parameters when toggling light (#44950) --- homeassistant/components/light/__init__.py | 2 +- tests/components/light/test_init.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index fdef5e61a76..f406366dc86 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -232,7 +232,7 @@ async def async_setup(hass, config): async def async_handle_toggle_service(light, call): """Handle toggling a light.""" if light.is_on: - off_params = filter_turn_off_params(call.data) + off_params = filter_turn_off_params(call.data["params"]) await light.async_turn_off(**off_params) else: await async_handle_light_on_service(light, call) diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index afc125e6423..72674a984fd 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -336,6 +336,21 @@ async def test_services(hass, mock_light_profiles): light.ATTR_TRANSITION: prof_t, } + await hass.services.async_call( + light.DOMAIN, + SERVICE_TOGGLE, + { + ATTR_ENTITY_ID: ent3.entity_id, + light.ATTR_TRANSITION: 4, + }, + blocking=True, + ) + + _, data = ent3.last_call("turn_off") + assert data == { + light.ATTR_TRANSITION: 4, + } + # Test bad data await hass.services.async_call( light.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True From 6dd6d9b36842ec39b0ccb5e254946d7a61453423 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 9 Jan 2021 14:37:33 +0100 Subject: [PATCH 144/507] Deduplicate MQTT entity discovery code (#44970) --- .../components/mqtt/alarm_control_panel.py | 33 ++++------------ .../components/mqtt/binary_sensor.py | 33 ++++------------ homeassistant/components/mqtt/camera.py | 33 ++++------------ homeassistant/components/mqtt/climate.py | 33 ++++------------ homeassistant/components/mqtt/cover.py | 33 ++++------------ .../components/mqtt/device_automation.py | 38 ++++++------------ .../mqtt/device_tracker/schema_discovery.py | 34 +++++----------- homeassistant/components/mqtt/fan.py | 33 ++++------------ .../components/mqtt/light/__init__.py | 33 ++++------------ homeassistant/components/mqtt/lock.py | 33 ++++------------ homeassistant/components/mqtt/mixins.py | 23 +++++++++++ homeassistant/components/mqtt/number.py | 33 ++++------------ homeassistant/components/mqtt/scene.py | 39 ++++++------------- homeassistant/components/mqtt/sensor.py | 33 ++++------------ homeassistant/components/mqtt/switch.py | 33 ++++------------ homeassistant/components/mqtt/tag.py | 28 +++---------- .../components/mqtt/vacuum/__init__.py | 33 ++++------------ 17 files changed, 145 insertions(+), 413 deletions(-) diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 21234fbc5b4..4c06f6af7c3 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -1,4 +1,5 @@ """This platform enables the possibility to control a MQTT alarm.""" +import functools import logging import re @@ -29,10 +30,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -46,9 +43,7 @@ from . import ( subscription, ) from .. import mqtt -from .const import ATTR_DISCOVERY_HASH from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash from .mixins import ( MQTT_AVAILABILITY_SCHEMA, MQTT_ENTITY_DEVICE_INFO_SCHEMA, @@ -57,6 +52,7 @@ from .mixins import ( MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, + async_setup_entry_helper, ) _LOGGER = logging.getLogger(__name__) @@ -112,35 +108,20 @@ async def async_setup_platform( ): """Set up MQTT alarm control panel through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(hass, config, async_add_entities) + await _async_setup_entity(hass, async_add_entities, config) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT alarm control panel dynamically through MQTT discovery.""" - async def async_discover(discovery_payload): - """Discover and add an MQTT alarm control panel.""" - discovery_data = discovery_payload.discovery_data - try: - config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity( - hass, config, async_add_entities, config_entry, discovery_data - ) - except Exception: - discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] - clear_discovery_hash(hass, discovery_hash) - async_dispatcher_send( - hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None - ) - raise - - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format(alarm.DOMAIN, "mqtt"), async_discover + setup = functools.partial( + _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) + await async_setup_entry_helper(hass, alarm.DOMAIN, setup, PLATFORM_SCHEMA) async def _async_setup_entity( - hass, config, async_add_entities, config_entry=None, discovery_data=None + hass, async_add_entities, config, config_entry=None, discovery_data=None ): """Set up the MQTT Alarm Control Panel platform.""" async_add_entities([MqttAlarm(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index c604181bdad..21d7740442c 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -1,5 +1,6 @@ """Support for MQTT binary sensors.""" from datetime import timedelta +import functools import logging import voluptuous as vol @@ -21,10 +22,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) import homeassistant.helpers.event as evt from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.reload import async_setup_reload_service @@ -33,9 +30,7 @@ from homeassistant.util import dt as dt_util from . import CONF_QOS, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, subscription from .. import mqtt -from .const import ATTR_DISCOVERY_HASH from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash from .mixins import ( MQTT_AVAILABILITY_SCHEMA, MQTT_ENTITY_DEVICE_INFO_SCHEMA, @@ -44,6 +39,7 @@ from .mixins import ( MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, + async_setup_entry_helper, ) _LOGGER = logging.getLogger(__name__) @@ -79,35 +75,20 @@ async def async_setup_platform( ): """Set up MQTT binary sensor through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(hass, config, async_add_entities) + await _async_setup_entity(hass, async_add_entities, config) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT binary sensor dynamically through MQTT discovery.""" - async def async_discover(discovery_payload): - """Discover and add a MQTT binary sensor.""" - discovery_data = discovery_payload.discovery_data - try: - config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity( - hass, config, async_add_entities, config_entry, discovery_data - ) - except Exception: - discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] - clear_discovery_hash(hass, discovery_hash) - async_dispatcher_send( - hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None - ) - raise - - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format(binary_sensor.DOMAIN, "mqtt"), async_discover + setup = functools.partial( + _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) + await async_setup_entry_helper(hass, binary_sensor.DOMAIN, setup, PLATFORM_SCHEMA) async def _async_setup_entity( - hass, config, async_add_entities, config_entry=None, discovery_data=None + hass, async_add_entities, config, config_entry=None, discovery_data=None ): """Set up the MQTT binary sensor.""" async_add_entities([MqttBinarySensor(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 3888fcd9663..5600dd8a1e3 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -1,4 +1,5 @@ """Camera that loads a picture from an MQTT topic.""" +import functools import logging import voluptuous as vol @@ -8,18 +9,12 @@ from homeassistant.components.camera import Camera from homeassistant.const import CONF_DEVICE, CONF_NAME, CONF_UNIQUE_ID from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import CONF_QOS, DOMAIN, PLATFORMS, subscription from .. import mqtt -from .const import ATTR_DISCOVERY_HASH from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash from .mixins import ( MQTT_AVAILABILITY_SCHEMA, MQTT_ENTITY_DEVICE_INFO_SCHEMA, @@ -28,6 +23,7 @@ from .mixins import ( MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, + async_setup_entry_helper, ) _LOGGER = logging.getLogger(__name__) @@ -54,35 +50,20 @@ async def async_setup_platform( ): """Set up MQTT camera through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(config, async_add_entities) + await _async_setup_entity(async_add_entities, config) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT camera dynamically through MQTT discovery.""" - async def async_discover(discovery_payload): - """Discover and add a MQTT camera.""" - 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: - discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] - clear_discovery_hash(hass, discovery_hash) - async_dispatcher_send( - hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None - ) - raise - - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format(camera.DOMAIN, "mqtt"), async_discover + setup = functools.partial( + _async_setup_entity, async_add_entities, config_entry=config_entry ) + await async_setup_entry_helper(hass, camera.DOMAIN, setup, PLATFORM_SCHEMA) async def _async_setup_entity( - config, async_add_entities, config_entry=None, discovery_data=None + async_add_entities, config, config_entry=None, discovery_data=None ): """Set up the MQTT Camera.""" async_add_entities([MqttCamera(config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 77503335ded..1c9bbd74bfb 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -1,4 +1,5 @@ """Support for MQTT climate devices.""" +import functools import logging import voluptuous as vol @@ -47,10 +48,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -63,9 +60,7 @@ from . import ( subscription, ) from .. import mqtt -from .const import ATTR_DISCOVERY_HASH from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash from .mixins import ( MQTT_AVAILABILITY_SCHEMA, MQTT_ENTITY_DEVICE_INFO_SCHEMA, @@ -74,6 +69,7 @@ from .mixins import ( MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, + async_setup_entry_helper, ) _LOGGER = logging.getLogger(__name__) @@ -248,7 +244,7 @@ PLATFORM_SCHEMA = ( async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistantType, async_add_entities, config: ConfigType, discovery_info=None ): """Set up MQTT climate device through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) @@ -258,29 +254,14 @@ async def async_setup_platform( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT climate device dynamically through MQTT discovery.""" - async def async_discover(discovery_payload): - """Discover and add a MQTT climate device.""" - discovery_data = discovery_payload.discovery_data - try: - config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity( - hass, config, async_add_entities, config_entry, discovery_data - ) - except Exception: - discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] - clear_discovery_hash(hass, discovery_hash) - async_dispatcher_send( - hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None - ) - raise - - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format(climate.DOMAIN, "mqtt"), async_discover + setup = functools.partial( + _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) + await async_setup_entry_helper(hass, climate.DOMAIN, setup, PLATFORM_SCHEMA) async def _async_setup_entity( - hass, config, async_add_entities, config_entry=None, discovery_data=None + hass, async_add_entities, config, config_entry=None, discovery_data=None ): """Set up the MQTT climate devices.""" async_add_entities([MqttClimate(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 6de560b52b1..498d6ffc8af 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -1,4 +1,5 @@ """Support for MQTT cover devices.""" +import functools import logging import voluptuous as vol @@ -33,10 +34,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -50,9 +47,7 @@ from . import ( subscription, ) from .. import mqtt -from .const import ATTR_DISCOVERY_HASH from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash from .mixins import ( MQTT_AVAILABILITY_SCHEMA, MQTT_ENTITY_DEVICE_INFO_SCHEMA, @@ -61,6 +56,7 @@ from .mixins import ( MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, + async_setup_entry_helper, ) _LOGGER = logging.getLogger(__name__) @@ -183,35 +179,20 @@ async def async_setup_platform( ): """Set up MQTT cover through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(hass, config, async_add_entities) + await _async_setup_entity(hass, async_add_entities, config) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT cover dynamically through MQTT discovery.""" - async def async_discover(discovery_payload): - """Discover and add an MQTT cover.""" - discovery_data = discovery_payload.discovery_data - try: - config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity( - hass, config, async_add_entities, config_entry, discovery_data - ) - except Exception: - discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] - clear_discovery_hash(hass, discovery_hash) - async_dispatcher_send( - hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None - ) - raise - - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format(cover.DOMAIN, "mqtt"), async_discover + setup = functools.partial( + _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) + await async_setup_entry_helper(hass, cover.DOMAIN, setup, PLATFORM_SCHEMA) async def _async_setup_entity( - hass, config, async_add_entities, config_entry=None, discovery_data=None + hass, async_add_entities, config, config_entry=None, discovery_data=None ): """Set up the MQTT Cover.""" async_add_entities([MqttCover(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/device_automation.py b/homeassistant/components/mqtt/device_automation.py index 423690d8e69..d3e1f33421d 100644 --- a/homeassistant/components/mqtt/device_automation.py +++ b/homeassistant/components/mqtt/device_automation.py @@ -1,18 +1,14 @@ """Provides device automations for MQTT.""" +import functools import logging import voluptuous as vol from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) from . import device_trigger from .. import mqtt -from .const import ATTR_DISCOVERY_HASH -from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash +from .mixins import async_setup_entry_helper _LOGGER = logging.getLogger(__name__) @@ -36,24 +32,14 @@ async def async_setup_entry(hass, config_entry): return await device_trigger.async_device_removed(hass, event.data["device_id"]) - async def async_discover(discovery_payload): - """Discover and add an MQTT device automation.""" - discovery_data = discovery_payload.discovery_data - try: - config = PLATFORM_SCHEMA(discovery_payload) - if config[CONF_AUTOMATION_TYPE] == AUTOMATION_TYPE_TRIGGER: - await device_trigger.async_setup_trigger( - hass, config, config_entry, discovery_data - ) - except Exception: - discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] - clear_discovery_hash(hass, discovery_hash) - async_dispatcher_send( - hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None - ) - raise - - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format("device_automation", "mqtt"), async_discover - ) + setup = functools.partial(_async_setup_automation, hass, config_entry=config_entry) + await async_setup_entry_helper(hass, "device_automation", setup, PLATFORM_SCHEMA) hass.bus.async_listen(EVENT_DEVICE_REGISTRY_UPDATED, async_device_removed) + + +async def _async_setup_automation(hass, config, config_entry, discovery_data): + """Set up an MQTT device automation.""" + if config[CONF_AUTOMATION_TYPE] == AUTOMATION_TYPE_TRIGGER: + await device_trigger.async_setup_trigger( + hass, config, config_entry, discovery_data + ) diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker/schema_discovery.py index 9fa40c18037..8e6019eefd7 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_discovery.py +++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py @@ -1,4 +1,5 @@ """Support for tracking MQTT enabled devices identified through discovery.""" +import functools import logging import voluptuous as vol @@ -20,16 +21,11 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) from .. import subscription from ... import mqtt -from ..const import ATTR_DISCOVERY_HASH, CONF_QOS, CONF_STATE_TOPIC +from ..const import CONF_QOS, CONF_STATE_TOPIC from ..debug_info import log_messages -from ..discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash from ..mixins import ( MQTT_AVAILABILITY_SCHEMA, MQTT_ENTITY_DEVICE_INFO_SCHEMA, @@ -38,6 +34,7 @@ from ..mixins import ( MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, + async_setup_entry_helper, ) _LOGGER = logging.getLogger(__name__) @@ -66,29 +63,16 @@ PLATFORM_SCHEMA_DISCOVERY = ( async def async_setup_entry_from_discovery(hass, config_entry, async_add_entities): """Set up MQTT device tracker dynamically through MQTT discovery.""" - async def async_discover(discovery_payload): - """Discover and add an MQTT device tracker.""" - discovery_data = discovery_payload.discovery_data - try: - config = PLATFORM_SCHEMA_DISCOVERY(discovery_payload) - await _async_setup_entity( - hass, config, async_add_entities, config_entry, discovery_data - ) - except Exception: - discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] - clear_discovery_hash(hass, discovery_hash) - async_dispatcher_send( - hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None - ) - raise - - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format(device_tracker.DOMAIN, "mqtt"), async_discover + setup = functools.partial( + _async_setup_entity, hass, async_add_entities, config_entry=config_entry + ) + await async_setup_entry_helper( + hass, device_tracker.DOMAIN, setup, PLATFORM_SCHEMA_DISCOVERY ) async def _async_setup_entity( - hass, config, async_add_entities, config_entry=None, discovery_data=None + hass, async_add_entities, config, config_entry=None, discovery_data=None ): """Set up the MQTT Device Tracker entity.""" async_add_entities([MqttDeviceTracker(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index eb713655821..5d3abd7b793 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -1,4 +1,5 @@ """Support for MQTT fans.""" +import functools import logging import voluptuous as vol @@ -25,10 +26,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -42,9 +39,7 @@ from . import ( subscription, ) from .. import mqtt -from .const import ATTR_DISCOVERY_HASH from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash from .mixins import ( MQTT_AVAILABILITY_SCHEMA, MQTT_ENTITY_DEVICE_INFO_SCHEMA, @@ -53,6 +48,7 @@ from .mixins import ( MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, + async_setup_entry_helper, ) _LOGGER = logging.getLogger(__name__) @@ -124,35 +120,20 @@ async def async_setup_platform( ): """Set up MQTT fan through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(hass, config, async_add_entities) + await _async_setup_entity(hass, async_add_entities, config) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT fan dynamically through MQTT discovery.""" - async def async_discover(discovery_payload): - """Discover and add a MQTT fan.""" - discovery_data = discovery_payload.discovery_data - try: - config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity( - hass, config, async_add_entities, config_entry, discovery_data - ) - except Exception: - discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] - clear_discovery_hash(hass, discovery_hash) - async_dispatcher_send( - hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None - ) - raise - - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format(fan.DOMAIN, "mqtt"), async_discover + setup = functools.partial( + _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) + await async_setup_entry_helper(hass, fan.DOMAIN, setup, PLATFORM_SCHEMA) async def _async_setup_entity( - hass, config, async_add_entities, config_entry=None, discovery_data=None + hass, async_add_entities, config, config_entry=None, discovery_data=None ): """Set up the MQTT fan.""" async_add_entities([MqttFan(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index cfeaaa0d7c9..e780332d093 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -1,19 +1,15 @@ """Support for MQTT lights.""" +import functools import logging import voluptuous as vol from homeassistant.components import light -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .. import DOMAIN, PLATFORMS -from ..const import ATTR_DISCOVERY_HASH -from ..discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash +from ..mixins import async_setup_entry_helper from .schema import CONF_SCHEMA, MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import PLATFORM_SCHEMA_BASIC, async_setup_entity_basic from .schema_json import PLATFORM_SCHEMA_JSON, async_setup_entity_json @@ -42,35 +38,20 @@ async def async_setup_platform( ): """Set up MQTT light through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(hass, config, async_add_entities) + await _async_setup_entity(hass, async_add_entities, config) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT light dynamically through MQTT discovery.""" - async def async_discover(discovery_payload): - """Discover and add a MQTT light.""" - discovery_data = discovery_payload.discovery_data - try: - config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity( - hass, config, async_add_entities, config_entry, discovery_data - ) - except Exception: - discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] - clear_discovery_hash(hass, discovery_hash) - async_dispatcher_send( - hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None - ) - raise - - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format(light.DOMAIN, "mqtt"), async_discover + setup = functools.partial( + _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) + await async_setup_entry_helper(hass, light.DOMAIN, setup, PLATFORM_SCHEMA) async def _async_setup_entity( - hass, config, async_add_entities, config_entry=None, discovery_data=None + hass, async_add_entities, config, config_entry=None, discovery_data=None ): """Set up a MQTT Light.""" setup_entity = { diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 72df487d22a..ef3ad6bb8eb 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -1,4 +1,5 @@ """Support for MQTT locks.""" +import functools import logging import voluptuous as vol @@ -14,10 +15,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -31,9 +28,7 @@ from . import ( subscription, ) from .. import mqtt -from .const import ATTR_DISCOVERY_HASH from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash from .mixins import ( MQTT_AVAILABILITY_SCHEMA, MQTT_ENTITY_DEVICE_INFO_SCHEMA, @@ -42,6 +37,7 @@ from .mixins import ( MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, + async_setup_entry_helper, ) _LOGGER = logging.getLogger(__name__) @@ -86,35 +82,20 @@ async def async_setup_platform( ): """Set up MQTT lock panel through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(hass, config, async_add_entities) + await _async_setup_entity(hass, async_add_entities, config) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT lock dynamically through MQTT discovery.""" - async def async_discover(discovery_payload): - """Discover and add an MQTT lock.""" - discovery_data = discovery_payload.discovery_data - try: - config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity( - hass, config, async_add_entities, config_entry, discovery_data - ) - except Exception: - discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] - clear_discovery_hash(hass, discovery_hash) - async_dispatcher_send( - hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None - ) - raise - - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format(lock.DOMAIN, "mqtt"), async_discover + setup = functools.partial( + _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) + await async_setup_entry_helper(hass, lock.DOMAIN, setup, PLATFORM_SCHEMA) async def _async_setup_entity( - hass, config, async_add_entities, config_entry=None, discovery_data=None + hass, async_add_entities, config, config_entry=None, discovery_data=None ): """Set up the MQTT Lock platform.""" async_add_entities([MqttLock(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index c627a254791..42171ef4e34 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -30,6 +30,7 @@ from .const import ( from .debug_info import log_messages from .discovery import ( MQTT_DISCOVERY_DONE, + MQTT_DISCOVERY_NEW, MQTT_DISCOVERY_UPDATED, clear_discovery_hash, set_discovery_hash, @@ -130,6 +131,28 @@ MQTT_JSON_ATTRS_SCHEMA = vol.Schema( ) +async def async_setup_entry_helper(hass, domain, async_setup, schema): + """Set up entity, automation or tag creation dynamically through MQTT discovery.""" + + async def async_discover(discovery_payload): + """Discover and add an MQTT entity, automation or tag.""" + discovery_data = discovery_payload.discovery_data + try: + config = schema(discovery_payload) + await async_setup(config, discovery_data=discovery_data) + except Exception: + discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(hass, discovery_hash) + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) + raise + + async_dispatcher_connect( + hass, MQTT_DISCOVERY_NEW.format(domain, "mqtt"), async_discover + ) + + class MqttAttributes(Entity): """Mixin used for platforms that support JSON attributes.""" diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 7196b348a6f..c844d888efe 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -1,4 +1,5 @@ """Configure number in a device through MQTT topic.""" +import functools import logging import voluptuous as vol @@ -14,10 +15,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -31,9 +28,7 @@ from . import ( subscription, ) from .. import mqtt -from .const import ATTR_DISCOVERY_HASH from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash from .mixins import ( MQTT_AVAILABILITY_SCHEMA, MQTT_ENTITY_DEVICE_INFO_SCHEMA, @@ -42,6 +37,7 @@ from .mixins import ( MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, + async_setup_entry_helper, ) _LOGGER = logging.getLogger(__name__) @@ -69,35 +65,20 @@ async def async_setup_platform( ): """Set up MQTT number through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(config, async_add_entities) + await _async_setup_entity(async_add_entities, config) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT number dynamically through MQTT discovery.""" - async def async_discover(discovery_payload): - """Discover and add a MQTT number.""" - 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: - discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] - clear_discovery_hash(hass, discovery_hash) - async_dispatcher_send( - hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None - ) - raise - - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format(number.DOMAIN, "mqtt"), async_discover + setup = functools.partial( + _async_setup_entity, async_add_entities, config_entry=config_entry ) + await async_setup_entry_helper(hass, number.DOMAIN, setup, PLATFORM_SCHEMA) async def _async_setup_entity( - config, async_add_entities, config_entry=None, discovery_data=None + async_add_entities, config, config_entry=None, discovery_data=None ): """Set up the MQTT number.""" async_add_entities([MqttNumber(config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index 2923f512d02..908f4bafd30 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -1,4 +1,5 @@ """Support for MQTT scenes.""" +import functools import logging import voluptuous as vol @@ -7,18 +8,17 @@ from homeassistant.components import 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, - async_dispatcher_send, -) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, DOMAIN, PLATFORMS from .. import mqtt -from .const import ATTR_DISCOVERY_HASH -from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash -from .mixins import MQTT_AVAILABILITY_SCHEMA, MqttAvailability, MqttDiscoveryUpdate +from .mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MqttAvailability, + MqttDiscoveryUpdate, + async_setup_entry_helper, +) _LOGGER = logging.getLogger(__name__) @@ -42,35 +42,20 @@ async def async_setup_platform( ): """Set up MQTT scene through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(config, async_add_entities) + await _async_setup_entity(async_add_entities, config) 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: - discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] - clear_discovery_hash(hass, discovery_hash) - async_dispatcher_send( - hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None - ) - raise - - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format(scene.DOMAIN, "mqtt"), async_discover + setup = functools.partial( + _async_setup_entity, async_add_entities, config_entry=config_entry ) + await async_setup_entry_helper(hass, scene.DOMAIN, setup, PLATFORM_SCHEMA) async def _async_setup_entity( - config, async_add_entities, config_entry=None, discovery_data=None + async_add_entities, config, config_entry=None, discovery_data=None ): """Set up the MQTT scene.""" async_add_entities([MqttScene(config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 5ddfc29d80f..159c4b1d118 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -1,5 +1,6 @@ """Support for MQTT sensors.""" from datetime import timedelta +import functools import logging from typing import Optional @@ -19,10 +20,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.reload import async_setup_reload_service @@ -31,9 +28,7 @@ from homeassistant.util import dt as dt_util from . import CONF_QOS, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, subscription from .. import mqtt -from .const import ATTR_DISCOVERY_HASH from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash from .mixins import ( MQTT_AVAILABILITY_SCHEMA, MQTT_ENTITY_DEVICE_INFO_SCHEMA, @@ -42,6 +37,7 @@ from .mixins import ( MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, + async_setup_entry_helper, ) _LOGGER = logging.getLogger(__name__) @@ -73,35 +69,20 @@ async def async_setup_platform( ): """Set up MQTT sensors through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(hass, config, async_add_entities) + await _async_setup_entity(hass, async_add_entities, config) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT sensors dynamically through MQTT discovery.""" - async def async_discover_sensor(discovery_payload): - """Discover and add a discovered MQTT sensor.""" - discovery_data = discovery_payload.discovery_data - try: - config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity( - hass, config, async_add_entities, config_entry, discovery_data - ) - except Exception: - discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] - clear_discovery_hash(hass, discovery_hash) - async_dispatcher_send( - hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None - ) - raise - - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format(sensor.DOMAIN, "mqtt"), async_discover_sensor + setup = functools.partial( + _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) + await async_setup_entry_helper(hass, sensor.DOMAIN, setup, PLATFORM_SCHEMA) async def _async_setup_entity( - hass, config: ConfigType, async_add_entities, config_entry=None, discovery_data=None + hass, async_add_entities, config: ConfigType, config_entry=None, discovery_data=None ): """Set up MQTT sensor.""" async_add_entities([MqttSensor(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 35a436b0be8..a4e65354594 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -1,4 +1,5 @@ """Support for MQTT switches.""" +import functools import logging import voluptuous as vol @@ -18,10 +19,6 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -36,9 +33,7 @@ from . import ( subscription, ) from .. import mqtt -from .const import ATTR_DISCOVERY_HASH from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash from .mixins import ( MQTT_AVAILABILITY_SCHEMA, MQTT_ENTITY_DEVICE_INFO_SCHEMA, @@ -47,6 +42,7 @@ from .mixins import ( MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, + async_setup_entry_helper, ) _LOGGER = logging.getLogger(__name__) @@ -82,35 +78,20 @@ async def async_setup_platform( ): """Set up MQTT switch through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(hass, config, async_add_entities) + await _async_setup_entity(hass, async_add_entities, config) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT switch dynamically through MQTT discovery.""" - async def async_discover(discovery_payload): - """Discover and add a MQTT switch.""" - discovery_data = discovery_payload.discovery_data - try: - config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity( - hass, config, async_add_entities, config_entry, discovery_data - ) - except Exception: - discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] - clear_discovery_hash(hass, discovery_hash) - async_dispatcher_send( - hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None - ) - raise - - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format(switch.DOMAIN, "mqtt"), async_discover + setup = functools.partial( + _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) + await async_setup_entry_helper(hass, switch.DOMAIN, setup, PLATFORM_SCHEMA) async def _async_setup_entity( - hass, config, async_add_entities, config_entry=None, discovery_data=None + hass, async_add_entities, config, config_entry=None, discovery_data=None ): """Set up the MQTT switch.""" async_add_entities([MqttSwitch(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/tag.py b/homeassistant/components/mqtt/tag.py index c4db7b5f4b9..b691c5cf8ce 100644 --- a/homeassistant/components/mqtt/tag.py +++ b/homeassistant/components/mqtt/tag.py @@ -1,4 +1,5 @@ """Provides tag scanning for MQTT.""" +import functools import logging import voluptuous as vol @@ -14,16 +15,12 @@ from homeassistant.helpers.dispatcher import ( from . import CONF_QOS, CONF_TOPIC, DOMAIN, subscription from .. import mqtt from .const import ATTR_DISCOVERY_HASH, ATTR_DISCOVERY_TOPIC -from .discovery import ( - MQTT_DISCOVERY_DONE, - MQTT_DISCOVERY_NEW, - MQTT_DISCOVERY_UPDATED, - clear_discovery_hash, -) +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_UPDATED, clear_discovery_hash from .mixins import ( CONF_CONNECTIONS, CONF_IDENTIFIERS, MQTT_ENTITY_DEVICE_INFO_SCHEMA, + async_setup_entry_helper, cleanup_device_registry, device_info_from_config, validate_device_has_at_least_one_identifier, @@ -49,23 +46,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( async def async_setup_entry(hass, config_entry): """Set up MQTT tag scan dynamically through MQTT discovery.""" - async def async_discover(discovery_payload): - """Discover and add MQTT tag scan.""" - discovery_data = discovery_payload.discovery_data - try: - config = PLATFORM_SCHEMA(discovery_payload) - await async_setup_tag(hass, config, config_entry, discovery_data) - except Exception: - discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] - clear_discovery_hash(hass, discovery_hash) - async_dispatcher_send( - hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None - ) - raise - - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format("tag", "mqtt"), async_discover - ) + setup = functools.partial(async_setup_tag, hass, config_entry=config_entry) + await async_setup_entry_helper(hass, "tag", setup, PLATFORM_SCHEMA) async def async_setup_tag(hass, config, config_entry, discovery_data): diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index 09f25eaf732..e580e874993 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -1,18 +1,14 @@ """Support for MQTT vacuums.""" +import functools import logging import voluptuous as vol from homeassistant.components.vacuum import DOMAIN -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) from homeassistant.helpers.reload import async_setup_reload_service from .. import DOMAIN as MQTT_DOMAIN, PLATFORMS -from ..const import ATTR_DISCOVERY_HASH -from ..discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_NEW, clear_discovery_hash +from ..mixins import async_setup_entry_helper from .schema import CONF_SCHEMA, LEGACY, MQTT_VACUUM_SCHEMA, STATE from .schema_legacy import PLATFORM_SCHEMA_LEGACY, async_setup_entity_legacy from .schema_state import PLATFORM_SCHEMA_STATE, async_setup_entity_state @@ -34,35 +30,20 @@ PLATFORM_SCHEMA = vol.All( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up MQTT vacuum through configuration.yaml.""" await async_setup_reload_service(hass, MQTT_DOMAIN, PLATFORMS) - await _async_setup_entity(config, async_add_entities) + await _async_setup_entity(async_add_entities, config) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT vacuum dynamically through MQTT discovery.""" - async def async_discover(discovery_payload): - """Discover and add a MQTT vacuum.""" - 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: - discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] - clear_discovery_hash(hass, discovery_hash) - async_dispatcher_send( - hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None - ) - raise - - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format(DOMAIN, "mqtt"), async_discover + setup = functools.partial( + _async_setup_entity, async_add_entities, config_entry=config_entry ) + await async_setup_entry_helper(hass, DOMAIN, setup, PLATFORM_SCHEMA) async def _async_setup_entity( - config, async_add_entities, config_entry=None, discovery_data=None + async_add_entities, config, config_entry=None, discovery_data=None ): """Set up the MQTT vacuum.""" setup_entity = {LEGACY: async_setup_entity_legacy, STATE: async_setup_entity_state} From 982c42e7467c9c48a18db8537d11bf92cc7a9130 Mon Sep 17 00:00:00 2001 From: bchastain Date: Sat, 9 Jan 2021 07:52:49 -0600 Subject: [PATCH 145/507] Add pressure forecast to HA weather entity model (#44965) --- homeassistant/components/weather/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 8ddcf052e1f..5127dae1102 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -35,6 +35,7 @@ ATTR_FORECAST = "forecast" ATTR_FORECAST_CONDITION = "condition" ATTR_FORECAST_PRECIPITATION = "precipitation" ATTR_FORECAST_PRECIPITATION_PROBABILITY = "precipitation_probability" +ATTR_FORECAST_PRESSURE = "pressure" ATTR_FORECAST_TEMP = "temperature" ATTR_FORECAST_TEMP_LOW = "templow" ATTR_FORECAST_TIME = "datetime" From 8b72324ae695d7231506e98daa1c3ae83e0bac3d Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 9 Jan 2021 15:23:03 +0100 Subject: [PATCH 146/507] Add zwave to ozw migration (#39081) Co-authored-by: Paulus Schoutsen Co-authored-by: Bram Kragten --- homeassistant/components/ozw/__init__.py | 3 +- homeassistant/components/ozw/config_flow.py | 19 +- homeassistant/components/ozw/const.py | 4 + homeassistant/components/ozw/manifest.json | 3 +- homeassistant/components/ozw/migration.py | 171 ++++++++++ homeassistant/components/ozw/websocket_api.py | 64 ++++ homeassistant/components/zwave/__init__.py | 68 ++++ homeassistant/components/zwave/manifest.json | 1 + .../components/zwave/websocket_api.py | 25 ++ tests/components/ozw/conftest.py | 6 + tests/components/ozw/test_config_flow.py | 49 +++ tests/components/ozw/test_migration.py | 292 ++++++++++++++++++ tests/components/zwave/test_websocket_api.py | 47 ++- tests/fixtures/ozw/migration_fixture.csv | 9 + 14 files changed, 751 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/ozw/migration.py create mode 100644 tests/components/ozw/test_migration.py create mode 100644 tests/fixtures/ozw/migration_fixture.csv diff --git a/homeassistant/components/ozw/__init__.py b/homeassistant/components/ozw/__init__.py index 18fffdbc66e..e56e3deb066 100644 --- a/homeassistant/components/ozw/__init__.py +++ b/homeassistant/components/ozw/__init__.py @@ -36,6 +36,7 @@ from .const import ( DATA_UNSUBSCRIBE, DOMAIN, MANAGER, + NODES_VALUES, PLATFORMS, TOPIC_OPENZWAVE, ) @@ -68,7 +69,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ozw_data[DATA_UNSUBSCRIBE] = [] data_nodes = {} - data_values = {} + hass.data[DOMAIN][NODES_VALUES] = data_values = {} removed_nodes = [] manager_options = {"topic_prefix": f"{TOPIC_OPENZWAVE}/"} diff --git a/homeassistant/components/ozw/config_flow.py b/homeassistant/components/ozw/config_flow.py index 7c7c6e65dfe..14d875e0a70 100644 --- a/homeassistant/components/ozw/config_flow.py +++ b/homeassistant/components/ozw/config_flow.py @@ -37,6 +37,15 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.integration_created_addon = False self.install_task = None + async def async_step_import(self, data): + """Handle imported data. + + This step will be used when importing data during zwave to ozw migration. + """ + self.network_key = data.get(CONF_NETWORK_KEY) + self.usb_path = data.get(CONF_USB_PATH) + return await self.async_step_user() + async def async_step_user(self, user_input=None): """Handle the initial step.""" if self._async_current_entries(): @@ -163,13 +172,15 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): else: return self._async_create_entry_from_vars() - self.usb_path = self.addon_config.get(CONF_ADDON_DEVICE, "") - self.network_key = self.addon_config.get(CONF_ADDON_NETWORK_KEY, "") + usb_path = self.addon_config.get(CONF_ADDON_DEVICE, self.usb_path or "") + network_key = self.addon_config.get( + CONF_ADDON_NETWORK_KEY, self.network_key or "" + ) data_schema = vol.Schema( { - vol.Required(CONF_USB_PATH, default=self.usb_path): str, - vol.Optional(CONF_NETWORK_KEY, default=self.network_key): str, + vol.Required(CONF_USB_PATH, default=usb_path): str, + vol.Optional(CONF_NETWORK_KEY, default=network_key): str, } ) diff --git a/homeassistant/components/ozw/const.py b/homeassistant/components/ozw/const.py index f8d5090aa84..68eaf9f7c8a 100644 --- a/homeassistant/components/ozw/const.py +++ b/homeassistant/components/ozw/const.py @@ -25,6 +25,7 @@ PLATFORMS = [ SWITCH_DOMAIN, ] MANAGER = "manager" +NODES_VALUES = "nodes_values" # MQTT Topics TOPIC_OPENZWAVE = "OpenZWave" @@ -40,6 +41,9 @@ ATTR_SCENE_LABEL = "scene_label" ATTR_SCENE_VALUE_ID = "scene_value_id" ATTR_SCENE_VALUE_LABEL = "scene_value_label" +# Config entry data and options +MIGRATED = "migrated" + # Service specific SERVICE_ADD_NODE = "add_node" SERVICE_REMOVE_NODE = "remove_node" diff --git a/homeassistant/components/ozw/manifest.json b/homeassistant/components/ozw/manifest.json index a1409fd79a8..984e3f9c51a 100644 --- a/homeassistant/components/ozw/manifest.json +++ b/homeassistant/components/ozw/manifest.json @@ -7,7 +7,8 @@ "python-openzwave-mqtt[mqtt-client]==1.4.0" ], "after_dependencies": [ - "mqtt" + "mqtt", + "zwave" ], "codeowners": [ "@cgarwood", diff --git a/homeassistant/components/ozw/migration.py b/homeassistant/components/ozw/migration.py new file mode 100644 index 00000000000..86df69bc955 --- /dev/null +++ b/homeassistant/components/ozw/migration.py @@ -0,0 +1,171 @@ +"""Provide tools for migrating from the zwave integration.""" +from homeassistant.helpers.device_registry import ( + async_get_registry as async_get_device_registry, +) +from homeassistant.helpers.entity_registry import ( + async_entries_for_config_entry, + async_get_registry as async_get_entity_registry, +) + +from .const import DOMAIN, MIGRATED, NODES_VALUES +from .entity import create_device_id, create_value_id + +# The following dicts map labels between OpenZWave 1.4 and 1.6. +METER_CC_LABELS = { + "Energy": "Electric - kWh", + "Power": "Electric - W", + "Count": "Electric - Pulses", + "Voltage": "Electric - V", + "Current": "Electric - A", + "Power Factor": "Electric - PF", +} + +NOTIFICATION_CC_LABELS = { + "General": "Start", + "Smoke": "Smoke Alarm", + "Carbon Monoxide": "Carbon Monoxide", + "Carbon Dioxide": "Carbon Dioxide", + "Heat": "Heat", + "Flood": "Water", + "Access Control": "Access Control", + "Burglar": "Home Security", + "Power Management": "Power Management", + "System": "System", + "Emergency": "Emergency", + "Clock": "Clock", + "Appliance": "Appliance", + "HomeHealth": "Home Health", +} + +CC_ID_LABELS = { + 50: METER_CC_LABELS, + 113: NOTIFICATION_CC_LABELS, +} + + +async def async_get_migration_data(hass): + """Return dict with ozw side migration info.""" + data = {} + nodes_values = hass.data[DOMAIN][NODES_VALUES] + ozw_config_entries = hass.config_entries.async_entries(DOMAIN) + config_entry = ozw_config_entries[0] # ozw only has a single config entry + ent_reg = await async_get_entity_registry(hass) + entity_entries = async_entries_for_config_entry(ent_reg, config_entry.entry_id) + unique_entries = {entry.unique_id: entry for entry in entity_entries} + dev_reg = await async_get_device_registry(hass) + + for node_id, node_values in nodes_values.items(): + for entity_values in node_values: + unique_id = create_value_id(entity_values.primary) + if unique_id not in unique_entries: + continue + node = entity_values.primary.node + device_identifier = ( + DOMAIN, + create_device_id(node, entity_values.primary.instance), + ) + device_entry = dev_reg.async_get_device({device_identifier}, set()) + data[unique_id] = { + "node_id": node_id, + "node_instance": entity_values.primary.instance, + "device_id": device_entry.id, + "command_class": entity_values.primary.command_class.value, + "command_class_label": entity_values.primary.label, + "value_index": entity_values.primary.index, + "unique_id": unique_id, + "entity_entry": unique_entries[unique_id], + } + + return data + + +def map_node_values(zwave_data, ozw_data): + """Map zwave node values onto ozw node values.""" + migration_map = {"device_entries": {}, "entity_entries": {}} + + for zwave_entry in zwave_data.values(): + node_id = zwave_entry["node_id"] + node_instance = zwave_entry["node_instance"] + cc_id = zwave_entry["command_class"] + zwave_cc_label = zwave_entry["command_class_label"] + + if cc_id in CC_ID_LABELS: + labels = CC_ID_LABELS[cc_id] + ozw_cc_label = labels.get(zwave_cc_label, zwave_cc_label) + + ozw_entry = next( + ( + entry + for entry in ozw_data.values() + if entry["node_id"] == node_id + and entry["node_instance"] == node_instance + and entry["command_class"] == cc_id + and entry["command_class_label"] == ozw_cc_label + ), + None, + ) + else: + value_index = zwave_entry["value_index"] + + ozw_entry = next( + ( + entry + for entry in ozw_data.values() + if entry["node_id"] == node_id + and entry["node_instance"] == node_instance + and entry["command_class"] == cc_id + and entry["value_index"] == value_index + ), + None, + ) + + if ozw_entry is None: + continue + + # Save the zwave_entry under the ozw entity_id to create the map. + # Check that the mapped entities have the same domain. + if zwave_entry["entity_entry"].domain == ozw_entry["entity_entry"].domain: + migration_map["entity_entries"][ + ozw_entry["entity_entry"].entity_id + ] = zwave_entry + migration_map["device_entries"][ozw_entry["device_id"]] = zwave_entry[ + "device_id" + ] + + return migration_map + + +async def async_migrate(hass, migration_map): + """Perform zwave to ozw migration.""" + dev_reg = await async_get_device_registry(hass) + for ozw_device_id, zwave_device_id in migration_map["device_entries"].items(): + zwave_device_entry = dev_reg.async_get(zwave_device_id) + dev_reg.async_update_device( + ozw_device_id, + area_id=zwave_device_entry.area_id, + name_by_user=zwave_device_entry.name_by_user, + ) + + ent_reg = await async_get_entity_registry(hass) + for zwave_entry in migration_map["entity_entries"].values(): + zwave_entity_id = zwave_entry["entity_entry"].entity_id + ent_reg.async_remove(zwave_entity_id) + + for ozw_entity_id, zwave_entry in migration_map["entity_entries"].items(): + entity_entry = zwave_entry["entity_entry"] + ent_reg.async_update_entity( + ozw_entity_id, + new_entity_id=entity_entry.entity_id, + name=entity_entry.name, + icon=entity_entry.icon, + ) + + zwave_config_entry = hass.config_entries.async_entries("zwave")[0] + await hass.config_entries.async_remove(zwave_config_entry.entry_id) + + ozw_config_entry = hass.config_entries.async_entries("ozw")[0] + updates = { + **ozw_config_entry.data, + MIGRATED: True, + } + hass.config_entries.async_update_entry(ozw_config_entry, data=updates) diff --git a/homeassistant/components/ozw/websocket_api.py b/homeassistant/components/ozw/websocket_api.py index 3ee6e040743..708b9045b57 100644 --- a/homeassistant/components/ozw/websocket_api.py +++ b/homeassistant/components/ozw/websocket_api.py @@ -1,4 +1,6 @@ """Web socket API for OpenZWave.""" +import logging + from openzwavemqtt.const import ( ATTR_CODE_SLOT, ATTR_LABEL, @@ -23,7 +25,11 @@ from homeassistant.helpers import config_validation as cv from .const import ATTR_CONFIG_PARAMETER, ATTR_CONFIG_VALUE, DOMAIN, MANAGER from .lock import ATTR_USERCODE +from .migration import async_get_migration_data, async_migrate, map_node_values +_LOGGER = logging.getLogger(__name__) + +DRY_RUN = "dry_run" TYPE = "type" ID = "id" OZW_INSTANCE = "ozw_instance" @@ -52,6 +58,7 @@ ATTR_NEIGHBORS = "neighbors" @callback def async_register_api(hass): """Register all of our api endpoints.""" + websocket_api.async_register_command(hass, websocket_migrate_zwave) websocket_api.async_register_command(hass, websocket_get_instances) websocket_api.async_register_command(hass, websocket_get_nodes) websocket_api.async_register_command(hass, websocket_network_status) @@ -161,6 +168,63 @@ def _get_config_params(node, *args): return config_params +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required(TYPE): "ozw/migrate_zwave", + vol.Optional(DRY_RUN, default=True): bool, + } +) +async def websocket_migrate_zwave(hass, connection, msg): + """Migrate the zwave integration device and entity data to ozw integration.""" + if "zwave" not in hass.config.components: + _LOGGER.error("Can not migrate, zwave integration is not loaded") + connection.send_message( + websocket_api.error_message( + msg["id"], "zwave_not_loaded", "Integration zwave is not loaded" + ) + ) + return + + zwave = hass.components.zwave + zwave_data = await zwave.async_get_ozw_migration_data(hass) + _LOGGER.debug("Migration zwave data: %s", zwave_data) + + ozw_data = await async_get_migration_data(hass) + _LOGGER.debug("Migration ozw data: %s", ozw_data) + + can_migrate = map_node_values(zwave_data, ozw_data) + + zwave_entity_ids = [ + entry["entity_entry"].entity_id for entry in zwave_data.values() + ] + ozw_entity_ids = [entry["entity_entry"].entity_id for entry in ozw_data.values()] + migration_device_map = { + zwave_device_id: ozw_device_id + for ozw_device_id, zwave_device_id in can_migrate["device_entries"].items() + } + migration_entity_map = { + zwave_entry["entity_entry"].entity_id: ozw_entity_id + for ozw_entity_id, zwave_entry in can_migrate["entity_entries"].items() + } + _LOGGER.debug("Migration entity map: %s", migration_entity_map) + + if not msg[DRY_RUN]: + await async_migrate(hass, can_migrate) + + connection.send_result( + msg[ID], + { + "migration_device_map": migration_device_map, + "zwave_entity_ids": zwave_entity_ids, + "ozw_entity_ids": ozw_entity_ids, + "migration_entity_map": migration_entity_map, + "migrated": not msg[DRY_RUN], + }, + ) + + @websocket_api.websocket_command({vol.Required(TYPE): "ozw/get_instances"}) def websocket_get_instances(hass, connection, msg): """Get a list of OZW instances.""" diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index aba169a1919..27f6c0a4801 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -28,6 +28,7 @@ from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_component import DEFAULT_SCAN_INTERVAL from homeassistant.helpers.entity_platform import EntityPlatform from homeassistant.helpers.entity_registry import ( + async_entries_for_config_entry, async_get_registry as async_get_entity_registry, ) from homeassistant.helpers.entity_values import EntityValues @@ -81,6 +82,8 @@ CONF_DEVICE_CONFIG = "device_config" CONF_DEVICE_CONFIG_GLOB = "device_config_glob" CONF_DEVICE_CONFIG_DOMAIN = "device_config_domain" +DATA_ZWAVE_CONFIG_YAML_PRESENT = "zwave_config_yaml_present" + DEFAULT_CONF_IGNORED = False DEFAULT_CONF_INVERT_OPENCLOSE_BUTTONS = False DEFAULT_CONF_INVERT_PERCENT = False @@ -250,6 +253,64 @@ CONFIG_SCHEMA = vol.Schema( ) +async def async_get_ozw_migration_data(hass): + """Return dict with info for migration to ozw integration.""" + data_to_migrate = {} + + zwave_config_entries = hass.config_entries.async_entries(DOMAIN) + if not zwave_config_entries: + _LOGGER.error("Config entry not set up") + return data_to_migrate + + if hass.data.get(DATA_ZWAVE_CONFIG_YAML_PRESENT): + _LOGGER.warning( + "Remove %s from configuration.yaml " + "to avoid setting up this integration on restart " + "after completing migration to ozw", + DOMAIN, + ) + + config_entry = zwave_config_entries[0] # zwave only has a single config entry + ent_reg = await async_get_entity_registry(hass) + entity_entries = async_entries_for_config_entry(ent_reg, config_entry.entry_id) + unique_entries = {entry.unique_id: entry for entry in entity_entries} + dev_reg = await async_get_device_registry(hass) + + for entity_values in hass.data[DATA_ENTITY_VALUES]: + node = entity_values.primary.node + unique_id = compute_value_unique_id(node, entity_values.primary) + if unique_id not in unique_entries: + continue + device_identifier, _ = node_device_id_and_name( + node, entity_values.primary.instance + ) + device_entry = dev_reg.async_get_device({device_identifier}, set()) + data_to_migrate[unique_id] = { + "node_id": node.node_id, + "node_instance": entity_values.primary.instance, + "device_id": device_entry.id, + "command_class": entity_values.primary.command_class, + "command_class_label": entity_values.primary.label, + "value_index": entity_values.primary.index, + "unique_id": unique_id, + "entity_entry": unique_entries[unique_id], + } + + return data_to_migrate + + +@callback +def async_is_ozw_migrated(hass): + """Return True if migration to ozw is done.""" + ozw_config_entries = hass.config_entries.async_entries("ozw") + if not ozw_config_entries: + return False + + ozw_config_entry = ozw_config_entries[0] # only one ozw entry is allowed + migrated = bool(ozw_config_entry.data.get("migrated")) + return migrated + + def _obj_to_dict(obj): """Convert an object into a hash for debug.""" return { @@ -312,6 +373,7 @@ async def async_setup(hass, config): conf = config[DOMAIN] hass.data[DATA_ZWAVE_CONFIG] = conf + hass.data[DATA_ZWAVE_CONFIG_YAML_PRESENT] = True if not hass.config_entries.async_entries(DOMAIN): hass.async_create_task( @@ -343,6 +405,12 @@ async def async_setup_entry(hass, config_entry): # pylint: enable=import-error from pydispatch import dispatcher + if async_is_ozw_migrated(hass): + _LOGGER.error( + "Migration to ozw has been done. Please remove the zwave integration" + ) + return False + # Merge config entry and yaml config config = config_entry.data if DATA_ZWAVE_CONFIG in hass.data: diff --git a/homeassistant/components/zwave/manifest.json b/homeassistant/components/zwave/manifest.json index 5fda2eac7c3..a3a2b5e0d83 100644 --- a/homeassistant/components/zwave/manifest.json +++ b/homeassistant/components/zwave/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave", "requirements": ["homeassistant-pyozw==0.1.10", "pydispatcher==2.0.5"], + "after_dependencies": ["ozw"], "codeowners": ["@home-assistant/z-wave"] } diff --git a/homeassistant/components/zwave/websocket_api.py b/homeassistant/components/zwave/websocket_api.py index 5e3a49df63c..bf84a27166e 100644 --- a/homeassistant/components/zwave/websocket_api.py +++ b/homeassistant/components/zwave/websocket_api.py @@ -2,6 +2,8 @@ import voluptuous as vol from homeassistant.components import websocket_api +from homeassistant.components.ozw.const import DOMAIN as OZW_DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.core import callback from .const import ( @@ -56,9 +58,32 @@ def websocket_get_migration_config(hass, connection, msg): ) +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command({vol.Required(TYPE): "zwave/start_ozw_config_flow"}) +async def websocket_start_ozw_config_flow(hass, connection, msg): + """Start the ozw integration config flow (for migration wizard). + + Return data with the flow id of the started ozw config flow. + """ + config = hass.data[DATA_ZWAVE_CONFIG] + data = { + "usb_path": config[CONF_USB_STICK_PATH], + "network_key": config[CONF_NETWORK_KEY], + } + result = await hass.config_entries.flow.async_init( + OZW_DOMAIN, context={"source": SOURCE_IMPORT}, data=data + ) + connection.send_result( + msg[ID], + {"flow_id": result["flow_id"]}, + ) + + @callback def async_load_websocket_api(hass): """Set up the web socket API.""" websocket_api.async_register_command(hass, websocket_network_status) websocket_api.async_register_command(hass, websocket_get_config) websocket_api.async_register_command(hass, websocket_get_migration_config) + websocket_api.async_register_command(hass, websocket_start_ozw_config_flow) diff --git a/tests/components/ozw/conftest.py b/tests/components/ozw/conftest.py index b2bd6486d0f..00f8d8e52d2 100644 --- a/tests/components/ozw/conftest.py +++ b/tests/components/ozw/conftest.py @@ -16,6 +16,12 @@ def generic_data_fixture(): return load_fixture("ozw/generic_network_dump.csv") +@pytest.fixture(name="migration_data", scope="session") +def migration_data_fixture(): + """Load migration MQTT data and return it.""" + return load_fixture("ozw/migration_fixture.csv") + + @pytest.fixture(name="fan_data", scope="session") def fan_data_fixture(): """Load fan MQTT data and return it.""" diff --git a/tests/components/ozw/test_config_flow.py b/tests/components/ozw/test_config_flow.py index c7ff2512ca7..d1ac413270d 100644 --- a/tests/components/ozw/test_config_flow.py +++ b/tests/components/ozw/test_config_flow.py @@ -535,3 +535,52 @@ async def test_discovery_addon_not_installed( assert result["type"] == "form" assert result["step_id"] == "start_addon" + + +async def test_import_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( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={"usb_path": "/test/imported", "network_key": "imported123"}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "on_supervisor" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"use_addon": True} + ) + + assert result["type"] == "form" + assert result["step_id"] == "start_addon" + + # the default input should be the imported data + default_input = result["data_schema"]({}) + + 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"], default_input + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == TITLE + assert result["data"] == { + "usb_path": "/test/imported", + "network_key": "imported123", + "use_addon": True, + "integration_created_addon": False, + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/ozw/test_migration.py b/tests/components/ozw/test_migration.py new file mode 100644 index 00000000000..d83a39f2b15 --- /dev/null +++ b/tests/components/ozw/test_migration.py @@ -0,0 +1,292 @@ +"""Test zwave to ozw migration.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.ozw.websocket_api import ID, TYPE +from homeassistant.helpers.device_registry import ( + DeviceEntry, + async_get_registry as async_get_device_registry, +) +from homeassistant.helpers.entity_registry import ( + RegistryEntry, + async_get_registry as async_get_entity_registry, +) + +from .common import setup_ozw + +from tests.common import MockConfigEntry, mock_device_registry, mock_registry + +ZWAVE_SOURCE_NODE_DEVICE_ID = "zwave_source_node_device_id" +ZWAVE_SOURCE_NODE_DEVICE_NAME = "Z-Wave Source Node Device" +ZWAVE_SOURCE_NODE_DEVICE_AREA = "Z-Wave Source Node Area" +ZWAVE_SOURCE_ENTITY = "sensor.zwave_source_node" +ZWAVE_SOURCE_NODE_UNIQUE_ID = "10-4321" +ZWAVE_BATTERY_DEVICE_ID = "zwave_battery_device_id" +ZWAVE_BATTERY_DEVICE_NAME = "Z-Wave Battery Device" +ZWAVE_BATTERY_DEVICE_AREA = "Z-Wave Battery Area" +ZWAVE_BATTERY_ENTITY = "sensor.zwave_battery_level" +ZWAVE_BATTERY_UNIQUE_ID = "36-1234" +ZWAVE_BATTERY_NAME = "Z-Wave Battery Level" +ZWAVE_BATTERY_ICON = "mdi:zwave-test-battery" +ZWAVE_POWER_DEVICE_ID = "zwave_power_device_id" +ZWAVE_POWER_DEVICE_NAME = "Z-Wave Power Device" +ZWAVE_POWER_DEVICE_AREA = "Z-Wave Power Area" +ZWAVE_POWER_ENTITY = "binary_sensor.zwave_power" +ZWAVE_POWER_UNIQUE_ID = "32-5678" +ZWAVE_POWER_NAME = "Z-Wave Power" +ZWAVE_POWER_ICON = "mdi:zwave-test-power" + + +@pytest.fixture(name="zwave_migration_data") +def zwave_migration_data_fixture(hass): + """Return mock zwave migration data.""" + zwave_source_node_device = DeviceEntry( + id=ZWAVE_SOURCE_NODE_DEVICE_ID, + name_by_user=ZWAVE_SOURCE_NODE_DEVICE_NAME, + area_id=ZWAVE_SOURCE_NODE_DEVICE_AREA, + ) + zwave_source_node_entry = RegistryEntry( + entity_id=ZWAVE_SOURCE_ENTITY, + unique_id=ZWAVE_SOURCE_NODE_UNIQUE_ID, + platform="zwave", + name="Z-Wave Source Node", + ) + zwave_battery_device = DeviceEntry( + id=ZWAVE_BATTERY_DEVICE_ID, + name_by_user=ZWAVE_BATTERY_DEVICE_NAME, + area_id=ZWAVE_BATTERY_DEVICE_AREA, + ) + zwave_battery_entry = RegistryEntry( + entity_id=ZWAVE_BATTERY_ENTITY, + unique_id=ZWAVE_BATTERY_UNIQUE_ID, + platform="zwave", + name=ZWAVE_BATTERY_NAME, + icon=ZWAVE_BATTERY_ICON, + ) + zwave_power_device = DeviceEntry( + id=ZWAVE_POWER_DEVICE_ID, + name_by_user=ZWAVE_POWER_DEVICE_NAME, + area_id=ZWAVE_POWER_DEVICE_AREA, + ) + zwave_power_entry = RegistryEntry( + entity_id=ZWAVE_POWER_ENTITY, + unique_id=ZWAVE_POWER_UNIQUE_ID, + platform="zwave", + name=ZWAVE_POWER_NAME, + icon=ZWAVE_POWER_ICON, + ) + zwave_migration_data = { + ZWAVE_SOURCE_NODE_UNIQUE_ID: { + "node_id": 10, + "node_instance": 1, + "device_id": zwave_source_node_device.id, + "command_class": 113, + "command_class_label": "SourceNodeId", + "value_index": 2, + "unique_id": ZWAVE_SOURCE_NODE_UNIQUE_ID, + "entity_entry": zwave_source_node_entry, + }, + ZWAVE_BATTERY_UNIQUE_ID: { + "node_id": 36, + "node_instance": 1, + "device_id": zwave_battery_device.id, + "command_class": 128, + "command_class_label": "Battery Level", + "value_index": 0, + "unique_id": ZWAVE_BATTERY_UNIQUE_ID, + "entity_entry": zwave_battery_entry, + }, + ZWAVE_POWER_UNIQUE_ID: { + "node_id": 32, + "node_instance": 1, + "device_id": zwave_power_device.id, + "command_class": 50, + "command_class_label": "Power", + "value_index": 8, + "unique_id": ZWAVE_POWER_UNIQUE_ID, + "entity_entry": zwave_power_entry, + }, + } + + mock_device_registry( + hass, + { + zwave_source_node_device.id: zwave_source_node_device, + zwave_battery_device.id: zwave_battery_device, + zwave_power_device.id: zwave_power_device, + }, + ) + mock_registry( + hass, + { + ZWAVE_SOURCE_ENTITY: zwave_source_node_entry, + ZWAVE_BATTERY_ENTITY: zwave_battery_entry, + ZWAVE_POWER_ENTITY: zwave_power_entry, + }, + ) + + return zwave_migration_data + + +@pytest.fixture(name="zwave_integration") +def zwave_integration_fixture(hass, zwave_migration_data): + """Mock the zwave integration.""" + hass.config.components.add("zwave") + zwave_config_entry = MockConfigEntry(domain="zwave", data={"usb_path": "/dev/test"}) + zwave_config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.zwave.async_get_ozw_migration_data", + return_value=zwave_migration_data, + ): + yield zwave_config_entry + + +async def test_migrate_zwave(hass, migration_data, hass_ws_client, zwave_integration): + """Test the zwave to ozw migration websocket api.""" + await setup_ozw(hass, fixture=migration_data) + client = await hass_ws_client(hass) + + assert hass.config_entries.async_entries("zwave") + + await client.send_json({ID: 5, TYPE: "ozw/migrate_zwave", "dry_run": False}) + msg = await client.receive_json() + result = msg["result"] + + migration_entity_map = { + ZWAVE_BATTERY_ENTITY: "sensor.water_sensor_6_battery_level", + } + + assert result["zwave_entity_ids"] == [ + ZWAVE_SOURCE_ENTITY, + ZWAVE_BATTERY_ENTITY, + ZWAVE_POWER_ENTITY, + ] + assert result["ozw_entity_ids"] == [ + "sensor.smart_plug_electric_w", + "sensor.water_sensor_6_battery_level", + ] + assert result["migration_entity_map"] == migration_entity_map + assert result["migrated"] is True + + dev_reg = await async_get_device_registry(hass) + ent_reg = await async_get_entity_registry(hass) + + # check the device registry migration + + # check that the migrated entries have correct attributes + battery_entry = dev_reg.async_get_device( + identifiers={("ozw", "1.36.1")}, connections=set() + ) + assert battery_entry.name_by_user == ZWAVE_BATTERY_DEVICE_NAME + assert battery_entry.area_id == ZWAVE_BATTERY_DEVICE_AREA + power_entry = dev_reg.async_get_device( + identifiers={("ozw", "1.32.1")}, connections=set() + ) + assert power_entry.name_by_user == ZWAVE_POWER_DEVICE_NAME + assert power_entry.area_id == ZWAVE_POWER_DEVICE_AREA + + migration_device_map = { + ZWAVE_BATTERY_DEVICE_ID: battery_entry.id, + ZWAVE_POWER_DEVICE_ID: power_entry.id, + } + + assert result["migration_device_map"] == migration_device_map + + # check the entity registry migration + + # this should have been migrated and no longer present under that id + assert not ent_reg.async_is_registered("sensor.water_sensor_6_battery_level") + + # these should not have been migrated and is still in the registry + assert ent_reg.async_is_registered(ZWAVE_SOURCE_ENTITY) + source_entry = ent_reg.async_get(ZWAVE_SOURCE_ENTITY) + assert source_entry.unique_id == ZWAVE_SOURCE_NODE_UNIQUE_ID + assert ent_reg.async_is_registered(ZWAVE_POWER_ENTITY) + source_entry = ent_reg.async_get(ZWAVE_POWER_ENTITY) + assert source_entry.unique_id == ZWAVE_POWER_UNIQUE_ID + assert ent_reg.async_is_registered("sensor.smart_plug_electric_w") + + # this is the new entity_id of the ozw entity + assert ent_reg.async_is_registered(ZWAVE_BATTERY_ENTITY) + + # check that the migrated entries have correct attributes + battery_entry = ent_reg.async_get(ZWAVE_BATTERY_ENTITY) + assert battery_entry.unique_id == "1-36-610271249" + assert battery_entry.name == ZWAVE_BATTERY_NAME + assert battery_entry.icon == ZWAVE_BATTERY_ICON + + # check that the zwave config entry has been removed + assert not hass.config_entries.async_entries("zwave") + + # Check that the zwave integration fails entry setup after migration + zwave_config_entry = MockConfigEntry(domain="zwave") + zwave_config_entry.add_to_hass(hass) + assert not await hass.config_entries.async_setup(zwave_config_entry.entry_id) + + +async def test_migrate_zwave_dry_run( + hass, migration_data, hass_ws_client, zwave_integration +): + """Test the zwave to ozw migration websocket api dry run.""" + await setup_ozw(hass, fixture=migration_data) + client = await hass_ws_client(hass) + + await client.send_json({ID: 5, TYPE: "ozw/migrate_zwave"}) + msg = await client.receive_json() + result = msg["result"] + + migration_entity_map = { + ZWAVE_BATTERY_ENTITY: "sensor.water_sensor_6_battery_level", + } + + assert result["zwave_entity_ids"] == [ + ZWAVE_SOURCE_ENTITY, + ZWAVE_BATTERY_ENTITY, + ZWAVE_POWER_ENTITY, + ] + assert result["ozw_entity_ids"] == [ + "sensor.smart_plug_electric_w", + "sensor.water_sensor_6_battery_level", + ] + assert result["migration_entity_map"] == migration_entity_map + assert result["migrated"] is False + + ent_reg = await async_get_entity_registry(hass) + + # no real migration should have been done + assert ent_reg.async_is_registered("sensor.water_sensor_6_battery_level") + assert ent_reg.async_is_registered("sensor.smart_plug_electric_w") + + assert ent_reg.async_is_registered(ZWAVE_SOURCE_ENTITY) + source_entry = ent_reg.async_get(ZWAVE_SOURCE_ENTITY) + assert source_entry.unique_id == ZWAVE_SOURCE_NODE_UNIQUE_ID + + assert ent_reg.async_is_registered(ZWAVE_BATTERY_ENTITY) + battery_entry = ent_reg.async_get(ZWAVE_BATTERY_ENTITY) + assert battery_entry.unique_id == ZWAVE_BATTERY_UNIQUE_ID + + assert ent_reg.async_is_registered(ZWAVE_POWER_ENTITY) + power_entry = ent_reg.async_get(ZWAVE_POWER_ENTITY) + assert power_entry.unique_id == ZWAVE_POWER_UNIQUE_ID + + # check that the zwave config entry has not been removed + assert hass.config_entries.async_entries("zwave") + + # Check that the zwave integration can be setup after dry run + zwave_config_entry = zwave_integration + with patch("openzwave.option.ZWaveOption"), patch("openzwave.network.ZWaveNetwork"): + assert await hass.config_entries.async_setup(zwave_config_entry.entry_id) + + +async def test_migrate_zwave_not_setup(hass, migration_data, hass_ws_client): + """Test the zwave to ozw migration websocket without zwave setup.""" + await setup_ozw(hass, fixture=migration_data) + client = await hass_ws_client(hass) + + await client.send_json({ID: 5, TYPE: "ozw/migrate_zwave"}) + msg = await client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_not_loaded" + assert msg["error"]["message"] == "Integration zwave is not loaded" diff --git a/tests/components/zwave/test_websocket_api.py b/tests/components/zwave/test_websocket_api.py index 25bc364a630..9727906709f 100644 --- a/tests/components/zwave/test_websocket_api.py +++ b/tests/components/zwave/test_websocket_api.py @@ -1,4 +1,6 @@ """Test Z-Wave Websocket API.""" +from unittest.mock import call, patch + from homeassistant.bootstrap import async_setup_component from homeassistant.components.zwave.const import ( CONF_AUTOHEAL, @@ -8,6 +10,8 @@ from homeassistant.components.zwave.const import ( ) from homeassistant.components.zwave.websocket_api import ID, TYPE +NETWORK_KEY = "0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST" + async def test_zwave_ws_api(hass, mock_openzwave, hass_ws_client): """Test Z-Wave websocket API.""" @@ -20,7 +24,7 @@ async def test_zwave_ws_api(hass, mock_openzwave, hass_ws_client): CONF_AUTOHEAL: False, CONF_USB_STICK_PATH: "/dev/zwave", CONF_POLLING_INTERVAL: 6000, - CONF_NETWORK_KEY: "0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST", + CONF_NETWORK_KEY: NETWORK_KEY, } }, ) @@ -38,12 +42,47 @@ async def test_zwave_ws_api(hass, mock_openzwave, hass_ws_client): assert not result[CONF_AUTOHEAL] assert result[CONF_POLLING_INTERVAL] == 6000 + +async def test_zwave_ozw_migration_api(hass, mock_openzwave, hass_ws_client): + """Test Z-Wave to OpenZWave websocket migration API.""" + + await async_setup_component( + hass, + "zwave", + { + "zwave": { + CONF_AUTOHEAL: False, + CONF_USB_STICK_PATH: "/dev/zwave", + CONF_POLLING_INTERVAL: 6000, + CONF_NETWORK_KEY: NETWORK_KEY, + } + }, + ) + + await hass.async_block_till_done() + + client = await hass_ws_client(hass) + await client.send_json({ID: 6, TYPE: "zwave/get_migration_config"}) msg = await client.receive_json() result = msg["result"] assert result[CONF_USB_STICK_PATH] == "/dev/zwave" - assert ( - result[CONF_NETWORK_KEY] - == "0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST" + assert result[CONF_NETWORK_KEY] == NETWORK_KEY + + with patch( + "homeassistant.config_entries.ConfigEntriesFlowManager.async_init" + ) as async_init: + + async_init.return_value = {"flow_id": "mock_flow_id"} + await client.send_json({ID: 7, TYPE: "zwave/start_ozw_config_flow"}) + msg = await client.receive_json() + + result = msg["result"] + + assert result["flow_id"] == "mock_flow_id" + assert async_init.call_args == call( + "ozw", + context={"source": "import"}, + data={"usb_path": "/dev/zwave", "network_key": NETWORK_KEY}, ) diff --git a/tests/fixtures/ozw/migration_fixture.csv b/tests/fixtures/ozw/migration_fixture.csv new file mode 100644 index 00000000000..92b68f448f6 --- /dev/null +++ b/tests/fixtures/ozw/migration_fixture.csv @@ -0,0 +1,9 @@ +OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1008", "OZWDeamon_Version": "0.1", "QTOpenZWave_Version": "1.0.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1579566933, "ManufacturerSpecificDBReady": true, "homeID": 3245146787, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 3.95", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/zwave"} +OpenZWave/1/node/32/,{ "NodeID": 32, "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/0208:0005:0101", "ZWAProductURL": "", "ProductPic": "images/hank/hkzw-so01-smartplug.png", "Description": "fixture description", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "Smart Plug", "ProductPicBase64": "iVBORggg==" }, "Event": "nodeQueriesComplete", "TimeStamp": 1579566933, "NodeManufacturerName": "HANK Electronics Ltd", "NodeProductName": "HKZW-SO01 Smart Plug", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Binary Switch", "NodeGeneric": 16, "NodeSpecificString": "Binary Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x0208", "NodeProductType": "0x0101", "NodeProductID": "0x0005", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 1, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "On/Off Power Switch", "NodeDeviceType": 1792, "NodeRole": 5, "NodeRoleString": "Always On Slave", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 33, 36, 37, 39 ]} +OpenZWave/1/node/32/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/50/,{ "Instance": 1, "CommandClassId": 50, "CommandClass": "COMMAND_CLASS_METER", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/50/value/562950495305746/,{ "Label": "Electric - W", "Value": 0.0, "Units": "W", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 2, "Node": 32, "Genre": "User", "Help": "", "ValueIDKey": 562950495305746, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/,{ "NodeID": 36, "NodeQueryStage": "CacheLoad", "isListening": false, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0086:007A:0102", "ZWAProductURL": "", "ProductPic": "images/aeotec/zw122.png", "Description": "fixture description", "WakeupHelp": "Pressing the Action Button once will trigger sending the Wake up notification command. If press and hold the Z-Wave button for 3 seconds, the Water Sensor will wake up for 10 minutes.", "ProductSupportURL": "", "Frequency": "", "Name": "Water Sensor 6", "ProductPicBase64": "kSuQmCC" }, "Event": "nodeNaming", "TimeStamp": 1579566891, "NodeManufacturerName": "AEON Labs", "NodeProductName": "ZW122 Water Sensor 6", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Notification Sensor", "NodeGeneric": 7, "NodeSpecificString": "Notification Sensor", "NodeSpecific": 1, "NodeManufacturerID": "0x0086", "NodeProductType": "0x0102", "NodeProductID": "0x007a", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 4} +OpenZWave/1/node/36/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/128/,{ "Instance": 1, "CommandClassId": 128, "CommandClass": "COMMAND_CLASS_BATTERY", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/128/value/610271249/,{ "Label": "Battery Level", "Value": 100, "Units": "%", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BATTERY", "Index": 0, "Node": 36, "Genre": "User", "Help": "Current Battery Level", "ValueIDKey": 610271249, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} From eabe757e200c442ec1db0bc9e96d83674b70a929 Mon Sep 17 00:00:00 2001 From: ehendrix23 Date: Sat, 9 Jan 2021 07:29:48 -0700 Subject: [PATCH 147/507] Bump pymyq to 2.0.13 (#44961) --- homeassistant/components/myq/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index 5f863ad7f34..653a2229296 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.12"], + "requirements": ["pymyq==2.0.13"], "codeowners": ["@bdraco"], "config_flow": true, "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 09add60b9f3..d897990dc76 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1539,7 +1539,7 @@ pymsteams==0.1.12 pymusiccast==0.1.6 # homeassistant.components.myq -pymyq==2.0.12 +pymyq==2.0.13 # homeassistant.components.mysensors pymysensors==0.18.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1c040933b99..1c82b6d4f4f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -782,7 +782,7 @@ pymodbus==2.3.0 pymonoprice==0.3 # homeassistant.components.myq -pymyq==2.0.12 +pymyq==2.0.13 # homeassistant.components.nut pynut2==2.1.2 From 248802efd546a87c2323d15c6aa48f88ff4e6a8d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 9 Jan 2021 17:46:53 +0100 Subject: [PATCH 148/507] Add MQTT base entity (#44971) --- .../components/mqtt/alarm_control_panel.py | 63 ++-------------- .../components/mqtt/binary_sensor.py | 62 ++------------- homeassistant/components/mqtt/camera.py | 51 ++----------- homeassistant/components/mqtt/climate.py | 61 ++------------- homeassistant/components/mqtt/cover.py | 63 ++-------------- .../mqtt/device_tracker/schema_discovery.py | 58 ++------------ homeassistant/components/mqtt/fan.py | 63 ++-------------- .../components/mqtt/light/schema_basic.py | 64 ++-------------- .../components/mqtt/light/schema_json.py | 63 ++-------------- .../components/mqtt/light/schema_template.py | 66 ++-------------- homeassistant/components/mqtt/lock.py | 63 ++-------------- homeassistant/components/mqtt/mixins.py | 75 ++++++++++++++++++- homeassistant/components/mqtt/number.py | 58 ++------------ homeassistant/components/mqtt/sensor.py | 58 ++------------ homeassistant/components/mqtt/switch.py | 64 ++-------------- .../components/mqtt/vacuum/schema_legacy.py | 64 ++-------------- .../components/mqtt/vacuum/schema_state.py | 59 ++------------- 17 files changed, 190 insertions(+), 865 deletions(-) diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 4c06f6af7c3..38fec57607e 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -48,10 +48,7 @@ from .mixins import ( MQTT_AVAILABILITY_SCHEMA, MQTT_ENTITY_DEVICE_INFO_SCHEMA, MQTT_JSON_ATTRS_SCHEMA, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, + MqttEntity, async_setup_entry_helper, ) @@ -127,46 +124,19 @@ async def _async_setup_entity( async_add_entities([MqttAlarm(hass, config, config_entry, discovery_data)]) -class MqttAlarm( - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - alarm.AlarmControlPanelEntity, -): +class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): """Representation of a MQTT alarm status.""" def __init__(self, hass, config, config_entry, discovery_data): """Init the MQTT Alarm Control Panel.""" - self.hass = hass self._state = None - self._unique_id = config.get(CONF_UNIQUE_ID) - self._sub_state = None - # Load config - self._setup_from_config(config) + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) - device_config = config.get(CONF_DEVICE) - - MqttAttributes.__init__(self, config) - MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config, config_entry) - - async def async_added_to_hass(self): - """Subscribe mqtt events.""" - await super().async_added_to_hass() - await self._subscribe_topics() - - async def discovery_update(self, discovery_payload): - """Handle updated discovery message.""" - config = PLATFORM_SCHEMA(discovery_payload) - self._setup_from_config(config) - await self.attributes_discovery_update(config) - await self.availability_discovery_update(config) - await self.device_info_discovery_update(config) - await self._subscribe_topics() - self.async_write_ha_state() + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA def _setup_from_config(self, config): self._config = config @@ -217,30 +187,11 @@ class MqttAlarm( }, ) - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._sub_state = await subscription.async_unsubscribe_topics( - self.hass, self._sub_state - ) - await MqttAttributes.async_will_remove_from_hass(self) - await MqttAvailability.async_will_remove_from_hass(self) - await MqttDiscoveryUpdate.async_will_remove_from_hass(self) - - @property - def should_poll(self): - """No polling needed.""" - return False - @property def name(self): """Return the name of the device.""" return self._config[CONF_NAME] - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id - @property def state(self): """Return the state of the device.""" diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 21d7740442c..d965401cef4 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -35,10 +35,8 @@ from .mixins import ( MQTT_AVAILABILITY_SCHEMA, MQTT_ENTITY_DEVICE_INFO_SCHEMA, MQTT_JSON_ATTRS_SCHEMA, - MqttAttributes, MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, + MqttEntity, async_setup_entry_helper, ) @@ -94,21 +92,12 @@ async def _async_setup_entity( async_add_entities([MqttBinarySensor(hass, config, config_entry, discovery_data)]) -class MqttBinarySensor( - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - BinarySensorEntity, -): +class MqttBinarySensor(MqttEntity, BinarySensorEntity): """Representation a binary sensor that is updated by MQTT.""" def __init__(self, hass, config, config_entry, discovery_data): """Initialize the MQTT binary sensor.""" - self.hass = hass - self._unique_id = config.get(CONF_UNIQUE_ID) self._state = None - self._sub_state = None self._expiration_trigger = None self._delay_listener = None expire_after = config.get(CONF_EXPIRE_AFTER) @@ -117,30 +106,12 @@ class MqttBinarySensor( else: self._expired = None - # Load config - self._setup_from_config(config) + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) - device_config = config.get(CONF_DEVICE) - - MqttAttributes.__init__(self, config) - MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config, config_entry) - - async def async_added_to_hass(self): - """Subscribe mqtt events.""" - await super().async_added_to_hass() - await self._subscribe_topics() - - async def discovery_update(self, discovery_payload): - """Handle updated discovery message.""" - config = PLATFORM_SCHEMA(discovery_payload) - self._setup_from_config(config) - await self.attributes_discovery_update(config) - await self.availability_discovery_update(config) - await self.device_info_discovery_update(config) - await self._subscribe_topics() - self.async_write_ha_state() + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA def _setup_from_config(self, config): self._config = config @@ -240,15 +211,6 @@ class MqttBinarySensor( }, ) - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._sub_state = await subscription.async_unsubscribe_topics( - self.hass, self._sub_state - ) - await MqttAttributes.async_will_remove_from_hass(self) - await MqttAvailability.async_will_remove_from_hass(self) - await MqttDiscoveryUpdate.async_will_remove_from_hass(self) - @callback def _value_is_expired(self, *_): """Triggered when value is expired.""" @@ -258,11 +220,6 @@ class MqttBinarySensor( self.async_write_ha_state() - @property - def should_poll(self): - """Return the polling state.""" - return False - @property def name(self): """Return the name of the binary sensor.""" @@ -283,11 +240,6 @@ class MqttBinarySensor( """Force update.""" return self._config[CONF_FORCE_UPDATE] - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id - @property def available(self) -> bool: """Return true if the device is available and value has not expired.""" diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 5600dd8a1e3..21fcb9276dd 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -19,10 +19,7 @@ from .mixins import ( MQTT_AVAILABILITY_SCHEMA, MQTT_ENTITY_DEVICE_INFO_SCHEMA, MQTT_JSON_ATTRS_SCHEMA, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, + MqttEntity, async_setup_entry_helper, ) @@ -69,41 +66,23 @@ async def _async_setup_entity( async_add_entities([MqttCamera(config, config_entry, discovery_data)]) -class MqttCamera( - MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, Camera -): +class MqttCamera(MqttEntity, Camera): """representation of a MQTT camera.""" def __init__(self, config, config_entry, discovery_data): """Initialize the MQTT Camera.""" - self._config = config - self._unique_id = config.get(CONF_UNIQUE_ID) - self._sub_state = None - self._last_image = None - device_config = config.get(CONF_DEVICE) - Camera.__init__(self) - MqttAttributes.__init__(self, config) - MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config, config_entry) + MqttEntity.__init__(self, None, config, config_entry, discovery_data) - async def async_added_to_hass(self): - """Subscribe MQTT events.""" - await super().async_added_to_hass() - await self._subscribe_topics() + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA - async def discovery_update(self, discovery_payload): - """Handle updated discovery message.""" - config = PLATFORM_SCHEMA(discovery_payload) + def _setup_from_config(self, config): self._config = config - await self.attributes_discovery_update(config) - await self.availability_discovery_update(config) - await self.device_info_discovery_update(config) - await self._subscribe_topics() - self.async_write_ha_state() async def _subscribe_topics(self): """(Re)Subscribe to topics.""" @@ -127,15 +106,6 @@ class MqttCamera( }, ) - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._sub_state = await subscription.async_unsubscribe_topics( - self.hass, self._sub_state - ) - await MqttAttributes.async_will_remove_from_hass(self) - await MqttAvailability.async_will_remove_from_hass(self) - await MqttDiscoveryUpdate.async_will_remove_from_hass(self) - async def async_camera_image(self): """Return image response.""" return self._last_image @@ -144,8 +114,3 @@ class MqttCamera( def name(self): """Return the name of this camera.""" return self._config[CONF_NAME] - - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 1c9bbd74bfb..b0d9ebd6ffa 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -65,10 +65,7 @@ from .mixins import ( MQTT_AVAILABILITY_SCHEMA, MQTT_ENTITY_DEVICE_INFO_SCHEMA, MQTT_JSON_ATTRS_SCHEMA, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, + MqttEntity, async_setup_entry_helper, ) @@ -267,22 +264,11 @@ async def _async_setup_entity( async_add_entities([MqttClimate(hass, config, config_entry, discovery_data)]) -class MqttClimate( - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - ClimateEntity, -): +class MqttClimate(MqttEntity, ClimateEntity): """Representation of an MQTT climate device.""" def __init__(self, hass, config, config_entry, discovery_data): """Initialize the climate device.""" - self._config = config - self._unique_id = config.get(CONF_UNIQUE_ID) - self._sub_state = None - - self.hass = hass self._action = None self._aux = False self._away = False @@ -297,33 +283,21 @@ class MqttClimate( self._topic = None self._value_templates = None - self._setup_from_config(config) + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) - device_config = config.get(CONF_DEVICE) - - MqttAttributes.__init__(self, config) - MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config, config_entry) + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA async def async_added_to_hass(self): """Handle being added to Home Assistant.""" await super().async_added_to_hass() await self._subscribe_topics() - async def discovery_update(self, discovery_payload): - """Handle updated discovery message.""" - config = PLATFORM_SCHEMA(discovery_payload) - self._config = config - self._setup_from_config(config) - await self.attributes_discovery_update(config) - await self.availability_discovery_update(config) - await self.device_info_discovery_update(config) - await self._subscribe_topics() - self.async_write_ha_state() - def _setup_from_config(self, config): """(Re)Setup the entity.""" + self._config = config self._topic = {key: config.get(key) for key in TOPIC_KEYS} # set to None in non-optimistic mode @@ -556,30 +530,11 @@ class MqttClimate( self.hass, self._sub_state, topics ) - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._sub_state = await subscription.async_unsubscribe_topics( - self.hass, self._sub_state - ) - await MqttAttributes.async_will_remove_from_hass(self) - await MqttAvailability.async_will_remove_from_hass(self) - await MqttDiscoveryUpdate.async_will_remove_from_hass(self) - - @property - def should_poll(self): - """Return the polling state.""" - return False - @property def name(self): """Return the name of the climate device.""" return self._config[CONF_NAME] - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id - @property def temperature_unit(self): """Return the unit of measurement.""" diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 498d6ffc8af..dc2cba0efab 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -52,10 +52,7 @@ from .mixins import ( MQTT_AVAILABILITY_SCHEMA, MQTT_ENTITY_DEVICE_INFO_SCHEMA, MQTT_JSON_ATTRS_SCHEMA, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, + MqttEntity, async_setup_entry_helper, ) @@ -198,51 +195,24 @@ async def _async_setup_entity( async_add_entities([MqttCover(hass, config, config_entry, discovery_data)]) -class MqttCover( - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - CoverEntity, -): +class MqttCover(MqttEntity, CoverEntity): """Representation of a cover that can be controlled using MQTT.""" def __init__(self, hass, config, config_entry, discovery_data): """Initialize the cover.""" - self.hass = hass - self._unique_id = config.get(CONF_UNIQUE_ID) self._position = None self._state = None - self._sub_state = None self._optimistic = None self._tilt_value = None self._tilt_optimistic = None - # Load config - self._setup_from_config(config) + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) - device_config = config.get(CONF_DEVICE) - - MqttAttributes.__init__(self, config) - MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config, config_entry) - - async def async_added_to_hass(self): - """Subscribe MQTT events.""" - await super().async_added_to_hass() - await self._subscribe_topics() - - async def discovery_update(self, discovery_payload): - """Handle updated discovery message.""" - config = PLATFORM_SCHEMA(discovery_payload) - self._setup_from_config(config) - await self.attributes_discovery_update(config) - await self.availability_discovery_update(config) - await self.device_info_discovery_update(config) - await self._subscribe_topics() - self.async_write_ha_state() + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA def _setup_from_config(self, config): self._config = config @@ -367,20 +337,6 @@ class MqttCover( self.hass, self._sub_state, topics ) - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._sub_state = await subscription.async_unsubscribe_topics( - self.hass, self._sub_state - ) - await MqttAttributes.async_will_remove_from_hass(self) - await MqttAvailability.async_will_remove_from_hass(self) - await MqttDiscoveryUpdate.async_will_remove_from_hass(self) - - @property - def should_poll(self): - """No polling needed.""" - return False - @property def assumed_state(self): """Return true if we do optimistic updates.""" @@ -628,8 +584,3 @@ class MqttCover( if range_type == TILT_PAYLOAD and self._config[CONF_TILT_INVERT_STATE]: position = max_range - position + offset return position - - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker/schema_discovery.py index 8e6019eefd7..8b51b9fac0e 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_discovery.py +++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py @@ -30,10 +30,7 @@ from ..mixins import ( MQTT_AVAILABILITY_SCHEMA, MQTT_ENTITY_DEVICE_INFO_SCHEMA, MQTT_JSON_ATTRS_SCHEMA, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, + MqttEntity, async_setup_entry_helper, ) @@ -78,46 +75,19 @@ async def _async_setup_entity( async_add_entities([MqttDeviceTracker(hass, config, config_entry, discovery_data)]) -class MqttDeviceTracker( - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - TrackerEntity, -): +class MqttDeviceTracker(MqttEntity, TrackerEntity): """Representation of a device tracker using MQTT.""" def __init__(self, hass, config, config_entry, discovery_data): """Initialize the tracker.""" - self.hass = hass self._location_name = None - self._sub_state = None - self._unique_id = config.get(CONF_UNIQUE_ID) - # Load config - self._setup_from_config(config) + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) - device_config = config.get(CONF_DEVICE) - - MqttAttributes.__init__(self, config) - MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config, config_entry) - - async def async_added_to_hass(self): - """Subscribe to MQTT events.""" - await super().async_added_to_hass() - await self._subscribe_topics() - - async def discovery_update(self, discovery_payload): - """Handle updated discovery message.""" - config = PLATFORM_SCHEMA_DISCOVERY(discovery_payload) - self._setup_from_config(config) - await self.attributes_discovery_update(config) - await self.availability_discovery_update(config) - await self.device_info_discovery_update(config) - await self._subscribe_topics() - self.async_write_ha_state() + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA_DISCOVERY def _setup_from_config(self, config): """(Re)Setup the entity.""" @@ -159,15 +129,6 @@ class MqttDeviceTracker( }, ) - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._sub_state = await subscription.async_unsubscribe_topics( - self.hass, self._sub_state - ) - await MqttAttributes.async_will_remove_from_hass(self) - await MqttAvailability.async_will_remove_from_hass(self) - await MqttDiscoveryUpdate.async_will_remove_from_hass(self) - @property def icon(self): """Return the icon of the device.""" @@ -213,11 +174,6 @@ class MqttDeviceTracker( """Return the name of the device tracker.""" return self._config.get(CONF_NAME) - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id - @property def source_type(self): """Return the source type, eg gps or router, of the device.""" diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 5d3abd7b793..5d69fb87b91 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -44,10 +44,7 @@ from .mixins import ( MQTT_AVAILABILITY_SCHEMA, MQTT_ENTITY_DEVICE_INFO_SCHEMA, MQTT_JSON_ATTRS_SCHEMA, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, + MqttEntity, async_setup_entry_helper, ) @@ -139,24 +136,15 @@ async def _async_setup_entity( async_add_entities([MqttFan(hass, config, config_entry, discovery_data)]) -class MqttFan( - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - FanEntity, -): +class MqttFan(MqttEntity, FanEntity): """A MQTT fan component.""" def __init__(self, hass, config, config_entry, discovery_data): """Initialize the MQTT fan.""" - self.hass = hass - self._unique_id = config.get(CONF_UNIQUE_ID) self._state = False self._speed = None self._oscillation = None self._supported_features = 0 - self._sub_state = None self._topic = None self._payload = None @@ -165,30 +153,12 @@ class MqttFan( self._optimistic_oscillation = None self._optimistic_speed = None - # Load config - self._setup_from_config(config) + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) - device_config = config.get(CONF_DEVICE) - - MqttAttributes.__init__(self, config) - MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config, config_entry) - - async def async_added_to_hass(self): - """Subscribe to MQTT events.""" - await super().async_added_to_hass() - await self._subscribe_topics() - - async def discovery_update(self, discovery_payload): - """Handle updated discovery message.""" - config = PLATFORM_SCHEMA(discovery_payload) - self._setup_from_config(config) - await self.attributes_discovery_update(config) - await self.availability_discovery_update(config) - await self.device_info_discovery_update(config) - await self._subscribe_topics() - self.async_write_ha_state() + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA def _setup_from_config(self, config): """(Re)Setup the entity.""" @@ -312,20 +282,6 @@ class MqttFan( self.hass, self._sub_state, topics ) - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._sub_state = await subscription.async_unsubscribe_topics( - self.hass, self._sub_state - ) - await MqttAttributes.async_will_remove_from_hass(self) - await MqttAvailability.async_will_remove_from_hass(self) - await MqttDiscoveryUpdate.async_will_remove_from_hass(self) - - @property - def should_poll(self): - """No polling needed for a MQTT fan.""" - return False - @property def assumed_state(self): """Return true if we do optimistic updates.""" @@ -444,8 +400,3 @@ class MqttFan( if self._optimistic_oscillation: self._oscillation = oscillating self.async_write_ha_state() - - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 230efa5e60a..04f01ea0d3a 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -38,10 +38,7 @@ from ..mixins import ( MQTT_AVAILABILITY_SCHEMA, MQTT_ENTITY_DEVICE_INFO_SCHEMA, MQTT_JSON_ATTRS_SCHEMA, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, + MqttEntity, ) from .schema import MQTT_LIGHT_SCHEMA_SCHEMA @@ -163,21 +160,12 @@ async def async_setup_entity_basic( async_add_entities([MqttLight(hass, config, config_entry, discovery_data)]) -class MqttLight( - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - LightEntity, - RestoreEntity, -): +class MqttLight(MqttEntity, LightEntity, RestoreEntity): """Representation of a MQTT light.""" def __init__(self, hass, config, config_entry, discovery_data): """Initialize MQTT light.""" - self.hass = hass self._state = False - self._sub_state = None self._brightness = None self._hs = None self._color_temp = None @@ -196,32 +184,13 @@ class MqttLight( self._optimistic_hs = False self._optimistic_white_value = False self._optimistic_xy = False - self._unique_id = config.get(CONF_UNIQUE_ID) - # Load config - self._setup_from_config(config) + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) - device_config = config.get(CONF_DEVICE) - - MqttAttributes.__init__(self, config) - MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config, config_entry) - - async def async_added_to_hass(self): - """Subscribe to MQTT events.""" - await super().async_added_to_hass() - await self._subscribe_topics() - - async def discovery_update(self, discovery_payload): - """Handle updated discovery message.""" - config = PLATFORM_SCHEMA_BASIC(discovery_payload) - self._setup_from_config(config) - await self.attributes_discovery_update(config) - await self.availability_discovery_update(config) - await self.device_info_discovery_update(config) - await self._subscribe_topics() - self.async_write_ha_state() + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA_BASIC def _setup_from_config(self, config): """(Re)Setup the entity.""" @@ -525,15 +494,6 @@ class MqttLight( self.hass, self._sub_state, topics ) - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._sub_state = await subscription.async_unsubscribe_topics( - self.hass, self._sub_state - ) - await MqttAttributes.async_will_remove_from_hass(self) - await MqttAvailability.async_will_remove_from_hass(self) - await MqttDiscoveryUpdate.async_will_remove_from_hass(self) - @property def brightness(self): """Return the brightness of this light between 0..255.""" @@ -580,21 +540,11 @@ class MqttLight( return white_value return None - @property - def should_poll(self): - """No polling needed for a MQTT light.""" - return False - @property def name(self): """Return the name of the device if any.""" return self._config[CONF_NAME] - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id - @property def is_on(self): """Return true if device is on.""" diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 16281da4169..c6622578a6f 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -49,10 +49,7 @@ from ..mixins import ( MQTT_AVAILABILITY_SCHEMA, MQTT_ENTITY_DEVICE_INFO_SCHEMA, MQTT_JSON_ATTRS_SCHEMA, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, + MqttEntity, ) from .schema import MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import CONF_BRIGHTNESS_SCALE @@ -130,20 +127,12 @@ async def async_setup_entity_json( async_add_entities([MqttLightJson(config, config_entry, discovery_data)]) -class MqttLightJson( - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - LightEntity, - RestoreEntity, -): +class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): """Representation of a MQTT JSON light.""" def __init__(self, config, config_entry, discovery_data): """Initialize MQTT JSON light.""" self._state = False - self._sub_state = None self._supported_features = 0 self._topic = None @@ -154,32 +143,13 @@ class MqttLightJson( self._hs = None self._white_value = None self._flash_times = None - self._unique_id = config.get(CONF_UNIQUE_ID) - # Load config - self._setup_from_config(config) + MqttEntity.__init__(self, None, config, config_entry, discovery_data) - device_config = config.get(CONF_DEVICE) - - MqttAttributes.__init__(self, config) - MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config, config_entry) - - async def async_added_to_hass(self): - """Subscribe to MQTT events.""" - await super().async_added_to_hass() - await self._subscribe_topics() - - async def discovery_update(self, discovery_payload): - """Handle updated discovery message.""" - config = PLATFORM_SCHEMA_JSON(discovery_payload) - self._setup_from_config(config) - await self.attributes_discovery_update(config) - await self.availability_discovery_update(config) - await self.device_info_discovery_update(config) - await self._subscribe_topics() - self.async_write_ha_state() + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA_JSON def _setup_from_config(self, config): """(Re)Setup the entity.""" @@ -314,15 +284,6 @@ class MqttLightJson( if last_state.attributes.get(ATTR_WHITE_VALUE): self._white_value = last_state.attributes.get(ATTR_WHITE_VALUE) - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._sub_state = await subscription.async_unsubscribe_topics( - self.hass, self._sub_state - ) - await MqttAttributes.async_will_remove_from_hass(self) - await MqttAvailability.async_will_remove_from_hass(self) - await MqttDiscoveryUpdate.async_will_remove_from_hass(self) - @property def brightness(self): """Return the brightness of this light between 0..255.""" @@ -363,21 +324,11 @@ class MqttLightJson( """Return the white property.""" return self._white_value - @property - def should_poll(self): - """No polling needed for a MQTT light.""" - return False - @property def name(self): """Return the name of the device if any.""" return self._config[CONF_NAME] - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id - @property def is_on(self): """Return true if device is on.""" diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index e33d169e47a..e696e99552e 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -40,10 +40,7 @@ from ..mixins import ( MQTT_AVAILABILITY_SCHEMA, MQTT_ENTITY_DEVICE_INFO_SCHEMA, MQTT_JSON_ATTRS_SCHEMA, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, + MqttEntity, ) from .schema import MQTT_LIGHT_SCHEMA_SCHEMA @@ -103,20 +100,12 @@ async def async_setup_entity_template( async_add_entities([MqttLightTemplate(config, config_entry, discovery_data)]) -class MqttLightTemplate( - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - LightEntity, - RestoreEntity, -): +class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): """Representation of a MQTT Template light.""" def __init__(self, config, config_entry, discovery_data): """Initialize a MQTT Template light.""" self._state = False - self._sub_state = None self._topics = None self._templates = None @@ -128,32 +117,13 @@ class MqttLightTemplate( self._white_value = None self._hs = None self._effect = None - self._unique_id = config.get(CONF_UNIQUE_ID) - # Load config - self._setup_from_config(config) + MqttEntity.__init__(self, None, config, config_entry, discovery_data) - device_config = config.get(CONF_DEVICE) - - MqttAttributes.__init__(self, config) - MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config, config_entry) - - async def async_added_to_hass(self): - """Subscribe to MQTT events.""" - await super().async_added_to_hass() - await self._subscribe_topics() - - async def discovery_update(self, discovery_payload): - """Handle updated discovery message.""" - config = PLATFORM_SCHEMA_TEMPLATE(discovery_payload) - self._setup_from_config(config) - await self.attributes_discovery_update(config) - await self.availability_discovery_update(config) - await self.device_info_discovery_update(config) - await self._subscribe_topics() - self.async_write_ha_state() + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA_TEMPLATE def _setup_from_config(self, config): """(Re)Setup the entity.""" @@ -299,15 +269,6 @@ class MqttLightTemplate( if last_state.attributes.get(ATTR_WHITE_VALUE): self._white_value = last_state.attributes.get(ATTR_WHITE_VALUE) - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._sub_state = await subscription.async_unsubscribe_topics( - self.hass, self._sub_state - ) - await MqttAttributes.async_will_remove_from_hass(self) - await MqttAvailability.async_will_remove_from_hass(self) - await MqttDiscoveryUpdate.async_will_remove_from_hass(self) - @property def brightness(self): """Return the brightness of this light between 0..255.""" @@ -338,24 +299,11 @@ class MqttLightTemplate( """Return the white property.""" return self._white_value - @property - def should_poll(self): - """Return True if entity has to be polled for state. - - False if entity pushes its state to HA. - """ - return False - @property def name(self): """Return the name of the entity.""" return self._config[CONF_NAME] - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id - @property def is_on(self): """Return True if entity is on.""" diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index ef3ad6bb8eb..b08f8f8bb43 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -33,10 +33,7 @@ from .mixins import ( MQTT_AVAILABILITY_SCHEMA, MQTT_ENTITY_DEVICE_INFO_SCHEMA, MQTT_JSON_ATTRS_SCHEMA, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, + MqttEntity, async_setup_entry_helper, ) @@ -101,47 +98,20 @@ async def _async_setup_entity( async_add_entities([MqttLock(hass, config, config_entry, discovery_data)]) -class MqttLock( - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - LockEntity, -): +class MqttLock(MqttEntity, LockEntity): """Representation of a lock that can be toggled using MQTT.""" def __init__(self, hass, config, config_entry, discovery_data): """Initialize the lock.""" - self.hass = hass - self._unique_id = config.get(CONF_UNIQUE_ID) self._state = False - self._sub_state = None self._optimistic = False - # Load config - self._setup_from_config(config) + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) - device_config = config.get(CONF_DEVICE) - - MqttAttributes.__init__(self, config) - MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config, config_entry) - - async def async_added_to_hass(self): - """Subscribe to MQTT events.""" - await super().async_added_to_hass() - await self._subscribe_topics() - - async def discovery_update(self, discovery_payload): - """Handle updated discovery message.""" - config = PLATFORM_SCHEMA(discovery_payload) - self._setup_from_config(config) - await self.attributes_discovery_update(config) - await self.availability_discovery_update(config) - await self.device_info_discovery_update(config) - await self._subscribe_topics() - self.async_write_ha_state() + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA def _setup_from_config(self, config): """(Re)Setup the entity.""" @@ -187,30 +157,11 @@ class MqttLock( }, ) - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._sub_state = await subscription.async_unsubscribe_topics( - self.hass, self._sub_state - ) - await MqttAttributes.async_will_remove_from_hass(self) - await MqttAvailability.async_will_remove_from_hass(self) - await MqttDiscoveryUpdate.async_will_remove_from_hass(self) - - @property - def should_poll(self): - """No polling needed.""" - return False - @property def name(self): """Return the name of the lock.""" return self._config[CONF_NAME] - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id - @property def is_locked(self): """Return true if lock is locked.""" diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 42171ef4e34..e4fa7e15526 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -1,11 +1,12 @@ """MQTT component mixins and helpers.""" +from abc import abstractmethod import json import logging from typing import Optional import voluptuous as vol -from homeassistant.const import CONF_DEVICE, CONF_NAME +from homeassistant.const import CONF_DEVICE, CONF_NAME, CONF_UNIQUE_ID from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import ( @@ -15,7 +16,7 @@ from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType -from . import CONF_TOPIC, DATA_MQTT, debug_info, publish +from . import CONF_TOPIC, DATA_MQTT, debug_info, publish, subscription from .const import ( ATTR_DISCOVERY_HASH, ATTR_DISCOVERY_PAYLOAD, @@ -493,3 +494,73 @@ class MqttEntityDeviceInfo(Entity): def device_info(self): """Return a device description for device registry.""" return device_info_from_config(self._device_config) + + +class MqttEntity( + MqttAttributes, + MqttAvailability, + MqttDiscoveryUpdate, + MqttEntityDeviceInfo, +): + """Representation of an MQTT entity.""" + + def __init__(self, hass, config, config_entry, discovery_data): + """Init the MQTT Entity.""" + self.hass = hass + self._unique_id = config.get(CONF_UNIQUE_ID) + self._sub_state = None + + # Load config + self._setup_from_config(config) + + # Initialize mixin classes + MqttAttributes.__init__(self, config) + MqttAvailability.__init__(self, config) + MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) + MqttEntityDeviceInfo.__init__(self, config.get(CONF_DEVICE), config_entry) + + async def async_added_to_hass(self): + """Subscribe mqtt events.""" + await super().async_added_to_hass() + await self._subscribe_topics() + + async def discovery_update(self, discovery_payload): + """Handle updated discovery message.""" + config = self.config_schema()(discovery_payload) + self._setup_from_config(config) + await self.attributes_discovery_update(config) + await self.availability_discovery_update(config) + await self.device_info_discovery_update(config) + await self._subscribe_topics() + self.async_write_ha_state() + + async def async_will_remove_from_hass(self): + """Unsubscribe when removed.""" + self._sub_state = await subscription.async_unsubscribe_topics( + self.hass, self._sub_state + ) + await MqttAttributes.async_will_remove_from_hass(self) + await MqttAvailability.async_will_remove_from_hass(self) + await MqttDiscoveryUpdate.async_will_remove_from_hass(self) + + @staticmethod + @abstractmethod + def config_schema(): + """Return the config schema.""" + + def _setup_from_config(self, config): + """(Re)Setup the entity.""" + + @abstractmethod + async def _subscribe_topics(self): + """(Re)Subscribe to topics.""" + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index c844d888efe..159f466f7ae 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -33,10 +33,7 @@ from .mixins import ( MQTT_AVAILABILITY_SCHEMA, MQTT_ENTITY_DEVICE_INFO_SCHEMA, MQTT_JSON_ATTRS_SCHEMA, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, + MqttEntity, async_setup_entry_helper, ) @@ -84,47 +81,27 @@ async def _async_setup_entity( async_add_entities([MqttNumber(config, config_entry, discovery_data)]) -class MqttNumber( - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - NumberEntity, - RestoreEntity, -): +class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): """representation of an MQTT number.""" def __init__(self, config, config_entry, discovery_data): """Initialize the MQTT Number.""" - self._config = config self._sub_state = None self._current_number = None self._optimistic = config.get(CONF_OPTIMISTIC) self._unique_id = config.get(CONF_UNIQUE_ID) - device_config = config.get(CONF_DEVICE) - NumberEntity.__init__(self) - MqttAttributes.__init__(self, config) - MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config, config_entry) + MqttEntity.__init__(self, None, config, config_entry, discovery_data) - async def async_added_to_hass(self): - """Subscribe MQTT events.""" - await super().async_added_to_hass() - await self._subscribe_topics() + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA - async def discovery_update(self, discovery_payload): - """Handle updated discovery message.""" - config = PLATFORM_SCHEMA(discovery_payload) + def _setup_from_config(self, config): self._config = config - await self.attributes_discovery_update(config) - await self.availability_discovery_update(config) - await self.device_info_discovery_update(config) - await self._subscribe_topics() - self.async_write_ha_state() async def _subscribe_topics(self): """(Re)Subscribe to topics.""" @@ -165,15 +142,6 @@ class MqttNumber( if last_state: self._current_number = last_state.state - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._sub_state = await subscription.async_unsubscribe_topics( - self.hass, self._sub_state - ) - await MqttAttributes.async_will_remove_from_hass(self) - await MqttAvailability.async_will_remove_from_hass(self) - await MqttDiscoveryUpdate.async_will_remove_from_hass(self) - @property def value(self): """Return the current value.""" @@ -203,16 +171,6 @@ class MqttNumber( """Return the name of this number.""" return self._config[CONF_NAME] - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id - - @property - def should_poll(self): - """Return the polling state.""" - return False - @property def assumed_state(self): """Return true if we do optimistic updates.""" diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 159c4b1d118..3f79ab1bafe 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -33,10 +33,8 @@ from .mixins import ( MQTT_AVAILABILITY_SCHEMA, MQTT_ENTITY_DEVICE_INFO_SCHEMA, MQTT_JSON_ATTRS_SCHEMA, - MqttAttributes, MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, + MqttEntity, async_setup_entry_helper, ) @@ -88,17 +86,12 @@ async def _async_setup_entity( async_add_entities([MqttSensor(hass, config, config_entry, discovery_data)]) -class MqttSensor( - MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, Entity -): +class MqttSensor(MqttEntity, Entity): """Representation of a sensor that can be updated using MQTT.""" def __init__(self, hass, config, config_entry, discovery_data): """Initialize the sensor.""" - self.hass = hass - self._unique_id = config.get(CONF_UNIQUE_ID) self._state = None - self._sub_state = None self._expiration_trigger = None expire_after = config.get(CONF_EXPIRE_AFTER) @@ -107,30 +100,12 @@ class MqttSensor( else: self._expired = None - # Load config - self._setup_from_config(config) + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) - device_config = config.get(CONF_DEVICE) - - MqttAttributes.__init__(self, config) - MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config, config_entry) - - async def async_added_to_hass(self): - """Subscribe to MQTT events.""" - await super().async_added_to_hass() - await self._subscribe_topics() - - async def discovery_update(self, discovery_payload): - """Handle updated discovery message.""" - config = PLATFORM_SCHEMA(discovery_payload) - self._setup_from_config(config) - await self.attributes_discovery_update(config) - await self.availability_discovery_update(config) - await self.device_info_discovery_update(config) - await self._subscribe_topics() - self.async_write_ha_state() + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA def _setup_from_config(self, config): """(Re)Setup the entity.""" @@ -185,15 +160,6 @@ class MqttSensor( }, ) - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._sub_state = await subscription.async_unsubscribe_topics( - self.hass, self._sub_state - ) - await MqttAttributes.async_will_remove_from_hass(self) - await MqttAvailability.async_will_remove_from_hass(self) - await MqttDiscoveryUpdate.async_will_remove_from_hass(self) - @callback def _value_is_expired(self, *_): """Triggered when value is expired.""" @@ -201,11 +167,6 @@ class MqttSensor( self._expired = True self.async_write_ha_state() - @property - def should_poll(self): - """No polling needed.""" - return False - @property def name(self): """Return the name of the sensor.""" @@ -226,11 +187,6 @@ class MqttSensor( """Return the state of the entity.""" return self._state - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id - @property def icon(self): """Return the icon.""" diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index a4e65354594..d6d476b680d 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -38,10 +38,7 @@ from .mixins import ( MQTT_AVAILABILITY_SCHEMA, MQTT_ENTITY_DEVICE_INFO_SCHEMA, MQTT_JSON_ATTRS_SCHEMA, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, + MqttEntity, async_setup_entry_helper, ) @@ -97,51 +94,23 @@ async def _async_setup_entity( async_add_entities([MqttSwitch(hass, config, config_entry, discovery_data)]) -class MqttSwitch( - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - SwitchEntity, - RestoreEntity, -): +class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): """Representation of a switch that can be toggled using MQTT.""" def __init__(self, hass, config, config_entry, discovery_data): """Initialize the MQTT switch.""" - self.hass = hass self._state = False - self._sub_state = None self._state_on = None self._state_off = None self._optimistic = None - self._unique_id = config.get(CONF_UNIQUE_ID) - # Load config - self._setup_from_config(config) + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) - device_config = config.get(CONF_DEVICE) - - MqttAttributes.__init__(self, config) - MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config, config_entry) - - async def async_added_to_hass(self): - """Subscribe to MQTT events.""" - await super().async_added_to_hass() - await self._subscribe_topics() - - async def discovery_update(self, discovery_payload): - """Handle updated discovery message.""" - config = PLATFORM_SCHEMA(discovery_payload) - self._setup_from_config(config) - await self.attributes_discovery_update(config) - await self.availability_discovery_update(config) - await self.device_info_discovery_update(config) - await self._subscribe_topics() - self.async_write_ha_state() + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA def _setup_from_config(self, config): """(Re)Setup the entity.""" @@ -198,20 +167,6 @@ class MqttSwitch( if last_state: self._state = last_state.state == STATE_ON - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._sub_state = await subscription.async_unsubscribe_topics( - self.hass, self._sub_state - ) - await MqttAttributes.async_will_remove_from_hass(self) - await MqttAvailability.async_will_remove_from_hass(self) - await MqttDiscoveryUpdate.async_will_remove_from_hass(self) - - @property - def should_poll(self): - """Return the polling state.""" - return False - @property def name(self): """Return the name of the switch.""" @@ -227,11 +182,6 @@ class MqttSwitch( """Return true if we do optimistic updates.""" return self._optimistic - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id - @property def icon(self): """Return the icon.""" diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index dd156720a01..e7be64be6ae 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -35,10 +35,7 @@ from ..mixins import ( MQTT_AVAILABILITY_SCHEMA, MQTT_ENTITY_DEVICE_INFO_SCHEMA, MQTT_JSON_ATTRS_SCHEMA, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, + MqttEntity, ) from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services @@ -176,16 +173,10 @@ async def async_setup_entity_legacy( async_add_entities([MqttVacuum(config, config_entry, discovery_data)]) -class MqttVacuum( - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - VacuumEntity, -): +class MqttVacuum(MqttEntity, VacuumEntity): """Representation of a MQTT-controlled legacy vacuum.""" - def __init__(self, config, config_entry, discovery_info): + def __init__(self, config, config_entry, discovery_data): """Initialize the vacuum.""" self._cleaning = False self._charging = False @@ -195,18 +186,13 @@ class MqttVacuum( self._battery_level = 0 self._fan_speed = "unknown" self._fan_speed_list = [] - self._sub_state = None - self._unique_id = config.get(CONF_UNIQUE_ID) - # Load config - self._setup_from_config(config) + MqttEntity.__init__(self, None, config, config_entry, discovery_data) - device_config = config.get(CONF_DEVICE) - - MqttAttributes.__init__(self, config) - MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_info, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config, config_entry) + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA_LEGACY def _setup_from_config(self, config): self._name = config[CONF_NAME] @@ -257,30 +243,6 @@ class MqttVacuum( ) } - async def discovery_update(self, discovery_payload): - """Handle updated discovery message.""" - config = PLATFORM_SCHEMA_LEGACY(discovery_payload) - self._setup_from_config(config) - await self.attributes_discovery_update(config) - await self.availability_discovery_update(config) - await self.device_info_discovery_update(config) - await self._subscribe_topics() - self.async_write_ha_state() - - async def async_added_to_hass(self): - """Subscribe MQTT events.""" - await super().async_added_to_hass() - await self._subscribe_topics() - - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._sub_state = await subscription.async_unsubscribe_topics( - self.hass, self._sub_state - ) - await MqttAttributes.async_will_remove_from_hass(self) - await MqttAvailability.async_will_remove_from_hass(self) - await MqttDiscoveryUpdate.async_will_remove_from_hass(self) - async def _subscribe_topics(self): """(Re)Subscribe to topics.""" for tpl in self._templates.values(): @@ -384,21 +346,11 @@ class MqttVacuum( """Return the name of the vacuum.""" return self._name - @property - def should_poll(self): - """No polling needed for an MQTT vacuum.""" - return False - @property def is_on(self): """Return true if vacuum is on.""" return self._cleaning - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id - @property def status(self): """Return a status string for the vacuum.""" diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index 536dd89b0fe..c754ba1604a 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -39,10 +39,7 @@ from ..mixins import ( MQTT_AVAILABILITY_SCHEMA, MQTT_ENTITY_DEVICE_INFO_SCHEMA, MQTT_JSON_ATTRS_SCHEMA, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, + MqttEntity, ) from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services @@ -160,32 +157,21 @@ async def async_setup_entity_state( async_add_entities([MqttStateVacuum(config, config_entry, discovery_data)]) -class MqttStateVacuum( - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - StateVacuumEntity, -): +class MqttStateVacuum(MqttEntity, StateVacuumEntity): """Representation of a MQTT-controlled state vacuum.""" - def __init__(self, config, config_entry, discovery_info): + def __init__(self, config, config_entry, discovery_data): """Initialize the vacuum.""" self._state = None self._state_attrs = {} self._fan_speed_list = [] - self._sub_state = None - self._unique_id = config.get(CONF_UNIQUE_ID) - # Load config - self._setup_from_config(config) + MqttEntity.__init__(self, None, config, config_entry, discovery_data) - device_config = config.get(CONF_DEVICE) - - MqttAttributes.__init__(self, config) - MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_info, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config, config_entry) + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA_STATE def _setup_from_config(self, config): self._config = config @@ -211,30 +197,6 @@ class MqttStateVacuum( ) } - async def discovery_update(self, discovery_payload): - """Handle updated discovery message.""" - config = PLATFORM_SCHEMA_STATE(discovery_payload) - self._setup_from_config(config) - await self.attributes_discovery_update(config) - await self.availability_discovery_update(config) - await self.device_info_discovery_update(config) - await self._subscribe_topics() - self.async_write_ha_state() - - async def async_added_to_hass(self): - """Subscribe MQTT events.""" - await super().async_added_to_hass() - await self._subscribe_topics() - - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._sub_state = await subscription.async_unsubscribe_topics( - self.hass, self._sub_state - ) - await MqttAttributes.async_will_remove_from_hass(self) - await MqttAvailability.async_will_remove_from_hass(self) - await MqttDiscoveryUpdate.async_will_remove_from_hass(self) - async def _subscribe_topics(self): """(Re)Subscribe to topics.""" topics = {} @@ -270,11 +232,6 @@ class MqttStateVacuum( """Return state of vacuum.""" return self._state - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id - @property def fan_speed(self): """Return fan speed of the vacuum.""" From f2401061891e85be7f115cf4ae9748af4dec3125 Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Sat, 9 Jan 2021 20:02:50 +0100 Subject: [PATCH 149/507] Upgrade requests to 2.25.1 (#44989) --- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3241cfb9391..a399080594a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -23,7 +23,7 @@ pip>=8.0.3,<20.3 python-slugify==4.0.1 pytz>=2020.5 pyyaml==5.3.1 -requests==2.25.0 +requests==2.25.1 ruamel.yaml==0.15.100 sqlalchemy==1.3.22 voluptuous-serialize==2.4.0 diff --git a/requirements.txt b/requirements.txt index d566cb738ae..84f60d92c40 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ pip>=8.0.3,<20.3 python-slugify==4.0.1 pytz>=2020.5 pyyaml==5.3.1 -requests==2.25.0 +requests==2.25.1 ruamel.yaml==0.15.100 voluptuous==0.12.1 voluptuous-serialize==2.4.0 diff --git a/setup.py b/setup.py index 82d6efacb1b..c0c0182c0b1 100755 --- a/setup.py +++ b/setup.py @@ -48,7 +48,7 @@ REQUIRES = [ "python-slugify==4.0.1", "pytz>=2020.5", "pyyaml==5.3.1", - "requests==2.25.0", + "requests==2.25.1", "ruamel.yaml==0.15.100", "voluptuous==0.12.1", "voluptuous-serialize==2.4.0", From a73a82e38145932238fa91e39e0c868edde524f7 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 9 Jan 2021 17:52:30 -0800 Subject: [PATCH 150/507] Improve nest client error handling using newest library (#44998) --- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 1d64ba73d89..7b282b19b4d 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.2.6" + "google-nest-sdm==0.2.8" ], "codeowners": [ "@awarecan", diff --git a/requirements_all.txt b/requirements_all.txt index d897990dc76..a6d094d1592 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -681,7 +681,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.2.6 +google-nest-sdm==0.2.8 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1c82b6d4f4f..5a150815bbd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -355,7 +355,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.2.6 +google-nest-sdm==0.2.8 # homeassistant.components.gree greeclimate==0.10.3 From 707a8e62f97f77c9777b751a2f1cdc56c97b5154 Mon Sep 17 00:00:00 2001 From: Olivier Cloirec <5033885+clook@users.noreply.github.com> Date: Sun, 10 Jan 2021 18:05:52 +0100 Subject: [PATCH 151/507] Add stop support to openzwave (mqtt) cover (#44622) * feat: add stop to openzwave (mqtt) cover * Fix isort and black linter * Remove supported_features for cover. As suggested by @MartinHjelmare, not needed anymore because base class implementation is sufficient. https://github.com/home-assistant/core/pull/44622#discussion_r549854542 * Make a simpler version depending on idempotency qt-openzwave already implements idempotency, see: https://github.com/OpenZWave/qt-openzwave/blob/77e414217f83fae89a8f41156db3783d562703b1/qt-openzwave/source/qtozwvalueidmodel.cpp#L180 We can use it and trigger button release anywhen. * Clean up Co-authored-by: Martin Hjelmare --- homeassistant/components/ozw/cover.py | 22 +++--- tests/components/ozw/test_cover.py | 96 +++++++++++++++++++-------- 2 files changed, 83 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/ozw/cover.py b/homeassistant/components/ozw/cover.py index 0ac2da91e44..1c708b55ffb 100644 --- a/homeassistant/components/ozw/cover.py +++ b/homeassistant/components/ozw/cover.py @@ -7,7 +7,6 @@ from homeassistant.components.cover import ( DOMAIN as COVER_DOMAIN, SUPPORT_CLOSE, SUPPORT_OPEN, - SUPPORT_SET_POSITION, CoverEntity, ) from homeassistant.core import callback @@ -16,9 +15,10 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import DATA_UNSUBSCRIBE, DOMAIN from .entity import ZWaveDeviceEntity -SUPPORTED_FEATURES_POSITION = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE VALUE_SELECTED_ID = "Selected_id" +PRESS_BUTTON = True +RELEASE_BUTTON = False async def async_setup_entry(hass, config_entry, async_add_entities): @@ -52,11 +52,6 @@ def percent_to_zwave_position(value): class ZWaveCoverEntity(ZWaveDeviceEntity, CoverEntity): """Representation of a Z-Wave Cover device.""" - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORTED_FEATURES_POSITION - @property def is_closed(self): """Return true if cover is closed.""" @@ -73,11 +68,20 @@ class ZWaveCoverEntity(ZWaveDeviceEntity, CoverEntity): async def async_open_cover(self, **kwargs): """Open the cover.""" - self.values.primary.send_value(99) + self.values.open.send_value(PRESS_BUTTON) async def async_close_cover(self, **kwargs): """Close cover.""" - self.values.primary.send_value(0) + self.values.close.send_value(PRESS_BUTTON) + + async def async_stop_cover(self, **kwargs): + """Stop cover.""" + # Need to issue both buttons release since qt-openzwave implements idempotency + # keeping internal state of model to trigger actual updates. We could also keep + # another state in Home Assistant to know which button to release, + # but this implementation is simpler. + self.values.open.send_value(RELEASE_BUTTON) + self.values.close.send_value(RELEASE_BUTTON) class ZwaveGarageDoorBarrier(ZWaveDeviceEntity, CoverEntity): diff --git a/tests/components/ozw/test_cover.py b/tests/components/ozw/test_cover.py index 07f7d76efb0..2b3b1e06862 100644 --- a/tests/components/ozw/test_cover.py +++ b/tests/components/ozw/test_cover.py @@ -16,6 +16,25 @@ async def test_cover(hass, cover_data, sent_messages, cover_msg): assert state.state == "closed" assert state.attributes[ATTR_CURRENT_POSITION] == 0 + # Test setting position + await hass.services.async_call( + "cover", + "set_cover_position", + {"entity_id": "cover.roller_shutter_3_instance_1_level", "position": 50}, + blocking=True, + ) + assert len(sent_messages) == 1 + msg = sent_messages[0] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == {"Value": 50, "ValueIDKey": 625573905} + + # Feedback on state + cover_msg.decode() + cover_msg.payload["Value"] = 50 + cover_msg.encode() + receive_message(cover_msg) + await hass.async_block_till_done() + # Test opening await hass.services.async_call( "cover", @@ -23,22 +42,26 @@ async def test_cover(hass, cover_data, sent_messages, cover_msg): {"entity_id": "cover.roller_shutter_3_instance_1_level"}, blocking=True, ) - assert len(sent_messages) == 1 - msg = sent_messages[0] + assert len(sent_messages) == 2 + msg = sent_messages[1] assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 99, "ValueIDKey": 625573905} + assert msg["payload"] == {"Value": True, "ValueIDKey": 281475602284568} - # Feedback on state - cover_msg.decode() - cover_msg.payload["Value"] = 99 - cover_msg.encode() - receive_message(cover_msg) - await hass.async_block_till_done() + # Test stopping after opening + await hass.services.async_call( + "cover", + "stop_cover", + {"entity_id": "cover.roller_shutter_3_instance_1_level"}, + blocking=True, + ) + assert len(sent_messages) == 4 + msg = sent_messages[2] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == {"Value": False, "ValueIDKey": 281475602284568} - state = hass.states.get("cover.roller_shutter_3_instance_1_level") - assert state is not None - assert state.state == "open" - assert state.attributes[ATTR_CURRENT_POSITION] == 100 + msg = sent_messages[3] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == {"Value": False, "ValueIDKey": 562950578995224} # Test closing await hass.services.async_call( @@ -47,22 +70,43 @@ async def test_cover(hass, cover_data, sent_messages, cover_msg): {"entity_id": "cover.roller_shutter_3_instance_1_level"}, blocking=True, ) - assert len(sent_messages) == 2 - msg = sent_messages[1] + assert len(sent_messages) == 5 + msg = sent_messages[4] assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 0, "ValueIDKey": 625573905} + assert msg["payload"] == {"Value": True, "ValueIDKey": 562950578995224} - # Test setting position + # Test stopping after closing await hass.services.async_call( "cover", - "set_cover_position", - {"entity_id": "cover.roller_shutter_3_instance_1_level", "position": 50}, + "stop_cover", + {"entity_id": "cover.roller_shutter_3_instance_1_level"}, blocking=True, ) - assert len(sent_messages) == 3 - msg = sent_messages[2] + assert len(sent_messages) == 7 + msg = sent_messages[5] assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 50, "ValueIDKey": 625573905} + assert msg["payload"] == {"Value": False, "ValueIDKey": 281475602284568} + + msg = sent_messages[6] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == {"Value": False, "ValueIDKey": 562950578995224} + + # Test stopping after no open/close + await hass.services.async_call( + "cover", + "stop_cover", + {"entity_id": "cover.roller_shutter_3_instance_1_level"}, + blocking=True, + ) + # both stop open/close messages sent + assert len(sent_messages) == 9 + msg = sent_messages[7] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == {"Value": False, "ValueIDKey": 281475602284568} + + msg = sent_messages[8] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == {"Value": False, "ValueIDKey": 562950578995224} # Test converting position to zwave range for position > 0 await hass.services.async_call( @@ -71,8 +115,8 @@ async def test_cover(hass, cover_data, sent_messages, cover_msg): {"entity_id": "cover.roller_shutter_3_instance_1_level", "position": 100}, blocking=True, ) - assert len(sent_messages) == 4 - msg = sent_messages[3] + assert len(sent_messages) == 10 + msg = sent_messages[9] assert msg["topic"] == "OpenZWave/1/command/setvalue/" assert msg["payload"] == {"Value": 99, "ValueIDKey": 625573905} @@ -83,8 +127,8 @@ async def test_cover(hass, cover_data, sent_messages, cover_msg): {"entity_id": "cover.roller_shutter_3_instance_1_level", "position": 0}, blocking=True, ) - assert len(sent_messages) == 5 - msg = sent_messages[4] + assert len(sent_messages) == 11 + msg = sent_messages[10] assert msg["topic"] == "OpenZWave/1/command/setvalue/" assert msg["payload"] == {"Value": 0, "ValueIDKey": 625573905} From b450d4c135f17f866c627118ac2f28e4306e1cb6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 10 Jan 2021 09:12:21 -1000 Subject: [PATCH 152/507] Improve unifi performance with many devices (#45006) With 250 clients, there were about 18000 timers updated every minute. To avoid this, we check which entities should be set to not_home only once every second. --- homeassistant/components/unifi/controller.py | 35 +++++++++ .../components/unifi/device_tracker.py | 72 +++++++++---------- tests/components/unifi/test_device_tracker.py | 40 +++++++---- 3 files changed, 95 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 30b82c65c85..9f264e6cd1c 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -32,6 +32,9 @@ from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_time_interval +import homeassistant.util.dt as dt_util from .const import ( CONF_ALLOW_BANDWIDTH_SENSORS, @@ -64,6 +67,7 @@ from .const import ( from .errors import AuthenticationRequired, CannotConnect RETRY_TIMER = 15 +CHECK_DISCONNECTED_INTERVAL = timedelta(seconds=1) SUPPORTED_PLATFORMS = [TRACKER_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN] CLIENT_CONNECTED = ( @@ -94,6 +98,9 @@ class UniFiController: self._site_name = None self._site_role = None + self._cancel_disconnected_check = None + self._watch_disconnected_entites = [] + self.entities = {} @property @@ -375,8 +382,32 @@ class UniFiController: self.config_entry.add_update_listener(self.async_config_entry_updated) + self._cancel_disconnected_check = async_track_time_interval( + self.hass, self._async_check_for_disconnected, CHECK_DISCONNECTED_INTERVAL + ) + return True + @callback + def add_disconnected_check(self, entity: Entity) -> None: + """Add an entity to watch for disconnection.""" + self._watch_disconnected_entites.append(entity) + + @callback + def remove_disconnected_check(self, entity: Entity) -> None: + """Remove an entity to watch for disconnection.""" + self._watch_disconnected_entites.remove(entity) + + @callback + def _async_check_for_disconnected(self, *_) -> None: + """Check for any devices scheduled to be marked disconnected.""" + now = dt_util.utcnow() + + for entity in self._watch_disconnected_entites: + disconnected_time = entity.disconnected_time + if disconnected_time is not None and now > disconnected_time: + entity.make_disconnected() + @staticmethod async def async_config_entry_updated(hass, config_entry) -> None: """Handle signals of config entry being updated.""" @@ -430,6 +461,10 @@ class UniFiController: unsub_dispatcher() self.listeners = [] + if self._cancel_disconnected_check: + self._cancel_disconnected_check() + self._cancel_disconnected_check = None + return True diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index a3352631885..9f7726e1ba1 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -21,7 +21,6 @@ from homeassistant.components.device_tracker.const import SOURCE_TYPE_ROUTER from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.event import async_track_point_in_utc_time import homeassistant.util.dt as dt_util from .const import ATTR_MANUFACTURER, DOMAIN as UNIFI_DOMAIN @@ -53,6 +52,8 @@ CLIENT_STATIC_ATTRIBUTES = [ "oui", ] +CLIENT_CONNECTED_ALL_ATTRIBUTES = CLIENT_CONNECTED_ATTRIBUTES + CLIENT_STATIC_ATTRIBUTES + DEVICE_UPGRADED = (ACCESS_POINT_UPGRADED, GATEWAY_UPGRADED, SWITCH_UPGRADED) WIRED_CONNECTION = (WIRED_CLIENT_CONNECTED,) @@ -142,7 +143,7 @@ class UniFiClientTracker(UniFiClient, ScannerEntity): super().__init__(client, controller) self.schedule_update = False - self.cancel_scheduled_update = None + self.disconnected_time = None self._is_connected = False if client.last_seen: self._is_connected = ( @@ -154,10 +155,14 @@ class UniFiClientTracker(UniFiClient, ScannerEntity): if self._is_connected: self.schedule_update = True + async def async_added_to_hass(self) -> None: + """Watch object when added.""" + self.controller.add_disconnected_check(self) + await super().async_added_to_hass() + async def async_will_remove_from_hass(self) -> None: """Disconnect object when removed.""" - if self.cancel_scheduled_update: - self.cancel_scheduled_update() + self.controller.remove_disconnected_check(self) await super().async_will_remove_from_hass() @callback @@ -170,12 +175,10 @@ class UniFiClientTracker(UniFiClient, ScannerEntity): ): self._is_connected = True self.schedule_update = False - if self.cancel_scheduled_update: - self.cancel_scheduled_update() - self.cancel_scheduled_update = None + self.disconnected_time = None # Ignore extra scheduled update from wired bug - elif not self.cancel_scheduled_update: + elif not self.disconnected_time: self.schedule_update = True elif not self.client.event and self.client.last_updated == SOURCE_DATA: @@ -185,23 +188,16 @@ class UniFiClientTracker(UniFiClient, ScannerEntity): if self.schedule_update: self.schedule_update = False - - if self.cancel_scheduled_update: - self.cancel_scheduled_update() - - self.cancel_scheduled_update = async_track_point_in_utc_time( - self.hass, - self._make_disconnected, - dt_util.utcnow() + self.controller.option_detection_time, + self.disconnected_time = ( + dt_util.utcnow() + self.controller.option_detection_time ) super().async_update_callback() @callback - def _make_disconnected(self, _): + def make_disconnected(self, *_): """Mark client as disconnected.""" self._is_connected = False - self.cancel_scheduled_update = None self.async_write_ha_state() @property @@ -230,16 +226,16 @@ class UniFiClientTracker(UniFiClient, ScannerEntity): @property def device_state_attributes(self): """Return the client state attributes.""" - attributes = {"is_wired": self.is_wired} + raw = self.client.raw if self.is_connected: - for variable in CLIENT_CONNECTED_ATTRIBUTES: - if variable in self.client.raw: - attributes[variable] = self.client.raw[variable] + attributes = { + k: raw[k] for k in CLIENT_CONNECTED_ALL_ATTRIBUTES if k in raw + } + else: + attributes = {k: raw[k] for k in CLIENT_STATIC_ATTRIBUTES if k in raw} - for variable in CLIENT_STATIC_ATTRIBUTES: - if variable in self.client.raw: - attributes[variable] = self.client.raw[variable] + attributes["is_wired"] = self.is_wired return attributes @@ -270,17 +266,21 @@ class UniFiDeviceTracker(UniFiBase, ScannerEntity): super().__init__(device, controller) self._is_connected = device.state == 1 - self.cancel_scheduled_update = None + self.disconnected_time = None @property def device(self): """Wrap item.""" return self._item + async def async_added_to_hass(self) -> None: + """Watch object when added.""" + self.controller.add_disconnected_check(self) + await super().async_added_to_hass() + async def async_will_remove_from_hass(self) -> None: - """Disconnect device object when removed.""" - if self.cancel_scheduled_update: - self.cancel_scheduled_update() + """Disconnect object when removed.""" + self.controller.remove_disconnected_check(self) await super().async_will_remove_from_hass() @callback @@ -288,16 +288,9 @@ class UniFiDeviceTracker(UniFiBase, ScannerEntity): """Update the devices' state.""" if self.device.last_updated == SOURCE_DATA: - self._is_connected = True - - if self.cancel_scheduled_update: - self.cancel_scheduled_update() - - self.cancel_scheduled_update = async_track_point_in_utc_time( - self.hass, - self._no_heartbeat, - dt_util.utcnow() + timedelta(seconds=self.device.next_interval + 60), + self.disconnected_time = dt_util.utcnow() + timedelta( + seconds=self.device.next_interval + 60 ) elif ( @@ -310,10 +303,9 @@ class UniFiDeviceTracker(UniFiBase, ScannerEntity): super().async_update_callback() @callback - def _no_heartbeat(self, _): + def make_disconnected(self, *_): """No heart beat by device.""" self._is_connected = False - self.cancel_scheduled_update = None self.async_write_ha_state() @property diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 3fef8a16d68..6bfe8f44b5c 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -1,6 +1,7 @@ """The tests for the UniFi device tracker platform.""" from copy import copy from datetime import timedelta +from unittest.mock import patch from aiounifi.controller import ( MESSAGE_CLIENT, @@ -200,8 +201,10 @@ async def test_tracked_wireless_clients(hass): client_1 = hass.states.get("device_tracker.client_1") assert client_1.state == "home" - async_fire_time_changed(hass, dt_util.utcnow() + controller.option_detection_time) - await hass.async_block_till_done() + new_time = dt_util.utcnow() + controller.option_detection_time + with patch("homeassistant.util.dt.utcnow", return_value=new_time): + async_fire_time_changed(hass, new_time) + await hass.async_block_till_done() client_1 = hass.states.get("device_tracker.client_1") assert client_1.state == "not_home" @@ -294,8 +297,10 @@ async def test_tracked_devices(hass): device_2 = hass.states.get("device_tracker.device_2") assert device_2.state == "home" - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=90)) - await hass.async_block_till_done() + new_time = dt_util.utcnow() + timedelta(seconds=90) + with patch("homeassistant.util.dt.utcnow", return_value=new_time): + async_fire_time_changed(hass, new_time) + await hass.async_block_till_done() device_1 = hass.states.get("device_tracker.device_1") assert device_1.state == "not_home" @@ -609,8 +614,10 @@ async def test_option_ssid_filter(hass): client_3 = hass.states.get("device_tracker.client_3") assert client_3.state == "home" - async_fire_time_changed(hass, dt_util.utcnow() + controller.option_detection_time) - await hass.async_block_till_done() + new_time = dt_util.utcnow() + controller.option_detection_time + with patch("homeassistant.util.dt.utcnow", return_value=new_time): + async_fire_time_changed(hass, new_time) + await hass.async_block_till_done() client_1 = hass.states.get("device_tracker.client_1") assert client_1.state == "not_home" @@ -622,8 +629,13 @@ async def test_option_ssid_filter(hass): # Trigger update to get client marked as away event = {"meta": {"message": MESSAGE_CLIENT}, "data": [CLIENT_3]} controller.api.message_handler(event) - async_fire_time_changed(hass, dt_util.utcnow() + controller.option_detection_time) - await hass.async_block_till_done() + + new_time = ( + dt_util.utcnow() + controller.option_detection_time + timedelta(seconds=1) + ) + with patch("homeassistant.util.dt.utcnow", return_value=new_time): + async_fire_time_changed(hass, new_time) + await hass.async_block_till_done() client_3 = hass.states.get("device_tracker.client_3") assert client_3.state == "not_home" @@ -658,8 +670,10 @@ async def test_wireless_client_go_wired_issue(hass): assert client_1.attributes["is_wired"] is False # Pass time - async_fire_time_changed(hass, dt_util.utcnow() + controller.option_detection_time) - await hass.async_block_till_done() + new_time = dt_util.utcnow() + controller.option_detection_time + with patch("homeassistant.util.dt.utcnow", return_value=new_time): + async_fire_time_changed(hass, new_time) + await hass.async_block_till_done() # Marked as home according to the timer client_1 = hass.states.get("device_tracker.client_1") @@ -716,8 +730,10 @@ async def test_option_ignore_wired_bug(hass): assert client_1.attributes["is_wired"] is True # pass time - async_fire_time_changed(hass, dt_util.utcnow() + controller.option_detection_time) - await hass.async_block_till_done() + new_time = dt_util.utcnow() + controller.option_detection_time + with patch("homeassistant.util.dt.utcnow", return_value=new_time): + async_fire_time_changed(hass, new_time) + await hass.async_block_till_done() # Timer marks client as away client_1 = hass.states.get("device_tracker.client_1") From 4b54694c5c809008037565e56d98c8f70a453074 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 10 Jan 2021 09:24:22 -1000 Subject: [PATCH 153/507] Add config flow for somfy_mylink (#44977) * Add config flow for somfy_mylink * fix typo --- .coveragerc | 3 +- CODEOWNERS | 1 + .../components/somfy_mylink/__init__.py | 128 +++++- .../components/somfy_mylink/config_flow.py | 203 +++++++++ .../components/somfy_mylink/const.py | 14 + .../components/somfy_mylink/cover.py | 138 +++++-- .../components/somfy_mylink/manifest.json | 9 +- .../components/somfy_mylink/strings.json | 44 ++ .../somfy_mylink/translations/en.json | 44 ++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/somfy_mylink/__init__.py | 1 + .../somfy_mylink/test_config_flow.py | 385 ++++++++++++++++++ 13 files changed, 920 insertions(+), 54 deletions(-) create mode 100644 homeassistant/components/somfy_mylink/config_flow.py create mode 100644 homeassistant/components/somfy_mylink/const.py create mode 100644 homeassistant/components/somfy_mylink/strings.json create mode 100644 homeassistant/components/somfy_mylink/translations/en.json create mode 100644 tests/components/somfy_mylink/__init__.py create mode 100644 tests/components/somfy_mylink/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index b3e58591bfa..2b08fc50336 100644 --- a/.coveragerc +++ b/.coveragerc @@ -838,7 +838,8 @@ omit = homeassistant/components/soma/cover.py homeassistant/components/soma/sensor.py homeassistant/components/somfy/* - homeassistant/components/somfy_mylink/* + homeassistant/components/somfy_mylink/__init__.py + homeassistant/components/somfy_mylink/cover.py homeassistant/components/sonos/* homeassistant/components/sony_projector/switch.py homeassistant/components/spc/* diff --git a/CODEOWNERS b/CODEOWNERS index 3a1f15c015d..9f6e08216b5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -421,6 +421,7 @@ homeassistant/components/solarlog/* @Ernst79 homeassistant/components/solax/* @squishykid homeassistant/components/soma/* @ratsept homeassistant/components/somfy/* @tetienne +homeassistant/components/somfy_mylink/* @bdraco homeassistant/components/sonarr/* @ctalkington homeassistant/components/songpal/* @rytilahti @shenxn homeassistant/components/sonos/* @cgtobi diff --git a/homeassistant/components/somfy_mylink/__init__.py b/homeassistant/components/somfy_mylink/__init__.py index 8106cde0c18..5427dee5916 100644 --- a/homeassistant/components/somfy_mylink/__init__.py +++ b/homeassistant/components/somfy_mylink/__init__.py @@ -1,18 +1,33 @@ """Component for the Somfy MyLink device supporting the Synergy API.""" +import asyncio +import logging + from somfy_mylink_synergy import SomfyMyLinkSynergy import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.discovery import async_load_platform -CONF_ENTITY_CONFIG = "entity_config" -CONF_SYSTEM_ID = "system_id" -CONF_REVERSE = "reverse" -CONF_DEFAULT_REVERSE = "default_reverse" -DATA_SOMFY_MYLINK = "somfy_mylink_data" -DOMAIN = "somfy_mylink" -SOMFY_MYLINK_COMPONENTS = ["cover"] +from .const import ( + CONF_DEFAULT_REVERSE, + CONF_ENTITY_CONFIG, + CONF_REVERSE, + CONF_SYSTEM_ID, + DATA_SOMFY_MYLINK, + DEFAULT_PORT, + DOMAIN, + MYLINK_ENTITY_IDS, + MYLINK_STATUS, + SOMFY_MYLINK_COMPONENTS, +) + +CONFIG_OPTIONS = (CONF_DEFAULT_REVERSE, CONF_ENTITY_CONFIG) +UNDO_UPDATE_LISTENER = "undo_update_listener" + +_LOGGER = logging.getLogger(__name__) def validate_entity_config(values): @@ -34,7 +49,7 @@ CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_SYSTEM_ID): cv.string, vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=44100): cv.port, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_DEFAULT_REVERSE, default=False): cv.boolean, vol.Optional(CONF_ENTITY_CONFIG, default={}): validate_entity_config, } @@ -47,15 +62,94 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the MyLink platform.""" - host = config[DOMAIN][CONF_HOST] - port = config[DOMAIN][CONF_PORT] - system_id = config[DOMAIN][CONF_SYSTEM_ID] - entity_config = config[DOMAIN][CONF_ENTITY_CONFIG] - entity_config[CONF_DEFAULT_REVERSE] = config[DOMAIN][CONF_DEFAULT_REVERSE] - somfy_mylink = SomfyMyLinkSynergy(system_id, host, port) - hass.data[DATA_SOMFY_MYLINK] = somfy_mylink + conf = config.get(DOMAIN) + hass.data.setdefault(DOMAIN, {}) + + if not conf: + return True + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf + ) + ) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Somfy MyLink from a config entry.""" + _async_import_options_from_data_if_missing(hass, entry) + + config = entry.data + somfy_mylink = SomfyMyLinkSynergy( + config[CONF_SYSTEM_ID], config[CONF_HOST], config[CONF_PORT] + ) + + try: + mylink_status = await somfy_mylink.status_info() + except asyncio.TimeoutError as ex: + raise ConfigEntryNotReady( + "Unable to connect to the Somfy MyLink device, please check your settings" + ) from ex + + if "error" in mylink_status: + _LOGGER.error( + "mylink failed to setup because of an error: %s", + mylink_status.get("error", {}).get("message"), + ) + return False + + undo_listener = entry.add_update_listener(_async_update_listener) + + hass.data[DOMAIN][entry.entry_id] = { + DATA_SOMFY_MYLINK: somfy_mylink, + MYLINK_STATUS: mylink_status, + MYLINK_ENTITY_IDS: [], + UNDO_UPDATE_LISTENER: undo_listener, + } + for component in SOMFY_MYLINK_COMPONENTS: hass.async_create_task( - async_load_platform(hass, component, DOMAIN, entity_config, config) + hass.config_entries.async_forward_entry_setup(entry, component) ) + return True + + +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + +@callback +def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: ConfigEntry): + options = dict(entry.options) + data = dict(entry.data) + modified = False + + for importable_option in CONFIG_OPTIONS: + if importable_option not in options and importable_option in data: + options[importable_option] = data.pop(importable_option) + modified = True + + if modified: + hass.config_entries.async_update_entry(entry, data=data, options=options) + + +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 SOMFY_MYLINK_COMPONENTS + ] + ) + ) + + hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() + + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/somfy_mylink/config_flow.py b/homeassistant/components/somfy_mylink/config_flow.py new file mode 100644 index 00000000000..6f66c9899b4 --- /dev/null +++ b/homeassistant/components/somfy_mylink/config_flow.py @@ -0,0 +1,203 @@ +"""Config flow for Somfy MyLink integration.""" +import asyncio +import logging + +from somfy_mylink_synergy import SomfyMyLinkSynergy +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import ATTR_FRIENDLY_NAME, CONF_ENTITY_ID, CONF_HOST, CONF_PORT +from homeassistant.core import callback + +from .const import ( + CONF_DEFAULT_REVERSE, + CONF_ENTITY_CONFIG, + CONF_REVERSE, + CONF_SYSTEM_ID, + DEFAULT_CONF_DEFAULT_REVERSE, + DEFAULT_PORT, + MYLINK_ENTITY_IDS, +) +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + +ENTITY_CONFIG_VERSION = "entity_config_version" + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_SYSTEM_ID): int, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): int, + } +) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect. + + Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. + """ + somfy_mylink = SomfyMyLinkSynergy( + data[CONF_SYSTEM_ID], data[CONF_HOST], data[CONF_PORT] + ) + + try: + status_info = await somfy_mylink.status_info() + except asyncio.TimeoutError as ex: + raise CannotConnect from ex + + if not status_info or "error" in status_info: + raise InvalidAuth + + return {"title": f"MyLink {data[CONF_HOST]}"} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Somfy MyLink.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_ASSUMED + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + if self._host_already_configured(user_input[CONF_HOST]): + return self.async_abort(reason="already_configured") + + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + async def async_step_import(self, user_input): + """Handle import.""" + if self._host_already_configured(user_input[CONF_HOST]): + return self.async_abort(reason="already_configured") + + return await self.async_step_user(user_input) + + def _host_already_configured(self, host): + """See if we already have an entry matching the host.""" + for entry in self._async_current_entries(): + if entry.data[CONF_HOST] == host: + return True + return False + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow for somfy_mylink.""" + + def __init__(self, config_entry: config_entries.ConfigEntry): + """Initialize options flow.""" + self.config_entry = config_entry + self.options = config_entry.options.copy() + self._entity_id = None + + async def async_step_init(self, user_input=None): + """Handle options flow.""" + + if self.config_entry.state != config_entries.ENTRY_STATE_LOADED: + _LOGGER.error("MyLink must be connected to manage device options") + return self.async_abort(reason="cannot_connect") + + if user_input is not None: + self.options[CONF_DEFAULT_REVERSE] = user_input[CONF_DEFAULT_REVERSE] + + entity_id = user_input.get(CONF_ENTITY_ID) + if entity_id: + return await self.async_step_entity_config(None, entity_id) + + return self.async_create_entry(title="", data=self.options) + + data_schema = vol.Schema( + { + vol.Required( + CONF_DEFAULT_REVERSE, + default=self.options.get( + CONF_DEFAULT_REVERSE, DEFAULT_CONF_DEFAULT_REVERSE + ), + ): bool + } + ) + data = self.hass.data[DOMAIN][self.config_entry.entry_id] + mylink_entity_ids = data[MYLINK_ENTITY_IDS] + + if mylink_entity_ids: + entity_dict = {None: None} + for entity_id in mylink_entity_ids: + name = entity_id + state = self.hass.states.get(entity_id) + if state: + name = state.attributes.get(ATTR_FRIENDLY_NAME, entity_id) + entity_dict[entity_id] = f"{name} ({entity_id})" + data_schema = data_schema.extend( + {vol.Optional(CONF_ENTITY_ID): vol.In(entity_dict)} + ) + + return self.async_show_form(step_id="init", data_schema=data_schema, errors={}) + + async def async_step_entity_config(self, user_input=None, entity_id=None): + """Handle options flow for entity.""" + entities_config = self.options.setdefault(CONF_ENTITY_CONFIG, {}) + + if user_input is not None: + entity_config = entities_config.setdefault(self._entity_id, {}) + if entity_config.get(CONF_REVERSE) != user_input[CONF_REVERSE]: + entity_config[CONF_REVERSE] = user_input[CONF_REVERSE] + # If we do not modify a top level key + # the entity config will never be written + self.options.setdefault(ENTITY_CONFIG_VERSION, 0) + self.options[ENTITY_CONFIG_VERSION] += 1 + return await self.async_step_init() + + self._entity_id = entity_id + default_reverse = self.options.get(CONF_DEFAULT_REVERSE, False) + entity_config = entities_config.get(entity_id, {}) + + return self.async_show_form( + step_id="entity_config", + data_schema=vol.Schema( + { + vol.Optional( + CONF_REVERSE, + default=entity_config.get(CONF_REVERSE, default_reverse), + ): bool + } + ), + description_placeholders={ + CONF_ENTITY_ID: entity_id, + }, + errors={}, + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/somfy_mylink/const.py b/homeassistant/components/somfy_mylink/const.py new file mode 100644 index 00000000000..fd4afb67e00 --- /dev/null +++ b/homeassistant/components/somfy_mylink/const.py @@ -0,0 +1,14 @@ +"""Component for the Somfy MyLink device supporting the Synergy API.""" + +CONF_ENTITY_CONFIG = "entity_config" +CONF_SYSTEM_ID = "system_id" +CONF_REVERSE = "reverse" +CONF_DEFAULT_REVERSE = "default_reverse" +DEFAULT_CONF_DEFAULT_REVERSE = False +DATA_SOMFY_MYLINK = "somfy_mylink_data" +MYLINK_STATUS = "mylink_status" +MYLINK_ENTITY_IDS = "mylink_entity_ids" +DOMAIN = "somfy_mylink" +SOMFY_MYLINK_COMPONENTS = ["cover"] +MANUFACTURER = "Somfy" +DEFAULT_PORT = 44100 diff --git a/homeassistant/components/somfy_mylink/cover.py b/homeassistant/components/somfy_mylink/cover.py index ac3bf0673f1..eee1ccf3b6f 100644 --- a/homeassistant/components/somfy_mylink/cover.py +++ b/homeassistant/components/somfy_mylink/cover.py @@ -2,49 +2,66 @@ import logging from homeassistant.components.cover import ( + DEVICE_CLASS_BLIND, + DEVICE_CLASS_SHUTTER, DEVICE_CLASS_WINDOW, ENTITY_ID_FORMAT, CoverEntity, ) +from homeassistant.const import STATE_CLOSED, STATE_OPEN +from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.util import slugify -from . import CONF_DEFAULT_REVERSE, DATA_SOMFY_MYLINK +from .const import ( + CONF_DEFAULT_REVERSE, + CONF_REVERSE, + DATA_SOMFY_MYLINK, + DOMAIN, + MANUFACTURER, + MYLINK_ENTITY_IDS, + MYLINK_STATUS, +) _LOGGER = logging.getLogger(__name__) +MYLINK_COVER_TYPE_TO_DEVICE_CLASS = {0: DEVICE_CLASS_BLIND, 1: DEVICE_CLASS_SHUTTER} -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + +async def async_setup_entry(hass, config_entry, async_add_entities): """Discover and configure Somfy covers.""" - if discovery_info is None: - return - somfy_mylink = hass.data[DATA_SOMFY_MYLINK] + data = hass.data[DOMAIN][config_entry.entry_id] + mylink_status = data[MYLINK_STATUS] + somfy_mylink = data[DATA_SOMFY_MYLINK] + mylink_entity_ids = data[MYLINK_ENTITY_IDS] cover_list = [] - try: - mylink_status = await somfy_mylink.status_info() - except TimeoutError: - _LOGGER.error( - "Unable to connect to the Somfy MyLink device, " - "please check your settings" - ) - return + for cover in mylink_status["result"]: entity_id = ENTITY_ID_FORMAT.format(slugify(cover["name"])) - entity_config = discovery_info.get(entity_id, {}) - default_reverse = discovery_info[CONF_DEFAULT_REVERSE] + mylink_entity_ids.append(entity_id) + + entity_config = config_entry.options.get(entity_id, {}) + default_reverse = config_entry.options.get(CONF_DEFAULT_REVERSE) + cover_config = {} cover_config["target_id"] = cover["targetID"] cover_config["name"] = cover["name"] - cover_config["reverse"] = entity_config.get("reverse", default_reverse) + cover_config["device_class"] = MYLINK_COVER_TYPE_TO_DEVICE_CLASS.get( + cover.get("type"), DEVICE_CLASS_WINDOW + ) + cover_config["reverse"] = entity_config.get(CONF_REVERSE, default_reverse) + cover_list.append(SomfyShade(somfy_mylink, **cover_config)) + _LOGGER.info( "Adding Somfy Cover: %s with targetID %s", cover_config["name"], cover_config["target_id"], ) + async_add_entities(cover_list) -class SomfyShade(CoverEntity): +class SomfyShade(RestoreEntity, CoverEntity): """Object for controlling a Somfy cover.""" def __init__( @@ -60,8 +77,16 @@ class SomfyShade(CoverEntity): self._target_id = target_id self._name = name self._reverse = reverse + self._closed = None + self._is_opening = None + self._is_closing = None self._device_class = device_class + @property + def should_poll(self): + """No polling since assumed state.""" + return False + @property def unique_id(self): """Return the unique ID of this cover.""" @@ -72,11 +97,6 @@ class SomfyShade(CoverEntity): """Return the name of the cover.""" return self._name - @property - def is_closed(self): - """Return if the cover is closed.""" - return None - @property def assumed_state(self): """Let HA know the integration is assumed state.""" @@ -87,20 +107,72 @@ class SomfyShade(CoverEntity): """Return the class of this device, from component DEVICE_CLASSES.""" return self._device_class - async def async_open_cover(self, **kwargs): - """Wrap Homeassistant calls to open the cover.""" - if not self._reverse: - await self.somfy_mylink.move_up(self._target_id) - else: - await self.somfy_mylink.move_down(self._target_id) + @property + def is_opening(self): + """Return if the cover is opening.""" + return self._is_opening + + @property + def is_closing(self): + """Return if the cover is closing.""" + return self._is_closing + + @property + def is_closed(self) -> bool: + """Return if the cover is closed.""" + return self._closed + + @property + def device_info(self): + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN, self._target_id)}, + "name": self._name, + "manufacturer": MANUFACTURER, + } async def async_close_cover(self, **kwargs): - """Wrap Homeassistant calls to close the cover.""" - if not self._reverse: - await self.somfy_mylink.move_down(self._target_id) - else: - await self.somfy_mylink.move_up(self._target_id) + """Close the cover.""" + self._is_closing = True + self.async_write_ha_state() + try: + # Blocks until the close command is sent + if not self._reverse: + await self.somfy_mylink.move_down(self._target_id) + else: + await self.somfy_mylink.move_up(self._target_id) + self._closed = True + finally: + self._is_closing = None + self.async_write_ha_state() + + async def async_open_cover(self, **kwargs): + """Open the cover.""" + self._is_opening = True + self.async_write_ha_state() + try: + # Blocks until the open command is sent + if not self._reverse: + await self.somfy_mylink.move_up(self._target_id) + else: + await self.somfy_mylink.move_down(self._target_id) + self._closed = False + finally: + self._is_opening = None + self.async_write_ha_state() async def async_stop_cover(self, **kwargs): """Stop the cover.""" await self.somfy_mylink.move_stop(self._target_id) + + async def async_added_to_hass(self): + """Complete the initialization.""" + await super().async_added_to_hass() + # Restore the last state + last_state = await self.async_get_last_state() + + if last_state is not None and last_state.state in ( + STATE_OPEN, + STATE_CLOSED, + ): + self._closed = last_state.state == STATE_CLOSED diff --git a/homeassistant/components/somfy_mylink/manifest.json b/homeassistant/components/somfy_mylink/manifest.json index c259f827d51..e9b4601dee3 100644 --- a/homeassistant/components/somfy_mylink/manifest.json +++ b/homeassistant/components/somfy_mylink/manifest.json @@ -2,6 +2,9 @@ "domain": "somfy_mylink", "name": "Somfy MyLink", "documentation": "https://www.home-assistant.io/integrations/somfy_mylink", - "requirements": ["somfy-mylink-synergy==1.0.6"], - "codeowners": [] -} + "requirements": [ + "somfy-mylink-synergy==1.0.6" + ], + "codeowners": ["@bdraco"], + "config_flow": true +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/strings.json b/homeassistant/components/somfy_mylink/strings.json new file mode 100644 index 00000000000..bd63fa93d86 --- /dev/null +++ b/homeassistant/components/somfy_mylink/strings.json @@ -0,0 +1,44 @@ +{ + "title": "Somfy MyLink", + "config": { + "step": { + "user": { + "description": "The System ID can be obtained in the MyLink app under Integration by selecting any non-Cloud service.", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "system_id": "System ID" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "options": { + "abort": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "step": { + "init": { + "title": "Configure MyLink Entities", + "data": { + "default_reverse": "Default reversal status for unconfigured covers", + "entity_id": "Configure a specific entity." + } + }, + "entity_config": { + "title": "Configure Entity", + "description": "Configure options for `{entity_id}`", + "data": { + "reverse": "Cover is reversed" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/en.json b/homeassistant/components/somfy_mylink/translations/en.json new file mode 100644 index 00000000000..bd63fa93d86 --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/en.json @@ -0,0 +1,44 @@ +{ + "title": "Somfy MyLink", + "config": { + "step": { + "user": { + "description": "The System ID can be obtained in the MyLink app under Integration by selecting any non-Cloud service.", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "system_id": "System ID" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "options": { + "abort": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "step": { + "init": { + "title": "Configure MyLink Entities", + "data": { + "default_reverse": "Default reversal status for unconfigured covers", + "entity_id": "Configure a specific entity." + } + }, + "entity_config": { + "title": "Configure Entity", + "description": "Configure options for `{entity_id}`", + "data": { + "reverse": "Cover is reversed" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 43e0647a258..b696e29964a 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -190,6 +190,7 @@ FLOWS = [ "solarlog", "soma", "somfy", + "somfy_mylink", "sonarr", "songpal", "sonos", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5a150815bbd..6dc4f724a7b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1014,6 +1014,9 @@ solaredge==0.0.2 # homeassistant.components.honeywell somecomfort==0.5.2 +# homeassistant.components.somfy_mylink +somfy-mylink-synergy==1.0.6 + # homeassistant.components.sonarr sonarr==0.3.0 diff --git a/tests/components/somfy_mylink/__init__.py b/tests/components/somfy_mylink/__init__.py new file mode 100644 index 00000000000..b1141243997 --- /dev/null +++ b/tests/components/somfy_mylink/__init__.py @@ -0,0 +1 @@ +"""Tests for the Somfy MyLink integration.""" diff --git a/tests/components/somfy_mylink/test_config_flow.py b/tests/components/somfy_mylink/test_config_flow.py new file mode 100644 index 00000000000..1a81e28a7c6 --- /dev/null +++ b/tests/components/somfy_mylink/test_config_flow.py @@ -0,0 +1,385 @@ +"""Test the Somfy MyLink config flow.""" +import asyncio +from unittest.mock import patch + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.somfy_mylink.const import ( + CONF_DEFAULT_REVERSE, + CONF_ENTITY_CONFIG, + CONF_REVERSE, + CONF_SYSTEM_ID, + DOMAIN, +) +from homeassistant.const import CONF_HOST, CONF_PORT + +from tests.common import MockConfigEntry + + +async def test_form_user(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", + return_value={"any": "data"}, + ), patch( + "homeassistant.components.somfy_mylink.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.somfy_mylink.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_PORT: 1234, + CONF_SYSTEM_ID: 456, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "MyLink 1.1.1.1" + assert result2["data"] == { + CONF_HOST: "1.1.1.1", + CONF_PORT: 1234, + CONF_SYSTEM_ID: 456, + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_user_already_configured(hass): + """Test we abort if already configured.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "1.1.1.1", CONF_PORT: 12, CONF_SYSTEM_ID: 46}, + ) + config_entry.add_to_hass(hass) + 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.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", + return_value={"any": "data"}, + ), patch( + "homeassistant.components.somfy_mylink.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.somfy_mylink.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_PORT: 1234, + CONF_SYSTEM_ID: 456, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert len(mock_setup.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_form_import(hass): + """Test we get the form with import source.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch( + "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", + return_value={"any": "data"}, + ), patch( + "homeassistant.components.somfy_mylink.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.somfy_mylink.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_HOST: "1.1.1.1", + CONF_PORT: 1234, + CONF_SYSTEM_ID: 456, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == "MyLink 1.1.1.1" + assert result["data"] == { + CONF_HOST: "1.1.1.1", + CONF_PORT: 1234, + CONF_SYSTEM_ID: 456, + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_import_with_entity_config(hass): + """Test we can import entity config.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch( + "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", + return_value={"any": "data"}, + ), patch( + "homeassistant.components.somfy_mylink.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.somfy_mylink.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_HOST: "1.1.1.1", + CONF_PORT: 1234, + CONF_SYSTEM_ID: 456, + CONF_DEFAULT_REVERSE: True, + CONF_ENTITY_CONFIG: {"cover.xyz": {CONF_REVERSE: False}}, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == "MyLink 1.1.1.1" + assert result["data"] == { + CONF_HOST: "1.1.1.1", + CONF_PORT: 1234, + CONF_SYSTEM_ID: 456, + CONF_DEFAULT_REVERSE: True, + CONF_ENTITY_CONFIG: {"cover.xyz": {CONF_REVERSE: False}}, + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_import_already_exists(hass): + """Test we get the form with import source.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "1.1.1.1", CONF_PORT: 12, CONF_SYSTEM_ID: 46}, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", + return_value={"any": "data"}, + ), patch( + "homeassistant.components.somfy_mylink.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.somfy_mylink.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_HOST: "1.1.1.1", + CONF_PORT: 1234, + CONF_SYSTEM_ID: 456, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert len(mock_setup.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_form_invalid_auth(hass): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", + return_value={ + "jsonrpc": "2.0", + "error": {"code": -32652, "message": "Invalid auth"}, + "id": 818, + }, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_PORT: 1234, + CONF_SYSTEM_ID: 456, + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", + side_effect=asyncio.TimeoutError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_PORT: 1234, + CONF_SYSTEM_ID: 456, + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_unknown_error(hass): + """Test we handle broad exception.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", + side_effect=ValueError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_PORT: 1234, + CONF_SYSTEM_ID: 456, + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} + + +async def test_options_not_loaded(hass): + """Test options will not display until loaded.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "1.1.1.1", CONF_PORT: 12, CONF_SYSTEM_ID: 46}, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.somfy_mylink.SomfyMyLinkSynergy.status_info", + return_value={"result": []}, + ): + result = await hass.config_entries.options.async_init(config_entry.entry_id) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +async def test_options_no_entities(hass): + """Test we can configure default reverse.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "1.1.1.1", CONF_PORT: 12, CONF_SYSTEM_ID: 46}, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.somfy_mylink.SomfyMyLinkSynergy.status_info", + return_value={"result": []}, + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.options.async_init(config_entry.entry_id) + await hass.async_block_till_done() + 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={"default_reverse": True}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == { + "default_reverse": True, + } + + await hass.async_block_till_done() + + +async def test_options_with_entities(hass): + """Test we can configure reverse for an entity.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "1.1.1.1", CONF_PORT: 12, CONF_SYSTEM_ID: 46}, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.somfy_mylink.SomfyMyLinkSynergy.status_info", + return_value={ + "result": [ + { + "targetID": "a", + "name": "Master Window", + "type": 0, + } + ] + }, + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.options.async_init(config_entry.entry_id) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"default_reverse": True, "entity_id": "cover.master_window"}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + result3 = await hass.config_entries.options.async_configure( + result2["flow_id"], + user_input={"reverse": False}, + ) + + assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + + result4 = await hass.config_entries.options.async_configure( + result3["flow_id"], + user_input={"default_reverse": True, "entity_id": None}, + ) + assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + assert config_entry.options == { + "default_reverse": True, + "entity_config": {"cover.master_window": {"reverse": False}}, + "entity_config_version": 1, + } + + await hass.async_block_till_done() From f5b389faa8ecf51f248005ed3dcbaa7ae4a15a2f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 10 Jan 2021 09:38:41 -1000 Subject: [PATCH 154/507] Warn users when their HomeKit configuration may be unstable (#44999) --- homeassistant/components/homekit/__init__.py | 20 ++++- homeassistant/components/homekit/strings.json | 4 +- .../components/homekit/translations/en.json | 83 +++++++++---------- tests/components/homekit/test_homekit.py | 51 +++++++++++- 4 files changed, 109 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 1155d3ef18e..2b11aee0bbd 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -5,7 +5,7 @@ import logging import os from aiohttp import web -from pyhap.const import STANDALONE_AID +from pyhap.const import CATEGORY_CAMERA, CATEGORY_TELEVISION, STANDALONE_AID import voluptuous as vol from homeassistant.components import zeroconf @@ -530,6 +530,24 @@ class HomeKit: try: acc = get_accessory(self.hass, self.driver, state, aid, conf) if acc is not None: + if acc.category == CATEGORY_CAMERA: + _LOGGER.warning( + "The bridge %s has camera %s. For best performance, " + "and to prevent unexpected unavailability, create and " + "pair a separate HomeKit instance in accessory mode for " + "each camera.", + self._name, + acc.entity_id, + ) + elif acc.category == CATEGORY_TELEVISION: + _LOGGER.warning( + "The bridge %s has tv %s. For best performance, " + "and to prevent unexpected unavailability, create and " + "pair a separate HomeKit instance in accessory mode for " + "each tv media player.", + self._name, + acc.entity_id, + ) self.bridge.add_accessory(acc) except Exception: # pylint: disable=broad-except _LOGGER.exception( diff --git a/homeassistant/components/homekit/strings.json b/homeassistant/components/homekit/strings.json index 3f12eca0f5f..9e3413e1d20 100644 --- a/homeassistant/components/homekit/strings.json +++ b/homeassistant/components/homekit/strings.json @@ -18,7 +18,7 @@ "mode": "[%key:common::config_flow::data::mode%]", "entities": "Entities" }, - "description": "Choose the entities to be exposed. In accessory mode, only a single entity is exposed. In bridge include mode, all entities in the domain will be exposed unless specific entities are selected. In bridge exclude mode, all entities in the domain will be exposed except for the excluded entities.", + "description": "Choose the entities to be exposed. In accessory mode, only a single entity is exposed. In bridge include mode, all entities in the domain will be exposed unless specific entities are selected. In bridge exclude mode, all entities in the domain will be exposed except for the excluded entities. For best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each tv media player and camera.", "title": "Select entities to be exposed" }, "cameras": { @@ -44,7 +44,7 @@ "data": { "include_domains": "Domains to include" }, - "description": "The HomeKit integration will allow you to access your Home Assistant entities in HomeKit. In bridge mode, HomeKit Bridges are limited to 150 accessories per instance including the bridge itself. If you wish to bridge more than the maximum number of accessories, it is recommended that you use multiple HomeKit bridges for different domains. Detailed entity configuration is only available via YAML for the primary bridge.", + "description": "The HomeKit integration will allow you to access your Home Assistant entities in HomeKit. In bridge mode, HomeKit Bridges are limited to 150 accessories per instance including the bridge itself. If you wish to bridge more than the maximum number of accessories, it is recommended that you use multiple HomeKit bridges for different domains. Detailed entity configuration is only available via YAML. For best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each tv media player and camera.", "title": "Activate HomeKit" }, "pairing": { diff --git a/homeassistant/components/homekit/translations/en.json b/homeassistant/components/homekit/translations/en.json index 39aa3522025..9e3413e1d20 100644 --- a/homeassistant/components/homekit/translations/en.json +++ b/homeassistant/components/homekit/translations/en.json @@ -1,32 +1,25 @@ { - "config": { - "abort": { - "port_name_in_use": "An accessory or bridge with the same name or port is already configured." - }, - "step": { - "pairing": { - "description": "As soon as the {name} is ready, pairing will be available in \u201cNotifications\u201d as \u201cHomeKit Bridge Setup\u201d.", - "title": "Pair HomeKit" - }, - "user": { - "data": { - "auto_start": "Autostart (disable if using Z-Wave or other delayed start system)", - "include_domains": "Domains to include" - }, - "description": "The HomeKit integration will allow you to access your Home Assistant entities in HomeKit. In bridge mode, HomeKit Bridges are limited to 150 accessories per instance including the bridge itself. If you wish to bridge more than the maximum number of accessories, it is recommended that you use multiple HomeKit bridges for different domains. Detailed entity configuration is only available via YAML for the primary bridge.", - "title": "Activate HomeKit" - } - } - }, "options": { "step": { - "advanced": { + "yaml": { + "title": "Adjust HomeKit Options", + "description": "This entry is controlled via YAML" + }, + "init": { "data": { - "auto_start": "Autostart (disable if using Z-Wave or other delayed start system)", - "safe_mode": "Safe Mode (enable only if pairing fails)" + "mode": "[%key:common::config_flow::data::mode%]", + "include_domains": "[%key:component::homekit::config::step::user::data::include_domains%]" }, - "description": "These settings only need to be adjusted if HomeKit is not functional.", - "title": "Advanced Configuration" + "description": "HomeKit can be configured expose a bridge or a single accessory. In accessory mode, only a single entity can be used. Accessory mode is required for media players with the TV device class to function properly. Entities in the \u201cDomains to include\u201d will be exposed to HomeKit. You will be able to select which entities to include or exclude from this list on the next screen.", + "title": "Select domains to expose." + }, + "include_exclude": { + "data": { + "mode": "[%key:common::config_flow::data::mode%]", + "entities": "Entities" + }, + "description": "Choose the entities to be exposed. In accessory mode, only a single entity is exposed. In bridge include mode, all entities in the domain will be exposed unless specific entities are selected. In bridge exclude mode, all entities in the domain will be exposed except for the excluded entities. For best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each tv media player and camera.", + "title": "Select entities to be exposed" }, "cameras": { "data": { @@ -35,26 +28,32 @@ "description": "Check all cameras that support native H.264 streams. If the camera does not output a H.264 stream, the system will transcode the video to H.264 for HomeKit. Transcoding requires a performant CPU and is unlikely to work on single board computers.", "title": "Select camera video codec." }, - "include_exclude": { + "advanced": { "data": { - "entities": "Entities", - "mode": "Mode" + "auto_start": "Autostart (disable if you are calling the homekit.start service manually)", + "safe_mode": "Safe Mode (enable only if pairing fails)" }, - "description": "Choose the entities to be exposed. In accessory mode, only a single entity is exposed. In bridge include mode, all entities in the domain will be exposed unless specific entities are selected. In bridge exclude mode, all entities in the domain will be exposed except for the excluded entities.", - "title": "Select entities to be exposed" - }, - "init": { - "data": { - "include_domains": "Domains to include", - "mode": "Mode" - }, - "description": "HomeKit can be configured expose a bridge or a single accessory. In accessory mode, only a single entity can be used. Accessory mode is required for media players with the TV device class to function properly. Entities in the \u201cDomains to include\u201d will be exposed to HomeKit. You will be able to select which entities to include or exclude from this list on the next screen.", - "title": "Select domains to expose." - }, - "yaml": { - "description": "This entry is controlled via YAML", - "title": "Adjust HomeKit Options" + "description": "These settings only need to be adjusted if HomeKit is not functional.", + "title": "Advanced Configuration" } } + }, + "config": { + "step": { + "user": { + "data": { + "include_domains": "Domains to include" + }, + "description": "The HomeKit integration will allow you to access your Home Assistant entities in HomeKit. In bridge mode, HomeKit Bridges are limited to 150 accessories per instance including the bridge itself. If you wish to bridge more than the maximum number of accessories, it is recommended that you use multiple HomeKit bridges for different domains. Detailed entity configuration is only available via YAML. For best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each tv media player and camera.", + "title": "Activate HomeKit" + }, + "pairing": { + "title": "Pair HomeKit", + "description": "As soon as the {name} is ready, pairing will be available in \u201cNotifications\u201d as \u201cHomeKit Bridge Setup\u201d." + } + }, + "abort": { + "port_name_in_use": "An accessory or bridge with the same name or port is already configured." + } } -} \ No newline at end of file +} diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 32d8a69417b..a3f6c1f57d2 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -4,6 +4,7 @@ from typing import Dict from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch from pyhap.accessory import Accessory +from pyhap.const import CATEGORY_CAMERA, CATEGORY_TELEVISION import pytest from homeassistant import config as hass_config @@ -340,7 +341,7 @@ async def test_homekit_setup_safe_mode(hass, hk_driver): assert homekit.driver.safe_mode is True -async def test_homekit_add_accessory(hass): +async def test_homekit_add_accessory(hass, mock_zeroconf): """Add accessory if config exists and get_acc returns an accessory.""" entry = await async_init_integration(hass) @@ -360,10 +361,12 @@ async def test_homekit_add_accessory(hass): homekit.bridge = mock_bridge = Mock() homekit.bridge.accessories = range(10) + mock_acc = Mock(category="any") + await async_init_integration(hass) with patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc: - mock_get_acc.side_effect = [None, "acc", None] + mock_get_acc.side_effect = [None, mock_acc, None] homekit.add_bridge_accessory(State("light.demo", "on")) mock_get_acc.assert_called_with(hass, "driver", ANY, 1403373688, {}) assert not mock_bridge.add_accessory.called @@ -374,10 +377,50 @@ async def test_homekit_add_accessory(hass): homekit.add_bridge_accessory(State("demo.test_2", "on")) mock_get_acc.assert_called_with(hass, "driver", ANY, 1467253281, {}) - mock_bridge.add_accessory.assert_called_with("acc") + mock_bridge.add_accessory.assert_called_with(mock_acc) -async def test_homekit_remove_accessory(hass): +@pytest.mark.parametrize("acc_category", [CATEGORY_TELEVISION, CATEGORY_CAMERA]) +async def test_homekit_warn_add_accessory_bridge( + hass, acc_category, mock_zeroconf, caplog +): + """Test we warn when adding cameras or tvs to a bridge.""" + entry = await async_init_integration(hass) + + homekit = HomeKit( + hass, + None, + None, + None, + lambda entity_id: True, + {}, + DEFAULT_SAFE_MODE, + HOMEKIT_MODE_BRIDGE, + advertise_ip=None, + entry_id=entry.entry_id, + ) + homekit.driver = "driver" + homekit.bridge = mock_bridge = Mock() + homekit.bridge.accessories = range(10) + + mock_camera_acc = Mock(category=acc_category) + + await async_init_integration(hass) + + with patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc: + mock_get_acc.side_effect = [None, mock_camera_acc, None] + homekit.add_bridge_accessory(State("light.demo", "on")) + mock_get_acc.assert_called_with(hass, "driver", ANY, 1403373688, {}) + assert not mock_bridge.add_accessory.called + + homekit.add_bridge_accessory(State("camera.test", "on")) + mock_get_acc.assert_called_with(hass, "driver", ANY, 1508819236, {}) + assert mock_bridge.add_accessory.called + + assert "accessory mode" in caplog.text + + +async def test_homekit_remove_accessory(hass, mock_zeroconf): """Remove accessory from bridge.""" entry = await async_init_integration(hass) From 4de9f5194f79e011297f6df8cd3735787f19e799 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Sun, 10 Jan 2021 21:37:55 +0100 Subject: [PATCH 155/507] Include current version in updater log output (#45022) --- homeassistant/components/updater/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py index 3d7b8b626c9..13497da8290 100644 --- a/homeassistant/components/updater/__init__.py +++ b/homeassistant/components/updater/__init__.py @@ -93,7 +93,11 @@ async def async_setup(hass, config): "You are on the latest version (%s) of Home Assistant", newest ) elif StrictVersion(newest) < StrictVersion(current_version): - _LOGGER.debug("Local version is newer than the latest version (%s)", newest) + _LOGGER.debug( + "Local version (%s) is newer than the latest available version (%s)", + current_version, + newest, + ) _LOGGER.debug("Update available: %s", update_available) From 1402e7ae567cd83e5b78ea09eba1f2cff83561ec Mon Sep 17 00:00:00 2001 From: Ryan Fleming Date: Sun, 10 Jan 2021 17:23:32 -0500 Subject: [PATCH 156/507] Use the camera UUID as the entity unique id (#44937) --- homeassistant/components/uvc/camera.py | 5 +++++ tests/components/uvc/test_camera.py | 2 ++ 2 files changed, 7 insertions(+) diff --git a/homeassistant/components/uvc/camera.py b/homeassistant/components/uvc/camera.py index da748c20c1c..1988350eed3 100644 --- a/homeassistant/components/uvc/camera.py +++ b/homeassistant/components/uvc/camera.py @@ -119,6 +119,11 @@ class UnifiVideoCamera(Camera): """Camera Motion Detection Status.""" return self._caminfo["recordingSettings"]["motionRecordEnabled"] + @property + def unique_id(self) -> str: + """Return a unique identifier for this client.""" + return self._uuid + @property def brand(self): """Return the brand of this camera.""" diff --git a/tests/components/uvc/test_camera.py b/tests/components/uvc/test_camera.py index 35e6c82ded6..9e904f82357 100644 --- a/tests/components/uvc/test_camera.py +++ b/tests/components/uvc/test_camera.py @@ -197,6 +197,7 @@ class TestUVC(unittest.TestCase): self.uvc = uvc.UnifiVideoCamera(self.nvr, self.uuid, self.name, self.password) self.nvr.get_camera.return_value = { "model": "UVC Fake", + "uuid": "06e3ff29-8048-31c2-8574-0852d1bd0e03", "recordingSettings": {"fullTimeRecordEnabled": True}, "host": "host-a", "internalHost": "host-b", @@ -238,6 +239,7 @@ class TestUVC(unittest.TestCase): assert "Ubiquiti" == self.uvc.brand assert "UVC Fake" == self.uvc.model assert SUPPORT_STREAM == self.uvc.supported_features + assert "uuid" == self.uvc.unique_id def test_stream(self): """Test the RTSP stream URI.""" From d68fdbc283ed257684477975cea94d7a14031bbb Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Sun, 10 Jan 2021 18:08:25 -0500 Subject: [PATCH 157/507] Add zwave_js integration (#45020) * Run zwave_js scaffold (#44891) * Add zwave_js basic connection to zwave server (#44904) * add the basic connection to zwave server * fix name * Fix requirements * Fix things * Version bump dep to 0.1.2 * fix pylint Co-authored-by: Paulus Schoutsen * Bump zwave-js-server-python to 0.2.0 * Use zwave js server version check instead of fetching full state (#44943) * Use version check instead of fetching full state * Fix tests * Use 0.3.0 * Also catch aiohttp client errors * Update docstring * Lint * Unignore zwave_js * Add zwave_js entity discovery basics and sensor platform (#44927) Co-authored-by: Martin Hjelmare Co-authored-by: Paulus Schoutsen * Complete zwave_js typing (#44960) * Type discovery * Type init * Type entity * Type config flow * Type sensor * Require typing of zwave_js * Complete zwave_js config flow test coverage (#44955) * Correct zwave_js sensor device class (#44968) * Fix zwave_js KeyError on entry setup timeout (#44966) * Bump zwave-js-server-python to 0.5.0 (#44975) * Remove stale callback signal from zwave_js (#44994) * Add light platform to zwave_js integration (#44974) * add light platform * styling fix * fix type hint * Fix typing * Update homeassistant/components/zwave_js/const.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zwave_js/entity.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zwave_js/entity.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zwave_js/entity.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zwave_js/entity.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zwave_js/entity.py Co-authored-by: Martin Hjelmare * color temp should be integer * guard Nonetype error * Update homeassistant/components/zwave_js/light.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zwave_js/light.py Co-authored-by: Martin Hjelmare * some fixes after merging * add additional guards for None values * adjustments for rgb lights * Fix typing * Fix black * Bump zwave-js-server-python to 0.6.0 * guard value updated log * remove value_id lookup as its no longer needed * fiz sending white value * Update homeassistant/components/zwave_js/light.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare * Add zwave_js test foundation (#44983) * Exclude text files from codespell * Add basic dump fixture * Add test foundation * Fix test after rebase * Exclude jsonl files from codespell * Rename fixture file type to jsonl * Update fixture path * Fix stale docstring * Add controller state json fixture * Add multisensor 6 state json fixture * Update fixtures * Remove basic dump fixture * Fix fixtures after library bump * Update codeowner * Minor cleanup Z-Wave JS (#45021) * Update zwave_js device_info (#45023) Co-authored-by: Martin Hjelmare Co-authored-by: Marcel van der Veldt Co-authored-by: Paulus Schoutsen --- .coveragerc | 5 + .pre-commit-config.yaml | 1 + CODEOWNERS | 1 + homeassistant/components/zwave_js/__init__.py | 179 ++ .../components/zwave_js/config_flow.py | 78 + homeassistant/components/zwave_js/const.py | 9 + .../components/zwave_js/discovery.py | 160 ++ homeassistant/components/zwave_js/entity.py | 151 ++ homeassistant/components/zwave_js/light.py | 322 +++ .../components/zwave_js/manifest.json | 8 + homeassistant/components/zwave_js/sensor.py | 149 ++ .../components/zwave_js/strings.json | 20 + .../components/zwave_js/translations/en.json | 20 + homeassistant/generated/config_flows.py | 3 +- requirements_all.txt | 3 + requirements_test_all.txt | 3 + setup.cfg | 2 +- tests/components/zwave_js/__init__.py | 1 + tests/components/zwave_js/conftest.py | 58 + tests/components/zwave_js/test_config_flow.py | 99 + tests/components/zwave_js/test_sensor.py | 14 + tests/fixtures/zwave_js/controller_state.json | 98 + .../zwave_js/multisensor_6_state.json | 1830 +++++++++++++++++ 23 files changed, 3212 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/zwave_js/__init__.py create mode 100644 homeassistant/components/zwave_js/config_flow.py create mode 100644 homeassistant/components/zwave_js/const.py create mode 100644 homeassistant/components/zwave_js/discovery.py create mode 100644 homeassistant/components/zwave_js/entity.py create mode 100644 homeassistant/components/zwave_js/light.py create mode 100644 homeassistant/components/zwave_js/manifest.json create mode 100644 homeassistant/components/zwave_js/sensor.py create mode 100644 homeassistant/components/zwave_js/strings.json create mode 100644 homeassistant/components/zwave_js/translations/en.json create mode 100644 tests/components/zwave_js/__init__.py create mode 100644 tests/components/zwave_js/conftest.py create mode 100644 tests/components/zwave_js/test_config_flow.py create mode 100644 tests/components/zwave_js/test_sensor.py create mode 100644 tests/fixtures/zwave_js/controller_state.json create mode 100644 tests/fixtures/zwave_js/multisensor_6_state.json diff --git a/.coveragerc b/.coveragerc index 2b08fc50336..9a8b062468f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1092,6 +1092,11 @@ omit = homeassistant/components/zoneminder/* homeassistant/components/supla/* homeassistant/components/zwave/util.py + homeassistant/components/zwave_js/__init__.py + homeassistant/components/zwave_js/discovery.py + homeassistant/components/zwave_js/entity.py + homeassistant/components/zwave_js/light.py + homeassistant/components/zwave_js/sensor.py [report] # Regexes for lines to exclude from consideration diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 087dc914035..00f2373f2db 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,6 +21,7 @@ repos: - --skip="./.*,*.csv,*.json" - --quiet-level=2 exclude_types: [csv, json] + exclude: ^tests/fixtures/ - repo: https://gitlab.com/pycqa/flake8 rev: 3.8.4 hooks: diff --git a/CODEOWNERS b/CODEOWNERS index 9f6e08216b5..d81247ce629 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -537,6 +537,7 @@ homeassistant/components/zodiac/* @JulienTant homeassistant/components/zone/* @home-assistant/core homeassistant/components/zoneminder/* @rohankapoorcom homeassistant/components/zwave/* @home-assistant/z-wave +homeassistant/components/zwave_js/* @home-assistant/z-wave # Individual files homeassistant/components/demo/weather @fabaff diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py new file mode 100644 index 00000000000..605235f61ba --- /dev/null +++ b/homeassistant/components/zwave_js/__init__.py @@ -0,0 +1,179 @@ +"""The Z-Wave JS integration.""" +import asyncio +import logging + +from async_timeout import timeout +from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.model.node import Node as ZwaveNode + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN, PLATFORMS +from .discovery import async_discover_values + +LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: dict) -> bool: + """Set up the Z-Wave JS component.""" + hass.data[DOMAIN] = {} + return True + + +@callback +def register_node_in_dev_reg( + entry: ConfigEntry, + dev_reg: device_registry.DeviceRegistry, + client: ZwaveClient, + node: ZwaveNode, +) -> None: + """Register node in dev reg.""" + dev_reg.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, f"{client.driver.controller.home_id}-{node.node_id}")}, + sw_version=node.firmware_version, + name=node.name or node.device_config.description, + model=node.device_config.label or str(node.product_type), + manufacturer=node.device_config.manufacturer or str(node.manufacturer_id), + ) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Z-Wave JS from a config entry.""" + client = ZwaveClient(entry.data[CONF_URL], async_get_clientsession(hass)) + initialized = asyncio.Event() + dev_reg = await device_registry.async_get_registry(hass) + + async def async_on_connect() -> None: + """Handle websocket is (re)connected.""" + LOGGER.info("Connected to Zwave JS Server") + if initialized.is_set(): + # update entity availability + async_dispatcher_send(hass, f"{DOMAIN}_connection_state") + + async def async_on_disconnect() -> None: + """Handle websocket is disconnected.""" + LOGGER.info("Disconnected from Zwave JS Server") + async_dispatcher_send(hass, f"{DOMAIN}_connection_state") + + async def async_on_initialized() -> None: + """Handle initial full state received.""" + LOGGER.info("Connection to Zwave JS Server initialized.") + initialized.set() + + @callback + def async_on_node_ready(node: ZwaveNode) -> None: + """Handle node ready event.""" + LOGGER.debug("Processing node %s", node) + + # register (or update) node in device registry + register_node_in_dev_reg(entry, dev_reg, client, node) + + # run discovery on all node values and create/update entities + for disc_info in async_discover_values(node): + LOGGER.debug("Discovered entity: %s", disc_info) + async_dispatcher_send(hass, f"{DOMAIN}_add_{disc_info.platform}", disc_info) + + @callback + def async_on_node_added(node: ZwaveNode) -> None: + """Handle node added event.""" + LOGGER.debug("Node added: %s - waiting for it to become ready.", node.node_id) + # we only want to run discovery when the node has reached ready state, + # otherwise we'll have all kinds of missing info issues. + if node.ready: + async_on_node_ready(node) + return + # if node is not yet ready, register one-time callback for ready state + node.once( + "ready", + lambda event: async_on_node_ready(event["node"]), + ) + # we do submit the node to device registry so user has + # some visual feedback that something is (in the process of) being added + register_node_in_dev_reg(entry, dev_reg, client, node) + + async def handle_ha_shutdown(event: Event) -> None: + """Handle HA shutdown.""" + await client.disconnect() + + # register main event callbacks. + unsubs = [ + client.register_on_initialized(async_on_initialized), + client.register_on_disconnect(async_on_disconnect), + client.register_on_connect(async_on_connect), + hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, handle_ha_shutdown), + ] + + # connect and throw error if connection failed + asyncio.create_task(client.connect()) + try: + async with timeout(10): + await initialized.wait() + except asyncio.TimeoutError as err: + for unsub in unsubs: + unsub() + await client.disconnect() + raise ConfigEntryNotReady from err + + hass.data[DOMAIN][entry.entry_id] = { + DATA_CLIENT: client, + DATA_UNSUBSCRIBE: unsubs, + } + + async def start_platforms() -> None: + """Start platforms and perform discovery.""" + # wait until all required platforms are ready + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_setup(entry, component) + for component in PLATFORMS + ] + ) + + # run discovery on all ready nodes + for node in client.driver.controller.nodes.values(): + if node.ready: + async_on_node_ready(node) + continue + # if node is not yet ready, register one-time callback for ready state + node.once( + "ready", + lambda event: async_on_node_ready(event["node"]), + ) + # listen for new nodes being added to the mesh + client.driver.controller.on( + "node added", lambda event: async_on_node_added(event["node"]) + ) + + hass.async_create_task(start_platforms()) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if not unload_ok: + return False + + info = hass.data[DOMAIN].pop(entry.entry_id) + + for unsub in info[DATA_UNSUBSCRIBE]: + unsub() + + await info[DATA_CLIENT].disconnect() + + return True diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py new file mode 100644 index 00000000000..2edb7012878 --- /dev/null +++ b/homeassistant/components/zwave_js/config_flow.py @@ -0,0 +1,78 @@ +"""Config flow for Z-Wave JS integration.""" +import asyncio +import logging +from typing import Any, Dict, Optional + +import aiohttp +from async_timeout import timeout +import voluptuous as vol +from zwave_js_server.version import VersionInfo, get_server_version + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_URL +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN, NAME # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema({CONF_URL: str}) + + +async def validate_input(hass: core.HomeAssistant, user_input: dict) -> VersionInfo: + """Validate if the user input allows us to connect.""" + ws_address = user_input[CONF_URL] + + if not ws_address.startswith(("ws://", "wss://")): + raise InvalidInput("invalid_ws_url") + + async with timeout(10): + try: + return await get_server_version(ws_address, async_get_clientsession(hass)) + except (asyncio.TimeoutError, aiohttp.ClientError) as err: + raise InvalidInput("cannot_connect") from err + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Z-Wave JS.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + async def async_step_user( + self, user_input: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + errors = {} + + assert self.hass # typing + + try: + version_info = await validate_input(self.hass, user_input) + except InvalidInput as err: + errors["base"] = err.error + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(version_info.home_id) + self._abort_if_unique_id_configured(user_input) + return self.async_create_entry(title=NAME, data=user_input) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + +class InvalidInput(exceptions.HomeAssistantError): + """Error to indicate input data is invalid.""" + + def __init__(self, error: str) -> None: + """Initialize error.""" + super().__init__() + self.error = error diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py new file mode 100644 index 00000000000..c2a9ac7b3cf --- /dev/null +++ b/homeassistant/components/zwave_js/const.py @@ -0,0 +1,9 @@ +"""Constants for the Z-Wave JS integration.""" + + +DOMAIN = "zwave_js" +NAME = "Z-Wave JS" +PLATFORMS = ["light", "sensor"] + +DATA_CLIENT = "client" +DATA_UNSUBSCRIBE = "unsubs" diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py new file mode 100644 index 00000000000..34f715a2d40 --- /dev/null +++ b/homeassistant/components/zwave_js/discovery.py @@ -0,0 +1,160 @@ +"""Map Z-Wave nodes and values to Home Assistant entities.""" + +from dataclasses import dataclass +from typing import Generator, Optional, Set, Union + +from zwave_js_server.const import CommandClass +from zwave_js_server.model.node import Node as ZwaveNode +from zwave_js_server.model.value import Value as ZwaveValue + +from homeassistant.core import callback + + +@dataclass +class ZwaveDiscoveryInfo: + """Info discovered from (primary) ZWave Value to create entity.""" + + node: ZwaveNode # node to which the value(s) belongs + primary_value: ZwaveValue # the value object itself for primary value + platform: str # the home assistant platform for which an entity should be created + platform_hint: Optional[ + str + ] = "" # hint for the platform about this discovered entity + + @property + def value_id(self) -> str: + """Return the unique value_id belonging to primary value.""" + return f"{self.node.node_id}.{self.primary_value.value_id}" + + +@dataclass +class ZWaveDiscoverySchema: + """Z-Wave discovery schema. + + The (primary) value for an entity must match these conditions. + Use the Z-Wave specifications to find out the values for these parameters: + https://github.com/zwave-js/node-zwave-js/tree/master/specs + """ + + # specify the hass platform for which this scheme applies (e.g. light, sensor) + platform: str + # [optional] hint for platform + hint: Optional[str] = None + # [optional] the node's basic device class must match ANY of these values + device_class_basic: Optional[Set[str]] = None + # [optional] the node's generic device class must match ANY of these values + device_class_generic: Optional[Set[str]] = None + # [optional] the node's specific device class must match ANY of these values + device_class_specific: Optional[Set[str]] = None + # [optional] the value's command class must match ANY of these values + command_class: Optional[Set[int]] = None + # [optional] the value's endpoint must match ANY of these values + endpoint: Optional[Set[int]] = None + # [optional] the value's property must match ANY of these values + property: Optional[Set[Union[str, int]]] = None + # [optional] the value's metadata_type must match ANY of these values + type: Optional[Set[str]] = None + + +DISCOVERY_SCHEMAS = [ + # light + # primary value is the currentValue (brightness) + ZWaveDiscoverySchema( + platform="light", + device_class_generic={"Multilevel Switch", "Remote Switch"}, + device_class_specific={ + "Multilevel Tunable Color Light", + "Binary Tunable Color Light", + "Multilevel Remote Switch", + "Multilevel Power Switch", + }, + command_class={CommandClass.SWITCH_MULTILEVEL}, + property={"currentValue"}, + type={"number"}, + ), + # generic text sensors + ZWaveDiscoverySchema( + platform="sensor", + hint="string_sensor", + command_class={ + CommandClass.ALARM, + CommandClass.SENSOR_ALARM, + CommandClass.INDICATOR, + CommandClass.NOTIFICATION, + }, + type={"string"}, + ), + # generic numeric sensors + ZWaveDiscoverySchema( + platform="sensor", + hint="numeric_sensor", + command_class={ + CommandClass.SENSOR_MULTILEVEL, + CommandClass.METER, + CommandClass.ALARM, + CommandClass.SENSOR_ALARM, + CommandClass.INDICATOR, + CommandClass.BATTERY, + CommandClass.NOTIFICATION, + CommandClass.BASIC, + }, + type={"number"}, + ), +] + + +@callback +def async_discover_values(node: ZwaveNode) -> Generator[ZwaveDiscoveryInfo, None, None]: + """Run discovery on ZWave node and return matching (primary) values.""" + for value in node.values.values(): + disc_val = async_discover_value(value) + if disc_val: + yield disc_val + + +@callback +def async_discover_value(value: ZwaveValue) -> Optional[ZwaveDiscoveryInfo]: + """Run discovery on Z-Wave value and return ZwaveDiscoveryInfo if match found.""" + for schema in DISCOVERY_SCHEMAS: + # check device_class_basic + if ( + schema.device_class_basic is not None + and value.node.device_class.basic not in schema.device_class_basic + ): + continue + # check device_class_generic + if ( + schema.device_class_generic is not None + and value.node.device_class.generic not in schema.device_class_generic + ): + continue + # check device_class_specific + if ( + schema.device_class_specific is not None + and value.node.device_class.specific not in schema.device_class_specific + ): + continue + # check command_class + if ( + schema.command_class is not None + and value.command_class not in schema.command_class + ): + continue + # check endpoint + if schema.endpoint is not None and value.endpoint not in schema.endpoint: + continue + # check property + if schema.property is not None and value.property_ not in schema.property: + continue + # check metadata_type + if schema.type is not None and value.metadata.type not in schema.type: + continue + # all checks passed, this value belongs to an entity + return ZwaveDiscoveryInfo( + node=value.node, + primary_value=value, + platform=schema.platform, + platform_hint=schema.hint, + ) + + return None diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py new file mode 100644 index 00000000000..70630cbd89c --- /dev/null +++ b/homeassistant/components/zwave_js/entity.py @@ -0,0 +1,151 @@ +"""Generic Z-Wave Entity Class.""" + +import logging +from typing import Optional, Union + +from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.model.value import Value as ZwaveValue, get_value_id + +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity + +from .const import DOMAIN +from .discovery import ZwaveDiscoveryInfo + +LOGGER = logging.getLogger(__name__) + +EVENT_VALUE_UPDATED = "value updated" + + +class ZWaveBaseEntity(Entity): + """Generic Entity Class for a Z-Wave Device.""" + + def __init__(self, client: ZwaveClient, info: ZwaveDiscoveryInfo) -> None: + """Initialize a generic Z-Wave device entity.""" + self.client = client + self.info = info + # entities requiring additional values, can add extra ids to this list + self.watched_value_ids = {self.info.primary_value.value_id} + + @callback + def on_value_update(self) -> None: + """Call when one of the watched values change. + + To be overridden by platforms needing this event. + """ + + async def async_added_to_hass(self) -> None: + """Call when entity is added.""" + assert self.hass # typing + # Add value_changed callbacks. + self.async_on_remove( + self.info.node.on(EVENT_VALUE_UPDATED, self._value_changed) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, f"{DOMAIN}_connection_state", self.async_write_ha_state + ) + ) + + @property + def device_info(self) -> dict: + """Return device information for the device registry.""" + # device is precreated in main handler + return { + "identifiers": { + ( + DOMAIN, + f"{self.client.driver.controller.home_id}-{self.info.node.node_id}", + ) + }, + } + + @property + def name(self) -> str: + """Return default name from device name and value name combination.""" + node_name = self.info.node.name or self.info.node.device_config.description + value_name = ( + self.info.primary_value.metadata.label + or self.info.primary_value.property_key_name + or self.info.primary_value.property_name + ) + return f"{node_name}: {value_name}" + + @property + def unique_id(self) -> str: + """Return the unique_id of the entity.""" + return f"{self.client.driver.controller.home_id}.{self.info.value_id}" + + @property + def available(self) -> bool: + """Return entity availability.""" + return self.client.connected and bool(self.info.node.ready) + + @callback + def _value_changed(self, event_data: Union[dict, ZwaveValue]) -> None: + """Call when (one of) our watched values changes. + + Should not be overridden by subclasses. + """ + if isinstance(event_data, ZwaveValue): + value_id = event_data.value_id + else: + value_id = event_data["value"].value_id + + if value_id not in self.watched_value_ids: + return + + value = self.info.node.values[value_id] + + LOGGER.debug( + "[%s] Value %s/%s changed to: %s", + self.entity_id, + value.property_, + value.property_key_name, + value.value, + ) + + self.on_value_update() + self.async_write_ha_state() + + @callback + def get_zwave_value( + self, + value_property: Union[str, int], + command_class: Optional[int] = None, + endpoint: Optional[int] = None, + value_property_key_name: Optional[str] = None, + add_to_watched_value_ids: bool = True, + ) -> Optional[ZwaveValue]: + """Return specific ZwaveValue on this ZwaveNode.""" + # use commandclass and endpoint from primary value if omitted + return_value = None + if command_class is None: + command_class = self.info.primary_value.command_class + if endpoint is None: + endpoint = self.info.primary_value.endpoint + # lookup value by value_id + value_id = get_value_id( + self.info.node, + { + "commandClass": command_class, + "endpoint": endpoint, + "property": value_property, + "propertyKeyName": value_property_key_name, + }, + ) + return_value = self.info.node.values.get(value_id) + # add to watched_ids list so we will be triggered when the value updates + if ( + return_value + and return_value.value_id not in self.watched_value_ids + and add_to_watched_value_ids + ): + self.watched_value_ids.add(return_value.value_id) + return return_value + + @property + def should_poll(self) -> bool: + """No polling needed.""" + return False diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py new file mode 100644 index 00000000000..45bd68aef81 --- /dev/null +++ b/homeassistant/components/zwave_js/light.py @@ -0,0 +1,322 @@ +"""Support for Z-Wave lights.""" +import logging +from typing import Any, Callable, List, Optional + +from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.const import CommandClass + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_HS_COLOR, + ATTR_TRANSITION, + ATTR_WHITE_VALUE, + DOMAIN as LIGHT_DOMAIN, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, + SUPPORT_TRANSITION, + SUPPORT_WHITE_VALUE, + LightEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +import homeassistant.util.color as color_util + +from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN +from .discovery import ZwaveDiscoveryInfo +from .entity import ZWaveBaseEntity + +LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable +) -> None: + """Set up Z-Wave Light from Config Entry.""" + client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] + + @callback + def async_add_light(info: ZwaveDiscoveryInfo) -> None: + """Add Z-Wave Light.""" + + light = ZwaveLight(client, info) + async_add_entities([light]) + + hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + async_dispatcher_connect(hass, f"{DOMAIN}_add_{LIGHT_DOMAIN}", async_add_light) + ) + + +def byte_to_zwave_brightness(value: int) -> int: + """Convert brightness in 0-255 scale to 0-99 scale. + + `value` -- (int) Brightness byte value from 0-255. + """ + if value > 0: + return max(1, round((value / 255) * 99)) + return 0 + + +class ZwaveLight(ZWaveBaseEntity, LightEntity): + """Representation of a Z-Wave light.""" + + def __init__(self, client: ZwaveClient, info: ZwaveDiscoveryInfo) -> None: + """Initialize the light.""" + super().__init__(client, info) + self._supports_color = False + self._supports_white_value = False + self._supports_color_temp = False + self._hs_color: Optional[List[float]] = None + self._white_value: Optional[int] = None + self._color_temp: Optional[int] = None + self._min_mireds = 153 # 6500K as a safe default + self._max_mireds = 370 # 2700K as a safe default + self._supported_features = SUPPORT_BRIGHTNESS + + # get additional (optional) values and set features + self._target_value = self.get_zwave_value("targetValue") + self._dimming_duration = self.get_zwave_value("duration") + if self._dimming_duration is not None: + self._supported_features |= SUPPORT_TRANSITION + self._calculate_color_values() + if self._supports_color: + self._supported_features |= SUPPORT_COLOR + if self._supports_color_temp: + self._supported_features |= SUPPORT_COLOR_TEMP + if self._supports_white_value: + self._supported_features |= SUPPORT_WHITE_VALUE + + @callback + def on_value_update(self) -> None: + """Call when a watched value is added or updated.""" + self._calculate_color_values() + + @property + def brightness(self) -> int: + """Return the brightness of this light between 0..255. + + Z-Wave multilevel switches use a range of [0, 99] to control brightness. + """ + if self._target_value is not None and self._target_value.value is not None: + return round((self._target_value.value / 99) * 255) + if self.info.primary_value.value is not None: + return round((self.info.primary_value.value / 99) * 255) + return 0 + + @property + def is_on(self) -> bool: + """Return true if device is on (brightness above 0).""" + return self.brightness > 0 + + @property + def hs_color(self) -> Optional[List[float]]: + """Return the hs color.""" + return self._hs_color + + @property + def white_value(self) -> Optional[int]: + """Return the white value of this light between 0..255.""" + return self._white_value + + @property + def color_temp(self) -> Optional[int]: + """Return the color temperature.""" + return self._color_temp + + @property + def min_mireds(self) -> int: + """Return the coldest color_temp that this light supports.""" + return self._min_mireds + + @property + def max_mireds(self) -> int: + """Return the warmest color_temp that this light supports.""" + return self._max_mireds + + @property + def supported_features(self) -> Optional[int]: + """Flag supported features.""" + return self._supported_features + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the device on.""" + # RGB/HS color + hs_color = kwargs.get(ATTR_HS_COLOR) + if hs_color is not None and self._supports_color: + # set white levels to 0 when setting rgb + await self._async_set_color("Warm White", 0) + await self._async_set_color("Cold White", 0) + red, green, blue = color_util.color_hs_to_RGB(*hs_color) + await self._async_set_color("Red", red) + await self._async_set_color("Green", green) + await self._async_set_color("Blue", blue) + else: + # turn off rgb when setting white values + await self._async_set_color("Red", 0) + await self._async_set_color("Green", 0) + await self._async_set_color("Blue", 0) + + # Color temperature + color_temp = kwargs.get(ATTR_COLOR_TEMP) + if color_temp is not None and self._supports_color_temp: + # Limit color temp to min/max values + cold = max( + 0, + min( + 255, + round( + (self._max_mireds - color_temp) + / (self._max_mireds - self._min_mireds) + * 255 + ), + ), + ) + warm = 255 - cold + await self._async_set_color("Warm White", warm) + await self._async_set_color("Cold White", cold) + + # White value + white_value = kwargs.get(ATTR_WHITE_VALUE) + if white_value is not None and self._supports_white_value: + await self._async_set_color("Warm White", white_value) + + # set brightness + await self._async_set_brightness( + kwargs.get(ATTR_BRIGHTNESS), kwargs.get(ATTR_TRANSITION) + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the light off.""" + await self._async_set_brightness(0, kwargs.get(ATTR_TRANSITION)) + + async def _async_set_color(self, color_name: str, new_value: int) -> None: + """Set defined color to given value.""" + cur_zwave_value = self.get_zwave_value( + "currentColor", + CommandClass.SWITCH_COLOR, + value_property_key_name=color_name, + ) + # guard for unsupported command + if cur_zwave_value is None: + return + # no need to send same value + if cur_zwave_value.value == new_value: + return + # actually set the new color value + target_zwave_value = self.get_zwave_value( + "targetColor", + CommandClass.SWITCH_COLOR, + value_property_key_name=color_name, + ) + if target_zwave_value is None: + return + await self.info.node.async_set_value(target_zwave_value, new_value) + + async def _async_set_brightness( + self, brightness: Optional[int], transition: Optional[int] = None + ) -> None: + """Set new brightness to light.""" + if self.info.primary_value.value == brightness: + # no point in setting same brightness + return + if brightness is None and self.info.primary_value.value: + # there is no point in setting default brightness when light is already on + return + if brightness is None: + # Level 255 means to set it to previous value. + brightness = 255 + else: + # Zwave multilevel switches use a range of [0, 99] to control brightness. + brightness = byte_to_zwave_brightness(brightness) + # set transition value before seinding new brightness + await self._async_set_transition_duration(transition) + # setting a value requires setting targetValue + await self.info.node.async_set_value(self._target_value, brightness) + + async def _async_set_transition_duration( + self, duration: Optional[int] = None + ) -> None: + """Set the transition time for the brightness value.""" + if self._dimming_duration is None: + return + # pylint: disable=fixme,unreachable + # TODO: setting duration needs to be fixed upstream + # https://github.com/zwave-js/node-zwave-js/issues/1321 + return + + if duration is None: # type: ignore + # no transition specified by user, use defaults + duration = 7621 # anything over 7620 uses the factory default + else: + # transition specified by user + transition = duration + if transition <= 127: + duration = transition + else: + minutes = round(transition / 60) + LOGGER.debug( + "Transition rounded to %d minutes for %s", + minutes, + self.entity_id, + ) + duration = minutes + 128 + + # only send value if it differs from current + # this prevents sending a command for nothing + if self._dimming_duration.value != duration: + await self.info.node.async_set_value(self._dimming_duration, duration) + + @callback + def _calculate_color_values(self) -> None: + """Calculate light colors.""" + + # RGB support + red_val = self.get_zwave_value( + "currentColor", CommandClass.SWITCH_COLOR, value_property_key_name="Red" + ) + green_val = self.get_zwave_value( + "currentColor", CommandClass.SWITCH_COLOR, value_property_key_name="Green" + ) + blue_val = self.get_zwave_value( + "currentColor", CommandClass.SWITCH_COLOR, value_property_key_name="Blue" + ) + if red_val and green_val and blue_val: + self._supports_color = True + # convert to HS + if ( + red_val.value is not None + and green_val.value is not None + and blue_val.value is not None + ): + self._hs = color_util.color_RGB_to_hs( + red_val.value, green_val.value, blue_val.value + ) + + # White colors + ww_val = self.get_zwave_value( + "currentColor", + CommandClass.SWITCH_COLOR, + value_property_key_name="Warm White", + ) + cw_val = self.get_zwave_value( + "currentColor", + CommandClass.SWITCH_COLOR, + value_property_key_name="Cold White", + ) + if ww_val and cw_val: + # Color temperature (CW + WW) Support + self._supports_color_temp = True + # Calculate color temps based on whites + cold_level = cw_val.value or 0 + if cold_level or ww_val.value is not None: + self._color_temp = round( + self._max_mireds + - ((cold_level / 255) * (self._max_mireds - self._min_mireds)) + ) + else: + self._color_temp = None + elif ww_val or cw_val: + # only one white channel + self._supports_white_value = True diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json new file mode 100644 index 00000000000..718a4da2b85 --- /dev/null +++ b/homeassistant/components/zwave_js/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "zwave_js", + "name": "Z-Wave JS", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/zwave_js", + "requirements": ["zwave-js-server-python==0.6.0"], + "codeowners": ["@home-assistant/z-wave"] +} diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py new file mode 100644 index 00000000000..ef1a68bb7a7 --- /dev/null +++ b/homeassistant/components/zwave_js/sensor.py @@ -0,0 +1,149 @@ +"""Representation of Z-Wave sensors.""" + +import logging +from typing import Callable, Dict, List, Optional + +from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.const import CommandClass + +from homeassistant.components.sensor import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DOMAIN as SENSOR_DOMAIN, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN +from .discovery import ZwaveDiscoveryInfo +from .entity import ZWaveBaseEntity + +LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable +) -> None: + """Set up Z-Wave sensor from config entry.""" + client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] + + @callback + def async_add_sensor(info: ZwaveDiscoveryInfo) -> None: + """Add Z-Wave Sensor.""" + entities: List[ZWaveBaseEntity] = [] + + if info.platform_hint == "string_sensor": + entities.append(ZWaveStringSensor(client, info)) + elif info.platform_hint == "numeric_sensor": + entities.append(ZWaveNumericSensor(client, info)) + else: + LOGGER.warning( + "Sensor not implemented for %s/%s", + info.platform_hint, + info.primary_value.propertyname, + ) + return + + async_add_entities(entities) + + hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + async_dispatcher_connect( + hass, f"{DOMAIN}_add_{SENSOR_DOMAIN}", async_add_sensor + ) + ) + + +class ZwaveSensorBase(ZWaveBaseEntity): + """Basic Representation of a Z-Wave sensor.""" + + @property + def device_class(self) -> Optional[str]: + """Return the device class of the sensor.""" + if self.info.primary_value.command_class == CommandClass.BATTERY: + return DEVICE_CLASS_BATTERY + if self.info.primary_value.command_class == CommandClass.METER: + return DEVICE_CLASS_POWER + if self.info.primary_value.property_key_name == "W_Consumed": + return DEVICE_CLASS_POWER + if self.info.primary_value.property_key_name == "kWh_Consumed": + return DEVICE_CLASS_ENERGY + if self.info.primary_value.property_ == "Air temperature": + return DEVICE_CLASS_TEMPERATURE + return None + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + # We hide some of the more advanced sensors by default to not overwhelm users + if self.info.primary_value.command_class in [ + CommandClass.BASIC, + CommandClass.INDICATOR, + CommandClass.NOTIFICATION, + ]: + return False + return True + + @property + def force_update(self) -> bool: + """Force updates.""" + return True + + +class ZWaveStringSensor(ZwaveSensorBase): + """Representation of a Z-Wave String sensor.""" + + @property + def state(self) -> Optional[str]: + """Return state of the sensor.""" + if self.info.primary_value.value is None: + return None + return str(self.info.primary_value.value) + + @property + def unit_of_measurement(self) -> Optional[str]: + """Return unit of measurement the value is expressed in.""" + if self.info.primary_value.metadata.unit is None: + return None + return str(self.info.primary_value.metadata.unit) + + +class ZWaveNumericSensor(ZwaveSensorBase): + """Representation of a Z-Wave Numeric sensor.""" + + @property + def state(self) -> float: + """Return state of the sensor.""" + if self.info.primary_value.value is None: + return 0 + return round(float(self.info.primary_value.value), 2) + + @property + def unit_of_measurement(self) -> Optional[str]: + """Return unit of measurement the value is expressed in.""" + if self.info.primary_value.metadata.unit is None: + return None + if self.info.primary_value.metadata.unit == "C": + return TEMP_CELSIUS + if self.info.primary_value.metadata.unit == "F": + return TEMP_FAHRENHEIT + + return str(self.info.primary_value.metadata.unit) + + @property + def device_state_attributes(self) -> Optional[Dict[str, str]]: + """Return the device specific state attributes.""" + if ( + self.info.primary_value.value is None + or not self.info.primary_value.metadata.states + ): + return None + # add the value's label as property for multi-value (list) items + label = self.info.primary_value.metadata.states.get( + self.info.primary_value.value + ) or self.info.primary_value.metadata.states.get( + str(self.info.primary_value.value) + ) + return {"label": label} diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json new file mode 100644 index 00000000000..29136a03f48 --- /dev/null +++ b/homeassistant/components/zwave_js/strings.json @@ -0,0 +1,20 @@ +{ + "title": "Z-Wave JS", + "config": { + "step": { + "user": { + "data": { + "url": "[%key:common::config_flow::data::url%]" + } + } + }, + "error": { + "invalid_ws_url": "Invalid websocket URL", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/zwave_js/translations/en.json b/homeassistant/components/zwave_js/translations/en.json new file mode 100644 index 00000000000..13b2e736bae --- /dev/null +++ b/homeassistant/components/zwave_js/translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_ws_url": "Invalid websocket URL", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + }, + "title": "Z-Wave JS" +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index b696e29964a..3218ab4baa5 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -240,5 +240,6 @@ FLOWS = [ "yeelight", "zerproc", "zha", - "zwave" + "zwave", + "zwave_js" ] diff --git a/requirements_all.txt b/requirements_all.txt index a6d094d1592..40b29e579d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2370,3 +2370,6 @@ zigpy==0.29.0 # homeassistant.components.zoneminder zm-py==0.5.2 + +# homeassistant.components.zwave_js +zwave-js-server-python==0.6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6dc4f724a7b..b6a004aed2c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1169,3 +1169,6 @@ zigpy-znp==0.3.0 # homeassistant.components.zha zigpy==0.29.0 + +# homeassistant.components.zwave_js +zwave-js-server-python==0.6.0 diff --git a/setup.cfg b/setup.cfg index 1fc973ef21c..4137554257f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,7 +42,7 @@ 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.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.*] +[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.components.zwave_js.*,homeassistant.helpers.*,homeassistant.scripts.*,homeassistant.util.*,tests.components.hyperion.*] strict = true ignore_errors = false warn_unreachable = true diff --git a/tests/components/zwave_js/__init__.py b/tests/components/zwave_js/__init__.py new file mode 100644 index 00000000000..bd4b740c856 --- /dev/null +++ b/tests/components/zwave_js/__init__.py @@ -0,0 +1 @@ +"""Tests for the Z-Wave JS integration.""" diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py new file mode 100644 index 00000000000..61e50d86a2d --- /dev/null +++ b/tests/components/zwave_js/conftest.py @@ -0,0 +1,58 @@ +"""Provide common Z-Wave JS fixtures.""" +import json +from unittest.mock import patch + +import pytest +from zwave_js_server.model.driver import Driver +from zwave_js_server.model.node import Node + +from tests.common import MockConfigEntry, load_fixture + + +@pytest.fixture(name="controller_state", scope="session") +def controller_state_fixture(): + """Load the controller state fixture data.""" + return json.loads(load_fixture("zwave_js/controller_state.json")) + + +@pytest.fixture(name="multisensor_6_state", scope="session") +def multisensor_6_state_fixture(): + """Load the multisensor 6 node state fixture data.""" + return json.loads(load_fixture("zwave_js/multisensor_6_state.json")) + + +@pytest.fixture(name="client") +def mock_client_fixture(controller_state): + """Mock a client.""" + with patch( + "homeassistant.components.zwave_js.ZwaveClient", autospec=True + ) as client_class: + driver = Driver(client_class.return_value, controller_state) + client_class.return_value.driver = driver + yield client_class.return_value + + +@pytest.fixture(name="multisensor_6") +def multisensor_6_fixture(client, multisensor_6_state): + """Mock a multisensor 6 node.""" + node = Node(client, multisensor_6_state) + client.driver.controller.nodes[node.node_id] = node + return node + + +@pytest.fixture(name="integration") +async def integration_fixture(hass, client): + """Set up the zwave_js integration.""" + entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"}) + entry.add_to_hass(hass) + + def initialize_client(async_on_initialized): + """Init the client.""" + hass.async_create_task(async_on_initialized()) + + client.register_on_initialized.side_effect = initialize_client + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py new file mode 100644 index 00000000000..c6885377490 --- /dev/null +++ b/tests/components/zwave_js/test_config_flow.py @@ -0,0 +1,99 @@ +"""Test the Z-Wave JS config flow.""" +import asyncio +from unittest.mock import patch + +from zwave_js_server.version import VersionInfo + +from homeassistant import config_entries, setup +from homeassistant.components.zwave_js.const import DOMAIN + + +async def test_user_step_full(hass): + """Test we create an entry with user step.""" + 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" + + with patch( + "homeassistant.components.zwave_js.config_flow.get_server_version", + return_value=VersionInfo( + driver_version="mock-driver-version", + server_version="mock-server-version", + home_id=1234, + ), + ), patch( + "homeassistant.components.zwave_js.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.zwave_js.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": "ws://localhost:3000", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Z-Wave JS" + assert result2["data"] == { + "url": "ws://localhost:3000", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + assert result2["result"].unique_id == 1234 + + +async def test_user_step_invalid_input(hass): + """Test we handle invalid auth in the user step.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.zwave_js.config_flow.get_server_version", + side_effect=asyncio.TimeoutError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": "ws://localhost:3000", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": "not-ws-url", + }, + ) + + assert result3["type"] == "form" + assert result3["errors"] == {"base": "invalid_ws_url"} + + +async def test_user_step_unexpected_exception(hass): + """Test we handle unexpected exception.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.zwave_js.config_flow.get_server_version", + side_effect=Exception("Boom"), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": "ws://localhost:3000", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py new file mode 100644 index 00000000000..79876a5b453 --- /dev/null +++ b/tests/components/zwave_js/test_sensor.py @@ -0,0 +1,14 @@ +"""Test the Z-Wave JS sensor platform.""" +from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS + +AIR_TEMPERATURE_SENSOR = "sensor.multisensor_6_air_temperature" + + +async def test_numeric_sensor(hass, multisensor_6, integration): + """Test the numeric sensor.""" + state = hass.states.get(AIR_TEMPERATURE_SENSOR) + + assert state + assert state.state == "9.0" + assert state.attributes["unit_of_measurement"] == TEMP_CELSIUS + assert state.attributes["device_class"] == DEVICE_CLASS_TEMPERATURE diff --git a/tests/fixtures/zwave_js/controller_state.json b/tests/fixtures/zwave_js/controller_state.json new file mode 100644 index 00000000000..df026e8fd2c --- /dev/null +++ b/tests/fixtures/zwave_js/controller_state.json @@ -0,0 +1,98 @@ +{ + "controller": { + "libraryVersion": "Z-Wave 3.95", + "type": 1, + "homeId": 3245146787, + "ownNodeId": 1, + "isSecondary": false, + "isUsingHomeIdFromOtherNetwork": false, + "isSISPresent": true, + "wasRealPrimary": true, + "isStaticUpdateController": true, + "isSlave": false, + "serialApiVersion": "1.0", + "manufacturerId": 134, + "productType": 257, + "productId": 90, + "supportedFunctionTypes": [ + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 28, + 32, + 33, + 34, + 35, + 36, + 39, + 41, + 42, + 43, + 44, + 45, + 65, + 66, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 80, + 81, + 83, + 84, + 85, + 86, + 87, + 94, + 96, + 97, + 98, + 99, + 102, + 103, + 128, + 144, + 146, + 147, + 152, + 180, + 182, + 183, + 184, + 185, + 186, + 189, + 190, + 191, + 210, + 211, + 212, + 238, + 239 + ], + "sucNodeId": 1, + "supportsTimers": false + }, + "nodes": [ + ] +} diff --git a/tests/fixtures/zwave_js/multisensor_6_state.json b/tests/fixtures/zwave_js/multisensor_6_state.json new file mode 100644 index 00000000000..3c508ffd3ff --- /dev/null +++ b/tests/fixtures/zwave_js/multisensor_6_state.json @@ -0,0 +1,1830 @@ +{ + "nodeId": 52, + "index": 0, + "installerIcon": 3079, + "userIcon": 3079, + "status": 1, + "ready": true, + "deviceClass": { + "basic": "Static Controller", + "generic": "Multilevel Sensor", + "specific": "Routing Multilevel Sensor", + "mandatorySupportedCCs": [ + "Basic", + "Multilevel Sensor" + ], + "mandatoryControlCCs": [] + }, + "isListening": true, + "isFrequentListening": false, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": false, + "version": 4, + "isBeaming": true, + "manufacturerId": 134, + "productId": 100, + "productType": 258, + "firmwareVersion": "1.12", + "zwavePlusVersion": 1, + "nodeType": 0, + "roleType": 5, + "deviceConfig": { + "manufacturerId": 134, + "manufacturer": "AEON Labs", + "label": "ZW100", + "description": "Multisensor 6", + "devices": [ + { + "productType": "0x0002", + "productId": "0x0064" + }, + { + "productType": "0x0102", + "productId": "0x0064" + }, + { + "productType": "0x0202", + "productId": "0x0064" + } + ], + "firmwareVersion": { + "min": "1.10", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + } + }, + "label": "ZW100", + "neighbors": [ + 1, + 32 + ], + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 52, + "index": 0, + "installerIcon": 3079, + "userIcon": 3079 + } + ], + "values": [ + { + "commandClassName": "Basic", + "commandClass": 32, + "endpoint": 0, + "property": "currentValue", + "propertyName": "currentValue", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 99, + "label": "Current value" + }, + "value": 255 + }, + { + "commandClassName": "Basic", + "commandClass": 32, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 99, + "label": "Target value" + } + }, + { + "commandClassName": "Binary Sensor", + "commandClass": 48, + "endpoint": 0, + "property": "Any", + "propertyName": "Any", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Any", + "ccSpecific": { + "sensorType": 255 + } + }, + "value": false + }, + { + "commandClassName": "Multilevel Sensor", + "commandClass": 49, + "endpoint": 0, + "property": "Air temperature", + "propertyName": "Air temperature", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "unit": "°C", + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 0 + } + }, + "value": 9 + }, + { + "commandClassName": "Multilevel Sensor", + "commandClass": 49, + "endpoint": 0, + "property": "Illuminance", + "propertyName": "Illuminance", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "unit": "Lux", + "label": "Illuminance", + "ccSpecific": { + "sensorType": 3, + "scale": 1 + } + }, + "value": 0 + }, + { + "commandClassName": "Multilevel Sensor", + "commandClass": 49, + "endpoint": 0, + "property": "Humidity", + "propertyName": "Humidity", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "unit": "%", + "label": "Humidity", + "ccSpecific": { + "sensorType": 5, + "scale": 0 + } + }, + "value": 65 + }, + { + "commandClassName": "Multilevel Sensor", + "commandClass": 49, + "endpoint": 0, + "property": "Ultraviolet", + "propertyName": "Ultraviolet", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Ultraviolet", + "ccSpecific": { + "sensorType": 27, + "scale": 0 + } + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 2, + "propertyName": "Stay Awake in Battery Mode", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Disable", + "1": "Enable" + }, + "label": "Stay Awake in Battery Mode", + "description": "Stay awake for 10 minutes at power on", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 3, + "propertyName": "Motion Sensor reset timeout", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 10, + "max": 3600, + "default": 240, + "format": 0, + "allowManualEntry": true, + "label": "Motion Sensor reset timeout", + "description": "Motion Sensor reset timeout", + "isFromConfig": true + }, + "value": 240 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 4, + "propertyName": "Motion sensor sensitivity", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 5, + "format": 1, + "allowManualEntry": false, + "states": { + "0": "Disable", + "1": "Enable, sensitivity level 1 (minimum)", + "2": "Enable, sensitivity level 2", + "3": "Enable, sensitivity level 3", + "4": "Enable, sensitivity level 4", + "5": "Enable, sensitivity level 5 (maximum)" + }, + "label": "Motion sensor sensitivity", + "description": "Sensitivity level of PIR sensor (1=minimum, 5=maximum)", + "isFromConfig": true + }, + "value": 5 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 5, + "propertyName": "Motion Sensor Triggered Command", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 1, + "format": 1, + "allowManualEntry": false, + "states": { + "1": "Send Basic Set CC", + "2": "Send Sensor Binary Report CC" + }, + "label": "Motion Sensor Triggered Command", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 8, + "propertyName": "Timeout after wake up", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 8, + "max": 255, + "default": 30, + "format": 1, + "allowManualEntry": true, + "label": "Timeout after wake up", + "description": "Set the timeout of awake after the Wake Up CC is sent out...", + "isFromConfig": true + }, + "value": 15 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 39, + "propertyName": "Low Battery Report", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 10, + "max": 50, + "default": 20, + "format": 0, + "allowManualEntry": true, + "label": "Low Battery Report", + "description": "Report Low Battery if below this value", + "isFromConfig": true + }, + "value": 20 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 40, + "propertyName": "Selective Reporting", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Disable", + "1": "Enable" + }, + "label": "Selective Reporting", + "description": "Select to report on thresholds", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 42, + "propertyName": "Humidity Threshold", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 100, + "default": 10, + "format": 0, + "allowManualEntry": true, + "label": "Humidity Threshold", + "description": "Humidity percent change threshold", + "isFromConfig": true + }, + "value": 10 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 43, + "propertyName": "Luminance Threshold", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 0, + "max": 1000, + "default": 100, + "format": 0, + "allowManualEntry": true, + "label": "Luminance Threshold", + "description": "Luminance change threshold", + "isFromConfig": true + }, + "value": 100 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 44, + "propertyName": "Battery Threshold", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 100, + "default": 10, + "format": 0, + "allowManualEntry": true, + "label": "Battery Threshold", + "description": "Battery level threshold", + "isFromConfig": true + }, + "value": 10 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 45, + "propertyName": "Ultraviolet Threshold", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 100, + "default": 2, + "format": 0, + "allowManualEntry": true, + "label": "Ultraviolet Threshold", + "description": "Ultraviolet change threshold", + "isFromConfig": true + }, + "value": 2 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 46, + "propertyName": "Send Alarm Report if low temperature", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": false, + "states": { + "0": "Disable", + "1": "Enable" + }, + "label": "Send Alarm Report if low temperature", + "description": "Send an alarm report if temperature is less than -15 °C", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 48, + "propertyName": "Send a report if the measurement is out of limits", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": true, + "label": "Send a report if the measurement is out of limits", + "description": "Send report when measurement is at upper/lower limit", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 51, + "propertyName": "Upper limit value of humidity sensor", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 100, + "default": 60, + "format": 0, + "allowManualEntry": true, + "label": "Upper limit value of humidity sensor", + "description": "Upper limit value of humidity sensor", + "isFromConfig": true + }, + "value": 60 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 52, + "propertyName": "Lower limit value of humidity sensor", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 100, + "default": 50, + "format": 0, + "allowManualEntry": true, + "label": "Lower limit value of humidity sensor", + "description": "Lower limit value of humidity sensor", + "isFromConfig": true + }, + "value": 50 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 53, + "propertyName": "Upper limit value of Lighting sensor", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 0, + "max": 30000, + "default": 1000, + "format": 0, + "allowManualEntry": true, + "label": "Upper limit value of Lighting sensor", + "description": "Upper limit value of Lighting sensor", + "isFromConfig": true + }, + "value": 1000 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 54, + "propertyName": "Lower limit value of Lighting sensor", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 0, + "max": 30000, + "default": 100, + "format": 0, + "allowManualEntry": true, + "label": "Lower limit value of Lighting sensor", + "description": "Lower limit value of Lighting sensor", + "isFromConfig": true + }, + "value": 100 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 55, + "propertyName": "Upper limit value of ultraviolet sensor", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 11, + "default": 8, + "format": 0, + "allowManualEntry": true, + "label": "Upper limit value of ultraviolet sensor", + "description": "Upper limit value of ultraviolet sensor", + "isFromConfig": true + }, + "value": 8 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 56, + "propertyName": "Lower limit value of ultraviolet sensor", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 11, + "default": 4, + "format": 0, + "allowManualEntry": true, + "label": "Lower limit value of ultraviolet sensor", + "description": "Lower limit value of ultraviolet sensor", + "isFromConfig": true + }, + "value": 4 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 57, + "propertyName": "Recover limit value of temperature sensor", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 0, + "max": 65535, + "default": 0, + "format": 1, + "allowManualEntry": true, + "label": "Recover limit value of temperature sensor", + "description": "Recover limit value of temperature sensor", + "isFromConfig": true + }, + "value": 5122 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 58, + "propertyName": "Recover limit value of humidity sensor", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 50, + "default": 5, + "format": 0, + "allowManualEntry": true, + "label": "Recover limit value of humidity sensor", + "description": "Recover limit value of humidity sensor", + "isFromConfig": true + }, + "value": 5 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 59, + "propertyName": "Recover limit value of Lighting sensor.", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 255, + "default": 10, + "format": 1, + "allowManualEntry": true, + "label": "Recover limit value of Lighting sensor.", + "description": "Recover limit value of Lighting sensor.", + "isFromConfig": true + }, + "value": 10 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 60, + "propertyName": "Recover limit value of Ultraviolet sensor", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 5, + "default": 2, + "format": 0, + "allowManualEntry": true, + "label": "Recover limit value of Ultraviolet sensor", + "description": "Recover limit value of Ultraviolet sensor", + "isFromConfig": true + }, + "value": 2 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 61, + "propertyName": "Out-of-limit state of the Sensors", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": true, + "label": "Out-of-limit state of the Sensors", + "description": "Out-of-limit state of the Sensors", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 64, + "propertyName": "Default unit of the automatic temperature report", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 2, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Default unit of the automatic temperature report", + "description": "Default unit of the automatic temperature report", + "isFromConfig": true + }, + "value": 2 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 81, + "propertyName": "LED function", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 2, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Enable LED blinking", + "1": "Disable PIR LED", + "2": "Disable ALL" + }, + "label": "LED function", + "description": "Disable/Enable LED function", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 111, + "propertyName": "Group 1 Report Interval", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 5, + "max": 2678400, + "default": 3600, + "format": 0, + "allowManualEntry": true, + "label": "Group 1 Report Interval", + "description": "How often to update Group 1", + "isFromConfig": true + }, + "value": 3600 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 112, + "propertyName": "Group 2 Report Interval", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 5, + "max": 2678400, + "default": 3600, + "format": 0, + "allowManualEntry": true, + "label": "Group 2 Report Interval", + "description": "Group 2 Report Interval", + "isFromConfig": true + }, + "value": 3600 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 113, + "propertyName": "Group 3 Report Interval", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 5, + "max": 2678400, + "default": 3600, + "format": 0, + "allowManualEntry": true, + "label": "Group 3 Report Interval", + "description": "Group 3 Report Interval", + "isFromConfig": true + }, + "value": 3600 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 202, + "propertyName": "Humidity Sensor Calibration", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": -50, + "max": 50, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Humidity Sensor Calibration", + "description": "Humidity Sensor Calibration", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 203, + "propertyName": "Luminance Sensor Calibration", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": -1000, + "max": 1000, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Luminance Sensor Calibration", + "description": "Luminance Sensor Calibration", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 204, + "propertyName": "Ultraviolet Sensor Calibration", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": -10, + "max": 10, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Ultraviolet Sensor Calibration", + "description": "Ultraviolet Sensor Calibration", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 252, + "propertyName": "Disable/Enable Configuration Lock", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Disable", + "1": "Enable" + }, + "label": "Disable/Enable Configuration Lock", + "description": "Disable/Enable Configuration Lock (0=Disable, 1=Enable)", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 101, + "propertyKey": 1, + "propertyName": "Group 1: Send battery reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 1: Send battery reports", + "description": "Include battery information in periodic reports to Group 1", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 101, + "propertyKey": 16, + "propertyName": "Group 1: Send ultraviolet reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 1: Send ultraviolet reports", + "description": "Include ultraviolet information in periodic reports to Group 1", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 101, + "propertyKey": 32, + "propertyName": "Group 1: Send temperature reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 1: Send temperature reports", + "description": "Include temperature information in periodic reports to Group 1", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 101, + "propertyKey": 64, + "propertyName": "Group 1: Send humidity reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 1: Send humidity reports", + "description": "Include humidity information in periodic reports to Group 1", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 101, + "propertyKey": 128, + "propertyName": "Group 1: Send luminance reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 1: Send luminance reports", + "description": "Include luminance information in periodic reports to Group 1", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 102, + "propertyKey": 1, + "propertyName": "Group 2: Send battery reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 2: Send battery reports", + "description": "Include battery information in periodic reports to Group 2", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 102, + "propertyKey": 16, + "propertyName": "Group 2: Send ultraviolet reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 2: Send ultraviolet reports", + "description": "Include ultraviolet information in periodic reports to Group 2", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 102, + "propertyKey": 32, + "propertyName": "Group 2: Send temperature reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 2: Send temperature reports", + "description": "Include temperature information in periodic reports to Group 2", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 102, + "propertyKey": 64, + "propertyName": "Group 2: Send humidity reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 2: Send humidity reports", + "description": "Include humidity information in periodic reports to Group 2", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 102, + "propertyKey": 128, + "propertyName": "Group 2: Send luminance reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 2: Send luminance reports", + "description": "Include luminance information in periodic reports to Group 2", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 103, + "propertyKey": 1, + "propertyName": "Group 3: Send battery reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 3: Send battery reports", + "description": "Include battery information in periodic reports to Group 3", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 103, + "propertyKey": 16, + "propertyName": "Group 3: Send ultraviolet reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 3: Send ultraviolet reports", + "description": "Include ultraviolet information in periodic reports to Group 3", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 103, + "propertyKey": 32, + "propertyName": "Group 3: Send temperature reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 3: Send temperature reports", + "description": "Include temperature information in periodic reports to Group 3", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 103, + "propertyKey": 64, + "propertyName": "Group 3: Send humidity reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 3: Send humidity reports", + "description": "Include humidity information in periodic reports to Group 3", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 103, + "propertyKey": 128, + "propertyName": "Group 3: Send luminance reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 3: Send luminance reports", + "description": "Include luminance information in periodic reports to Group 3", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 9, + "propertyKey": 1, + "propertyName": "Sleep State", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "valueSize": 2, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Asleep", + "1": "Awake" + }, + "label": "Sleep State", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 9, + "propertyKey": 256, + "propertyName": "Power Mode", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "valueSize": 2, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "USB", + "1": "Battery" + }, + "label": "Power Mode", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 41, + "propertyKey": 15, + "propertyName": "Temperature Threshold (Unit)", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 3, + "min": 1, + "max": 2, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "1": "Celsius", + "2": "Fahrenheit" + }, + "label": "Temperature Threshold (Unit)", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 41, + "propertyKey": 16776960, + "propertyName": "Temperature Threshold", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 3, + "min": 0, + "max": 100, + "default": 20, + "format": 0, + "allowManualEntry": true, + "label": "Temperature Threshold", + "description": "Threshold change in temperature to induce an automatic report.", + "isFromConfig": true + }, + "value": 5122 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 49, + "propertyKey": 65280, + "propertyName": "Upper temperature limit (Unit)", + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "valueSize": 4, + "min": 1, + "max": 2, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "1": "Celsius", + "2": "Fahrenheit" + }, + "label": "Upper temperature limit (Unit)", + "isFromConfig": true + }, + "value": 2 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 49, + "propertyKey": 4294901760, + "propertyName": "Upper temperature limit", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": -400, + "max": 2120, + "default": 280, + "format": 0, + "allowManualEntry": true, + "label": "Upper temperature limit", + "isFromConfig": true + }, + "value": 824 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 50, + "propertyKey": 65280, + "propertyName": "Lower temperature limit (Unit)", + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "valueSize": 4, + "min": 1, + "max": 2, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "1": "Celsius", + "2": "Fahrenheit" + }, + "label": "Lower temperature limit (Unit)", + "isFromConfig": true + }, + "value": 2 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 50, + "propertyKey": 4294901760, + "propertyName": "Lower temperature limit", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": -400, + "max": 2120, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Lower temperature limit", + "isFromConfig": true + }, + "value": 320 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 201, + "propertyKey": 255, + "propertyName": "Temperature Calibration (Unit)", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 1, + "max": 2, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "1": "Celsius", + "2": "Fahrenheit" + }, + "label": "Temperature Calibration (Unit)", + "isFromConfig": true + }, + "value": 2 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 201, + "propertyKey": 65280, + "propertyName": "Temperature Calibration", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": -127, + "max": 127, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Temperature Calibration", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 100, + "propertyName": "Set parameters 101-103 to default.", + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Set parameters 101-103 to default.", + "description": "Reset 101-103 to defaults", + "isFromConfig": true + } + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 110, + "propertyName": "Set parameters 111-113 to default.", + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Set parameters 111-113 to default.", + "description": "Set parameters 111-113 to default.", + "isFromConfig": true + } + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 255, + "propertyName": "Reset to default factory settings", + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1431655765, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "1": "Resets all configuration parameters to defaults", + "1431655765": "Reset to default factory settings and be excluded" + }, + "label": "Reset to default factory settings", + "isFromConfig": true + } + }, + { + "commandClassName": "Notification", + "commandClass": 113, + "endpoint": 0, + "property": "Home Security", + "propertyKey": "Cover status", + "propertyName": "Home Security", + "propertyKeyName": "Cover status", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Cover status", + "states": { + "0": "idle", + "3": "Tampering, product cover removed" + }, + "ccSpecific": { + "notificationType": 7 + } + }, + "value": 0 + }, + { + "commandClassName": "Notification", + "commandClass": 113, + "endpoint": 0, + "property": "Home Security", + "propertyKey": "Motion sensor status", + "propertyName": "Home Security", + "propertyKeyName": "Motion sensor status", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Motion sensor status", + "states": { + "0": "idle", + "8": "Motion detection" + }, + "ccSpecific": { + "notificationType": 7 + } + }, + "value": 8 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "manufacturerId", + "propertyName": "manufacturerId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + }, + "value": 134 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productType", + "propertyName": "productType", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + }, + "value": 258 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productId", + "propertyName": "productId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + }, + "value": 100 + }, + { + "commandClassName": "Battery", + "commandClass": 128, + "endpoint": 0, + "property": "level", + "propertyName": "level", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 100, + "unit": "%", + "label": "Battery level" + }, + "value": 100 + }, + { + "commandClassName": "Battery", + "commandClass": 128, + "endpoint": 0, + "property": "isLow", + "propertyName": "isLow", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + }, + "value": false + }, + { + "commandClassName": "Wake Up", + "commandClass": 132, + "endpoint": 0, + "property": "wakeUpInterval", + "propertyName": "wakeUpInterval", + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "min": 240, + "max": 3600, + "label": "Wake Up interval", + "steps": 60, + "default": 3600 + }, + "value": 3600 + }, + { + "commandClassName": "Wake Up", + "commandClass": 132, + "endpoint": 0, + "property": "controllerNodeId", + "propertyName": "controllerNodeId", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Node ID of the controller" + }, + "value": 1 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "libraryType", + "propertyName": "libraryType", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Libary type" + }, + "value": 3 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "protocolVersion", + "propertyName": "protocolVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.54" + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": [ + "1.12" + ] + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + } + ] +} From ab25c5a2bdb010fc9ae68bdc0eb5a96dc461e46d Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Mon, 11 Jan 2021 09:36:14 +0100 Subject: [PATCH 158/507] Shutdown asyncio http server within 10 seconds (#45033) If there are open requests, the http server waits up to 60 seconds. However, some requests (such as the Reboot button) seems to keep a connection open and needlessly slow down the reboot process: ``` Jan 11 00:52:54 homeassistant eb034fca9c7d[404]: 2021-01-11 00:52:54 DEBUG (MainThread) [homeassistant.core] Bus:Handling Jan 11 00:52:54 homeassistant eb034fca9c7d[404]: 2021-01-11 00:52:54 DEBUG (MainThread) [homeassistant.helpers.restore_state] Dumping states 111 Jan 11 00:52:54 homeassistant eb034fca9c7d[404]: 2021-01-11 00:52:54 DEBUG (MainThread) [homeassistant.helpers.restore_state] Dumping states Jan 11 00:52:54 homeassistant eb034fca9c7d[404]: 2021-01-11 00:52:54 INFO (MainThread) [homeassistant.components.websocket_api.http.connection] [281473359593728] Connection closed by client Jan 11 00:52:56 homeassistant eb034fca9c7d[404]: 2021-01-11 00:52:56 DEBUG (MainThread) [homeassistant.components.websocket_api.http.connection] [281473359593728] Disconnected Jan 11 00:53:54 homeassistant eb034fca9c7d[404]: 2021-01-11 00:53:54 DEBUG (MainThread) [homeassistant.core] Waited 60 seconds for task: .stop_server() running at /usr/src/homeassistant/homeassistant/components/http/__init__.py:228> wait_for=<_GatheringFuture pending cb=[()]> cb=[_wait.._on_completion() at /usr/local/lib/python3.8/asyncio/tasks.py:518]> ... ``` --- homeassistant/components/http/web_runner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/http/web_runner.py b/homeassistant/components/http/web_runner.py index 67621d63412..c30ba32b780 100644 --- a/homeassistant/components/http/web_runner.py +++ b/homeassistant/components/http/web_runner.py @@ -28,7 +28,7 @@ class HomeAssistantTCPSite(web.BaseSite): host: Union[None, str, List[str]], port: int, *, - shutdown_timeout: float = 60.0, + shutdown_timeout: float = 10.0, ssl_context: Optional[SSLContext] = None, backlog: int = 128, reuse_address: Optional[bool] = None, From 54064b4010369b85307f76bb0bdf26e8a9e21a0c Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Mon, 11 Jan 2021 11:24:02 +0100 Subject: [PATCH 159/507] Increase timeout to avoid killing the core during shutdown (#45029) Stopping the core goes through several stages, which can take up to 120s, 60s and 30s respectively. However, if shutdown is taking longer than 60s overall, s6 isn't patient and kills the core: Jan 10 23:56:58 homeassistant eb034fca9c7d[407]: s6-svwait: fatal: timed out Jan 10 23:56:58 homeassistant eb034fca9c7d[407]: [s6-finish] sending all processes the TERM signal. Jan 10 23:57:01 homeassistant eb034fca9c7d[407]: [s6-finish] sending all processes the KILL signal and exiting. This is most of the time not a problem since shutdown is quicker than that. However, increasing the timeout is especialy useful to debug cases when an event is hanging, since the core will point it out after its timeout elapsed. Set the timeout to 220s, which is all core timeouts plus 10s grace time. --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index cbcc948f5dc..6bcb080a06e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,9 @@ ARG BUILD_FROM FROM ${BUILD_FROM} +# Synchronize with homeassistant/core.py:async_stop ENV \ - S6_SERVICES_GRACETIME=60000 + S6_SERVICES_GRACETIME=220000 WORKDIR /usr/src From e584902b8bb969c742109b565a858f1f49c97cfe Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 11 Jan 2021 14:25:09 +0100 Subject: [PATCH 160/507] Remove empty schema (#45044) --- homeassistant/components/flo/__init__.py | 3 --- homeassistant/components/intent/__init__.py | 2 -- homeassistant/components/kulersky/__init__.py | 4 ---- homeassistant/components/lirc/__init__.py | 3 --- homeassistant/components/onvif/__init__.py | 3 --- homeassistant/components/ozw/__init__.py | 2 -- homeassistant/components/plugwise/gateway.py | 3 --- homeassistant/components/stream/__init__.py | 2 -- script/scaffold/templates/config_flow/integration/__init__.py | 4 ---- .../templates/config_flow_discovery/integration/__init__.py | 4 ---- 10 files changed, 30 deletions(-) diff --git a/homeassistant/components/flo/__init__.py b/homeassistant/components/flo/__init__.py index 14d00aa000a..b57cdd5f871 100644 --- a/homeassistant/components/flo/__init__.py +++ b/homeassistant/components/flo/__init__.py @@ -4,7 +4,6 @@ import logging from aioflo import async_get_api from aioflo.errors import RequestError -import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -15,8 +14,6 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CLIENT, DOMAIN from .device import FloDeviceDataUpdateCoordinator -CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) - _LOGGER = logging.getLogger(__name__) PLATFORMS = ["binary_sensor", "sensor", "switch"] diff --git a/homeassistant/components/intent/__init__.py b/homeassistant/components/intent/__init__.py index 8410303ac81..4fd6daa5102 100644 --- a/homeassistant/components/intent/__init__.py +++ b/homeassistant/components/intent/__init__.py @@ -9,8 +9,6 @@ from homeassistant.helpers import config_validation as cv, integration_platform, from .const import DOMAIN -CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) - async def async_setup(hass: HomeAssistant, config: dict): """Set up the Intent component.""" diff --git a/homeassistant/components/kulersky/__init__.py b/homeassistant/components/kulersky/__init__.py index ff984e2c0d3..9459d44805c 100644 --- a/homeassistant/components/kulersky/__init__.py +++ b/homeassistant/components/kulersky/__init__.py @@ -1,15 +1,11 @@ """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"] diff --git a/homeassistant/components/lirc/__init__.py b/homeassistant/components/lirc/__init__.py index bfc8e455624..bf939fb7535 100644 --- a/homeassistant/components/lirc/__init__.py +++ b/homeassistant/components/lirc/__init__.py @@ -5,7 +5,6 @@ import threading import time import lirc -import voluptuous as vol from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP @@ -19,8 +18,6 @@ EVENT_IR_COMMAND_RECEIVED = "ir_command_received" ICON = "mdi:remote" -CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) - def setup(hass, config): """Set up the LIRC capability.""" diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index c4fbdc3f40f..b332b7a795a 100644 --- a/homeassistant/components/onvif/__init__.py +++ b/homeassistant/components/onvif/__init__.py @@ -2,7 +2,6 @@ import asyncio from onvif.exceptions import ONVIFAuthError, ONVIFError, ONVIFTimeoutError -import voluptuous as vol from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -33,8 +32,6 @@ from .const import ( ) from .device import ONVIFDevice -CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) - async def async_setup(hass: HomeAssistant, config: dict): """Set up the ONVIF component.""" diff --git a/homeassistant/components/ozw/__init__.py b/homeassistant/components/ozw/__init__.py index e56e3deb066..0636671188d 100644 --- a/homeassistant/components/ozw/__init__.py +++ b/homeassistant/components/ozw/__init__.py @@ -18,7 +18,6 @@ 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 @@ -52,7 +51,6 @@ from .websocket_api import async_register_api _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" diff --git a/homeassistant/components/plugwise/gateway.py b/homeassistant/components/plugwise/gateway.py index 3b61bd3930d..a0bf23986bd 100644 --- a/homeassistant/components/plugwise/gateway.py +++ b/homeassistant/components/plugwise/gateway.py @@ -12,7 +12,6 @@ from plugwise.exceptions import ( XMLDataMissingError, ) from plugwise.smile import Smile -import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -46,8 +45,6 @@ from .const import ( UNDO_UPDATE_LISTENER, ) -CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 2b242389ef0..c7d1dad4835 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -27,8 +27,6 @@ from .hls import async_setup_hls _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) - STREAM_SERVICE_SCHEMA = vol.Schema({vol.Required(CONF_STREAM_SOURCE): cv.string}) SERVICE_RECORD_SCHEMA = STREAM_SERVICE_SCHEMA.extend( diff --git a/script/scaffold/templates/config_flow/integration/__init__.py b/script/scaffold/templates/config_flow/integration/__init__.py index 4a206981c3c..c6df6e99979 100644 --- a/script/scaffold/templates/config_flow/integration/__init__.py +++ b/script/scaffold/templates/config_flow/integration/__init__.py @@ -1,15 +1,11 @@ """The NEW_NAME 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) - # TODO List the platforms that you want to support. # For your initial PR, limit it to 1 platform. PLATFORMS = ["light"] diff --git a/script/scaffold/templates/config_flow_discovery/integration/__init__.py b/script/scaffold/templates/config_flow_discovery/integration/__init__.py index 4a206981c3c..c6df6e99979 100644 --- a/script/scaffold/templates/config_flow_discovery/integration/__init__.py +++ b/script/scaffold/templates/config_flow_discovery/integration/__init__.py @@ -1,15 +1,11 @@ """The NEW_NAME 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) - # TODO List the platforms that you want to support. # For your initial PR, limit it to 1 platform. PLATFORMS = ["light"] From af21893652215c8f084e5a08a266aab6a7978c81 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 11 Jan 2021 03:26:09 -1000 Subject: [PATCH 161/507] Remove safe mode from HomeKit (#45028) Safe mode was added to work around a race condition where the mdns announcment was sent too early and would cause pairing to fail. Since this has been corrected in HAP-python, there is no longer a need to have safe mode. --- homeassistant/components/homekit/__init__.py | 9 +- .../components/homekit/config_flow.py | 48 ++++------ homeassistant/components/homekit/strings.json | 3 +- .../components/homekit/translations/en.json | 3 +- tests/components/homekit/test_config_flow.py | 89 ++---------------- tests/components/homekit/test_homekit.py | 94 +++++-------------- 6 files changed, 52 insertions(+), 194 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 2b11aee0bbd..53fbd7cf8f1 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -114,6 +114,7 @@ def _has_all_unique_names_and_ports(bridges): BRIDGE_SCHEMA = vol.All( cv.deprecated(CONF_ZEROCONF_DEFAULT_INTERFACE), + cv.deprecated(CONF_SAFE_MODE), vol.Schema( { vol.Optional(CONF_HOMEKIT_MODE, default=DEFAULT_HOMEKIT_MODE): vol.In( @@ -246,7 +247,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): homekit_mode = options.get(CONF_HOMEKIT_MODE, DEFAULT_HOMEKIT_MODE) entity_config = options.get(CONF_ENTITY_CONFIG, {}).copy() auto_start = options.get(CONF_AUTO_START, DEFAULT_AUTO_START) - safe_mode = options.get(CONF_SAFE_MODE, DEFAULT_SAFE_MODE) entity_filter = FILTER_SCHEMA(options.get(CONF_FILTER, {})) homekit = HomeKit( @@ -256,7 +256,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ip_address, entity_filter, entity_config, - safe_mode, homekit_mode, advertise_ip, entry.entry_id, @@ -421,7 +420,6 @@ class HomeKit: ip_address, entity_filter, entity_config, - safe_mode, homekit_mode, advertise_ip=None, entry_id=None, @@ -433,7 +431,6 @@ class HomeKit: self._ip_address = ip_address self._filter = entity_filter self._config = entity_config - self._safe_mode = safe_mode self._advertise_ip = advertise_ip self._entry_id = entry_id self._homekit_mode = homekit_mode @@ -470,10 +467,6 @@ class HomeKit: else: self.driver.persist() - if self._safe_mode: - _LOGGER.debug("Safe_mode selected for %s", self._name) - self.driver.safe_mode = True - def reset_accessories(self, entity_ids): """Reset the accessory to load the latest configuration.""" if not self.bridge: diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index 9d50e62fcd1..8d763581615 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -21,12 +21,10 @@ from .const import ( CONF_ENTITY_CONFIG, CONF_FILTER, CONF_HOMEKIT_MODE, - CONF_SAFE_MODE, CONF_VIDEO_CODEC, DEFAULT_AUTO_START, DEFAULT_CONFIG_FLOW_PORT, DEFAULT_HOMEKIT_MODE, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_ACCESSORY, HOMEKIT_MODES, SHORT_BRIDGE_NAME, @@ -217,40 +215,32 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_advanced(self, user_input=None): """Choose advanced options.""" - if user_input is not None: - self.homekit_options.update(user_input) - for key in (CONF_DOMAINS, CONF_ENTITIES): - if key in self.homekit_options: - del self.homekit_options[key] - return self.async_create_entry(title="", data=self.homekit_options) + if not self.show_advanced_options or user_input is not None: + if user_input: + self.homekit_options.update(user_input) - schema_base = {} - - if self.show_advanced_options: - schema_base[ - vol.Optional( - CONF_AUTO_START, - default=self.homekit_options.get( - CONF_AUTO_START, DEFAULT_AUTO_START - ), - ) - ] = bool - else: self.homekit_options[CONF_AUTO_START] = self.homekit_options.get( CONF_AUTO_START, DEFAULT_AUTO_START ) - schema_base.update( - { - vol.Optional( - CONF_SAFE_MODE, - default=self.homekit_options.get(CONF_SAFE_MODE, DEFAULT_SAFE_MODE), - ): bool - } - ) + for key in (CONF_DOMAINS, CONF_ENTITIES): + if key in self.homekit_options: + del self.homekit_options[key] + + return self.async_create_entry(title="", data=self.homekit_options) return self.async_show_form( - step_id="advanced", data_schema=vol.Schema(schema_base) + step_id="advanced", + data_schema=vol.Schema( + { + vol.Optional( + CONF_AUTO_START, + default=self.homekit_options.get( + CONF_AUTO_START, DEFAULT_AUTO_START + ), + ): bool + } + ), ) async def async_step_cameras(self, user_input=None): diff --git a/homeassistant/components/homekit/strings.json b/homeassistant/components/homekit/strings.json index 9e3413e1d20..5ba578f38c3 100644 --- a/homeassistant/components/homekit/strings.json +++ b/homeassistant/components/homekit/strings.json @@ -30,8 +30,7 @@ }, "advanced": { "data": { - "auto_start": "Autostart (disable if you are calling the homekit.start service manually)", - "safe_mode": "Safe Mode (enable only if pairing fails)" + "auto_start": "Autostart (disable if you are calling the homekit.start service manually)" }, "description": "These settings only need to be adjusted if HomeKit is not functional.", "title": "Advanced Configuration" diff --git a/homeassistant/components/homekit/translations/en.json b/homeassistant/components/homekit/translations/en.json index 9e3413e1d20..5ba578f38c3 100644 --- a/homeassistant/components/homekit/translations/en.json +++ b/homeassistant/components/homekit/translations/en.json @@ -30,8 +30,7 @@ }, "advanced": { "data": { - "auto_start": "Autostart (disable if you are calling the homekit.start service manually)", - "safe_mode": "Safe Mode (enable only if pairing fails)" + "auto_start": "Autostart (disable if you are calling the homekit.start service manually)" }, "description": "These settings only need to be adjusted if HomeKit is not functional.", "title": "Advanced Configuration" diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index a32de91cebd..4438404af2e 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -28,7 +28,6 @@ def _mock_config_entry_with_options_populated(): ], "exclude_entities": ["climate.front_gate"], }, - "safe_mode": False, }, ) @@ -159,7 +158,7 @@ async def test_options_flow_exclude_mode_advanced(auto_start, hass): with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): result3 = await hass.config_entries.options.async_configure( result2["flow_id"], - user_input={"auto_start": auto_start, "safe_mode": True}, + user_input={"auto_start": auto_start}, ) assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -172,7 +171,6 @@ async def test_options_flow_exclude_mode_advanced(auto_start, hass): "include_domains": ["fan", "vacuum", "climate", "humidifier"], "include_entities": [], }, - "safe_mode": True, } @@ -204,16 +202,7 @@ async def test_options_flow_exclude_mode_basic(hass): result["flow_id"], user_input={"entities": ["climate.old"], "include_exclude_mode": "exclude"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["step_id"] == "advanced" - - with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): - result3 = await hass.config_entries.options.async_configure( - result2["flow_id"], - user_input={"safe_mode": True}, - ) - - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { "auto_start": True, "mode": "bridge", @@ -223,7 +212,6 @@ async def test_options_flow_exclude_mode_basic(hass): "include_domains": ["fan", "vacuum", "climate"], "include_entities": [], }, - "safe_mode": True, } @@ -257,16 +245,7 @@ async def test_options_flow_include_mode_basic(hass): result["flow_id"], user_input={"entities": ["climate.new"], "include_exclude_mode": "include"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["step_id"] == "advanced" - - with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): - result3 = await hass.config_entries.options.async_configure( - result2["flow_id"], - user_input={"safe_mode": True}, - ) - - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { "auto_start": True, "mode": "bridge", @@ -276,7 +255,6 @@ async def test_options_flow_include_mode_basic(hass): "include_domains": ["fan", "vacuum"], "include_entities": ["climate.new"], }, - "safe_mode": True, } @@ -323,16 +301,7 @@ async def test_options_flow_exclude_mode_with_cameras(hass): user_input={"camera_copy": ["camera.native_h264"]}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result3["step_id"] == "advanced" - - with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): - result4 = await hass.config_entries.options.async_configure( - result3["flow_id"], - user_input={"safe_mode": True}, - ) - - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { "auto_start": True, "mode": "bridge", @@ -343,7 +312,6 @@ async def test_options_flow_exclude_mode_with_cameras(hass): "include_entities": [], }, "entity_config": {"camera.native_h264": {"video_codec": "copy"}}, - "safe_mode": True, } # Now run though again and verify we can turn off copy @@ -378,16 +346,7 @@ async def test_options_flow_exclude_mode_with_cameras(hass): user_input={"camera_copy": []}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result3["step_id"] == "advanced" - - with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): - result4 = await hass.config_entries.options.async_configure( - result3["flow_id"], - user_input={"safe_mode": True}, - ) - - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { "auto_start": True, "mode": "bridge", @@ -398,7 +357,6 @@ async def test_options_flow_exclude_mode_with_cameras(hass): "include_entities": [], }, "entity_config": {"camera.native_h264": {}}, - "safe_mode": True, } @@ -445,16 +403,7 @@ async def test_options_flow_include_mode_with_cameras(hass): user_input={"camera_copy": ["camera.native_h264"]}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result3["step_id"] == "advanced" - - with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): - result4 = await hass.config_entries.options.async_configure( - result3["flow_id"], - user_input={"safe_mode": True}, - ) - - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { "auto_start": True, "mode": "bridge", @@ -465,7 +414,6 @@ async def test_options_flow_include_mode_with_cameras(hass): "include_entities": ["camera.native_h264", "camera.transcode_h264"], }, "entity_config": {"camera.native_h264": {"video_codec": "copy"}}, - "safe_mode": True, } # Now run though again and verify we can turn off copy @@ -500,16 +448,7 @@ async def test_options_flow_include_mode_with_cameras(hass): user_input={"camera_copy": []}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result3["step_id"] == "advanced" - - with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): - result4 = await hass.config_entries.options.async_configure( - result3["flow_id"], - user_input={"safe_mode": True}, - ) - - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { "auto_start": True, "mode": "bridge", @@ -520,7 +459,6 @@ async def test_options_flow_include_mode_with_cameras(hass): "include_entities": [], }, "entity_config": {"camera.native_h264": {}}, - "safe_mode": True, } @@ -543,7 +481,6 @@ async def test_options_flow_blocked_when_from_yaml(hass): ], "exclude_entities": ["climate.front_gate"], }, - "safe_mode": False, }, source=SOURCE_IMPORT, ) @@ -594,16 +531,7 @@ async def test_options_flow_include_mode_basic_accessory(hass): result["flow_id"], user_input={"entities": "media_player.tv"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["step_id"] == "advanced" - - with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): - result3 = await hass.config_entries.options.async_configure( - result2["flow_id"], - user_input={"safe_mode": False}, - ) - - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { "auto_start": True, "mode": "accessory", @@ -613,5 +541,4 @@ async def test_options_flow_include_mode_basic_accessory(hass): "include_domains": [], "include_entities": ["media_player.tv"], }, - "safe_mode": False, } diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index a3f6c1f57d2..c6f897c32a2 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -28,9 +28,7 @@ from homeassistant.components.homekit.const import ( BRIDGE_SERIAL_NUMBER, CONF_AUTO_START, CONF_ENTRY_INDEX, - CONF_SAFE_MODE, DEFAULT_PORT, - DEFAULT_SAFE_MODE, DOMAIN, HOMEKIT, HOMEKIT_FILE, @@ -99,7 +97,7 @@ def debounce_patcher_fixture(): patcher.stop() -async def test_setup_min(hass): +async def test_setup_min(hass, mock_zeroconf): """Test async_setup with min config options.""" entry = MockConfigEntry( domain=DOMAIN, @@ -121,7 +119,6 @@ async def test_setup_min(hass): None, ANY, {}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, None, entry.entry_id, @@ -136,12 +133,12 @@ async def test_setup_min(hass): mock_homekit().async_start.assert_called() -async def test_setup_auto_start_disabled(hass): +async def test_setup_auto_start_disabled(hass, mock_zeroconf): """Test async_setup with auto start disabled and test service calls.""" entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "Test Name", CONF_PORT: 11111, CONF_IP_ADDRESS: "172.0.0.0"}, - options={CONF_AUTO_START: False, CONF_SAFE_MODE: DEFAULT_SAFE_MODE}, + options={CONF_AUTO_START: False}, ) entry.add_to_hass(hass) @@ -158,7 +155,6 @@ async def test_setup_auto_start_disabled(hass): "172.0.0.0", ANY, {}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, None, entry.entry_id, @@ -191,7 +187,7 @@ async def test_setup_auto_start_disabled(hass): assert homekit.async_start.called is False -async def test_homekit_setup(hass, hk_driver): +async def test_homekit_setup(hass, hk_driver, mock_zeroconf): """Test setup of bridge and driver.""" entry = MockConfigEntry( domain=DOMAIN, @@ -205,7 +201,6 @@ async def test_homekit_setup(hass, hk_driver): None, {}, {}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, @@ -238,7 +233,7 @@ async def test_homekit_setup(hass, hk_driver): assert hass.bus.async_listeners().get(EVENT_HOMEASSISTANT_STOP) == 1 -async def test_homekit_setup_ip_address(hass, hk_driver): +async def test_homekit_setup_ip_address(hass, hk_driver, mock_zeroconf): """Test setup with given IP address.""" entry = MockConfigEntry( domain=DOMAIN, @@ -252,7 +247,6 @@ async def test_homekit_setup_ip_address(hass, hk_driver): "172.0.0.0", {}, {}, - None, HOMEKIT_MODE_BRIDGE, None, entry_id=entry.entry_id, @@ -277,7 +271,7 @@ async def test_homekit_setup_ip_address(hass, hk_driver): ) -async def test_homekit_setup_advertise_ip(hass, hk_driver): +async def test_homekit_setup_advertise_ip(hass, hk_driver, mock_zeroconf): """Test setup with given IP address to advertise.""" entry = MockConfigEntry( domain=DOMAIN, @@ -291,7 +285,6 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver): "0.0.0.0", {}, {}, - None, HOMEKIT_MODE_BRIDGE, "192.168.1.100", entry_id=entry.entry_id, @@ -316,31 +309,6 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver): ) -async def test_homekit_setup_safe_mode(hass, hk_driver): - """Test if safe_mode flag is set.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={CONF_NAME: "mock_name", CONF_PORT: 12345}, - source=SOURCE_IMPORT, - ) - homekit = HomeKit( - hass, - BRIDGE_NAME, - DEFAULT_PORT, - None, - {}, - {}, - True, - HOMEKIT_MODE_BRIDGE, - advertise_ip=None, - entry_id=entry.entry_id, - ) - - with patch(f"{PATH_HOMEKIT}.accessories.HomeDriver", return_value=hk_driver): - await hass.async_add_executor_job(homekit.setup, MagicMock()) - assert homekit.driver.safe_mode is True - - async def test_homekit_add_accessory(hass, mock_zeroconf): """Add accessory if config exists and get_acc returns an accessory.""" entry = await async_init_integration(hass) @@ -352,7 +320,6 @@ async def test_homekit_add_accessory(hass, mock_zeroconf): None, lambda entity_id: True, {}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, @@ -394,7 +361,6 @@ async def test_homekit_warn_add_accessory_bridge( None, lambda entity_id: True, {}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, @@ -431,7 +397,6 @@ async def test_homekit_remove_accessory(hass, mock_zeroconf): None, lambda entity_id: True, {}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, @@ -445,7 +410,7 @@ async def test_homekit_remove_accessory(hass, mock_zeroconf): assert len(mock_bridge.accessories) == 0 -async def test_homekit_entity_filter(hass): +async def test_homekit_entity_filter(hass, mock_zeroconf): """Test the entity filter.""" entry = await async_init_integration(hass) @@ -457,7 +422,6 @@ async def test_homekit_entity_filter(hass): None, entity_filter, {}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, @@ -480,7 +444,7 @@ async def test_homekit_entity_filter(hass): assert mock_get_acc.called is False -async def test_homekit_entity_glob_filter(hass): +async def test_homekit_entity_glob_filter(hass, mock_zeroconf): """Test the entity filter.""" entry = await async_init_integration(hass) @@ -494,7 +458,6 @@ async def test_homekit_entity_glob_filter(hass): None, entity_filter, {}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, @@ -534,7 +497,6 @@ async def test_homekit_start(hass, hk_driver, device_reg, debounce_patcher): None, {}, {}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, @@ -611,7 +573,9 @@ async def test_homekit_start(hass, hk_driver, device_reg, debounce_patcher): assert len(device_reg.devices) == 1 -async def test_homekit_start_with_a_broken_accessory(hass, hk_driver, debounce_patcher): +async def test_homekit_start_with_a_broken_accessory( + hass, hk_driver, debounce_patcher, mock_zeroconf +): """Test HomeKit start method.""" pin = b"123-45-678" entry = MockConfigEntry( @@ -627,7 +591,6 @@ async def test_homekit_start_with_a_broken_accessory(hass, hk_driver, debounce_p None, entity_filter, {}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, @@ -674,7 +637,6 @@ async def test_homekit_stop(hass): None, {}, {}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, @@ -702,7 +664,7 @@ async def test_homekit_stop(hass): assert homekit.driver.async_stop.called is True -async def test_homekit_reset_accessories(hass): +async def test_homekit_reset_accessories(hass, mock_zeroconf): """Test adding too many accessories to HomeKit.""" entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} @@ -715,7 +677,6 @@ async def test_homekit_reset_accessories(hass): None, {}, {entity_id: {}}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, @@ -751,7 +712,7 @@ async def test_homekit_reset_accessories(hass): homekit.status = STATUS_READY -async def test_homekit_too_many_accessories(hass, hk_driver, caplog): +async def test_homekit_too_many_accessories(hass, hk_driver, caplog, mock_zeroconf): """Test adding too many accessories to HomeKit.""" entry = await async_init_integration(hass) @@ -764,7 +725,6 @@ async def test_homekit_too_many_accessories(hass, hk_driver, caplog): None, entity_filter, {}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, @@ -794,7 +754,7 @@ async def test_homekit_too_many_accessories(hass, hk_driver, caplog): async def test_homekit_finds_linked_batteries( - hass, hk_driver, debounce_patcher, device_reg, entity_reg + hass, hk_driver, debounce_patcher, device_reg, entity_reg, mock_zeroconf ): """Test HomeKit start method.""" entry = await async_init_integration(hass) @@ -806,7 +766,6 @@ async def test_homekit_finds_linked_batteries( None, {}, {"light.demo": {}}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, @@ -881,7 +840,7 @@ async def test_homekit_finds_linked_batteries( async def test_homekit_async_get_integration_fails( - hass, hk_driver, debounce_patcher, device_reg, entity_reg + hass, hk_driver, debounce_patcher, device_reg, entity_reg, mock_zeroconf ): """Test that we continue if async_get_integration fails.""" entry = await async_init_integration(hass) @@ -893,7 +852,6 @@ async def test_homekit_async_get_integration_fails( None, {}, {"light.demo": {}}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, @@ -966,7 +924,7 @@ async def test_homekit_async_get_integration_fails( ) -async def test_setup_imported(hass): +async def test_setup_imported(hass, mock_zeroconf): """Test async_setup with imported config options.""" legacy_persist_file_path = hass.config.path(HOMEKIT_FILE) legacy_aid_storage_path = hass.config.path(STORAGE_DIR, "homekit.aids") @@ -1000,7 +958,6 @@ async def test_setup_imported(hass): None, ANY, {}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, None, entry.entry_id, @@ -1030,7 +987,7 @@ async def test_setup_imported(hass): os.unlink(migrated_aid_file_path) -async def test_yaml_updates_update_config_entry_for_name(hass): +async def test_yaml_updates_update_config_entry_for_name(hass, mock_zeroconf): """Test async_setup with imported config.""" entry = MockConfigEntry( domain=DOMAIN, @@ -1055,7 +1012,6 @@ async def test_yaml_updates_update_config_entry_for_name(hass): None, ANY, {}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, None, entry.entry_id, @@ -1070,7 +1026,7 @@ async def test_yaml_updates_update_config_entry_for_name(hass): mock_homekit().async_start.assert_called() -async def test_raise_config_entry_not_ready(hass): +async def test_raise_config_entry_not_ready(hass, mock_zeroconf): """Test async_setup when the port is not available.""" entry = MockConfigEntry( domain=DOMAIN, @@ -1116,7 +1072,7 @@ def _write_data(path: str, data: Dict) -> None: async def test_homekit_ignored_missing_devices( - hass, hk_driver, debounce_patcher, device_reg, entity_reg + hass, hk_driver, debounce_patcher, device_reg, entity_reg, mock_zeroconf ): """Test HomeKit handles a device in the entity registry but missing from the device registry.""" entry = await async_init_integration(hass) @@ -1128,7 +1084,6 @@ async def test_homekit_ignored_missing_devices( None, {}, {"light.demo": {}}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, @@ -1198,7 +1153,7 @@ async def test_homekit_ignored_missing_devices( async def test_homekit_finds_linked_motion_sensors( - hass, hk_driver, debounce_patcher, device_reg, entity_reg + hass, hk_driver, debounce_patcher, device_reg, entity_reg, mock_zeroconf ): """Test HomeKit start method.""" entry = await async_init_integration(hass) @@ -1210,7 +1165,6 @@ async def test_homekit_finds_linked_motion_sensors( None, {}, {"camera.camera_demo": {}}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, @@ -1274,7 +1228,7 @@ async def test_homekit_finds_linked_motion_sensors( async def test_homekit_finds_linked_humidity_sensors( - hass, hk_driver, debounce_patcher, device_reg, entity_reg + hass, hk_driver, debounce_patcher, device_reg, entity_reg, mock_zeroconf ): """Test HomeKit start method.""" entry = await async_init_integration(hass) @@ -1286,7 +1240,6 @@ async def test_homekit_finds_linked_humidity_sensors( None, {}, {"humidifier.humidifier": {}}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, @@ -1351,7 +1304,7 @@ async def test_homekit_finds_linked_humidity_sensors( ) -async def test_reload(hass): +async def test_reload(hass, mock_zeroconf): """Test we can reload from yaml.""" entry = MockConfigEntry( domain=DOMAIN, @@ -1376,7 +1329,6 @@ async def test_reload(hass): None, ANY, {}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, None, entry.entry_id, @@ -1413,7 +1365,6 @@ async def test_reload(hass): None, ANY, {}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, None, entry.entry_id, @@ -1439,7 +1390,6 @@ async def test_homekit_start_in_accessory_mode( None, {}, {}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_ACCESSORY, advertise_ip=None, entry_id=entry.entry_id, From ed4e8cdbc5f046a3fff052844a9bd8b427b0e4b8 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Mon, 11 Jan 2021 08:34:08 -0500 Subject: [PATCH 162/507] Bump zwave-js-server-python to 0.7.0 (#45045) --- homeassistant/components/zwave_js/manifest.json | 10 +++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 718a4da2b85..ef48b5bfd12 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,6 +3,10 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.6.0"], - "codeowners": ["@home-assistant/z-wave"] -} + "requirements": [ + "zwave-js-server-python==0.7.0" + ], + "codeowners": [ + "@home-assistant/z-wave" + ] +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 40b29e579d5..16941bda7ce 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2372,4 +2372,4 @@ zigpy==0.29.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.6.0 +zwave-js-server-python==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b6a004aed2c..b3908f59372 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1171,4 +1171,4 @@ zigpy-znp==0.3.0 zigpy==0.29.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.6.0 +zwave-js-server-python==0.7.0 From 65e3661f88836e74f20f67874131ad05419f0b45 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 11 Jan 2021 05:34:45 -0800 Subject: [PATCH 163/507] Repair flaky and broken stream tests in test_hls.py, and turn back on (#45025) * Unmark tests as flaky (though still flaky) This put tests into the broken state where they are flaky and do not yet pass * Fix bug in test_hls_stream with incorrect path * Enable and de-flake HLS stream tests Background: Tests encode a fake video them start a stream to be decoded. Test assert on the decoded segments, however there is a race with the stream worker which can finish decoding first, and end the stream which ereases all buffers. Breadown of fixes: - Fix the race conditions by adding synchronization points right before the stream is finalized. - Refactor StreamOutput.put so that a patch() can block the worker thread. Previously, the put call would happen in the event loop which was not safe to block. This is a bit of a hack, but it is the simplist possible code change to add this synchronization and arguably provides slightly better separation of responsibilities from the worker anyway. - Fix bugs in the tests that make them not pass, likely due to changes introduced while the tests were disabled - Fix case where the HLS stream view recv() call returns None, indicating the worker finished while the request was waiting. The tests were previously failing anywhere from 2-5% of the time on a lightly loaded machine doing 1k iterations. Now, have 0% flake rate. Tested with: $ py.test --count=1000 tests/components/strema/test_hls.py --- homeassistant/components/stream/core.py | 6 +- homeassistant/components/stream/hls.py | 6 +- homeassistant/components/stream/worker.py | 5 +- tests/components/stream/test_hls.py | 78 ++++++++++++++++++++--- 4 files changed, 81 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 20931abf11e..5158ba185b1 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -117,9 +117,13 @@ class StreamOutput: self._cursor = segment.sequence return segment - @callback def put(self, segment: Segment) -> None: """Store output.""" + self._stream.hass.loop.call_soon_threadsafe(self._async_put, segment) + + @callback + def _async_put(self, segment: Segment) -> None: + """Store output from event loop.""" # Start idle timeout when we start receiving data if self._unsub is None: self._unsub = async_call_later( diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index 92801c4807f..2b305442b80 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -52,7 +52,8 @@ class HlsMasterPlaylistView(StreamView): stream.start() # Wait for a segment to be ready if not track.segments: - await track.recv() + if not await track.recv(): + return web.HTTPNotFound() headers = {"Content-Type": FORMAT_CONTENT_TYPE["hls"]} return web.Response(body=self.render(track).encode("utf-8"), headers=headers) @@ -105,7 +106,8 @@ class HlsPlaylistView(StreamView): stream.start() # Wait for a segment to be ready if not track.segments: - await track.recv() + if not await track.recv(): + return web.HTTPNotFound() headers = {"Content-Type": FORMAT_CONTENT_TYPE["hls"]} return web.Response(body=self.render(track).encode("utf-8"), headers=headers) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 68cbbc79726..cccbfd1b48b 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -224,7 +224,7 @@ def _stream_worker_internal(hass, stream, quit_event): if not stream.keepalive: # End of stream, clear listeners and stop thread for fmt in stream.outputs: - hass.loop.call_soon_threadsafe(stream.outputs[fmt].put, None) + stream.outputs[fmt].put(None) if not peek_first_pts(): container.close() @@ -275,8 +275,7 @@ def _stream_worker_internal(hass, stream, quit_event): for fmt, (buffer, _) in outputs.items(): buffer.output.close() if stream.outputs.get(fmt): - hass.loop.call_soon_threadsafe( - stream.outputs[fmt].put, + stream.outputs[fmt].put( Segment( sequence, buffer.segment, diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index 3af48cb580d..e19a3b96687 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -1,5 +1,16 @@ -"""The tests for hls streams.""" +"""The tests for hls streams. + +The tests encode stream (as an h264 video), then load the stream and verify +that it is decoded properly. The background worker thread responsible for +decoding will decode the stream as fast as possible, and when completed +clears all output buffers. This can be a problem for the test that wishes +to retrieve and verify decoded segments. If the worker finishes first, there is +nothing for the test to verify. The solution is the WorkerSync class that +allows the tests to pause the worker thread before finalizing the stream +so that it can inspect the output. +""" from datetime import timedelta +import threading from unittest.mock import patch from urllib.parse import urlparse @@ -7,6 +18,7 @@ import av import pytest from homeassistant.components.stream import request_stream +from homeassistant.components.stream.core import Segment, StreamOutput from homeassistant.const import HTTP_NOT_FOUND from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -15,8 +27,47 @@ from tests.common import async_fire_time_changed from tests.components.stream.common import generate_h264_video, preload_stream -@pytest.mark.skip("Flaky in CI") -async def test_hls_stream(hass, hass_client): +class WorkerSync: + """Test fixture that intercepts stream worker calls to StreamOutput.""" + + def __init__(self): + """Initialize WorkerSync.""" + self._event = None + self._put_original = StreamOutput.put + + def pause(self): + """Pause the worker before it finalizes the stream.""" + self._event = threading.Event() + + def resume(self): + """Allow the worker thread to finalize the stream.""" + self._event.set() + + def blocking_put(self, stream_output: StreamOutput, segment: Segment): + """Proxy StreamOutput.put, intercepted for test to pause worker.""" + if segment is None and self._event: + # Worker is ending the stream, which clears all output buffers. + # Block the worker thread until the test has a chance to verify + # the segments under test. + self._event.wait() + + # Forward to actual StreamOutput.put + self._put_original(stream_output, segment) + + +@pytest.fixture() +def worker_sync(hass): + """Patch StreamOutput to allow test to synchronize worker stream end.""" + sync = WorkerSync() + with patch( + "homeassistant.components.stream.core.StreamOutput.put", + side_effect=sync.blocking_put, + autospec=True, + ): + yield sync + + +async def test_hls_stream(hass, hass_client, worker_sync): """ Test hls stream. @@ -25,6 +76,8 @@ async def test_hls_stream(hass, hass_client): """ await async_setup_component(hass, "stream", {"stream": {}}) + worker_sync.pause() + # Setup demo HLS track source = generate_h264_video() stream = preload_stream(hass, source) @@ -50,10 +103,12 @@ async def test_hls_stream(hass, hass_client): # Fetch segment playlist = await playlist_response.text() playlist_url = "/".join(parsed_url.path.split("/")[:-1]) - segment_url = playlist_url + playlist.splitlines()[-1][1:] + segment_url = playlist_url + "/" + playlist.splitlines()[-1] segment_response = await http_client.get(segment_url) assert segment_response.status == 200 + worker_sync.resume() + # Stop stream, if it hasn't quit already stream.stop() @@ -62,11 +117,12 @@ async def test_hls_stream(hass, hass_client): assert fail_response.status == HTTP_NOT_FOUND -@pytest.mark.skip("Flaky in CI") -async def test_stream_timeout(hass, hass_client): +async def test_stream_timeout(hass, hass_client, worker_sync): """Test hls stream timeout.""" await async_setup_component(hass, "stream", {"stream": {}}) + worker_sync.pause() + # Setup demo HLS track source = generate_h264_video() stream = preload_stream(hass, source) @@ -90,6 +146,8 @@ async def test_stream_timeout(hass, hass_client): playlist_response = await http_client.get(parsed_url.path) assert playlist_response.status == 200 + worker_sync.resume() + # Wait 5 minutes future = dt_util.utcnow() + timedelta(minutes=5) async_fire_time_changed(hass, future) @@ -99,11 +157,12 @@ async def test_stream_timeout(hass, hass_client): assert fail_response.status == HTTP_NOT_FOUND -@pytest.mark.skip("Flaky in CI") -async def test_stream_ended(hass): +async def test_stream_ended(hass, worker_sync): """Test hls stream packets ended.""" await async_setup_component(hass, "stream", {"stream": {}}) + worker_sync.pause() + # Setup demo HLS track source = generate_h264_video() stream = preload_stream(hass, source) @@ -118,6 +177,9 @@ async def test_stream_ended(hass): if segment is None: break segments = segment.sequence + # Allow worker to finalize once enough of the stream is been consumed + if segments > 1: + worker_sync.resume() assert segments > 1 assert not track.get_segment() From 38a5f25b595f92a7327b2d1ea339eea8bcb4fd40 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 11 Jan 2021 03:38:42 -1000 Subject: [PATCH 164/507] Fix vacuums that do not support start with homekit (#45030) * Fix vacuums that do not support start with homekit * fix tests --- .../components/homekit/accessories.py | 8 +- .../components/homekit/type_switches.py | 21 +++-- .../homekit/test_get_accessories.py | 4 +- .../components/homekit/test_type_switches.py | 87 ++++++++++++++++--- 4 files changed, 95 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 6dc2e2364b6..51b6508149b 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -8,7 +8,7 @@ from pyhap.accessory import Accessory, Bridge from pyhap.accessory_driver import AccessoryDriver from pyhap.const import CATEGORY_OTHER -from homeassistant.components import cover, vacuum +from homeassistant.components import cover from homeassistant.components.cover import ( DEVICE_CLASS_GARAGE, DEVICE_CLASS_GATE, @@ -215,11 +215,7 @@ def get_accessory(hass, driver, state, aid, config): a_type = SWITCH_TYPES[switch_type] elif state.domain == "vacuum": - features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - if features & (vacuum.SUPPORT_START | vacuum.SUPPORT_RETURN_HOME): - a_type = "DockVacuum" - else: - a_type = "Switch" + a_type = "Vacuum" elif state.domain in ("automation", "input_boolean", "remote", "scene", "script"): a_type = "Switch" diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index beaccd1f3dc..b3ee8a06497 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -15,9 +15,12 @@ from homeassistant.components.vacuum import ( SERVICE_RETURN_TO_BASE, SERVICE_START, STATE_CLEANING, + SUPPORT_RETURN_HOME, + SUPPORT_START, ) from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, CONF_TYPE, SERVICE_TURN_OFF, SERVICE_TURN_ON, @@ -149,16 +152,24 @@ class Switch(HomeAccessory): self.char_on.set_value(current_state) -@TYPES.register("DockVacuum") -class DockVacuum(Switch): +@TYPES.register("Vacuum") +class Vacuum(Switch): """Generate a Switch accessory.""" def set_state(self, value): """Move switch state to value if call came from HomeKit.""" _LOGGER.debug("%s: Set switch state to %s", self.entity_id, value) - params = {ATTR_ENTITY_ID: self.entity_id} - service = SERVICE_START if value else SERVICE_RETURN_TO_BASE - self.call_service(VACUUM_DOMAIN, service, params) + state = self.hass.states.get(self.entity_id) + features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + + if value: + sup_start = features & SUPPORT_START + service = SERVICE_START if sup_start else SERVICE_TURN_ON + else: + sup_return_home = features & SUPPORT_RETURN_HOME + service = SERVICE_RETURN_TO_BASE if sup_return_home else SERVICE_TURN_OFF + + self.call_service(VACUUM_DOMAIN, service, {ATTR_ENTITY_ID: self.entity_id}) @callback def async_update_state(self, new_state): diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 314d0516223..70d59408011 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -257,7 +257,7 @@ def test_type_switches(type_name, entity_id, state, attrs, config): "type_name, entity_id, state, attrs", [ ( - "DockVacuum", + "Vacuum", "vacuum.dock_vacuum", "docked", { @@ -265,7 +265,7 @@ def test_type_switches(type_name, entity_id, state, attrs, config): | vacuum.SUPPORT_RETURN_HOME }, ), - ("Switch", "vacuum.basic_vacuum", "off", {}), + ("Vacuum", "vacuum.basic_vacuum", "off", {}), ], ) def test_type_vacuum(type_name, entity_id, state, attrs): diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 4f36adc99e1..5d218a6ef8a 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -10,20 +10,25 @@ from homeassistant.components.homekit.const import ( TYPE_SPRINKLER, TYPE_VALVE, ) -from homeassistant.components.homekit.type_switches import ( - DockVacuum, - Outlet, - Switch, - Valve, -) +from homeassistant.components.homekit.type_switches import Outlet, Switch, Vacuum, Valve from homeassistant.components.vacuum import ( DOMAIN as VACUUM_DOMAIN, SERVICE_RETURN_TO_BASE, SERVICE_START, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, STATE_CLEANING, STATE_DOCKED, + SUPPORT_RETURN_HOME, + SUPPORT_START, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + CONF_TYPE, + STATE_OFF, + STATE_ON, ) -from homeassistant.const import ATTR_ENTITY_ID, CONF_TYPE, STATE_OFF, STATE_ON from homeassistant.core import split_entity_id import homeassistant.util.dt as dt_util @@ -193,14 +198,18 @@ async def test_valve_set_state(hass, hk_driver, events): assert events[-1].data[ATTR_VALUE] is None -async def test_vacuum_set_state(hass, hk_driver, events): +async def test_vacuum_set_state_with_returnhome_and_start_support( + hass, hk_driver, events +): """Test if Vacuum accessory and HA are updated accordingly.""" entity_id = "vacuum.roomba" - hass.states.async_set(entity_id, None) + hass.states.async_set( + entity_id, None, {ATTR_SUPPORTED_FEATURES: SUPPORT_RETURN_HOME | SUPPORT_START} + ) await hass.async_block_till_done() - acc = DockVacuum(hass, hk_driver, "DockVacuum", entity_id, 2, None) + acc = Vacuum(hass, hk_driver, "Vacuum", entity_id, 2, None) await acc.run_handler() await hass.async_block_till_done() assert acc.aid == 2 @@ -208,11 +217,19 @@ async def test_vacuum_set_state(hass, hk_driver, events): assert acc.char_on.value == 0 - hass.states.async_set(entity_id, STATE_CLEANING) + hass.states.async_set( + entity_id, + STATE_CLEANING, + {ATTR_SUPPORTED_FEATURES: SUPPORT_RETURN_HOME | SUPPORT_START}, + ) await hass.async_block_till_done() assert acc.char_on.value == 1 - hass.states.async_set(entity_id, STATE_DOCKED) + hass.states.async_set( + entity_id, + STATE_DOCKED, + {ATTR_SUPPORTED_FEATURES: SUPPORT_RETURN_HOME | SUPPORT_START}, + ) await hass.async_block_till_done() assert acc.char_on.value == 0 @@ -239,6 +256,52 @@ async def test_vacuum_set_state(hass, hk_driver, events): assert events[-1].data[ATTR_VALUE] is None +async def test_vacuum_set_state_without_returnhome_and_start_support( + hass, hk_driver, events +): + """Test if Vacuum accessory and HA are updated accordingly.""" + entity_id = "vacuum.roomba" + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + + acc = Vacuum(hass, hk_driver, "Vacuum", entity_id, 2, None) + await acc.run_handler() + await hass.async_block_till_done() + assert acc.aid == 2 + assert acc.category == 8 # Switch + + assert acc.char_on.value == 0 + + hass.states.async_set(entity_id, STATE_ON) + await hass.async_block_till_done() + assert acc.char_on.value == 1 + + hass.states.async_set(entity_id, STATE_OFF) + await hass.async_block_till_done() + assert acc.char_on.value == 0 + + # Set from HomeKit + call_turn_on = async_mock_service(hass, VACUUM_DOMAIN, SERVICE_TURN_ON) + call_turn_off = async_mock_service(hass, VACUUM_DOMAIN, SERVICE_TURN_OFF) + + await hass.async_add_executor_job(acc.char_on.client_update_value, 1) + await hass.async_block_till_done() + assert acc.char_on.value == 1 + assert call_turn_on + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] is None + + await hass.async_add_executor_job(acc.char_on.client_update_value, 0) + await hass.async_block_till_done() + assert acc.char_on.value == 0 + assert call_turn_off + assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id + assert len(events) == 2 + assert events[-1].data[ATTR_VALUE] is None + + async def test_reset_switch(hass, hk_driver, events): """Test if switch accessory is reset correctly.""" domain = "scene" From eb5f3b282b0043b7ee78cfeb0b1d18a4bd1c274b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 11 Jan 2021 03:40:32 -1000 Subject: [PATCH 165/507] Mark YAML support for August deprecated (#45039) --- homeassistant/components/august/__init__.py | 27 ++++++++++++--------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index feaf61450e8..6f16f7d5b31 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -43,17 +43,22 @@ _LOGGER = logging.getLogger(__name__) TWO_FA_REVALIDATE = "verify_configurator" CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_LOGIN_METHOD): vol.In(LOGIN_METHODS), - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_INSTALL_ID): cv.string, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_LOGIN_METHOD): vol.In(LOGIN_METHODS), + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_INSTALL_ID): cv.string, + vol.Optional( + CONF_TIMEOUT, default=DEFAULT_TIMEOUT + ): cv.positive_int, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) From 74e7f7c879d5041179d2651b6d74fadbb94736d2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 11 Jan 2021 03:46:54 -1000 Subject: [PATCH 166/507] Update roomba config flow to walk users through pairing (#45037) * Update roomba config flow to walk users though pairing * Remove YAML support * adjust tests * increase cover * pylint * pylint --- homeassistant/components/roomba/__init__.py | 65 +-- .../components/roomba/config_flow.py | 165 ++++++- homeassistant/components/roomba/manifest.json | 2 +- homeassistant/components/roomba/strings.json | 36 +- .../components/roomba/translations/en.json | 68 ++- tests/components/roomba/test_config_flow.py | 462 +++++++++++++++--- 6 files changed, 624 insertions(+), 174 deletions(-) diff --git a/homeassistant/components/roomba/__init__.py b/homeassistant/components/roomba/__init__.py index be85ec3619f..63deead7307 100644 --- a/homeassistant/components/roomba/__init__.py +++ b/homeassistant/components/roomba/__init__.py @@ -4,24 +4,17 @@ import logging import async_timeout from roombapy import Roomba, RoombaConnectionError -import voluptuous as vol -from homeassistant import config_entries, exceptions +from homeassistant import exceptions from homeassistant.const import CONF_HOST, CONF_PASSWORD -from homeassistant.core import callback -from homeassistant.helpers import config_validation as cv from .const import ( BLID, COMPONENTS, CONF_BLID, - CONF_CERT, CONF_CONTINUOUS, CONF_DELAY, CONF_NAME, - DEFAULT_CERT, - DEFAULT_CONTINUOUS, - DEFAULT_DELAY, DOMAIN, ROOMBA_SESSION, ) @@ -29,54 +22,9 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -def _has_all_unique_bilds(value): - """Validate that each vacuum configured has a unique bild. - - Uniqueness is determined case-independently. - """ - bilds = [device[CONF_BLID] for device in value] - schema = vol.Schema(vol.Unique()) - schema(bilds) - return value - - -DEVICE_SCHEMA = vol.All( - cv.deprecated(CONF_CERT), - vol.Schema( - { - vol.Required(CONF_HOST): str, - vol.Required(CONF_BLID): str, - vol.Required(CONF_PASSWORD): str, - vol.Optional(CONF_CERT, default=DEFAULT_CERT): str, - vol.Optional(CONF_CONTINUOUS, default=DEFAULT_CONTINUOUS): bool, - vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): int, - }, - ), -) - - -CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.All(cv.ensure_list, [DEVICE_SCHEMA], _has_all_unique_bilds)}, - extra=vol.ALLOW_EXTRA, -) - - async def async_setup(hass, config): """Set up the roomba environment.""" hass.data.setdefault(DOMAIN, {}) - - if DOMAIN not in config: - return True - for index, conf in enumerate(config[DOMAIN]): - _LOGGER.debug("Importing Roomba #%d - %s", index, conf[CONF_HOST]) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=conf, - ) - ) - return True @@ -88,8 +36,8 @@ async def async_setup_entry(hass, config_entry): hass.config_entries.async_update_entry( config_entry, options={ - "continuous": config_entry.data[CONF_CONTINUOUS], - "delay": config_entry.data[CONF_DELAY], + CONF_CONTINUOUS: config_entry.data[CONF_CONTINUOUS], + CONF_DELAY: config_entry.data[CONF_DELAY], }, ) @@ -184,12 +132,5 @@ def roomba_reported_state(roomba): return roomba.master_state.get("state", {}).get("reported", {}) -@callback -def _async_find_matching_config_entry(hass, prefix): - for entry in hass.config_entries.async_entries(DOMAIN): - if entry.unique_id == prefix: - return entry - - class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/roomba/config_flow.py b/homeassistant/components/roomba/config_flow.py index 166b5992d86..b99f62e8bdc 100644 --- a/homeassistant/components/roomba/config_flow.py +++ b/homeassistant/components/roomba/config_flow.py @@ -1,5 +1,8 @@ """Config flow to configure roomba component.""" + from roombapy import Roomba +from roombapy.discovery import RoombaDiscovery +from roombapy.getpassword import RoombaPassword import voluptuous as vol from homeassistant import config_entries, core @@ -18,15 +21,9 @@ from .const import ( ) from .const import DOMAIN # pylint:disable=unused-import -DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_HOST): str, - vol.Required(CONF_BLID): str, - vol.Required(CONF_PASSWORD): str, - vol.Optional(CONF_CONTINUOUS, default=DEFAULT_CONTINUOUS): bool, - vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): int, - } -) +DEFAULT_OPTIONS = {CONF_CONTINUOUS: DEFAULT_CONTINUOUS, CONF_DELAY: DEFAULT_DELAY} + +MAX_NUM_DEVICES_TO_DISCOVER = 25 async def validate_input(hass: core.HomeAssistant, data): @@ -57,34 +54,156 @@ class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + def __init__(self): + """Initialize the roomba flow.""" + self.discovered_robots = {} + self.name = None + self.blid = None + self.host = None + @staticmethod @callback def async_get_options_flow(config_entry): """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) - async def async_step_import(self, import_info): - """Set the config entry up from yaml.""" - return await self.async_step_user(import_info) - async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" + # This is for backwards compatibility. + return await self.async_step_init(user_input) + + async def async_step_init(self, user_input=None): + """Handle a flow start.""" + # Check if user chooses manual entry + if user_input is not None and not user_input.get(CONF_HOST): + return await self.async_step_manual() + + if ( + user_input is not None + and self.discovered_robots is not None + and user_input[CONF_HOST] in self.discovered_robots + ): + self.host = user_input[CONF_HOST] + device = self.discovered_robots[self.host] + self.blid = device.blid + self.name = device.robot_name + await self.async_set_unique_id(self.blid, raise_on_progress=False) + self._abort_if_unique_id_configured() + return await self.async_step_link() + + already_configured = self._async_current_ids(False) + discovery = _async_get_roomba_discovery() + devices = await self.hass.async_add_executor_job(discovery.get_all) + + if devices: + # Find already configured hosts + self.discovered_robots = { + device.ip: device + for device in devices + if device.blid not in already_configured + } + + if not self.discovered_robots: + return await self.async_step_manual() + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional("host"): vol.In( + { + **{ + device.ip: f"{device.robot_name} ({device.ip})" + for device in devices + if device.blid not in already_configured + }, + None: "Manually add a Roomba or Braava", + } + ) + } + ), + ) + + async def async_step_manual(self, user_input=None): + """Handle manual device setup.""" + if user_input is None: + return self.async_show_form( + step_id="manual", + data_schema=vol.Schema( + {vol.Required(CONF_HOST): str, vol.Required(CONF_BLID): str} + ), + ) + + if any( + user_input["host"] == entry.data.get("host") + for entry in self._async_current_entries() + ): + return self.async_abort(reason="already_configured") + + self.host = user_input[CONF_HOST] + self.blid = user_input[CONF_BLID] + await self.async_set_unique_id(self.blid, raise_on_progress=False) + self._abort_if_unique_id_configured() + return await self.async_step_link() + + async def async_step_link(self, user_input=None): + """Attempt to link with the Roomba. + + Given a configured host, will ask the user to press the home and target buttons + to connect to the device. + """ + if user_input is None: + return self.async_show_form(step_id="link") + + password = await self.hass.async_add_executor_job( + RoombaPassword(self.host).get_password + ) + + if not password: + return await self.async_step_link_manual() + + config = { + CONF_HOST: self.host, + CONF_BLID: self.blid, + CONF_PASSWORD: password, + **DEFAULT_OPTIONS, + } + + if not self.name: + try: + info = await validate_input(self.hass, config) + except CannotConnect: + return self.async_abort(reason="cannot_connect") + + await async_disconnect_or_timeout(self.hass, info[ROOMBA_SESSION]) + self.name = info[CONF_NAME] + + return self.async_create_entry(title=self.name, data=config) + + async def async_step_link_manual(self, user_input=None): + """Handle manual linking.""" errors = {} if user_input is not None: - await self.async_set_unique_id(user_input[CONF_BLID]) - self._abort_if_unique_id_configured() + config = { + CONF_HOST: self.host, + CONF_BLID: self.blid, + CONF_PASSWORD: user_input[CONF_PASSWORD], + **DEFAULT_OPTIONS, + } try: - info = await validate_input(self.hass, user_input) + info = await validate_input(self.hass, config) except CannotConnect: errors = {"base": "cannot_connect"} - if "base" not in errors: + if not errors: await async_disconnect_or_timeout(self.hass, info[ROOMBA_SESSION]) - return self.async_create_entry(title=info[CONF_NAME], data=user_input) + return self.async_create_entry(title=info[CONF_NAME], data=config) return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors + step_id="link_manual", + data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}), + errors=errors, ) @@ -119,3 +238,11 @@ class OptionsFlowHandler(config_entries.OptionsFlow): } ), ) + + +@callback +def _async_get_roomba_discovery(): + """Create a discovery object.""" + discovery = RoombaDiscovery() + discovery.amount_of_broadcasted_messages = MAX_NUM_DEVICES_TO_DISCOVER + return discovery diff --git a/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json index 808c7eb9432..f2e5c8035aa 100644 --- a/homeassistant/components/roomba/manifest.json +++ b/homeassistant/components/roomba/manifest.json @@ -1,6 +1,6 @@ { "domain": "roomba", - "name": "iRobot Roomba", + "name": "iRobot Roomba and Braava", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/roomba", "requirements": ["roombapy==1.6.2"], diff --git a/homeassistant/components/roomba/strings.json b/homeassistant/components/roomba/strings.json index cbe7c06ae36..4d0b396d2a9 100644 --- a/homeassistant/components/roomba/strings.json +++ b/homeassistant/components/roomba/strings.json @@ -1,21 +1,39 @@ { "config": { "step": { - "user": { - "title": "Connect to the device", - "description": "Currently retrieving the BLID and password is a manual process. Please follow the steps outlined in the documentation at: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", + "init": { + "title": "Automaticlly connect to the device", + "description": "Select a Roomba or Braava.", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, + "manual": { + "title": "Manually connect to the device", + "description": "No Roomba or Braava have been discovered on your network. The BLID is the portion of the device hostname after `iRobot-`. Please follow the steps outlined in the documentation at: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", "data": { "host": "[%key:common::config_flow::data::host%]", - "blid": "BLID", - "password": "[%key:common::config_flow::data::password%]", - "continuous": "Continuous", - "delay": "Delay" + "blid": "BLID" } - } + }, + "link": { + "title": "Retrieve Password", + "description": "Press and hold the Home button until the device generates a sound (about two seconds)." + }, + "link_manual": { + "title": "Enter Password", + "description": "The password could not be retrivied from the device automaticlly. Please follow the steps outlined in the documentation at: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } + } }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" - } + }, + "abort": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + } }, "options": { "step": { diff --git a/homeassistant/components/roomba/translations/en.json b/homeassistant/components/roomba/translations/en.json index b6222f2adf8..4d0b396d2a9 100644 --- a/homeassistant/components/roomba/translations/en.json +++ b/homeassistant/components/roomba/translations/en.json @@ -1,30 +1,48 @@ { - "config": { - "error": { - "cannot_connect": "Failed to connect" - }, - "step": { - "user": { - "data": { - "blid": "BLID", - "continuous": "Continuous", - "delay": "Delay", - "host": "Host", - "password": "Password" - }, - "description": "Currently retrieving the BLID and password is a manual process. Please follow the steps outlined in the documentation at: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", - "title": "Connect to the device" - } + "config": { + "step": { + "init": { + "title": "Automaticlly connect to the device", + "description": "Select a Roomba or Braava.", + "data": { + "host": "[%key:common::config_flow::data::host%]" } + }, + "manual": { + "title": "Manually connect to the device", + "description": "No Roomba or Braava have been discovered on your network. The BLID is the portion of the device hostname after `iRobot-`. Please follow the steps outlined in the documentation at: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "blid": "BLID" + } + }, + "link": { + "title": "Retrieve Password", + "description": "Press and hold the Home button until the device generates a sound (about two seconds)." + }, + "link_manual": { + "title": "Enter Password", + "description": "The password could not be retrivied from the device automaticlly. Please follow the steps outlined in the documentation at: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } + } }, - "options": { - "step": { - "init": { - "data": { - "continuous": "Continuous", - "delay": "Delay" - } - } + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + } + }, + "options": { + "step": { + "init": { + "data": { + "continuous": "Continuous", + "delay": "Delay" } + } } -} \ No newline at end of file + } +} diff --git a/tests/components/roomba/test_config_flow.py b/tests/components/roomba/test_config_flow.py index 54ef229ec49..a90cc9c621f 100644 --- a/tests/components/roomba/test_config_flow.py +++ b/tests/components/roomba/test_config_flow.py @@ -2,6 +2,7 @@ from unittest.mock import MagicMock, PropertyMock, patch from roombapy import RoombaConnectionError +from roombapy.roomba import RoombaInfo from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.roomba.const import ( @@ -14,16 +15,9 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD from tests.common import MockConfigEntry +MOCK_IP = "1.2.3.4" VALID_CONFIG = {CONF_HOST: "1.2.3.4", CONF_BLID: "blid", CONF_PASSWORD: "password"} -VALID_YAML_CONFIG = { - CONF_HOST: "1.2.3.4", - CONF_BLID: "blid", - CONF_PASSWORD: "password", - CONF_CONTINUOUS: True, - CONF_DELAY: 1, -} - def _create_mocked_roomba( roomba_connected=None, master_state=None, connect=None, disconnect=None @@ -36,55 +30,227 @@ def _create_mocked_roomba( return mocked_roomba -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} +def _mocked_discovery(*_): + roomba_discovery = MagicMock() + + roomba = RoombaInfo( + hostname="iRobot-blid", + robot_name="robot_name", + ip=MOCK_IP, + mac="mac", + firmware="firmware", + sku="sku", + capabilities="capabilities", ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {} + + roomba_discovery.get_all = MagicMock(return_value=[roomba]) + return roomba_discovery + + +def _mocked_failed_discovery(*_): + roomba_discovery = MagicMock() + roomba_discovery.get_all = MagicMock(return_value=[]) + return roomba_discovery + + +def _mocked_getpassword(*_): + roomba_password = MagicMock() + roomba_password.get_password = MagicMock(return_value="password") + return roomba_password + + +def _mocked_failed_getpassword(*_): + roomba_password = MagicMock() + roomba_password.get_password = MagicMock(return_value=None) + return roomba_password + + +async def test_form_user_discovery_and_password_fetch(hass): + """Test we can discovery and fetch the password.""" + await setup.async_setup_component(hass, "persistent_notification", {}) mocked_roomba = _create_mocked_roomba( roomba_connected=True, master_state={"state": {"reported": {"name": "myroomba"}}}, ) + with patch( + "homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "init" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: MOCK_IP}, + ) + await hass.async_block_till_done() + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] is None + assert result2["step_id"] == "link" + with patch( "homeassistant.components.roomba.config_flow.Roomba", return_value=mocked_roomba, + ), patch( + "homeassistant.components.roomba.config_flow.RoombaPassword", + _mocked_getpassword, ), patch( "homeassistant.components.roomba.async_setup", return_value=True ) as mock_setup, patch( "homeassistant.components.roomba.async_setup_entry", return_value=True, ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - VALID_CONFIG, + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == "myroomba" - - assert result2["result"].unique_id == "blid" - assert result2["data"] == { + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["title"] == "robot_name" + assert result3["result"].unique_id == "blid" + assert result3["data"] == { CONF_BLID: "blid", CONF_CONTINUOUS: True, CONF_DELAY: 1, - CONF_HOST: "1.2.3.4", + CONF_HOST: MOCK_IP, CONF_PASSWORD: "password", } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_cannot_connect(hass): - """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} +async def test_form_user_discovery_skips_known(hass): + """Test discovery proceeds to manual if all discovered are already known.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry = MockConfigEntry(domain=DOMAIN, data=VALID_CONFIG, unique_id="blid") + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "manual" + + +async def test_form_user_failed_discovery_aborts_already_configured(hass): + """Test if we manually configure an existing host we abort.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry = MockConfigEntry(domain=DOMAIN, data=VALID_CONFIG, unique_id="blid") + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.roomba.config_flow.RoombaDiscovery", + _mocked_failed_discovery, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "manual" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: MOCK_IP, CONF_BLID: "blid"}, ) + await hass.async_block_till_done() + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "already_configured" + + +async def test_form_user_discovery_manual_and_auto_password_fetch(hass): + """Test discovery skipped and we can auto fetch the password.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + mocked_roomba = _create_mocked_roomba( + roomba_connected=True, + master_state={"state": {"reported": {"name": "myroomba"}}}, + ) + + with patch( + "homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "init" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: None}, + ) + await hass.async_block_till_done() + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] is None + assert result2["step_id"] == "manual" + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {CONF_HOST: MOCK_IP, CONF_BLID: "blid"}, + ) + await hass.async_block_till_done() + assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["errors"] is None + + with patch( + "homeassistant.components.roomba.config_flow.Roomba", + return_value=mocked_roomba, + ), patch( + "homeassistant.components.roomba.config_flow.RoombaPassword", + _mocked_getpassword, + ), patch( + "homeassistant.components.roomba.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.roomba.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["title"] == "myroomba" + assert result4["result"].unique_id == "blid" + assert result4["data"] == { + CONF_BLID: "blid", + CONF_CONTINUOUS: True, + CONF_DELAY: 1, + CONF_HOST: MOCK_IP, + CONF_PASSWORD: "password", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_user_discovery_manual_and_auto_password_fetch_but_cannot_connect( + hass, +): + """Test discovery skipped and we can auto fetch the password then we fail to connect.""" + await setup.async_setup_component(hass, "persistent_notification", {}) mocked_roomba = _create_mocked_roomba( connect=RoombaConnectionError, @@ -92,27 +258,161 @@ async def test_form_cannot_connect(hass): master_state={"state": {"reported": {"name": "myroomba"}}}, ) + with patch( + "homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "init" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: None}, + ) + await hass.async_block_till_done() + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] is None + assert result2["step_id"] == "manual" + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {CONF_HOST: MOCK_IP, CONF_BLID: "blid"}, + ) + await hass.async_block_till_done() + assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["errors"] is None + with patch( "homeassistant.components.roomba.config_flow.Roomba", return_value=mocked_roomba, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - VALID_CONFIG, + ), patch( + "homeassistant.components.roomba.config_flow.RoombaPassword", + _mocked_getpassword, + ), patch( + "homeassistant.components.roomba.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.roomba.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + {}, ) + await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["errors"] == {"base": "cannot_connect"} + assert result4["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result4["reason"] == "cannot_connect" + assert len(mock_setup.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 0 -async def test_form_import(hass): - """Test we can import yaml config.""" +async def test_form_user_discovery_fails_and_auto_password_fetch(hass): + """Test discovery fails and we can auto fetch the password.""" + await setup.async_setup_component(hass, "persistent_notification", {}) mocked_roomba = _create_mocked_roomba( roomba_connected=True, - master_state={"state": {"reported": {"name": "imported_roomba"}}}, + master_state={"state": {"reported": {"name": "myroomba"}}}, ) + with patch( + "homeassistant.components.roomba.config_flow.RoombaDiscovery", + _mocked_failed_discovery, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "manual" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: MOCK_IP, CONF_BLID: "blid"}, + ) + await hass.async_block_till_done() + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] is None + + with patch( + "homeassistant.components.roomba.config_flow.Roomba", + return_value=mocked_roomba, + ), patch( + "homeassistant.components.roomba.config_flow.RoombaPassword", + _mocked_getpassword, + ), patch( + "homeassistant.components.roomba.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.roomba.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["title"] == "myroomba" + assert result3["result"].unique_id == "blid" + assert result3["data"] == { + CONF_BLID: "blid", + CONF_CONTINUOUS: True, + CONF_DELAY: 1, + CONF_HOST: MOCK_IP, + CONF_PASSWORD: "password", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_user_discovery_fails_and_password_fetch_fails(hass): + """Test discovery fails and password fetch fails.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + mocked_roomba = _create_mocked_roomba( + roomba_connected=True, + master_state={"state": {"reported": {"name": "myroomba"}}}, + ) + + with patch( + "homeassistant.components.roomba.config_flow.RoombaDiscovery", + _mocked_failed_discovery, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "manual" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: MOCK_IP, CONF_BLID: "blid"}, + ) + await hass.async_block_till_done() + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] is None + + with patch( + "homeassistant.components.roomba.config_flow.RoombaPassword", + _mocked_failed_getpassword, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {}, + ) + await hass.async_block_till_done() + with patch( "homeassistant.components.roomba.config_flow.Roomba", return_value=mocked_roomba, @@ -122,39 +422,85 @@ async def test_form_import(hass): "homeassistant.components.roomba.async_setup_entry", return_value=True, ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=VALID_YAML_CONFIG.copy(), + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + {CONF_PASSWORD: "password"}, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["result"].unique_id == "blid" - assert result["title"] == "imported_roomba" - assert result["data"] == { + assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["title"] == "myroomba" + assert result4["result"].unique_id == "blid" + assert result4["data"] == { CONF_BLID: "blid", CONF_CONTINUOUS: True, CONF_DELAY: 1, - CONF_HOST: "1.2.3.4", + CONF_HOST: MOCK_IP, CONF_PASSWORD: "password", } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_import_dupe(hass): - """Test we get abort on duplicate import.""" +async def test_form_user_discovery_fails_and_password_fetch_fails_and_cannot_connect( + hass, +): + """Test discovery fails and password fetch fails then we cannot connect.""" await setup.async_setup_component(hass, "persistent_notification", {}) - entry = MockConfigEntry(domain=DOMAIN, data=VALID_CONFIG, unique_id="blid") - entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=VALID_YAML_CONFIG.copy(), + mocked_roomba = _create_mocked_roomba( + connect=RoombaConnectionError, + roomba_connected=True, + master_state={"state": {"reported": {"name": "myroomba"}}}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" + + with patch( + "homeassistant.components.roomba.config_flow.RoombaDiscovery", + _mocked_failed_discovery, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "manual" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: MOCK_IP, CONF_BLID: "blid"}, + ) + await hass.async_block_till_done() + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] is None + + with patch( + "homeassistant.components.roomba.config_flow.RoombaPassword", + _mocked_failed_getpassword, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {}, + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.roomba.config_flow.Roomba", + return_value=mocked_roomba, + ), patch( + "homeassistant.components.roomba.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.roomba.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + {CONF_PASSWORD: "password"}, + ) + await hass.async_block_till_done() + + assert result4["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result4["errors"] == {"base": "cannot_connect"} + assert len(mock_setup.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 0 From d60fc0de3807cc680016713670767d4efeba8d38 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 11 Jan 2021 16:04:22 +0100 Subject: [PATCH 167/507] Add availability_mode "all" and "any" to MQTT entities (#44987) * Add availability_mode "all" to MQTT entities * Add availability mode any --- .../components/mqtt/abbreviations.py | 1 + homeassistant/components/mqtt/mixins.py | 28 +++- tests/components/mqtt/test_common.py | 129 ++++++++++++++++++ tests/components/mqtt/test_sensor.py | 16 +++ 4 files changed, 170 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index c3f6b55e0fe..9cfa84c6c1b 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -8,6 +8,7 @@ ABBREVIATIONS = { "aux_stat_tpl": "aux_state_template", "aux_stat_t": "aux_state_topic", "avty": "availability", + "avty_mode": "availability_mode", "avty_t": "availability_topic", "away_mode_cmd_t": "away_mode_command_topic", "away_mode_stat_tpl": "away_mode_state_template", diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index e4fa7e15526..1ab2054b355 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -42,7 +42,14 @@ from .util import valid_subscribe_topic _LOGGER = logging.getLogger(__name__) +AVAILABILITY_ALL = "all" +AVAILABILITY_ANY = "any" +AVAILABILITY_LATEST = "latest" + +AVAILABILITY_MODES = [AVAILABILITY_ALL, AVAILABILITY_ANY, AVAILABILITY_LATEST] + CONF_AVAILABILITY = "availability" +CONF_AVAILABILITY_MODE = "availability_mode" CONF_AVAILABILITY_TOPIC = "availability_topic" CONF_PAYLOAD_AVAILABLE = "payload_available" CONF_PAYLOAD_NOT_AVAILABLE = "payload_not_available" @@ -71,6 +78,9 @@ MQTT_AVAILABILITY_SINGLE_SCHEMA = vol.Schema( MQTT_AVAILABILITY_LIST_SCHEMA = vol.Schema( { + vol.Optional(CONF_AVAILABILITY_MODE, default=AVAILABILITY_LATEST): vol.All( + cv.string, vol.In(AVAILABILITY_MODES) + ), vol.Exclusive(CONF_AVAILABILITY, "availability"): vol.All( cv.ensure_list, [ @@ -227,7 +237,8 @@ class MqttAvailability(Entity): def __init__(self, config: dict) -> None: """Initialize the availability mixin.""" self._availability_sub_state = None - self._available = False + self._available = {} + self._available_latest = False self._availability_setup_from_config(config) async def async_added_to_hass(self) -> None: @@ -275,12 +286,15 @@ class MqttAvailability(Entity): """Handle a new received MQTT availability message.""" topic = msg.topic if msg.payload == self._avail_topics[topic][CONF_PAYLOAD_AVAILABLE]: - self._available = True + self._available[topic] = True + self._available_latest = True elif msg.payload == self._avail_topics[topic][CONF_PAYLOAD_NOT_AVAILABLE]: - self._available = False + self._available[topic] = False + self._available_latest = False self.async_write_ha_state() + self._available = {topic: False for topic in self._avail_topics} topics = { f"availability_{topic}": { "topic": topic, @@ -313,7 +327,13 @@ class MqttAvailability(Entity): """Return if the device is available.""" if not self.hass.data[DATA_MQTT].connected and not self.hass.is_stopping: return False - return not self._avail_topics or self._available + if not self._avail_topics: + return True + if self._avail_config[CONF_AVAILABILITY_MODE] == AVAILABILITY_ALL: + return all(self._available.values()) + if self._avail_config[CONF_AVAILABILITY_MODE] == AVAILABILITY_ANY: + return any(self._available.values()) + return self._available_latest async def cleanup_device_registry(hass, device_id): diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index c5e271f1625..c8cd80372c6 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -171,6 +171,135 @@ async def help_test_default_availability_list_payload( assert state.state != STATE_UNAVAILABLE +async def help_test_default_availability_list_payload_all( + hass, + mqtt_mock, + domain, + config, + no_assumed_state=False, + state_topic=None, + state_message=None, +): + """Test availability by default payload with defined topic. + + This is a test helper for the MqttAvailability mixin. + """ + # Add availability settings to config + config = copy.deepcopy(config) + config[domain]["availability_mode"] = "all" + config[domain]["availability"] = [ + {"topic": "availability-topic1"}, + {"topic": "availability-topic2"}, + ] + assert await async_setup_component( + hass, + domain, + config, + ) + await hass.async_block_till_done() + + state = hass.states.get(f"{domain}.test") + assert state.state == STATE_UNAVAILABLE + + async_fire_mqtt_message(hass, "availability-topic1", "online") + + state = hass.states.get(f"{domain}.test") + assert state.state == STATE_UNAVAILABLE + if no_assumed_state: + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "availability-topic2", "online") + + state = hass.states.get(f"{domain}.test") + assert state.state != STATE_UNAVAILABLE + + async_fire_mqtt_message(hass, "availability-topic2", "offline") + + state = hass.states.get(f"{domain}.test") + assert state.state == STATE_UNAVAILABLE + if no_assumed_state: + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "availability-topic2", "online") + + state = hass.states.get(f"{domain}.test") + assert state.state != STATE_UNAVAILABLE + + async_fire_mqtt_message(hass, "availability-topic1", "offline") + + state = hass.states.get(f"{domain}.test") + assert state.state == STATE_UNAVAILABLE + if no_assumed_state: + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "availability-topic1", "online") + + state = hass.states.get(f"{domain}.test") + assert state.state != STATE_UNAVAILABLE + + +async def help_test_default_availability_list_payload_any( + hass, + mqtt_mock, + domain, + config, + no_assumed_state=False, + state_topic=None, + state_message=None, +): + """Test availability by default payload with defined topic. + + This is a test helper for the MqttAvailability mixin. + """ + # Add availability settings to config + config = copy.deepcopy(config) + config[domain]["availability_mode"] = "any" + config[domain]["availability"] = [ + {"topic": "availability-topic1"}, + {"topic": "availability-topic2"}, + ] + assert await async_setup_component( + hass, + domain, + config, + ) + await hass.async_block_till_done() + + state = hass.states.get(f"{domain}.test") + assert state.state == STATE_UNAVAILABLE + + async_fire_mqtt_message(hass, "availability-topic1", "online") + + state = hass.states.get(f"{domain}.test") + assert state.state != STATE_UNAVAILABLE + if no_assumed_state: + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "availability-topic2", "online") + + state = hass.states.get(f"{domain}.test") + assert state.state != STATE_UNAVAILABLE + + async_fire_mqtt_message(hass, "availability-topic2", "offline") + + state = hass.states.get(f"{domain}.test") + assert state.state != STATE_UNAVAILABLE + if no_assumed_state: + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "availability-topic1", "offline") + + state = hass.states.get(f"{domain}.test") + assert state.state == STATE_UNAVAILABLE + + async_fire_mqtt_message(hass, "availability-topic1", "online") + + state = hass.states.get(f"{domain}.test") + assert state.state != STATE_UNAVAILABLE + if no_assumed_state: + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async def help_test_default_availability_list_single( hass, mqtt_mock, diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index d818338a0ca..a2c2605d6ab 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -17,6 +17,8 @@ from .test_common import ( help_test_availability_without_topic, help_test_custom_availability_payload, help_test_default_availability_list_payload, + help_test_default_availability_list_payload_all, + help_test_default_availability_list_payload_any, help_test_default_availability_list_single, help_test_default_availability_payload, help_test_discovery_broken, @@ -297,6 +299,20 @@ async def test_default_availability_list_payload(hass, mqtt_mock): ) +async def test_default_availability_list_payload_all(hass, mqtt_mock): + """Test availability by default payload with defined topic.""" + await help_test_default_availability_list_payload_all( + hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_default_availability_list_payload_any(hass, mqtt_mock): + """Test availability by default payload with defined topic.""" + await help_test_default_availability_list_payload_any( + hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + ) + + async def test_default_availability_list_single(hass, mqtt_mock, caplog): """Test availability list and availability_topic are mutually exclusive.""" await help_test_default_availability_list_single( From d270f9515be39abf7c1989c90956a5c78ed384d2 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 11 Jan 2021 16:05:11 +0100 Subject: [PATCH 168/507] Add zwave_js init module tests (#45048) * Add entry setup and unload test * Test home assistant stop * Test on connect and on disconnect * Test client connect timeout * Test ready node added * Test non ready node added * Test existing node not ready * Test device registry state * Add common test tools module * Add existing ready node test * Include init module in coverage calculation * Clean docstrings --- .coveragerc | 1 - homeassistant/components/zwave_js/__init__.py | 3 +- tests/components/zwave_js/common.py | 2 + tests/components/zwave_js/conftest.py | 13 +- tests/components/zwave_js/test_init.py | 212 ++++++++++++++++++ tests/components/zwave_js/test_sensor.py | 2 +- 6 files changed, 229 insertions(+), 4 deletions(-) create mode 100644 tests/components/zwave_js/common.py create mode 100644 tests/components/zwave_js/test_init.py diff --git a/.coveragerc b/.coveragerc index 9a8b062468f..7f05f4c064b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1092,7 +1092,6 @@ omit = homeassistant/components/zoneminder/* homeassistant/components/supla/* homeassistant/components/zwave/util.py - homeassistant/components/zwave_js/__init__.py homeassistant/components/zwave_js/discovery.py homeassistant/components/zwave_js/entity.py homeassistant/components/zwave_js/light.py diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 605235f61ba..b1502163f79 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -18,6 +18,7 @@ from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN, PLATFORMS from .discovery import async_discover_values LOGGER = logging.getLogger(__name__) +CONNECT_TIMEOUT = 10 async def async_setup(hass: HomeAssistant, config: dict) -> bool: @@ -113,7 +114,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # connect and throw error if connection failed asyncio.create_task(client.connect()) try: - async with timeout(10): + async with timeout(CONNECT_TIMEOUT): await initialized.wait() except asyncio.TimeoutError as err: for unsub in unsubs: diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py new file mode 100644 index 00000000000..8d2a2195913 --- /dev/null +++ b/tests/components/zwave_js/common.py @@ -0,0 +1,2 @@ +"""Provide common test tools for Z-Wave JS.""" +AIR_TEMPERATURE_SENSOR = "sensor.multisensor_6_air_temperature" diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 61e50d86a2d..8dd24aaa3ab 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -1,14 +1,24 @@ """Provide common Z-Wave JS fixtures.""" import json -from unittest.mock import patch +from unittest.mock import DEFAULT, patch import pytest from zwave_js_server.model.driver import Driver from zwave_js_server.model.node import Node +from homeassistant.helpers.device_registry import ( + async_get_registry as async_get_device_registry, +) + from tests.common import MockConfigEntry, load_fixture +@pytest.fixture(name="device_registry") +async def device_registry_fixture(hass): + """Return the device registry.""" + return await async_get_device_registry(hass) + + @pytest.fixture(name="controller_state", scope="session") def controller_state_fixture(): """Load the controller state fixture data.""" @@ -49,6 +59,7 @@ async def integration_fixture(hass, client): def initialize_client(async_on_initialized): """Init the client.""" hass.async_create_task(async_on_initialized()) + return DEFAULT client.register_on_initialized.side_effect = initialize_client diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py new file mode 100644 index 00000000000..0df05aba864 --- /dev/null +++ b/tests/components/zwave_js/test_init.py @@ -0,0 +1,212 @@ +"""Test the Z-Wave JS init module.""" +from copy import deepcopy +from unittest.mock import DEFAULT, patch + +import pytest +from zwave_js_server.model.node import Node + +from homeassistant.components.zwave_js.const import DOMAIN +from homeassistant.config_entries import ( + ENTRY_STATE_LOADED, + ENTRY_STATE_NOT_LOADED, + ENTRY_STATE_SETUP_RETRY, +) +from homeassistant.const import STATE_UNAVAILABLE + +from .common import AIR_TEMPERATURE_SENSOR + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="connect_timeout") +def connect_timeout_fixture(): + """Mock the connect timeout.""" + with patch("homeassistant.components.zwave_js.CONNECT_TIMEOUT", new=0) as timeout: + yield timeout + + +async def test_entry_setup_unload(hass, client, integration): + """Test the integration set up and unload.""" + entry = integration + + assert client.connect.call_count == 1 + assert client.register_on_initialized.call_count == 1 + assert client.register_on_disconnect.call_count == 1 + assert client.register_on_connect.call_count == 1 + assert entry.state == ENTRY_STATE_LOADED + + await hass.config_entries.async_unload(entry.entry_id) + + assert client.disconnect.call_count == 1 + assert client.register_on_initialized.return_value.call_count == 1 + assert client.register_on_disconnect.return_value.call_count == 1 + assert client.register_on_connect.return_value.call_count == 1 + assert entry.state == ENTRY_STATE_NOT_LOADED + + +async def test_home_assistant_stop(hass, client, integration): + """Test we clean up on home assistant stop.""" + await hass.async_stop() + + assert client.disconnect.call_count == 1 + + +async def test_on_connect_disconnect(hass, client, multisensor_6, integration): + """Test we handle disconnect and reconnect.""" + on_connect = client.register_on_connect.call_args[0][0] + on_disconnect = client.register_on_disconnect.call_args[0][0] + state = hass.states.get(AIR_TEMPERATURE_SENSOR) + + assert state + assert state.state != STATE_UNAVAILABLE + + client.connected = False + + await on_disconnect() + await hass.async_block_till_done() + + state = hass.states.get(AIR_TEMPERATURE_SENSOR) + + assert state + assert state.state == STATE_UNAVAILABLE + + client.connected = True + + await on_connect() + await hass.async_block_till_done() + + state = hass.states.get(AIR_TEMPERATURE_SENSOR) + + assert state + assert state.state != STATE_UNAVAILABLE + + +async def test_initialized_timeout(hass, client, connect_timeout): + """Test we handle a timeout during client initialization.""" + entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"}) + 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_RETRY + + +async def test_on_node_added_ready( + hass, multisensor_6_state, client, integration, device_registry +): + """Test we handle a ready node added event.""" + node = Node(client, multisensor_6_state) + event = {"node": node} + air_temperature_device_id = f"{client.driver.controller.home_id}-{node.node_id}" + + state = hass.states.get(AIR_TEMPERATURE_SENSOR) + + assert not state # entity and device not yet added + assert not device_registry.async_get_device( + identifiers={(DOMAIN, air_temperature_device_id)} + ) + + client.driver.controller.emit("node added", event) + await hass.async_block_till_done() + + state = hass.states.get(AIR_TEMPERATURE_SENSOR) + + assert state # entity and device added + assert state.state != STATE_UNAVAILABLE + assert device_registry.async_get_device( + identifiers={(DOMAIN, air_temperature_device_id)} + ) + + +async def test_on_node_added_not_ready( + hass, multisensor_6_state, client, integration, device_registry +): + """Test we handle a non ready node added event.""" + node_data = deepcopy(multisensor_6_state) # Copy to allow modification in tests. + node = Node(client, node_data) + node.data["ready"] = False + event = {"node": node} + air_temperature_device_id = f"{client.driver.controller.home_id}-{node.node_id}" + + state = hass.states.get(AIR_TEMPERATURE_SENSOR) + + assert not state # entity and device not yet added + assert not device_registry.async_get_device( + identifiers={(DOMAIN, air_temperature_device_id)} + ) + + client.driver.controller.emit("node added", event) + await hass.async_block_till_done() + + state = hass.states.get(AIR_TEMPERATURE_SENSOR) + + assert not state # entity not yet added but device added in registry + assert device_registry.async_get_device( + identifiers={(DOMAIN, air_temperature_device_id)} + ) + + node.data["ready"] = True + node.emit("ready", event) + await hass.async_block_till_done() + + state = hass.states.get(AIR_TEMPERATURE_SENSOR) + + assert state # entity added + assert state.state != STATE_UNAVAILABLE + + +async def test_existing_node_ready( + hass, client, multisensor_6, integration, device_registry +): + """Test we handle a ready node that exists during integration setup.""" + node = multisensor_6 + air_temperature_device_id = f"{client.driver.controller.home_id}-{node.node_id}" + + state = hass.states.get(AIR_TEMPERATURE_SENSOR) + + assert state # entity and device added + assert state.state != STATE_UNAVAILABLE + assert device_registry.async_get_device( + identifiers={(DOMAIN, air_temperature_device_id)} + ) + + +async def test_existing_node_not_ready(hass, client, multisensor_6, device_registry): + """Test we handle a non ready node that exists during integration setup.""" + node = multisensor_6 + node.data = deepcopy(node.data) # Copy to allow modification in tests. + node.data["ready"] = False + event = {"node": node} + air_temperature_device_id = f"{client.driver.controller.home_id}-{node.node_id}" + entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"}) + entry.add_to_hass(hass) + + def initialize_client(async_on_initialized): + """Init the client.""" + hass.async_create_task(async_on_initialized()) + return DEFAULT + + client.register_on_initialized.side_effect = initialize_client + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(AIR_TEMPERATURE_SENSOR) + + assert not state # entity and device not yet added + assert not device_registry.async_get_device( + identifiers={(DOMAIN, air_temperature_device_id)} + ) + + node.data["ready"] = True + node.emit("ready", event) + await hass.async_block_till_done() + + state = hass.states.get(AIR_TEMPERATURE_SENSOR) + + assert state # entity and device added + assert state.state != STATE_UNAVAILABLE + assert device_registry.async_get_device( + identifiers={(DOMAIN, air_temperature_device_id)} + ) diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index 79876a5b453..75ce016fb04 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -1,7 +1,7 @@ """Test the Z-Wave JS sensor platform.""" from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS -AIR_TEMPERATURE_SENSOR = "sensor.multisensor_6_air_temperature" +from .common import AIR_TEMPERATURE_SENSOR async def test_numeric_sensor(hass, multisensor_6, integration): From e3f38942ccb2445ea1617d74f34e326ecaf35833 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 11 Jan 2021 17:45:06 +0200 Subject: [PATCH 169/507] Add 100% tests coverage for Shelly cover and switch platforms (#45001) --- .coveragerc | 2 - tests/components/shelly/conftest.py | 37 +++++++++- tests/components/shelly/test_cover.py | 93 ++++++++++++++++++++++++++ tests/components/shelly/test_switch.py | 85 +++++++++++++++++++++++ 4 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 tests/components/shelly/test_cover.py create mode 100644 tests/components/shelly/test_switch.py diff --git a/.coveragerc b/.coveragerc index 7f05f4c064b..a8f198d436c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -791,11 +791,9 @@ omit = homeassistant/components/shodan/sensor.py homeassistant/components/shelly/__init__.py homeassistant/components/shelly/binary_sensor.py - homeassistant/components/shelly/cover.py homeassistant/components/shelly/entity.py homeassistant/components/shelly/light.py homeassistant/components/shelly/sensor.py - homeassistant/components/shelly/switch.py homeassistant/components/shelly/utils.py homeassistant/components/sht31/sensor.py homeassistant/components/sigfox/sensor.py diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 6887730b3b1..804d5a75952 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -1,5 +1,5 @@ """Test configuration for Shelly.""" -from unittest.mock import Mock, patch +from unittest.mock import AsyncMock, Mock, patch import pytest @@ -17,6 +17,7 @@ from tests.common import MockConfigEntry, async_mock_service, mock_device_regist MOCK_SETTINGS = { "name": "Test name", + "mode": "relay", "device": { "mac": "test-mac", "hostname": "test-host", @@ -26,13 +27,38 @@ MOCK_SETTINGS = { "coiot": {"update_period": 15}, "fw": "20201124-092159/v1.9.0@57ac4ad8", "relays": [{"btn_type": "momentary"}, {"btn_type": "toggle"}], + "rollers": [{"positioning": True}], } MOCK_BLOCKS = [ - Mock(sensor_ids={"inputEvent": "S", "inputEventCnt": 2}, channel="0", type="relay") + Mock( + sensor_ids={"inputEvent": "S", "inputEventCnt": 2}, + channel="0", + type="relay", + set_state=AsyncMock(side_effect=lambda turn: {"ison": turn == "on"}), + ), + Mock( + sensor_ids={"roller": "stop", "rollerPos": 0}, + channel="1", + type="roller", + set_state=AsyncMock( + side_effect=lambda go, roller_pos=0: { + "current_pos": roller_pos, + "state": go, + } + ), + ), ] +MOCK_SHELLY = { + "mac": "test-mac", + "auth": False, + "fw": "20201124-092854/v1.9.0@57ac4ad8", + "num_outputs": 2, +} + + @pytest.fixture(autouse=True) def mock_coap(): """Mock out coap.""" @@ -68,7 +94,12 @@ async def coap_wrapper(hass): config_entry = MockConfigEntry(domain=DOMAIN, data={}) config_entry.add_to_hass(hass) - device = Mock(blocks=MOCK_BLOCKS, settings=MOCK_SETTINGS) + device = Mock( + blocks=MOCK_BLOCKS, + settings=MOCK_SETTINGS, + shelly=MOCK_SHELLY, + update=AsyncMock(), + ) hass.data[DOMAIN] = {DATA_CONFIG_ENTRY: {}} hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id] = {} diff --git a/tests/components/shelly/test_cover.py b/tests/components/shelly/test_cover.py new file mode 100644 index 00000000000..5c34fcc1bf5 --- /dev/null +++ b/tests/components/shelly/test_cover.py @@ -0,0 +1,93 @@ +"""The scene tests for the myq platform.""" +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + ATTR_POSITION, + DOMAIN as COVER_DOMAIN, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, + SERVICE_SET_COVER_POSITION, + SERVICE_STOP_COVER, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, +) +from homeassistant.const import ATTR_ENTITY_ID + +ROLLER_BLOCK_ID = 1 + + +async def test_services(hass, coap_wrapper, monkeypatch): + """Test device turn on/off services.""" + assert coap_wrapper + + monkeypatch.setitem(coap_wrapper.device.settings, "mode", "roller") + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, COVER_DOMAIN) + ) + await hass.async_block_till_done() + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: "cover.test_name", ATTR_POSITION: 50}, + blocking=True, + ) + state = hass.states.get("cover.test_name") + assert state.attributes[ATTR_CURRENT_POSITION] == 50 + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: "cover.test_name"}, + blocking=True, + ) + assert hass.states.get("cover.test_name").state == STATE_OPENING + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: "cover.test_name"}, + blocking=True, + ) + assert hass.states.get("cover.test_name").state == STATE_CLOSING + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: "cover.test_name"}, + blocking=True, + ) + assert hass.states.get("cover.test_name").state == STATE_CLOSED + + +async def test_update(hass, coap_wrapper, monkeypatch): + """Test device update.""" + assert coap_wrapper + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, COVER_DOMAIN) + ) + await hass.async_block_till_done() + + monkeypatch.setattr(coap_wrapper.device.blocks[ROLLER_BLOCK_ID], "rollerPos", 0) + await hass.helpers.entity_component.async_update_entity("cover.test_name") + await hass.async_block_till_done() + assert hass.states.get("cover.test_name").state == STATE_CLOSED + + monkeypatch.setattr(coap_wrapper.device.blocks[ROLLER_BLOCK_ID], "rollerPos", 100) + await hass.helpers.entity_component.async_update_entity("cover.test_name") + await hass.async_block_till_done() + assert hass.states.get("cover.test_name").state == STATE_OPEN + + +async def test_no_roller_blocks(hass, coap_wrapper, monkeypatch): + """Test device without roller blocks.""" + assert coap_wrapper + + monkeypatch.setattr(coap_wrapper.device.blocks[ROLLER_BLOCK_ID], "type", None) + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, COVER_DOMAIN) + ) + await hass.async_block_till_done() + assert hass.states.get("cover.test_name") is None diff --git a/tests/components/shelly/test_switch.py b/tests/components/shelly/test_switch.py new file mode 100644 index 00000000000..b1dcc05bb80 --- /dev/null +++ b/tests/components/shelly/test_switch.py @@ -0,0 +1,85 @@ +"""The scene tests for the myq platform.""" +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) + +RELAY_BLOCK_ID = 0 + + +async def test_services(hass, coap_wrapper): + """Test device turn on/off services.""" + assert coap_wrapper + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, SWITCH_DOMAIN) + ) + await hass.async_block_till_done() + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.test_name_channel_1"}, + blocking=True, + ) + assert hass.states.get("switch.test_name_channel_1").state == STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.test_name_channel_1"}, + blocking=True, + ) + assert hass.states.get("switch.test_name_channel_1").state == STATE_OFF + + +async def test_update(hass, coap_wrapper, monkeypatch): + """Test device update.""" + assert coap_wrapper + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, SWITCH_DOMAIN) + ) + await hass.async_block_till_done() + + monkeypatch.setattr(coap_wrapper.device.blocks[RELAY_BLOCK_ID], "output", False) + await hass.helpers.entity_component.async_update_entity( + "switch.test_name_channel_1" + ) + await hass.async_block_till_done() + assert hass.states.get("switch.test_name_channel_1").state == STATE_OFF + + monkeypatch.setattr(coap_wrapper.device.blocks[RELAY_BLOCK_ID], "output", True) + await hass.helpers.entity_component.async_update_entity( + "switch.test_name_channel_1" + ) + await hass.async_block_till_done() + assert hass.states.get("switch.test_name_channel_1").state == STATE_ON + + +async def test_no_relay_blocks(hass, coap_wrapper, monkeypatch): + """Test device without relay blocks.""" + assert coap_wrapper + + monkeypatch.setattr(coap_wrapper.device.blocks[RELAY_BLOCK_ID], "type", "roller") + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, SWITCH_DOMAIN) + ) + await hass.async_block_till_done() + assert hass.states.get("switch.test_name_channel_1") is None + + +async def test_device_mode_roller(hass, coap_wrapper, monkeypatch): + """Test switch device in roller mode.""" + assert coap_wrapper + + monkeypatch.setitem(coap_wrapper.device.settings, "mode", "roller") + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, SWITCH_DOMAIN) + ) + await hass.async_block_till_done() + assert hass.states.get("switch.test_name_channel_1") is None From bc2c7b2d486018787078e489432f3b1e565e2832 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 11 Jan 2021 16:47:49 +0100 Subject: [PATCH 170/507] Add Shelly RGB devices management (#43993) * Add support for RGB devices * White value handling * Fixed logic for some devices (ColorTemp, White, Kelvin limits) * Code cleanup * Moved func from utils to light * Fix for DUO * Added "Optional" to properties that need it * Code more understandable * Applied code review suggestions * Applied code review suggestions * Updated logic to always show all available options --- homeassistant/components/shelly/const.py | 5 + homeassistant/components/shelly/light.py | 134 +++++++++++++++--- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 124 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index b63de2e5fe0..a5922d0b9c0 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -70,3 +70,8 @@ INPUTS_EVENTS_SUBTYPES = { "button2": 2, "button3": 3, } + +# Kelvin value for colorTemp +KELVIN_MAX_VALUE = 6500 +KELVIN_MIN_VALUE = 2700 +KELVIN_MIN_VALUE_SHBLB_1 = 3000 diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index b3a6869d67d..0c91ddc1088 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -1,27 +1,47 @@ """Light for Shelly.""" -from typing import Optional +from typing import Optional, Tuple from aioshelly import Block from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, + ATTR_HS_COLOR, + ATTR_WHITE_VALUE, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_COLOR_TEMP, + SUPPORT_WHITE_VALUE, LightEntity, ) from homeassistant.core import callback from homeassistant.util.color import ( + color_hs_to_RGB, + color_RGB_to_hs, color_temperature_kelvin_to_mired, color_temperature_mired_to_kelvin, ) from . import ShellyDeviceWrapper -from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN +from .const import ( + COAP, + DATA_CONFIG_ENTRY, + DOMAIN, + KELVIN_MAX_VALUE, + KELVIN_MIN_VALUE, + KELVIN_MIN_VALUE_SHBLB_1, +) from .entity import ShellyBlockEntity from .utils import async_remove_shelly_entity +def min_kelvin(model: str): + """Kelvin (min) for colorTemp.""" + if model in ["SHBLB-1"]: + return KELVIN_MIN_VALUE_SHBLB_1 + return KELVIN_MIN_VALUE + + 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][COAP] @@ -54,11 +74,17 @@ class ShellyLight(ShellyBlockEntity, LightEntity): """Initialize light.""" super().__init__(wrapper, block) self.control_result = None + self.mode_result = None self._supported_features = 0 - if hasattr(block, "brightness"): + + if hasattr(block, "brightness") or hasattr(block, "gain"): self._supported_features |= SUPPORT_BRIGHTNESS if hasattr(block, "colorTemp"): self._supported_features |= SUPPORT_COLOR_TEMP + if hasattr(block, "white"): + self._supported_features |= SUPPORT_WHITE_VALUE + if hasattr(block, "red") and hasattr(block, "green") and hasattr(block, "blue"): + self._supported_features |= SUPPORT_COLOR @property def supported_features(self) -> int: @@ -73,18 +99,70 @@ class ShellyLight(ShellyBlockEntity, LightEntity): return self.block.output + @property + def mode(self) -> Optional[str]: + """Return the color mode of the light.""" + if self.mode_result: + return self.mode_result["mode"] + + if hasattr(self.block, "mode"): + return self.block.mode + + if ( + hasattr(self.block, "red") + and hasattr(self.block, "green") + and hasattr(self.block, "blue") + ): + return "color" + + return "white" + @property def brightness(self) -> Optional[int]: """Brightness of light.""" - if self.control_result: - brightness = self.control_result["brightness"] + if self.mode == "color": + if self.control_result: + brightness = self.control_result["gain"] + else: + brightness = self.block.gain else: - brightness = self.block.brightness + if self.control_result: + brightness = self.control_result["brightness"] + else: + brightness = self.block.brightness return int(brightness / 100 * 255) + @property + def white_value(self) -> Optional[int]: + """White value of light.""" + if self.control_result: + white = self.control_result["white"] + else: + white = self.block.white + return int(white) + + @property + def hs_color(self) -> Optional[Tuple[float, float]]: + """Return the hue and saturation color value of light.""" + if self.mode == "white": + return color_RGB_to_hs(255, 255, 255) + + if self.control_result: + red = self.control_result["red"] + green = self.control_result["green"] + blue = self.control_result["blue"] + else: + red = self.block.red + green = self.block.green + blue = self.block.blue + return color_RGB_to_hs(red, green, blue) + @property def color_temp(self) -> Optional[float]: """Return the CT color value in mireds.""" + if self.mode == "color": + return None + if self.control_result: color_temp = self.control_result["temp"] else: @@ -93,33 +171,52 @@ class ShellyLight(ShellyBlockEntity, LightEntity): # If you set DUO to max mireds in Shelly app, 2700K, # It reports 0 temp if color_temp == 0: - return self.max_mireds + return min_kelvin(self.wrapper.model) return int(color_temperature_kelvin_to_mired(color_temp)) @property - def min_mireds(self) -> float: + def min_mireds(self) -> Optional[float]: """Return the coldest color_temp that this light supports.""" - return color_temperature_kelvin_to_mired(6500) + return color_temperature_kelvin_to_mired(KELVIN_MAX_VALUE) @property - def max_mireds(self) -> float: + def max_mireds(self) -> Optional[float]: """Return the warmest color_temp that this light supports.""" - return color_temperature_kelvin_to_mired(2700) + return color_temperature_kelvin_to_mired(min_kelvin(self.wrapper.model)) async def async_turn_on(self, **kwargs) -> None: """Turn on light.""" params = {"turn": "on"} if ATTR_BRIGHTNESS in kwargs: - tmp_brightness = kwargs[ATTR_BRIGHTNESS] - params["brightness"] = int(tmp_brightness / 255 * 100) + tmp_brightness = int(kwargs[ATTR_BRIGHTNESS] / 255 * 100) + if hasattr(self.block, "gain"): + params["gain"] = tmp_brightness + if hasattr(self.block, "brightness"): + params["brightness"] = tmp_brightness if ATTR_COLOR_TEMP in kwargs: color_temp = color_temperature_mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]) - if color_temp > 6500: - color_temp = 6500 - elif color_temp < 2700: - color_temp = 2700 + color_temp = min( + KELVIN_MAX_VALUE, max(min_kelvin(self.wrapper.model), color_temp) + ) + # Color temperature change - used only in white mode, switch device mode to white + if self.mode == "color": + self.mode_result = await self.wrapper.device.switch_light_mode("white") + params["red"] = params["green"] = params["blue"] = 255 params["temp"] = int(color_temp) + elif ATTR_HS_COLOR in kwargs: + red, green, blue = color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) + # Color channels change - used only in color mode, switch device mode to color + if self.mode == "white": + self.mode_result = await self.wrapper.device.switch_light_mode("color") + params["red"] = red + params["green"] = green + params["blue"] = blue + elif ATTR_WHITE_VALUE in kwargs: + # White channel change - used only in color mode, switch device mode device to color + if self.mode == "white": + self.mode_result = await self.wrapper.device.switch_light_mode("color") + params["white"] = int(kwargs[ATTR_WHITE_VALUE]) self.control_result = await self.block.set_state(**params) self.async_write_ha_state() @@ -130,6 +227,7 @@ class ShellyLight(ShellyBlockEntity, LightEntity): @callback def _update_callback(self): - """When device updates, clear control result that overrides state.""" + """When device updates, clear control & mode result that overrides state.""" self.control_result = None + self.mode_result = None super()._update_callback() diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 71ee230d83d..923bcdced34 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -3,7 +3,7 @@ "name": "Shelly", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly", - "requirements": ["aioshelly==0.5.1"], + "requirements": ["aioshelly==0.5.3"], "zeroconf": [{ "type": "_http._tcp.local.", "name": "shelly*" }], "codeowners": ["@balloob", "@bieniu", "@thecode", "@chemelli74"] } diff --git a/requirements_all.txt b/requirements_all.txt index 16941bda7ce..82034f49020 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -218,7 +218,7 @@ aiopylgtv==0.3.3 aiorecollect==1.0.1 # homeassistant.components.shelly -aioshelly==0.5.1 +aioshelly==0.5.3 # homeassistant.components.switcher_kis aioswitcher==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b3908f59372..12419afea5f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -134,7 +134,7 @@ aiopylgtv==0.3.3 aiorecollect==1.0.1 # homeassistant.components.shelly -aioshelly==0.5.1 +aioshelly==0.5.3 # homeassistant.components.switcher_kis aioswitcher==1.2.1 From f19b72ea02c11bf4fefb059d5e79e6496b7fe143 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 11 Jan 2021 17:58:59 +0100 Subject: [PATCH 171/507] Drop awarecan from codeowners (#45049) --- CODEOWNERS | 3 +-- homeassistant/components/aws/manifest.json | 2 +- homeassistant/components/nest/manifest.json | 10 ++-------- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index d81247ce629..720320430b1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -56,7 +56,6 @@ homeassistant/components/auth/* @home-assistant/core homeassistant/components/automation/* @home-assistant/core homeassistant/components/avea/* @pattyland homeassistant/components/awair/* @ahayworth @danielsjf -homeassistant/components/aws/* @awarecan homeassistant/components/axis/* @Kane610 homeassistant/components/azure_devops/* @timmo001 homeassistant/components/azure_event_hub/* @eavanvalkenburg @@ -290,7 +289,7 @@ homeassistant/components/neato/* @dshokouhi @Santobert homeassistant/components/nederlandse_spoorwegen/* @YarmoM homeassistant/components/nello/* @pschmitt homeassistant/components/ness_alarm/* @nickw444 -homeassistant/components/nest/* @awarecan @allenporter +homeassistant/components/nest/* @allenporter homeassistant/components/netatmo/* @cgtobi homeassistant/components/netdata/* @fabaff homeassistant/components/nexia/* @bdraco diff --git a/homeassistant/components/aws/manifest.json b/homeassistant/components/aws/manifest.json index fb29418b376..bd9c76cc397 100644 --- a/homeassistant/components/aws/manifest.json +++ b/homeassistant/components/aws/manifest.json @@ -3,5 +3,5 @@ "name": "Amazon Web Services (AWS)", "documentation": "https://www.home-assistant.io/integrations/aws", "requirements": ["aiobotocore==0.11.1"], - "codeowners": ["@awarecan"] + "codeowners": [] } diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 7b282b19b4d..42b790e5612 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,13 +4,7 @@ "config_flow": true, "dependencies": ["ffmpeg", "http"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": [ - "python-nest==4.1.0", - "google-nest-sdm==0.2.8" - ], - "codeowners": [ - "@awarecan", - "@allenporter" - ], + "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.2.8"], + "codeowners": ["@allenporter"], "quality_scale": "platinum" } From 13cdf0ba6310c92758a858405dab60ad371e8a46 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 11 Jan 2021 10:10:02 -1000 Subject: [PATCH 172/507] Cleanups for somfy_mylink (#45026) * Cleanups for somfy_mylink * Use the target/unique_id to configure reverse * Simplify options flow * Various code review cleanups * Deprecate YAML * revert get change * revert get change * add note about empty response * move CONF_DEFAULT_REVERSE out of loop * Update homeassistant/components/somfy_mylink/config_flow.py Co-authored-by: Martin Hjelmare * Ensure we deepcopy options Co-authored-by: Martin Hjelmare --- .../components/somfy_mylink/__init__.py | 68 +++++++--- .../components/somfy_mylink/config_flow.py | 95 ++++++-------- .../components/somfy_mylink/const.py | 11 +- .../components/somfy_mylink/cover.py | 30 ++--- .../components/somfy_mylink/strings.json | 11 +- .../somfy_mylink/translations/en.json | 11 +- .../somfy_mylink/test_config_flow.py | 123 ++++++++++++------ 7 files changed, 205 insertions(+), 144 deletions(-) diff --git a/homeassistant/components/somfy_mylink/__init__.py b/homeassistant/components/somfy_mylink/__init__.py index 5427dee5916..d15ea029530 100644 --- a/homeassistant/components/somfy_mylink/__init__.py +++ b/homeassistant/components/somfy_mylink/__init__.py @@ -5,21 +5,23 @@ import logging from somfy_mylink_synergy import SomfyMyLinkSynergy import voluptuous as vol +from homeassistant.components.cover import ENTITY_ID_FORMAT from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv +from homeassistant.util import slugify from .const import ( CONF_DEFAULT_REVERSE, CONF_ENTITY_CONFIG, CONF_REVERSE, + CONF_REVERSED_TARGET_IDS, CONF_SYSTEM_ID, DATA_SOMFY_MYLINK, DEFAULT_PORT, DOMAIN, - MYLINK_ENTITY_IDS, MYLINK_STATUS, SOMFY_MYLINK_COMPONENTS, ) @@ -44,17 +46,22 @@ def validate_entity_config(values): CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_SYSTEM_ID): cv.string, - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_DEFAULT_REVERSE, default=False): cv.boolean, - vol.Optional(CONF_ENTITY_CONFIG, default={}): validate_entity_config, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_SYSTEM_ID): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_DEFAULT_REVERSE, default=False): cv.boolean, + vol.Optional( + CONF_ENTITY_CONFIG, default={} + ): validate_entity_config, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) @@ -92,19 +99,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): "Unable to connect to the Somfy MyLink device, please check your settings" ) from ex - if "error" in mylink_status: + if not mylink_status or "error" in mylink_status: _LOGGER.error( "mylink failed to setup because of an error: %s", - mylink_status.get("error", {}).get("message"), + mylink_status.get("error", {}).get( + "message", "Empty response from mylink device" + ), ) return False + _async_migrate_entity_config(hass, entry, mylink_status) + undo_listener = entry.add_update_listener(_async_update_listener) hass.data[DOMAIN][entry.entry_id] = { DATA_SOMFY_MYLINK: somfy_mylink, MYLINK_STATUS: mylink_status, - MYLINK_ENTITY_IDS: [], UNDO_UPDATE_LISTENER: undo_listener, } @@ -136,6 +146,34 @@ def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: Confi hass.config_entries.async_update_entry(entry, data=data, options=options) +@callback +def _async_migrate_entity_config( + hass: HomeAssistant, entry: ConfigEntry, mylink_status: dict +): + if CONF_ENTITY_CONFIG not in entry.options: + return + + options = dict(entry.options) + + reversed_target_ids = options[CONF_REVERSED_TARGET_IDS] = {} + legacy_entry_config = options[CONF_ENTITY_CONFIG] + default_reverse = options.get(CONF_DEFAULT_REVERSE) + + for cover in mylink_status["result"]: + legacy_entity_id = ENTITY_ID_FORMAT.format(slugify(cover["name"])) + target_id = cover["targetID"] + + entity_config = legacy_entry_config.get(legacy_entity_id, {}) + if entity_config.get(CONF_REVERSE, default_reverse): + reversed_target_ids[target_id] = True + + for legacy_key in (CONF_DEFAULT_REVERSE, CONF_ENTITY_CONFIG): + if legacy_key in options: + del options[legacy_key] + + hass.config_entries.async_update_entry(entry, data=entry.data, options=options) + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" unload_ok = all( diff --git a/homeassistant/components/somfy_mylink/config_flow.py b/homeassistant/components/somfy_mylink/config_flow.py index 6f66c9899b4..4a955c53df5 100644 --- a/homeassistant/components/somfy_mylink/config_flow.py +++ b/homeassistant/components/somfy_mylink/config_flow.py @@ -1,29 +1,28 @@ """Config flow for Somfy MyLink integration.""" import asyncio +from copy import deepcopy import logging from somfy_mylink_synergy import SomfyMyLinkSynergy import voluptuous as vol from homeassistant import config_entries, core, exceptions -from homeassistant.const import ATTR_FRIENDLY_NAME, CONF_ENTITY_ID, CONF_HOST, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import callback from .const import ( - CONF_DEFAULT_REVERSE, - CONF_ENTITY_CONFIG, CONF_REVERSE, + CONF_REVERSED_TARGET_IDS, CONF_SYSTEM_ID, - DEFAULT_CONF_DEFAULT_REVERSE, + CONF_TARGET_ID, + CONF_TARGET_NAME, DEFAULT_PORT, - MYLINK_ENTITY_IDS, + MYLINK_STATUS, ) from .const import DOMAIN # pylint:disable=unused-import _LOGGER = logging.getLogger(__name__) -ENTITY_CONFIG_VERSION = "entity_config_version" - STEP_USER_DATA_SCHEMA = vol.Schema( { vol.Required(CONF_HOST): str, @@ -114,8 +113,24 @@ class OptionsFlowHandler(config_entries.OptionsFlow): def __init__(self, config_entry: config_entries.ConfigEntry): """Initialize options flow.""" self.config_entry = config_entry - self.options = config_entry.options.copy() - self._entity_id = None + self.options = deepcopy(dict(config_entry.options)) + self._target_id = None + + @callback + def _async_callback_targets(self): + """Return the list of targets.""" + return self.hass.data[DOMAIN][self.config_entry.entry_id][MYLINK_STATUS][ + "result" + ] + + @callback + def _async_get_target_name(self, target_id) -> str: + """Find the name of a target in the api data.""" + mylink_targets = self._async_callback_targets() + for cover in mylink_targets: + if cover["targetID"] == target_id: + return cover["name"] + raise KeyError async def async_step_init(self, user_input=None): """Handle options flow.""" @@ -125,71 +140,45 @@ class OptionsFlowHandler(config_entries.OptionsFlow): return self.async_abort(reason="cannot_connect") if user_input is not None: - self.options[CONF_DEFAULT_REVERSE] = user_input[CONF_DEFAULT_REVERSE] - - entity_id = user_input.get(CONF_ENTITY_ID) - if entity_id: - return await self.async_step_entity_config(None, entity_id) + target_id = user_input.get(CONF_TARGET_ID) + if target_id: + return await self.async_step_target_config(None, target_id) return self.async_create_entry(title="", data=self.options) - data_schema = vol.Schema( - { - vol.Required( - CONF_DEFAULT_REVERSE, - default=self.options.get( - CONF_DEFAULT_REVERSE, DEFAULT_CONF_DEFAULT_REVERSE - ), - ): bool - } - ) - data = self.hass.data[DOMAIN][self.config_entry.entry_id] - mylink_entity_ids = data[MYLINK_ENTITY_IDS] + cover_dict = {None: None} + mylink_targets = self._async_callback_targets() + if mylink_targets: + for cover in mylink_targets: + cover_dict[cover["targetID"]] = cover["name"] - if mylink_entity_ids: - entity_dict = {None: None} - for entity_id in mylink_entity_ids: - name = entity_id - state = self.hass.states.get(entity_id) - if state: - name = state.attributes.get(ATTR_FRIENDLY_NAME, entity_id) - entity_dict[entity_id] = f"{name} ({entity_id})" - data_schema = data_schema.extend( - {vol.Optional(CONF_ENTITY_ID): vol.In(entity_dict)} - ) + data_schema = vol.Schema({vol.Optional(CONF_TARGET_ID): vol.In(cover_dict)}) return self.async_show_form(step_id="init", data_schema=data_schema, errors={}) - async def async_step_entity_config(self, user_input=None, entity_id=None): - """Handle options flow for entity.""" - entities_config = self.options.setdefault(CONF_ENTITY_CONFIG, {}) + async def async_step_target_config(self, user_input=None, target_id=None): + """Handle options flow for target.""" + reversed_target_ids = self.options.setdefault(CONF_REVERSED_TARGET_IDS, {}) if user_input is not None: - entity_config = entities_config.setdefault(self._entity_id, {}) - if entity_config.get(CONF_REVERSE) != user_input[CONF_REVERSE]: - entity_config[CONF_REVERSE] = user_input[CONF_REVERSE] - # If we do not modify a top level key - # the entity config will never be written - self.options.setdefault(ENTITY_CONFIG_VERSION, 0) - self.options[ENTITY_CONFIG_VERSION] += 1 + if user_input[CONF_REVERSE] != reversed_target_ids.get(self._target_id): + reversed_target_ids[self._target_id] = user_input[CONF_REVERSE] return await self.async_step_init() - self._entity_id = entity_id - default_reverse = self.options.get(CONF_DEFAULT_REVERSE, False) - entity_config = entities_config.get(entity_id, {}) + self._target_id = target_id return self.async_show_form( - step_id="entity_config", + step_id="target_config", data_schema=vol.Schema( { vol.Optional( CONF_REVERSE, - default=entity_config.get(CONF_REVERSE, default_reverse), + default=reversed_target_ids.get(target_id, False), ): bool } ), description_placeholders={ - CONF_ENTITY_ID: entity_id, + CONF_TARGET_NAME: self._async_get_target_name(target_id), }, errors={}, ) diff --git a/homeassistant/components/somfy_mylink/const.py b/homeassistant/components/somfy_mylink/const.py index fd4afb67e00..a7cbf864cd9 100644 --- a/homeassistant/components/somfy_mylink/const.py +++ b/homeassistant/components/somfy_mylink/const.py @@ -4,11 +4,16 @@ CONF_ENTITY_CONFIG = "entity_config" CONF_SYSTEM_ID = "system_id" CONF_REVERSE = "reverse" CONF_DEFAULT_REVERSE = "default_reverse" -DEFAULT_CONF_DEFAULT_REVERSE = False +CONF_TARGET_NAME = "target_name" +CONF_REVERSED_TARGET_IDS = "reversed_target_ids" +CONF_TARGET_ID = "target_id" + +DEFAULT_PORT = 44100 + DATA_SOMFY_MYLINK = "somfy_mylink_data" MYLINK_STATUS = "mylink_status" -MYLINK_ENTITY_IDS = "mylink_entity_ids" DOMAIN = "somfy_mylink" + SOMFY_MYLINK_COMPONENTS = ["cover"] + MANUFACTURER = "Somfy" -DEFAULT_PORT = 44100 diff --git a/homeassistant/components/somfy_mylink/cover.py b/homeassistant/components/somfy_mylink/cover.py index eee1ccf3b6f..2725e2da9c7 100644 --- a/homeassistant/components/somfy_mylink/cover.py +++ b/homeassistant/components/somfy_mylink/cover.py @@ -5,20 +5,16 @@ from homeassistant.components.cover import ( DEVICE_CLASS_BLIND, DEVICE_CLASS_SHUTTER, DEVICE_CLASS_WINDOW, - ENTITY_ID_FORMAT, CoverEntity, ) from homeassistant.const import STATE_CLOSED, STATE_OPEN from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.util import slugify from .const import ( - CONF_DEFAULT_REVERSE, - CONF_REVERSE, + CONF_REVERSED_TARGET_IDS, DATA_SOMFY_MYLINK, DOMAIN, MANUFACTURER, - MYLINK_ENTITY_IDS, MYLINK_STATUS, ) @@ -29,26 +25,22 @@ MYLINK_COVER_TYPE_TO_DEVICE_CLASS = {0: DEVICE_CLASS_BLIND, 1: DEVICE_CLASS_SHUT async def async_setup_entry(hass, config_entry, async_add_entities): """Discover and configure Somfy covers.""" + reversed_target_ids = config_entry.options.get(CONF_REVERSED_TARGET_IDS, {}) + data = hass.data[DOMAIN][config_entry.entry_id] mylink_status = data[MYLINK_STATUS] somfy_mylink = data[DATA_SOMFY_MYLINK] - mylink_entity_ids = data[MYLINK_ENTITY_IDS] cover_list = [] for cover in mylink_status["result"]: - entity_id = ENTITY_ID_FORMAT.format(slugify(cover["name"])) - mylink_entity_ids.append(entity_id) - - entity_config = config_entry.options.get(entity_id, {}) - default_reverse = config_entry.options.get(CONF_DEFAULT_REVERSE) - - cover_config = {} - cover_config["target_id"] = cover["targetID"] - cover_config["name"] = cover["name"] - cover_config["device_class"] = MYLINK_COVER_TYPE_TO_DEVICE_CLASS.get( - cover.get("type"), DEVICE_CLASS_WINDOW - ) - cover_config["reverse"] = entity_config.get(CONF_REVERSE, default_reverse) + cover_config = { + "target_id": cover["targetID"], + "name": cover["name"], + "device_class": MYLINK_COVER_TYPE_TO_DEVICE_CLASS.get( + cover.get("type"), DEVICE_CLASS_WINDOW + ), + "reverse": reversed_target_ids.get(cover["targetID"], False), + } cover_list.append(SomfyShade(somfy_mylink, **cover_config)) diff --git a/homeassistant/components/somfy_mylink/strings.json b/homeassistant/components/somfy_mylink/strings.json index bd63fa93d86..f6eed3457d2 100644 --- a/homeassistant/components/somfy_mylink/strings.json +++ b/homeassistant/components/somfy_mylink/strings.json @@ -26,15 +26,14 @@ }, "step": { "init": { - "title": "Configure MyLink Entities", + "title": "Configure MyLink Options", "data": { - "default_reverse": "Default reversal status for unconfigured covers", - "entity_id": "Configure a specific entity." + "target_id": "Configure options for a cover." } }, - "entity_config": { - "title": "Configure Entity", - "description": "Configure options for `{entity_id}`", + "target_config": { + "title": "Configure MyLink Cover", + "description": "Configure options for `{target_name}`", "data": { "reverse": "Cover is reversed" } diff --git a/homeassistant/components/somfy_mylink/translations/en.json b/homeassistant/components/somfy_mylink/translations/en.json index bd63fa93d86..f6eed3457d2 100644 --- a/homeassistant/components/somfy_mylink/translations/en.json +++ b/homeassistant/components/somfy_mylink/translations/en.json @@ -26,15 +26,14 @@ }, "step": { "init": { - "title": "Configure MyLink Entities", + "title": "Configure MyLink Options", "data": { - "default_reverse": "Default reversal status for unconfigured covers", - "entity_id": "Configure a specific entity." + "target_id": "Configure options for a cover." } }, - "entity_config": { - "title": "Configure Entity", - "description": "Configure options for `{entity_id}`", + "target_config": { + "title": "Configure MyLink Cover", + "description": "Configure options for `{target_name}`", "data": { "reverse": "Cover is reversed" } diff --git a/tests/components/somfy_mylink/test_config_flow.py b/tests/components/somfy_mylink/test_config_flow.py index 1a81e28a7c6..6445e5a2535 100644 --- a/tests/components/somfy_mylink/test_config_flow.py +++ b/tests/components/somfy_mylink/test_config_flow.py @@ -2,11 +2,14 @@ import asyncio from unittest.mock import patch +import pytest + from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.somfy_mylink.const import ( CONF_DEFAULT_REVERSE, CONF_ENTITY_CONFIG, CONF_REVERSE, + CONF_REVERSED_TARGET_IDS, CONF_SYSTEM_ID, DOMAIN, ) @@ -294,42 +297,9 @@ async def test_options_not_loaded(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT -async def test_options_no_entities(hass): - """Test we can configure default reverse.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - - config_entry = MockConfigEntry( - domain=DOMAIN, - data={CONF_HOST: "1.1.1.1", CONF_PORT: 12, CONF_SYSTEM_ID: 46}, - ) - config_entry.add_to_hass(hass) - - with patch( - "homeassistant.components.somfy_mylink.SomfyMyLinkSynergy.status_info", - return_value={"result": []}, - ): - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - result = await hass.config_entries.options.async_init(config_entry.entry_id) - await hass.async_block_till_done() - 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={"default_reverse": True}, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert config_entry.options == { - "default_reverse": True, - } - - await hass.async_block_till_done() - - -async def test_options_with_entities(hass): - """Test we can configure reverse for an entity.""" +@pytest.mark.parametrize("reversed", [True, False]) +async def test_options_with_targets(hass, reversed): + """Test we can configure reverse for a target.""" await setup.async_setup_component(hass, "persistent_notification", {}) config_entry = MockConfigEntry( @@ -359,27 +329,96 @@ async def test_options_with_entities(hass): result2 = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={"default_reverse": True, "entity_id": "cover.master_window"}, + user_input={"target_id": "a"}, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM result3 = await hass.config_entries.options.async_configure( result2["flow_id"], - user_input={"reverse": False}, + user_input={"reverse": reversed}, ) assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM result4 = await hass.config_entries.options.async_configure( result3["flow_id"], - user_input={"default_reverse": True, "entity_id": None}, + user_input={"target_id": None}, ) assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { - "default_reverse": True, - "entity_config": {"cover.master_window": {"reverse": False}}, - "entity_config_version": 1, + CONF_REVERSED_TARGET_IDS: {"a": reversed}, + } + + await hass.async_block_till_done() + + +@pytest.mark.parametrize("reversed", [True, False]) +async def test_form_import_with_entity_config_modify_options(hass, reversed): + """Test we can import entity config and modify options.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + mock_imported_config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "1.1.1.1", + CONF_PORT: 1234, + CONF_SYSTEM_ID: 456, + CONF_DEFAULT_REVERSE: True, + CONF_ENTITY_CONFIG: {"cover.xyz": {CONF_REVERSE: False}}, + }, + ) + mock_imported_config_entry.add_to_hass(hass) + + mock_status_info = { + "result": [ + {"targetID": "1.1", "name": "xyz"}, + {"targetID": "1.2", "name": "zulu"}, + ] + } + + with patch( + "homeassistant.components.somfy_mylink.SomfyMyLinkSynergy.status_info", + return_value=mock_status_info, + ): + assert await hass.config_entries.async_setup( + mock_imported_config_entry.entry_id + ) + await hass.async_block_till_done() + + assert mock_imported_config_entry.options == { + "reversed_target_ids": {"1.2": True} + } + + result = await hass.config_entries.options.async_init( + mock_imported_config_entry.entry_id + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"target_id": "1.2"}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + result3 = await hass.config_entries.options.async_configure( + result2["flow_id"], + user_input={"reverse": reversed}, + ) + + assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + + result4 = await hass.config_entries.options.async_configure( + result3["flow_id"], + user_input={"target_id": None}, + ) + assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + # Will not be altered if nothing changes + assert mock_imported_config_entry.options == { + CONF_REVERSED_TARGET_IDS: {"1.2": reversed}, } await hass.async_block_till_done() From bade98624d4201e87afb336d30ee706ae361eeca Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Mon, 11 Jan 2021 23:13:16 +0100 Subject: [PATCH 173/507] Fix tests for input_datetime (#45055) --- tests/components/input_datetime/test_init.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index f6cff819791..e83086f108b 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -131,7 +131,7 @@ async def test_set_datetime(hass): entity_id = "input_datetime.test_datetime" - dt_obj = datetime.datetime(2017, 9, 7, 19, 46, 30) + dt_obj = datetime.datetime(2017, 9, 7, 19, 46, 30, tzinfo=datetime.timezone.utc) await async_set_date_and_time(hass, entity_id, dt_obj) @@ -157,7 +157,7 @@ async def test_set_datetime_2(hass): entity_id = "input_datetime.test_datetime" - dt_obj = datetime.datetime(2017, 9, 7, 19, 46, 30) + dt_obj = datetime.datetime(2017, 9, 7, 19, 46, 30, tzinfo=datetime.timezone.utc) await async_set_datetime(hass, entity_id, dt_obj) @@ -183,7 +183,7 @@ async def test_set_datetime_3(hass): entity_id = "input_datetime.test_datetime" - dt_obj = datetime.datetime(2017, 9, 7, 19, 46, 30) + dt_obj = datetime.datetime(2017, 9, 7, 19, 46, 30, tzinfo=datetime.timezone.utc) await async_set_timestamp(hass, entity_id, dt_util.as_utc(dt_obj).timestamp()) @@ -677,6 +677,7 @@ async def test_timestamp(hass): # 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 + # Timezone LA is UTC-8 => timestamp carries +01:00 => delta is -9 => 10:00 - 09:00 => 01:00 assert state_with_tz.state == "2020-12-13 01:00:00" assert ( dt_util.as_local( @@ -691,6 +692,13 @@ async def test_timestamp(hass): ) assert state_without_tz is not None assert state_without_tz.state == "2020-12-13 10:00:00" + # Timezone LA is UTC-8 => timestamp has no zone (= assumed local) => delta to UTC is +8 => 10:00 + 08:00 => 18:00 + assert ( + dt_util.utc_from_timestamp( + state_without_tz.attributes[ATTR_TIMESTAMP] + ).strftime(FMT_DATETIME) + == "2020-12-13 18:00:00" + ) assert ( dt_util.as_local( dt_util.utc_from_timestamp(state_without_tz.attributes[ATTR_TIMESTAMP]) @@ -701,7 +709,7 @@ async def test_timestamp(hass): assert ( dt_util.as_local( datetime.datetime.fromtimestamp( - state_without_tz.attributes[ATTR_TIMESTAMP] + state_without_tz.attributes[ATTR_TIMESTAMP], datetime.timezone.utc ) ).strftime(FMT_DATETIME) == "2020-12-13 10:00:00" From cad2fa89edfb7fcaa5aeec58ec17c0bdba698053 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Mon, 11 Jan 2021 23:45:58 +0100 Subject: [PATCH 174/507] Default `input_datetime` to current date (#45052) --- .../components/input_datetime/__init__.py | 20 +++++++++++-------- tests/components/input_datetime/test_init.py | 8 +++++--- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index 195e4c2242e..0eab810245d 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -32,8 +32,6 @@ CONF_HAS_DATE = "has_date" CONF_HAS_TIME = "has_time" CONF_INITIAL = "initial" -DEFAULT_VALUE = "1970-01-01 00:00:00" -DEFAULT_DATE = py_datetime.date(1970, 1, 1) DEFAULT_TIME = py_datetime.time(0, 0, 0) ATTR_DATETIME = "datetime" @@ -218,7 +216,9 @@ class InputDatetime(RestoreEntity): else: time = dt_util.parse_time(initial) - current_datetime = py_datetime.datetime.combine(DEFAULT_DATE, time) + current_datetime = py_datetime.datetime.combine( + py_datetime.date.today(), time + ) # If the user passed in an initial value with a timezone, convert it to right tz if current_datetime.tzinfo is not None: @@ -246,32 +246,36 @@ class InputDatetime(RestoreEntity): if self.state is not None: return + default_value = py_datetime.datetime.today().strftime("%Y-%m-%d 00:00:00") + # Priority 2: Old state old_state = await self.async_get_last_state() if old_state is None: - self._current_datetime = dt_util.parse_datetime(DEFAULT_VALUE) + self._current_datetime = dt_util.parse_datetime(default_value) return if self.has_date and self.has_time: date_time = dt_util.parse_datetime(old_state.state) if date_time is None: - current_datetime = dt_util.parse_datetime(DEFAULT_VALUE) + 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: - current_datetime = dt_util.parse_datetime(DEFAULT_VALUE) + 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: - current_datetime = dt_util.parse_datetime(DEFAULT_VALUE) + current_datetime = dt_util.parse_datetime(default_value) else: - current_datetime = py_datetime.datetime.combine(DEFAULT_DATE, time) + current_datetime = py_datetime.datetime.combine( + py_datetime.date.today(), time + ) self._current_datetime = current_datetime.replace( tzinfo=dt_util.DEFAULT_TIME_ZONE diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index e83086f108b..8d9ddf9546d 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -320,7 +320,7 @@ async def test_restore_state(hass): hass.state = CoreState.starting initial = datetime.datetime(2017, 1, 1, 23, 42) - default = datetime.datetime(1970, 1, 1, 0, 0) + default = datetime.datetime.combine(datetime.date.today(), DEFAULT_TIME) await async_setup_component( hass, @@ -375,7 +375,7 @@ async def test_default_value(hass): }, ) - dt_obj = datetime.datetime(1970, 1, 1, 0, 0) + dt_obj = datetime.datetime.combine(datetime.date.today(), DEFAULT_TIME) state_time = hass.states.get("input_datetime.test_time") assert state_time.state == dt_obj.strftime(FMT_TIME) assert state_time.attributes.get("timestamp") is not None @@ -477,7 +477,9 @@ async def test_reload(hass, hass_admin_user, hass_read_only_user): assert state_2 is not None assert state_3 is None assert state_1.state == DEFAULT_TIME.strftime(FMT_TIME) - assert state_2.state == datetime.datetime(1970, 1, 1, 0, 0).strftime(FMT_DATETIME) + assert state_2.state == datetime.datetime.combine( + datetime.date.today(), DEFAULT_TIME + ).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" From f312b87a3f90681d5f1a7fda99939449c2fa0766 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Mon, 11 Jan 2021 18:40:39 -0500 Subject: [PATCH 175/507] Add switch platform to zwave_js (#45046) --- homeassistant/components/zwave_js/const.py | 2 +- .../components/zwave_js/discovery.py | 6 + homeassistant/components/zwave_js/switch.py | 59 ++ tests/components/zwave_js/common.py | 1 + tests/components/zwave_js/conftest.py | 14 + tests/components/zwave_js/test_switch.py | 85 ++ .../zwave_js/hank_binary_switch_state.json | 727 ++++++++++++++++++ 7 files changed, 893 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/zwave_js/switch.py create mode 100644 tests/components/zwave_js/test_switch.py create mode 100644 tests/fixtures/zwave_js/hank_binary_switch_state.json diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index c2a9ac7b3cf..e1a86115d50 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -3,7 +3,7 @@ DOMAIN = "zwave_js" NAME = "Z-Wave JS" -PLATFORMS = ["light", "sensor"] +PLATFORMS = ["light", "sensor", "switch"] DATA_CLIENT = "client" DATA_UNSUBSCRIBE = "unsubs" diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 34f715a2d40..39e9dd8d561 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -100,6 +100,12 @@ DISCOVERY_SCHEMAS = [ }, type={"number"}, ), + # binary switches + ZWaveDiscoverySchema( + platform="switch", + command_class={CommandClass.SWITCH_BINARY}, + property={"currentValue"}, + ), ] diff --git a/homeassistant/components/zwave_js/switch.py b/homeassistant/components/zwave_js/switch.py new file mode 100644 index 00000000000..e1606695599 --- /dev/null +++ b/homeassistant/components/zwave_js/switch.py @@ -0,0 +1,59 @@ +"""Representation of Z-Wave switches.""" + +import logging +from typing import Any, Callable, List + +from zwave_js_server.client import Client as ZwaveClient + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN +from .discovery import ZwaveDiscoveryInfo +from .entity import ZWaveBaseEntity + +LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable +) -> None: + """Set up Z-Wave sensor from config entry.""" + client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] + + @callback + def async_add_switch(info: ZwaveDiscoveryInfo) -> None: + """Add Z-Wave Switch.""" + entities: List[ZWaveBaseEntity] = [] + entities.append(ZWaveSwitch(client, info)) + + async_add_entities(entities) + + hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + async_dispatcher_connect( + hass, f"{DOMAIN}_add_{SWITCH_DOMAIN}", async_add_switch + ) + ) + + +class ZWaveSwitch(ZWaveBaseEntity, SwitchEntity): + """Representation of a Z-Wave switch.""" + + @property + def is_on(self) -> bool: + """Return a boolean for the state of the switch.""" + return bool(self.info.primary_value.value) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the switch on.""" + target_value = self.get_zwave_value("targetValue") + if target_value is not None: + await self.info.node.async_set_value(target_value, True) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the switch off.""" + target_value = self.get_zwave_value("targetValue") + if target_value is not None: + await self.info.node.async_set_value(target_value, False) diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py index 8d2a2195913..c78c6d544b6 100644 --- a/tests/components/zwave_js/common.py +++ b/tests/components/zwave_js/common.py @@ -1,2 +1,3 @@ """Provide common test tools for Z-Wave JS.""" AIR_TEMPERATURE_SENSOR = "sensor.multisensor_6_air_temperature" +SWITCH_ENTITY = "switch.smart_plug_with_two_usb_ports_current_value" diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 8dd24aaa3ab..389245b32a3 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -31,6 +31,12 @@ def multisensor_6_state_fixture(): return json.loads(load_fixture("zwave_js/multisensor_6_state.json")) +@pytest.fixture(name="hank_binary_switch_state", scope="session") +def binary_switch_state_fixture(): + """Load the hank binary switch node state fixture data.""" + return json.loads(load_fixture("zwave_js/hank_binary_switch_state.json")) + + @pytest.fixture(name="client") def mock_client_fixture(controller_state): """Mock a client.""" @@ -50,6 +56,14 @@ def multisensor_6_fixture(client, multisensor_6_state): return node +@pytest.fixture(name="hank_binary_switch") +def hank_binary_switch_fixture(client, hank_binary_switch_state): + """Mock a binary switch node.""" + node = Node(client, hank_binary_switch_state) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="integration") async def integration_fixture(hass, client): """Set up the zwave_js integration.""" diff --git a/tests/components/zwave_js/test_switch.py b/tests/components/zwave_js/test_switch.py new file mode 100644 index 00000000000..09d893a1906 --- /dev/null +++ b/tests/components/zwave_js/test_switch.py @@ -0,0 +1,85 @@ +"""Test the Z-Wave JS sensor platform.""" + +from zwave_js_server.event import Event + +from .common import SWITCH_ENTITY + + +async def test_switch(hass, hank_binary_switch, integration, client): + """Test the switch.""" + state = hass.states.get(SWITCH_ENTITY) + node = hank_binary_switch + + assert state + assert state.state == "off" + + # Test turning on + await hass.services.async_call( + "switch", "turn_on", {"entity_id": SWITCH_ENTITY}, blocking=True + ) + + args = client.async_send_json_message.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 32 + assert args["valueId"] == { + "commandClassName": "Binary Switch", + "commandClass": 37, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "type": "boolean", + "readable": True, + "writeable": True, + "label": "Target value", + }, + "value": False, + } + assert args["value"] is True + + # Test state updates from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 32, + "args": { + "commandClassName": "Binary Switch", + "commandClass": 37, + "endpoint": 0, + "property": "currentValue", + "newValue": True, + "prevValue": False, + "propertyName": "currentValue", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(SWITCH_ENTITY) + assert state.state == "on" + + # Test turning off + await hass.services.async_call( + "switch", "turn_off", {"entity_id": SWITCH_ENTITY}, blocking=True + ) + + args = client.async_send_json_message.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 32 + assert args["valueId"] == { + "commandClassName": "Binary Switch", + "commandClass": 37, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "type": "boolean", + "readable": True, + "writeable": True, + "label": "Target value", + }, + "value": False, + } + assert args["value"] is False diff --git a/tests/fixtures/zwave_js/hank_binary_switch_state.json b/tests/fixtures/zwave_js/hank_binary_switch_state.json new file mode 100644 index 00000000000..0c629b3cf99 --- /dev/null +++ b/tests/fixtures/zwave_js/hank_binary_switch_state.json @@ -0,0 +1,727 @@ +{ + "nodeId": 32, + "index": 0, + "installerIcon": 1792, + "userIcon": 1792, + "status": 4, + "ready": true, + "deviceClass": { + "basic": "Static Controller", + "generic": "Binary Switch", + "specific": "Binary Power Switch", + "mandatorySupportedCCs": [ + "Basic", + "Binary Switch", + "All Switch" + ], + "mandatoryControlCCs": [] + }, + "isListening": true, + "isFrequentListening": false, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": false, + "version": 4, + "isBeaming": true, + "manufacturerId": 520, + "productId": 5, + "productType": 257, + "firmwareVersion": "1.5", + "zwavePlusVersion": 1, + "nodeType": 0, + "roleType": 5, + "deviceConfig": { + "manufacturerId": 520, + "manufacturer": "HANK Electronics Ltd.", + "label": "HKZW-SO01", + "description": "Smart Plug with two USB ports", + "devices": [ + { + "productType": "0x0101", + "productId": "0x0005" + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + } + }, + "label": "HKZW-SO01", + "neighbors": [ + 1, + 33, + 36, + 37, + 39, + 52 + ], + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 32, + "index": 0, + "installerIcon": 1792, + "userIcon": 1792 + } + ], + "values": [ + { + "commandClassName": "Binary Switch", + "commandClass": 37, + "endpoint": 0, + "property": "currentValue", + "propertyName": "currentValue", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value" + }, + "value": false + }, + { + "commandClassName": "Binary Switch", + "commandClass": 37, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value" + }, + "value": false + }, + { + "commandClassName": "Scene Activation", + "commandClass": 43, + "endpoint": 0, + "property": "sceneId", + "propertyName": "sceneId", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 1, + "max": 255, + "label": "Scene ID" + } + }, + { + "commandClassName": "Scene Activation", + "commandClass": 43, + "endpoint": 0, + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Dimming duration" + } + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "value", + "propertyKey": 66049, + "propertyName": "value", + "propertyKeyName": "W_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Value (Electric, Consumed)", + "unit": "W", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 2 + } + }, + "value": 0 + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "deltaTime", + "propertyKey": 66049, + "propertyName": "deltaTime", + "propertyKeyName": "W_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Time since the previous reading", + "unit": "s", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 2 + } + }, + "value": 0 + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "value", + "propertyKey": 65537, + "propertyName": "value", + "propertyKeyName": "kWh_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Value (Electric, Consumed)", + "unit": "kWh", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 0 + } + }, + "value": 0.164 + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "previousValue", + "propertyKey": 65537, + "propertyName": "previousValue", + "propertyKeyName": "kWh_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Previous value (Electric, Consumed)", + "unit": "kWh", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 0 + } + }, + "value": 0.164 + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "deltaTime", + "propertyKey": 65537, + "propertyName": "deltaTime", + "propertyKeyName": "kWh_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Time since the previous reading", + "unit": "s", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 0 + } + }, + "value": 30 + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "value", + "propertyKey": 66561, + "propertyName": "value", + "propertyKeyName": "V_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Value (Electric, Consumed)", + "unit": "V", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 4 + } + }, + "value": 122.963 + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "deltaTime", + "propertyKey": 66561, + "propertyName": "deltaTime", + "propertyKeyName": "V_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Time since the previous reading", + "unit": "s", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 4 + } + }, + "value": 0 + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "value", + "propertyKey": 66817, + "propertyName": "value", + "propertyKeyName": "A_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Value (Electric, Consumed)", + "unit": "A", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 5 + } + }, + "value": 0 + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "deltaTime", + "propertyKey": 66817, + "propertyName": "deltaTime", + "propertyKeyName": "A_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Time since the previous reading", + "unit": "s", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 5 + } + }, + "value": 0 + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "reset", + "propertyName": "reset", + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Reset accumulated values" + } + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "previousValue", + "propertyKey": 66049, + "propertyName": "previousValue", + "propertyKeyName": "W_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Previous value (Electric, Consumed)", + "unit": "W", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 2 + } + } + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "previousValue", + "propertyKey": 66561, + "propertyName": "previousValue", + "propertyKeyName": "V_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Previous value (Electric, Consumed)", + "unit": "V", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 4 + } + } + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "previousValue", + "propertyKey": 66817, + "propertyName": "previousValue", + "propertyKeyName": "A_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Previous value (Electric, Consumed)", + "unit": "A", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 5 + } + } + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 20, + "propertyName": "Overload Protection", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Overload Protection", + "description": "If current exceeds 16.5A over 5 seconds, relay will turn off.", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 21, + "propertyName": "Device Status after Power Failure", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 2, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Device Status after Power Failure", + "description": "Define how the plug reacts after power failure", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 24, + "propertyName": "Notifcation on Load Change", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 2, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Notifcation on Load Change", + "description": "Smart Plug can send notifications to association device load state changes.", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 27, + "propertyName": "Indicator Modes", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Indicator Modes", + "description": "LED in the device will indicate the state of load", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 151, + "propertyName": "Threshold of power report", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 0, + "max": 65535, + "default": 50, + "format": 1, + "allowManualEntry": true, + "label": "Threshold of power report", + "description": "Power Threshold at which to send meter report", + "isFromConfig": true + }, + "value": 50 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 152, + "propertyName": "Percentage Threshold of Power Report", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 10, + "format": 1, + "allowManualEntry": true, + "label": "Percentage Threshold of Power Report", + "description": "Percentage Threshold at which to send meter report", + "isFromConfig": true + }, + "value": 10 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 171, + "propertyName": "Power Report Frequency", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 5, + "max": 2678400, + "default": 30, + "format": 0, + "allowManualEntry": true, + "label": "Power Report Frequency", + "description": "The interval of sending power report to association device (Group Lifeline).", + "isFromConfig": true + }, + "value": 30 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 172, + "propertyName": "Energy Report Frequency", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 5, + "max": 2678400, + "default": 300, + "format": 0, + "allowManualEntry": true, + "label": "Energy Report Frequency", + "description": "The interval of sending energy report to association device (Group Lifeline).", + "isFromConfig": true + }, + "value": 300 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 173, + "propertyName": "Voltage Report Frequency", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 2678400, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Voltage Report Frequency", + "description": "The interval of sending voltage report to association device (Group Lifeline)", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 174, + "propertyName": "Electricity Report Frequency", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 2678400, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Electricity Report Frequency", + "description": "Interval for sending electricity report.", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "manufacturerId", + "propertyName": "manufacturerId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + }, + "value": 520 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productType", + "propertyName": "productType", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + }, + "value": 257 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productId", + "propertyName": "productId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + }, + "value": 5 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "libraryType", + "propertyName": "libraryType", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Libary type" + }, + "value": 3 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "protocolVersion", + "propertyName": "protocolVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.24" + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": [ + "1.5" + ] + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + } + ] +} \ No newline at end of file From e83ced673708f7659ff4205c1bbd5cd665c25293 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 12 Jan 2021 09:26:20 +0100 Subject: [PATCH 176/507] Add name to ignored entries (#45051) * Add name to ignored entries * Fix test --- homeassistant/components/config/config_entries.py | 6 ++++-- homeassistant/config_entries.py | 2 +- tests/components/config/test_config_entries.py | 2 ++ tests/helpers/test_config_entry_flow.py | 2 +- tests/test_config_entries.py | 12 ++++++++---- 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index f67bfb98641..b8d9944d7af 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -318,7 +318,9 @@ async def config_entry_update(hass, connection, msg): @websocket_api.require_admin @websocket_api.async_response -@websocket_api.websocket_command({"type": "config_entries/ignore_flow", "flow_id": str}) +@websocket_api.websocket_command( + {"type": "config_entries/ignore_flow", "flow_id": str, "title": str} +) async def ignore_config_flow(hass, connection, msg): """Ignore a config flow.""" flow = next( @@ -345,7 +347,7 @@ async def ignore_config_flow(hass, connection, msg): await hass.config_entries.flow.async_init( flow["handler"], context={"source": config_entries.SOURCE_IGNORE}, - data={"unique_id": flow["context"]["unique_id"]}, + data={"unique_id": flow["context"]["unique_id"], "title": msg["title"]}, ) connection.send_result(msg["id"]) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 601ce1efbfe..f87e76edec8 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -975,7 +975,7 @@ class ConfigFlow(data_entry_flow.FlowHandler): async def async_step_ignore(self, user_input: Dict[str, Any]) -> Dict[str, Any]: """Ignore this config flow.""" await self.async_set_unique_id(user_input["unique_id"], raise_on_progress=False) - return self.async_create_entry(title="Ignored", data={}) + return self.async_create_entry(title=user_input["title"], data={}) async def async_step_unignore(self, user_input: Dict[str, Any]) -> Dict[str, Any]: """Rediscover a config entry by it's unique_id.""" diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 6873bc8311a..87b1559a21b 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -750,6 +750,7 @@ async def test_ignore_flow(hass, hass_ws_client): "id": 5, "type": "config_entries/ignore_flow", "flow_id": result["flow_id"], + "title": "Test Integration", } ) response = await ws_client.receive_json() @@ -761,3 +762,4 @@ async def test_ignore_flow(hass, hass_ws_client): entry = hass.config_entries.async_entries("test")[0] assert entry.source == "ignore" assert entry.unique_id == "mock-unique-id" + assert entry.title == "Test Integration" diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index a70f6ad8d5c..b5ba206f908 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -220,7 +220,7 @@ async def test_ignored_discoveries(hass, discovery_flow_conf): await hass.config_entries.flow.async_init( flow["handler"], context={"source": config_entries.SOURCE_IGNORE}, - data={"unique_id": flow["context"]["unique_id"]}, + data={"unique_id": flow["context"]["unique_id"], "title": "Ignored Entry"}, ) # Second discovery should be aborted diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index a048f5a7043..56387069fe4 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1523,7 +1523,7 @@ async def test_unique_id_ignore(hass, manager): result2 = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_IGNORE}, - data={"unique_id": "mock-unique-id"}, + data={"unique_id": "mock-unique-id", "title": "Ignored Title"}, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -1537,6 +1537,7 @@ async def test_unique_id_ignore(hass, manager): assert entry.source == "ignore" assert entry.unique_id == "mock-unique-id" + assert entry.title == "Ignored Title" async def test_manual_add_overrides_ignored_entry(hass, manager): @@ -1605,7 +1606,7 @@ async def test_unignore_step_form(hass, manager): result = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_IGNORE}, - data={"unique_id": "mock-unique-id"}, + data={"unique_id": "mock-unique-id", "title": "Ignored Title"}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -1613,6 +1614,7 @@ async def test_unignore_step_form(hass, manager): assert entry.source == "ignore" assert entry.unique_id == "mock-unique-id" assert entry.domain == "comp" + assert entry.title == "Ignored Title" await manager.async_remove(entry.entry_id) @@ -1649,7 +1651,7 @@ async def test_unignore_create_entry(hass, manager): result = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_IGNORE}, - data={"unique_id": "mock-unique-id"}, + data={"unique_id": "mock-unique-id", "title": "Ignored Title"}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -1657,6 +1659,7 @@ async def test_unignore_create_entry(hass, manager): assert entry.source == "ignore" assert entry.unique_id == "mock-unique-id" assert entry.domain == "comp" + assert entry.title == "Ignored Title" await manager.async_remove(entry.entry_id) @@ -1690,7 +1693,7 @@ async def test_unignore_default_impl(hass, manager): result = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_IGNORE}, - data={"unique_id": "mock-unique-id"}, + data={"unique_id": "mock-unique-id", "title": "Ignored Title"}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -1698,6 +1701,7 @@ async def test_unignore_default_impl(hass, manager): assert entry.source == "ignore" assert entry.unique_id == "mock-unique-id" assert entry.domain == "comp" + assert entry.title == "Ignored Title" await manager.async_remove(entry.entry_id) await hass.async_block_till_done() From 8ce32d67f9640f7c42cb923ffdc066fca80c23b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 12 Jan 2021 10:33:14 +0100 Subject: [PATCH 177/507] Fallback to tag for any AfterShip tracking that have no checkpoints (#45053) --- homeassistant/components/aftership/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/aftership/sensor.py b/homeassistant/components/aftership/sensor.py index 07267c1185d..2d9021f8009 100644 --- a/homeassistant/components/aftership/sensor.py +++ b/homeassistant/components/aftership/sensor.py @@ -181,8 +181,8 @@ class AfterShipSensor(Entity): track["tracking_number"] if track["title"] is None else track["title"] ) last_checkpoint = ( - "Shipment pending" - if track["tag"] == "Pending" + f"Shipment {track['tag'].lower()}" + if not track["checkpoints"] else track["checkpoints"][-1] ) status_counts[status] = status_counts.get(status, 0) + 1 From be2aba6c52998be068ec2b18cc2762032fe97b86 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Tue, 12 Jan 2021 08:29:12 -0500 Subject: [PATCH 178/507] Fix docstring in zwave_js switch test (#45076) --- tests/components/zwave_js/test_switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/zwave_js/test_switch.py b/tests/components/zwave_js/test_switch.py index 09d893a1906..09745bf3d32 100644 --- a/tests/components/zwave_js/test_switch.py +++ b/tests/components/zwave_js/test_switch.py @@ -1,4 +1,4 @@ -"""Test the Z-Wave JS sensor platform.""" +"""Test the Z-Wave JS switch platform.""" from zwave_js_server.event import Event From 4e71be852af8b42a3f43c8da79df4bb956439a80 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 12 Jan 2021 16:18:06 +0100 Subject: [PATCH 179/507] Bump Z-Wave JS to 0.7.1 (#45080) --- homeassistant/components/zwave_js/manifest.json | 10 +++------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index ef48b5bfd12..792db8af6ef 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,10 +3,6 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": [ - "zwave-js-server-python==0.7.0" - ], - "codeowners": [ - "@home-assistant/z-wave" - ] -} \ No newline at end of file + "requirements": ["zwave-js-server-python==0.7.1"], + "codeowners": ["@home-assistant/z-wave"] +} diff --git a/requirements_all.txt b/requirements_all.txt index 82034f49020..d0c2af809a7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2372,4 +2372,4 @@ zigpy==0.29.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.7.0 +zwave-js-server-python==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 12419afea5f..199e3c351a3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1171,4 +1171,4 @@ zigpy-znp==0.3.0 zigpy==0.29.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.7.0 +zwave-js-server-python==0.7.1 From 82746616fa92abc89a3a2b2e692c50ed4df2f97e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 13 Jan 2021 00:05:30 +0100 Subject: [PATCH 180/507] Cloud: Add web socket API to pick default TTS language (#45064) * Allow picking default TTS language * Fix test * Fix coroutine function * Improve test coverage * Remove stale import * Clean up hass Co-authored-by: Martin Hjelmare --- homeassistant/components/cloud/const.py | 2 + homeassistant/components/cloud/http_api.py | 14 ++++ homeassistant/components/cloud/prefs.py | 10 +++ homeassistant/components/cloud/tts.py | 25 +++++-- tests/components/cloud/test_http_api.py | 27 +++++++- tests/components/cloud/test_tts.py | 77 +++++++++++++++++++++- 6 files changed, 144 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index 3c7804970fb..d0417e0d38d 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -20,6 +20,8 @@ PREF_GOOGLE_LOCAL_WEBHOOK_ID = "google_local_webhook_id" PREF_USERNAME = "username" PREF_ALEXA_DEFAULT_EXPOSE = "alexa_default_expose" PREF_GOOGLE_DEFAULT_EXPOSE = "google_default_expose" +PREF_TTS_DEFAULT_VOICE = "tts_default_voice" +DEFAULT_TTS_DEFAULT_VOICE = ("en-US", "female") DEFAULT_DISABLE_2FA = False DEFAULT_ALEXA_REPORT_STATE = False DEFAULT_GOOGLE_REPORT_STATE = False diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index a4d8b84b1ad..2bcc37fec05 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -8,6 +8,7 @@ import async_timeout import attr from hass_nabucasa import Cloud, auth, thingtalk from hass_nabucasa.const import STATE_DISCONNECTED +from hass_nabucasa.voice import MAP_VOICE import voluptuous as vol from homeassistant.components import websocket_api @@ -37,6 +38,7 @@ from .const import ( PREF_GOOGLE_DEFAULT_EXPOSE, PREF_GOOGLE_REPORT_STATE, PREF_GOOGLE_SECURE_DEVICES_PIN, + PREF_TTS_DEFAULT_VOICE, REQUEST_TIMEOUT, InvalidTrustedNetworks, InvalidTrustedProxies, @@ -115,6 +117,7 @@ async def async_setup(hass): async_register_command(alexa_sync) async_register_command(thingtalk_convert) + async_register_command(tts_info) hass.http.register_view(GoogleActionsSyncView) hass.http.register_view(CloudLoginView) @@ -385,6 +388,9 @@ async def websocket_subscription(hass, connection, msg): vol.Optional(PREF_ALEXA_DEFAULT_EXPOSE): [str], vol.Optional(PREF_GOOGLE_DEFAULT_EXPOSE): [str], vol.Optional(PREF_GOOGLE_SECURE_DEVICES_PIN): vol.Any(None, str), + vol.Optional(PREF_TTS_DEFAULT_VOICE): vol.All( + vol.Coerce(tuple), vol.In(MAP_VOICE) + ), } ) async def websocket_update_prefs(hass, connection, msg): @@ -637,3 +643,11 @@ async def thingtalk_convert(hass, connection, msg): ) except thingtalk.ThingTalkConversionError as err: connection.send_error(msg["id"], ws_const.ERR_UNKNOWN_ERROR, str(err)) + + +@websocket_api.websocket_command({"type": "cloud/tts/info"}) +def tts_info(hass, connection, msg): + """Fetch available tts info.""" + connection.send_result( + msg["id"], {"languages": [(lang, gender.value) for lang, gender in MAP_VOICE]} + ) diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index 6e0e78839c1..a15eafc4d08 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -12,6 +12,7 @@ from .const import ( DEFAULT_ALEXA_REPORT_STATE, DEFAULT_EXPOSED_DOMAINS, DEFAULT_GOOGLE_REPORT_STATE, + DEFAULT_TTS_DEFAULT_VOICE, DOMAIN, PREF_ALEXA_DEFAULT_EXPOSE, PREF_ALEXA_ENTITY_CONFIGS, @@ -30,6 +31,7 @@ from .const import ( PREF_GOOGLE_SECURE_DEVICES_PIN, PREF_OVERRIDE_NAME, PREF_SHOULD_EXPOSE, + PREF_TTS_DEFAULT_VOICE, PREF_USERNAME, InvalidTrustedNetworks, InvalidTrustedProxies, @@ -86,6 +88,7 @@ class CloudPreferences: google_report_state=UNDEFINED, alexa_default_expose=UNDEFINED, google_default_expose=UNDEFINED, + tts_default_voice=UNDEFINED, ): """Update user preferences.""" prefs = {**self._prefs} @@ -103,6 +106,7 @@ class CloudPreferences: (PREF_GOOGLE_REPORT_STATE, google_report_state), (PREF_ALEXA_DEFAULT_EXPOSE, alexa_default_expose), (PREF_GOOGLE_DEFAULT_EXPOSE, google_default_expose), + (PREF_TTS_DEFAULT_VOICE, tts_default_voice), ): if value is not UNDEFINED: prefs[key] = value @@ -203,6 +207,7 @@ class CloudPreferences: PREF_GOOGLE_ENTITY_CONFIGS: self.google_entity_configs, PREF_GOOGLE_REPORT_STATE: self.google_report_state, PREF_GOOGLE_SECURE_DEVICES_PIN: self.google_secure_devices_pin, + PREF_TTS_DEFAULT_VOICE: self.tts_default_voice, } @property @@ -279,6 +284,11 @@ class CloudPreferences: """Return the published cloud webhooks.""" return self._prefs.get(PREF_CLOUDHOOKS, {}) + @property + def tts_default_voice(self): + """Return the default TTS voice.""" + return self._prefs.get(PREF_TTS_DEFAULT_VOICE, DEFAULT_TTS_DEFAULT_VOICE) + async def get_cloud_user(self) -> str: """Return ID from Home Assistant Cloud system user.""" user = await self._load_cloud_user() diff --git a/homeassistant/components/cloud/tts.py b/homeassistant/components/cloud/tts.py index 9dd392a12c5..4d19547d30c 100644 --- a/homeassistant/components/cloud/tts.py +++ b/homeassistant/components/cloud/tts.py @@ -12,13 +12,14 @@ CONF_GENDER = "gender" SUPPORT_LANGUAGES = list({key[0] for key in MAP_VOICE}) -DEFAULT_LANG = "en-US" -DEFAULT_GENDER = "female" - def validate_lang(value): """Validate chosen gender or language.""" - lang = value[CONF_LANG] + lang = value.get(CONF_LANG) + + if lang is None: + return value + gender = value.get(CONF_GENDER) if gender is None: @@ -35,7 +36,7 @@ def validate_lang(value): PLATFORM_SCHEMA = vol.All( PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_LANG, default=DEFAULT_LANG): str, + vol.Optional(CONF_LANG): str, vol.Optional(CONF_GENDER): str, } ), @@ -48,8 +49,8 @@ async def async_get_engine(hass, config, discovery_info=None): cloud: Cloud = hass.data[DOMAIN] if discovery_info is not None: - language = DEFAULT_LANG - gender = DEFAULT_GENDER + language = None + gender = None else: language = config[CONF_LANG] gender = config[CONF_GENDER] @@ -67,6 +68,16 @@ class CloudProvider(Provider): self._language = language self._gender = gender + if self._language is not None: + return + + self._language, self._gender = cloud.client.prefs.tts_default_voice + cloud.client.prefs.async_listen_updates(self._sync_prefs) + + async def _sync_prefs(self, prefs): + """Sync preferences.""" + self._language, self._gender = prefs.tts_default_voice + @property def default_language(self): """Return the default language.""" diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 047a69184ba..80641c304be 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -4,7 +4,7 @@ from ipaddress import ip_network from unittest.mock import AsyncMock, MagicMock, Mock, patch import aiohttp -from hass_nabucasa import thingtalk +from hass_nabucasa import thingtalk, voice from hass_nabucasa.auth import Unauthenticated, UnknownError from hass_nabucasa.const import STATE_CONNECTED from jose import jwt @@ -361,6 +361,7 @@ async def test_websocket_status( "alexa_report_state": False, "google_report_state": False, "remote_enabled": False, + "tts_default_voice": ["en-US", "female"], }, "alexa_entities": { "include_domains": [], @@ -491,6 +492,7 @@ async def test_websocket_update_preferences( "google_secure_devices_pin": "1234", "google_default_expose": ["light", "switch"], "alexa_default_expose": ["sensor", "media_player"], + "tts_default_voice": ["en-GB", "male"], } ) response = await client.receive_json() @@ -501,6 +503,7 @@ async def test_websocket_update_preferences( assert setup_api.google_secure_devices_pin == "1234" assert setup_api.google_default_expose == ["light", "switch"] assert setup_api.alexa_default_expose == ["sensor", "media_player"] + assert setup_api.tts_default_voice == ("en-GB", "male") async def test_websocket_update_preferences_require_relink( @@ -975,3 +978,25 @@ async def test_thingtalk_convert_internal(hass, hass_ws_client, setup_api): assert not response["success"] assert response["error"]["code"] == "unknown_error" assert response["error"]["message"] == "Did not understand" + + +async def test_tts_info(hass, hass_ws_client, setup_api): + """Test that we can get TTS info.""" + # Verify the format is as expected + assert voice.MAP_VOICE[("en-US", voice.Gender.FEMALE)] == "JennyNeural" + + client = await hass_ws_client(hass) + + with patch.dict( + "homeassistant.components.cloud.http_api.MAP_VOICE", + { + ("en-US", voice.Gender.MALE): "GuyNeural", + ("en-US", voice.Gender.FEMALE): "JennyNeural", + }, + clear=True, + ): + await client.send_json({"id": 5, "type": "cloud/tts/info"}) + response = await client.receive_json() + + assert response["success"] + assert response["result"] == {"languages": [["en-US", "male"], ["en-US", "female"]]} diff --git a/tests/components/cloud/test_tts.py b/tests/components/cloud/test_tts.py index 32a4ca7cb50..23760956935 100644 --- a/tests/components/cloud/test_tts.py +++ b/tests/components/cloud/test_tts.py @@ -1,5 +1,22 @@ """Tests for cloud tts.""" -from homeassistant.components.cloud import tts +from unittest.mock import Mock + +from hass_nabucasa import voice +import pytest +import voluptuous as vol + +from homeassistant.components.cloud import const, tts + + +@pytest.fixture() +def cloud_with_prefs(cloud_prefs): + """Return a cloud mock with prefs.""" + return Mock(client=Mock(prefs=cloud_prefs)) + + +def test_default_exists(): + """Test our default language exists.""" + assert const.DEFAULT_TTS_DEFAULT_VOICE in voice.MAP_VOICE def test_schema(): @@ -9,7 +26,61 @@ def test_schema(): processed = tts.PLATFORM_SCHEMA({"platform": "cloud", "language": "nl-NL"}) assert processed["gender"] == "female" + with pytest.raises(vol.Invalid): + tts.PLATFORM_SCHEMA( + {"platform": "cloud", "language": "non-existing", "gender": "female"} + ) + + with pytest.raises(vol.Invalid): + tts.PLATFORM_SCHEMA( + {"platform": "cloud", "language": "nl-NL", "gender": "not-supported"} + ) + # Should not raise - processed = tts.PLATFORM_SCHEMA( - {"platform": "cloud", "language": "nl-NL", "gender": "female"} + tts.PLATFORM_SCHEMA({"platform": "cloud", "language": "nl-NL", "gender": "female"}) + tts.PLATFORM_SCHEMA({"platform": "cloud"}) + + +async def test_prefs_default_voice(hass, cloud_with_prefs, cloud_prefs): + """Test cloud provider uses the preferences.""" + assert cloud_prefs.tts_default_voice == ("en-US", "female") + + provider_pref = await tts.async_get_engine( + Mock(data={const.DOMAIN: cloud_with_prefs}), None, {} ) + provider_conf = await tts.async_get_engine( + Mock(data={const.DOMAIN: cloud_with_prefs}), + {"language": "fr-FR", "gender": "female"}, + None, + ) + + assert provider_pref.default_language == "en-US" + assert provider_pref.default_options == {"gender": "female"} + assert provider_conf.default_language == "fr-FR" + assert provider_conf.default_options == {"gender": "female"} + + await cloud_prefs.async_update(tts_default_voice=("nl-NL", "male")) + await hass.async_block_till_done() + + assert provider_pref.default_language == "nl-NL" + assert provider_pref.default_options == {"gender": "male"} + assert provider_conf.default_language == "fr-FR" + assert provider_conf.default_options == {"gender": "female"} + + +async def test_provider_properties(cloud_with_prefs): + """Test cloud provider.""" + provider = await tts.async_get_engine( + Mock(data={const.DOMAIN: cloud_with_prefs}), None, {} + ) + assert provider.supported_options == ["gender"] + assert "nl-NL" in provider.supported_languages + + +async def test_get_tts_audio(cloud_with_prefs): + """Test cloud provider.""" + provider = await tts.async_get_engine( + Mock(data={const.DOMAIN: cloud_with_prefs}), None, {} + ) + assert provider.supported_options == ["gender"] + assert "nl-NL" in provider.supported_languages From eebd0d333e158f54a500cbb2fefbc28a0e793242 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 12 Jan 2021 22:08:59 -0800 Subject: [PATCH 181/507] Clear cached nest event images after expiration (#44956) * Clear cached nest event images after expiration * Don't share removal cleanup with alarm cleanup Don't share code across these functions since it would require a dummy timestamp values that is unnecessary. * Increase test coverage on sdm camera remove * Update homeassistant/components/nest/camera_sdm.py Co-authored-by: Paulus Schoutsen Co-authored-by: Paulus Schoutsen --- homeassistant/components/nest/camera_sdm.py | 42 ++++++-- tests/components/nest/camera_sdm_test.py | 110 ++++++++++++++++++-- 2 files changed, 134 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index 262ed3325b2..aa8e100059a 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -66,6 +66,7 @@ class NestCamera(Camera): # Cache of most recent event image self._event_id = None self._event_image_bytes = None + self._event_image_cleanup_unsub = None @property def should_poll(self) -> bool: @@ -154,6 +155,10 @@ class NestCamera(Camera): await self._stream.stop_rtsp_stream() if self._stream_refresh_unsub: self._stream_refresh_unsub() + self._event_id = None + self._event_image_bytes = None + if self._event_image_cleanup_unsub is not None: + self._event_image_cleanup_unsub() async def async_added_to_hass(self): """Run when entity is added to register update signal handler.""" @@ -181,10 +186,20 @@ class NestCamera(Camera): if not trait: return None # Reuse image bytes if they have already been fetched - event_id = trait.last_event.event_id - if self._event_id is not None and self._event_id == event_id: + event = trait.last_event + if self._event_id is not None and self._event_id == event.event_id: return self._event_image_bytes - _LOGGER.info("Fetching URL for event_id %s", event_id) + _LOGGER.debug("Generating event image URL for event_id %s", event.event_id) + image_bytes = await self._async_fetch_active_event_image(trait) + if image_bytes is None: + return None + self._event_id = event.event_id + self._event_image_bytes = image_bytes + self._schedule_event_image_cleanup(event.expires_at) + return image_bytes + + async def _async_fetch_active_event_image(self, trait): + """Return image bytes for an active event.""" try: event_image = await trait.generate_active_event_image() except GoogleNestException as err: @@ -193,10 +208,23 @@ class NestCamera(Camera): if not event_image: return None try: - image_bytes = await event_image.contents() + return await event_image.contents() except GoogleNestException as err: _LOGGER.debug("Unable to fetch event image: %s", err) return None - self._event_id = event_id - self._event_image_bytes = image_bytes - return image_bytes + + def _schedule_event_image_cleanup(self, point_in_time): + """Schedules an alarm to remove the image bytes from memory, honoring expiration.""" + if self._event_image_cleanup_unsub is not None: + self._event_image_cleanup_unsub() + self._event_image_cleanup_unsub = async_track_point_in_utc_time( + self.hass, + self._handle_event_image_cleanup, + point_in_time, + ) + + def _handle_event_image_cleanup(self, now): + """Clear images cached from events and scheduled callback.""" + self._event_id = None + self._event_image_bytes = None + self._event_image_cleanup_unsub = None diff --git a/tests/components/nest/camera_sdm_test.py b/tests/components/nest/camera_sdm_test.py index f2aee2d17c5..84deef92d62 100644 --- a/tests/components/nest/camera_sdm_test.py +++ b/tests/components/nest/camera_sdm_test.py @@ -59,20 +59,22 @@ GENERATE_IMAGE_URL_RESPONSE = { IMAGE_AUTHORIZATION_HEADERS = {"Authorization": "Basic g.0.eventToken"} -def make_motion_event(timestamp: datetime.datetime = None) -> EventMessage: +def make_motion_event( + event_id: str = MOTION_EVENT_ID, timestamp: datetime.datetime = None +) -> EventMessage: """Create an EventMessage for a motion event.""" if not timestamp: timestamp = utcnow() return EventMessage( { - "eventId": "some-event-id", + "eventId": "some-event-id", # Ignored; we use the resource updated event id below "timestamp": timestamp.isoformat(timespec="seconds"), "resourceUpdate": { "name": DEVICE_ID, "events": { "sdm.devices.events.CameraMotion.Motion": { "eventSessionId": "CjY5Y3VKaTZwR3o4Y19YbTVfMF...", - "eventId": MOTION_EVENT_ID, + "eventId": event_id, }, }, }, @@ -127,7 +129,7 @@ async def fire_alarm(hass, point_in_time): async def async_get_image(hass): """Get image from the camera, a wrapper around camera.async_get_image.""" # Note: this patches ImageFrame to simulate decoding an image from a live - # stream, however the test may not use it. Tests assert on the image + # stream, however the test may not use it. Tests assert on the image # contents to determine if the image came from the live stream or event. with patch( "homeassistant.components.ffmpeg.ImageFrame.get_image", @@ -306,11 +308,7 @@ async def test_stream_response_already_expired(hass, auth): async def test_camera_removed(hass, auth): """Test case where entities are removed and stream tokens expired.""" - auth.responses = [ - make_stream_url_response(), - aiohttp.web.json_response({"results": {}}), - ] - await async_setup_camera( + subscriber = await async_setup_camera( hass, DEVICE_TRAITS, auth=auth, @@ -321,9 +319,24 @@ async def test_camera_removed(hass, auth): assert cam is not None assert cam.state == STATE_IDLE + # Start a stream, exercising cleanup on remove + auth.responses = [ + make_stream_url_response(), + aiohttp.web.json_response({"results": {}}), + ] stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") assert stream_source == "rtsp://some/url?auth=g.0.streamingToken" + # Fetch an event image, exercising cleanup on remove + await subscriber.async_receive_event(make_motion_event()) + await hass.async_block_till_done() + auth.responses = [ + aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), + aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT), + ] + image = await async_get_image(hass) + assert image.content == IMAGE_BYTES_FROM_EVENT + for config_entry in hass.config_entries.async_entries(DOMAIN): await hass.config_entries.async_remove(config_entry.entry_id) assert len(hass.states.async_all()) == 0 @@ -363,7 +376,7 @@ async def test_refresh_expired_stream_failure(hass, auth): async def test_camera_image_from_last_event(hass, auth): """Test an image generated from an event.""" - # The subscriber receives a message related to an image event. The camera + # The subscriber receives a message related to an image event. The camera # holds on to the event message. When the test asks for a capera snapshot # it exchanges the event id for an image url and fetches the image. subscriber = await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) @@ -464,7 +477,7 @@ async def test_event_image_expired(hass, auth): # Simulate a pubsub message has already expired event_timestamp = utcnow() - datetime.timedelta(seconds=40) - await subscriber.async_receive_event(make_motion_event(event_timestamp)) + await subscriber.async_receive_event(make_motion_event(timestamp=event_timestamp)) await hass.async_block_till_done() # Fallback to a stream url since the event message is expired. @@ -472,3 +485,78 @@ async def test_event_image_expired(hass, auth): image = await async_get_image(hass) assert image.content == IMAGE_BYTES_FROM_STREAM + + +async def test_event_image_becomes_expired(hass, auth): + """Test fallback for an event event image that has been cleaned up on expiration.""" + subscriber = await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) + assert len(hass.states.async_all()) == 1 + assert hass.states.get("camera.my_camera") + + event_timestamp = utcnow() + await subscriber.async_receive_event(make_motion_event(timestamp=event_timestamp)) + await hass.async_block_till_done() + + auth.responses = [ + # Fake response from API that returns url image + aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), + # Fake response for the image content fetch + aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT), + # Image is refetched after being cleared by expiration alarm + aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), + aiohttp.web.Response(body=b"updated image bytes"), + ] + + image = await async_get_image(hass) + assert image.content == IMAGE_BYTES_FROM_EVENT + + # Event image is still valid before expiration + next_update = event_timestamp + datetime.timedelta(seconds=25) + await fire_alarm(hass, next_update) + + image = await async_get_image(hass) + assert image.content == IMAGE_BYTES_FROM_EVENT + + # Fire an alarm well after expiration, removing image from cache + # Note: This test does not override the "now" logic within the underlying + # python library that tracks active events. Instead, it exercises the + # alarm behavior only. That is, the library may still think the event is + # active even though Home Assistant does not due to patching time. + next_update = event_timestamp + datetime.timedelta(seconds=180) + await fire_alarm(hass, next_update) + + image = await async_get_image(hass) + assert image.content == b"updated image bytes" + + +async def test_multiple_event_images(hass, auth): + """Test fallback for an event event image that has been cleaned up on expiration.""" + subscriber = await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) + assert len(hass.states.async_all()) == 1 + assert hass.states.get("camera.my_camera") + + event_timestamp = utcnow() + await subscriber.async_receive_event(make_motion_event(timestamp=event_timestamp)) + await hass.async_block_till_done() + + auth.responses = [ + # Fake response from API that returns url image + aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), + # Fake response for the image content fetch + aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT), + # Image is refetched after being cleared by expiration alarm + aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), + aiohttp.web.Response(body=b"updated image bytes"), + ] + + image = await async_get_image(hass) + assert image.content == IMAGE_BYTES_FROM_EVENT + + next_event_timestamp = event_timestamp + datetime.timedelta(seconds=25) + await subscriber.async_receive_event( + make_motion_event(event_id="updated-event-id", timestamp=next_event_timestamp) + ) + await hass.async_block_till_done() + + image = await async_get_image(hass) + assert image.content == b"updated image bytes" From ac60b34d1727a18a9978321ca9dc2303ad25104f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 12 Jan 2021 20:27:25 -1000 Subject: [PATCH 182/507] Roomba cleanups (#45097) * Roomba cleanups Remove async_step_init backwards compat Move urls to description_placeholders. Fix typos * fix test * fix fallback to manual when roomba is in the wrong state --- .../components/roomba/config_flow.py | 23 +++--- homeassistant/components/roomba/strings.json | 4 +- .../components/roomba/translations/en.json | 4 +- tests/components/roomba/test_config_flow.py | 81 ++++++++++++++++++- 4 files changed, 96 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/roomba/config_flow.py b/homeassistant/components/roomba/config_flow.py index b99f62e8bdc..4917653353e 100644 --- a/homeassistant/components/roomba/config_flow.py +++ b/homeassistant/components/roomba/config_flow.py @@ -25,6 +25,11 @@ DEFAULT_OPTIONS = {CONF_CONTINUOUS: DEFAULT_CONTINUOUS, CONF_DELAY: DEFAULT_DELA MAX_NUM_DEVICES_TO_DISCOVER = 25 +AUTH_HELP_URL_KEY = "auth_help_url" +AUTH_HELP_URL_VALUE = ( + "https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials" +) + async def validate_input(hass: core.HomeAssistant, data): """Validate the user input allows us to connect. @@ -68,11 +73,6 @@ class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return OptionsFlowHandler(config_entry) async def async_step_user(self, user_input=None): - """Handle a flow initialized by the user.""" - # This is for backwards compatibility. - return await self.async_step_init(user_input) - - async def async_step_init(self, user_input=None): """Handle a flow start.""" # Check if user chooses manual entry if user_input is not None and not user_input.get(CONF_HOST): @@ -107,7 +107,7 @@ class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_manual() return self.async_show_form( - step_id="init", + step_id="user", data_schema=vol.Schema( { vol.Optional("host"): vol.In( @@ -129,6 +129,7 @@ class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is None: return self.async_show_form( step_id="manual", + description_placeholders={AUTH_HELP_URL_KEY: AUTH_HELP_URL_VALUE}, data_schema=vol.Schema( {vol.Required(CONF_HOST): str, vol.Required(CONF_BLID): str} ), @@ -155,9 +156,12 @@ class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is None: return self.async_show_form(step_id="link") - password = await self.hass.async_add_executor_job( - RoombaPassword(self.host).get_password - ) + try: + password = await self.hass.async_add_executor_job( + RoombaPassword(self.host).get_password + ) + except ConnectionRefusedError: + return await self.async_step_link_manual() if not password: return await self.async_step_link_manual() @@ -202,6 +206,7 @@ class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="link_manual", + description_placeholders={AUTH_HELP_URL_KEY: AUTH_HELP_URL_VALUE}, data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}), errors=errors, ) diff --git a/homeassistant/components/roomba/strings.json b/homeassistant/components/roomba/strings.json index 4d0b396d2a9..da2a193d4c9 100644 --- a/homeassistant/components/roomba/strings.json +++ b/homeassistant/components/roomba/strings.json @@ -10,7 +10,7 @@ }, "manual": { "title": "Manually connect to the device", - "description": "No Roomba or Braava have been discovered on your network. The BLID is the portion of the device hostname after `iRobot-`. Please follow the steps outlined in the documentation at: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", + "description": "No Roomba or Braava have been discovered on your network. The BLID is the portion of the device hostname after `iRobot-`. Please follow the steps outlined in the documentation at: {auth_help_url}", "data": { "host": "[%key:common::config_flow::data::host%]", "blid": "BLID" @@ -22,7 +22,7 @@ }, "link_manual": { "title": "Enter Password", - "description": "The password could not be retrivied from the device automaticlly. Please follow the steps outlined in the documentation at: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", + "description": "The password could not be retrivied from the device automatically. Please follow the steps outlined in the documentation at: {auth_help_url}", "data": { "password": "[%key:common::config_flow::data::password%]" } diff --git a/homeassistant/components/roomba/translations/en.json b/homeassistant/components/roomba/translations/en.json index 4d0b396d2a9..da2a193d4c9 100644 --- a/homeassistant/components/roomba/translations/en.json +++ b/homeassistant/components/roomba/translations/en.json @@ -10,7 +10,7 @@ }, "manual": { "title": "Manually connect to the device", - "description": "No Roomba or Braava have been discovered on your network. The BLID is the portion of the device hostname after `iRobot-`. Please follow the steps outlined in the documentation at: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", + "description": "No Roomba or Braava have been discovered on your network. The BLID is the portion of the device hostname after `iRobot-`. Please follow the steps outlined in the documentation at: {auth_help_url}", "data": { "host": "[%key:common::config_flow::data::host%]", "blid": "BLID" @@ -22,7 +22,7 @@ }, "link_manual": { "title": "Enter Password", - "description": "The password could not be retrivied from the device automaticlly. Please follow the steps outlined in the documentation at: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", + "description": "The password could not be retrivied from the device automatically. Please follow the steps outlined in the documentation at: {auth_help_url}", "data": { "password": "[%key:common::config_flow::data::password%]" } diff --git a/tests/components/roomba/test_config_flow.py b/tests/components/roomba/test_config_flow.py index a90cc9c621f..ef236a39e3c 100644 --- a/tests/components/roomba/test_config_flow.py +++ b/tests/components/roomba/test_config_flow.py @@ -65,6 +65,12 @@ def _mocked_failed_getpassword(*_): return roomba_password +def _mocked_connection_refused_on_getpassword(*_): + roomba_password = MagicMock() + roomba_password.get_password = MagicMock(side_effect=ConnectionRefusedError) + return roomba_password + + async def test_form_user_discovery_and_password_fetch(hass): """Test we can discovery and fetch the password.""" await setup.async_setup_component(hass, "persistent_notification", {}) @@ -84,7 +90,7 @@ async def test_form_user_discovery_and_password_fetch(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] is None - assert result["step_id"] == "init" + assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -195,7 +201,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] is None - assert result["step_id"] == "init" + assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -268,7 +274,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch_but_cannot_con assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] is None - assert result["step_id"] == "init" + assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -504,3 +510,72 @@ async def test_form_user_discovery_fails_and_password_fetch_fails_and_cannot_con assert result4["errors"] == {"base": "cannot_connect"} assert len(mock_setup.mock_calls) == 0 assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_form_user_discovery_and_password_fetch_gets_connection_refused(hass): + """Test we can discovery and fetch the password manually.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + mocked_roomba = _create_mocked_roomba( + roomba_connected=True, + master_state={"state": {"reported": {"name": "myroomba"}}}, + ) + + with patch( + "homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "user" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: MOCK_IP}, + ) + await hass.async_block_till_done() + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] is None + assert result2["step_id"] == "link" + + with patch( + "homeassistant.components.roomba.config_flow.RoombaPassword", + _mocked_connection_refused_on_getpassword, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {}, + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.roomba.config_flow.Roomba", + return_value=mocked_roomba, + ), patch( + "homeassistant.components.roomba.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.roomba.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + {CONF_PASSWORD: "password"}, + ) + await hass.async_block_till_done() + + assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["title"] == "myroomba" + assert result4["result"].unique_id == "blid" + assert result4["data"] == { + CONF_BLID: "blid", + CONF_CONTINUOUS: True, + CONF_DELAY: 1, + CONF_HOST: MOCK_IP, + CONF_PASSWORD: "password", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 From 10bc05df00a3e90cb0d4134271b627d51ea78849 Mon Sep 17 00:00:00 2001 From: Santobert Date: Wed, 13 Jan 2021 10:23:16 +0100 Subject: [PATCH 183/507] Fix neato battery sensor not ready (#44946) * Fix neato battery sensor not ready * Edit available attribute * Remove unnecessary condition --- homeassistant/components/neato/sensor.py | 2 +- homeassistant/components/neato/switch.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py index b083ec1d7df..50af42e7007 100644 --- a/homeassistant/components/neato/sensor.py +++ b/homeassistant/components/neato/sensor.py @@ -37,7 +37,7 @@ class NeatoSensor(Entity): def __init__(self, neato, robot): """Initialize Neato sensor.""" self.robot = robot - self._available = neato is not None + self._available = False self._robot_name = f"{self.robot.name} {BATTERY}" self._robot_serial = self.robot.serial self._state = None diff --git a/homeassistant/components/neato/switch.py b/homeassistant/components/neato/switch.py index 204adb108a8..a3cc51b82c6 100644 --- a/homeassistant/components/neato/switch.py +++ b/homeassistant/components/neato/switch.py @@ -40,7 +40,7 @@ class NeatoConnectedSwitch(ToggleEntity): """Initialize the Neato Connected switches.""" self.type = switch_type self.robot = robot - self._available = neato is not None + self._available = False self._robot_name = f"{self.robot.name} {SWITCH_TYPES[self.type][0]}" self._state = None self._schedule_state = None From ec038bc6ea55807bda95ecc10df335247d3af77a Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Wed, 13 Jan 2021 06:11:20 -0500 Subject: [PATCH 184/507] Allow any parameter of a light profile as an optional parameter (#44079) * No code duplication for profile application * Refactor color profile as a dataclass * Typing * Make color_x and color_y of a Light profile optional * Update tests * Make brightness field of a Light profile optional * Transition can be of a float type * Allow fractional transition times in light profiles Make transition of a float type. Allow transition to be optional with 5 column CSV files. * Make pylint happy * Fix dropped async_mock * Simplify CSV row schema --- homeassistant/components/light/__init__.py | 119 ++++++---- tests/components/light/conftest.py | 1 - tests/components/light/test_init.py | 241 ++++++++++++++++----- 3 files changed, 271 insertions(+), 90 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index f406366dc86..d2fbc641be5 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -1,8 +1,10 @@ """Provides functionality to interact with lights.""" import csv +import dataclasses from datetime import timedelta import logging import os +from typing import Dict, List, Optional, Tuple, cast import voluptuous as vol @@ -21,6 +23,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 ) from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import bind_hass import homeassistant.util.color as color_util @@ -270,24 +273,74 @@ async def async_unload_entry(hass, entry): return await hass.data[DOMAIN].async_unload_entry(entry) -class Profiles: - """Representation of available color profiles.""" +def _coerce_none(value: str) -> None: + """Coerce an empty string as None.""" - SCHEMA = vol.Schema( + if not isinstance(value, str): + raise vol.Invalid("Expected a string") + + if value: + raise vol.Invalid("Not an empty string") + + +@dataclasses.dataclass +class Profile: + """Representation of a profile.""" + + name: str + color_x: Optional[float] = dataclasses.field(repr=False) + color_y: Optional[float] = dataclasses.field(repr=False) + brightness: Optional[int] + transition: Optional[int] = None + hs_color: Optional[Tuple[float, float]] = dataclasses.field(init=False) + + SCHEMA = vol.Schema( # pylint: disable=invalid-name vol.Any( - vol.ExactSequence((str, cv.small_float, cv.small_float, cv.byte)), vol.ExactSequence( - (str, cv.small_float, cv.small_float, cv.byte, cv.positive_int) + ( + str, + vol.Any(cv.small_float, _coerce_none), + vol.Any(cv.small_float, _coerce_none), + vol.Any(cv.byte, _coerce_none), + ) + ), + vol.ExactSequence( + ( + str, + vol.Any(cv.small_float, _coerce_none), + vol.Any(cv.small_float, _coerce_none), + vol.Any(cv.byte, _coerce_none), + vol.Any(VALID_TRANSITION, _coerce_none), + ) ), ) ) - def __init__(self, hass): + def __post_init__(self) -> None: + """Convert xy to hs color.""" + if None in (self.color_x, self.color_y): + self.hs_color = None + return + + self.hs_color = color_util.color_xy_to_hs( + cast(float, self.color_x), cast(float, self.color_y) + ) + + @classmethod + def from_csv_row(cls, csv_row: List[str]) -> "Profile": + """Create profile from a CSV row tuple.""" + return cls(*cls.SCHEMA(csv_row)) + + +class Profiles: + """Representation of available color profiles.""" + + def __init__(self, hass: HomeAssistantType): """Initialize profiles.""" self.hass = hass - self.data = None + self.data: Dict[str, Profile] = {} - def _load_profile_data(self): + def _load_profile_data(self) -> Dict[str, Profile]: """Load built-in profiles and custom profiles.""" profile_paths = [ os.path.join(os.path.dirname(__file__), LIGHT_PROFILES_FILE), @@ -306,56 +359,46 @@ class Profiles: try: for rec in reader: - ( - profile, - color_x, - color_y, - brightness, - *transition, - ) = Profiles.SCHEMA(rec) + profile = Profile.from_csv_row(rec) + profiles[profile.name] = profile - transition = transition[0] if transition else 0 - - profiles[profile] = color_util.color_xy_to_hs( - color_x, color_y - ) + ( - brightness, - transition, - ) except vol.MultipleInvalid as ex: _LOGGER.error( - "Error parsing light profile from %s: %s", profile_path, ex + "Error parsing light profile row '%s' from %s: %s", + rec, + profile_path, + ex, ) continue return profiles - async def async_initialize(self): + async def async_initialize(self) -> None: """Load and cache profiles.""" self.data = await self.hass.async_add_executor_job(self._load_profile_data) @callback - def apply_default(self, entity_id, params): + def apply_default(self, entity_id: str, params: Dict) -> None: """Return the default turn-on profile for the given light.""" - name = f"{entity_id}.default" - if name in self.data: - self.apply_profile(name, params) - return - - name = "group.all_lights.default" - if name in self.data: - self.apply_profile(name, params) + for _entity_id in (entity_id, "group.all_lights"): + name = f"{_entity_id}.default" + if name in self.data: + self.apply_profile(name, params) + return @callback - def apply_profile(self, name, params): + def apply_profile(self, name: str, params: Dict) -> None: """Apply a profile.""" profile = self.data.get(name) if profile is None: return - params.setdefault(ATTR_HS_COLOR, profile[:2]) - params.setdefault(ATTR_BRIGHTNESS, profile[2]) - params.setdefault(ATTR_TRANSITION, profile[3]) + if profile.hs_color is not None: + params.setdefault(ATTR_HS_COLOR, profile.hs_color) + if profile.brightness is not None: + params.setdefault(ATTR_BRIGHTNESS, profile.brightness) + if profile.transition is not None: + params.setdefault(ATTR_TRANSITION, profile.transition) class LightEntity(ToggleEntity): diff --git a/tests/components/light/conftest.py b/tests/components/light/conftest.py index 4ce72f441c2..12bd62edcb7 100644 --- a/tests/components/light/conftest.py +++ b/tests/components/light/conftest.py @@ -20,7 +20,6 @@ def mock_light_profiles(): with patch( "homeassistant.components.light.Profiles", - SCHEMA=Profiles.SCHEMA, side_effect=mock_profiles_class, ): yield data diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 72674a984fd..12a64417987 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -1,4 +1,6 @@ """The tests for the Light component.""" +from unittest.mock import MagicMock, mock_open, patch + import pytest import voluptuous as vol @@ -16,7 +18,6 @@ from homeassistant.const import ( ) from homeassistant.exceptions import Unauthorized from homeassistant.setup import async_setup_component -from homeassistant.util import color from tests.common import async_mock_service @@ -280,14 +281,14 @@ async def test_services(hass, mock_light_profiles): assert data == {} # One of the light profiles - mock_light_profiles["relax"] = (35.932, 69.412, 144, 0) - prof_name, prof_h, prof_s, prof_bri, prof_t = "relax", 35.932, 69.412, 144, 0 + profile = light.Profile("relax", 0.513, 0.413, 144, 0) + mock_light_profiles[profile.name] = profile # Test light profiles await hass.services.async_call( light.DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: ent1.entity_id, light.ATTR_PROFILE: prof_name}, + {ATTR_ENTITY_ID: ent1.entity_id, light.ATTR_PROFILE: profile.name}, blocking=True, ) # Specify a profile and a brightness attribute to overwrite it @@ -296,7 +297,7 @@ async def test_services(hass, mock_light_profiles): SERVICE_TURN_ON, { ATTR_ENTITY_ID: ent2.entity_id, - light.ATTR_PROFILE: prof_name, + light.ATTR_PROFILE: profile.name, light.ATTR_BRIGHTNESS: 100, light.ATTR_TRANSITION: 1, }, @@ -305,15 +306,15 @@ async def test_services(hass, mock_light_profiles): _, data = ent1.last_call("turn_on") assert data == { - light.ATTR_BRIGHTNESS: prof_bri, - light.ATTR_HS_COLOR: (prof_h, prof_s), - light.ATTR_TRANSITION: prof_t, + light.ATTR_BRIGHTNESS: profile.brightness, + light.ATTR_HS_COLOR: profile.hs_color, + light.ATTR_TRANSITION: profile.transition, } _, data = ent2.last_call("turn_on") assert data == { light.ATTR_BRIGHTNESS: 100, - light.ATTR_HS_COLOR: (prof_h, prof_s), + light.ATTR_HS_COLOR: profile.hs_color, light.ATTR_TRANSITION: 1, } @@ -323,7 +324,7 @@ async def test_services(hass, mock_light_profiles): SERVICE_TOGGLE, { ATTR_ENTITY_ID: ent3.entity_id, - light.ATTR_PROFILE: prof_name, + light.ATTR_PROFILE: profile.name, light.ATTR_BRIGHTNESS_PCT: 100, }, blocking=True, @@ -332,8 +333,8 @@ async def test_services(hass, mock_light_profiles): _, data = ent3.last_call("turn_on") assert data == { light.ATTR_BRIGHTNESS: 255, - light.ATTR_HS_COLOR: (prof_h, prof_s), - light.ATTR_TRANSITION: prof_t, + light.ATTR_HS_COLOR: profile.hs_color, + light.ATTR_TRANSITION: profile.transition, } await hass.services.async_call( @@ -392,7 +393,7 @@ async def test_services(hass, mock_light_profiles): SERVICE_TURN_ON, { ATTR_ENTITY_ID: ent1.entity_id, - light.ATTR_PROFILE: prof_name, + light.ATTR_PROFILE: profile.name, light.ATTR_BRIGHTNESS: "bright", }, blocking=True, @@ -422,13 +423,92 @@ async def test_services(hass, mock_light_profiles): assert data == {} -async def test_light_profiles(hass, mock_light_profiles): +@pytest.mark.parametrize( + "profile_name, last_call, expected_data", + ( + ( + "test", + "turn_on", + { + light.ATTR_HS_COLOR: (71.059, 100), + light.ATTR_BRIGHTNESS: 100, + light.ATTR_TRANSITION: 0, + }, + ), + ( + "color_no_brightness_no_transition", + "turn_on", + { + light.ATTR_HS_COLOR: (71.059, 100), + }, + ), + ( + "no color", + "turn_on", + { + light.ATTR_BRIGHTNESS: 110, + light.ATTR_TRANSITION: 0, + }, + ), + ( + "test_off", + "turn_off", + { + light.ATTR_TRANSITION: 0, + }, + ), + ( + "no brightness", + "turn_on", + { + light.ATTR_HS_COLOR: (71.059, 100), + }, + ), + ( + "color_and_brightness", + "turn_on", + { + light.ATTR_HS_COLOR: (71.059, 100), + light.ATTR_BRIGHTNESS: 120, + }, + ), + ( + "color_and_transition", + "turn_on", + { + light.ATTR_HS_COLOR: (71.059, 100), + light.ATTR_TRANSITION: 4.2, + }, + ), + ( + "brightness_and_transition", + "turn_on", + { + light.ATTR_BRIGHTNESS: 130, + light.ATTR_TRANSITION: 5.3, + }, + ), + ), +) +async def test_light_profiles( + hass, mock_light_profiles, profile_name, expected_data, last_call +): """Test light profiles.""" platform = getattr(hass.components, "test.light") platform.init() - mock_light_profiles["test"] = color.color_xy_to_hs(0.4, 0.6) + (100, 0) - mock_light_profiles["test_off"] = 0, 0, 0, 0 + profile_mock_data = { + "test": (0.4, 0.6, 100, 0), + "color_no_brightness_no_transition": (0.4, 0.6, None, None), + "no color": (None, None, 110, 0), + "test_off": (0, 0, 0, 0), + "no brightness": (0.4, 0.6, None), + "color_and_brightness": (0.4, 0.6, 120), + "color_and_transition": (0.4, 0.6, None, 4.2), + "brightness_and_transition": (None, None, 130, 5.3), + } + for name, data in profile_mock_data.items(): + mock_light_profiles[name] = light.Profile(*(name, *data)) assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} @@ -442,29 +522,17 @@ async def test_light_profiles(hass, mock_light_profiles): SERVICE_TURN_ON, { ATTR_ENTITY_ID: ent1.entity_id, - light.ATTR_PROFILE: "test", + light.ATTR_PROFILE: profile_name, }, blocking=True, ) - _, data = ent1.last_call("turn_on") - assert light.is_on(hass, ent1.entity_id) - assert data == { - light.ATTR_HS_COLOR: (71.059, 100), - light.ATTR_BRIGHTNESS: 100, - light.ATTR_TRANSITION: 0, - } - - await hass.services.async_call( - light.DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: ent1.entity_id, light.ATTR_PROFILE: "test_off"}, - blocking=True, - ) - - _, data = ent1.last_call("turn_off") - assert not light.is_on(hass, ent1.entity_id) - assert data == {light.ATTR_TRANSITION: 0} + _, data = ent1.last_call(last_call) + if last_call == "turn_on": + assert light.is_on(hass, ent1.entity_id) + else: + assert not light.is_on(hass, ent1.entity_id) + assert data == expected_data async def test_default_profiles_group(hass, mock_light_profiles): @@ -477,10 +545,8 @@ async def test_default_profiles_group(hass, mock_light_profiles): ) await hass.async_block_till_done() - mock_light_profiles["group.all_lights.default"] = color.color_xy_to_hs(0.4, 0.6) + ( - 99, - 2, - ) + profile = light.Profile("group.all_lights.default", 0.4, 0.6, 99, 2) + mock_light_profiles[profile.name] = profile ent, _, _ = platform.ENTITIES await hass.services.async_call( @@ -505,14 +571,10 @@ async def test_default_profiles_light(hass, mock_light_profiles): ) await hass.async_block_till_done() - mock_light_profiles["group.all_lights.default"] = color.color_xy_to_hs(0.3, 0.5) + ( - 200, - 0, - ) - mock_light_profiles["light.ceiling_2.default"] = color.color_xy_to_hs(0.6, 0.6) + ( - 100, - 3, - ) + profile = light.Profile("group.all_lights.default", 0.3, 0.5, 200, 0) + mock_light_profiles[profile.name] = profile + profile = light.Profile("light.ceiling_2.default", 0.6, 0.6, 100, 3) + mock_light_profiles[profile.name] = profile dev = next(filter(lambda x: x.entity_id == "light.ceiling_2", platform.ENTITIES)) await hass.services.async_call( @@ -693,8 +755,85 @@ async def test_profiles(hass): profiles = orig_Profiles(hass) await profiles.async_initialize() assert profiles.data == { - "concentrate": (35.932, 69.412, 219, 0), - "energize": (43.333, 21.176, 203, 0), - "reading": (38.88, 49.02, 240, 0), - "relax": (35.932, 69.412, 144, 0), + "concentrate": light.Profile("concentrate", 0.5119, 0.4147, 219, None), + "energize": light.Profile("energize", 0.368, 0.3686, 203, None), + "reading": light.Profile("reading", 0.4448, 0.4066, 240, None), + "relax": light.Profile("relax", 0.5119, 0.4147, 144, None), } + assert profiles.data["concentrate"].hs_color == (35.932, 69.412) + assert profiles.data["energize"].hs_color == (43.333, 21.176) + assert profiles.data["reading"].hs_color == (38.88, 49.02) + assert profiles.data["relax"].hs_color == (35.932, 69.412) + + +@patch("os.path.isfile", MagicMock(side_effect=(True, False))) +async def test_profile_load_optional_hs_color(hass): + """Test profile loading with profiles containing no xy color.""" + + csv_file = """the first line is skipped +no_color,,,100,1 +no_color_no_transition,,,110 +color,0.5119,0.4147,120,2 +color_no_transition,0.4448,0.4066,130 +color_and_brightness,0.4448,0.4066,170, +only_brightness,,,140 +only_transition,,,,150 +transition_float,,,,1.6 +invalid_profile_1, +invalid_color_2,,0.1,1,2 +invalid_color_3,,0.1,1 +invalid_color_4,0.1,,1,3 +invalid_color_5,0.1,,1 +invalid_brightness,0,0,256,4 +invalid_brightness_2,0,0,256 +invalid_no_brightness_no_color_no_transition,,, +""" + + profiles = orig_Profiles(hass) + with patch("builtins.open", mock_open(read_data=csv_file)): + await profiles.async_initialize() + await hass.async_block_till_done() + + assert profiles.data["no_color"].hs_color is None + assert profiles.data["no_color"].brightness == 100 + assert profiles.data["no_color"].transition == 1 + + assert profiles.data["no_color_no_transition"].hs_color is None + assert profiles.data["no_color_no_transition"].brightness == 110 + assert profiles.data["no_color_no_transition"].transition is None + + assert profiles.data["color"].hs_color == (35.932, 69.412) + assert profiles.data["color"].brightness == 120 + assert profiles.data["color"].transition == 2 + + assert profiles.data["color_no_transition"].hs_color == (38.88, 49.02) + assert profiles.data["color_no_transition"].brightness == 130 + assert profiles.data["color_no_transition"].transition is None + + assert profiles.data["color_and_brightness"].hs_color == (38.88, 49.02) + assert profiles.data["color_and_brightness"].brightness == 170 + assert profiles.data["color_and_brightness"].transition is None + + assert profiles.data["only_brightness"].hs_color is None + assert profiles.data["only_brightness"].brightness == 140 + assert profiles.data["only_brightness"].transition is None + + assert profiles.data["only_transition"].hs_color is None + assert profiles.data["only_transition"].brightness is None + assert profiles.data["only_transition"].transition == 150 + + assert profiles.data["transition_float"].hs_color is None + assert profiles.data["transition_float"].brightness is None + assert profiles.data["transition_float"].transition == 1.6 + + for invalid_profile_name in ( + "invalid_profile_1", + "invalid_color_2", + "invalid_color_3", + "invalid_color_4", + "invalid_color_5", + "invalid_brightness", + "invalid_brightness_2", + "invalid_no_brightness_no_color_no_transition", + ): + assert invalid_profile_name not in profiles.data From ff3a1f2050662298b258ada2b9cebab0e83d1103 Mon Sep 17 00:00:00 2001 From: ErnstEeldert Date: Wed, 13 Jan 2021 13:45:11 +0100 Subject: [PATCH 185/507] Add device class attribute to tado humidity sensor state (#45084) * add device class attribute to humidity sensor state * * explict return none * use const for device class value * removed unnecessary icon definitions --- homeassistant/components/tado/sensor.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index f9a924b52c6..6613de82bff 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -2,7 +2,12 @@ import logging from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE, TEMP_CELSIUS +from homeassistant.const import ( + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity @@ -126,12 +131,13 @@ class TadoZoneSensor(TadoZoneEntity, Entity): return None @property - def icon(self): - """Icon for the sensor.""" - if self.zone_variable == "temperature": - return "mdi:thermometer" + def device_class(self): + """Return the device class.""" if self.zone_variable == "humidity": - return "mdi:water-percent" + return DEVICE_CLASS_HUMIDITY + if self.zone_variable == "temperature": + return DEVICE_CLASS_TEMPERATURE + return None @callback def _async_update_callback(self): From 6325bc8bfebae40f17ae00cefb3e467065c09e5d Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 13 Jan 2021 14:03:54 +0100 Subject: [PATCH 186/507] Follow Axis library changes and improve tests (#44126) --- .../components/axis/binary_sensor.py | 7 +- homeassistant/components/axis/device.py | 17 +- homeassistant/components/axis/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/axis/test_binary_sensor.py | 11 +- tests/components/axis/test_config_flow.py | 28 +++- tests/components/axis/test_device.py | 148 ++++++++++++------ tests/components/axis/test_light.py | 4 +- tests/components/axis/test_switch.py | 6 +- 10 files changed, 149 insertions(+), 78 deletions(-) diff --git a/homeassistant/components/axis/binary_sensor.py b/homeassistant/components/axis/binary_sensor.py index 1881fe887f9..32d4afa328d 100644 --- a/homeassistant/components/axis/binary_sensor.py +++ b/homeassistant/components/axis/binary_sensor.py @@ -7,10 +7,12 @@ from axis.event_stream import ( CLASS_LIGHT, CLASS_MOTION, CLASS_OUTPUT, + CLASS_PTZ, CLASS_SOUND, FenceGuard, LoiteringGuard, MotionGuard, + ObjectAnalytics, Vmd4, ) @@ -46,7 +48,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Add binary sensor from Axis device.""" event = device.api.event[event_id] - if event.CLASS != CLASS_OUTPUT and not ( + if event.CLASS not in (CLASS_OUTPUT, CLASS_PTZ) and not ( event.CLASS == CLASS_LIGHT and event.TYPE == "Light" ): async_add_entities([AxisBinarySensor(event, device)]) @@ -101,7 +103,7 @@ class AxisBinarySensor(AxisEventBase, BinarySensorEntity): """Return the name of the event.""" if ( self.event.CLASS == CLASS_INPUT - and self.event.id + and self.event.id in self.device.api.vapix.ports and self.device.api.vapix.ports[self.event.id].name ): return ( @@ -114,6 +116,7 @@ class AxisBinarySensor(AxisEventBase, BinarySensorEntity): (FenceGuard, self.device.api.vapix.fence_guard), (LoiteringGuard, self.device.api.vapix.loitering_guard), (MotionGuard, self.device.api.vapix.motion_guard), + (ObjectAnalytics, self.device.api.vapix.object_analytics), (Vmd4, self.device.api.vapix.vmd4), ): if ( diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index 79204bf3002..d589ebb46bd 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -25,6 +25,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.httpx_client import get_async_client from homeassistant.setup import async_when_setup from .const import ( @@ -177,7 +178,7 @@ class AxisNetworkDevice: self.disconnect_from_stream() event = mqtt_json_to_event(message.payload) - self.api.event.process_event(event) + self.api.event.update([event]) # Setup and teardown methods @@ -195,8 +196,10 @@ class AxisNetworkDevice: except CannotConnect as err: raise ConfigEntryNotReady from err - except Exception: # pylint: disable=broad-except - LOGGER.error("Unknown error connecting with Axis device on %s", self.host) + except Exception as err: # pylint: disable=broad-except + LOGGER.error( + "Unknown error connecting with Axis device (%s): %s", self.host, err + ) return False self.fw_version = self.api.vapix.firmware_version @@ -239,12 +242,10 @@ class AxisNetworkDevice: async def shutdown(self, event): """Stop the event stream.""" self.disconnect_from_stream() - await self.api.vapix.close() async def async_reset(self): """Reset this device to default state.""" self.disconnect_from_stream() - await self.api.vapix.close() unload_ok = all( await asyncio.gather( @@ -267,9 +268,10 @@ class AxisNetworkDevice: async def get_device(hass, host, port, username, password): """Create a Axis device.""" + session = get_async_client(hass, verify_ssl=False) device = axis.AxisDevice( - Configuration(host, port=port, username=username, password=password) + Configuration(session, host, port=port, username=username, password=password) ) try: @@ -280,15 +282,12 @@ async def get_device(hass, host, port, username, password): except axis.Unauthorized as err: LOGGER.warning("Connected to device at %s but not registered.", host) - await device.vapix.close() raise AuthenticationRequired from err except (asyncio.TimeoutError, axis.RequestError) as err: LOGGER.error("Error connecting to the Axis device at %s", host) - await device.vapix.close() raise CannotConnect from err except axis.AxisException as err: LOGGER.exception("Unknown Axis communication error occurred") - await device.vapix.close() raise AuthenticationRequired from err diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index 188520241f3..09015dc92d2 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -3,7 +3,7 @@ "name": "Axis", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/axis", - "requirements": ["axis==41"], + "requirements": ["axis==42"], "zeroconf": [ { "type": "_axis-video._tcp.local.", "macaddress": "00408C*" }, { "type": "_axis-video._tcp.local.", "macaddress": "ACCC8E*" }, diff --git a/requirements_all.txt b/requirements_all.txt index d0c2af809a7..460c3010fec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -306,7 +306,7 @@ av==8.0.2 # avion==0.10 # homeassistant.components.axis -axis==41 +axis==42 # homeassistant.components.azure_event_hub azure-eventhub==5.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 199e3c351a3..034f3d3dd61 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -177,7 +177,7 @@ auroranoaa==0.0.2 av==8.0.2 # homeassistant.components.axis -axis==41 +axis==42 # homeassistant.components.azure_event_hub azure-eventhub==5.1.0 diff --git a/tests/components/axis/test_binary_sensor.py b/tests/components/axis/test_binary_sensor.py index 0c6f4535cd0..98ef55282c3 100644 --- a/tests/components/axis/test_binary_sensor.py +++ b/tests/components/axis/test_binary_sensor.py @@ -19,6 +19,14 @@ EVENTS = [ "type": "state", "value": "0", }, + { + "operation": "Initialized", + "topic": "tns1:PTZController/tnsaxis:PTZPresets/Channel_1", + "source": "PresetToken", + "source_idx": "0", + "type": "on_preset", + "value": "1", + }, { "operation": "Initialized", "topic": "tnsaxis:CameraApplicationPlatform/VMD/Camera1Profile1", @@ -54,8 +62,7 @@ async def test_binary_sensors(hass): config_entry = await setup_axis_integration(hass) device = hass.data[AXIS_DOMAIN][config_entry.unique_id] - for event in EVENTS: - device.api.event.process_event(event) + device.api.event.update(EVENTS) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 2 diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index a557c1144e5..91674883378 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -1,6 +1,8 @@ """Test Axis config flow.""" from unittest.mock import patch +import respx + from homeassistant import data_entry_flow from homeassistant.components.axis import config_flow from homeassistant.components.axis.const import ( @@ -25,7 +27,13 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_FORM, ) -from .test_device import MAC, MODEL, NAME, setup_axis_integration, vapix_request +from .test_device import ( + MAC, + MODEL, + NAME, + mock_default_vapix_requests, + setup_axis_integration, +) from tests.common import MockConfigEntry @@ -41,7 +49,8 @@ async def test_flow_manual_configuration(hass): assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == SOURCE_USER - with patch("axis.vapix.Vapix.request", new=vapix_request): + with respx.mock: + mock_default_vapix_requests(respx) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ @@ -80,7 +89,8 @@ async def test_manual_configuration_update_configuration(hass): with patch( "homeassistant.components.axis.async_setup_entry", return_value=True, - ) as mock_setup_entry, patch("axis.vapix.Vapix.request", new=vapix_request): + ) as mock_setup_entry, respx.mock: + mock_default_vapix_requests(respx, "2.3.4.5") result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ @@ -109,7 +119,8 @@ async def test_flow_fails_already_configured(hass): assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == SOURCE_USER - with patch("axis.vapix.Vapix.request", new=vapix_request): + with respx.mock: + mock_default_vapix_requests(respx) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ @@ -196,7 +207,8 @@ async def test_flow_create_entry_multiple_existing_entries_of_same_model(hass): assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == SOURCE_USER - with patch("axis.vapix.Vapix.request", new=vapix_request): + with respx.mock: + mock_default_vapix_requests(respx) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ @@ -238,7 +250,8 @@ async def test_zeroconf_flow(hass): assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == SOURCE_USER - with patch("axis.vapix.Vapix.request", new=vapix_request): + with respx.mock: + mock_default_vapix_requests(respx) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ @@ -304,7 +317,8 @@ async def test_zeroconf_flow_updated_configuration(hass): with patch( "homeassistant.components.axis.async_setup_entry", return_value=True, - ) as mock_setup_entry, patch("axis.vapix.Vapix.request", new=vapix_request): + ) as mock_setup_entry, respx.mock: + mock_default_vapix_requests(respx, "2.3.4.5") result = await hass.config_entries.flow.async_init( AXIS_DOMAIN, data={ diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index ec9313e3cd5..6c3b35125be 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -1,26 +1,12 @@ """Test Axis device.""" from copy import deepcopy from unittest import mock -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import Mock, patch import axis as axislib -from axis.api_discovery import URL as API_DISCOVERY_URL -from axis.applications import URL_LIST as APPLICATIONS_URL -from axis.applications.vmd4 import URL as VMD4_URL -from axis.basic_device_info import URL as BASIC_DEVICE_INFO_URL from axis.event_stream import OPERATION_INITIALIZED -from axis.light_control import URL as LIGHT_CONTROL_URL -from axis.mqtt import URL_CLIENT as MQTT_CLIENT_URL -from axis.param_cgi import ( - BRAND as BRAND_URL, - INPUT as INPUT_URL, - IOPORT as IOPORT_URL, - OUTPUT as OUTPUT_URL, - PROPERTIES as PROPERTIES_URL, - STREAM_PROFILES as STREAM_PROFILES_URL, -) -from axis.port_management import URL as PORT_MANAGEMENT_URL import pytest +import respx from homeassistant import config_entries from homeassistant.components import axis @@ -47,10 +33,12 @@ MAC = "00408C12345" MODEL = "model" NAME = "name" +DEFAULT_HOST = "1.2.3.4" + ENTRY_OPTIONS = {CONF_EVENTS: True} ENTRY_CONFIG = { - CONF_HOST: "1.2.3.4", + CONF_HOST: DEFAULT_HOST, CONF_USERNAME: "root", CONF_PASSWORD: "pass", CONF_PORT: 80, @@ -166,6 +154,14 @@ root.Brand.ProdVariant= root.Brand.WebURL=http://www.axis.com """ +IMAGE_RESPONSE = """root.Image.I0.Enabled=yes +root.Image.I0.Name=View Area 1 +root.Image.I0.Source=0 +root.Image.I1.Enabled=no +root.Image.I1.Name=View Area 2 +root.Image.I1.Source=0 +""" + PORTS_RESPONSE = """root.Input.NbrOfInputs=1 root.IOPort.I0.Configurable=no root.IOPort.I0.Direction=input @@ -188,6 +184,9 @@ root.Properties.Image.Rotation=0,180 root.Properties.System.SerialNumber=00408C12345 """ +PTZ_RESPONSE = "" + + STREAM_PROFILES_RESPONSE = """root.StreamProfile.MaxGroups=26 root.StreamProfile.S0.Description=profile_1_description root.StreamProfile.S0.Name=profile_1 @@ -197,31 +196,85 @@ root.StreamProfile.S1.Name=profile_2 root.StreamProfile.S1.Parameters=videocodec=h265 """ +VIEW_AREAS_RESPONSE = {"apiVersion": "1.0", "method": "list", "data": {"viewAreas": []}} -async def vapix_request(self, session, url, **kwargs): - """Return data based on url.""" - if API_DISCOVERY_URL in url: - return API_DISCOVERY_RESPONSE - if APPLICATIONS_URL in url: - return APPLICATIONS_LIST_RESPONSE - if BASIC_DEVICE_INFO_URL in url: - return BASIC_DEVICE_INFO_RESPONSE - if LIGHT_CONTROL_URL in url: - return LIGHT_CONTROL_RESPONSE - if MQTT_CLIENT_URL in url: - return MQTT_CLIENT_RESPONSE - if PORT_MANAGEMENT_URL in url: - return PORT_MANAGEMENT_RESPONSE - if VMD4_URL in url: - return VMD4_RESPONSE - if BRAND_URL in url: - return BRAND_RESPONSE - if IOPORT_URL in url or INPUT_URL in url or OUTPUT_URL in url: - return PORTS_RESPONSE - if PROPERTIES_URL in url: - return PROPERTIES_RESPONSE - if STREAM_PROFILES_URL in url: - return STREAM_PROFILES_RESPONSE + +def mock_default_vapix_requests(respx: respx, host: str = DEFAULT_HOST) -> None: + """Mock default Vapix requests responses.""" + respx.post(f"http://{host}:80/axis-cgi/apidiscovery.cgi").respond( + json=API_DISCOVERY_RESPONSE, + ) + respx.post(f"http://{host}:80/axis-cgi/basicdeviceinfo.cgi").respond( + json=BASIC_DEVICE_INFO_RESPONSE, + ) + respx.post(f"http://{host}:80/axis-cgi/io/portmanagement.cgi").respond( + json=PORT_MANAGEMENT_RESPONSE, + ) + respx.post(f"http://{host}:80/axis-cgi/lightcontrol.cgi").respond( + json=LIGHT_CONTROL_RESPONSE, + ) + respx.post(f"http://{host}:80/axis-cgi/mqtt/client.cgi").respond( + json=MQTT_CLIENT_RESPONSE, + ) + respx.post(f"http://{host}:80/axis-cgi/streamprofile.cgi").respond( + json=STREAM_PROFILES_RESPONSE, + ) + respx.post(f"http://{host}:80/axis-cgi/viewarea/info.cgi").respond( + json=VIEW_AREAS_RESPONSE + ) + respx.get( + f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.Brand" + ).respond( + text=BRAND_RESPONSE, + headers={"Content-Type": "text/plain"}, + ) + respx.get( + f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.Image" + ).respond( + text=IMAGE_RESPONSE, + headers={"Content-Type": "text/plain"}, + ) + respx.get( + f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.Input" + ).respond( + text=PORTS_RESPONSE, + headers={"Content-Type": "text/plain"}, + ) + respx.get( + f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.IOPort" + ).respond( + text=PORTS_RESPONSE, + headers={"Content-Type": "text/plain"}, + ) + respx.get( + f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.Output" + ).respond( + text=PORTS_RESPONSE, + headers={"Content-Type": "text/plain"}, + ) + respx.get( + f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.Properties" + ).respond( + text=PROPERTIES_RESPONSE, + headers={"Content-Type": "text/plain"}, + ) + respx.get( + f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.PTZ" + ).respond( + text=PTZ_RESPONSE, + headers={"Content-Type": "text/plain"}, + ) + respx.get( + f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.StreamProfile" + ).respond( + text=STREAM_PROFILES_RESPONSE, + headers={"Content-Type": "text/plain"}, + ) + respx.post(f"http://{host}:80/axis-cgi/applications/list.cgi").respond( + text=APPLICATIONS_LIST_RESPONSE, + headers={"Content-Type": "text/xml"}, + ) + respx.post(f"http://{host}:80/local/vmd/control.cgi").respond(json=VMD4_RESPONSE) async def setup_axis_integration(hass, config=ENTRY_CONFIG, options=ENTRY_OPTIONS): @@ -235,10 +288,8 @@ async def setup_axis_integration(hass, config=ENTRY_CONFIG, options=ENTRY_OPTION ) config_entry.add_to_hass(hass) - with patch("axis.vapix.Vapix.request", new=vapix_request), patch( - "axis.rtsp.RTSPClient.start", - return_value=True, - ): + with patch("axis.rtsp.RTSPClient.start", return_value=True), respx.mock: + mock_default_vapix_requests(respx) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -317,10 +368,11 @@ async def test_update_address(hass): device = hass.data[AXIS_DOMAIN][config_entry.unique_id] assert device.api.config.host == "1.2.3.4" - with patch("axis.vapix.Vapix.request", new=vapix_request), patch( + with patch( "homeassistant.components.axis.async_setup_entry", return_value=True, - ) as mock_setup_entry: + ) as mock_setup_entry, respx.mock: + mock_default_vapix_requests(respx, "2.3.4.5") await hass.config_entries.flow.async_init( AXIS_DOMAIN, data={ @@ -390,12 +442,10 @@ async def test_shutdown(): axis_device = axis.device.AxisNetworkDevice(hass, entry) axis_device.api = Mock() - axis_device.api.vapix.close = AsyncMock() await axis_device.shutdown(None) assert len(axis_device.api.stream.stop.mock_calls) == 1 - assert len(axis_device.api.vapix.close.mock_calls) == 1 async def test_get_device_fails(hass): diff --git a/tests/components/axis/test_light.py b/tests/components/axis/test_light.py index 05b58c04565..37b251d8ede 100644 --- a/tests/components/axis/test_light.py +++ b/tests/components/axis/test_light.py @@ -74,7 +74,7 @@ async def test_lights(hass): "axis.light_control.LightControl.get_valid_intensity", return_value={"data": {"ranges": [{"high": 150}]}}, ): - device.api.event.process_event(EVENT_ON) + device.api.event.update([EVENT_ON]) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(LIGHT_DOMAIN)) == 1 @@ -119,7 +119,7 @@ async def test_lights(hass): mock_deactivate.assert_called_once() # Event turn off light - device.api.event.process_event(EVENT_OFF) + device.api.event.update([EVENT_OFF]) await hass.async_block_till_done() light_0 = hass.states.get(entity_id) diff --git a/tests/components/axis/test_switch.py b/tests/components/axis/test_switch.py index 2f8cde777b5..dcbe285cb54 100644 --- a/tests/components/axis/test_switch.py +++ b/tests/components/axis/test_switch.py @@ -68,8 +68,7 @@ async def test_switches_with_port_cgi(hass): device.api.vapix.ports["0"].close = AsyncMock() device.api.vapix.ports["1"].name = "" - for event in EVENTS: - device.api.event.process_event(event) + device.api.event.update(EVENTS) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 @@ -116,8 +115,7 @@ async def test_switches_with_port_management(hass): device.api.vapix.ports["0"].close = AsyncMock() device.api.vapix.ports["1"].name = "" - for event in EVENTS: - device.api.event.process_event(event) + device.api.event.update(EVENTS) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 From b9e4d5988f0796f410e99631501a38065038ca24 Mon Sep 17 00:00:00 2001 From: badguy99 <61918526+badguy99@users.noreply.github.com> Date: Wed, 13 Jan 2021 14:14:15 +0000 Subject: [PATCH 187/507] Soma: fix battery drain issue caused by excess update requests (#45104) * split up update and throttle update on sensor * Update imports * Add blank lines for isort --- homeassistant/components/soma/__init__.py | 44 ----------------------- homeassistant/components/soma/cover.py | 22 ++++++++++++ homeassistant/components/soma/sensor.py | 37 +++++++++++++++++++ 3 files changed, 59 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/soma/__init__.py b/homeassistant/components/soma/__init__.py index d4dbbced453..bd5695cb7ec 100644 --- a/homeassistant/components/soma/__init__.py +++ b/homeassistant/components/soma/__init__.py @@ -1,9 +1,7 @@ """Support for Soma Smartshades.""" import asyncio -import logging from api.soma_api import SomaApi -from requests import RequestException import voluptuous as vol from homeassistant import config_entries @@ -17,8 +15,6 @@ from .const import API, DOMAIN, HOST, PORT DEVICES = "devices" -_LOGGER = logging.getLogger(__name__) - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -113,43 +109,3 @@ class SomaEntity(Entity): "name": self.name, "manufacturer": "Wazombi Labs", } - - async def async_update(self): - """Update the device with the latest data.""" - try: - response = await self.hass.async_add_executor_job( - self.api.get_shade_state, self.device["mac"] - ) - except RequestException: - _LOGGER.error("Connection to SOMA Connect failed") - self.is_available = False - return - if response["result"] != "success": - _LOGGER.error( - "Unable to reach device %s (%s)", self.device["name"], response["msg"] - ) - self.is_available = False - return - self.current_position = 100 - response["position"] - try: - response = await self.hass.async_add_executor_job( - self.api.get_battery_level, self.device["mac"] - ) - except RequestException: - _LOGGER.error("Connection to SOMA Connect failed") - self.is_available = False - return - if response["result"] != "success": - _LOGGER.error( - "Unable to reach device %s (%s)", self.device["name"], response["msg"] - ) - self.is_available = False - return - # https://support.somasmarthome.com/hc/en-us/articles/360026064234-HTTP-API - # battery_level response is expected to be min = 360, max 410 for - # 0-100% levels above 410 are consider 100% and below 360, 0% as the - # device considers 360 the minimum to move the motor. - _battery = round(2 * (response["battery_level"] - 360)) - battery = max(min(100, _battery), 0) - self.battery_state = battery - self.is_available = True diff --git a/homeassistant/components/soma/cover.py b/homeassistant/components/soma/cover.py index f2929dd8ddd..1005bf32f20 100644 --- a/homeassistant/components/soma/cover.py +++ b/homeassistant/components/soma/cover.py @@ -2,6 +2,8 @@ import logging +from requests import RequestException + from homeassistant.components.cover import ATTR_POSITION, CoverEntity from homeassistant.components.soma import API, DEVICES, DOMAIN, SomaEntity @@ -67,3 +69,23 @@ class SomaCover(SomaEntity, CoverEntity): def is_closed(self): """Return if the cover is closed.""" return self.current_position == 0 + + async def async_update(self): + """Update the cover with the latest data.""" + try: + _LOGGER.debug("Soma Cover Update") + response = await self.hass.async_add_executor_job( + self.api.get_shade_state, self.device["mac"] + ) + except RequestException: + _LOGGER.error("Connection to SOMA Connect failed") + self.is_available = False + return + if response["result"] != "success": + _LOGGER.error( + "Unable to reach device %s (%s)", self.device["name"], response["msg"] + ) + self.is_available = False + return + self.current_position = 100 - response["position"] + self.is_available = True diff --git a/homeassistant/components/soma/sensor.py b/homeassistant/components/soma/sensor.py index 2d37a0b0dce..9430a929e1e 100644 --- a/homeassistant/components/soma/sensor.py +++ b/homeassistant/components/soma/sensor.py @@ -1,10 +1,20 @@ """Support for Soma sensors.""" +from datetime import timedelta +import logging + +from requests import RequestException + from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle from . import DEVICES, SomaEntity from .const import API, DOMAIN +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30) + +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Soma sensor platform.""" @@ -38,3 +48,30 @@ class SomaSensor(SomaEntity, Entity): def unit_of_measurement(self): """Return the unit of measurement this sensor expresses itself in.""" return PERCENTAGE + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def async_update(self): + """Update the sensor with the latest data.""" + try: + _LOGGER.debug("Soma Sensor Update") + response = await self.hass.async_add_executor_job( + self.api.get_battery_level, self.device["mac"] + ) + except RequestException: + _LOGGER.error("Connection to SOMA Connect failed") + self.is_available = False + return + if response["result"] != "success": + _LOGGER.error( + "Unable to reach device %s (%s)", self.device["name"], response["msg"] + ) + self.is_available = False + return + # https://support.somasmarthome.com/hc/en-us/articles/360026064234-HTTP-API + # battery_level response is expected to be min = 360, max 410 for + # 0-100% levels above 410 are consider 100% and below 360, 0% as the + # device considers 360 the minimum to move the motor. + _battery = round(2 * (response["battery_level"] - 360)) + battery = max(min(100, _battery), 0) + self.battery_state = battery + self.is_available = True From ffd9c4e41043abec8463306de0413b9a7a5a644f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 13 Jan 2021 04:14:45 -1000 Subject: [PATCH 188/507] Add additional roku model to discovery (#45103) --- homeassistant/components/roku/manifest.json | 3 ++- homeassistant/generated/zeroconf.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index 682576b534a..f1509edb6fb 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -8,7 +8,8 @@ "3810X", "4660X", "7820X", - "C105X" + "C105X", + "C135X" ] }, "ssdp": [ diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 49527666f53..6acf52872d0 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -165,6 +165,7 @@ HOMEKIT = { "Abode": "abode", "BSB002": "hue", "C105X": "roku", + "C135X": "roku", "Healty Home Coach": "netatmo", "Iota": "abode", "LIFX": "lifx", From f78b02b163eb73e3c4ade80d28512136375dc512 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 14 Jan 2021 03:17:13 +1300 Subject: [PATCH 189/507] Do not try to connect to disabled ESPHome devices. (#45092) --- homeassistant/components/esphome/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index fcfb4cf7ff1..c0c3d02ec56 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -225,6 +225,14 @@ async def _setup_auto_reconnect_logic( # When removing/disconnecting manually return + device_registry = await hass.helpers.device_registry.async_get_registry() + devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id) + for device in devices: + # There is only one device in ESPHome + if device.disabled: + # Don't attempt to connect if it's disabled + return + data: RuntimeEntryData = hass.data[DOMAIN][entry.entry_id] for disconnect_cb in data.disconnect_callbacks: disconnect_cb() From 3364e945aabbec646e52104403b43b5378e422f9 Mon Sep 17 00:00:00 2001 From: Jacob Southard Date: Wed, 13 Jan 2021 08:21:32 -0600 Subject: [PATCH 190/507] Fix HomeKit climate integration for devices with a single set point in Heat_Cool mode. (#45065) * Check supported flags in auto mode, and add tests. * Fix test description. --- .../components/homekit_controller/climate.py | 53 +++++++--- .../homekit_controller/test_climate.py | 100 ++++++++++++++++++ 2 files changed, 139 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 042bc4771c1..cb0feb6ba77 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -346,7 +346,9 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): heat_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) cool_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) - if MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT_COOL}: + if (MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT_COOL}) and ( + SUPPORT_TARGET_TEMPERATURE_RANGE & self.supported_features + ): if temp is None: temp = (cool_temp + heat_temp) / 2 await self.async_put_characteristics( @@ -386,37 +388,54 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): def target_temperature(self): """Return the temperature we try to reach.""" value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) - if MODE_HOMEKIT_TO_HASS.get(value) not in {HVAC_MODE_HEAT, HVAC_MODE_COOL}: - return None - return self.service.value(CharacteristicsTypes.TEMPERATURE_TARGET) + if (MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT, HVAC_MODE_COOL}) or ( + (MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT_COOL}) + and not (SUPPORT_TARGET_TEMPERATURE_RANGE & self.supported_features) + ): + return self.service.value(CharacteristicsTypes.TEMPERATURE_TARGET) + return None @property def target_temperature_high(self): """Return the highbound target temperature we try to reach.""" value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) - if MODE_HOMEKIT_TO_HASS.get(value) not in {HVAC_MODE_HEAT_COOL}: - return None - return self.service.value(CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD) + if (MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT_COOL}) and ( + SUPPORT_TARGET_TEMPERATURE_RANGE & self.supported_features + ): + return self.service.value( + CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD + ) + return None @property def target_temperature_low(self): """Return the lowbound target temperature we try to reach.""" value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) - if MODE_HOMEKIT_TO_HASS.get(value) not in {HVAC_MODE_HEAT_COOL}: - return None - return self.service.value(CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD) + if (MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT_COOL}) and ( + SUPPORT_TARGET_TEMPERATURE_RANGE & self.supported_features + ): + return self.service.value( + CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD + ) + return None @property def min_temp(self): """Return the minimum target temp.""" value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) - if MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT_COOL}: + if (MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT_COOL}) and ( + SUPPORT_TARGET_TEMPERATURE_RANGE & self.supported_features + ): min_temp = self.service[ CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD ].minValue if min_temp is not None: return min_temp - if MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT, HVAC_MODE_COOL}: + elif MODE_HOMEKIT_TO_HASS.get(value) in { + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + }: min_temp = self.service[CharacteristicsTypes.TEMPERATURE_TARGET].minValue if min_temp is not None: return min_temp @@ -426,13 +445,19 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): def max_temp(self): """Return the maximum target temp.""" value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) - if MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT_COOL}: + if (MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT_COOL}) and ( + SUPPORT_TARGET_TEMPERATURE_RANGE & self.supported_features + ): max_temp = self.service[ CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD ].maxValue if max_temp is not None: return max_temp - if MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT, HVAC_MODE_COOL}: + elif MODE_HOMEKIT_TO_HASS.get(value) in { + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + }: max_temp = self.service[CharacteristicsTypes.TEMPERATURE_TARGET].maxValue if max_temp is not None: return max_temp diff --git a/tests/components/homekit_controller/test_climate.py b/tests/components/homekit_controller/test_climate.py index d3f852d7a49..52671703cca 100644 --- a/tests/components/homekit_controller/test_climate.py +++ b/tests/components/homekit_controller/test_climate.py @@ -283,6 +283,106 @@ async def test_climate_cannot_set_thermostat_temp_range_in_wrong_mode(hass, utcn assert helper.characteristics[THERMOSTAT_TEMPERATURE_COOLING_THRESHOLD].value == 0 +def create_thermostat_single_set_point_auto(accessory): + """Define thermostat characteristics with a single set point in auto.""" + service = accessory.add_service(ServicesTypes.THERMOSTAT) + + char = service.add_char(CharacteristicsTypes.HEATING_COOLING_TARGET) + char.value = 0 + + char = service.add_char(CharacteristicsTypes.HEATING_COOLING_CURRENT) + char.value = 0 + + char = service.add_char(CharacteristicsTypes.TEMPERATURE_TARGET) + char.minValue = 7 + char.maxValue = 35 + char.value = 0 + + char = service.add_char(CharacteristicsTypes.TEMPERATURE_CURRENT) + char.value = 0 + + char = service.add_char(CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET) + char.value = 0 + + char = service.add_char(CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT) + char.value = 0 + + +async def test_climate_check_min_max_values_per_mode_sspa_device(hass, utcnow): + """Test appropriate min/max values for each mode on sspa devices.""" + helper = await setup_test_component(hass, create_thermostat_single_set_point_auto) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_HEAT}, + blocking=True, + ) + climate_state = await helper.poll_and_get_state() + assert climate_state.attributes["min_temp"] == 7 + assert climate_state.attributes["max_temp"] == 35 + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_COOL}, + blocking=True, + ) + climate_state = await helper.poll_and_get_state() + assert climate_state.attributes["min_temp"] == 7 + assert climate_state.attributes["max_temp"] == 35 + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_HEAT_COOL}, + blocking=True, + ) + climate_state = await helper.poll_and_get_state() + assert climate_state.attributes["min_temp"] == 7 + assert climate_state.attributes["max_temp"] == 35 + + +async def test_climate_set_thermostat_temp_on_sspa_device(hass, utcnow): + """Test setting temperature in different modes on device with single set point in auto.""" + helper = await setup_test_component(hass, create_thermostat_single_set_point_auto) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_HEAT}, + blocking=True, + ) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + {"entity_id": "climate.testdevice", "temperature": 21}, + blocking=True, + ) + assert helper.characteristics[TEMPERATURE_TARGET].value == 21 + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_HEAT_COOL}, + blocking=True, + ) + assert helper.characteristics[TEMPERATURE_TARGET].value == 21 + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + { + "entity_id": "climate.testdevice", + "hvac_mode": HVAC_MODE_HEAT_COOL, + "temperature": 22, + }, + blocking=True, + ) + assert helper.characteristics[TEMPERATURE_TARGET].value == 22 + + async def test_climate_change_thermostat_humidity(hass, utcnow): """Test that we can turn a HomeKit thermostat on and off again.""" helper = await setup_test_component(hass, create_thermostat_service) From 411cc6542ca034694eadaa7502fac4c68b516cf1 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 13 Jan 2021 08:24:44 -0600 Subject: [PATCH 191/507] Move Plex->Sonos playback to built-in service (#45066) * Move Plex->Sonos playback service from integration to platform * Test against 'native' Plex media_players * Add Plex to Sonos after_dependencies * Remove circular dependency * Raise exceptions in failed service calls * Add test to forward service call from Sonos * Additional Sonos->Plex tests * Fix docstring --- homeassistant/components/plex/__init__.py | 59 +--- homeassistant/components/plex/const.py | 1 - homeassistant/components/plex/manifest.json | 1 - homeassistant/components/plex/services.py | 42 ++- homeassistant/components/sonos/__init__.py | 24 +- homeassistant/components/sonos/manifest.json | 1 + .../components/sonos/media_player.py | 9 +- tests/components/plex/conftest.py | 6 - tests/components/plex/test_playback.py | 309 +++++++----------- tests/components/plex/test_services.py | 30 +- tests/components/sonos/conftest.py | 20 +- tests/components/sonos/test_plex_playback.py | 120 +++++++ tests/fixtures/plex/sonos_resources.xml | 6 +- 13 files changed, 312 insertions(+), 316 deletions(-) create mode 100644 tests/components/sonos/test_plex_playback.py diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index de8b278f3cf..6b403150e9c 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -14,24 +14,17 @@ from plexwebsocket import ( PlexWebsocket, ) import requests.exceptions -import voluptuous as vol from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.components.media_player.const import ( - ATTR_MEDIA_CONTENT_ID, - ATTR_MEDIA_CONTENT_TYPE, -) from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY, SOURCE_REAUTH from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_SOURCE, CONF_URL, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import callback -from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError -from homeassistant.helpers import config_validation as cv +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -48,12 +41,11 @@ from .const import ( PLEX_SERVER_CONFIG, PLEX_UPDATE_PLATFORMS_SIGNAL, SERVERS, - SERVICE_PLAY_ON_SONOS, WEBSOCKETS, ) from .errors import ShouldUpdateConfigEntry from .server import PlexServer -from .services import async_setup_services, lookup_plex_media +from .services import async_setup_services _LOGGER = logging.getLogger(__package__) @@ -218,31 +210,13 @@ async def async_setup_entry(hass, entry): ) 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) - - play_on_sonos_schema = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_id, - vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string, - vol.Optional(ATTR_MEDIA_CONTENT_TYPE): vol.In("music"), - } - ) - def get_plex_account(plex_server): try: return plex_server.account except (plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized): return None - plex_account = await hass.async_add_executor_job(get_plex_account, plex_server) - if plex_account: - hass.services.async_register( - PLEX_DOMAIN, - SERVICE_PLAY_ON_SONOS, - async_play_on_sonos_service, - schema=play_on_sonos_schema, - ) + await hass.async_add_executor_job(get_plex_account, plex_server) return True @@ -276,30 +250,3 @@ async def async_options_updated(hass, entry): # Guard incomplete setup during reauth flows if server_id in hass.data[PLEX_DOMAIN][SERVERS]: hass.data[PLEX_DOMAIN][SERVERS][server_id].options = entry.options - - -def play_on_sonos(hass, service_call): - """Play Plex media on a linked Sonos device.""" - entity_id = service_call.data[ATTR_ENTITY_ID] - content_id = service_call.data[ATTR_MEDIA_CONTENT_ID] - content_type = service_call.data.get(ATTR_MEDIA_CONTENT_TYPE) - - sonos = hass.components.sonos - try: - sonos_name = sonos.get_coordinator_name(entity_id) - except HomeAssistantError as err: - _LOGGER.error("Cannot get Sonos device: %s", err) - return - - media, plex_server = lookup_plex_media(hass, content_type, content_id) - if media is None: - return - - sonos_speaker = plex_server.account.sonos_speaker(sonos_name) - if sonos_speaker is None: - _LOGGER.error( - "Sonos speaker '%s' could not be found on this Plex account", sonos_name - ) - return - - sonos_speaker.playMedia(media) diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index c13be439be7..eec433202e4 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -47,7 +47,6 @@ X_PLEX_VERSION = __version__ AUTOMATIC_SETUP_STRING = "Obtain a new token from plex.tv" MANUAL_SETUP_STRING = "Configure Plex server manually" -SERVICE_PLAY_ON_SONOS = "play_on_sonos" SERVICE_REFRESH_LIBRARY = "refresh_library" SERVICE_SCAN_CLIENTS = "scan_for_clients" diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 5bfbc4932ab..34a02e8ae20 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -9,6 +9,5 @@ "plexwebsocket==0.0.12" ], "dependencies": ["http"], - "after_dependencies": ["sonos"], "codeowners": ["@jjlawren"] } diff --git a/homeassistant/components/plex/services.py b/homeassistant/components/plex/services.py index 19a04a61a9f..4d9b518aa62 100644 --- a/homeassistant/components/plex/services.py +++ b/homeassistant/components/plex/services.py @@ -5,6 +5,7 @@ import logging from plexapi.exceptions import NotFound import voluptuous as vol +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( @@ -52,8 +53,6 @@ def refresh_library(hass, service_call): library_name = service_call.data["library_name"] plex_server = get_plex_server(hass, plex_server_name) - if not plex_server: - return try: library = plex_server.library.section(title=library_name) @@ -73,31 +72,31 @@ def get_plex_server(hass, plex_server_name=None): """Retrieve a configured Plex server by name.""" plex_servers = hass.data[DOMAIN][SERVERS].values() + if not plex_servers: + raise HomeAssistantError("No Plex servers available") + if plex_server_name: plex_server = next( (x for x in plex_servers if x.friendly_name == plex_server_name), None ) if plex_server is not None: return plex_server - _LOGGER.error( - "Requested Plex server '%s' not found in %s", - plex_server_name, - [x.friendly_name for x in plex_servers], + friendly_names = [x.friendly_name for x in plex_servers] + raise HomeAssistantError( + f"Requested Plex server '{plex_server_name}' not found in {friendly_names}" ) - return None if len(plex_servers) == 1: return next(iter(plex_servers)) - _LOGGER.error( - "Multiple Plex servers configured, choose with 'plex_server' key: %s", - [x.friendly_name for x in plex_servers], + friendly_names = [x.friendly_name for x in plex_servers] + raise HomeAssistantError( + f"Multiple Plex servers configured, choose with 'plex_server' key: {friendly_names}" ) - return None def lookup_plex_media(hass, content_type, content_id): - """Look up Plex media using media_player.play_media service payloads.""" + """Look up Plex media for other integrations using media_player.play_media service payloads.""" content = json.loads(content_id) if isinstance(content, int): @@ -108,13 +107,24 @@ def lookup_plex_media(hass, content_type, content_id): shuffle = content.pop("shuffle", 0) plex_server = get_plex_server(hass, plex_server_name=plex_server_name) - if not plex_server: - return (None, None) media = plex_server.lookup_media(content_type, **content) if media is None: - _LOGGER.error("Media could not be found: %s", content) - return (None, None) + raise HomeAssistantError(f"Plex media not found using payload: '{content_id}'") playqueue = plex_server.create_playqueue(media, shuffle=shuffle) return (playqueue, plex_server) + + +def play_on_sonos(hass, content_type, content_id, speaker_name): + """Play music on a connected Sonos speaker using Plex APIs. + + Called by Sonos 'media_player.play_media' service. + """ + media, plex_server = lookup_plex_media(hass, content_type, content_id) + sonos_speaker = plex_server.account.sonos_speaker(speaker_name) + if sonos_speaker is None: + message = f"Sonos speaker '{speaker_name}' is not associated with '{plex_server.friendly_name}'" + _LOGGER.error(message) + raise HomeAssistantError(message) + sonos_speaker.playMedia(media) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index cc33134c810..c3a977e32e1 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -4,11 +4,9 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.const import CONF_HOSTS -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv -from homeassistant.loader import bind_hass -from .const import DATA_SONOS, DOMAIN +from .const import DOMAIN CONF_ADVERTISE_ADDR = "advertise_addr" CONF_INTERFACE_ADDR = "interface_addr" @@ -55,23 +53,3 @@ async def async_setup_entry(hass, entry): hass.config_entries.async_forward_entry_setup(entry, MP_DOMAIN) ) return True - - -@bind_hass -def get_coordinator_name(hass, entity_id): - """Obtain the room/name of a device's coordinator. - - Used by the Plex integration. - - This function is safe to run inside the event loop. - """ - if DATA_SONOS not in hass.data: - raise HomeAssistantError("Sonos integration not set up") - - device = next( - (x for x in hass.data[DATA_SONOS].entities if x.entity_id == entity_id), None - ) - - if device.is_coordinator: - return device.name - return device.coordinator.name diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 66e6587b9ff..1852f9c3849 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -4,6 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", "requirements": ["pysonos==0.0.37"], + "after_dependencies": ["plex"], "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 48b22256030..9d89bdf68f8 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -59,6 +59,8 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_SET, ) from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.plex.const import PLEX_URI_SCHEME +from homeassistant.components.plex.services import play_on_sonos from homeassistant.const import ( ATTR_TIME, EVENT_HOMEASSISTANT_STOP, @@ -1186,12 +1188,17 @@ class SonosEntity(MediaPlayerEntity): """ Send the play_media command to the media player. + If media_id is a Plex payload, attempt Plex->Sonos playback. + If media_type is "playlist", media_id should be a Sonos Playlist name. Otherwise, media_id should be a URI. If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue. """ - if media_type in (MEDIA_TYPE_MUSIC, MEDIA_TYPE_TRACK): + if media_id and media_id.startswith(PLEX_URI_SCHEME): + media_id = media_id[len(PLEX_URI_SCHEME) :] + play_on_sonos(self.hass, media_type, media_id, self.name) + elif media_type in (MEDIA_TYPE_MUSIC, MEDIA_TYPE_TRACK): if kwargs.get(ATTR_MEDIA_ENQUEUE): try: if self.soco.is_spotify_uri(media_id): diff --git a/tests/components/plex/conftest.py b/tests/components/plex/conftest.py index 8fc25a819e8..d74c8df1594 100644 --- a/tests/components/plex/conftest.py +++ b/tests/components/plex/conftest.py @@ -254,12 +254,6 @@ def show_seasons_fixture(): return load_fixture("plex/show_seasons.xml") -@pytest.fixture(name="sonos_resources", scope="session") -def sonos_resources_fixture(): - """Load Sonos resources payload and return it.""" - return load_fixture("plex/sonos_resources.xml") - - @pytest.fixture(name="entry") def mock_config_entry(): """Return the default mocked config entry.""" diff --git a/tests/components/plex/test_playback.py b/tests/components/plex/test_playback.py index e8d528eb073..86e55dab613 100644 --- a/tests/components/plex/test_playback.py +++ b/tests/components/plex/test_playback.py @@ -1,219 +1,136 @@ """Tests for Plex player playback methods/services.""" from unittest.mock import patch -from plexapi.exceptions import NotFound - from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, + DOMAIN as MP_DOMAIN, + MEDIA_TYPE_EPISODE, + MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, + SERVICE_PLAY_MEDIA, ) -from homeassistant.components.plex.const import ( - CONF_SERVER, - CONF_SERVER_IDENTIFIER, - DOMAIN, - PLEX_SERVER_CONFIG, - SERVERS, - SERVICE_PLAY_ON_SONOS, -) -from homeassistant.const import ATTR_ENTITY_ID, CONF_URL -from homeassistant.exceptions import HomeAssistantError - -from .const import DEFAULT_OPTIONS, MOCK_SERVERS, SECONDARY_DATA - -from tests.common import MockConfigEntry +from homeassistant.const import ATTR_ENTITY_ID -async def test_sonos_playback( - hass, mock_plex_server, requests_mock, playqueue_created, sonos_resources +async def test_media_player_playback( + hass, setup_plex_server, requests_mock, playqueue_created, player_plexweb_resources ): - """Test playing media on a Sonos speaker.""" - server_id = mock_plex_server.machine_identifier - loaded_server = hass.data[DOMAIN][SERVERS][server_id] - - # Test Sonos integration lookup failure - with patch.object( - hass.components.sonos, "get_coordinator_name", side_effect=HomeAssistantError - ): - assert await hass.services.async_call( - DOMAIN, - SERVICE_PLAY_ON_SONOS, - { - ATTR_ENTITY_ID: "media_player.sonos_kitchen", - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, - ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album"}', - }, - True, - ) - - # Test success with plex_key - requests_mock.get("https://sonos.plex.tv/resources", text=sonos_resources) - requests_mock.get( - "https://sonos.plex.tv/player/playback/playMedia", status_code=200 - ) - requests_mock.post("/playqueues", text=playqueue_created) - with patch.object( - hass.components.sonos, - "get_coordinator_name", - return_value="Speaker 2", - ): - assert await hass.services.async_call( - DOMAIN, - SERVICE_PLAY_ON_SONOS, - { - ATTR_ENTITY_ID: "media_player.sonos_kitchen", - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, - ATTR_MEDIA_CONTENT_ID: "100", - }, - True, - ) - - # Test success with dict - with patch.object( - hass.components.sonos, - "get_coordinator_name", - return_value="Speaker 2", - ): - assert await hass.services.async_call( - DOMAIN, - SERVICE_PLAY_ON_SONOS, - { - ATTR_ENTITY_ID: "media_player.sonos_kitchen", - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, - ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album"}', - }, - True, - ) - - # Test media lookup failure - with patch.object( - hass.components.sonos, - "get_coordinator_name", - return_value="Speaker 2", - ), patch("plexapi.server.PlexServer.fetchItem", side_effect=NotFound): - assert await hass.services.async_call( - DOMAIN, - SERVICE_PLAY_ON_SONOS, - { - ATTR_ENTITY_ID: "media_player.sonos_kitchen", - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, - ATTR_MEDIA_CONTENT_ID: "999", - }, - True, - ) - - # Test invalid Plex server requested - with patch.object( - hass.components.sonos, - "get_coordinator_name", - return_value="Speaker 2", - ): - assert await hass.services.async_call( - DOMAIN, - SERVICE_PLAY_ON_SONOS, - { - ATTR_ENTITY_ID: "media_player.sonos_kitchen", - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, - ATTR_MEDIA_CONTENT_ID: '{"plex_server": "unknown_plex_server", "library_name": "Music", "artist_name": "Artist", "album_name": "Album"}', - }, - True, - ) - - # Test no speakers available - with patch.object( - loaded_server.account, "sonos_speaker", return_value=None - ), patch.object( - hass.components.sonos, - "get_coordinator_name", - return_value="Speaker 2", - ), patch( - "plexapi.playqueue.PlayQueue.create" - ): - assert await hass.services.async_call( - DOMAIN, - SERVICE_PLAY_ON_SONOS, - { - ATTR_ENTITY_ID: "media_player.sonos_kitchen", - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, - ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album"}', - }, - True, - ) - - -async def test_playback_multiple_servers( - hass, - setup_plex_server, - requests_mock, - caplog, - empty_payload, - playqueue_created, - plex_server_accounts, - plex_server_base, - sonos_resources, -): - """Test playing media when multiple servers available.""" - secondary_entry = MockConfigEntry( - domain=DOMAIN, - data=SECONDARY_DATA, - options=DEFAULT_OPTIONS, - unique_id=SECONDARY_DATA["server_id"], - ) - - secondary_url = SECONDARY_DATA[PLEX_SERVER_CONFIG][CONF_URL] - secondary_name = SECONDARY_DATA[CONF_SERVER] - secondary_id = SECONDARY_DATA[CONF_SERVER_IDENTIFIER] - requests_mock.get( - secondary_url, - text=plex_server_base.format( - name=secondary_name, machine_identifier=secondary_id - ), - ) - requests_mock.get(f"{secondary_url}/accounts", text=plex_server_accounts) - requests_mock.get(f"{secondary_url}/clients", text=empty_payload) - requests_mock.get(f"{secondary_url}/status/sessions", text=empty_payload) + """Test playing media on a Plex media_player.""" + requests_mock.get("http://1.2.3.5:32400/resources", text=player_plexweb_resources) await setup_plex_server() - await setup_plex_server(config_entry=secondary_entry) - requests_mock.get("https://sonos.plex.tv/resources", text=sonos_resources) - requests_mock.get( - "https://sonos.plex.tv/player/playback/playMedia", status_code=200 - ) + media_player = "media_player.plex_plex_web_chrome" requests_mock.post("/playqueues", text=playqueue_created) + requests_mock.get("/player/playback/playMedia", status_code=200) - with patch.object( - hass.components.sonos, - "get_coordinator_name", - return_value="Speaker 2", - ): - assert await hass.services.async_call( - DOMAIN, - SERVICE_PLAY_ON_SONOS, - { - ATTR_ENTITY_ID: "media_player.sonos_kitchen", - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, - ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album"}', - }, - True, - ) - - assert ( - "Multiple Plex servers configured, choose with 'plex_server' key" in caplog.text + # Test movie success + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies", "title": "Movie 1" }', + }, + True, ) - with patch.object( - hass.components.sonos, - "get_coordinator_name", - return_value="Speaker 2", - ): + # Test movie incomplete dict + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies"}', + }, + True, + ) + + # Test movie failure with options + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies", "title": "Does not exist" }', + }, + True, + ) + + # Test movie failure with nothing found + with patch("plexapi.library.LibrarySection.search", return_value=None): assert await hass.services.async_call( - DOMAIN, - SERVICE_PLAY_ON_SONOS, + MP_DOMAIN, + SERVICE_PLAY_MEDIA, { - ATTR_ENTITY_ID: "media_player.sonos_kitchen", - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, - ATTR_MEDIA_CONTENT_ID: f'{{"plex_server": "{MOCK_SERVERS[0][CONF_SERVER]}", "library_name": "Music", "artist_name": "Artist", "album_name": "Album"}}', + ATTR_ENTITY_ID: media_player, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies", "title": "Does not exist" }', }, True, ) + + # Test movie success with dict + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album"}', + }, + True, + ) + + # Test TV show episoe lookup failure + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_EPISODE, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "TV Shows", "show_name": "TV Show", "season_number": 1, "episode_number": 99}', + }, + True, + ) + + # Test track name lookup failure + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album", "track_name": "Not a track"}', + }, + True, + ) + + # Test media lookup failure by key + requests_mock.get("/library/metadata/999", status_code=404) + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_ID: "999", + }, + True, + ) + + # Test invalid Plex server requested + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_ID: '{"plex_server": "unknown_plex_server", "library_name": "Music", "artist_name": "Artist", "album_name": "Album"}', + }, + True, + ) diff --git a/tests/components/plex/test_services.py b/tests/components/plex/test_services.py index 18375a9f80f..520458ebfe8 100644 --- a/tests/components/plex/test_services.py +++ b/tests/components/plex/test_services.py @@ -1,4 +1,6 @@ """Tests for various Plex services.""" +import pytest + from homeassistant.components.plex.const import ( CONF_SERVER, CONF_SERVER_IDENTIFIER, @@ -8,6 +10,7 @@ from homeassistant.components.plex.const import ( SERVICE_SCAN_CLIENTS, ) from homeassistant.const import CONF_URL +from homeassistant.exceptions import HomeAssistantError from .const import DEFAULT_OPTIONS, SECONDARY_DATA @@ -28,12 +31,13 @@ async def test_refresh_library( refresh = requests_mock.get(f"{url}/library/sections/1/refresh", status_code=200) # Test with non-existent server - assert await hass.services.async_call( - DOMAIN, - SERVICE_REFRESH_LIBRARY, - {"server_name": "Not a Server", "library_name": "Movies"}, - True, - ) + with pytest.raises(HomeAssistantError): + assert await hass.services.async_call( + DOMAIN, + SERVICE_REFRESH_LIBRARY, + {"server_name": "Not a Server", "library_name": "Movies"}, + True, + ) assert not refresh.called # Test with non-existent library @@ -78,12 +82,14 @@ async def test_refresh_library( await setup_plex_server(config_entry=entry_2) # Test multiple servers available but none specified - assert await hass.services.async_call( - DOMAIN, - SERVICE_REFRESH_LIBRARY, - {"library_name": "Movies"}, - True, - ) + with pytest.raises(HomeAssistantError) as excinfo: + assert await hass.services.async_call( + DOMAIN, + SERVICE_REFRESH_LIBRARY, + {"library_name": "Movies"}, + True, + ) + assert "Multiple Plex servers configured" in str(excinfo.value) assert refresh.call_count == 1 diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 1ce2205813b..688026ba06c 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -7,7 +7,7 @@ from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.sonos import DOMAIN from homeassistant.const import CONF_HOSTS -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_fixture @pytest.fixture(name="config_entry") @@ -77,3 +77,21 @@ def speaker_info_fixture(): "software_version": "49.2-64250", "mac_address": "00-11-22-33-44-55", } + + +@pytest.fixture(name="plex_empty_payload", scope="session") +def plex_empty_payload_fixture(): + """Load an empty payload and return it.""" + return load_fixture("plex/empty_payload.xml") + + +@pytest.fixture(name="plextv_account", scope="session") +def plextv_account_fixture(): + """Load account info from plex.tv and return it.""" + return load_fixture("plex/plextv_account.xml") + + +@pytest.fixture(name="plex_sonos_resources", scope="session") +def plex_sonos_resources_fixture(): + """Load Sonos resources payload and return it.""" + return load_fixture("plex/sonos_resources.xml") diff --git a/tests/components/sonos/test_plex_playback.py b/tests/components/sonos/test_plex_playback.py new file mode 100644 index 00000000000..ce84b69057a --- /dev/null +++ b/tests/components/sonos/test_plex_playback.py @@ -0,0 +1,120 @@ +"""Tests for the Sonos Media Player platform.""" +from unittest.mock import patch + +from plexapi.myplex import MyPlexAccount +import pytest + +from homeassistant.components.media_player.const import ( + ATTR_MEDIA_CONTENT_ID, + ATTR_MEDIA_CONTENT_TYPE, + DOMAIN as MP_DOMAIN, + MEDIA_TYPE_MUSIC, + SERVICE_PLAY_MEDIA, +) +from homeassistant.components.plex.const import DOMAIN as PLEX_DOMAIN, SERVERS +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.exceptions import HomeAssistantError + +from .test_media_player import setup_platform + + +async def test_plex_play_media( + hass, + config_entry, + config, + requests_mock, + plextv_account, + plex_empty_payload, + plex_sonos_resources, +): + """Test playing media via the Plex integration.""" + requests_mock.get("https://plex.tv/users/account", text=plextv_account) + requests_mock.get("https://sonos.plex.tv/resources", text=plex_empty_payload) + + class MockPlexServer: + """Mock a PlexServer instance.""" + + def __init__(self, has_media=False): + self.account = MyPlexAccount(token="token") + self.friendly_name = "plex" + if has_media: + self.media = "media" + else: + self.media = None + + def create_playqueue(self, media, **kwargs): + pass + + def lookup_media(self, content_type, **kwargs): + return self.media + + await setup_platform(hass, config_entry, config) + hass.data[PLEX_DOMAIN] = {SERVERS: {}} + media_player = "media_player.zone_a" + + # Test Plex service call with media key + with pytest.raises(HomeAssistantError) as excinfo: + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_ID: "plex://5", + }, + True, + ) + assert "No Plex servers available" in str(excinfo.value) + + # Add a mocked Plex server with no media + hass.data[PLEX_DOMAIN][SERVERS] = {"plex": MockPlexServer()} + + # Test Plex service call with dict + with pytest.raises(HomeAssistantError) as excinfo: + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_ID: 'plex://{"library_name": "Music", "artist_name": "Artist"}', + }, + True, + ) + assert "Plex media not found" in str(excinfo.value) + + # Add a mocked Plex server + hass.data[PLEX_DOMAIN][SERVERS] = {"plex": MockPlexServer(has_media=True)} + + # Test Plex service call with no Sonos speakers + requests_mock.get("https://sonos.plex.tv/resources", text=plex_empty_payload) + with pytest.raises(HomeAssistantError) as excinfo: + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_ID: 'plex://{"library_name": "Music", "artist_name": "Artist"}', + }, + True, + ) + assert "Sonos speaker 'Zone A' is not associated with" in str(excinfo.value) + + # Test successful Plex service call + account = hass.data[PLEX_DOMAIN][SERVERS]["plex"].account + requests_mock.get("https://sonos.plex.tv/resources", text=plex_sonos_resources) + + with patch.object(account, "_sonos_cache_timestamp", 0), patch( + "plexapi.sonos.PlexSonosClient.playMedia" + ): + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_ID: 'plex://{"plex_server": "plex", "library_name": "Music", "artist_name": "Artist", "album_name": "Album"}', + }, + True, + ) diff --git a/tests/fixtures/plex/sonos_resources.xml b/tests/fixtures/plex/sonos_resources.xml index 334fdd311ef..1cf8f276822 100644 --- a/tests/fixtures/plex/sonos_resources.xml +++ b/tests/fixtures/plex/sonos_resources.xml @@ -1,5 +1,5 @@ - - - + + + From 938d8be0c83301f33c636791cc7f2e9d7dc39d77 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Wed, 13 Jan 2021 15:25:28 +0100 Subject: [PATCH 192/507] Bump bimmer_connected to 0.7.14 (#45086) Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 5bce904e1cd..c1d90f713f4 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -2,7 +2,7 @@ "domain": "bmw_connected_drive", "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", - "requirements": ["bimmer_connected==0.7.13"], + "requirements": ["bimmer_connected==0.7.14"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true } diff --git a/requirements_all.txt b/requirements_all.txt index 460c3010fec..eebeccf5e08 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -339,7 +339,7 @@ beautifulsoup4==4.9.1 bellows==0.21.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.7.13 +bimmer_connected==0.7.14 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 034f3d3dd61..5d0e316d48f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -189,7 +189,7 @@ base36==0.1.1 bellows==0.21.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.7.13 +bimmer_connected==0.7.14 # homeassistant.components.blebox blebox_uniapi==1.3.2 From 3537a7c3d5b639baf8656393b0ca25134cdedb7b Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 13 Jan 2021 15:31:31 +0100 Subject: [PATCH 193/507] Correct zwave_js value changed callback signature (#45110) --- .coveragerc | 1 - homeassistant/components/zwave_js/entity.py | 7 ++----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.coveragerc b/.coveragerc index a8f198d436c..d57ffe39217 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1091,7 +1091,6 @@ omit = homeassistant/components/supla/* homeassistant/components/zwave/util.py homeassistant/components/zwave_js/discovery.py - homeassistant/components/zwave_js/entity.py homeassistant/components/zwave_js/light.py homeassistant/components/zwave_js/sensor.py diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index 70630cbd89c..5bc1e477523 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -83,15 +83,12 @@ class ZWaveBaseEntity(Entity): return self.client.connected and bool(self.info.node.ready) @callback - def _value_changed(self, event_data: Union[dict, ZwaveValue]) -> None: + def _value_changed(self, event_data: dict) -> None: """Call when (one of) our watched values changes. Should not be overridden by subclasses. """ - if isinstance(event_data, ZwaveValue): - value_id = event_data.value_id - else: - value_id = event_data["value"].value_id + value_id = event_data["value"].value_id if value_id not in self.watched_value_ids: return From 83b210061df567926dc3d6dbc832541b71442f4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Conde=20G=C3=B3mez?= Date: Wed, 13 Jan 2021 16:09:05 +0100 Subject: [PATCH 194/507] Add config_flow and stream selection to foscam (#41429) * Add config_flow and stream selection to foscam * Simplify config_flow steps * Make debug log entry more useful * Deprecate config and platform schemas * Simplify config loading * Add config flow testing * Remove unneeded CONFIG_SCHEMA deprecation * Improve test coverage * Unload service by tracking loaded entries * Address comment Co-authored-by: Paulus Schoutsen --- .coveragerc | 2 +- homeassistant/components/foscam/__init__.py | 50 +++ homeassistant/components/foscam/camera.py | 184 ++++----- .../components/foscam/config_flow.py | 123 ++++++ homeassistant/components/foscam/const.py | 9 +- homeassistant/components/foscam/manifest.json | 1 + homeassistant/components/foscam/strings.json | 24 ++ .../components/foscam/translations/en.json | 24 ++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/foscam/__init__.py | 1 + tests/components/foscam/test_config_flow.py | 358 ++++++++++++++++++ 12 files changed, 689 insertions(+), 91 deletions(-) create mode 100644 homeassistant/components/foscam/config_flow.py create mode 100644 homeassistant/components/foscam/strings.json create mode 100644 homeassistant/components/foscam/translations/en.json create mode 100644 tests/components/foscam/__init__.py create mode 100644 tests/components/foscam/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index d57ffe39217..4599ddfbd7b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -300,8 +300,8 @@ omit = homeassistant/components/folder_watcher/* homeassistant/components/foobot/sensor.py homeassistant/components/fortios/device_tracker.py + homeassistant/components/foscam/__init__.py homeassistant/components/foscam/camera.py - homeassistant/components/foscam/const.py homeassistant/components/foursquare/* homeassistant/components/free_mobile/notify.py homeassistant/components/freebox/__init__.py diff --git a/homeassistant/components/foscam/__init__.py b/homeassistant/components/foscam/__init__.py index 5c63f7b2a15..0b9620ceab3 100644 --- a/homeassistant/components/foscam/__init__.py +++ b/homeassistant/components/foscam/__init__.py @@ -1 +1,51 @@ """The foscam component.""" +import asyncio + +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN, SERVICE_PTZ + +PLATFORMS = ["camera"] + +CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the foscam component.""" + hass.data.setdefault(DOMAIN, {}) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up foscam from a config entry.""" + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + hass.data[DOMAIN][entry.unique_id] = entry.data + + 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.unique_id) + + if not hass.data[DOMAIN]: + hass.services.async_remove(domain=DOMAIN, service=SERVICE_PTZ) + + return unload_ok diff --git a/homeassistant/components/foscam/camera.py b/homeassistant/components/foscam/camera.py index bc28e160b25..edbd10e04b9 100644 --- a/homeassistant/components/foscam/camera.py +++ b/homeassistant/components/foscam/camera.py @@ -1,13 +1,14 @@ """This component provides basic support for Foscam IP cameras.""" import asyncio -import logging from libpyfoscam import FoscamCamera import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_ENTITY_ID, + CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, @@ -15,21 +16,18 @@ from homeassistant.const import ( ) from homeassistant.helpers import config_validation as cv, entity_platform -from .const import DATA as FOSCAM_DATA, ENTITIES as FOSCAM_ENTITIES +from .const import CONF_STREAM, DOMAIN, LOGGER, SERVICE_PTZ -_LOGGER = logging.getLogger(__name__) - -CONF_IP = "ip" -CONF_RTSP_PORT = "rtsp_port" - -DEFAULT_NAME = "Foscam Camera" -DEFAULT_PORT = 88 - -SERVICE_PTZ = "ptz" -ATTR_MOVEMENT = "movement" -ATTR_TRAVELTIME = "travel_time" - -DEFAULT_TRAVELTIME = 0.125 +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required("ip"): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_NAME, default="Foscam Camera"): cv.string, + vol.Optional(CONF_PORT, default=88): cv.port, + vol.Optional("rtsp_port"): cv.port, + } +) DIR_UP = "up" DIR_DOWN = "down" @@ -52,16 +50,11 @@ MOVEMENT_ATTRS = { DIR_BOTTOMRIGHT: "ptz_move_bottom_right", } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_IP): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_RTSP_PORT): cv.port, - } -) +DEFAULT_TRAVELTIME = 0.125 + +ATTR_MOVEMENT = "movement" +ATTR_TRAVELTIME = "travel_time" + SERVICE_PTZ_SCHEMA = vol.Schema( { @@ -85,83 +78,90 @@ SERVICE_PTZ_SCHEMA = vol.Schema( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up a Foscam IP Camera.""" + LOGGER.warning( + "Loading foscam via platform config is deprecated, it will be automatically imported. Please remove it afterwards." + ) + + config_new = { + CONF_NAME: config[CONF_NAME], + CONF_HOST: config["ip"], + CONF_PORT: config[CONF_PORT], + CONF_USERNAME: config[CONF_USERNAME], + CONF_PASSWORD: config[CONF_PASSWORD], + CONF_STREAM: "Main", + } + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config_new + ) + ) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add a Foscam IP camera from a config entry.""" 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", + SERVICE_PTZ, SERVICE_PTZ_SCHEMA, "async_perform_ptz" ) camera = FoscamCamera( - config[CONF_IP], - config[CONF_PORT], - config[CONF_USERNAME], - config[CONF_PASSWORD], + config_entry.data[CONF_HOST], + config_entry.data[CONF_PORT], + config_entry.data[CONF_USERNAME], + config_entry.data[CONF_PASSWORD], verbose=False, ) - rtsp_port = config.get(CONF_RTSP_PORT) - if not rtsp_port: - ret, response = await hass.async_add_executor_job(camera.get_port_info) - - if ret == 0: - rtsp_port = response.get("rtspPort") or response.get("mediaPort") - - ret, response = await hass.async_add_executor_job(camera.get_motion_detect_config) - - motion_status = False - if ret != 0 and response == 1: - motion_status = True - - async_add_entities( - [ - HassFoscamCamera( - camera, - config[CONF_NAME], - config[CONF_USERNAME], - config[CONF_PASSWORD], - rtsp_port, - motion_status, - ) - ] - ) + async_add_entities([HassFoscamCamera(camera, config_entry)]) class HassFoscamCamera(Camera): """An implementation of a Foscam IP camera.""" - def __init__(self, camera, name, username, password, rtsp_port, motion_status): + def __init__(self, camera, config_entry): """Initialize a Foscam camera.""" super().__init__() self._foscam_session = camera - self._name = name - self._username = username - self._password = password - self._rtsp_port = rtsp_port - self._motion_status = motion_status + self._name = config_entry.title + self._username = config_entry.data[CONF_USERNAME] + self._password = config_entry.data[CONF_PASSWORD] + self._stream = config_entry.data[CONF_STREAM] + self._unique_id = config_entry.unique_id + self._rtsp_port = None + self._motion_status = False async def async_added_to_hass(self): """Handle entity addition to hass.""" - entities = self.hass.data.setdefault(FOSCAM_DATA, {}).setdefault( - FOSCAM_ENTITIES, [] + # Get motion detection status + ret, response = await self.hass.async_add_executor_job( + self._foscam_session.get_motion_detect_config ) - entities.append(self) + + if ret != 0: + LOGGER.error( + "Error getting motion detection status of %s: %s", self._name, ret + ) + + else: + self._motion_status = response == 1 + + # Get RTSP port + ret, response = await self.hass.async_add_executor_job( + self._foscam_session.get_port_info + ) + + if ret != 0: + LOGGER.error("Error getting RTSP port of %s: %s", self._name, ret) + + else: + self._rtsp_port = response.get("rtspPort") or response.get("mediaPort") + + @property + def unique_id(self): + """Return the entity unique ID.""" + return self._unique_id def camera_image(self): """Return a still image response from the camera.""" @@ -178,12 +178,14 @@ class HassFoscamCamera(Camera): """Return supported features.""" if self._rtsp_port: return SUPPORT_STREAM - return 0 + + return None async def stream_source(self): """Return the stream source.""" if self._rtsp_port: - return f"rtsp://{self._username}:{self._password}@{self._foscam_session.host}:{self._rtsp_port}/videoMain" + return f"rtsp://{self._username}:{self._password}@{self._foscam_session.host}:{self._rtsp_port}/video{self._stream}" + return None @property @@ -201,7 +203,10 @@ class HassFoscamCamera(Camera): self._motion_status = True except TypeError: - _LOGGER.debug("Communication problem") + LOGGER.debug( + "Failed enabling motion detection on '%s'. Is it supported by the device?", + self._name, + ) def disable_motion_detection(self): """Disable motion detection.""" @@ -213,18 +218,21 @@ class HassFoscamCamera(Camera): self._motion_status = False except TypeError: - _LOGGER.debug("Communication problem") + LOGGER.debug( + "Failed disabling motion detection on '%s'. Is it supported by the device?", + self._name, + ) async def async_perform_ptz(self, movement, travel_time): """Perform a PTZ action on the camera.""" - _LOGGER.debug("PTZ action '%s' on %s", movement, self._name) + LOGGER.debug("PTZ action '%s' on %s", movement, self._name) movement_function = getattr(self._foscam_session, MOVEMENT_ATTRS[movement]) ret, _ = await self.hass.async_add_executor_job(movement_function) if ret != 0: - _LOGGER.error("Error moving %s '%s': %s", movement, self._name, ret) + LOGGER.error("Error moving %s '%s': %s", movement, self._name, ret) return await asyncio.sleep(travel_time) @@ -234,7 +242,7 @@ class HassFoscamCamera(Camera): ) if ret != 0: - _LOGGER.error("Error stopping movement on '%s': %s", self._name, ret) + LOGGER.error("Error stopping movement on '%s': %s", self._name, ret) return @property diff --git a/homeassistant/components/foscam/config_flow.py b/homeassistant/components/foscam/config_flow.py new file mode 100644 index 00000000000..7bb8cb50a51 --- /dev/null +++ b/homeassistant/components/foscam/config_flow.py @@ -0,0 +1,123 @@ +"""Config flow for foscam integration.""" +from libpyfoscam import FoscamCamera +from libpyfoscam.foscam import ERROR_FOSCAM_AUTH, ERROR_FOSCAM_UNAVAILABLE +import voluptuous as vol + +from homeassistant import config_entries, exceptions +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, +) +from homeassistant.data_entry_flow import AbortFlow + +from .const import CONF_STREAM, LOGGER +from .const import DOMAIN # pylint:disable=unused-import + +STREAMS = ["Main", "Sub"] + +DEFAULT_PORT = 88 + + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_STREAM, default=STREAMS[0]): vol.In(STREAMS), + } +) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for foscam.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + async def _validate_and_create(self, data): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + camera = FoscamCamera( + data[CONF_HOST], + data[CONF_PORT], + data[CONF_USERNAME], + data[CONF_PASSWORD], + verbose=False, + ) + + # Validate data by sending a request to the camera + ret, response = await self.hass.async_add_executor_job(camera.get_dev_info) + + if ret == ERROR_FOSCAM_UNAVAILABLE: + raise CannotConnect + + if ret == ERROR_FOSCAM_AUTH: + raise InvalidAuth + + await self.async_set_unique_id(response["mac"]) + self._abort_if_unique_id_configured() + + name = data.pop(CONF_NAME, response["devName"]) + + return self.async_create_entry(title=name, data=data) + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + + if user_input is not None: + try: + return await self._validate_and_create(user_input) + + except CannotConnect: + errors["base"] = "cannot_connect" + + except InvalidAuth: + 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="user", data_schema=DATA_SCHEMA, errors=errors + ) + + async def async_step_import(self, import_config): + """Handle config import from yaml.""" + try: + return await self._validate_and_create(import_config) + + except CannotConnect: + LOGGER.error("Error importing foscam platform config: cannot connect.") + return self.async_abort(reason="cannot_connect") + + except InvalidAuth: + LOGGER.error("Error importing foscam platform config: invalid auth.") + return self.async_abort(reason="invalid_auth") + + except AbortFlow: + raise + + except Exception: # pylint: disable=broad-except + LOGGER.exception( + "Error importing foscam platform config: unexpected exception." + ) + return self.async_abort(reason="unknown") + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/foscam/const.py b/homeassistant/components/foscam/const.py index 63b4b74a763..c0cb8c25e9f 100644 --- a/homeassistant/components/foscam/const.py +++ b/homeassistant/components/foscam/const.py @@ -1,5 +1,10 @@ """Constants for Foscam component.""" +import logging + +LOGGER = logging.getLogger(__package__) DOMAIN = "foscam" -DATA = "foscam" -ENTITIES = "entities" + +CONF_STREAM = "stream" + +SERVICE_PTZ = "ptz" diff --git a/homeassistant/components/foscam/manifest.json b/homeassistant/components/foscam/manifest.json index 8c7e8e7d77a..fdd050d5133 100644 --- a/homeassistant/components/foscam/manifest.json +++ b/homeassistant/components/foscam/manifest.json @@ -1,6 +1,7 @@ { "domain": "foscam", "name": "Foscam", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/foscam", "requirements": ["libpyfoscam==1.0"], "codeowners": ["@skgsergio"] diff --git a/homeassistant/components/foscam/strings.json b/homeassistant/components/foscam/strings.json new file mode 100644 index 00000000000..6033fa099cd --- /dev/null +++ b/homeassistant/components/foscam/strings.json @@ -0,0 +1,24 @@ +{ + "title": "Foscam", + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "stream": "Stream" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/foscam/translations/en.json b/homeassistant/components/foscam/translations/en.json new file mode 100644 index 00000000000..521a22076dd --- /dev/null +++ b/homeassistant/components/foscam/translations/en.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "port": "Port", + "stream": "Stream", + "username": "Username" + } + } + } + }, + "title": "Foscam" +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 3218ab4baa5..a1941d08f1f 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -66,6 +66,7 @@ FLOWS = [ "flume", "flunearyou", "forked_daapd", + "foscam", "freebox", "fritzbox", "garmin_connect", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5d0e316d48f..792b0342b21 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -443,6 +443,9 @@ konnected==1.2.0 # homeassistant.components.dyson libpurecool==0.6.4 +# homeassistant.components.foscam +libpyfoscam==1.0 + # homeassistant.components.mikrotik librouteros==3.0.0 diff --git a/tests/components/foscam/__init__.py b/tests/components/foscam/__init__.py new file mode 100644 index 00000000000..391907b8a8c --- /dev/null +++ b/tests/components/foscam/__init__.py @@ -0,0 +1 @@ +"""Tests for the Foscam integration.""" diff --git a/tests/components/foscam/test_config_flow.py b/tests/components/foscam/test_config_flow.py new file mode 100644 index 00000000000..8087ac1894f --- /dev/null +++ b/tests/components/foscam/test_config_flow.py @@ -0,0 +1,358 @@ +"""Test the Foscam config flow.""" +from unittest.mock import patch + +from libpyfoscam.foscam import ERROR_FOSCAM_AUTH, ERROR_FOSCAM_UNAVAILABLE + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.foscam import config_flow + +from tests.common import MockConfigEntry + +VALID_CONFIG = { + config_flow.CONF_HOST: "10.0.0.2", + config_flow.CONF_PORT: 88, + config_flow.CONF_USERNAME: "admin", + config_flow.CONF_PASSWORD: "1234", + config_flow.CONF_STREAM: "Main", +} +CAMERA_NAME = "Mocked Foscam Camera" +CAMERA_MAC = "C0:C1:D0:F4:B4:D4" + + +def setup_mock_foscam_camera(mock_foscam_camera): + """Mock FoscamCamera simulating behaviour using a base valid config.""" + + def configure_mock_on_init(host, port, user, passwd, verbose=False): + return_code = 0 + data = {} + + if ( + host != VALID_CONFIG[config_flow.CONF_HOST] + or port != VALID_CONFIG[config_flow.CONF_PORT] + ): + return_code = ERROR_FOSCAM_UNAVAILABLE + + elif ( + user != VALID_CONFIG[config_flow.CONF_USERNAME] + or passwd != VALID_CONFIG[config_flow.CONF_PASSWORD] + ): + return_code = ERROR_FOSCAM_AUTH + + else: + data["devName"] = CAMERA_NAME + data["mac"] = CAMERA_MAC + + mock_foscam_camera.get_dev_info.return_value = (return_code, data) + + return mock_foscam_camera + + mock_foscam_camera.side_effect = configure_mock_on_init + + +async def test_user_valid(hass): + """Test valid config from user input.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.foscam.config_flow.FoscamCamera", + ) as mock_foscam_camera, patch( + "homeassistant.components.foscam.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.foscam.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + setup_mock_foscam_camera(mock_foscam_camera) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + VALID_CONFIG, + ) + + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == CAMERA_NAME + assert result["data"] == VALID_CONFIG + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_invalid_auth(hass): + """Test we handle invalid auth from user input.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.foscam.config_flow.FoscamCamera", + ) as mock_foscam_camera: + setup_mock_foscam_camera(mock_foscam_camera) + + invalid_user = VALID_CONFIG.copy() + invalid_user[config_flow.CONF_USERNAME] = "invalid" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + invalid_user, + ) + + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_user_cannot_connect(hass): + """Test we handle cannot connect error from user input.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.foscam.config_flow.FoscamCamera", + ) as mock_foscam_camera: + setup_mock_foscam_camera(mock_foscam_camera) + + invalid_host = VALID_CONFIG.copy() + invalid_host[config_flow.CONF_HOST] = "127.0.0.1" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + invalid_host, + ) + + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_user_already_configured(hass): + """Test we handle already configured from user input.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry = MockConfigEntry( + domain=config_flow.DOMAIN, data=VALID_CONFIG, unique_id=CAMERA_MAC + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.foscam.config_flow.FoscamCamera", + ) as mock_foscam_camera: + setup_mock_foscam_camera(mock_foscam_camera) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + VALID_CONFIG, + ) + + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_user_unknown_exception(hass): + """Test we handle unknown exceptions from user input.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.foscam.config_flow.FoscamCamera", + ) as mock_foscam_camera: + mock_foscam_camera.side_effect = Exception("test") + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + VALID_CONFIG, + ) + + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "unknown"} + + +async def test_import_user_valid(hass): + """Test valid config from import.""" + with patch( + "homeassistant.components.foscam.config_flow.FoscamCamera", + ) as mock_foscam_camera, patch( + "homeassistant.components.foscam.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.foscam.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + setup_mock_foscam_camera(mock_foscam_camera) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=VALID_CONFIG, + ) + + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == CAMERA_NAME + assert result["data"] == VALID_CONFIG + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_user_valid_with_name(hass): + """Test valid config with extra name from import.""" + with patch( + "homeassistant.components.foscam.config_flow.FoscamCamera", + ) as mock_foscam_camera, patch( + "homeassistant.components.foscam.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.foscam.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + setup_mock_foscam_camera(mock_foscam_camera) + + name = CAMERA_NAME + " 1234" + with_name = VALID_CONFIG.copy() + with_name[config_flow.CONF_NAME] = name + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=with_name, + ) + + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == name + assert result["data"] == VALID_CONFIG + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_invalid_auth(hass): + """Test we handle invalid auth from import.""" + entry = MockConfigEntry( + domain=config_flow.DOMAIN, data=VALID_CONFIG, unique_id=CAMERA_MAC + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.foscam.config_flow.FoscamCamera", + ) as mock_foscam_camera: + setup_mock_foscam_camera(mock_foscam_camera) + + invalid_user = VALID_CONFIG.copy() + invalid_user[config_flow.CONF_USERNAME] = "invalid" + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=invalid_user, + ) + + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "invalid_auth" + + +async def test_import_cannot_connect(hass): + """Test we handle invalid auth from import.""" + entry = MockConfigEntry( + domain=config_flow.DOMAIN, data=VALID_CONFIG, unique_id=CAMERA_MAC + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.foscam.config_flow.FoscamCamera", + ) as mock_foscam_camera: + setup_mock_foscam_camera(mock_foscam_camera) + + invalid_host = VALID_CONFIG.copy() + invalid_host[config_flow.CONF_HOST] = "127.0.0.1" + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=invalid_host, + ) + + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + + +async def test_import_already_configured(hass): + """Test we handle already configured from import.""" + entry = MockConfigEntry( + domain=config_flow.DOMAIN, data=VALID_CONFIG, unique_id=CAMERA_MAC + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.foscam.config_flow.FoscamCamera", + ) as mock_foscam_camera: + setup_mock_foscam_camera(mock_foscam_camera) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=VALID_CONFIG, + ) + + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_import_unknown_exception(hass): + """Test we handle unknown exceptions from import.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch( + "homeassistant.components.foscam.config_flow.FoscamCamera", + ) as mock_foscam_camera: + mock_foscam_camera.side_effect = Exception("test") + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=VALID_CONFIG, + ) + + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "unknown" From de8f273bd09332dcda1206d925924570f48691f4 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Wed, 13 Jan 2021 10:20:59 -0500 Subject: [PATCH 195/507] Allow input_number entity_id as for numeric_state trigger thresholds (#45091) * Allow input_number as limits for numeric_state trigger * Rename threshold schema --- .../homeassistant/triggers/numeric_state.py | 7 +- homeassistant/helpers/config_validation.py | 12 +- .../triggers/test_numeric_state.py | 436 +++++++++++++----- 3 files changed, 344 insertions(+), 111 deletions(-) diff --git a/homeassistant/components/homeassistant/triggers/numeric_state.py b/homeassistant/components/homeassistant/triggers/numeric_state.py index 25b9a4417dc..7cfee8fad93 100644 --- a/homeassistant/components/homeassistant/triggers/numeric_state.py +++ b/homeassistant/components/homeassistant/triggers/numeric_state.py @@ -32,6 +32,9 @@ def validate_above_below(value): if above is None or below is None: return value + if isinstance(above, str) or isinstance(below, str): + return value + if above > below: raise vol.Invalid( f"A value can never be above {above} and below {below} at the same time. You probably want two different triggers.", @@ -45,8 +48,8 @@ TRIGGER_SCHEMA = vol.All( { vol.Required(CONF_PLATFORM): "numeric_state", vol.Required(CONF_ENTITY_ID): cv.entity_ids, - vol.Optional(CONF_BELOW): vol.Coerce(float), - vol.Optional(CONF_ABOVE): vol.Coerce(float), + vol.Optional(CONF_BELOW): cv.NUMERIC_STATE_THRESHOLD_SCHEMA, + vol.Optional(CONF_ABOVE): cv.NUMERIC_STATE_THRESHOLD_SCHEMA, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_FOR): cv.positive_time_period_template, vol.Optional(CONF_ATTRIBUTE): cv.match_all, diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index a4499209640..28f18cf9407 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -916,18 +916,18 @@ SERVICE_SCHEMA = vol.All( has_at_least_one_key(CONF_SERVICE, CONF_SERVICE_TEMPLATE), ) +NUMERIC_STATE_THRESHOLD_SCHEMA = vol.Any( + vol.Coerce(float), vol.All(str, entity_domain("input_number")) +) + NUMERIC_STATE_CONDITION_SCHEMA = vol.All( vol.Schema( { vol.Required(CONF_CONDITION): "numeric_state", vol.Required(CONF_ENTITY_ID): entity_ids, vol.Optional(CONF_ATTRIBUTE): str, - CONF_BELOW: vol.Any( - vol.Coerce(float), vol.All(str, entity_domain("input_number")) - ), - CONF_ABOVE: vol.Any( - vol.Coerce(float), vol.All(str, entity_domain("input_number")) - ), + CONF_BELOW: NUMERIC_STATE_THRESHOLD_SCHEMA, + CONF_ABOVE: NUMERIC_STATE_THRESHOLD_SCHEMA, vol.Optional(CONF_VALUE_TEMPLATE): template, } ), diff --git a/tests/components/homeassistant/triggers/test_numeric_state.py b/tests/components/homeassistant/triggers/test_numeric_state.py index d63cb970f80..b9696fffe06 100644 --- a/tests/components/homeassistant/triggers/test_numeric_state.py +++ b/tests/components/homeassistant/triggers/test_numeric_state.py @@ -29,12 +29,27 @@ def calls(hass): @pytest.fixture(autouse=True) -def setup_comp(hass): +async def setup_comp(hass): """Initialize components.""" mock_component(hass, "group") + await async_setup_component( + hass, + "input_number", + { + "input_number": { + "value_3": {"min": 0, "max": 255, "initial": 3}, + "value_5": {"min": 0, "max": 255, "initial": 5}, + "value_8": {"min": 0, "max": 255, "initial": 8}, + "value_10": {"min": 0, "max": 255, "initial": 10}, + "value_12": {"min": 0, "max": 255, "initial": 12}, + "value_100": {"min": 0, "max": 255, "initial": 100}, + } + }, + ) -async def test_if_not_fires_on_entity_removal(hass, calls): +@pytest.mark.parametrize("below", (10, "input_number.value_10")) +async def test_if_not_fires_on_entity_removal(hass, calls, below): """Test the firing with removed entity.""" hass.states.async_set("test.entity", 11) @@ -46,7 +61,7 @@ async def test_if_not_fires_on_entity_removal(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "below": 10, + "below": below, }, "action": {"service": "test.automation"}, } @@ -59,7 +74,8 @@ async def test_if_not_fires_on_entity_removal(hass, calls): assert len(calls) == 0 -async def test_if_fires_on_entity_change_below(hass, calls): +@pytest.mark.parametrize("below", (10, "input_number.value_10")) +async def test_if_fires_on_entity_change_below(hass, calls, below): """Test the firing with changed entity.""" hass.states.async_set("test.entity", 11) await hass.async_block_till_done() @@ -73,7 +89,7 @@ async def test_if_fires_on_entity_change_below(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "below": 10, + "below": below, }, "action": {"service": "test.automation"}, } @@ -99,7 +115,8 @@ async def test_if_fires_on_entity_change_below(hass, calls): assert len(calls) == 1 -async def test_if_fires_on_entity_change_over_to_below(hass, calls): +@pytest.mark.parametrize("below", (10, "input_number.value_10")) +async def test_if_fires_on_entity_change_over_to_below(hass, calls, below): """Test the firing with changed entity.""" hass.states.async_set("test.entity", 11) await hass.async_block_till_done() @@ -112,7 +129,7 @@ async def test_if_fires_on_entity_change_over_to_below(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "below": 10, + "below": below, }, "action": {"service": "test.automation"}, } @@ -125,7 +142,8 @@ async def test_if_fires_on_entity_change_over_to_below(hass, calls): assert len(calls) == 1 -async def test_if_fires_on_entities_change_over_to_below(hass, calls): +@pytest.mark.parametrize("below", (10, "input_number.value_10")) +async def test_if_fires_on_entities_change_over_to_below(hass, calls, below): """Test the firing with changed entities.""" hass.states.async_set("test.entity_1", 11) hass.states.async_set("test.entity_2", 11) @@ -139,7 +157,7 @@ async def test_if_fires_on_entities_change_over_to_below(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": ["test.entity_1", "test.entity_2"], - "below": 10, + "below": below, }, "action": {"service": "test.automation"}, } @@ -155,7 +173,8 @@ async def test_if_fires_on_entities_change_over_to_below(hass, calls): assert len(calls) == 2 -async def test_if_not_fires_on_entity_change_below_to_below(hass, calls): +@pytest.mark.parametrize("below", (10, "input_number.value_10")) +async def test_if_not_fires_on_entity_change_below_to_below(hass, calls, below): """Test the firing with changed entity.""" context = Context() hass.states.async_set("test.entity", 11) @@ -169,7 +188,7 @@ async def test_if_not_fires_on_entity_change_below_to_below(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "below": 10, + "below": below, }, "action": {"service": "test.automation"}, } @@ -193,7 +212,8 @@ async def test_if_not_fires_on_entity_change_below_to_below(hass, calls): assert len(calls) == 1 -async def test_if_not_below_fires_on_entity_change_to_equal(hass, calls): +@pytest.mark.parametrize("below", (10, "input_number.value_10")) +async def test_if_not_below_fires_on_entity_change_to_equal(hass, calls, below): """Test the firing with changed entity.""" hass.states.async_set("test.entity", 11) await hass.async_block_till_done() @@ -206,7 +226,7 @@ async def test_if_not_below_fires_on_entity_change_to_equal(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "below": 10, + "below": below, }, "action": {"service": "test.automation"}, } @@ -219,7 +239,8 @@ async def test_if_not_below_fires_on_entity_change_to_equal(hass, calls): assert len(calls) == 0 -async def test_if_fires_on_initial_entity_below(hass, calls): +@pytest.mark.parametrize("below", (10, "input_number.value_10")) +async def test_if_fires_on_initial_entity_below(hass, calls, below): """Test the firing when starting with a match.""" hass.states.async_set("test.entity", 9) await hass.async_block_till_done() @@ -232,7 +253,7 @@ async def test_if_fires_on_initial_entity_below(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "below": 10, + "below": below, }, "action": {"service": "test.automation"}, } @@ -245,7 +266,8 @@ async def test_if_fires_on_initial_entity_below(hass, calls): assert len(calls) == 1 -async def test_if_fires_on_initial_entity_above(hass, calls): +@pytest.mark.parametrize("above", (10, "input_number.value_10")) +async def test_if_fires_on_initial_entity_above(hass, calls, above): """Test the firing when starting with a match.""" hass.states.async_set("test.entity", 11) await hass.async_block_till_done() @@ -258,7 +280,7 @@ async def test_if_fires_on_initial_entity_above(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "above": 10, + "above": above, }, "action": {"service": "test.automation"}, } @@ -271,7 +293,8 @@ async def test_if_fires_on_initial_entity_above(hass, calls): assert len(calls) == 1 -async def test_if_fires_on_entity_change_above(hass, calls): +@pytest.mark.parametrize("above", (10, "input_number.value_10")) +async def test_if_fires_on_entity_change_above(hass, calls, above): """Test the firing with changed entity.""" hass.states.async_set("test.entity", 9) await hass.async_block_till_done() @@ -284,7 +307,7 @@ async def test_if_fires_on_entity_change_above(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "above": 10, + "above": above, }, "action": {"service": "test.automation"}, } @@ -296,7 +319,8 @@ async def test_if_fires_on_entity_change_above(hass, calls): assert len(calls) == 1 -async def test_if_fires_on_entity_change_below_to_above(hass, calls): +@pytest.mark.parametrize("above", (10, "input_number.value_10")) +async def test_if_fires_on_entity_change_below_to_above(hass, calls, above): """Test the firing with changed entity.""" # set initial state hass.states.async_set("test.entity", 9) @@ -310,7 +334,7 @@ async def test_if_fires_on_entity_change_below_to_above(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "above": 10, + "above": above, }, "action": {"service": "test.automation"}, } @@ -323,7 +347,8 @@ async def test_if_fires_on_entity_change_below_to_above(hass, calls): assert len(calls) == 1 -async def test_if_not_fires_on_entity_change_above_to_above(hass, calls): +@pytest.mark.parametrize("above", (10, "input_number.value_10")) +async def test_if_not_fires_on_entity_change_above_to_above(hass, calls, above): """Test the firing with changed entity.""" # set initial state hass.states.async_set("test.entity", 9) @@ -337,7 +362,7 @@ async def test_if_not_fires_on_entity_change_above_to_above(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "above": 10, + "above": above, }, "action": {"service": "test.automation"}, } @@ -355,7 +380,8 @@ async def test_if_not_fires_on_entity_change_above_to_above(hass, calls): assert len(calls) == 1 -async def test_if_not_above_fires_on_entity_change_to_equal(hass, calls): +@pytest.mark.parametrize("above", (10, "input_number.value_10")) +async def test_if_not_above_fires_on_entity_change_to_equal(hass, calls, above): """Test the firing with changed entity.""" # set initial state hass.states.async_set("test.entity", 9) @@ -369,7 +395,7 @@ async def test_if_not_above_fires_on_entity_change_to_equal(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "above": 10, + "above": above, }, "action": {"service": "test.automation"}, } @@ -382,7 +408,16 @@ async def test_if_not_above_fires_on_entity_change_to_equal(hass, calls): assert len(calls) == 0 -async def test_if_fires_on_entity_change_below_range(hass, calls): +@pytest.mark.parametrize( + "above, below", + ( + (5, 10), + (5, "input_number.value_10"), + ("input_number.value_5", 10), + ("input_number.value_5", "input_number.value_10"), + ), +) +async def test_if_fires_on_entity_change_below_range(hass, calls, above, below): """Test the firing with changed entity.""" hass.states.async_set("test.entity", 11) await hass.async_block_till_done() @@ -395,8 +430,8 @@ async def test_if_fires_on_entity_change_below_range(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "below": 10, - "above": 5, + "below": below, + "above": above, }, "action": {"service": "test.automation"}, } @@ -408,7 +443,16 @@ async def test_if_fires_on_entity_change_below_range(hass, calls): assert len(calls) == 1 -async def test_if_fires_on_entity_change_below_above_range(hass, calls): +@pytest.mark.parametrize( + "above, below", + ( + (5, 10), + (5, "input_number.value_10"), + ("input_number.value_5", 10), + ("input_number.value_5", "input_number.value_10"), + ), +) +async def test_if_fires_on_entity_change_below_above_range(hass, calls, above, below): """Test the firing with changed entity.""" assert await async_setup_component( hass, @@ -418,8 +462,8 @@ async def test_if_fires_on_entity_change_below_above_range(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "below": 10, - "above": 5, + "below": below, + "above": above, }, "action": {"service": "test.automation"}, } @@ -431,7 +475,16 @@ async def test_if_fires_on_entity_change_below_above_range(hass, calls): assert len(calls) == 0 -async def test_if_fires_on_entity_change_over_to_below_range(hass, calls): +@pytest.mark.parametrize( + "above, below", + ( + (5, 10), + (5, "input_number.value_10"), + ("input_number.value_5", 10), + ("input_number.value_5", "input_number.value_10"), + ), +) +async def test_if_fires_on_entity_change_over_to_below_range(hass, calls, above, below): """Test the firing with changed entity.""" hass.states.async_set("test.entity", 11) await hass.async_block_till_done() @@ -444,8 +497,8 @@ async def test_if_fires_on_entity_change_over_to_below_range(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "below": 10, - "above": 5, + "below": below, + "above": above, }, "action": {"service": "test.automation"}, } @@ -458,7 +511,18 @@ async def test_if_fires_on_entity_change_over_to_below_range(hass, calls): assert len(calls) == 1 -async def test_if_fires_on_entity_change_over_to_below_above_range(hass, calls): +@pytest.mark.parametrize( + "above, below", + ( + (5, 10), + (5, "input_number.value_10"), + ("input_number.value_5", 10), + ("input_number.value_5", "input_number.value_10"), + ), +) +async def test_if_fires_on_entity_change_over_to_below_above_range( + hass, calls, above, below +): """Test the firing with changed entity.""" hass.states.async_set("test.entity", 11) await hass.async_block_till_done() @@ -471,8 +535,8 @@ async def test_if_fires_on_entity_change_over_to_below_above_range(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "below": 10, - "above": 5, + "below": above, + "above": below, }, "action": {"service": "test.automation"}, } @@ -485,7 +549,8 @@ async def test_if_fires_on_entity_change_over_to_below_above_range(hass, calls): assert len(calls) == 0 -async def test_if_not_fires_if_entity_not_match(hass, calls): +@pytest.mark.parametrize("below", (100, "input_number.value_100")) +async def test_if_not_fires_if_entity_not_match(hass, calls, below): """Test if not fired with non matching entity.""" assert await async_setup_component( hass, @@ -495,7 +560,7 @@ async def test_if_not_fires_if_entity_not_match(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.another_entity", - "below": 100, + "below": below, }, "action": {"service": "test.automation"}, } @@ -507,7 +572,8 @@ async def test_if_not_fires_if_entity_not_match(hass, calls): assert len(calls) == 0 -async def test_if_fires_on_entity_change_below_with_attribute(hass, calls): +@pytest.mark.parametrize("below", (10, "input_number.value_10")) +async def test_if_fires_on_entity_change_below_with_attribute(hass, calls, below): """Test attributes change.""" hass.states.async_set("test.entity", 11, {"test_attribute": 11}) await hass.async_block_till_done() @@ -520,7 +586,7 @@ async def test_if_fires_on_entity_change_below_with_attribute(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "below": 10, + "below": below, }, "action": {"service": "test.automation"}, } @@ -532,7 +598,10 @@ async def test_if_fires_on_entity_change_below_with_attribute(hass, calls): assert len(calls) == 1 -async def test_if_not_fires_on_entity_change_not_below_with_attribute(hass, calls): +@pytest.mark.parametrize("below", (10, "input_number.value_10")) +async def test_if_not_fires_on_entity_change_not_below_with_attribute( + hass, calls, below +): """Test attributes.""" assert await async_setup_component( hass, @@ -542,7 +611,7 @@ async def test_if_not_fires_on_entity_change_not_below_with_attribute(hass, call "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "below": 10, + "below": below, }, "action": {"service": "test.automation"}, } @@ -554,7 +623,8 @@ async def test_if_not_fires_on_entity_change_not_below_with_attribute(hass, call assert len(calls) == 0 -async def test_if_fires_on_attribute_change_with_attribute_below(hass, calls): +@pytest.mark.parametrize("below", (10, "input_number.value_10")) +async def test_if_fires_on_attribute_change_with_attribute_below(hass, calls, below): """Test attributes change.""" hass.states.async_set("test.entity", "entity", {"test_attribute": 11}) await hass.async_block_till_done() @@ -568,7 +638,7 @@ async def test_if_fires_on_attribute_change_with_attribute_below(hass, calls): "platform": "numeric_state", "entity_id": "test.entity", "value_template": "{{ state.attributes.test_attribute }}", - "below": 10, + "below": below, }, "action": {"service": "test.automation"}, } @@ -580,7 +650,10 @@ async def test_if_fires_on_attribute_change_with_attribute_below(hass, calls): assert len(calls) == 1 -async def test_if_not_fires_on_attribute_change_with_attribute_not_below(hass, calls): +@pytest.mark.parametrize("below", (10, "input_number.value_10")) +async def test_if_not_fires_on_attribute_change_with_attribute_not_below( + hass, calls, below +): """Test attributes change.""" assert await async_setup_component( hass, @@ -591,7 +664,7 @@ async def test_if_not_fires_on_attribute_change_with_attribute_not_below(hass, c "platform": "numeric_state", "entity_id": "test.entity", "value_template": "{{ state.attributes.test_attribute }}", - "below": 10, + "below": below, }, "action": {"service": "test.automation"}, } @@ -603,7 +676,8 @@ async def test_if_not_fires_on_attribute_change_with_attribute_not_below(hass, c assert len(calls) == 0 -async def test_if_not_fires_on_entity_change_with_attribute_below(hass, calls): +@pytest.mark.parametrize("below", (10, "input_number.value_10")) +async def test_if_not_fires_on_entity_change_with_attribute_below(hass, calls, below): """Test attributes change.""" assert await async_setup_component( hass, @@ -614,7 +688,7 @@ async def test_if_not_fires_on_entity_change_with_attribute_below(hass, calls): "platform": "numeric_state", "entity_id": "test.entity", "value_template": "{{ state.attributes.test_attribute }}", - "below": 10, + "below": below, }, "action": {"service": "test.automation"}, } @@ -626,7 +700,10 @@ async def test_if_not_fires_on_entity_change_with_attribute_below(hass, calls): assert len(calls) == 0 -async def test_if_not_fires_on_entity_change_with_not_attribute_below(hass, calls): +@pytest.mark.parametrize("below", (10, "input_number.value_10")) +async def test_if_not_fires_on_entity_change_with_not_attribute_below( + hass, calls, below +): """Test attributes change.""" assert await async_setup_component( hass, @@ -637,7 +714,7 @@ async def test_if_not_fires_on_entity_change_with_not_attribute_below(hass, call "platform": "numeric_state", "entity_id": "test.entity", "value_template": "{{ state.attributes.test_attribute }}", - "below": 10, + "below": below, }, "action": {"service": "test.automation"}, } @@ -649,7 +726,10 @@ async def test_if_not_fires_on_entity_change_with_not_attribute_below(hass, call assert len(calls) == 0 -async def test_fires_on_attr_change_with_attribute_below_and_multiple_attr(hass, calls): +@pytest.mark.parametrize("below", (10, "input_number.value_10")) +async def test_fires_on_attr_change_with_attribute_below_and_multiple_attr( + hass, calls, below +): """Test attributes change.""" hass.states.async_set( "test.entity", "entity", {"test_attribute": 11, "not_test_attribute": 11} @@ -664,7 +744,7 @@ async def test_fires_on_attr_change_with_attribute_below_and_multiple_attr(hass, "platform": "numeric_state", "entity_id": "test.entity", "value_template": "{{ state.attributes.test_attribute }}", - "below": 10, + "below": below, }, "action": {"service": "test.automation"}, } @@ -678,7 +758,8 @@ async def test_fires_on_attr_change_with_attribute_below_and_multiple_attr(hass, assert len(calls) == 1 -async def test_template_list(hass, calls): +@pytest.mark.parametrize("below", (10, "input_number.value_10")) +async def test_template_list(hass, calls, below): """Test template list.""" hass.states.async_set("test.entity", "entity", {"test_attribute": [11, 15, 11]}) await hass.async_block_till_done() @@ -691,7 +772,7 @@ async def test_template_list(hass, calls): "platform": "numeric_state", "entity_id": "test.entity", "value_template": "{{ state.attributes.test_attribute[2] }}", - "below": 10, + "below": below, }, "action": {"service": "test.automation"}, } @@ -703,7 +784,8 @@ async def test_template_list(hass, calls): assert len(calls) == 1 -async def test_template_string(hass, calls): +@pytest.mark.parametrize("below", (10.0, "input_number.value_10")) +async def test_template_string(hass, calls, below): """Test template string.""" assert await async_setup_component( hass, @@ -714,7 +796,7 @@ async def test_template_string(hass, calls): "platform": "numeric_state", "entity_id": "test.entity", "value_template": "{{ state.attributes.test_attribute | multiply(10) }}", - "below": 10, + "below": below, }, "action": { "service": "test.automation", @@ -742,7 +824,7 @@ async def test_template_string(hass, calls): assert len(calls) == 1 assert ( calls[0].data["some"] - == "numeric_state - test.entity - 10.0 - None - test state 1 - test state 2" + == f"numeric_state - test.entity - {below} - None - test state 1 - test state 2" ) @@ -771,7 +853,16 @@ async def test_not_fires_on_attr_change_with_attr_not_below_multiple_attr(hass, assert len(calls) == 0 -async def test_if_action(hass, calls): +@pytest.mark.parametrize( + "above, below", + ( + (8, 12), + (8, "input_number.value_12"), + ("input_number.value_8", 12), + ("input_number.value_8", "input_number.value_12"), + ), +) +async def test_if_action(hass, calls, above, below): """Test if action.""" entity_id = "domain.test_entity" assert await async_setup_component( @@ -783,8 +874,8 @@ async def test_if_action(hass, calls): "condition": { "condition": "numeric_state", "entity_id": entity_id, - "above": 8, - "below": 12, + "above": above, + "below": below, }, "action": {"service": "test.automation"}, } @@ -810,7 +901,16 @@ async def test_if_action(hass, calls): assert len(calls) == 2 -async def test_if_fails_setup_bad_for(hass, calls): +@pytest.mark.parametrize( + "above, below", + ( + (8, 12), + (8, "input_number.value_12"), + ("input_number.value_8", 12), + ("input_number.value_8", "input_number.value_12"), + ), +) +async def test_if_fails_setup_bad_for(hass, calls, above, below): """Test for setup failure for bad for.""" hass.states.async_set("test.entity", 5) await hass.async_block_till_done() @@ -823,8 +923,8 @@ async def test_if_fails_setup_bad_for(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "above": 8, - "below": 12, + "above": above, + "below": below, "for": {"invalid": 5}, }, "action": {"service": "homeassistant.turn_on"}, @@ -857,7 +957,16 @@ async def test_if_fails_setup_for_without_above_below(hass, calls): ) -async def test_if_not_fires_on_entity_change_with_for(hass, calls): +@pytest.mark.parametrize( + "above, below", + ( + (8, 12), + (8, "input_number.value_12"), + ("input_number.value_8", 12), + ("input_number.value_8", "input_number.value_12"), + ), +) +async def test_if_not_fires_on_entity_change_with_for(hass, calls, above, below): """Test for not firing on entity change with for.""" assert await async_setup_component( hass, @@ -867,8 +976,8 @@ async def test_if_not_fires_on_entity_change_with_for(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "above": 8, - "below": 12, + "above": above, + "below": below, "for": {"seconds": 5}, }, "action": {"service": "test.automation"}, @@ -885,7 +994,18 @@ async def test_if_not_fires_on_entity_change_with_for(hass, calls): assert len(calls) == 0 -async def test_if_not_fires_on_entities_change_with_for_after_stop(hass, calls): +@pytest.mark.parametrize( + "above, below", + ( + (8, 12), + (8, "input_number.value_12"), + ("input_number.value_8", 12), + ("input_number.value_8", "input_number.value_12"), + ), +) +async def test_if_not_fires_on_entities_change_with_for_after_stop( + hass, calls, above, below +): """Test for not firing on entities change with for after stop.""" hass.states.async_set("test.entity_1", 0) hass.states.async_set("test.entity_2", 0) @@ -899,8 +1019,8 @@ async def test_if_not_fires_on_entities_change_with_for_after_stop(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": ["test.entity_1", "test.entity_2"], - "above": 8, - "below": 12, + "above": above, + "below": below, "for": {"seconds": 5}, }, "action": {"service": "test.automation"}, @@ -932,7 +1052,18 @@ async def test_if_not_fires_on_entities_change_with_for_after_stop(hass, calls): assert len(calls) == 1 -async def test_if_fires_on_entity_change_with_for_attribute_change(hass, calls): +@pytest.mark.parametrize( + "above, below", + ( + (8, 12), + (8, "input_number.value_12"), + ("input_number.value_8", 12), + ("input_number.value_8", "input_number.value_12"), + ), +) +async def test_if_fires_on_entity_change_with_for_attribute_change( + hass, calls, above, below +): """Test for firing on entity change with for and attribute change.""" hass.states.async_set("test.entity", 0) await hass.async_block_till_done() @@ -945,8 +1076,8 @@ async def test_if_fires_on_entity_change_with_for_attribute_change(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "above": 8, - "below": 12, + "above": above, + "below": below, "for": {"seconds": 5}, }, "action": {"service": "test.automation"}, @@ -970,7 +1101,16 @@ async def test_if_fires_on_entity_change_with_for_attribute_change(hass, calls): assert len(calls) == 1 -async def test_if_fires_on_entity_change_with_for(hass, calls): +@pytest.mark.parametrize( + "above, below", + ( + (8, 12), + (8, "input_number.value_12"), + ("input_number.value_8", 12), + ("input_number.value_8", "input_number.value_12"), + ), +) +async def test_if_fires_on_entity_change_with_for(hass, calls, above, below): """Test for firing on entity change with for.""" hass.states.async_set("test.entity", 0) await hass.async_block_till_done() @@ -983,8 +1123,8 @@ async def test_if_fires_on_entity_change_with_for(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "above": 8, - "below": 12, + "above": above, + "below": below, "for": {"seconds": 5}, }, "action": {"service": "test.automation"}, @@ -999,7 +1139,8 @@ async def test_if_fires_on_entity_change_with_for(hass, calls): assert len(calls) == 1 -async def test_wait_template_with_trigger(hass, calls): +@pytest.mark.parametrize("above", (10, "input_number.value_10")) +async def test_wait_template_with_trigger(hass, calls, above): """Test using wait template with 'trigger.entity_id'.""" hass.states.async_set("test.entity", "0") await hass.async_block_till_done() @@ -1012,7 +1153,7 @@ async def test_wait_template_with_trigger(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "above": 10, + "above": above, }, "action": [ {"wait_template": "{{ states(trigger.entity_id) | int < 10 }}"}, @@ -1039,7 +1180,16 @@ async def test_wait_template_with_trigger(hass, calls): assert "numeric_state - test.entity - 12" == calls[0].data["some"] -async def test_if_fires_on_entities_change_no_overlap(hass, calls): +@pytest.mark.parametrize( + "above, below", + ( + (8, 12), + (8, "input_number.value_12"), + ("input_number.value_8", 12), + ("input_number.value_8", "input_number.value_12"), + ), +) +async def test_if_fires_on_entities_change_no_overlap(hass, calls, above, below): """Test for firing on entities change with no overlap.""" hass.states.async_set("test.entity_1", 0) hass.states.async_set("test.entity_2", 0) @@ -1053,8 +1203,8 @@ async def test_if_fires_on_entities_change_no_overlap(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": ["test.entity_1", "test.entity_2"], - "above": 8, - "below": 12, + "above": above, + "below": below, "for": {"seconds": 5}, }, "action": { @@ -1086,7 +1236,16 @@ async def test_if_fires_on_entities_change_no_overlap(hass, calls): assert calls[1].data["some"] == "test.entity_2" -async def test_if_fires_on_entities_change_overlap(hass, calls): +@pytest.mark.parametrize( + "above, below", + ( + (8, 12), + (8, "input_number.value_12"), + ("input_number.value_8", 12), + ("input_number.value_8", "input_number.value_12"), + ), +) +async def test_if_fires_on_entities_change_overlap(hass, calls, above, below): """Test for firing on entities change with overlap.""" hass.states.async_set("test.entity_1", 0) hass.states.async_set("test.entity_2", 0) @@ -1100,8 +1259,8 @@ async def test_if_fires_on_entities_change_overlap(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": ["test.entity_1", "test.entity_2"], - "above": 8, - "below": 12, + "above": above, + "below": below, "for": {"seconds": 5}, }, "action": { @@ -1144,7 +1303,16 @@ async def test_if_fires_on_entities_change_overlap(hass, calls): assert calls[1].data["some"] == "test.entity_2" -async def test_if_fires_on_change_with_for_template_1(hass, calls): +@pytest.mark.parametrize( + "above, below", + ( + (8, 12), + (8, "input_number.value_12"), + ("input_number.value_8", 12), + ("input_number.value_8", "input_number.value_12"), + ), +) +async def test_if_fires_on_change_with_for_template_1(hass, calls, above, below): """Test for firing on change with for template.""" hass.states.async_set("test.entity", 0) await hass.async_block_till_done() @@ -1157,8 +1325,8 @@ async def test_if_fires_on_change_with_for_template_1(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "above": 8, - "below": 12, + "above": above, + "below": below, "for": {"seconds": "{{ 5 }}"}, }, "action": {"service": "test.automation"}, @@ -1174,7 +1342,16 @@ async def test_if_fires_on_change_with_for_template_1(hass, calls): assert len(calls) == 1 -async def test_if_fires_on_change_with_for_template_2(hass, calls): +@pytest.mark.parametrize( + "above, below", + ( + (8, 12), + (8, "input_number.value_12"), + ("input_number.value_8", 12), + ("input_number.value_8", "input_number.value_12"), + ), +) +async def test_if_fires_on_change_with_for_template_2(hass, calls, above, below): """Test for firing on change with for template.""" hass.states.async_set("test.entity", 0) await hass.async_block_till_done() @@ -1187,8 +1364,8 @@ async def test_if_fires_on_change_with_for_template_2(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "above": 8, - "below": 12, + "above": above, + "below": below, "for": "{{ 5 }}", }, "action": {"service": "test.automation"}, @@ -1204,7 +1381,16 @@ async def test_if_fires_on_change_with_for_template_2(hass, calls): assert len(calls) == 1 -async def test_if_fires_on_change_with_for_template_3(hass, calls): +@pytest.mark.parametrize( + "above, below", + ( + (8, 12), + (8, "input_number.value_12"), + ("input_number.value_8", 12), + ("input_number.value_8", "input_number.value_12"), + ), +) +async def test_if_fires_on_change_with_for_template_3(hass, calls, above, below): """Test for firing on change with for template.""" hass.states.async_set("test.entity", 0) await hass.async_block_till_done() @@ -1217,8 +1403,8 @@ async def test_if_fires_on_change_with_for_template_3(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "above": 8, - "below": 12, + "above": above, + "below": below, "for": "00:00:{{ 5 }}", }, "action": {"service": "test.automation"}, @@ -1234,7 +1420,16 @@ async def test_if_fires_on_change_with_for_template_3(hass, calls): assert len(calls) == 1 -async def test_invalid_for_template(hass, calls): +@pytest.mark.parametrize( + "above, below", + ( + (8, 12), + (8, "input_number.value_12"), + ("input_number.value_8", 12), + ("input_number.value_8", "input_number.value_12"), + ), +) +async def test_invalid_for_template(hass, calls, above, below): """Test for invalid for template.""" hass.states.async_set("test.entity", 0) await hass.async_block_till_done() @@ -1247,8 +1442,8 @@ async def test_invalid_for_template(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "above": 8, - "below": 12, + "above": above, + "below": below, "for": "{{ five }}", }, "action": {"service": "test.automation"}, @@ -1262,7 +1457,18 @@ async def test_invalid_for_template(hass, calls): assert mock_logger.error.called -async def test_if_fires_on_entities_change_overlap_for_template(hass, calls): +@pytest.mark.parametrize( + "above, below", + ( + (8, 12), + (8, "input_number.value_12"), + ("input_number.value_8", 12), + ("input_number.value_8", "input_number.value_12"), + ), +) +async def test_if_fires_on_entities_change_overlap_for_template( + hass, calls, above, below +): """Test for firing on entities change with overlap and for template.""" hass.states.async_set("test.entity_1", 0) hass.states.async_set("test.entity_2", 0) @@ -1276,8 +1482,8 @@ async def test_if_fires_on_entities_change_overlap_for_template(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": ["test.entity_1", "test.entity_2"], - "above": 8, - "below": 12, + "above": above, + "below": below, "for": '{{ 5 if trigger.entity_id == "test.entity_1"' " else 10 }}", }, @@ -1335,7 +1541,30 @@ def test_below_above(): ) -async def test_attribute_if_fires_on_entity_change_with_both_filters(hass, calls): +def test_schema_input_number(): + """Test input_number only is accepted for above/below.""" + with pytest.raises(vol.Invalid): + numeric_state_trigger.TRIGGER_SCHEMA( + { + "platform": "numeric_state", + "above": "input_datetime.some_input", + "below": 1000, + } + ) + with pytest.raises(vol.Invalid): + numeric_state_trigger.TRIGGER_SCHEMA( + { + "platform": "numeric_state", + "below": "input_datetime.some_input", + "above": 1200, + } + ) + + +@pytest.mark.parametrize("above", (3, "input_number.value_3")) +async def test_attribute_if_fires_on_entity_change_with_both_filters( + hass, calls, above +): """Test for firing if both filters are match attribute.""" hass.states.async_set("test.entity", "bla", {"test-measurement": 1}) @@ -1347,7 +1576,7 @@ async def test_attribute_if_fires_on_entity_change_with_both_filters(hass, calls "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "above": 3, + "above": above, "attribute": "test-measurement", }, "action": {"service": "test.automation"}, @@ -1361,8 +1590,9 @@ async def test_attribute_if_fires_on_entity_change_with_both_filters(hass, calls assert len(calls) == 1 +@pytest.mark.parametrize("above", (3, "input_number.value_3")) async def test_attribute_if_not_fires_on_entities_change_with_for_after_stop( - hass, calls + hass, calls, above ): """Test for not firing on entity change with for after stop trigger.""" hass.states.async_set("test.entity", "bla", {"test-measurement": 1}) @@ -1375,7 +1605,7 @@ async def test_attribute_if_not_fires_on_entities_change_with_for_after_stop( "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "above": 3, + "above": above, "attribute": "test-measurement", "for": 5, }, From 79d37fdf124f973abf9fb7ceeca4426e5d64491e Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 13 Jan 2021 16:28:51 +0100 Subject: [PATCH 196/507] Add zwave_js light platform tests (#45107) * Add bulb 6 multi color device state fixture * Add light test foundation * Add no cover comment for todo code * Update hs_color * Test turn on light * Test light turn off * Fix brightness comparison * Test setting same brightness * Test setting same rgb color * Test color temp update * Test setting same color temp * Add entity module to coverage calculation * Fix typing --- homeassistant/components/zwave_js/light.py | 26 +- tests/components/zwave_js/conftest.py | 14 + tests/components/zwave_js/test_light.py | 377 +++++++++++ .../zwave_js/bulb_6_multi_color_state.json | 632 ++++++++++++++++++ 4 files changed, 1037 insertions(+), 12 deletions(-) create mode 100644 tests/components/zwave_js/test_light.py create mode 100644 tests/fixtures/zwave_js/bulb_6_multi_color_state.json diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py index 45bd68aef81..64b5ea4ef02 100644 --- a/homeassistant/components/zwave_js/light.py +++ b/homeassistant/components/zwave_js/light.py @@ -1,6 +1,6 @@ """Support for Z-Wave lights.""" import logging -from typing import Any, Callable, List, Optional +from typing import Any, Callable, Optional, Tuple from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import CommandClass @@ -68,7 +68,7 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): self._supports_color = False self._supports_white_value = False self._supports_color_temp = False - self._hs_color: Optional[List[float]] = None + self._hs_color: Optional[Tuple[float, float]] = None self._white_value: Optional[int] = None self._color_temp: Optional[int] = None self._min_mireds = 153 # 6500K as a safe default @@ -111,7 +111,7 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): return self.brightness > 0 @property - def hs_color(self) -> Optional[List[float]]: + def hs_color(self) -> Optional[Tuple[float, float]]: """Return the hs color.""" return self._hs_color @@ -218,22 +218,23 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): self, brightness: Optional[int], transition: Optional[int] = None ) -> None: """Set new brightness to light.""" - if self.info.primary_value.value == brightness: - # no point in setting same brightness - return if brightness is None and self.info.primary_value.value: # there is no point in setting default brightness when light is already on return if brightness is None: # Level 255 means to set it to previous value. - brightness = 255 + zwave_brightness = 255 else: # Zwave multilevel switches use a range of [0, 99] to control brightness. - brightness = byte_to_zwave_brightness(brightness) + zwave_brightness = byte_to_zwave_brightness(brightness) + + if self.info.primary_value.value == zwave_brightness: + # no point in setting same brightness + return # set transition value before seinding new brightness await self._async_set_transition_duration(transition) # setting a value requires setting targetValue - await self.info.node.async_set_value(self._target_value, brightness) + await self.info.node.async_set_value(self._target_value, zwave_brightness) async def _async_set_transition_duration( self, duration: Optional[int] = None @@ -249,7 +250,7 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): if duration is None: # type: ignore # no transition specified by user, use defaults duration = 7621 # anything over 7620 uses the factory default - else: + else: # pragma: no cover # transition specified by user transition = duration if transition <= 127: @@ -265,7 +266,7 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): # only send value if it differs from current # this prevents sending a command for nothing - if self._dimming_duration.value != duration: + if self._dimming_duration.value != duration: # pragma: no cover await self.info.node.async_set_value(self._dimming_duration, duration) @callback @@ -290,7 +291,7 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): and green_val.value is not None and blue_val.value is not None ): - self._hs = color_util.color_RGB_to_hs( + self._hs_color = color_util.color_RGB_to_hs( red_val.value, green_val.value, blue_val.value ) @@ -320,3 +321,4 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): elif ww_val or cw_val: # only one white channel self._supports_white_value = True + # FIXME: Update self._white_value diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 389245b32a3..ae62abaeb1a 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -37,6 +37,12 @@ def binary_switch_state_fixture(): return json.loads(load_fixture("zwave_js/hank_binary_switch_state.json")) +@pytest.fixture(name="bulb_6_multi_color_state", scope="session") +def bulb_6_multi_color_state_fixture(): + """Load the bulb 6 multi-color node state fixture data.""" + return json.loads(load_fixture("zwave_js/bulb_6_multi_color_state.json")) + + @pytest.fixture(name="client") def mock_client_fixture(controller_state): """Mock a client.""" @@ -64,6 +70,14 @@ def hank_binary_switch_fixture(client, hank_binary_switch_state): return node +@pytest.fixture(name="bulb_6_multi_color") +def bulb_6_multi_color_fixture(client, bulb_6_multi_color_state): + """Mock a bulb 6 multi-color node.""" + node = Node(client, bulb_6_multi_color_state) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="integration") async def integration_fixture(hass, client): """Set up the zwave_js integration.""" diff --git a/tests/components/zwave_js/test_light.py b/tests/components/zwave_js/test_light.py new file mode 100644 index 00000000000..d26ddde9d4d --- /dev/null +++ b/tests/components/zwave_js/test_light.py @@ -0,0 +1,377 @@ +"""Test the Z-Wave JS light platform.""" +from copy import deepcopy + +from zwave_js_server.event import Event + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_MAX_MIREDS, + ATTR_MIN_MIREDS, + ATTR_RGB_COLOR, +) +from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON + +BULB_6_MULTI_COLOR_LIGHT_ENTITY = "light.bulb_6_multi_color_current_value" + + +async def test_light(hass, client, bulb_6_multi_color, integration): + """Test the light entity.""" + node = bulb_6_multi_color + state = hass.states.get(BULB_6_MULTI_COLOR_LIGHT_ENTITY) + + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_MIN_MIREDS] == 153 + assert state.attributes[ATTR_MAX_MIREDS] == 370 + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 51 + + # Test turning on + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": BULB_6_MULTI_COLOR_LIGHT_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_json_message.call_args_list) == 1 + args = client.async_send_json_message.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 39 + assert args["valueId"] == { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "label": "Target value", + "max": 99, + "min": 0, + "type": "number", + "readable": True, + "writeable": True, + "label": "Target value", + }, + } + assert args["value"] == 255 + + client.async_send_json_message.reset_mock() + + # Test brightness update from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 39, + "args": { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "currentValue", + "newValue": 99, + "prevValue": 0, + "propertyName": "currentValue", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(BULB_6_MULTI_COLOR_LIGHT_ENTITY) + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 255 + assert state.attributes[ATTR_COLOR_TEMP] == 370 + + # Test turning on with same brightness + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": BULB_6_MULTI_COLOR_LIGHT_ENTITY, ATTR_BRIGHTNESS: 255}, + blocking=True, + ) + + assert len(client.async_send_json_message.call_args_list) == 0 + + client.async_send_json_message.reset_mock() + + # Test turning on with brightness + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": BULB_6_MULTI_COLOR_LIGHT_ENTITY, ATTR_BRIGHTNESS: 129}, + blocking=True, + ) + + assert len(client.async_send_json_message.call_args_list) == 1 + args = client.async_send_json_message.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 39 + assert args["valueId"] == { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "label": "Target value", + "max": 99, + "min": 0, + "type": "number", + "readable": True, + "writeable": True, + "label": "Target value", + }, + } + assert args["value"] == 50 + + client.async_send_json_message.reset_mock() + + # Test turning on with rgb color + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": BULB_6_MULTI_COLOR_LIGHT_ENTITY, ATTR_RGB_COLOR: (255, 76, 255)}, + blocking=True, + ) + + assert len(client.async_send_json_message.call_args_list) == 4 + warm_args = client.async_send_json_message.call_args_list[0][0][0] # warm white 0 + assert warm_args["command"] == "node.set_value" + assert warm_args["nodeId"] == 39 + assert warm_args["valueId"]["commandClassName"] == "Color Switch" + assert warm_args["valueId"]["commandClass"] == 51 + assert warm_args["valueId"]["endpoint"] == 0 + assert warm_args["valueId"]["metadata"]["label"] == "Target value (Warm White)" + assert warm_args["valueId"]["property"] == "targetColor" + assert warm_args["valueId"]["propertyName"] == "targetColor" + assert warm_args["value"] == 0 + red_args = client.async_send_json_message.call_args_list[1][0][0] # red 255 + assert red_args["command"] == "node.set_value" + assert red_args["nodeId"] == 39 + assert red_args["valueId"]["commandClassName"] == "Color Switch" + assert red_args["valueId"]["commandClass"] == 51 + assert red_args["valueId"]["endpoint"] == 0 + assert red_args["valueId"]["metadata"]["label"] == "Target value (Red)" + assert red_args["valueId"]["property"] == "targetColor" + assert red_args["valueId"]["propertyName"] == "targetColor" + assert red_args["value"] == 255 + green_args = client.async_send_json_message.call_args_list[2][0][0] # green 76 + assert green_args["command"] == "node.set_value" + assert green_args["nodeId"] == 39 + assert green_args["valueId"]["commandClassName"] == "Color Switch" + assert green_args["valueId"]["commandClass"] == 51 + assert green_args["valueId"]["endpoint"] == 0 + assert green_args["valueId"]["metadata"]["label"] == "Target value (Green)" + assert green_args["valueId"]["property"] == "targetColor" + assert green_args["valueId"]["propertyName"] == "targetColor" + assert green_args["value"] == 76 + blue_args = client.async_send_json_message.call_args_list[3][0][0] # blue 255 + assert blue_args["command"] == "node.set_value" + assert blue_args["nodeId"] == 39 + assert blue_args["valueId"]["commandClassName"] == "Color Switch" + assert blue_args["valueId"]["commandClass"] == 51 + assert blue_args["valueId"]["endpoint"] == 0 + assert blue_args["valueId"]["metadata"]["label"] == "Target value (Blue)" + assert blue_args["valueId"]["property"] == "targetColor" + assert blue_args["valueId"]["propertyName"] == "targetColor" + assert blue_args["value"] == 255 + + # Test rgb color update from value updated event + red_event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 39, + "args": { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "currentColor", + "newValue": 255, + "prevValue": 0, + "propertyKeyName": "Red", + }, + }, + ) + green_event = deepcopy(red_event) + green_event.data["args"].update({"newValue": 76, "propertyKeyName": "Green"}) + blue_event = deepcopy(red_event) + blue_event.data["args"]["propertyKeyName"] = "Blue" + warm_white_event = deepcopy(red_event) + warm_white_event.data["args"].update( + {"newValue": 0, "propertyKeyName": "Warm White"} + ) + node.receive_event(warm_white_event) + node.receive_event(red_event) + node.receive_event(green_event) + node.receive_event(blue_event) + + state = hass.states.get(BULB_6_MULTI_COLOR_LIGHT_ENTITY) + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 255 + assert state.attributes[ATTR_COLOR_TEMP] == 370 + assert state.attributes[ATTR_RGB_COLOR] == (255, 76, 255) + + client.async_send_json_message.reset_mock() + + # Test turning on with same rgb color + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": BULB_6_MULTI_COLOR_LIGHT_ENTITY, ATTR_RGB_COLOR: (255, 76, 255)}, + blocking=True, + ) + + assert len(client.async_send_json_message.call_args_list) == 0 + + client.async_send_json_message.reset_mock() + + # Test turning on with color temp + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": BULB_6_MULTI_COLOR_LIGHT_ENTITY, ATTR_COLOR_TEMP: 170}, + blocking=True, + ) + + assert len(client.async_send_json_message.call_args_list) == 5 + red_args = client.async_send_json_message.call_args_list[0][0][0] # red 0 + assert red_args["command"] == "node.set_value" + assert red_args["nodeId"] == 39 + assert red_args["valueId"]["commandClassName"] == "Color Switch" + assert red_args["valueId"]["commandClass"] == 51 + assert red_args["valueId"]["endpoint"] == 0 + assert red_args["valueId"]["metadata"]["label"] == "Target value (Red)" + assert red_args["valueId"]["property"] == "targetColor" + assert red_args["valueId"]["propertyName"] == "targetColor" + assert red_args["value"] == 0 + red_args = client.async_send_json_message.call_args_list[1][0][0] # green 0 + assert red_args["command"] == "node.set_value" + assert red_args["nodeId"] == 39 + assert red_args["valueId"]["commandClassName"] == "Color Switch" + assert red_args["valueId"]["commandClass"] == 51 + assert red_args["valueId"]["endpoint"] == 0 + assert red_args["valueId"]["metadata"]["label"] == "Target value (Green)" + assert red_args["valueId"]["property"] == "targetColor" + assert red_args["valueId"]["propertyName"] == "targetColor" + assert red_args["value"] == 0 + red_args = client.async_send_json_message.call_args_list[2][0][0] # blue 0 + assert red_args["command"] == "node.set_value" + assert red_args["nodeId"] == 39 + assert red_args["valueId"]["commandClassName"] == "Color Switch" + assert red_args["valueId"]["commandClass"] == 51 + assert red_args["valueId"]["endpoint"] == 0 + assert red_args["valueId"]["metadata"]["label"] == "Target value (Blue)" + assert red_args["valueId"]["property"] == "targetColor" + assert red_args["valueId"]["propertyName"] == "targetColor" + assert red_args["value"] == 0 + warm_args = client.async_send_json_message.call_args_list[3][0][0] # warm white 0 + assert warm_args["command"] == "node.set_value" + assert warm_args["nodeId"] == 39 + assert warm_args["valueId"]["commandClassName"] == "Color Switch" + assert warm_args["valueId"]["commandClass"] == 51 + assert warm_args["valueId"]["endpoint"] == 0 + assert warm_args["valueId"]["metadata"]["label"] == "Target value (Warm White)" + assert warm_args["valueId"]["property"] == "targetColor" + assert warm_args["valueId"]["propertyName"] == "targetColor" + assert warm_args["value"] == 20 + red_args = client.async_send_json_message.call_args_list[4][0][0] # cold white + assert red_args["command"] == "node.set_value" + assert red_args["nodeId"] == 39 + assert red_args["valueId"]["commandClassName"] == "Color Switch" + assert red_args["valueId"]["commandClass"] == 51 + assert red_args["valueId"]["endpoint"] == 0 + assert red_args["valueId"]["metadata"]["label"] == "Target value (Cold White)" + assert red_args["valueId"]["property"] == "targetColor" + assert red_args["valueId"]["propertyName"] == "targetColor" + assert red_args["value"] == 235 + + client.async_send_json_message.reset_mock() + + # Test color temp update from value updated event + red_event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 39, + "args": { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "currentColor", + "newValue": 0, + "prevValue": 255, + "propertyKeyName": "Red", + }, + }, + ) + green_event = deepcopy(red_event) + green_event.data["args"].update( + {"newValue": 0, "prevValue": 76, "propertyKeyName": "Green"} + ) + blue_event = deepcopy(red_event) + blue_event.data["args"]["propertyKeyName"] = "Blue" + warm_white_event = deepcopy(red_event) + warm_white_event.data["args"].update( + {"newValue": 20, "propertyKeyName": "Warm White"} + ) + cold_white_event = deepcopy(red_event) + cold_white_event.data["args"].update( + {"newValue": 235, "propertyKeyName": "Cold White"} + ) + node.receive_event(red_event) + node.receive_event(green_event) + node.receive_event(blue_event) + node.receive_event(warm_white_event) + node.receive_event(cold_white_event) + + state = hass.states.get(BULB_6_MULTI_COLOR_LIGHT_ENTITY) + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 255 + assert state.attributes[ATTR_COLOR_TEMP] == 170 + assert state.attributes[ATTR_RGB_COLOR] == (255, 255, 255) + + # Test turning on with same color temp + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": BULB_6_MULTI_COLOR_LIGHT_ENTITY, ATTR_COLOR_TEMP: 170}, + blocking=True, + ) + + assert len(client.async_send_json_message.call_args_list) == 0 + + client.async_send_json_message.reset_mock() + + # Test turning off + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": BULB_6_MULTI_COLOR_LIGHT_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_json_message.call_args_list) == 1 + args = client.async_send_json_message.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 39 + assert args["valueId"] == { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "label": "Target value", + "max": 99, + "min": 0, + "type": "number", + "readable": True, + "writeable": True, + "label": "Target value", + }, + } + assert args["value"] == 0 diff --git a/tests/fixtures/zwave_js/bulb_6_multi_color_state.json b/tests/fixtures/zwave_js/bulb_6_multi_color_state.json new file mode 100644 index 00000000000..b7c422121c9 --- /dev/null +++ b/tests/fixtures/zwave_js/bulb_6_multi_color_state.json @@ -0,0 +1,632 @@ +{ + "nodeId": 39, + "index": 0, + "installerIcon": 1536, + "userIcon": 1536, + "status": 4, + "ready": true, + "deviceClass": { + "basic": "Static Controller", + "generic": "Multilevel Switch", + "specific": "Multilevel Power Switch", + "mandatorySupportedCCs": [ + "Basic", + "Multilevel Switch", + "All Switch" + ], + "mandatoryControlCCs": [] + }, + "isListening": true, + "isFrequentListening": false, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": "unknown", + "version": 4, + "isBeaming": true, + "manufacturerId": 881, + "productId": 2, + "productType": 259, + "firmwareVersion": "2.0", + "zwavePlusVersion": 1, + "nodeType": 0, + "roleType": 5, + "deviceConfig": { + "manufacturerId": 881, + "manufacturer": "Aeotec Ltd.", + "label": "ZWA002", + "description": "Bulb 6 Multi-Color", + "devices": [ + { + "productType": "0x0003", + "productId": "0x0002" + }, + { + "productType": "0x0103", + "productId": "0x0002" + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + } + }, + "label": "ZWA002", + "neighbors": [ + 1, + 32 + ], + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 39, + "index": 0, + "installerIcon": 1536, + "userIcon": 1536 + } + ], + "values": [ + { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 99, + "label": "Target value" + } + }, + { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "duration", + "propertyName": "duration", + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Transition duration" + } + }, + { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "currentValue", + "propertyName": "currentValue", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 99, + "label": "Current value" + }, + "value": 0 + }, + { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "Up", + "propertyName": "Up", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Up)", + "ccSpecific": { + "switchType": 2 + } + } + }, + { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "Down", + "propertyName": "Down", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Down)", + "ccSpecific": { + "switchType": 2 + } + } + }, + { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "duration", + "propertyName": "duration", + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Transition duration" + } + }, + { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "currentColor", + "propertyKey": 0, + "propertyName": "currentColor", + "propertyKeyName": "Warm White", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Current value (Warm White)", + "description": "The current value of the Warm White color." + }, + "value": 255 + }, + { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "currentColor", + "propertyKey": 1, + "propertyName": "currentColor", + "propertyKeyName": "Cold White", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Current value (Cold White)", + "description": "The current value of the Cold White color." + }, + "value": 0 + }, + { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "currentColor", + "propertyKey": 2, + "propertyName": "currentColor", + "propertyKeyName": "Red", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Current value (Red)", + "description": "The current value of the Red color." + }, + "value": 0 + }, + { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "currentColor", + "propertyKey": 3, + "propertyName": "currentColor", + "propertyKeyName": "Green", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Current value (Green)", + "description": "The current value of the Green color." + }, + "value": 0 + }, + { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "currentColor", + "propertyKey": 4, + "propertyName": "currentColor", + "propertyKeyName": "Blue", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Current value (Blue)", + "description": "The current value of the Blue color." + }, + "value": 0 + }, + { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "targetColor", + "propertyKey": 0, + "propertyName": "targetColor", + "propertyKeyName": "Warm White", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 255, + "label": "Target value (Warm White)", + "description": "The target value of the Warm White color." + } + }, + { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "targetColor", + "propertyKey": 1, + "propertyName": "targetColor", + "propertyKeyName": "Cold White", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 255, + "label": "Target value (Cold White)", + "description": "The target value of the Cold White color." + } + }, + { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "targetColor", + "propertyKey": 2, + "propertyName": "targetColor", + "propertyKeyName": "Red", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 255, + "label": "Target value (Red)", + "description": "The target value of the Red color." + } + }, + { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "targetColor", + "propertyKey": 3, + "propertyName": "targetColor", + "propertyKeyName": "Green", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 255, + "label": "Target value (Green)", + "description": "The target value of the Green color." + } + }, + { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "targetColor", + "propertyKey": 4, + "propertyName": "targetColor", + "propertyKeyName": "Blue", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 255, + "label": "Target value (Blue)", + "description": "The target value of the Blue color." + } + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 1, + "propertyName": "Use custom mode for LED animations", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 2, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Disable", + "1": "Blink Colors in order mode", + "2": "Randomized blink color mode" + }, + "label": "Use custom mode for LED animations", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 2, + "propertyName": "Enable/Disable Strobe over Custom Color", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Disable", + "1": "Enable" + }, + "label": "Enable/Disable Strobe over Custom Color", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 3, + "propertyName": "Rate of change to next color in Custom Mode", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 5, + "max": 8640000, + "default": 50, + "format": 0, + "allowManualEntry": true, + "label": "Rate of change to next color in Custom Mode", + "isFromConfig": true + }, + "value": 50 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 16, + "propertyName": "Ramp rate when dimming using Multilevel Switch", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 100, + "default": 20, + "format": 0, + "allowManualEntry": true, + "label": "Ramp rate when dimming using Multilevel Switch", + "isFromConfig": true + }, + "value": 20 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 80, + "propertyName": "Enable notifications", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Nothing", + "1": "Basic CC report" + }, + "label": "Enable notifications", + "description": "Enable notifications to associated devices (Group 1) when the state is changed", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 81, + "propertyName": "Adjust color component of Warm White", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 2700, + "max": 4999, + "default": 2700, + "format": 0, + "allowManualEntry": true, + "label": "Adjust color component of Warm White", + "isFromConfig": true + }, + "value": 2700 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 82, + "propertyName": "Adjust color component of Cold White", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 5000, + "max": 6500, + "default": 6500, + "format": 0, + "allowManualEntry": true, + "label": "Adjust color component of Cold White", + "isFromConfig": true + }, + "value": 6500 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 4, + "propertyName": "Set color that LED Bulb blinks in (Blink Mode)", + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 255, + "default": 1, + "format": 1, + "allowManualEntry": true, + "label": "Set color that LED Bulb blinks in (Blink Mode)", + "isFromConfig": true + } + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "manufacturerId", + "propertyName": "manufacturerId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + }, + "value": 881 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productType", + "propertyName": "productType", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + }, + "value": 259 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productId", + "propertyName": "productId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + }, + "value": 2 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "libraryType", + "propertyName": "libraryType", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Libary type" + }, + "value": 3 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "protocolVersion", + "propertyName": "protocolVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.38" + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": [ + "2.0" + ] + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + } + ] +} From 81c77942ebe5977c542f1a1f383e3776694c451c Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Wed, 13 Jan 2021 15:42:28 +0000 Subject: [PATCH 197/507] Update the Utility Meter sensor status on HA start (#44765) * fix status on HA start * better coverage and fix * fix test * address review --- .../components/utility_meter/sensor.py | 21 +++-- tests/components/utility_meter/test_sensor.py | 89 ++++++++++++++++++- 2 files changed, 101 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 6b25ec7d123..e8d551ba280 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -169,7 +169,11 @@ class UtilityMeterSensor(RestoreEntity): new_state = event.data.get("new_state") if new_state is None: return - if self._tariff == new_state.state: + + self._change_status(new_state.state) + + def _change_status(self, tariff): + if self._tariff == tariff: self._collecting = async_track_state_change_event( self.hass, [self._sensor_source_id], self.async_reading ) @@ -271,25 +275,26 @@ class UtilityMeterSensor(RestoreEntity): self._last_reset = dt_util.parse_datetime( state.attributes.get(ATTR_LAST_RESET) ) - self.async_write_ha_state() - if state.attributes.get(ATTR_STATUS) == PAUSED: - # Fake cancellation function to init the meter paused + if state.attributes.get(ATTR_STATUS) == COLLECTING: + # Fake cancellation function to init the meter in similar state self._collecting = lambda: None @callback def async_source_tracking(event): """Wait for source to be ready, then start meter.""" if self._tariff_entity is not None: - _LOGGER.debug("Track %s", self._tariff_entity) + _LOGGER.debug( + "<%s> tracks utility meter %s", self.name, self._tariff_entity + ) async_track_state_change_event( self.hass, [self._tariff_entity], self.async_tariff_change ) tariff_entity_state = self.hass.states.get(self._tariff_entity) - if self._tariff != tariff_entity_state.state: - return + self._change_status(tariff_entity_state.state) + return - _LOGGER.debug("tracking source: %s", self._sensor_source_id) + _LOGGER.debug("<%s> collecting from %s", self.name, self._sensor_source_id) self._collecting = async_track_state_change_event( self.hass, [self._sensor_source_id], self.async_reading ) diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index bd34238592c..24938b1e818 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -11,16 +11,23 @@ from homeassistant.components.utility_meter.const import ( SERVICE_CALIBRATE_METER, SERVICE_SELECT_TARIFF, ) +from homeassistant.components.utility_meter.sensor import ( + ATTR_LAST_RESET, + ATTR_STATUS, + COLLECTING, + PAUSED, +) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, ENERGY_KILO_WATT_HOUR, EVENT_HOMEASSISTANT_START, ) +from homeassistant.core import State from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, mock_restore_cache @contextmanager @@ -55,6 +62,21 @@ async def test_state(hass): ) await hass.async_block_till_done() + state = hass.states.get("sensor.energy_bill_onpeak") + assert state is not None + assert state.state == "0" + assert state.attributes.get("status") == COLLECTING + + state = hass.states.get("sensor.energy_bill_midpeak") + assert state is not None + assert state.state == "0" + assert state.attributes.get("status") == PAUSED + + state = hass.states.get("sensor.energy_bill_offpeak") + assert state is not None + assert state.state == "0" + assert state.attributes.get("status") == PAUSED + now = dt_util.utcnow() + timedelta(seconds=10) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.states.async_set( @@ -68,14 +90,17 @@ async def test_state(hass): state = hass.states.get("sensor.energy_bill_onpeak") assert state is not None assert state.state == "1" + assert state.attributes.get("status") == COLLECTING state = hass.states.get("sensor.energy_bill_midpeak") assert state is not None assert state.state == "0" + assert state.attributes.get("status") == PAUSED state = hass.states.get("sensor.energy_bill_offpeak") assert state is not None assert state.state == "0" + assert state.attributes.get("status") == PAUSED await hass.services.async_call( DOMAIN, @@ -99,14 +124,17 @@ async def test_state(hass): state = hass.states.get("sensor.energy_bill_onpeak") assert state is not None assert state.state == "1" + assert state.attributes.get("status") == PAUSED state = hass.states.get("sensor.energy_bill_midpeak") assert state is not None assert state.state == "0" + assert state.attributes.get("status") == PAUSED state = hass.states.get("sensor.energy_bill_offpeak") assert state is not None assert state.state == "3" + assert state.attributes.get("status") == COLLECTING await hass.services.async_call( DOMAIN, @@ -131,6 +159,65 @@ async def test_state(hass): assert state.state == "0.123" +async def test_restore_state(hass): + """Test utility sensor restore state.""" + config = { + "utility_meter": { + "energy_bill": { + "source": "sensor.energy", + "tariffs": ["onpeak", "midpeak", "offpeak"], + } + } + } + mock_restore_cache( + hass, + [ + State( + "sensor.energy_bill_onpeak", + "3", + attributes={ + ATTR_STATUS: PAUSED, + ATTR_LAST_RESET: "2020-12-21T00:00:00.013073+00:00", + }, + ), + State( + "sensor.energy_bill_offpeak", + "6", + attributes={ + ATTR_STATUS: COLLECTING, + ATTR_LAST_RESET: "2020-12-21T00:00:00.013073+00:00", + }, + ), + ], + ) + + assert await async_setup_component(hass, DOMAIN, config) + assert await async_setup_component(hass, SENSOR_DOMAIN, config) + await hass.async_block_till_done() + + # restore from cache + state = hass.states.get("sensor.energy_bill_onpeak") + assert state.state == "3" + assert state.attributes.get("status") == PAUSED + + state = hass.states.get("sensor.energy_bill_offpeak") + assert state.state == "6" + assert state.attributes.get("status") == COLLECTING + + # utility_meter is loaded, now set sensors according to utility_meter: + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + state = hass.states.get("utility_meter.energy_bill") + assert state.state == "onpeak" + + state = hass.states.get("sensor.energy_bill_onpeak") + assert state.attributes.get("status") == COLLECTING + + state = hass.states.get("sensor.energy_bill_offpeak") + assert state.attributes.get("status") == PAUSED + + async def test_net_consumption(hass): """Test utility sensor state.""" config = { From 732cf47ff6259f47e16c76a731d8c8b2d6b7d4f9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 13 Jan 2021 16:54:54 +0100 Subject: [PATCH 198/507] Filter some Alexa reports that are duplicate (#45093) * Filter some Alexa reports that are duplicate * When state changes during reporting, only report last state, not all state changes --- .../components/alexa/state_report.py | 96 +++++++++--- tests/components/alexa/test_state_report.py | 142 ++++++++++++++++++ 2 files changed, 219 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index 1d06422056d..6d31862509b 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -2,15 +2,17 @@ import asyncio import json import logging +from typing import Dict, Optional import aiohttp import async_timeout from homeassistant.const import HTTP_ACCEPTED, MATCH_ALL, STATE_ON +from homeassistant.core import State import homeassistant.util.dt as dt_util from .const import API_CHANGE, Cause -from .entities import ENTITY_ADAPTERS, generate_alexa_id +from .entities import ENTITY_ADAPTERS, AlexaEntity, generate_alexa_id from .messages import AlexaResponse _LOGGER = logging.getLogger(__name__) @@ -25,7 +27,13 @@ async def async_enable_proactive_mode(hass, smart_home_config): # Validate we can get access token. await smart_home_config.async_get_access_token() - async def async_entity_state_listener(changed_entity, old_state, new_state): + progress: Dict[str, AlexaEntity] = {} + + async def async_entity_state_listener( + changed_entity: str, + old_state: Optional[State], + new_state: Optional[State], + ): if not hass.is_running: return @@ -39,24 +47,79 @@ async def async_enable_proactive_mode(hass, smart_home_config): _LOGGER.debug("Not exposing %s because filtered by config", changed_entity) return - alexa_changed_entity = ENTITY_ADAPTERS[new_state.domain]( + alexa_changed_entity: AlexaEntity = ENTITY_ADAPTERS[new_state.domain]( hass, smart_home_config, new_state ) + # Queue up entity to be sent later. + # If two states come in while we are reporting the state, only the last one will be reported. + if changed_entity in progress: + progress[changed_entity] = alexa_changed_entity + return + + # Determine how entity should be reported on + should_report = False + should_doorbell = False + for interface in alexa_changed_entity.interfaces(): - if interface.properties_proactively_reported(): - await async_send_changereport_message( - hass, smart_home_config, alexa_changed_entity - ) - return + if not should_report and interface.properties_proactively_reported(): + should_report = True + if ( interface.name() == "Alexa.DoorbellEventSource" and new_state.state == STATE_ON ): - await async_send_doorbell_event_message( - hass, smart_home_config, alexa_changed_entity - ) - return + should_doorbell = True + break + + if not should_report and not should_doorbell: + return + + if should_doorbell: + should_report = False + + # Store current state change information + last_state: Optional[AlexaEntity] = None + if old_state: + last_state = ENTITY_ADAPTERS[old_state.domain]( + hass, smart_home_config, old_state + ) + progress[changed_entity] = alexa_changed_entity + + # Start reporting on entity. Keep reporting as long as new states come in + # while we were reporting a state. + while last_state != progress[changed_entity]: + to_report = progress[changed_entity] + alexa_properties = None + + if should_report: + # this sends all the properties of the Alexa Entity, whether they have + # changed or not. this should be improved, and properties that have not + # changed should be moved to the 'context' object + alexa_properties = list(alexa_changed_entity.serialize_properties()) + + if last_state and last_state.entity.state == to_report.entity.state: + old_alexa_properties = list(last_state.serialize_properties()) + if old_alexa_properties == alexa_properties: + return + + try: + if should_report: + await async_send_changereport_message( + hass, smart_home_config, alexa_changed_entity, alexa_properties + ) + + elif should_doorbell: + await async_send_doorbell_event_message( + hass, smart_home_config, alexa_changed_entity + ) + except Exception: + progress.pop(changed_entity) + raise + + last_state = to_report + + progress.pop(changed_entity) return hass.helpers.event.async_track_state_change( MATCH_ALL, async_entity_state_listener @@ -64,7 +127,7 @@ async def async_enable_proactive_mode(hass, smart_home_config): async def async_send_changereport_message( - hass, config, alexa_entity, *, invalidate_access_token=True + hass, config, alexa_entity, properties, *, invalidate_access_token=True ): """Send a ChangeReport message for an Alexa entity. @@ -76,11 +139,6 @@ async def async_send_changereport_message( endpoint = alexa_entity.alexa_id() - # this sends all the properties of the Alexa Entity, whether they have - # changed or not. this should be improved, and properties that have not - # changed should be moved to the 'context' object - properties = list(alexa_entity.serialize_properties()) - payload = { API_CHANGE: {"cause": {"type": Cause.APP_INTERACTION}, "properties": properties} } @@ -120,7 +178,7 @@ async def async_send_changereport_message( ): config.async_invalidate_access_token() return await async_send_changereport_message( - hass, config, alexa_entity, invalidate_access_token=False + hass, config, alexa_entity, properties, invalidate_access_token=False ) _LOGGER.error( diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index 42a8ab48279..afa024ce89b 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -1,4 +1,8 @@ """Test report state.""" +import asyncio +from unittest.mock import Mock, patch + +from homeassistant import core from homeassistant.components.alexa import state_report from . import DEFAULT_CONFIG, TEST_URL @@ -171,3 +175,141 @@ async def test_doorbell_event(hass, aioclient_mock): assert call_json["event"]["header"]["name"] == "DoorbellPress" assert call_json["event"]["payload"]["cause"]["type"] == "PHYSICAL_INTERACTION" assert call_json["event"]["endpoint"]["endpointId"] == "binary_sensor#test_doorbell" + + +async def test_proactive_mode_filter_states(hass, aioclient_mock): + """Test all the cases that filter states.""" + hass.states.async_set( + "binary_sensor.test_contact", + "on", + {"friendly_name": "Test Contact Sensor", "device_class": "door"}, + ) + + await state_report.async_enable_proactive_mode(hass, DEFAULT_CONFIG) + + # Force update should not report + hass.states.async_set( + "binary_sensor.test_contact", + "on", + {"friendly_name": "Test Contact Sensor", "device_class": "door"}, + force_update=True, + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + assert len(aioclient_mock.mock_calls) == 0 + + # hass not running should not report + hass.states.async_set( + "binary_sensor.test_contact", + "off", + {"friendly_name": "Test Contact Sensor", "device_class": "door"}, + ) + with patch.object(hass, "state", core.CoreState.stopping): + await hass.async_block_till_done() + await hass.async_block_till_done() + assert len(aioclient_mock.mock_calls) == 0 + + # unsupported entity should not report + hass.states.async_set( + "binary_sensor.test_contact", + "on", + {"friendly_name": "Test Contact Sensor", "device_class": "door"}, + ) + with patch.dict( + "homeassistant.components.alexa.state_report.ENTITY_ADAPTERS", {}, clear=True + ): + await hass.async_block_till_done() + await hass.async_block_till_done() + assert len(aioclient_mock.mock_calls) == 0 + + # Not exposed by config should not report + hass.states.async_set( + "binary_sensor.test_contact", + "off", + {"friendly_name": "Test Contact Sensor", "device_class": "door"}, + ) + with patch.object(DEFAULT_CONFIG, "should_expose", return_value=False): + await hass.async_block_till_done() + await hass.async_block_till_done() + assert len(aioclient_mock.mock_calls) == 0 + + # Removing an entity + hass.states.async_remove("binary_sensor.test_contact") + await hass.async_block_till_done() + await hass.async_block_till_done() + assert len(aioclient_mock.mock_calls) == 0 + + +async def test_proactive_mode_filter_in_progress(hass, aioclient_mock): + """When in progress, queue up state.""" + hass.states.async_set( + "binary_sensor.test_contact", + "off", + {"friendly_name": "Test Contact Sensor", "device_class": "door"}, + ) + + await state_report.async_enable_proactive_mode(hass, DEFAULT_CONFIG) + + # Progress should filter out the 2nd event. + long_sendchange = asyncio.Event() + + with patch( + "homeassistant.components.alexa.state_report.async_send_changereport_message", + Mock(side_effect=lambda *args: long_sendchange.wait()), + ) as mock_report: + hass.states.async_set( + "binary_sensor.test_contact", + "on", + { + "friendly_name": "Test Contact Sensor", + "device_class": "door", + "update": 1, + }, + ) + + await asyncio.sleep(0) + await asyncio.sleep(0) + await asyncio.sleep(0) + + assert len(mock_report.mock_calls) == 1 + + with patch( + "homeassistant.components.alexa.state_report.async_send_changereport_message", + ) as mock_report_2: + hass.states.async_set( + "binary_sensor.test_contact", + "off", + { + "friendly_name": "Test Contact Sensor", + "device_class": "door", + "update": 2, + }, + ) + hass.states.async_set( + "binary_sensor.test_contact", + "on", + { + "friendly_name": "Test Contact Sensor", + "device_class": "door", + "update": 3, + }, + ) + hass.states.async_set( + "binary_sensor.test_contact", + "off", + { + "friendly_name": "Test Contact Sensor", + "device_class": "door", + "update": 4, + }, + ) + + await asyncio.sleep(0) + await asyncio.sleep(0) + await asyncio.sleep(0) + long_sendchange.set() + await hass.async_block_till_done() + + # Should be 1 because the 4rd state change + assert len(mock_report_2.mock_calls) == 1 + mock_report_2.mock_calls[0][1][2].entity.attributes["update"] == 4 From e05bb7e8583ba0232dd9825394aeb39babef1bc6 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 13 Jan 2021 17:01:29 +0100 Subject: [PATCH 199/507] Expose selected Netatmo schedule (#45077) --- homeassistant/components/netatmo/climate.py | 10 ++++++++++ homeassistant/components/netatmo/const.py | 1 + 2 files changed, 11 insertions(+) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index bb930c5a994..63d845b5ab7 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -31,6 +31,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( ATTR_HEATING_POWER_REQUEST, ATTR_SCHEDULE_NAME, + ATTR_SELECTED_SCHEDULE, DATA_HANDLER, DATA_HOMES, DATA_SCHEDULES, @@ -212,6 +213,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self._hg_temperature = None self._boilerstatus = None self._setpoint_duration = None + self._selected_schedule = None if self._model == NA_THERM: self._operation_list.append(HVAC_MODE_OFF) @@ -415,6 +417,8 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): "heating_power_request", 0 ) + attr[ATTR_SELECTED_SCHEDULE] = self._selected_schedule + return attr def turn_off(self): @@ -463,6 +467,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self._away_temperature = self._data.get_away_temp(self._home_id) self._hg_temperature = self._data.get_hg_temp(self._home_id) self._setpoint_duration = self._data.setpoint_duration[self._home_id] + self._selected_schedule = roomstatus.get("selected_schedule") if "current_temperature" not in roomstatus: return @@ -492,6 +497,11 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): "module_id": None, "heating_status": None, "heating_power_request": None, + "selected_schedule": self._data._get_selected_schedule( # pylint: disable=protected-access + home_id=self._home_id + ).get( + "name" + ), } batterylevel = None diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index 138065f086b..ed1c5f0a880 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -69,6 +69,7 @@ ATTR_IS_KNOWN = "is_known" ATTR_FACE_URL = "face_url" ATTR_SCHEDULE_ID = "schedule_id" ATTR_SCHEDULE_NAME = "schedule_name" +ATTR_SELECTED_SCHEDULE = "selected_schedule" ATTR_CAMERA_LIGHT_MODE = "camera_light_mode" SERVICE_SET_CAMERA_LIGHT = "set_camera_light" From 94417e3e14cd135ac249d7ed857c9943ec870ae3 Mon Sep 17 00:00:00 2001 From: Julien Roy Date: Wed, 13 Jan 2021 17:44:57 +0100 Subject: [PATCH 200/507] Add start torrent and stop torrent service for transmission integration (#43920) --- .../components/transmission/__init__.py | 60 +++++++++++++++++++ .../components/transmission/const.py | 2 + .../components/transmission/services.yaml | 20 +++++++ 3 files changed, 82 insertions(+) diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index b1837708919..d020bfe9745 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -40,6 +40,8 @@ from .const import ( EVENT_STARTED_TORRENT, SERVICE_ADD_TORRENT, SERVICE_REMOVE_TORRENT, + SERVICE_START_TORRENT, + SERVICE_STOP_TORRENT, ) from .errors import AuthenticationError, CannotConnect, UnknownError @@ -58,6 +60,20 @@ SERVICE_REMOVE_TORRENT_SCHEMA = vol.Schema( } ) +SERVICE_START_TORRENT_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_ID): cv.positive_int, + } +) + +SERVICE_STOP_TORRENT_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_ID): cv.positive_int, + } +) + TRANS_SCHEMA = vol.All( vol.Schema( { @@ -116,6 +132,8 @@ async def async_unload_entry(hass, config_entry): if not hass.data[DOMAIN]: hass.services.async_remove(DOMAIN, SERVICE_ADD_TORRENT) hass.services.async_remove(DOMAIN, SERVICE_REMOVE_TORRENT) + hass.services.async_remove(DOMAIN, SERVICE_START_TORRENT) + hass.services.async_remove(DOMAIN, SERVICE_STOP_TORRENT) return True @@ -207,6 +225,34 @@ class TransmissionClient: "Could not add torrent: unsupported type or no permission" ) + def start_torrent(service): + """Start torrent.""" + tm_client = None + for entry in self.hass.config_entries.async_entries(DOMAIN): + if entry.data[CONF_NAME] == service.data[CONF_NAME]: + tm_client = self.hass.data[DOMAIN][entry.entry_id] + break + if tm_client is None: + _LOGGER.error("Transmission instance is not found") + return + torrent_id = service.data[CONF_ID] + tm_client.tm_api.start_torrent(torrent_id) + tm_client.api.update() + + def stop_torrent(service): + """Stop torrent.""" + tm_client = None + for entry in self.hass.config_entries.async_entries(DOMAIN): + if entry.data[CONF_NAME] == service.data[CONF_NAME]: + tm_client = self.hass.data[DOMAIN][entry.entry_id] + break + if tm_client is None: + _LOGGER.error("Transmission instance is not found") + return + torrent_id = service.data[CONF_ID] + tm_client.tm_api.stop_torrent(torrent_id) + tm_client.api.update() + def remove_torrent(service): """Remove torrent.""" tm_client = None @@ -233,6 +279,20 @@ class TransmissionClient: schema=SERVICE_REMOVE_TORRENT_SCHEMA, ) + self.hass.services.async_register( + DOMAIN, + SERVICE_START_TORRENT, + start_torrent, + schema=SERVICE_START_TORRENT_SCHEMA, + ) + + self.hass.services.async_register( + DOMAIN, + SERVICE_STOP_TORRENT, + stop_torrent, + schema=SERVICE_STOP_TORRENT_SCHEMA, + ) + self.config_entry.add_update_listener(self.async_options_updated) return True diff --git a/homeassistant/components/transmission/const.py b/homeassistant/components/transmission/const.py index 960fd7a65b4..185148f3bd9 100644 --- a/homeassistant/components/transmission/const.py +++ b/homeassistant/components/transmission/const.py @@ -36,6 +36,8 @@ ATTR_TORRENT = "torrent" SERVICE_ADD_TORRENT = "add_torrent" SERVICE_REMOVE_TORRENT = "remove_torrent" +SERVICE_START_TORRENT = "start_torrent" +SERVICE_STOP_TORRENT = "stop_torrent" DATA_UPDATED = "transmission_data_updated" diff --git a/homeassistant/components/transmission/services.yaml b/homeassistant/components/transmission/services.yaml index e8114b680ab..04ac5472d4c 100644 --- a/homeassistant/components/transmission/services.yaml +++ b/homeassistant/components/transmission/services.yaml @@ -20,3 +20,23 @@ remove_torrent: delete_data: description: Delete torrent data example: false + +start_torrent: + description: Start a torrent + fields: + name: + description: Instance name as entered during entry config + example: Transmission + id: + description: ID of a torrent + example: 123 + +stop_torrent: + description: Stop a torrent + fields: + name: + description: Instance name as entered during entry config + example: Transmission + id: + description: ID of a torrent + example: 123 From 7872e6caf8bac40c8ccb293020f7367d56f48139 Mon Sep 17 00:00:00 2001 From: TimothyLeeAdams Date: Wed, 13 Jan 2021 12:59:57 -0500 Subject: [PATCH 201/507] Change attribute key for Lutron cover to lutron_integration_id (#45114) Currently, covers return "Lutron Integration ID" as a state attribute. This is inconsistent with the light, switch, and binary_sensor which return "lutron_integration_id". --- homeassistant/components/lutron/cover.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/lutron/cover.py b/homeassistant/components/lutron/cover.py index 1ec7c07aac0..f1faed32161 100644 --- a/homeassistant/components/lutron/cover.py +++ b/homeassistant/components/lutron/cover.py @@ -66,4 +66,4 @@ class LutronCover(LutronDevice, CoverEntity): @property def device_state_attributes(self): """Return the state attributes.""" - return {"Lutron Integration ID": self._lutron_device.id} + return {"lutron_integration_id": self._lutron_device.id} From eca6bc6a73258cd9f0d56385a7d0c2ab6a8920b7 Mon Sep 17 00:00:00 2001 From: Nikolay Vasilchuk Date: Wed, 13 Jan 2021 22:44:24 +0300 Subject: [PATCH 202/507] Starline OBD information (#37608) * Starline OBD data * Small fix * Review (comments) * Review (service description) * Review (service method) * starline updated to 0.1.5 * Small typo fix --- homeassistant/components/starline/__init__.py | 35 ++++++++++++++++- homeassistant/components/starline/account.py | 38 +++++++++++++++++-- homeassistant/components/starline/const.py | 3 ++ .../components/starline/manifest.json | 2 +- homeassistant/components/starline/sensor.py | 25 +++++++++++- .../components/starline/services.yaml | 7 ++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 105 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/starline/__init__.py b/homeassistant/components/starline/__init__.py index 303507b1491..392dbff9e03 100644 --- a/homeassistant/components/starline/__init__.py +++ b/homeassistant/components/starline/__init__.py @@ -8,10 +8,13 @@ from homeassistant.exceptions import ConfigEntryNotReady from .account import StarlineAccount from .const import ( CONF_SCAN_INTERVAL, + CONF_SCAN_OBD_INTERVAL, DEFAULT_SCAN_INTERVAL, + DEFAULT_SCAN_OBD_INTERVAL, DOMAIN, PLATFORMS, SERVICE_SET_SCAN_INTERVAL, + SERVICE_SET_SCAN_OBD_INTERVAL, SERVICE_UPDATE_STATE, ) @@ -25,6 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b """Set up the StarLine device from a config entry.""" account = StarlineAccount(hass, config_entry) await account.update() + await account.update_obd() if not account.api.available: raise ConfigEntryNotReady @@ -44,12 +48,23 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ) async def async_set_scan_interval(call): - """Service for set scan interval.""" + """Set scan interval.""" options = dict(config_entry.options) options[CONF_SCAN_INTERVAL] = call.data[CONF_SCAN_INTERVAL] hass.config_entries.async_update_entry(entry=config_entry, options=options) - hass.services.async_register(DOMAIN, SERVICE_UPDATE_STATE, account.update) + async def async_set_scan_obd_interval(call): + """Set OBD info scan interval.""" + options = dict(config_entry.options) + options[CONF_SCAN_OBD_INTERVAL] = call.data[CONF_SCAN_INTERVAL] + hass.config_entries.async_update_entry(entry=config_entry, options=options) + + async def async_update(call=None): + """Update all data.""" + await account.update() + await account.update_obd() + + hass.services.async_register(DOMAIN, SERVICE_UPDATE_STATE, async_update) hass.services.async_register( DOMAIN, SERVICE_SET_SCAN_INTERVAL, @@ -62,6 +77,18 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b } ), ) + hass.services.async_register( + DOMAIN, + SERVICE_SET_SCAN_OBD_INTERVAL, + async_set_scan_obd_interval, + schema=vol.Schema( + { + vol.Required(CONF_SCAN_INTERVAL): vol.All( + vol.Coerce(int), vol.Range(min=180) + ) + } + ), + ) config_entry.add_update_listener(async_options_updated) await async_options_updated(hass, config_entry) @@ -83,4 +110,8 @@ async def async_options_updated(hass: HomeAssistant, config_entry: ConfigEntry) """Triggered by config entry options updates.""" account: StarlineAccount = hass.data[DOMAIN][config_entry.entry_id] scan_interval = config_entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + scan_obd_interval = config_entry.options.get( + CONF_SCAN_OBD_INTERVAL, DEFAULT_SCAN_OBD_INTERVAL + ) account.set_update_interval(scan_interval) + account.set_update_obd_interval(scan_obd_interval) diff --git a/homeassistant/components/starline/account.py b/homeassistant/components/starline/account.py index 1cedcef8e84..7452253019b 100644 --- a/homeassistant/components/starline/account.py +++ b/homeassistant/components/starline/account.py @@ -15,6 +15,7 @@ from .const import ( DATA_SLNET_TOKEN, DATA_USER_ID, DEFAULT_SCAN_INTERVAL, + DEFAULT_SCAN_OBD_INTERVAL, DOMAIN, ) @@ -27,17 +28,19 @@ class StarlineAccount: self._hass: HomeAssistant = hass self._config_entry: ConfigEntry = config_entry self._update_interval: int = DEFAULT_SCAN_INTERVAL + self._update_obd_interval: int = DEFAULT_SCAN_OBD_INTERVAL self._unsubscribe_auto_updater: Optional[Callable] = None + self._unsubscribe_auto_obd_updater: Optional[Callable] = None self._api: StarlineApi = StarlineApi( config_entry.data[DATA_USER_ID], config_entry.data[DATA_SLNET_TOKEN] ) - def _check_slnet_token(self) -> None: + def _check_slnet_token(self, interval: int) -> None: """Check SLNet token expiration and update if needed.""" now = datetime.now().timestamp() slnet_token_expires = self._config_entry.data[DATA_EXPIRES] - if now + self._update_interval > slnet_token_expires: + if now + interval > slnet_token_expires: self._update_slnet_token() def _update_slnet_token(self) -> None: @@ -64,9 +67,14 @@ class StarlineAccount: def _update_data(self): """Update StarLine data.""" - self._check_slnet_token() + self._check_slnet_token(self._update_interval) self._api.update() + def _update_obd_data(self): + """Update StarLine OBD data.""" + self._check_slnet_token(self._update_obd_interval) + self._api.update_obd() + @property def api(self) -> StarlineApi: """Return the instance of the API.""" @@ -76,6 +84,10 @@ class StarlineAccount: """Update StarLine data.""" await self._hass.async_add_executor_job(self._update_data) + async def update_obd(self, unused=None): + """Update StarLine OBD data.""" + await self._hass.async_add_executor_job(self._update_obd_data) + def set_update_interval(self, interval: int) -> None: """Set StarLine API update interval.""" _LOGGER.debug("Setting update interval: %ds", interval) @@ -88,12 +100,27 @@ class StarlineAccount: self._hass, self.update, delta ) + def set_update_obd_interval(self, interval: int) -> None: + """Set StarLine API OBD update interval.""" + _LOGGER.debug("Setting OBD update interval: %ds", interval) + self._update_obd_interval = interval + if self._unsubscribe_auto_obd_updater is not None: + self._unsubscribe_auto_obd_updater() + + delta = timedelta(seconds=interval) + self._unsubscribe_auto_obd_updater = async_track_time_interval( + self._hass, self.update_obd, delta + ) + def unload(self): """Unload StarLine API.""" _LOGGER.debug("Unloading StarLine API.") if self._unsubscribe_auto_updater is not None: self._unsubscribe_auto_updater() self._unsubscribe_auto_updater = None + if self._unsubscribe_auto_obd_updater is not None: + self._unsubscribe_auto_obd_updater() + self._unsubscribe_auto_obd_updater = None @staticmethod def device_info(device: StarlineDevice) -> Dict[str, Any]: @@ -140,3 +167,8 @@ class StarlineAccount: "autostart": device.car_state.get("r_start"), "ignition": device.car_state.get("run"), } + + @staticmethod + def errors_attrs(device: StarlineDevice) -> Dict[str, Any]: + """Attributes for errors sensor.""" + return {"errors": device.errors.get("errors")} diff --git a/homeassistant/components/starline/const.py b/homeassistant/components/starline/const.py index 64ba8fc3d2d..89ea0873aa1 100644 --- a/homeassistant/components/starline/const.py +++ b/homeassistant/components/starline/const.py @@ -13,6 +13,8 @@ CONF_CAPTCHA_CODE = "captcha_code" CONF_SCAN_INTERVAL = "scan_interval" DEFAULT_SCAN_INTERVAL = 180 # in seconds +CONF_SCAN_OBD_INTERVAL = "scan_obd_interval" +DEFAULT_SCAN_OBD_INTERVAL = 10800 # 3 hours in seconds ERROR_AUTH_APP = "error_auth_app" ERROR_AUTH_USER = "error_auth_user" @@ -25,3 +27,4 @@ DATA_EXPIRES = "expires" SERVICE_UPDATE_STATE = "update_state" SERVICE_SET_SCAN_INTERVAL = "set_scan_interval" +SERVICE_SET_SCAN_OBD_INTERVAL = "set_scan_obd_interval" diff --git a/homeassistant/components/starline/manifest.json b/homeassistant/components/starline/manifest.json index d0cba029787..79b163ee115 100644 --- a/homeassistant/components/starline/manifest.json +++ b/homeassistant/components/starline/manifest.json @@ -3,6 +3,6 @@ "name": "StarLine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/starline", - "requirements": ["starline==0.1.3"], + "requirements": ["starline==0.1.5"], "codeowners": ["@anonym-tsk"] } diff --git a/homeassistant/components/starline/sensor.py b/homeassistant/components/starline/sensor.py index 2de4647aa94..8aba1b54269 100644 --- a/homeassistant/components/starline/sensor.py +++ b/homeassistant/components/starline/sensor.py @@ -1,6 +1,12 @@ """Reads vehicle status from StarLine API.""" from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE -from homeassistant.const import PERCENTAGE, TEMP_CELSIUS, VOLT +from homeassistant.const import ( + LENGTH_KILOMETERS, + PERCENTAGE, + TEMP_CELSIUS, + VOLT, + VOLUME_LITERS, +) from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level, icon_for_signal_level @@ -14,6 +20,9 @@ SENSOR_TYPES = { "ctemp": ["Interior Temperature", DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None], "etemp": ["Engine Temperature", DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None], "gsm_lvl": ["GSM Signal", None, PERCENTAGE, None], + "fuel": ["Fuel Volume", None, None, "mdi:fuel"], + "errors": ["OBD Errors", None, None, "mdi:alert-octagon"], + "mileage": ["Mileage", None, LENGTH_KILOMETERS, "mdi:counter"], } @@ -73,6 +82,12 @@ class StarlineSensor(StarlineEntity, Entity): return self._device.temp_engine if self._key == "gsm_lvl": return self._device.gsm_level_percent + if self._key == "fuel" and self._device.fuel: + return self._device.fuel.get("val") + if self._key == "errors" and self._device.errors: + return self._device.errors.get("val") + if self._key == "mileage" and self._device.mileage: + return self._device.mileage.get("val") return None @property @@ -80,6 +95,12 @@ class StarlineSensor(StarlineEntity, Entity): """Get the unit of measurement.""" if self._key == "balance": return self._device.balance.get("currency") or "₽" + if self._key == "fuel": + type_value = self._device.fuel.get("type") + if type_value == "percents": + return PERCENTAGE + if type_value == "litres": + return VOLUME_LITERS return self._unit @property @@ -94,4 +115,6 @@ class StarlineSensor(StarlineEntity, Entity): return self._account.balance_attrs(self._device) if self._key == "gsm_lvl": return self._account.gsm_attrs(self._device) + if self._key == "errors": + return self._account.errors_attrs(self._device) return None diff --git a/homeassistant/components/starline/services.yaml b/homeassistant/components/starline/services.yaml index bef3a16803e..4eab51b94d7 100644 --- a/homeassistant/components/starline/services.yaml +++ b/homeassistant/components/starline/services.yaml @@ -8,3 +8,10 @@ set_scan_interval: scan_interval: description: Update frequency (in seconds). example: 180 +set_scan_obd_interval: + description: > + Set OBD info update frequency. + fields: + scan_interval: + description: Update frequency (in seconds). + example: 10800 diff --git a/requirements_all.txt b/requirements_all.txt index eebeccf5e08..6125b094d4d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2100,7 +2100,7 @@ sqlalchemy==1.3.22 srpenergy==1.3.2 # homeassistant.components.starline -starline==0.1.3 +starline==0.1.5 # homeassistant.components.starlingbank starlingbank==3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 792b0342b21..6eb22edb1ab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1043,7 +1043,7 @@ sqlalchemy==1.3.22 srpenergy==1.3.2 # homeassistant.components.starline -starline==0.1.3 +starline==0.1.5 # homeassistant.components.statsd statsd==3.2.1 From 3ebc5d45a8af9cb2e9d7d73d0bdd477443a6188c Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Wed, 13 Jan 2021 15:37:54 -0500 Subject: [PATCH 203/507] Add energy and power sensor tests & fix device_class (#45122) --- homeassistant/components/zwave_js/sensor.py | 6 ++--- tests/components/zwave_js/common.py | 2 ++ tests/components/zwave_js/test_sensor.py | 28 +++++++++++++++++++-- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index ef1a68bb7a7..89c34844a1e 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -65,11 +65,9 @@ class ZwaveSensorBase(ZWaveBaseEntity): if self.info.primary_value.command_class == CommandClass.BATTERY: return DEVICE_CLASS_BATTERY if self.info.primary_value.command_class == CommandClass.METER: + if self.info.primary_value.property_key_name == "kWh_Consumed": + return DEVICE_CLASS_ENERGY return DEVICE_CLASS_POWER - if self.info.primary_value.property_key_name == "W_Consumed": - return DEVICE_CLASS_POWER - if self.info.primary_value.property_key_name == "kWh_Consumed": - return DEVICE_CLASS_ENERGY if self.info.primary_value.property_ == "Air temperature": return DEVICE_CLASS_TEMPERATURE return None diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py index c78c6d544b6..73d70d31669 100644 --- a/tests/components/zwave_js/common.py +++ b/tests/components/zwave_js/common.py @@ -1,3 +1,5 @@ """Provide common test tools for Z-Wave JS.""" AIR_TEMPERATURE_SENSOR = "sensor.multisensor_6_air_temperature" +ENERGY_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_2" +POWER_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed" SWITCH_ENTITY = "switch.smart_plug_with_two_usb_ports_current_value" diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index 75ce016fb04..284d2e1a84f 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -1,7 +1,14 @@ """Test the Z-Wave JS sensor platform.""" -from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS +from homeassistant.const import ( + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + ENERGY_KILO_WATT_HOUR, + POWER_WATT, + TEMP_CELSIUS, +) -from .common import AIR_TEMPERATURE_SENSOR +from .common import AIR_TEMPERATURE_SENSOR, ENERGY_SENSOR, POWER_SENSOR async def test_numeric_sensor(hass, multisensor_6, integration): @@ -12,3 +19,20 @@ async def test_numeric_sensor(hass, multisensor_6, integration): assert state.state == "9.0" assert state.attributes["unit_of_measurement"] == TEMP_CELSIUS assert state.attributes["device_class"] == DEVICE_CLASS_TEMPERATURE + + +async def test_energy_sensors(hass, hank_binary_switch, integration): + """Test power and energy sensors.""" + state = hass.states.get(POWER_SENSOR) + + assert state + assert state.state == "0.0" + assert state.attributes["unit_of_measurement"] == POWER_WATT + assert state.attributes["device_class"] == DEVICE_CLASS_POWER + + state = hass.states.get(ENERGY_SENSOR) + + assert state + assert state.state == "0.16" + assert state.attributes["unit_of_measurement"] == ENERGY_KILO_WATT_HOUR + assert state.attributes["device_class"] == DEVICE_CLASS_ENERGY From 17cb0711738bd5e287af9afd895d1120e4aaa331 Mon Sep 17 00:00:00 2001 From: ehendrix23 Date: Wed, 13 Jan 2021 23:15:23 -0700 Subject: [PATCH 204/507] Bump MyQ to 2.0.14 (#45067) --- homeassistant/components/myq/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index 653a2229296..aba2f24b5bd 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.13"], + "requirements": ["pymyq==2.0.14"], "codeowners": ["@bdraco"], "config_flow": true, "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 6125b094d4d..097ea7c50c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1539,7 +1539,7 @@ pymsteams==0.1.12 pymusiccast==0.1.6 # homeassistant.components.myq -pymyq==2.0.13 +pymyq==2.0.14 # homeassistant.components.mysensors pymysensors==0.18.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6eb22edb1ab..7ddd32be363 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -785,7 +785,7 @@ pymodbus==2.3.0 pymonoprice==0.3 # homeassistant.components.myq -pymyq==2.0.13 +pymyq==2.0.14 # homeassistant.components.nut pynut2==2.1.2 From 402a0ea7dac4cb4f88afa1d862fee637ad1f9648 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Thu, 14 Jan 2021 08:47:45 +0100 Subject: [PATCH 205/507] Fix OpenWeatherMap forecast timestamp (#45124) --- homeassistant/components/openweathermap/sensor.py | 10 +++++++++- .../openweathermap/weather_update_coordinator.py | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py index 39c50c3b941..b1ba4ab7625 100644 --- a/homeassistant/components/openweathermap/sensor.py +++ b/homeassistant/components/openweathermap/sensor.py @@ -1,7 +1,10 @@ """Support for the OpenWeatherMap (OWM) service.""" +import datetime + from .abstract_owm_sensor import AbstractOpenWeatherMapSensor from .const import ( ATTR_API_FORECAST, + DEVICE_CLASS_TIMESTAMP, DOMAIN, ENTRY_NAME, ENTRY_WEATHER_COORDINATOR, @@ -95,5 +98,10 @@ class OpenWeatherMapForecastSensor(AbstractOpenWeatherMapSensor): """Return the state of the device.""" forecasts = self._weather_coordinator.data.get(ATTR_API_FORECAST) if forecasts is not None and len(forecasts) > 0: - return forecasts[0].get(self._sensor_type, None) + value = forecasts[0].get(self._sensor_type, None) + if self._device_class is DEVICE_CLASS_TIMESTAMP: + value = datetime.datetime.fromtimestamp( + value, datetime.timezone.utc + ).isoformat() + return value return None diff --git a/homeassistant/components/openweathermap/weather_update_coordinator.py b/homeassistant/components/openweathermap/weather_update_coordinator.py index b4ddb40c046..605e6f9edc1 100644 --- a/homeassistant/components/openweathermap/weather_update_coordinator.py +++ b/homeassistant/components/openweathermap/weather_update_coordinator.py @@ -138,7 +138,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): def _convert_forecast(self, entry): forecast = { - ATTR_FORECAST_TIME: entry.reference_time("unix") * 1000, + ATTR_FORECAST_TIME: entry.reference_time("unix"), ATTR_FORECAST_PRECIPITATION: self._calc_precipitation( entry.rain, entry.snow ), From da677f7d5a889be26118ed92eaf04514ba918e1b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 13 Jan 2021 22:09:08 -1000 Subject: [PATCH 206/507] Add support for discovery via DHCP (#45087) * Add support for discovery via DHCP * additional tesla ouis * merge tests * dhcp test * merge requirements test * dhcp test * dhcp discovery * dhcp discovery * pylint * pylint * pylint * fix * Add matching tests * 100% cover * cleanup * fix codespell * Update exception handling * remove unneeded comment * fix options handling exception * fix options handling exception --- .pre-commit-config.yaml | 2 +- CODEOWNERS | 1 + homeassistant/components/august/manifest.json | 4 + .../components/default_config/manifest.json | 1 + homeassistant/components/dhcp/__init__.py | 159 +++++++++ homeassistant/components/dhcp/const.py | 3 + homeassistant/components/dhcp/manifest.json | 11 + homeassistant/components/flume/manifest.json | 6 +- homeassistant/components/nest/manifest.json | 3 +- homeassistant/components/nexia/manifest.json | 3 +- homeassistant/components/nuheat/manifest.json | 3 +- .../components/powerwall/manifest.json | 6 +- homeassistant/components/rachio/manifest.json | 12 + homeassistant/components/ring/manifest.json | 3 +- homeassistant/components/roomba/manifest.json | 3 +- homeassistant/components/sense/manifest.json | 3 +- .../components/solaredge/manifest.json | 3 +- homeassistant/components/somfy/manifest.json | 7 +- .../components/somfy_mylink/manifest.json | 7 +- homeassistant/components/tesla/manifest.json | 7 +- homeassistant/config_entries.py | 2 + homeassistant/generated/dhcp.py | 118 +++++++ homeassistant/helpers/config_entry_flow.py | 1 + .../helpers/config_entry_oauth2_flow.py | 1 + homeassistant/loader.py | 20 ++ homeassistant/package_constraints.txt | 1 + homeassistant/requirements.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/hassfest/__main__.py | 2 + script/hassfest/config_flow.py | 7 + script/hassfest/dhcp.py | 63 ++++ script/hassfest/manifest.py | 8 + tests/components/dhcp/__init__.py | 1 + tests/components/dhcp/test_init.py | 302 ++++++++++++++++++ tests/helpers/test_config_entry_flow.py | 4 +- tests/test_loader.py | 53 +++ tests/test_requirements.py | 23 ++ 38 files changed, 843 insertions(+), 17 deletions(-) create mode 100644 homeassistant/components/dhcp/__init__.py create mode 100644 homeassistant/components/dhcp/const.py create mode 100644 homeassistant/components/dhcp/manifest.json create mode 100644 homeassistant/generated/dhcp.py create mode 100644 script/hassfest/dhcp.py create mode 100644 tests/components/dhcp/__init__.py create mode 100644 tests/components/dhcp/test_init.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 00f2373f2db..8944a69d9ed 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: hooks: - id: codespell args: - - --ignore-words-list=hass,alot,datas,dof,dur,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing,iam,incomfort + - --ignore-words-list=hass,alot,datas,dof,dur,ether,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing,iam,incomfort - --skip="./.*,*.csv,*.json" - --quiet-level=2 exclude_types: [csv, json] diff --git a/CODEOWNERS b/CODEOWNERS index 720320430b1..d128a6563ab 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -107,6 +107,7 @@ homeassistant/components/derivative/* @afaucogney homeassistant/components/device_automation/* @home-assistant/core homeassistant/components/devolo_home_control/* @2Fake @Shutgun homeassistant/components/dexcom/* @gagebenne +homeassistant/components/dhcp/* @bdraco homeassistant/components/digital_ocean/* @fabaff homeassistant/components/directv/* @ctalkington homeassistant/components/discogs/* @thibmaek diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 67649b7edba..dcdfb0a0497 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -5,5 +5,9 @@ "requirements": ["py-august==0.25.2"], "dependencies": ["configurator"], "codeowners": ["@bdraco"], + "dhcp": [ + {"hostname":"connect","macaddress":"D86162*"}, + {"hostname":"connect","macaddress":"B8B7F1*"} + ], "config_flow": true } diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 9a533092b8b..f8be3c9fe2a 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -6,6 +6,7 @@ "automation", "cloud", "counter", + "dhcp", "frontend", "history", "input_boolean", diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py new file mode 100644 index 00000000000..b677325726c --- /dev/null +++ b/homeassistant/components/dhcp/__init__.py @@ -0,0 +1,159 @@ +"""The dhcp integration.""" + +import fnmatch +import logging +from threading import Event, Thread + +from scapy.error import Scapy_Exception +from scapy.layers.dhcp import DHCP +from scapy.layers.l2 import Ether +from scapy.sendrecv import sniff + +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import format_mac +from homeassistant.loader import async_get_dhcp + +from .const import DOMAIN + +FILTER = "udp and (port 67 or 68)" +REQUESTED_ADDR = "requested_addr" +MESSAGE_TYPE = "message-type" +HOSTNAME = "hostname" +MAC_ADDRESS = "macaddress" +IP_ADDRESS = "ip" +DHCP_REQUEST = 3 + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: dict) -> bool: + """Set up the dhcp component.""" + + async def _initialize(_): + dhcp_watcher = DHCPWatcher(hass, await async_get_dhcp(hass)) + dhcp_watcher.start() + + def _stop(*_): + dhcp_watcher.stop() + dhcp_watcher.join() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _initialize) + return True + + +class DHCPWatcher(Thread): + """Class to watch dhcp requests.""" + + def __init__(self, hass, integration_matchers): + """Initialize class.""" + super().__init__() + + self.hass = hass + self.name = "dhcp-discovery" + self._integration_matchers = integration_matchers + self._address_data = {} + self._stop_event = Event() + + def stop(self): + """Stop the thread.""" + self._stop_event.set() + + def run(self): + """Start watching for dhcp packets.""" + try: + sniff( + filter=FILTER, + prn=self.handle_dhcp_packet, + stop_filter=lambda _: self._stop_event.is_set(), + ) + except (Scapy_Exception, OSError) as ex: + _LOGGER.info("Cannot watch for dhcp packets: %s", ex) + return + + def handle_dhcp_packet(self, packet): + """Process a dhcp packet.""" + if DHCP not in packet: + return + + options = packet[DHCP].options + + request_type = _decode_dhcp_option(options, MESSAGE_TYPE) + if request_type != DHCP_REQUEST: + # DHCP request + return + + ip_address = _decode_dhcp_option(options, REQUESTED_ADDR) + hostname = _decode_dhcp_option(options, HOSTNAME) + mac_address = _format_mac(packet[Ether].src) + + if ip_address is None or hostname is None or mac_address is None: + return + + data = self._address_data.get(ip_address) + + if data and data[MAC_ADDRESS] == mac_address and data[HOSTNAME] == hostname: + # If the address data is the same no need + # to process it + return + + self._address_data[ip_address] = {MAC_ADDRESS: mac_address, HOSTNAME: hostname} + + self.process_updated_address_data(ip_address, self._address_data[ip_address]) + + def process_updated_address_data(self, ip_address, data): + """Process the address data update.""" + lowercase_hostname = data[HOSTNAME].lower() + uppercase_mac = data[MAC_ADDRESS].upper() + + _LOGGER.debug( + "Processing updated address data for %s: mac=%s hostname=%s", + ip_address, + uppercase_mac, + lowercase_hostname, + ) + + for entry in self._integration_matchers: + if MAC_ADDRESS in entry and not fnmatch.fnmatch( + uppercase_mac, entry[MAC_ADDRESS] + ): + continue + + if HOSTNAME in entry and not fnmatch.fnmatch( + lowercase_hostname, entry[HOSTNAME] + ): + continue + + _LOGGER.debug("Matched %s against %s", data, entry) + + self.hass.add_job( + self.hass.config_entries.flow.async_init( + entry["domain"], + context={"source": DOMAIN}, + data={IP_ADDRESS: ip_address, **data}, + ) + ) + + +def _decode_dhcp_option(dhcp_options, key): + """Extract and decode data from a packet option.""" + for option in dhcp_options: + if len(option) < 2 or option[0] != key: + continue + + value = option[1] + if value is None or key != HOSTNAME: + return value + + # hostname is unicode + try: + return value.decode() + except (AttributeError, UnicodeDecodeError): + return None + + +def _format_mac(mac_address): + """Format a mac address for matching.""" + return format_mac(mac_address).replace(":", "") diff --git a/homeassistant/components/dhcp/const.py b/homeassistant/components/dhcp/const.py new file mode 100644 index 00000000000..c28a699c64c --- /dev/null +++ b/homeassistant/components/dhcp/const.py @@ -0,0 +1,3 @@ +"""Constants for the dhcp integration.""" + +DOMAIN = "dhcp" diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json new file mode 100644 index 00000000000..eda229ebec7 --- /dev/null +++ b/homeassistant/components/dhcp/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "dhcp", + "name": "DHCP Discovery", + "documentation": "https://www.home-assistant.io/integrations/dhcp", + "requirements": [ + "scapy==2.4.4" + ], + "codeowners": [ + "@bdraco" + ] +} diff --git a/homeassistant/components/flume/manifest.json b/homeassistant/components/flume/manifest.json index f00bccb886a..813b8788ed5 100644 --- a/homeassistant/components/flume/manifest.json +++ b/homeassistant/components/flume/manifest.json @@ -4,5 +4,9 @@ "documentation": "https://www.home-assistant.io/integrations/flume/", "requirements": ["pyflume==0.5.5"], "codeowners": ["@ChrisMandich", "@bdraco"], - "config_flow": true + "config_flow": true, + "dhcp": [ + {"hostname":"flume-gw-*","macaddress":"ECFABC*"}, + {"hostname":"flume-gw-*","macaddress":"B4E62D*"} + ] } diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 42b790e5612..bde21868cb5 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -6,5 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/nest", "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.2.8"], "codeowners": ["@allenporter"], - "quality_scale": "platinum" + "quality_scale": "platinum", + "dhcp": [{"macaddress":"18B430*"}] } diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index a3446f6168c..cb3493ebc55 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -4,5 +4,6 @@ "requirements": ["nexia==0.9.5"], "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", - "config_flow": true + "config_flow": true, + "dhcp": [{"hostname":"xl857-*","macaddress":"000231*"}] } diff --git a/homeassistant/components/nuheat/manifest.json b/homeassistant/components/nuheat/manifest.json index d479f570d60..92527f50660 100644 --- a/homeassistant/components/nuheat/manifest.json +++ b/homeassistant/components/nuheat/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/nuheat", "requirements": ["nuheat==0.3.0"], "codeowners": ["@bdraco"], - "config_flow": true + "config_flow": true, + "dhcp": [{"hostname":"nuheat","macaddress":"002338*"}] } diff --git a/homeassistant/components/powerwall/manifest.json b/homeassistant/components/powerwall/manifest.json index be83f6825e7..6b7b147d3c5 100644 --- a/homeassistant/components/powerwall/manifest.json +++ b/homeassistant/components/powerwall/manifest.json @@ -4,5 +4,9 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/powerwall", "requirements": ["tesla-powerwall==0.3.3"], - "codeowners": ["@bdraco", "@jrester"] + "codeowners": ["@bdraco", "@jrester"], + "dhcp": [ + {"hostname":"1118431-*","macaddress":"88DA1A*"}, + {"hostname":"1118431-*","macaddress":"000145*"} + ] } diff --git a/homeassistant/components/rachio/manifest.json b/homeassistant/components/rachio/manifest.json index 224f59ea173..ba81b65b37f 100644 --- a/homeassistant/components/rachio/manifest.json +++ b/homeassistant/components/rachio/manifest.json @@ -7,6 +7,18 @@ "after_dependencies": ["cloud"], "codeowners": ["@bdraco"], "config_flow": true, + "dhcp": [{ + "hostname": "rachio-*", + "macaddress": "009D6B*" + }, + { + "hostname": "rachio-*", + "macaddress": "F0038C*" + }, + { + "hostname": "rachio-*", + "macaddress": "74C63B*" + }], "homekit": { "models": ["Rachio"] } diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 550da4d38ec..38083830311 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -5,5 +5,6 @@ "requirements": ["ring_doorbell==0.6.2"], "dependencies": ["ffmpeg"], "codeowners": ["@balloob"], - "config_flow": true + "config_flow": true, + "dhcp": [{"hostname":"ring*","macaddress":"0CAE7D*"}] } diff --git a/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json index f2e5c8035aa..5ceb44ff780 100644 --- a/homeassistant/components/roomba/manifest.json +++ b/homeassistant/components/roomba/manifest.json @@ -4,5 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/roomba", "requirements": ["roombapy==1.6.2"], - "codeowners": ["@pschmitt", "@cyr-ius", "@shenxn"] + "codeowners": ["@pschmitt", "@cyr-ius", "@shenxn"], + "dhcp": [{"hostname":"irobot-*","macaddress":"501479*"}] } diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json index f0b20f27a55..bd132f1f983 100644 --- a/homeassistant/components/sense/manifest.json +++ b/homeassistant/components/sense/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/sense", "requirements": ["sense_energy==0.8.1"], "codeowners": ["@kbickar"], - "config_flow": true + "config_flow": true, + "dhcp": [{"hostname":"sense-*","macaddress":"009D6B*"}, {"hostname":"sense-*","macaddress":"DCEFCA*"}] } diff --git a/homeassistant/components/solaredge/manifest.json b/homeassistant/components/solaredge/manifest.json index 59b8cba7446..f0a620021ad 100644 --- a/homeassistant/components/solaredge/manifest.json +++ b/homeassistant/components/solaredge/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/solaredge", "requirements": ["solaredge==0.0.2", "stringcase==1.2.0"], "config_flow": true, - "codeowners": [] + "codeowners": [], + "dhcp": [{"hostname":"target","macaddress":"002702*"}] } diff --git a/homeassistant/components/somfy/manifest.json b/homeassistant/components/somfy/manifest.json index ea84bf34586..7fb41992164 100644 --- a/homeassistant/components/somfy/manifest.json +++ b/homeassistant/components/somfy/manifest.json @@ -5,5 +5,8 @@ "documentation": "https://www.home-assistant.io/integrations/somfy", "dependencies": ["http"], "codeowners": ["@tetienne"], - "requirements": ["pymfy==0.9.3"] -} \ No newline at end of file + "requirements": ["pymfy==0.9.3"], + "dhcp": [ + {"hostname":"gateway-*","macaddress":"F8811A*"} + ] +} diff --git a/homeassistant/components/somfy_mylink/manifest.json b/homeassistant/components/somfy_mylink/manifest.json index e9b4601dee3..a7be33583d2 100644 --- a/homeassistant/components/somfy_mylink/manifest.json +++ b/homeassistant/components/somfy_mylink/manifest.json @@ -6,5 +6,8 @@ "somfy-mylink-synergy==1.0.6" ], "codeowners": ["@bdraco"], - "config_flow": true -} \ No newline at end of file + "config_flow": true, + "dhcp": [{ + "hostname":"somfy_*", "macaddress":"B8B7F1*" + }] +} diff --git a/homeassistant/components/tesla/manifest.json b/homeassistant/components/tesla/manifest.json index f1f4df6edd6..3679c0f74d1 100644 --- a/homeassistant/components/tesla/manifest.json +++ b/homeassistant/components/tesla/manifest.json @@ -4,5 +4,10 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tesla", "requirements": ["teslajsonpy==0.10.4"], - "codeowners": ["@zabuldon", "@alandtse"] + "codeowners": ["@zabuldon", "@alandtse"], + "dhcp": [ + {"hostname":"tesla_*","macaddress":"4CFCAA*"}, + {"hostname":"tesla_*","macaddress":"044EAF*"}, + {"hostname":"tesla_*","macaddress":"98ED5C*"} + ] } diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index f87e76edec8..eca157b6403 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -29,6 +29,7 @@ SOURCE_MQTT = "mqtt" SOURCE_SSDP = "ssdp" SOURCE_USER = "user" SOURCE_ZEROCONF = "zeroconf" +SOURCE_DHCP = "dhcp" # If a user wants to hide a discovery from the UI they can "Ignore" it. The config_entries/ignore_flow # websocket command creates a config entry with this source and while it exists normal discoveries @@ -1045,6 +1046,7 @@ class ConfigFlow(data_entry_flow.FlowHandler): async_step_mqtt = async_step_discovery async_step_ssdp = async_step_discovery async_step_zeroconf = async_step_discovery + async_step_dhcp = async_step_discovery class OptionsFlowManager(data_entry_flow.FlowManager): diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py new file mode 100644 index 00000000000..f9319c7432a --- /dev/null +++ b/homeassistant/generated/dhcp.py @@ -0,0 +1,118 @@ +"""Automatically generated by hassfest. + +To update, run python3 -m script.hassfest +""" + +# fmt: off + +DHCP = [ + { + "domain": "august", + "hostname": "connect", + "macaddress": "D86162*" + }, + { + "domain": "august", + "hostname": "connect", + "macaddress": "B8B7F1*" + }, + { + "domain": "flume", + "hostname": "flume-gw-*", + "macaddress": "ECFABC*" + }, + { + "domain": "flume", + "hostname": "flume-gw-*", + "macaddress": "B4E62D*" + }, + { + "domain": "nest", + "macaddress": "18B430*" + }, + { + "domain": "nexia", + "hostname": "xl857-*", + "macaddress": "000231*" + }, + { + "domain": "nuheat", + "hostname": "nuheat", + "macaddress": "002338*" + }, + { + "domain": "powerwall", + "hostname": "1118431-*", + "macaddress": "88DA1A*" + }, + { + "domain": "powerwall", + "hostname": "1118431-*", + "macaddress": "000145*" + }, + { + "domain": "rachio", + "hostname": "rachio-*", + "macaddress": "009D6B*" + }, + { + "domain": "rachio", + "hostname": "rachio-*", + "macaddress": "F0038C*" + }, + { + "domain": "rachio", + "hostname": "rachio-*", + "macaddress": "74C63B*" + }, + { + "domain": "ring", + "hostname": "ring*", + "macaddress": "0CAE7D*" + }, + { + "domain": "roomba", + "hostname": "irobot-*", + "macaddress": "501479*" + }, + { + "domain": "sense", + "hostname": "sense-*", + "macaddress": "009D6B*" + }, + { + "domain": "sense", + "hostname": "sense-*", + "macaddress": "DCEFCA*" + }, + { + "domain": "solaredge", + "hostname": "target", + "macaddress": "002702*" + }, + { + "domain": "somfy", + "hostname": "gateway-*", + "macaddress": "F8811A*" + }, + { + "domain": "somfy_mylink", + "hostname": "somfy_*", + "macaddress": "B8B7F1*" + }, + { + "domain": "tesla", + "hostname": "tesla_*", + "macaddress": "4CFCAA*" + }, + { + "domain": "tesla", + "hostname": "tesla_*", + "macaddress": "044EAF*" + }, + { + "domain": "tesla", + "hostname": "tesla_*", + "macaddress": "98ED5C*" + } +] diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 6b9df47c4d8..889981537c6 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -82,6 +82,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): async_step_ssdp = async_step_discovery async_step_mqtt = async_step_discovery async_step_homekit = async_step_discovery + async_step_dhcp = async_step_discovery async def async_step_import(self, _: Optional[Dict[str, Any]]) -> Dict[str, Any]: """Handle a flow initialized by import.""" diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index c4d7de3839e..653d07a333e 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -329,6 +329,7 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): async_step_ssdp = async_step_discovery async_step_zeroconf = async_step_discovery async_step_homekit = async_step_discovery + async_step_dhcp = async_step_discovery @classmethod def async_register_implementation( diff --git a/homeassistant/loader.py b/homeassistant/loader.py index ba29ff4a8da..fc0355c0892 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -25,6 +25,7 @@ from typing import ( cast, ) +from homeassistant.generated.dhcp import DHCP from homeassistant.generated.mqtt import MQTT from homeassistant.generated.ssdp import SSDP from homeassistant.generated.zeroconf import HOMEKIT, ZEROCONF @@ -171,6 +172,20 @@ async def async_get_zeroconf(hass: "HomeAssistant") -> Dict[str, List[Dict[str, return zeroconf +async def async_get_dhcp(hass: "HomeAssistant") -> List[Dict[str, str]]: + """Return cached list of dhcp types.""" + dhcp: List[Dict[str, str]] = DHCP.copy() + + integrations = await async_get_custom_components(hass) + for integration in integrations.values(): + if not integration.dhcp: + continue + for entry in integration.dhcp: + dhcp.append({"domain": integration.domain, **entry}) + + return dhcp + + async def async_get_homekit(hass: "HomeAssistant") -> Dict[str, str]: """Return cached list of homekit models.""" @@ -356,6 +371,11 @@ class Integration: """Return Integration zeroconf entries.""" return cast(List[str], self.manifest.get("zeroconf")) + @property + def dhcp(self) -> Optional[list]: + """Return Integration dhcp entries.""" + return cast(List[str], self.manifest.get("dhcp")) + @property def homekit(self) -> Optional[dict]: """Return Integration homekit entries.""" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a399080594a..cb637a6f539 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -25,6 +25,7 @@ pytz>=2020.5 pyyaml==5.3.1 requests==2.25.1 ruamel.yaml==0.15.100 +scapy==2.4.4 sqlalchemy==1.3.22 voluptuous-serialize==2.4.0 voluptuous==0.12.1 diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index cebfd95591f..f26c8fa8dfc 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -14,6 +14,7 @@ DATA_PKG_CACHE = "pkg_cache" DATA_INTEGRATIONS_WITH_REQS = "integrations_with_reqs" CONSTRAINT_FILE = "package_constraints.txt" DISCOVERY_INTEGRATIONS: Dict[str, Iterable[str]] = { + "dhcp": ("dhcp",), "mqtt": ("mqtt",), "ssdp": ("ssdp",), "zeroconf": ("zeroconf", "homekit"), diff --git a/requirements_all.txt b/requirements_all.txt index 097ea7c50c7..c11b35312b1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1984,6 +1984,9 @@ samsungtvws==1.4.0 # homeassistant.components.satel_integra satel_integra==0.3.4 +# homeassistant.components.dhcp +scapy==2.4.4 + # homeassistant.components.deutsche_bahn schiene==0.23 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7ddd32be363..10307a16b8c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -980,6 +980,9 @@ samsungctl[websocket]==0.7.1 # homeassistant.components.samsungtv samsungtvws==1.4.0 +# homeassistant.components.dhcp +scapy==2.4.4 + # homeassistant.components.emulated_kasa # homeassistant.components.sense sense_energy==0.8.1 diff --git a/script/hassfest/__main__.py b/script/hassfest/__main__.py index 4b2e91524e2..6e88efa2177 100644 --- a/script/hassfest/__main__.py +++ b/script/hassfest/__main__.py @@ -9,6 +9,7 @@ from . import ( config_flow, coverage, dependencies, + dhcp, json, manifest, mqtt, @@ -31,6 +32,7 @@ INTEGRATION_PLUGINS = [ ssdp, translations, zeroconf, + dhcp, ] HASS_PLUGINS = [ coverage, diff --git a/script/hassfest/config_flow.py b/script/hassfest/config_flow.py index d3402c3dc9a..9ae0daee1b0 100644 --- a/script/hassfest/config_flow.py +++ b/script/hassfest/config_flow.py @@ -48,6 +48,11 @@ def validate_integration(config: Config, integration: Integration): "config_flow", "Zeroconf information in a manifest requires a config flow to exist", ) + if integration.manifest.get("dhcp"): + integration.add_error( + "config_flow", + "DHCP information in a manifest requires a config flow to exist", + ) return config_flow = config_flow_file.read_text() @@ -59,6 +64,7 @@ def validate_integration(config: Config, integration: Integration): or "async_step_mqtt" in config_flow or "async_step_ssdp" in config_flow or "async_step_zeroconf" in config_flow + or "async_step_dhcp" in config_flow ) if not needs_unique_id: @@ -100,6 +106,7 @@ def generate_and_validate(integrations: Dict[str, Integration], config: Config): or integration.manifest.get("mqtt") or integration.manifest.get("ssdp") or integration.manifest.get("zeroconf") + or integration.manifest.get("dhcp") ): continue diff --git a/script/hassfest/dhcp.py b/script/hassfest/dhcp.py new file mode 100644 index 00000000000..fbf695a9f73 --- /dev/null +++ b/script/hassfest/dhcp.py @@ -0,0 +1,63 @@ +"""Generate dhcp file.""" +import json +from typing import Dict, List + +from .model import Config, Integration + +BASE = """ +\"\"\"Automatically generated by hassfest. + +To update, run python3 -m script.hassfest +\"\"\" + +# fmt: off + +DHCP = {} +""".strip() + + +def generate_and_validate(integrations: List[Dict[str, str]]): + """Validate and generate dhcp data.""" + match_list = [] + + for domain in sorted(integrations): + integration = integrations[domain] + + if not integration.manifest: + continue + + match_types = integration.manifest.get("dhcp", []) + + if not match_types: + continue + + for entry in match_types: + match_list.append({"domain": domain, **entry}) + + return BASE.format(json.dumps(match_list, indent=4)) + + +def validate(integrations: Dict[str, Integration], config: Config): + """Validate dhcp file.""" + dhcp_path = config.root / "homeassistant/generated/dhcp.py" + config.cache["dhcp"] = content = generate_and_validate(integrations) + + if config.specific_integrations: + return + + with open(str(dhcp_path)) as fp: + current = fp.read().strip() + if current != content: + config.add_error( + "dhcp", + "File dhcp.py is not up to date. Run python3 -m script.hassfest", + fixable=True, + ) + return + + +def generate(integrations: Dict[str, Integration], config: Config): + """Generate dhcp file.""" + dhcp_path = config.root / "homeassistant/generated/dhcp.py" + with open(str(dhcp_path), "w") as fp: + fp.write(f"{config.cache['dhcp']}\n") diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 7500483ec53..af483c3c5e7 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -71,6 +71,14 @@ MANIFEST_SCHEMA = vol.Schema( vol.All([vol.All(vol.Schema({}, extra=vol.ALLOW_EXTRA), vol.Length(min=1))]) ), vol.Optional("homekit"): vol.Schema({vol.Optional("models"): [str]}), + vol.Optional("dhcp"): [ + vol.Schema( + { + vol.Optional("macaddress"): vol.All(str, verify_uppercase), + vol.Optional("hostname"): vol.All(str, verify_lowercase), + } + ) + ], vol.Required("documentation"): vol.All( vol.Url(), documentation_url # pylint: disable=no-value-for-parameter ), diff --git a/tests/components/dhcp/__init__.py b/tests/components/dhcp/__init__.py new file mode 100644 index 00000000000..fc58a7de903 --- /dev/null +++ b/tests/components/dhcp/__init__.py @@ -0,0 +1 @@ +"""Tests for the dhcp integration.""" diff --git a/tests/components/dhcp/test_init.py b/tests/components/dhcp/test_init.py new file mode 100644 index 00000000000..d4d22a5f929 --- /dev/null +++ b/tests/components/dhcp/test_init.py @@ -0,0 +1,302 @@ +"""Test the DHCP discovery integration.""" +import threading +from unittest.mock import patch + +from scapy.error import Scapy_Exception +from scapy.layers.dhcp import DHCP +from scapy.layers.l2 import Ether + +from homeassistant.components import dhcp +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP +from homeassistant.setup import async_setup_component + +from tests.common import mock_coro + +# connect b8:b7:f1:6d:b5:33 192.168.210.56 +RAW_DHCP_REQUEST = ( + b"\xff\xff\xff\xff\xff\xff\xb8\xb7\xf1m\xb53\x08\x00E\x00\x01P\x06E" + b"\x00\x00\xff\x11\xb4X\x00\x00\x00\x00\xff\xff\xff\xff\x00D\x00C\x01<" + b"\x0b\x14\x01\x01\x06\x00jmjV\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb8\xb7\xf1m\xb53\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00c\x82Sc5\x01\x039\x02\x05\xdc2\x04\xc0\xa8\xd286" + b"\x04\xc0\xa8\xd0\x017\x04\x01\x03\x1c\x06\x0c\x07connect\xff\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +) + + +async def test_dhcp_match_hostname_and_macaddress(hass): + """Test matching based on hostname and macaddress.""" + dhcp_watcher = dhcp.DHCPWatcher( + hass, + [{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}], + ) + + packet = Ether(RAW_DHCP_REQUEST) + + with patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: + dhcp_watcher.handle_dhcp_packet(packet) + # Ensure no change is ignored + dhcp_watcher.handle_dhcp_packet(packet) + + assert len(mock_init.mock_calls) == 1 + assert mock_init.mock_calls[0][1][0] == "mock-domain" + assert mock_init.mock_calls[0][2]["context"] == {"source": "dhcp"} + assert mock_init.mock_calls[0][2]["data"] == { + dhcp.IP_ADDRESS: "192.168.210.56", + dhcp.HOSTNAME: "connect", + dhcp.MAC_ADDRESS: "b8b7f16db533", + } + + +async def test_dhcp_match_hostname(hass): + """Test matching based on hostname only.""" + dhcp_watcher = dhcp.DHCPWatcher( + hass, [{"domain": "mock-domain", "hostname": "connect"}] + ) + + packet = Ether(RAW_DHCP_REQUEST) + + with patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: + dhcp_watcher.handle_dhcp_packet(packet) + + assert len(mock_init.mock_calls) == 1 + assert mock_init.mock_calls[0][1][0] == "mock-domain" + assert mock_init.mock_calls[0][2]["context"] == {"source": "dhcp"} + assert mock_init.mock_calls[0][2]["data"] == { + dhcp.IP_ADDRESS: "192.168.210.56", + dhcp.HOSTNAME: "connect", + dhcp.MAC_ADDRESS: "b8b7f16db533", + } + + +async def test_dhcp_match_macaddress(hass): + """Test matching based on macaddress only.""" + dhcp_watcher = dhcp.DHCPWatcher( + hass, [{"domain": "mock-domain", "macaddress": "B8B7F1*"}] + ) + + packet = Ether(RAW_DHCP_REQUEST) + + with patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: + dhcp_watcher.handle_dhcp_packet(packet) + + assert len(mock_init.mock_calls) == 1 + assert mock_init.mock_calls[0][1][0] == "mock-domain" + assert mock_init.mock_calls[0][2]["context"] == {"source": "dhcp"} + assert mock_init.mock_calls[0][2]["data"] == { + dhcp.IP_ADDRESS: "192.168.210.56", + dhcp.HOSTNAME: "connect", + dhcp.MAC_ADDRESS: "b8b7f16db533", + } + + +async def test_dhcp_nomatch(hass): + """Test not matching based on macaddress only.""" + dhcp_watcher = dhcp.DHCPWatcher( + hass, [{"domain": "mock-domain", "macaddress": "ABC123*"}] + ) + + packet = Ether(RAW_DHCP_REQUEST) + + with patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: + dhcp_watcher.handle_dhcp_packet(packet) + + assert len(mock_init.mock_calls) == 0 + + +async def test_dhcp_nomatch_hostname(hass): + """Test not matching based on hostname only.""" + dhcp_watcher = dhcp.DHCPWatcher( + hass, [{"domain": "mock-domain", "hostname": "nomatch*"}] + ) + + packet = Ether(RAW_DHCP_REQUEST) + + with patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: + dhcp_watcher.handle_dhcp_packet(packet) + + assert len(mock_init.mock_calls) == 0 + + +async def test_dhcp_nomatch_non_dhcp_packet(hass): + """Test matching does not throw on a non-dhcp packet.""" + dhcp_watcher = dhcp.DHCPWatcher( + hass, [{"domain": "mock-domain", "hostname": "nomatch*"}] + ) + + packet = Ether(b"") + + with patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: + dhcp_watcher.handle_dhcp_packet(packet) + + assert len(mock_init.mock_calls) == 0 + + +async def test_dhcp_nomatch_non_dhcp_request_packet(hass): + """Test nothing happens with the wrong message-type.""" + dhcp_watcher = dhcp.DHCPWatcher( + hass, [{"domain": "mock-domain", "hostname": "nomatch*"}] + ) + + packet = Ether(RAW_DHCP_REQUEST) + + packet[DHCP].options = [ + ("message-type", 4), + ("max_dhcp_size", 1500), + ("requested_addr", "192.168.210.56"), + ("server_id", "192.168.208.1"), + ("param_req_list", [1, 3, 28, 6]), + ("hostname", b"connect"), + ] + + with patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: + dhcp_watcher.handle_dhcp_packet(packet) + + assert len(mock_init.mock_calls) == 0 + + +async def test_dhcp_invalid_hostname(hass): + """Test we ignore invalid hostnames.""" + dhcp_watcher = dhcp.DHCPWatcher( + hass, [{"domain": "mock-domain", "hostname": "nomatch*"}] + ) + + packet = Ether(RAW_DHCP_REQUEST) + + packet[DHCP].options = [ + ("message-type", 3), + ("max_dhcp_size", 1500), + ("requested_addr", "192.168.210.56"), + ("server_id", "192.168.208.1"), + ("param_req_list", [1, 3, 28, 6]), + ("hostname", "connect"), + ] + + with patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: + dhcp_watcher.handle_dhcp_packet(packet) + + assert len(mock_init.mock_calls) == 0 + + +async def test_dhcp_missing_hostname(hass): + """Test we ignore missing hostnames.""" + dhcp_watcher = dhcp.DHCPWatcher( + hass, [{"domain": "mock-domain", "hostname": "nomatch*"}] + ) + + packet = Ether(RAW_DHCP_REQUEST) + + packet[DHCP].options = [ + ("message-type", 3), + ("max_dhcp_size", 1500), + ("requested_addr", "192.168.210.56"), + ("server_id", "192.168.208.1"), + ("param_req_list", [1, 3, 28, 6]), + ("hostname", None), + ] + + with patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: + dhcp_watcher.handle_dhcp_packet(packet) + + assert len(mock_init.mock_calls) == 0 + + +async def test_dhcp_invalid_option(hass): + """Test we ignore invalid hostname option.""" + dhcp_watcher = dhcp.DHCPWatcher( + hass, [{"domain": "mock-domain", "hostname": "nomatch*"}] + ) + + packet = Ether(RAW_DHCP_REQUEST) + + packet[DHCP].options = [ + ("message-type", 3), + ("max_dhcp_size", 1500), + ("requested_addr", "192.168.208.55"), + ("server_id", "192.168.208.1"), + ("param_req_list", [1, 3, 28, 6]), + ("hostname"), + ] + + with patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: + dhcp_watcher.handle_dhcp_packet(packet) + + assert len(mock_init.mock_calls) == 0 + + +async def test_setup_and_stop(hass): + """Test we can setup and stop.""" + + assert await async_setup_component( + hass, + dhcp.DOMAIN, + {}, + ) + await hass.async_block_till_done() + + wait_event = threading.Event() + + def _sniff_wait(): + wait_event.wait() + + with patch("homeassistant.components.dhcp.sniff", _sniff_wait): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + wait_event.set() + + +async def test_setup_fails(hass): + """Test we handle sniff setup failing.""" + + assert await async_setup_component( + hass, + dhcp.DOMAIN, + {}, + ) + await hass.async_block_till_done() + + wait_event = threading.Event() + + with patch("homeassistant.components.dhcp.sniff", side_effect=Scapy_Exception): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + wait_event.set() diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index b5ba206f908..874fd5df29a 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -82,7 +82,7 @@ async def test_user_has_confirmation(hass, discovery_flow_conf): assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY -@pytest.mark.parametrize("source", ["discovery", "mqtt", "ssdp", "zeroconf"]) +@pytest.mark.parametrize("source", ["discovery", "mqtt", "ssdp", "zeroconf", "dhcp"]) async def test_discovery_single_instance(hass, discovery_flow_conf, source): """Test we not allow duplicates.""" flow = config_entries.HANDLERS["test"]() @@ -96,7 +96,7 @@ async def test_discovery_single_instance(hass, discovery_flow_conf, source): assert result["reason"] == "single_instance_allowed" -@pytest.mark.parametrize("source", ["discovery", "mqtt", "ssdp", "zeroconf"]) +@pytest.mark.parametrize("source", ["discovery", "mqtt", "ssdp", "zeroconf", "dhcp"]) async def test_discovery_confirmation(hass, discovery_flow_conf, source): """Test we ask for confirmation via discovery.""" flow = config_entries.HANDLERS["test"]() diff --git a/tests/test_loader.py b/tests/test_loader.py index 00c6e2b0c20..c1c27f56cb7 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -172,6 +172,11 @@ def test_integration_properties(hass): "requirements": ["test-req==1.0.0"], "zeroconf": ["_hue._tcp.local."], "homekit": {"models": ["BSB002"]}, + "dhcp": [ + {"hostname": "tesla_*", "macaddress": "4CFCAA*"}, + {"hostname": "tesla_*", "macaddress": "044EAF*"}, + {"hostname": "tesla_*", "macaddress": "98ED5C*"}, + ], "ssdp": [ { "manufacturer": "Royal Philips Electronics", @@ -190,6 +195,11 @@ def test_integration_properties(hass): assert integration.domain == "hue" assert integration.homekit == {"models": ["BSB002"]} assert integration.zeroconf == ["_hue._tcp.local."] + assert integration.dhcp == [ + {"hostname": "tesla_*", "macaddress": "4CFCAA*"}, + {"hostname": "tesla_*", "macaddress": "044EAF*"}, + {"hostname": "tesla_*", "macaddress": "98ED5C*"}, + ] assert integration.ssdp == [ { "manufacturer": "Royal Philips Electronics", @@ -220,6 +230,7 @@ def test_integration_properties(hass): assert integration.is_built_in is False assert integration.homekit is None assert integration.zeroconf is None + assert integration.dhcp is None assert integration.ssdp is None assert integration.mqtt is None @@ -238,6 +249,7 @@ def test_integration_properties(hass): assert integration.is_built_in is False assert integration.homekit is None assert integration.zeroconf == [{"type": "_hue._tcp.local.", "name": "hue*"}] + assert integration.dhcp is None assert integration.ssdp is None @@ -295,6 +307,30 @@ def _get_test_integration_with_zeroconf_matcher(hass, name, config_flow): ) +def _get_test_integration_with_dhcp_matcher(hass, name, config_flow): + """Return a generated test integration with a dhcp matcher.""" + return loader.Integration( + hass, + f"homeassistant.components.{name}", + None, + { + "name": name, + "domain": name, + "config_flow": config_flow, + "dependencies": [], + "requirements": [], + "zeroconf": [], + "dhcp": [ + {"hostname": "tesla_*", "macaddress": "4CFCAA*"}, + {"hostname": "tesla_*", "macaddress": "044EAF*"}, + {"hostname": "tesla_*", "macaddress": "98ED5C*"}, + ], + "homekit": {"models": [name]}, + "ssdp": [{"manufacturer": name, "modelName": name}], + }, + ) + + 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) @@ -347,6 +383,23 @@ async def test_get_zeroconf(hass): ] +async def test_get_dhcp(hass): + """Verify that custom components with dhcp are found.""" + test_1_integration = _get_test_integration_with_dhcp_matcher(hass, "test_1", True) + + with patch("homeassistant.loader.async_get_custom_components") as mock_get: + mock_get.return_value = { + "test_1": test_1_integration, + } + dhcp = await loader.async_get_dhcp(hass) + dhcp_for_domain = [entry for entry in dhcp if entry["domain"] == "test_1"] + assert dhcp_for_domain == [ + {"domain": "test_1", "hostname": "tesla_*", "macaddress": "4CFCAA*"}, + {"domain": "test_1", "hostname": "tesla_*", "macaddress": "044EAF*"}, + {"domain": "test_1", "hostname": "tesla_*", "macaddress": "98ED5C*"}, + ] + + async def test_get_homekit(hass): """Verify that custom components with homekit are found.""" test_1_integration = _get_test_integration(hass, "test_1", True) diff --git a/tests/test_requirements.py b/tests/test_requirements.py index bc206a136c2..5f74e504de8 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -244,3 +244,26 @@ async def test_discovery_requirements_zeroconf(hass, partial_manifest): assert len(mock_process.mock_calls) == 2 # zeroconf also depends on http assert mock_process.mock_calls[0][1][2] == zeroconf.requirements + + +async def test_discovery_requirements_dhcp(hass): + """Test that we load dhcp discovery requirements.""" + hass.config.skip_pip = False + dhcp = await loader.async_get_integration(hass, "dhcp") + + mock_integration( + hass, + MockModule( + "comp", + partial_manifest={ + "dhcp": [{"hostname": "somfy_*", "macaddress": "B8B7F1*"}] + }, + ), + ) + with patch( + "homeassistant.requirements.async_process_requirements", + ) as mock_process: + await async_get_integration_with_requirements(hass, "comp") + + assert len(mock_process.mock_calls) == 1 # dhcp does not depend on http + assert mock_process.mock_calls[0][1][2] == dhcp.requirements From e0c8b1aab6f9c8d02a45176e0733a8ed671fa638 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 14 Jan 2021 09:12:32 +0100 Subject: [PATCH 207/507] Remove from_state from alarm device triggers (#45127) --- .../components/alarm_control_panel/device_trigger.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/device_trigger.py b/homeassistant/components/alarm_control_panel/device_trigger.py index cb07ff35e96..bb5d82c52b1 100644 --- a/homeassistant/components/alarm_control_panel/device_trigger.py +++ b/homeassistant/components/alarm_control_panel/device_trigger.py @@ -23,7 +23,6 @@ from homeassistant.const import ( STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMING, STATE_ALARM_DISARMED, - STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant @@ -143,13 +142,10 @@ async def async_attach_trigger( from_state = STATE_ALARM_DISARMED to_state = STATE_ALARM_ARMING elif config[CONF_TYPE] == "armed_home": - from_state = STATE_ALARM_PENDING or STATE_ALARM_ARMING to_state = STATE_ALARM_ARMED_HOME elif config[CONF_TYPE] == "armed_away": - from_state = STATE_ALARM_PENDING or STATE_ALARM_ARMING to_state = STATE_ALARM_ARMED_AWAY elif config[CONF_TYPE] == "armed_night": - from_state = STATE_ALARM_PENDING or STATE_ALARM_ARMING to_state = STATE_ALARM_ARMED_NIGHT state_config = { From 2ac658d257f026bab76fb9cada97064561de5ba7 Mon Sep 17 00:00:00 2001 From: On Freund Date: Thu, 14 Jan 2021 10:39:11 +0200 Subject: [PATCH 208/507] Get A/V info for Onkyo receivers (#34477) * Get A/V info for Onkyo receivers * Fix lint errors * Remove blank line * Trigger CI --- .../components/onkyo/media_player.py | 91 ++++++++++++++++--- 1 file changed, 80 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index 2cd4faaf314..c127d4150c5 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -93,6 +93,9 @@ TIMEOUT_MESSAGE = "Timeout waiting for response." ATTR_HDMI_OUTPUT = "hdmi_output" ATTR_PRESET = "preset" +ATTR_AUDIO_INFORMATION = "audio_information" +ATTR_VIDEO_INFORMATION = "video_information" +ATTR_VIDEO_OUT = "video_out" ACCEPTED_VALUES = [ "no", @@ -115,6 +118,22 @@ ONKYO_SELECT_OUTPUT_SCHEMA = vol.Schema( SERVICE_SELECT_HDMI_OUTPUT = "onkyo_select_hdmi_output" +def _parse_onkyo_tuple(tup): + """Parse a tuple returned from the eiscp library.""" + if len(tup) < 2: + return None + + if isinstance(tup[1], str): + return tup[1].split(",") + + return tup[1] + + +def _tuple_get(tup, index, default=None): + """Return a tuple item at index or a default value if it doesn't exist.""" + return (tup[index : index + 1] or [default])[0] + + def determine_zones(receiver): """Determine what zones are available for the receiver.""" out = {"zone2": False, "zone3": False} @@ -229,9 +248,17 @@ class OnkyoDevice(MediaPlayerEntity): self._muted = False self._volume = 0 self._pwstate = STATE_OFF - self._name = ( - name or f"{receiver.info['model_name']}_{receiver.info['identifier']}" - ) + if name: + # not discovered + self._name = name + self._unique_id = None + else: + # discovered + self._unique_id = ( + f"{receiver.info['model_name']}_{receiver.info['identifier']}" + ) + self._name = self._unique_id + self._max_volume = max_volume self._receiver_max_volume = receiver_max_volume self._current_source = None @@ -265,6 +292,10 @@ class OnkyoDevice(MediaPlayerEntity): self._pwstate = STATE_ON else: self._pwstate = STATE_OFF + self._attributes.pop(ATTR_AUDIO_INFORMATION, None) + self._attributes.pop(ATTR_VIDEO_INFORMATION, None) + self._attributes.pop(ATTR_PRESET, None) + self._attributes.pop(ATTR_VIDEO_OUT, None) return volume_raw = self.command("volume query") @@ -278,20 +309,19 @@ class OnkyoDevice(MediaPlayerEntity): else: hdmi_out_raw = [] preset_raw = self.command("preset query") + audio_information_raw = self.command("audio-information query") + video_information_raw = self.command("video-information query") if not (volume_raw and mute_raw and current_source_raw): return - # eiscp can return string or tuple. Make everything tuples. - if isinstance(current_source_raw[1], str): - current_source_tuples = (current_source_raw[0], (current_source_raw[1],)) - else: - current_source_tuples = current_source_raw + sources = _parse_onkyo_tuple(current_source_raw) - for source in current_source_tuples[1]: + for source in sources: if source in self._source_mapping: self._current_source = self._source_mapping[source] break - self._current_source = "_".join(current_source_tuples[1]) + self._current_source = "_".join(sources) + if preset_raw and self._current_source.lower() == "radio": self._attributes[ATTR_PRESET] = preset_raw[1] elif ATTR_PRESET in self._attributes: @@ -303,12 +333,20 @@ class OnkyoDevice(MediaPlayerEntity): self._receiver_max_volume * self._max_volume / 100 ) + self._parse_audio_inforamtion(audio_information_raw) + self._parse_video_inforamtion(video_information_raw) + if not hdmi_out_raw: return - self._attributes["video_out"] = ",".join(hdmi_out_raw[1]) + self._attributes[ATTR_VIDEO_OUT] = ",".join(hdmi_out_raw[1]) if hdmi_out_raw[1] == "N/A": self._hdmi_out_supported = False + @property + def unique_id(self): + """Return unique ID for this device.""" + return self._unique_id + @property def name(self): """Return the name of the device.""" @@ -402,6 +440,37 @@ class OnkyoDevice(MediaPlayerEntity): """Set hdmi-out.""" self.command(f"hdmi-output-selector={output}") + def _parse_audio_inforamtion(self, audio_information_raw): + values = _parse_onkyo_tuple(audio_information_raw) + if values: + info = { + "format": _tuple_get(values, 1), + "input_frequency": _tuple_get(values, 2), + "input_channels": _tuple_get(values, 3), + "listening_mode": _tuple_get(values, 4), + "output_channels": _tuple_get(values, 5), + "output_frequency": _tuple_get(values, 6), + } + self._attributes[ATTR_AUDIO_INFORMATION] = info + else: + self._attributes.pop(ATTR_AUDIO_INFORMATION, None) + + def _parse_video_inforamtion(self, video_information_raw): + values = _parse_onkyo_tuple(video_information_raw) + if values: + info = { + "input_resolution": _tuple_get(values, 1), + "input_color_schema": _tuple_get(values, 2), + "input_color_depth": _tuple_get(values, 3), + "output_resolution": _tuple_get(values, 5), + "output_color_schema": _tuple_get(values, 6), + "output_color_depth": _tuple_get(values, 7), + "picture_mode": _tuple_get(values, 8), + } + self._attributes[ATTR_VIDEO_INFORMATION] = info + else: + self._attributes.pop(ATTR_VIDEO_INFORMATION, None) + class OnkyoDeviceZone(OnkyoDevice): """Representation of an Onkyo device's extra zone.""" From 23a73dc5b17fe00f07b6a638bbdbcd95464ead28 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 13 Jan 2021 22:44:01 -1000 Subject: [PATCH 209/507] Mark YAML support for DoorBird deprecated (#45139) --- homeassistant/components/doorbird/__init__.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 1f7e02e8569..1dc5bf56c86 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -58,11 +58,14 @@ DEVICE_SCHEMA = vol.Schema( ) CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - {vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [DEVICE_SCHEMA])} - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + {vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [DEVICE_SCHEMA])} + ) + }, + ), extra=vol.ALLOW_EXTRA, ) From 4efe6762c40dcf35fb00a079f2cb79a38ccb3159 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 13 Jan 2021 22:45:32 -1000 Subject: [PATCH 210/507] Remove YAML support from harmony (#45140) --- .../components/harmony/config_flow.py | 13 ----- homeassistant/components/harmony/remote.py | 55 +------------------ homeassistant/components/harmony/util.py | 23 +------- tests/components/harmony/test_config_flow.py | 43 --------------- 4 files changed, 3 insertions(+), 131 deletions(-) diff --git a/homeassistant/components/harmony/config_flow.py b/homeassistant/components/harmony/config_flow.py index 6d5adabe235..e01febbef43 100644 --- a/homeassistant/components/harmony/config_flow.py +++ b/homeassistant/components/harmony/config_flow.py @@ -129,19 +129,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): }, ) - async def async_step_import(self, validated_input): - """Handle import.""" - await self.async_set_unique_id( - validated_input[UNIQUE_ID], raise_on_progress=False - ) - self._abort_if_unique_id_configured() - - # Everything was validated in remote async_setup_platform - # all we do now is create. - return await self._async_create_entry_from_valid_input( - validated_input, validated_input - ) - @staticmethod @callback def async_get_options_flow(config_entry): diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index b9205a4befb..8409983789b 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -12,12 +12,10 @@ from homeassistant.components.remote import ( ATTR_HOLD_SECS, ATTR_NUM_REPEATS, DEFAULT_DELAY_SECS, - PLATFORM_SCHEMA, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -36,15 +34,8 @@ from .const import ( PREVIOUS_ACTIVE_ACTIVITY, SERVICE_CHANGE_CHANNEL, SERVICE_SYNC, - UNIQUE_ID, ) from .subscriber import HarmonyCallback -from .util import ( - find_best_name_for_remote, - find_matching_config_entries_for_host, - find_unique_id_for_remote, - get_harmony_client_if_available, -) _LOGGER = logging.getLogger(__name__) @@ -53,18 +44,6 @@ PARALLEL_UPDATES = 0 ATTR_CHANNEL = "channel" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(ATTR_ACTIVITY): cv.string, - vol.Required(CONF_NAME): cv.string, - vol.Optional(ATTR_DELAY_SECS, default=DEFAULT_DELAY_SECS): vol.Coerce(float), - vol.Required(CONF_HOST): cv.string, - # The client ignores port so lets not confuse the user by pretenting we do anything with this - }, - extra=vol.ALLOW_EXTRA, -) - - HARMONY_SYNC_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) HARMONY_CHANGE_CHANNEL_SCHEMA = vol.Schema( @@ -75,36 +54,6 @@ HARMONY_CHANGE_CHANNEL_SCHEMA = vol.Schema( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Harmony platform.""" - - if discovery_info: - # Now handled by ssdp in the config flow - return - - if find_matching_config_entries_for_host(hass, config[CONF_HOST]): - return - - # We do the validation to verify we can connect - # so we can raise PlatformNotReady to force - # a retry so we can avoid a scenario where the config - # entry cannot be created via import because hub - # is not yet ready. - harmony = await get_harmony_client_if_available(config[CONF_HOST]) - if not harmony: - raise PlatformNotReady - - validated_config = config.copy() - validated_config[UNIQUE_ID] = find_unique_id_for_remote(harmony) - validated_config[CONF_NAME] = find_best_name_for_remote(config, harmony) - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=validated_config - ) - ) - - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities ): diff --git a/homeassistant/components/harmony/util.py b/homeassistant/components/harmony/util.py index b0a16004065..3f126f22f3c 100644 --- a/homeassistant/components/harmony/util.py +++ b/homeassistant/components/harmony/util.py @@ -2,9 +2,7 @@ import aioharmony.exceptions as harmony_exceptions from aioharmony.harmonyapi import HarmonyAPI -from homeassistant.const import CONF_HOST, CONF_NAME - -from .const import DOMAIN +from homeassistant.const import CONF_NAME def find_unique_id_for_remote(harmony: HarmonyAPI): @@ -41,22 +39,3 @@ async def get_harmony_client_if_available(ip_address: str): await harmony.close() return harmony - - -def find_matching_config_entries_for_host(hass, host): - """Search existing config entries for one matching the host.""" - for entry in hass.config_entries.async_entries(DOMAIN): - if entry.data[CONF_HOST] == host: - return entry - return None - - -def list_names_from_hublist(hub_list): - """Extract the name key value from a hub list of names.""" - if not hub_list: - return [] - return [ - element["name"] - for element in hub_list - if element.get("name") and element.get("id") != -1 - ] diff --git a/tests/components/harmony/test_config_flow.py b/tests/components/harmony/test_config_flow.py index e5d0c6f0570..52ef71fc8bc 100644 --- a/tests/components/harmony/test_config_flow.py +++ b/tests/components/harmony/test_config_flow.py @@ -49,49 +49,6 @@ async def test_user_form(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_import(hass): - """Test we get the form with import source.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - - harmonyapi = _get_mock_harmonyapi(connect=True) - with patch( - "homeassistant.components.harmony.util.HarmonyAPI", - return_value=harmonyapi, - ), patch( - "homeassistant.components.harmony.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.harmony.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - "host": "1.2.3.4", - "name": "friend", - "activity": "Watch TV", - "delay_secs": 0.9, - "unique_id": "555234534543", - }, - ) - await hass.async_block_till_done() - - assert result["result"].unique_id == "555234534543" - assert result["type"] == "create_entry" - assert result["title"] == "friend" - assert result["data"] == { - "host": "1.2.3.4", - "name": "friend", - "activity": "Watch TV", - "delay_secs": 0.9, - } - # It is not possible to import options at this time - # so they end up in the config entry data and are - # used a fallback when they are not in options - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - async def test_form_ssdp(hass): """Test we get the form with ssdp source.""" await setup.async_setup_component(hass, "persistent_notification", {}) From 4bca9596ee5d0e5af7a4faa1d896fe7e88ecc849 Mon Sep 17 00:00:00 2001 From: Corbeno Date: Thu, 14 Jan 2021 04:31:37 -0600 Subject: [PATCH 211/507] Rework Proxmoxve to use a DataUpdateCoordinator (#45068) Co-authored-by: Paulus Schoutsen --- .../components/proxmoxve/__init__.py | 235 ++++++++++++++---- .../components/proxmoxve/binary_sensor.py | 159 ++++++------ 2 files changed, 264 insertions(+), 130 deletions(-) diff --git a/homeassistant/components/proxmoxve/__init__.py b/homeassistant/components/proxmoxve/__init__.py index 0919feb15e3..2f42ca8fe9e 100644 --- a/homeassistant/components/proxmoxve/__init__.py +++ b/homeassistant/components/proxmoxve/__init__.py @@ -1,9 +1,10 @@ """Support for Proxmox VE.""" -from enum import Enum +from datetime import timedelta import logging from proxmoxer import ProxmoxAPI from proxmoxer.backends.https import AuthenticationError +from proxmoxer.core import ResourceException from requests.exceptions import SSLError import voluptuous as vol @@ -14,11 +15,14 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) -_LOGGER = logging.getLogger(__name__) - - +PLATFORMS = ["binary_sensor"] DOMAIN = "proxmoxve" PROXMOX_CLIENTS = "proxmox_clients" CONF_REALM = "realm" @@ -27,9 +31,17 @@ CONF_NODES = "nodes" CONF_VMS = "vms" CONF_CONTAINERS = "containers" +COORDINATOR = "coordinator" +API_DATA = "api_data" + DEFAULT_PORT = 8006 DEFAULT_REALM = "pam" DEFAULT_VERIFY_SSL = True +TYPE_VM = 0 +TYPE_CONTAINER = 1 +UPDATE_INTERVAL = 60 + +_LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( { @@ -71,52 +83,191 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass, config): - """Set up the component.""" +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the platform.""" + hass.data.setdefault(DOMAIN, {}) - # Create API Clients for later use - hass.data[PROXMOX_CLIENTS] = {} - for entry in config[DOMAIN]: - host = entry[CONF_HOST] - port = entry[CONF_PORT] - user = entry[CONF_USERNAME] - realm = entry[CONF_REALM] - password = entry[CONF_PASSWORD] - verify_ssl = entry[CONF_VERIFY_SSL] + def build_client() -> ProxmoxAPI: + """Build the Proxmox client connection.""" + hass.data[PROXMOX_CLIENTS] = {} + for entry in config[DOMAIN]: + host = entry[CONF_HOST] + port = entry[CONF_PORT] + user = entry[CONF_USERNAME] + realm = entry[CONF_REALM] + password = entry[CONF_PASSWORD] + verify_ssl = entry[CONF_VERIFY_SSL] - try: - # Construct an API client with the given data for the given host - proxmox_client = ProxmoxClient( - host, port, user, realm, password, verify_ssl + try: + # Construct an API client with the given data for the given host + proxmox_client = ProxmoxClient( + host, port, user, realm, password, verify_ssl + ) + proxmox_client.build_client() + except AuthenticationError: + _LOGGER.warning( + "Invalid credentials for proxmox instance %s:%d", host, port + ) + continue + except SSLError: + _LOGGER.error( + 'Unable to verify proxmox server SSL. Try using "verify_ssl: false"' + ) + continue + + return proxmox_client + + proxmox_client = await hass.async_add_executor_job(build_client) + + async def async_update_data() -> dict: + """Fetch data from API endpoint.""" + + proxmox = proxmox_client.get_api_client() + + def poll_api() -> dict: + data = {} + + for host_config in config[DOMAIN]: + host_name = host_config["host"] + + data[host_name] = {} + + for node_config in host_config["nodes"]: + node_name = node_config["node"] + data[host_name][node_name] = {} + + for vm_id in node_config["vms"]: + data[host_name][node_name][vm_id] = {} + + vm_status = call_api_container_vm( + proxmox, node_name, vm_id, TYPE_VM + ) + + if vm_status is None: + _LOGGER.warning("Vm/Container %s unable to be found", vm_id) + data[host_name][node_name][vm_id] = None + continue + + data[host_name][node_name][vm_id] = parse_api_container_vm( + vm_status + ) + + for container_id in node_config["containers"]: + data[host_name][node_name][container_id] = {} + + container_status = call_api_container_vm( + proxmox, node_name, container_id, TYPE_CONTAINER + ) + + if container_status is None: + _LOGGER.error( + "Vm/Container %s unable to be found", container_id + ) + data[host_name][node_name][container_id] = None + continue + + data[host_name][node_name][ + container_id + ] = parse_api_container_vm(container_status) + + return data + + return await hass.async_add_executor_job(poll_api) + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="proxmox_coordinator", + update_method=async_update_data, + update_interval=timedelta(seconds=UPDATE_INTERVAL), + ) + + hass.data[DOMAIN][COORDINATOR] = coordinator + + # Fetch initial data + await coordinator.async_refresh() + + for component in PLATFORMS: + await hass.async_create_task( + hass.helpers.discovery.async_load_platform( + component, DOMAIN, {"config": config}, config ) - proxmox_client.build_client() - except AuthenticationError: - _LOGGER.warning( - "Invalid credentials for proxmox instance %s:%d", host, port - ) - continue - except SSLError: - _LOGGER.error( - 'Unable to verify proxmox server SSL. Try using "verify_ssl: false"' - ) - continue - - hass.data[PROXMOX_CLIENTS][f"{host}:{port}"] = proxmox_client - - if hass.data[PROXMOX_CLIENTS]: - hass.helpers.discovery.load_platform( - "binary_sensor", DOMAIN, {"entries": config[DOMAIN]}, config ) - return True - return False + return True -class ProxmoxItemType(Enum): - """Represents the different types of machines in Proxmox.""" +def parse_api_container_vm(status): + """Get the container or vm api data and return it formatted in a dictionary. - qemu = 0 - lxc = 1 + It is implemented in this way to allow for more data to be added for sensors + in the future. + """ + + return {"status": status["status"], "name": status["name"]} + + +def call_api_container_vm(proxmox, node_name, vm_id, machine_type): + """Make proper api calls.""" + status = None + + try: + if machine_type == TYPE_VM: + status = proxmox.nodes(node_name).qemu(vm_id).status.current.get() + elif machine_type == TYPE_CONTAINER: + status = proxmox.nodes(node_name).lxc(vm_id).status.current.get() + except ResourceException: + return None + + return status + + +class ProxmoxEntity(CoordinatorEntity): + """Represents any entity created for the Proxmox VE platform.""" + + def __init__( + self, + coordinator: DataUpdateCoordinator, + unique_id, + name, + icon, + host_name, + node_name, + vm_id=None, + ): + """Initialize the Proxmox entity.""" + super().__init__(coordinator) + + self.coordinator = coordinator + self._unique_id = unique_id + self._name = name + self._host_name = host_name + self._icon = icon + self._available = True + self._node_name = node_name + self._vm_id = vm_id + + self._state = None + + @property + def unique_id(self) -> str: + """Return the unique ID for this sensor.""" + return self._unique_id + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name + + @property + def icon(self) -> str: + """Return the mdi icon of the entity.""" + return self._icon + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self.coordinator.last_update_success and self._available class ProxmoxClient: diff --git a/homeassistant/components/proxmoxve/binary_sensor.py b/homeassistant/components/proxmoxve/binary_sensor.py index 698a2c35ae1..014766b532e 100644 --- a/homeassistant/components/proxmoxve/binary_sensor.py +++ b/homeassistant/components/proxmoxve/binary_sensor.py @@ -1,112 +1,95 @@ """Binary sensor to read Proxmox VE data.""" import logging -from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.const import ATTR_ATTRIBUTION, CONF_HOST, CONF_PORT +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import CONF_CONTAINERS, CONF_NODES, CONF_VMS, PROXMOX_CLIENTS, ProxmoxItemType +from . import COORDINATOR, DOMAIN, ProxmoxEntity -ATTRIBUTION = "Data provided by Proxmox VE" _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the sensor platform.""" +async def async_setup_platform(hass, config, add_entities, discovery_info=None): + """Set up binary sensors.""" + if discovery_info is None: + return + + coordinator = hass.data[DOMAIN][COORDINATOR] sensors = [] - for entry in discovery_info["entries"]: - port = entry[CONF_PORT] + for host_config in discovery_info["config"][DOMAIN]: + host_name = host_config["host"] - for node in entry[CONF_NODES]: - for virtual_machine in node[CONF_VMS]: - sensors.append( - ProxmoxBinarySensor( - hass.data[PROXMOX_CLIENTS][f"{entry[CONF_HOST]}:{port}"], - node["node"], - ProxmoxItemType.qemu, - virtual_machine, - ) + for node_config in host_config["nodes"]: + node_name = node_config["node"] + + for vm_id in node_config["vms"]: + coordinator_data = coordinator.data[host_name][node_name][vm_id] + + # unfound vm case + if coordinator_data is None: + continue + + vm_name = coordinator_data["name"] + vm_status = create_binary_sensor( + coordinator, host_name, node_name, vm_id, vm_name ) + sensors.append(vm_status) - for container in node[CONF_CONTAINERS]: - sensors.append( - ProxmoxBinarySensor( - hass.data[PROXMOX_CLIENTS][f"{entry[CONF_HOST]}:{port}"], - node["node"], - ProxmoxItemType.lxc, - container, - ) + for container_id in node_config["containers"]: + coordinator_data = coordinator.data[host_name][node_name][container_id] + + # unfound container case + if coordinator_data is None: + continue + + container_name = coordinator_data["name"] + container_status = create_binary_sensor( + coordinator, host_name, node_name, container_id, container_name ) + sensors.append(container_status) - add_entities(sensors, True) + add_entities(sensors) -class ProxmoxBinarySensor(BinarySensorEntity): +def create_binary_sensor(coordinator, host_name, node_name, vm_id, name): + """Create a binary sensor based on the given data.""" + return ProxmoxBinarySensor( + coordinator=coordinator, + unique_id=f"proxmox_{node_name}_{vm_id}_running", + name=f"{node_name}_{name}_running", + icon="", + host_name=host_name, + node_name=node_name, + vm_id=vm_id, + ) + + +class ProxmoxBinarySensor(ProxmoxEntity): """A binary sensor for reading Proxmox VE data.""" - def __init__(self, proxmox_client, item_node, item_type, item_id): - """Initialize the binary sensor.""" - self._proxmox_client = proxmox_client - self._item_node = item_node - self._item_type = item_type - self._item_id = item_id - - self._vmname = None - self._name = None + def __init__( + self, + coordinator: DataUpdateCoordinator, + unique_id, + name, + icon, + host_name, + node_name, + vm_id, + ): + """Create the binary sensor for vms or containers.""" + super().__init__( + coordinator, unique_id, name, icon, host_name, node_name, vm_id + ) self._state = None @property - def name(self): - """Return the name of the entity.""" - return self._name - - @property - def is_on(self): - """Return true if VM/container is running.""" - return self._state - - @property - def device_state_attributes(self): - """Return device attributes of the entity.""" - return { - "node": self._item_node, - "vmid": self._item_id, - "vmname": self._vmname, - "type": self._item_type.name, - ATTR_ATTRIBUTION: ATTRIBUTION, - } - - def update(self): - """Check if the VM/Container is running.""" - item = self.poll_item() - - if item is None: - _LOGGER.warning("Failed to poll VM/container %s", self._item_id) - return - - self._state = item["status"] == "running" - - def poll_item(self): - """Find the VM/Container with the set item_id.""" - items = ( - self._proxmox_client.get_api_client() - .nodes(self._item_node) - .get(self._item_type.name) - ) - item = next( - (item for item in items if item["vmid"] == str(self._item_id)), None - ) - - if item is None: - _LOGGER.warning("Couldn't find VM/Container with the ID %s", self._item_id) - return None - - if self._vmname is None: - self._vmname = item["name"] - - if self._name is None: - self._name = f"{self._item_node} {self._vmname} running" - - return item + def state(self): + """Return the state of the binary sensor.""" + data = self.coordinator.data[self._host_name][self._node_name][self._vm_id] + if data["status"] == "running": + return STATE_ON + return STATE_OFF From ab518a7755f882c9d5ece877d67400d68644c5a4 Mon Sep 17 00:00:00 2001 From: unaiur Date: Thu, 14 Jan 2021 11:33:02 +0100 Subject: [PATCH 212/507] Migrate to maxcube-api 0.3.0 version (#45126) Upgrade maxcube-api to solve bugs fixed in last 3 years. --- homeassistant/components/maxcube/binary_sensor.py | 2 +- homeassistant/components/maxcube/climate.py | 10 +++++----- homeassistant/components/maxcube/manifest.json | 2 +- requirements_all.txt | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/maxcube/binary_sensor.py b/homeassistant/components/maxcube/binary_sensor.py index 06b53456973..376076352a6 100644 --- a/homeassistant/components/maxcube/binary_sensor.py +++ b/homeassistant/components/maxcube/binary_sensor.py @@ -16,7 +16,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): name = f"{cube.room_by_id(device.room_id).name} {device.name}" # Only add Window Shutters - if cube.is_windowshutter(device): + if device.is_windowshutter(): devices.append(MaxCubeShutter(handler, name, device.rf_address)) if devices: diff --git a/homeassistant/components/maxcube/climate.py b/homeassistant/components/maxcube/climate.py index e222784ca57..c17cc988c1d 100644 --- a/homeassistant/components/maxcube/climate.py +++ b/homeassistant/components/maxcube/climate.py @@ -70,7 +70,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for device in cube.devices: name = f"{cube.room_by_id(device.room_id).name} {device.name}" - if cube.is_thermostat(device) or cube.is_wallthermostat(device): + if device.is_thermostat() or device.is_wallthermostat(): devices.append(MaxCubeClimate(handler, name, device.rf_address)) if devices: @@ -180,11 +180,11 @@ class MaxCubeClimate(ClimateEntity): device = cube.device_by_rf(self._rf_address) valve = 0 - if cube.is_thermostat(device): + if device.is_thermostat(): valve = device.valve_position - elif cube.is_wallthermostat(device): + elif device.is_wallthermostat(): for device in cube.devices_by_room(cube.room_by_id(device.room_id)): - if cube.is_thermostat(device) and device.valve_position > 0: + if device.is_thermostat() and device.valve_position > 0: valve = device.valve_position break else: @@ -287,7 +287,7 @@ class MaxCubeClimate(ClimateEntity): cube = self._cubehandle.cube device = cube.device_by_rf(self._rf_address) - if not cube.is_thermostat(device): + if not device.is_thermostat(): return {} return {ATTR_VALVE_POSITION: device.valve_position} diff --git a/homeassistant/components/maxcube/manifest.json b/homeassistant/components/maxcube/manifest.json index 0aae92c2079..e6badb254f7 100644 --- a/homeassistant/components/maxcube/manifest.json +++ b/homeassistant/components/maxcube/manifest.json @@ -2,6 +2,6 @@ "domain": "maxcube", "name": "eQ-3 MAX!", "documentation": "https://www.home-assistant.io/integrations/maxcube", - "requirements": ["maxcube-api==0.1.0"], + "requirements": ["maxcube-api==0.3.0"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index c11b35312b1..bcf061fc1aa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -916,7 +916,7 @@ magicseaweed==1.0.3 matrix-client==0.3.2 # homeassistant.components.maxcube -maxcube-api==0.1.0 +maxcube-api==0.3.0 # homeassistant.components.mythicbeastsdns mbddns==0.1.2 From 3800a4feee0e2c4f185806c72d7594719293de7f Mon Sep 17 00:00:00 2001 From: bchastain Date: Thu, 14 Jan 2021 12:47:48 -0600 Subject: [PATCH 213/507] Add pressure to OWM forecast data (#43843) --- homeassistant/components/openweathermap/const.py | 3 +++ .../components/openweathermap/weather_update_coordinator.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index 2eb23e23861..c70afa9cab0 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -16,6 +16,7 @@ from homeassistant.components.weather import ( ATTR_CONDITION_WINDY_VARIANT, ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRESSURE, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, @@ -92,6 +93,7 @@ MONITORED_CONDITIONS = [ FORECAST_MONITORED_CONDITIONS = [ ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRESSURE, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, @@ -210,6 +212,7 @@ WEATHER_SENSOR_TYPES = { FORECAST_SENSOR_TYPES = { ATTR_FORECAST_CONDITION: {SENSOR_NAME: "Condition"}, ATTR_FORECAST_PRECIPITATION: {SENSOR_NAME: "Precipitation"}, + ATTR_FORECAST_PRESSURE: {SENSOR_NAME: "Pressure"}, ATTR_FORECAST_TEMP: { SENSOR_NAME: "Temperature", SENSOR_UNIT: TEMP_CELSIUS, diff --git a/homeassistant/components/openweathermap/weather_update_coordinator.py b/homeassistant/components/openweathermap/weather_update_coordinator.py index 605e6f9edc1..901ea029743 100644 --- a/homeassistant/components/openweathermap/weather_update_coordinator.py +++ b/homeassistant/components/openweathermap/weather_update_coordinator.py @@ -10,6 +10,7 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SUNNY, ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRESSURE, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, @@ -142,6 +143,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): ATTR_FORECAST_PRECIPITATION: self._calc_precipitation( entry.rain, entry.snow ), + ATTR_FORECAST_PRESSURE: entry.pressure.get("press"), ATTR_FORECAST_WIND_SPEED: entry.wind().get("speed"), ATTR_FORECAST_WIND_BEARING: entry.wind().get("deg"), ATTR_FORECAST_CONDITION: self._get_condition( From f047d04882e145928f1cfe73c04b5872e241b3bd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Jan 2021 20:02:01 +0100 Subject: [PATCH 214/507] Add filtering --- homeassistant/components/http/__init__.py | 6 +- .../components/http/security_filter.py | 51 ++++++++++++++++ tests/components/http/test_security_filter.py | 58 +++++++++++++++++++ 3 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/http/security_filter.py create mode 100644 tests/components/http/test_security_filter.py diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 9e47dd29a23..7f70d49f686 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -30,6 +30,7 @@ from .const import KEY_AUTHENTICATED, KEY_HASS, KEY_HASS_USER # noqa: F401 from .cors import setup_cors from .forwarded import async_setup_forwarded from .request_context import setup_request_context +from .security_filter import setup_security_filter from .static import CACHE_HEADERS, CachingStaticResource from .view import HomeAssistantView # noqa: F401 from .web_runner import HomeAssistantTCPSite @@ -296,7 +297,10 @@ class HomeAssistantHTTP: ) app[KEY_HASS] = hass - # Order matters, forwarded middleware needs to go first. + # Order matters, security filters middle ware needs to go first, + # forwarded middleware needs to go second. + setup_security_filter(app) + # Only register middleware if `use_x_forwarded_for` is enabled # and trusted proxies are provided if use_x_forwarded_for and trusted_proxies: diff --git a/homeassistant/components/http/security_filter.py b/homeassistant/components/http/security_filter.py new file mode 100644 index 00000000000..32ebcacfff4 --- /dev/null +++ b/homeassistant/components/http/security_filter.py @@ -0,0 +1,51 @@ +"""Middleware to add some basic security filtering to requests.""" +import logging +import re + +from aiohttp.web import HTTPBadRequest, middleware + +from homeassistant.core import callback + +_LOGGER = logging.getLogger(__name__) + +# mypy: allow-untyped-defs + +# fmt: off +FILTERS = re.compile( + r"(?:" + + # Common exploits + r"proc/self/environ" + r"|(<|%3C).*script.*(>|%3E)" + + # File Injections + r"|(\.\.//?)+" # ../../anywhere + r"|[a-zA-Z0-9_]=/([a-z0-9_.]//?)+" # .html?v=/.//test + + # SQL Injections + r"|union.*select.*\(" + r"|union.*all.*select.*" + r"|concat.*\(" + + r")", + flags=re.IGNORECASE, +) +# fmt: on + + +@callback +def setup_security_filter(app): + """Create security filter middleware for the app.""" + + @middleware + async def security_filter_middleware(request, handler): + """Process request and block commonly known exploit attempts.""" + if FILTERS.search(request.raw_path): + _LOGGER.warning( + "Filtered a potential harmful request to: %s", request.raw_path + ) + raise HTTPBadRequest + + return await handler(request) + + app.middlewares.append(security_filter_middleware) diff --git a/tests/components/http/test_security_filter.py b/tests/components/http/test_security_filter.py new file mode 100644 index 00000000000..8190c514603 --- /dev/null +++ b/tests/components/http/test_security_filter.py @@ -0,0 +1,58 @@ +"""Test security filter middleware.""" +from aiohttp import web +import pytest + +from homeassistant.components.http.security_filter import setup_security_filter + + +async def mock_handler(request): + """Return OK.""" + return web.Response(text="OK") + + +@pytest.mark.parametrize( + "request_path,request_params", + [ + ("/", {}), + ("/lovelace/dashboard", {}), + ("/frontend_latest/chunk.4c9e2d8dc10f77b885b0.js", {}), + ("/static/translations/en-f96a262a5a6eede29234dc45dc63abf2.json", {}), + ("/", {"test": "123"}), + ], +) +async def test_ok_requests(request_path, request_params, aiohttp_client): + """Test request paths that should not be filtered.""" + app = web.Application() + app.router.add_get("/{all:.*}", mock_handler) + + setup_security_filter(app) + + mock_api_client = await aiohttp_client(app) + resp = await mock_api_client.get(request_path, params=request_params) + + assert resp.status == 200 + assert await resp.text() == "OK" + + +@pytest.mark.parametrize( + "request_path,request_params", + [ + ("/proc/self/environ", {}), + ("/", {"test": "/test/../../api"}), + ("/", {"test": "test/../../api"}), + ("/", {"sql": ";UNION SELECT (a, b"}), + ("/", {"sql": "concat(..."}), + ("/", {"xss": "