From 628a148944afaafd8cdd15075867b362d5ed6982 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 5 Dec 2019 00:28:56 -0800 Subject: [PATCH 01/10] Install requirements of after_dependencies when loading integrations (#29491) * Install requirements of after_dependencies when loading integrations * Fix smartthings test --- homeassistant/components/auth/manifest.json | 9 +++----- homeassistant/components/camera/manifest.json | 7 +----- homeassistant/components/cast/manifest.json | 1 + .../components/google_assistant/manifest.json | 1 + .../components/mobile_app/http_api.py | 13 +++++------ .../components/mobile_app/manifest.json | 1 + .../components/mobile_app/webhook.py | 9 ++++---- .../components/mobile_app/websocket_api.py | 2 +- .../components/onboarding/manifest.json | 9 ++------ .../components/owntracks/__init__.py | 2 +- .../components/owntracks/config_flow.py | 5 +---- .../components/owntracks/manifest.json | 12 +++------- homeassistant/components/proxy/camera.py | 22 +++++++++++-------- homeassistant/components/proxy/manifest.json | 4 +--- .../components/smartthings/manifest.json | 14 ++++-------- .../components/smartthings/smartapp.py | 18 +++------------ homeassistant/setup.py | 4 ++-- script/hassfest/dependencies.py | 13 +++++------ .../smartthings/test_config_flow.py | 9 ++++---- 19 files changed, 57 insertions(+), 98 deletions(-) diff --git a/homeassistant/components/auth/manifest.json b/homeassistant/components/auth/manifest.json index 1b0ab33f381..2f3e724b583 100644 --- a/homeassistant/components/auth/manifest.json +++ b/homeassistant/components/auth/manifest.json @@ -3,10 +3,7 @@ "name": "Auth", "documentation": "https://www.home-assistant.io/integrations/auth", "requirements": [], - "dependencies": [ - "http" - ], - "codeowners": [ - "@home-assistant/core" - ] + "dependencies": ["http"], + "after_dependencies": ["onboarding"], + "codeowners": ["@home-assistant/core"] } diff --git a/homeassistant/components/camera/manifest.json b/homeassistant/components/camera/manifest.json index a3395965e4f..1bd4bb7caeb 100644 --- a/homeassistant/components/camera/manifest.json +++ b/homeassistant/components/camera/manifest.json @@ -3,11 +3,6 @@ "name": "Camera", "documentation": "https://www.home-assistant.io/integrations/camera", "requirements": [], - "dependencies": [ - "http" - ], - "after_dependencies": [ - "stream" - ], + "dependencies": ["http"], "codeowners": [] } diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index b6776a17f7c..8ad6f8fdb8d 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -5,6 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/cast", "requirements": ["pychromecast==4.0.1"], "dependencies": [], + "after_dependencies": ["cloud"], "zeroconf": ["_googlecast._tcp.local."], "codeowners": [] } diff --git a/homeassistant/components/google_assistant/manifest.json b/homeassistant/components/google_assistant/manifest.json index f97977a7400..94dd3b7f079 100644 --- a/homeassistant/components/google_assistant/manifest.json +++ b/homeassistant/components/google_assistant/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/google_assistant", "requirements": [], "dependencies": ["http"], + "after_dependencies": ["camera"], "codeowners": ["@home-assistant/cloud"] } diff --git a/homeassistant/components/mobile_app/http_api.py b/homeassistant/components/mobile_app/http_api.py index 11ca39e8b60..5be2d19789e 100644 --- a/homeassistant/components/mobile_app/http_api.py +++ b/homeassistant/components/mobile_app/http_api.py @@ -37,9 +37,7 @@ class RegistrationsView(HomeAssistantView): webhook_id = generate_secret() - cloud_loaded = "cloud" in hass.config.components - - if cloud_loaded and hass.components.cloud.async_active_subscription(): + if hass.components.cloud.async_active_subscription(): data[ CONF_CLOUDHOOK_URL ] = await hass.components.cloud.async_create_cloudhook(webhook_id) @@ -59,11 +57,10 @@ class RegistrationsView(HomeAssistantView): ) remote_ui_url = None - if cloud_loaded: - try: - remote_ui_url = hass.components.cloud.async_remote_ui_url() - except hass.components.cloud.CloudNotAvailable: - pass + try: + remote_ui_url = hass.components.cloud.async_remote_ui_url() + except hass.components.cloud.CloudNotAvailable: + pass return self.json( { diff --git a/homeassistant/components/mobile_app/manifest.json b/homeassistant/components/mobile_app/manifest.json index 29ee35e002c..230a60fdf25 100644 --- a/homeassistant/components/mobile_app/manifest.json +++ b/homeassistant/components/mobile_app/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/mobile_app", "requirements": ["PyNaCl==1.3.0"], "dependencies": ["http", "webhook"], + "after_dependencies": ["cloud"], "codeowners": ["@robbiet480"] } diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 98687e6658f..46f17b401bc 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -309,10 +309,9 @@ async def handle_webhook( if CONF_CLOUDHOOK_URL in registration: resp[CONF_CLOUDHOOK_URL] = registration[CONF_CLOUDHOOK_URL] - if "cloud" in hass.config.components: - try: - resp[CONF_REMOTE_UI_URL] = hass.components.cloud.async_remote_ui_url() - except hass.components.cloud.CloudNotAvailable: - pass + try: + resp[CONF_REMOTE_UI_URL] = hass.components.cloud.async_remote_ui_url() + except hass.components.cloud.CloudNotAvailable: + pass return webhook_response(resp, registration=registration, headers=headers) diff --git a/homeassistant/components/mobile_app/websocket_api.py b/homeassistant/components/mobile_app/websocket_api.py index bc5305c36fa..a18e5247bfa 100644 --- a/homeassistant/components/mobile_app/websocket_api.py +++ b/homeassistant/components/mobile_app/websocket_api.py @@ -115,7 +115,7 @@ async def websocket_delete_registration( except HomeAssistantError: return error_message(msg["id"], "internal_error", "Error deleting registration") - if CONF_CLOUDHOOK_URL in registration and "cloud" in hass.config.components: + if CONF_CLOUDHOOK_URL in registration: await hass.components.cloud.async_delete_cloudhook(webhook_id) connection.send_message(result_message(msg["id"], "ok")) diff --git a/homeassistant/components/onboarding/manifest.json b/homeassistant/components/onboarding/manifest.json index 2febfc481e0..8e525ff0baa 100644 --- a/homeassistant/components/onboarding/manifest.json +++ b/homeassistant/components/onboarding/manifest.json @@ -3,11 +3,6 @@ "name": "Onboarding", "documentation": "https://www.home-assistant.io/integrations/onboarding", "requirements": [], - "dependencies": [ - "auth", - "http" - ], - "codeowners": [ - "@home-assistant/core" - ] + "dependencies": ["auth", "http", "person"], + "codeowners": ["@home-assistant/core"] } diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index d30e667f368..8556e8a7556 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -118,7 +118,7 @@ async def async_unload_entry(hass, entry): async def async_remove_entry(hass, entry): """Remove an OwnTracks config entry.""" - if not entry.data.get("cloudhook") or "cloud" not in hass.config.components: + if not entry.data.get("cloudhook"): return await hass.components.cloud.async_delete_cloudhook(entry.data[CONF_WEBHOOK_ID]) diff --git a/homeassistant/components/owntracks/config_flow.py b/homeassistant/components/owntracks/config_flow.py index ff4a649e0ce..1a8bb838e18 100644 --- a/homeassistant/components/owntracks/config_flow.py +++ b/homeassistant/components/owntracks/config_flow.py @@ -66,10 +66,7 @@ class OwnTracksFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _get_webhook_id(self): """Generate webhook ID.""" webhook_id = self.hass.components.webhook.async_generate_id() - if ( - "cloud" in self.hass.config.components - and self.hass.components.cloud.async_active_subscription() - ): + if self.hass.components.cloud.async_active_subscription(): webhook_url = await self.hass.components.cloud.async_create_cloudhook( webhook_id ) diff --git a/homeassistant/components/owntracks/manifest.json b/homeassistant/components/owntracks/manifest.json index 529d7990a86..63fdfb94cf7 100644 --- a/homeassistant/components/owntracks/manifest.json +++ b/homeassistant/components/owntracks/manifest.json @@ -3,14 +3,8 @@ "name": "Owntracks", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/owntracks", - "requirements": [ - "PyNaCl==1.3.0" - ], - "dependencies": [ - "webhook" - ], - "after_dependencies": [ - "mqtt" - ], + "requirements": ["PyNaCl==1.3.0"], + "dependencies": ["webhook"], + "after_dependencies": ["mqtt", "cloud"], "codeowners": [] } diff --git a/homeassistant/components/proxy/camera.py b/homeassistant/components/proxy/camera.py index 90487120ffe..893fadfe178 100644 --- a/homeassistant/components/proxy/camera.py +++ b/homeassistant/components/proxy/camera.py @@ -7,7 +7,13 @@ import logging from PIL import Image import voluptuous as vol -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.components.camera import ( + PLATFORM_SCHEMA, + Camera, + async_get_image, + async_get_mjpeg_stream, + async_get_still_stream, +) from homeassistant.const import CONF_ENTITY_ID, CONF_MODE, CONF_NAME from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv @@ -227,7 +233,7 @@ class ProxyCamera(Camera): return self._last_image self._last_image_time = now - image = await self.hass.components.camera.async_get_image(self._proxied_camera) + image = await async_get_image(self.hass, self._proxied_camera) if not image: _LOGGER.error("Error getting original camera image") return self._last_image @@ -247,12 +253,12 @@ class ProxyCamera(Camera): async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from camera images.""" if not self._stream_opts: - return await self.hass.components.camera.async_get_mjpeg_stream( - request, self._proxied_camera + return await async_get_mjpeg_stream( + self.hass, request, self._proxied_camera ) - return await self.hass.components.camera.async_get_still_stream( - request, self._async_stream_image, self.content_type, self.frame_interval + return await async_get_still_stream( + request, self._async_stream_image, self.content_type, self.frame_interval, ) @property @@ -263,9 +269,7 @@ class ProxyCamera(Camera): async def _async_stream_image(self): """Return a still image response from the camera.""" try: - image = await self.hass.components.camera.async_get_image( - self._proxied_camera - ) + image = await async_get_image(self.hass, self._proxied_camera) if not image: return None except HomeAssistantError: diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index 39f7b9064fc..5344ea6fe83 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -2,9 +2,7 @@ "domain": "proxy", "name": "Proxy", "documentation": "https://www.home-assistant.io/integrations/proxy", - "requirements": [ - "pillow==6.2.1" - ], + "requirements": ["pillow==6.2.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/smartthings/manifest.json b/homeassistant/components/smartthings/manifest.json index 8b5bf65afa1..0ab71382fad 100644 --- a/homeassistant/components/smartthings/manifest.json +++ b/homeassistant/components/smartthings/manifest.json @@ -3,14 +3,8 @@ "name": "Smartthings", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/smartthings", - "requirements": [ - "pysmartapp==0.3.2", - "pysmartthings==0.6.9" - ], - "dependencies": [ - "webhook" - ], - "codeowners": [ - "@andrewsayre" - ] + "requirements": ["pysmartapp==0.3.2", "pysmartthings==0.6.9"], + "dependencies": ["webhook"], + "after_dependencies": ["cloud"], + "codeowners": ["@andrewsayre"] } diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index 6acd29397ae..9b67df21491 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -88,10 +88,7 @@ async def validate_installed_app(api, installed_app_id: str): def validate_webhook_requirements(hass: HomeAssistantType) -> bool: """Ensure HASS is setup properly to receive webhooks.""" - if ( - "cloud" in hass.config.components - and hass.components.cloud.async_active_subscription() - ): + if hass.components.cloud.async_active_subscription(): return True if hass.data[DOMAIN][CONF_CLOUDHOOK_URL] is not None: return True @@ -105,11 +102,7 @@ def get_webhook_url(hass: HomeAssistantType) -> str: Return the cloudhook if available, otherwise local webhook. """ cloudhook_url = hass.data[DOMAIN][CONF_CLOUDHOOK_URL] - if ( - "cloud" in hass.config.components - and hass.components.cloud.async_active_subscription() - and cloudhook_url is not None - ): + if hass.components.cloud.async_active_subscription() and cloudhook_url is not None: return cloudhook_url return webhook.async_generate_url(hass, hass.data[DOMAIN][CONF_WEBHOOK_ID]) @@ -229,7 +222,6 @@ async def setup_smartapp_endpoint(hass: HomeAssistantType): cloudhook_url = config.get(CONF_CLOUDHOOK_URL) if ( cloudhook_url is None - and "cloud" in hass.config.components and hass.components.cloud.async_active_subscription() and not hass.config_entries.async_entries(DOMAIN) ): @@ -281,11 +273,7 @@ async def unload_smartapp_endpoint(hass: HomeAssistantType): return # Remove the cloudhook if it was created cloudhook_url = hass.data[DOMAIN][CONF_CLOUDHOOK_URL] - if ( - cloudhook_url - and "cloud" in hass.config.components - and hass.components.cloud.async_is_logged_in() - ): + if cloudhook_url and hass.components.cloud.async_is_logged_in(): await hass.components.cloud.async_delete_cloudhook( hass.data[DOMAIN][CONF_WEBHOOK_ID] ) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 314938feeed..42296a4935d 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -288,8 +288,8 @@ async def async_process_deps_reqs( raise HomeAssistantError("Could not set up all dependencies.") if not hass.config.skip_pip and integration.requirements: - await requirements.async_process_requirements( - hass, integration.domain, integration.requirements + await requirements.async_get_integration_with_requirements( + hass, integration.domain ) processed.add(integration.domain) diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index e9933995715..e47deb76ad5 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -43,15 +43,12 @@ def validate_dependencies(integration: Integration): if referenced: for domain in sorted(referenced): - print( - "Warning: {} references integration {} but it's not a " - "dependency".format(integration.domain, domain) + integration.add_error( + "dependencies", + "Using component {} but it's not in 'dependencies' or 'after_dependencies'".format( + domain + ), ) - # Not enforced yet. - # integration.add_error( - # 'dependencies', - # "Using component {} but it's not a dependency".format(domain) - # ) def validate(integrations: Dict[str, Integration], config): diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index 521f1c6a6a8..82a24f38287 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -7,7 +7,6 @@ from pysmartthings import APIResponseError from homeassistant import data_entry_flow from homeassistant.setup import async_setup_component -from homeassistant.components import cloud from homeassistant.components.smartthings import smartapp from homeassistant.components.smartthings.config_flow import SmartThingsFlowHandler from homeassistant.components.smartthings.const import ( @@ -18,7 +17,7 @@ from homeassistant.components.smartthings.const import ( DOMAIN, ) -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, mock_coro async def test_step_user(hass): @@ -211,9 +210,11 @@ async def test_cloudhook_app_created_then_show_wait_form( await smartapp.unload_smartapp_endpoint(hass) with patch.object( - cloud, "async_active_subscription", return_value=True + hass.components.cloud, "async_active_subscription", return_value=True ), patch.object( - cloud, "async_create_cloudhook", return_value="http://cloud.test" + hass.components.cloud, + "async_create_cloudhook", + return_value=mock_coro("http://cloud.test"), ) as mock_create_cloudhook: await smartapp.setup_smartapp_endpoint(hass) From a466ae02795077ae47a3dfe566ac49b6c87c6505 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 12 Dec 2019 02:24:57 -0700 Subject: [PATCH 02/10] Bump aioambient to 1.0.2 (#29850) --- homeassistant/components/ambient_station/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ambient_station/manifest.json b/homeassistant/components/ambient_station/manifest.json index 8f363ba219f..1e6c06f260a 100644 --- a/homeassistant/components/ambient_station/manifest.json +++ b/homeassistant/components/ambient_station/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ambient_station", "requirements": [ - "aioambient==0.3.2" + "aioambient==1.0.2" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 9e2f31443e5..046ee6fc5fb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -133,7 +133,7 @@ aio_geojson_geonetnz_volcano==0.5 aio_geojson_nsw_rfs_incidents==0.1 # homeassistant.components.ambient_station -aioambient==0.3.2 +aioambient==1.0.2 # homeassistant.components.asuswrt aioasuswrt==1.1.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 036de45e342..700bdf6a3b2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -44,7 +44,7 @@ aio_geojson_geonetnz_volcano==0.5 aio_geojson_nsw_rfs_incidents==0.1 # homeassistant.components.ambient_station -aioambient==0.3.2 +aioambient==1.0.2 # homeassistant.components.asuswrt aioasuswrt==1.1.22 From 29e412a6c45c981b30dfee7a0b34d3233a493f72 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 13 Dec 2019 10:31:53 +0100 Subject: [PATCH 03/10] Fix setup for tank_utility component (#29902) --- homeassistant/components/tank_utility/sensor.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/tank_utility/sensor.py b/homeassistant/components/tank_utility/sensor.py index 3ab4a027b04..4dcc880703c 100644 --- a/homeassistant/components/tank_utility/sensor.py +++ b/homeassistant/components/tank_utility/sensor.py @@ -4,7 +4,7 @@ import datetime import logging import requests -import tank_utility +from tank_utility import auth, device as tank_monitor import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -47,7 +47,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): devices = config.get(CONF_DEVICES) try: - token = tank_utility.auth.get_token(email, password) + token = auth.get_token(email, password) except requests.exceptions.HTTPError as http_error: if ( http_error.response.status_code @@ -111,17 +111,15 @@ class TankUtilitySensor(Entity): data = {} try: - data = tank_utility.device.get_device_data(self._token, self.device) + data = tank_monitor.get_device_data(self._token, self.device) except requests.exceptions.HTTPError as http_error: if ( http_error.response.status_code == requests.codes.unauthorized # pylint: disable=no-member ): _LOGGER.info("Getting new token") - self._token = tank_utility.auth.get_token( - self._email, self._password, force=True - ) - data = tank_utility.device.get_device_data(self._token, self.device) + self._token = auth.get_token(self._email, self._password, force=True) + data = tank_monitor.get_device_data(self._token, self.device) else: raise http_error data.update(data.pop("device", {})) From 9b65d83e28bf27115a83675b3cfdafb0e8d83c29 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 13 Dec 2019 14:08:30 +0100 Subject: [PATCH 04/10] Fix setup error for logbook (#29908) * Fix setup error by moving an import back into the setup function * Revert c741664d4da76611b5bb7502dda61a24dce22c61 * Add homekit as after_dependency to logbook manifest.json --- homeassistant/components/logbook/manifest.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/logbook/manifest.json b/homeassistant/components/logbook/manifest.json index e8e3ad8ac2e..9d5c78dc34d 100644 --- a/homeassistant/components/logbook/manifest.json +++ b/homeassistant/components/logbook/manifest.json @@ -3,9 +3,7 @@ "name": "Logbook", "documentation": "https://www.home-assistant.io/integrations/logbook", "requirements": [], - "dependencies": [ - "frontend", - "recorder" - ], + "dependencies": ["frontend", "http", "recorder"], + "after_dependencies": ["homekit"], "codeowners": [] } From 24c87638e6e428308e0d46275e0bf81a445c3b65 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sat, 14 Dec 2019 07:36:33 +0100 Subject: [PATCH 05/10] Support entity_id: all in lifx.set_state (#29919) --- homeassistant/components/lifx/light.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 1fb614f856f..aa63be04f0d 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -35,7 +35,12 @@ from homeassistant.components.light import ( Light, preprocess_turn_on_alternatives, ) -from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_MODE, + ENTITY_MATCH_ALL, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv import homeassistant.helpers.device_registry as dr @@ -369,6 +374,9 @@ class LIFXManager: async def async_service_to_entities(self, service): """Return the known entities that a service call mentions.""" + if service.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL: + return self.entities.values() + entity_ids = await async_extract_entity_ids(self.hass, service) return [ entity From f1d22db009200bfe8445c6029192f6957590d4f7 Mon Sep 17 00:00:00 2001 From: Justin Bassett Date: Sat, 14 Dec 2019 01:36:12 -0500 Subject: [PATCH 06/10] Fix mobile app device identifiers (#29920) Fix identifiers when updating device registration. --- homeassistant/components/mobile_app/webhook.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 46f17b401bc..c2bc6c94112 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -190,10 +190,7 @@ async def handle_webhook( device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, - identifiers={ - (ATTR_DEVICE_ID, registration[ATTR_DEVICE_ID]), - (CONF_WEBHOOK_ID, registration[CONF_WEBHOOK_ID]), - }, + identifiers={(DOMAIN, registration[ATTR_DEVICE_ID])}, manufacturer=new_registration[ATTR_MANUFACTURER], model=new_registration[ATTR_MODEL], name=new_registration[ATTR_DEVICE_NAME], From 9836e781206a5f1b6c66621c72998c900ccd37f5 Mon Sep 17 00:00:00 2001 From: Chris Mandich Date: Sat, 14 Dec 2019 18:45:29 -0800 Subject: [PATCH 07/10] Fix loading flume integration (#29926) * Fix https://github.com/home-assistant/home-assistant/issues/29853 * Run script.gen_requirements * Update to store Token File in config directory * Update to store Token File in config directory * Update to store Token File in config directory --- homeassistant/components/flume/manifest.json | 4 ++-- homeassistant/components/flume/sensor.py | 6 +++++- requirements_all.txt | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/flume/manifest.json b/homeassistant/components/flume/manifest.json index 800751e80ef..6a9fb7a1fd8 100644 --- a/homeassistant/components/flume/manifest.json +++ b/homeassistant/components/flume/manifest.json @@ -3,9 +3,9 @@ "name": "Flume", "documentation": "https://www.home-assistant.io/integrations/flume/", "requirements": [ - "pyflume==0.2.1" + "pyflume==0.2.4" ], "dependencies": [], "codeowners": ["@ChrisMandich"] } - \ No newline at end of file + diff --git a/homeassistant/components/flume/sensor.py b/homeassistant/components/flume/sensor.py index 5fee408e0dc..e96ce0d96ef 100644 --- a/homeassistant/components/flume/sensor.py +++ b/homeassistant/components/flume/sensor.py @@ -37,11 +37,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): password = config[CONF_PASSWORD] client_id = config[CONF_CLIENT_ID] client_secret = config[CONF_CLIENT_SECRET] + flume_token_file = hass.config.path("FLUME_TOKEN_FILE") time_zone = str(hass.config.time_zone) name = config[CONF_NAME] flume_entity_list = [] - flume_devices = FlumeDeviceList(username, password, client_id, client_secret) + flume_devices = FlumeDeviceList( + username, password, client_id, client_secret, flume_token_file + ) for device in flume_devices.device_list: if device["type"] == FLUME_TYPE_SENSOR: @@ -53,6 +56,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device["id"], time_zone, SCAN_INTERVAL, + flume_token_file, ) flume_entity_list.append(FlumeSensor(flume, f"{name} {device['id']}")) diff --git a/requirements_all.txt b/requirements_all.txt index 046ee6fc5fb..68c772785b3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1231,7 +1231,7 @@ pyflexit==0.3 pyflic-homeassistant==0.4.dev0 # homeassistant.components.flume -pyflume==0.2.1 +pyflume==0.2.4 # homeassistant.components.flunearyou pyflunearyou==1.0.3 From 6d06cec0e0b8c31269e0602b97548fef0cb4f96a Mon Sep 17 00:00:00 2001 From: Andrew Onyshchuk Date: Sun, 15 Dec 2019 19:02:18 -0600 Subject: [PATCH 08/10] Fix support for legacy Z-Wave thermostats (#29955) This brings back support for Z-Wave thermostats of SETPOINT_THERMOSTAT specific device class. Such devices don't have COMMAND_CLASS_THERMOSTAT_MODE and are now handled separately. --- homeassistant/components/zwave/climate.py | 103 ++++++--- .../components/zwave/discovery_schemas.py | 51 +++- tests/components/zwave/test_climate.py | 217 +++++++++++++++++- tests/components/zwave/test_init.py | 42 +++- 4 files changed, 377 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/zwave/climate.py b/homeassistant/components/zwave/climate.py index e5090878328..2b421db70b5 100644 --- a/homeassistant/components/zwave/climate.py +++ b/homeassistant/components/zwave/climate.py @@ -1,11 +1,12 @@ """Support for Z-Wave climate devices.""" # Because we do not compile openzwave on CI import logging - -from typing import Optional +from typing import Optional, Tuple from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, CURRENT_HVAC_COOL, CURRENT_HVAC_FAN, CURRENT_HVAC_HEAT, @@ -14,29 +15,26 @@ from homeassistant.components.climate.const import ( DOMAIN, HVAC_MODE_AUTO, HVAC_MODE_COOL, - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, PRESET_AWAY, PRESET_BOOST, PRESET_NONE, SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, - SUPPORT_PRESET_MODE, - ATTR_TARGET_TEMP_LOW, - ATTR_TARGET_TEMP_HIGH, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect - -from . import ZWaveDeviceEntity +from . import ZWaveDeviceEntity, const _LOGGER = logging.getLogger(__name__) @@ -149,10 +147,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities): def get_device(hass, values, **kwargs): """Create Z-Wave entity device.""" temp_unit = hass.config.units.temperature_unit - return ZWaveClimate(values, temp_unit) + if values.primary.command_class == const.COMMAND_CLASS_THERMOSTAT_SETPOINT: + return ZWaveClimateSingleSetpoint(values, temp_unit) + if values.primary.command_class == const.COMMAND_CLASS_THERMOSTAT_MODE: + return ZWaveClimateMultipleSetpoint(values, temp_unit) + return None -class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): +class ZWaveClimateBase(ZWaveDeviceEntity, ClimateDevice): """Representation of a Z-Wave Climate device.""" def __init__(self, values, temp_unit): @@ -190,18 +192,21 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): self._zxt_120 = 1 self.update_properties() - def _current_mode_setpoints(self): - current_mode = str(self.values.primary.data).lower() - setpoints_names = MODE_SETPOINT_MAPPINGS.get(current_mode, ()) - return tuple(getattr(self.values, name, None) for name in setpoints_names) + def _mode(self) -> None: + """Return thermostat mode Z-Wave value.""" + raise NotImplementedError() + + def _current_mode_setpoints(self) -> Tuple: + """Return a tuple of current setpoint Z-Wave value(s).""" + raise NotImplementedError() @property def supported_features(self): """Return the list of supported features.""" support = SUPPORT_TARGET_TEMPERATURE - if HVAC_MODE_HEAT_COOL in self._hvac_list: + if self._hvac_list and HVAC_MODE_HEAT_COOL in self._hvac_list: support |= SUPPORT_TARGET_TEMPERATURE_RANGE - if PRESET_AWAY in self._preset_list: + if self._preset_list and PRESET_AWAY in self._preset_list: support |= SUPPORT_TARGET_TEMPERATURE_RANGE if self.values.fan_mode: @@ -239,13 +244,13 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): def _update_operation_mode(self): """Update hvac and preset modes.""" - if self.values.primary: + if self._mode(): self._hvac_list = [] self._hvac_mapping = {} self._preset_list = [] self._preset_mapping = {} - mode_list = self.values.primary.data_items + mode_list = self._mode().data_items if mode_list: for mode in mode_list: ha_mode = HVAC_STATE_MAPPINGS.get(str(mode).lower()) @@ -273,7 +278,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): # Presets are supported self._preset_list.append(PRESET_NONE) - current_mode = self.values.primary.data + current_mode = self._mode().data _LOGGER.debug("current_mode=%s", current_mode) _hvac_temp = next( ( @@ -426,7 +431,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): Need to be one of HVAC_MODE_*. """ - if self.values.primary: + if self._mode(): return self._hvac_mode return self._default_hvac_mode @@ -436,7 +441,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): Need to be a subset of HVAC_MODES. """ - if self.values.primary: + if self._mode(): return self._hvac_list return [] @@ -453,7 +458,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): """Return true if aux heater.""" if not self._aux_heat: return None - if self.values.primary.data == AUX_HEAT_ZWAVE_MODE: + if self._mode().data == AUX_HEAT_ZWAVE_MODE: return True return False @@ -463,7 +468,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): Need to be one of PRESET_*. """ - if self.values.primary: + if self._mode(): return self._preset_mode return PRESET_NONE @@ -473,7 +478,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): Need to be a subset of PRESET_MODES. """ - if self.values.primary: + if self._mode(): return self._preset_list return [] @@ -522,11 +527,11 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): def set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" _LOGGER.debug("Set hvac_mode to %s", hvac_mode) - if not self.values.primary: + if not self._mode(): return operation_mode = self._hvac_mapping.get(hvac_mode) _LOGGER.debug("Set operation_mode to %s", operation_mode) - self.values.primary.data = operation_mode + self._mode().data = operation_mode def turn_aux_heat_on(self): """Turn auxillary heater on.""" @@ -534,7 +539,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): return operation_mode = AUX_HEAT_ZWAVE_MODE _LOGGER.debug("Aux heat on. Set operation mode to %s", operation_mode) - self.values.primary.data = operation_mode + self._mode().data = operation_mode def turn_aux_heat_off(self): """Turn auxillary heater off.""" @@ -545,23 +550,23 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): else: operation_mode = self._hvac_mapping.get(HVAC_MODE_OFF) _LOGGER.debug("Aux heat off. Set operation mode to %s", operation_mode) - self.values.primary.data = operation_mode + self._mode().data = operation_mode def set_preset_mode(self, preset_mode): """Set new target preset mode.""" _LOGGER.debug("Set preset_mode to %s", preset_mode) - if not self.values.primary: + if not self._mode(): return if preset_mode == PRESET_NONE: # Activate the current hvac mode self._update_operation_mode() operation_mode = self._hvac_mapping.get(self.hvac_mode) _LOGGER.debug("Set operation_mode to %s", operation_mode) - self.values.primary.data = operation_mode + self._mode().data = operation_mode else: operation_mode = self._preset_mapping.get(preset_mode, preset_mode) _LOGGER.debug("Set operation_mode to %s", operation_mode) - self.values.primary.data = operation_mode + self._mode().data = operation_mode def set_swing_mode(self, swing_mode): """Set new target swing mode.""" @@ -577,3 +582,37 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): if self._fan_action: data[ATTR_FAN_ACTION] = self._fan_action return data + + +class ZWaveClimateSingleSetpoint(ZWaveClimateBase): + """Representation of a single setpoint Z-Wave thermostat device.""" + + def __init__(self, values, temp_unit): + """Initialize the Z-Wave climate device.""" + ZWaveClimateBase.__init__(self, values, temp_unit) + + def _mode(self) -> None: + """Return thermostat mode Z-Wave value.""" + return self.values.mode + + def _current_mode_setpoints(self) -> Tuple: + """Return a tuple of current setpoint Z-Wave value(s).""" + return (self.values.primary,) + + +class ZWaveClimateMultipleSetpoint(ZWaveClimateBase): + """Representation of a multiple setpoint Z-Wave thermostat device.""" + + def __init__(self, values, temp_unit): + """Initialize the Z-Wave climate device.""" + ZWaveClimateBase.__init__(self, values, temp_unit) + + def _mode(self) -> None: + """Return thermostat mode Z-Wave value.""" + return self.values.primary + + def _current_mode_setpoints(self) -> Tuple: + """Return a tuple of current setpoint Z-Wave value(s).""" + current_mode = str(self.values.primary.data).lower() + setpoints_names = MODE_SETPOINT_MAPPINGS.get(current_mode, ()) + return tuple(getattr(self.values, name, None) for name in setpoints_names) diff --git a/homeassistant/components/zwave/discovery_schemas.py b/homeassistant/components/zwave/discovery_schemas.py index 2d6f08169ea..5e4b83d81e1 100644 --- a/homeassistant/components/zwave/discovery_schemas.py +++ b/homeassistant/components/zwave/discovery_schemas.py @@ -48,11 +48,60 @@ DISCOVERY_SCHEMAS = [ ), }, { - const.DISC_COMPONENT: "climate", + const.DISC_COMPONENT: "climate", # thermostat without COMMAND_CLASS_THERMOSTAT_MODE const.DISC_GENERIC_DEVICE_CLASS: [ const.GENERIC_TYPE_THERMOSTAT, const.GENERIC_TYPE_SENSOR_MULTILEVEL, ], + const.DISC_SPECIFIC_DEVICE_CLASS: [ + const.SPECIFIC_TYPE_THERMOSTAT_HEATING, + const.SPECIFIC_TYPE_SETPOINT_THERMOSTAT, + ], + const.DISC_VALUES: dict( + DEFAULT_VALUES_SCHEMA, + **{ + const.DISC_PRIMARY: { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT] + }, + "temperature": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SENSOR_MULTILEVEL], + const.DISC_INDEX: [const.INDEX_SENSOR_MULTILEVEL_TEMPERATURE], + const.DISC_OPTIONAL: True, + }, + "fan_mode": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_FAN_MODE], + const.DISC_OPTIONAL: True, + }, + "operating_state": { + const.DISC_COMMAND_CLASS: [ + const.COMMAND_CLASS_THERMOSTAT_OPERATING_STATE + ], + const.DISC_OPTIONAL: True, + }, + "fan_action": { + const.DISC_COMMAND_CLASS: [ + const.COMMAND_CLASS_THERMOSTAT_FAN_ACTION + ], + const.DISC_OPTIONAL: True, + }, + "mode": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_MODE], + const.DISC_OPTIONAL: True, + }, + }, + ), + }, + { + const.DISC_COMPONENT: "climate", # thermostat with COMMAND_CLASS_THERMOSTAT_MODE + const.DISC_GENERIC_DEVICE_CLASS: [ + const.GENERIC_TYPE_THERMOSTAT, + const.GENERIC_TYPE_SENSOR_MULTILEVEL, + ], + const.DISC_SPECIFIC_DEVICE_CLASS: [ + const.SPECIFIC_TYPE_THERMOSTAT_GENERAL, + const.SPECIFIC_TYPE_THERMOSTAT_GENERAL_V2, + const.SPECIFIC_TYPE_SETBACK_THERMOSTAT, + ], const.DISC_VALUES: dict( DEFAULT_VALUES_SCHEMA, **{ diff --git a/tests/components/zwave/test_climate.py b/tests/components/zwave/test_climate.py index c9fe123af82..98e7bdbcbca 100644 --- a/tests/components/zwave/test_climate.py +++ b/tests/components/zwave/test_climate.py @@ -13,6 +13,7 @@ from homeassistant.components.climate.const import ( PRESET_BOOST, PRESET_ECO, PRESET_NONE, + SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, SUPPORT_SWING_MODE, @@ -21,8 +22,11 @@ from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, ) -from homeassistant.components.zwave import climate -from homeassistant.components.zwave.climate import DEFAULT_HVAC_MODES +from homeassistant.components.zwave import climate, const +from homeassistant.components.zwave.climate import ( + AUX_HEAT_ZWAVE_MODE, + DEFAULT_HVAC_MODES, +) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed @@ -34,6 +38,7 @@ def device(hass, mock_openzwave): node = MockNode() values = MockEntityValues( primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, data=HVAC_MODE_HEAT, data_items=[ HVAC_MODE_OFF, @@ -62,6 +67,7 @@ def device_zxt_120(hass, mock_openzwave): values = MockEntityValues( primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, data=HVAC_MODE_HEAT, data_items=[ HVAC_MODE_OFF, @@ -90,6 +96,7 @@ def device_mapping(hass, mock_openzwave): node = MockNode() values = MockEntityValues( primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, data="Heat", data_items=["Off", "Cool", "Heat", "Full Power", "Auto"], node=node, @@ -112,6 +119,7 @@ def device_unknown(hass, mock_openzwave): node = MockNode() values = MockEntityValues( primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, data="Heat", data_items=["Off", "Cool", "Heat", "heat_cool", "Abcdefg"], node=node, @@ -134,6 +142,7 @@ def device_heat_cool(hass, mock_openzwave): node = MockNode() values = MockEntityValues( primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, data=HVAC_MODE_HEAT, data_items=[ HVAC_MODE_OFF, @@ -162,6 +171,7 @@ def device_heat_cool_range(hass, mock_openzwave): node = MockNode() values = MockEntityValues( primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, data=HVAC_MODE_HEAT_COOL, data_items=[ HVAC_MODE_OFF, @@ -189,6 +199,7 @@ def device_heat_cool_away(hass, mock_openzwave): node = MockNode() values = MockEntityValues( primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, data=HVAC_MODE_HEAT_COOL, data_items=[ HVAC_MODE_OFF, @@ -219,6 +230,7 @@ def device_heat_eco(hass, mock_openzwave): node = MockNode() values = MockEntityValues( primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, data=HVAC_MODE_HEAT, data_items=[HVAC_MODE_OFF, HVAC_MODE_HEAT, "heat econ"], node=node, @@ -235,6 +247,100 @@ def device_heat_eco(hass, mock_openzwave): yield device +@pytest.fixture +def device_aux_heat(hass, mock_openzwave): + """Fixture to provide a precreated climate device. aux heat.""" + node = MockNode() + values = MockEntityValues( + primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, + data=HVAC_MODE_HEAT, + data_items=[HVAC_MODE_OFF, HVAC_MODE_HEAT, "Aux Heat"], + node=node, + ), + setpoint_heating=MockValue(data=2, node=node), + setpoint_eco_heating=MockValue(data=1, node=node), + temperature=MockValue(data=5, node=node, units=None), + fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), + operating_state=MockValue(data="test4", node=node), + fan_action=MockValue(data=7, node=node), + ) + device = climate.get_device(hass, node=node, values=values, node_config={}) + + yield device + + +@pytest.fixture +def device_single_setpoint(hass, mock_openzwave): + """Fixture to provide a precreated climate device. + + SETPOINT_THERMOSTAT device class. + """ + + node = MockNode() + values = MockEntityValues( + primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_SETPOINT, data=1, node=node + ), + mode=None, + temperature=MockValue(data=5, node=node, units=None), + fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), + operating_state=MockValue(data=CURRENT_HVAC_HEAT, node=node), + fan_action=MockValue(data=7, node=node), + ) + device = climate.get_device(hass, node=node, values=values, node_config={}) + + yield device + + +@pytest.fixture +def device_single_setpoint_with_mode(hass, mock_openzwave): + """Fixture to provide a precreated climate device. + + SETPOINT_THERMOSTAT device class with COMMAND_CLASS_THERMOSTAT_MODE command class + """ + + node = MockNode() + values = MockEntityValues( + primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_SETPOINT, data=1, node=node + ), + mode=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, + data=HVAC_MODE_HEAT, + data_items=[HVAC_MODE_OFF, HVAC_MODE_HEAT], + node=node, + ), + temperature=MockValue(data=5, node=node, units=None), + fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), + operating_state=MockValue(data=CURRENT_HVAC_HEAT, node=node), + fan_action=MockValue(data=7, node=node), + ) + device = climate.get_device(hass, node=node, values=values, node_config={}) + + yield device + + +def test_get_device_detects_none(hass, mock_openzwave): + """Test get_device returns None.""" + node = MockNode() + value = MockValue(data=0, node=node) + values = MockEntityValues(primary=value) + + device = climate.get_device(hass, node=node, values=values, node_config={}) + assert device is None + + +def test_get_device_detects_multiple_setpoint_device(device): + """Test get_device returns a Z-Wave multiple setpoint device.""" + assert isinstance(device, climate.ZWaveClimateMultipleSetpoint) + + +def test_get_device_detects_single_setpoint_device(device_single_setpoint): + """Test get_device returns a Z-Wave single setpoint device.""" + assert isinstance(device_single_setpoint, climate.ZWaveClimateSingleSetpoint) + + def test_default_hvac_modes(): """Test wether all hvac modes are included in default_hvac_modes.""" for hvac_mode in HVAC_MODES: @@ -274,6 +380,18 @@ def test_supported_features_preset_mode(device_mapping): ) +def test_supported_features_preset_mode_away(device_heat_cool_away): + """Test supported features flags with swing mode.""" + device = device_heat_cool_away + assert ( + device.supported_features + == SUPPORT_FAN_MODE + + SUPPORT_TARGET_TEMPERATURE + + SUPPORT_TARGET_TEMPERATURE_RANGE + + SUPPORT_PRESET_MODE + ) + + def test_supported_features_swing_mode(device_zxt_120): """Test supported features flags with swing mode.""" device = device_zxt_120 @@ -286,6 +404,27 @@ def test_supported_features_swing_mode(device_zxt_120): ) +def test_supported_features_aux_heat(device_aux_heat): + """Test supported features flags with aux heat.""" + device = device_aux_heat + assert ( + device.supported_features + == SUPPORT_FAN_MODE + SUPPORT_TARGET_TEMPERATURE + SUPPORT_AUX_HEAT + ) + + +def test_supported_features_single_setpoint(device_single_setpoint): + """Test supported features flags for SETPOINT_THERMOSTAT.""" + device = device_single_setpoint + assert device.supported_features == SUPPORT_FAN_MODE + SUPPORT_TARGET_TEMPERATURE + + +def test_supported_features_single_setpoint_with_mode(device_single_setpoint_with_mode): + """Test supported features flags for SETPOINT_THERMOSTAT.""" + device = device_single_setpoint_with_mode + assert device.supported_features == SUPPORT_FAN_MODE + SUPPORT_TARGET_TEMPERATURE + + def test_zxt_120_swing_mode(device_zxt_120): """Test operation of the zxt 120 swing mode.""" device = device_zxt_120 @@ -331,6 +470,22 @@ def test_data_lists(device): assert device.preset_modes == [] +def test_data_lists_single_setpoint(device_single_setpoint): + """Test data lists from zwave value items.""" + device = device_single_setpoint + assert device.fan_modes == [3, 4, 5] + assert device.hvac_modes == [] + assert device.preset_modes == [] + + +def test_data_lists_single_setpoint_with_mode(device_single_setpoint_with_mode): + """Test data lists from zwave value items.""" + device = device_single_setpoint_with_mode + assert device.fan_modes == [3, 4, 5] + assert device.hvac_modes == [HVAC_MODE_OFF, HVAC_MODE_HEAT] + assert device.preset_modes == [] + + def test_data_lists_mapping(device_mapping): """Test data lists from zwave value items.""" device = device_mapping @@ -404,6 +559,14 @@ def test_target_value_set_eco(device_heat_eco): assert device.values.setpoint_eco_heating.data == 0 +def test_target_value_set_single_setpoint(device_single_setpoint): + """Test values changed for climate device.""" + device = device_single_setpoint + assert device.values.primary.data == 1 + device.set_temperature(**{ATTR_TEMPERATURE: 2}) + assert device.values.primary.data == 2 + + def test_operation_value_set(device): """Test values changed for climate device.""" assert device.values.primary.data == HVAC_MODE_HEAT @@ -546,6 +709,15 @@ def test_target_changed_with_mode(device): assert device.target_temperature_high == 10 +def test_target_value_changed_single_setpoint(device_single_setpoint): + """Test values changed for climate device.""" + device = device_single_setpoint + assert device.target_temperature == 1 + device.values.primary.data = 2 + value_changed(device.values.primary) + assert device.target_temperature == 2 + + def test_temperature_value_changed(device): """Test values changed for climate device.""" assert device.current_temperature == 5 @@ -677,3 +849,44 @@ def test_fan_action_value_changed(device): device.values.fan_action.data = 9 value_changed(device.values.fan_action) assert device.device_state_attributes[climate.ATTR_FAN_ACTION] == 9 + + +def test_aux_heat_unsupported_set(device): + """Test aux heat for climate device.""" + device = device + assert device.values.primary.data == HVAC_MODE_HEAT + device.turn_aux_heat_on() + assert device.values.primary.data == HVAC_MODE_HEAT + device.turn_aux_heat_off() + assert device.values.primary.data == HVAC_MODE_HEAT + + +def test_aux_heat_unsupported_value_changed(device): + """Test aux heat for climate device.""" + device = device + assert device.is_aux_heat is None + device.values.primary.data = HVAC_MODE_HEAT + value_changed(device.values.primary) + assert device.is_aux_heat is None + + +def test_aux_heat_set(device_aux_heat): + """Test aux heat for climate device.""" + device = device_aux_heat + assert device.values.primary.data == HVAC_MODE_HEAT + device.turn_aux_heat_on() + assert device.values.primary.data == AUX_HEAT_ZWAVE_MODE + device.turn_aux_heat_off() + assert device.values.primary.data == HVAC_MODE_HEAT + + +def test_aux_heat_value_changed(device_aux_heat): + """Test aux heat for climate device.""" + device = device_aux_heat + assert device.is_aux_heat is False + device.values.primary.data = AUX_HEAT_ZWAVE_MODE + value_changed(device.values.primary) + assert device.is_aux_heat is True + device.values.primary.data = HVAC_MODE_HEAT + value_changed(device.values.primary) + assert device.is_aux_heat is False diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 7038d6b6114..faa1ed2a88c 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -573,7 +573,11 @@ async def test_value_discovery_existing_entity(hass, mock_openzwave): assert len(mock_receivers) == 1 - node = MockNode(node_id=11, generic=const.GENERIC_TYPE_THERMOSTAT) + node = MockNode( + node_id=11, + generic=const.GENERIC_TYPE_THERMOSTAT, + specific=const.SPECIFIC_TYPE_THERMOSTAT_GENERAL_V2, + ) thermostat_mode = MockValue( data="Heat", data_items=["Off", "Heat"], @@ -638,6 +642,42 @@ async def test_value_discovery_existing_entity(hass, mock_openzwave): ) +async def test_value_discovery_legacy_thermostat(hass, mock_openzwave): + """Test discovery of a node. Special case for legacy thermostats.""" + mock_receivers = [] + + def mock_connect(receiver, signal, *args, **kwargs): + if signal == MockNetwork.SIGNAL_VALUE_ADDED: + mock_receivers.append(receiver) + + with patch("pydispatch.dispatcher.connect", new=mock_connect): + await async_setup_component(hass, "zwave", {"zwave": {}}) + await hass.async_block_till_done() + + assert len(mock_receivers) == 1 + + node = MockNode( + node_id=11, + generic=const.GENERIC_TYPE_THERMOSTAT, + specific=const.SPECIFIC_TYPE_SETPOINT_THERMOSTAT, + ) + setpoint_heating = MockValue( + data=22.0, + node=node, + command_class=const.COMMAND_CLASS_THERMOSTAT_SETPOINT, + index=1, + genre=const.GENRE_USER, + ) + + hass.async_add_job(mock_receivers[0], node, setpoint_heating) + await hass.async_block_till_done() + + assert ( + hass.states.get("climate.mock_node_mock_value").attributes["temperature"] + == 22.0 + ) + + async def test_power_schemes(hass, mock_openzwave): """Test power attribute.""" mock_receivers = [] From 9f64656603ec13466fdc5e2d6dfbaebc34f663e9 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 18 Dec 2019 13:06:57 -0700 Subject: [PATCH 09/10] Bump simplisafe-python to 5.3.6 (#30055) --- homeassistant/components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 4115ce455b5..2df49bb5209 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", "requirements": [ - "simplisafe-python==5.3.5" + "simplisafe-python==5.3.6" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 68c772785b3..9c2fb16fa16 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1793,7 +1793,7 @@ shodan==1.20.0 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==5.3.5 +simplisafe-python==5.3.6 # homeassistant.components.sisyphus sisyphus-control==2.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 700bdf6a3b2..a59a9a57d54 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -558,7 +558,7 @@ rxv==0.6.0 samsungctl[websocket]==0.7.1 # homeassistant.components.simplisafe -simplisafe-python==5.3.5 +simplisafe-python==5.3.6 # homeassistant.components.sleepiq sleepyq==0.7 From 952b21faccdbbe15fa28e54dd83273b7578a27d2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 18 Dec 2019 21:24:39 +0100 Subject: [PATCH 10/10] Bumped version to 0.103.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index e312bd4c257..34e6153b966 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 103 -PATCH_VERSION = "0" +PATCH_VERSION = "1" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1)